@irtio/cli 0.6.0 → 0.7.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/dist/api-keys-UTLYMZYN.js +222 -0
- package/dist/api.js +1 -1
- package/dist/{chunk-3HQMVCYA.js → chunk-BQBOBFBO.js} +4 -4
- package/dist/{chunk-RNAH5T4W.js → chunk-IDF46P7R.js} +2 -0
- package/dist/{chunk-DKWG7MGO.js → chunk-JL235KIE.js} +1 -1
- package/dist/{chunk-ZD4ND6X6.js → chunk-OCVALOGK.js} +1 -1
- package/dist/{chunk-RQSJZWQC.js → chunk-WFMRNGO5.js} +134 -105
- package/dist/{delete-project-VENS2B44.js → delete-project-MXUYNGAO.js} +2 -2
- package/dist/deploy.d.ts +3 -1
- package/dist/deploy.js +163 -5
- package/dist/{dev-QM26ONKS.js → dev-AUZ4OLA3.js} +139 -10
- package/dist/index.js +39 -15
- package/dist/init.js +2 -2
- package/dist/{keys-JHLMEGRA.js → keys-NXRIBJZP.js} +3 -3
- package/dist/{leaderboard-SYPSBPS3.js → leaderboard-ZBJBKETM.js} +65 -11
- package/dist/{login-2M73HBZT.js → login-3RVN5PPN.js} +2 -2
- package/dist/{logs-2W7CPZO5.js → logs-AT7G6YRH.js} +3 -3
- package/dist/{migrate-T3DZJREY.js → migrate-FMXTRVUV.js} +3 -3
- package/dist/{ratings-VG32WFDG.js → ratings-XBLX2MUW.js} +3 -3
- package/dist/{rollback-SO74MVZV.js → rollback-LI4TDLQA.js} +3 -3
- package/dist/{rooms-VI33P4RA.js → rooms-52Q5KBUS.js} +179 -20
- package/dist/simulate.d.ts +115 -10
- package/dist/simulate.js +270 -36
- package/dist/{static-deploy-KOWFKWZA.js → static-deploy-7UCYINJB.js} +5 -5
- package/dist/{status-HF3ZEKB7.js → status-JZGKH2P6.js} +3 -3
- package/dist/{usage-4G23QXCH.js → usage-7S447INI.js} +3 -3
- package/dist/{whoami-KTMTQNHM.js → whoami-UFSWPWK6.js} +2 -2
- package/package.json +7 -7
package/dist/deploy.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
logEnsured,
|
|
3
3
|
runStaticDeploy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BQBOBFBO.js";
|
|
5
5
|
import {
|
|
6
6
|
clientImportsRoom
|
|
7
7
|
} from "./chunk-TQU6345E.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-WFMRNGO5.js";
|
|
9
9
|
import {
|
|
10
10
|
ensureProject,
|
|
11
11
|
readProjectConfig
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JL235KIE.js";
|
|
13
13
|
import {
|
|
14
14
|
bundleRoom
|
|
15
15
|
} from "./chunk-OTSFRVJN.js";
|
|
@@ -17,12 +17,12 @@ import {
|
|
|
17
17
|
HelpRequested,
|
|
18
18
|
helpFor,
|
|
19
19
|
helpRequested
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-OCVALOGK.js";
|
|
21
21
|
import {
|
|
22
22
|
ApiClientError,
|
|
23
23
|
createApiClient,
|
|
24
24
|
isLoginRequired
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-IDF46P7R.js";
|
|
26
26
|
import {
|
|
27
27
|
resolveControlUrlForUser
|
|
28
28
|
} from "./chunk-UPHQM6NZ.js";
|
|
@@ -37,6 +37,7 @@ import { DEFAULT_ROOM_TYPE, ROOM_TYPE_RE } from "@irtio/protocol";
|
|
|
37
37
|
import { diffSchemas, schemaFromCanonical } from "@irtio/schema";
|
|
38
38
|
import pc from "picocolors";
|
|
39
39
|
var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
|
|
40
|
+
var SCHEMA_CANDIDATES = ["irtio/schema.ts", "irtio/schema.js"];
|
|
40
41
|
var MIGRATIONS_DIR = "irtio/migrations";
|
|
41
42
|
var ROOM_TYPES_DIR = "irtio/rooms";
|
|
42
43
|
var USAGE = `usage: irtio deploy [options]
|
|
@@ -145,12 +146,20 @@ function findRoomEntry(cwd, room) {
|
|
|
145
146
|
}
|
|
146
147
|
return void 0;
|
|
147
148
|
}
|
|
149
|
+
function findSchemaEntry(cwd) {
|
|
150
|
+
for (const candidate of SCHEMA_CANDIDATES) {
|
|
151
|
+
const file = path.resolve(cwd, candidate);
|
|
152
|
+
if (existsSync(file)) return file;
|
|
153
|
+
}
|
|
154
|
+
return void 0;
|
|
155
|
+
}
|
|
148
156
|
function resolveEntry(cwd, room) {
|
|
149
157
|
const entry = findRoomEntry(cwd, room);
|
|
150
158
|
if (entry !== void 0) return entry;
|
|
151
159
|
throw new Error(
|
|
152
160
|
`irtio deploy: no room file found in ${cwd}
|
|
153
161
|
looked for: ${ROOM_CANDIDATES.join(", ")}
|
|
162
|
+
or ${SCHEMA_CANDIDATES.join(" / ")}, to deploy a schema with no room code
|
|
154
163
|
pass --room <file> if it lives elsewhere`
|
|
155
164
|
);
|
|
156
165
|
}
|
|
@@ -235,12 +244,160 @@ async function findMigrationFile(cwd, version) {
|
|
|
235
244
|
return match ? path.join(dir, match) : void 0;
|
|
236
245
|
}
|
|
237
246
|
var DEPLOY_BUILD_DIR = path.join(".irtio", "deploy");
|
|
247
|
+
function isSchemaOnly(cwd, room) {
|
|
248
|
+
if (room !== void 0) return false;
|
|
249
|
+
if (findRoomEntry(cwd, void 0) !== void 0) return false;
|
|
250
|
+
if (existsSync(path.resolve(cwd, ROOM_TYPES_DIR))) return false;
|
|
251
|
+
return findSchemaEntry(cwd) !== void 0;
|
|
252
|
+
}
|
|
253
|
+
async function loadDeployedSchema(entry, outDir, irtioPackages) {
|
|
254
|
+
const bundled = await bundleRoom({
|
|
255
|
+
entry,
|
|
256
|
+
outDir,
|
|
257
|
+
verify: false,
|
|
258
|
+
...irtioPackages !== void 0 ? { irtioPackages } : {}
|
|
259
|
+
});
|
|
260
|
+
const mod = await import(pathToFileURL(bundled.file).href);
|
|
261
|
+
const schema = mod.schema ?? mod.default;
|
|
262
|
+
if (!schema || typeof schema.canonical !== "string" || typeof schema.hash !== "string") {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`irtio deploy: ${path.basename(entry)} does not export a schema
|
|
265
|
+
it must export the value defineSchema(...) returned, as \`schema\` or as the default`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return schema;
|
|
269
|
+
}
|
|
270
|
+
async function runSchemaOnlyDeploy(options, cwd, log, ask, controlUrl, config) {
|
|
271
|
+
const entry = findSchemaEntry(cwd);
|
|
272
|
+
const outDir = path.join(cwd, DEPLOY_BUILD_DIR);
|
|
273
|
+
await mkdir(outDir, { recursive: true });
|
|
274
|
+
const schema = await loadDeployedSchema(entry, outDir, options.irtioPackages);
|
|
275
|
+
log(pc.green(`bundled ${path.relative(cwd, entry)} (schema ${schema.hash.slice(0, 12)})`));
|
|
276
|
+
const client = options.client ?? await createApiClient(controlUrl);
|
|
277
|
+
const projectId = options.project ?? config.project ?? schema.project;
|
|
278
|
+
if (projectId === void 0) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'irtio deploy: no project id \u2014 pass --project, add "project" to irtio.json, or set it in defineSchema(...)'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const ensured = await ensureProject(client, projectId, {
|
|
284
|
+
cwd,
|
|
285
|
+
config,
|
|
286
|
+
command: "irtio deploy",
|
|
287
|
+
...options.name !== void 0 ? { name: options.name } : {}
|
|
288
|
+
});
|
|
289
|
+
logEnsured(log, projectId, ensured);
|
|
290
|
+
const deployments = await client.get(`/v1/projects/${projectId}/deployments`);
|
|
291
|
+
const newestOf = (list) => list.reduce(
|
|
292
|
+
(best, d) => best === void 0 || d.version > best.version ? d : best,
|
|
293
|
+
void 0
|
|
294
|
+
);
|
|
295
|
+
const previous = newestOf(deployments.filter((d) => d.rolledBackAt == null));
|
|
296
|
+
const newest = newestOf(deployments);
|
|
297
|
+
const nextVersion = (newest?.version ?? 0) + 1;
|
|
298
|
+
let allowBreaking = options.allowBreaking ?? false;
|
|
299
|
+
let migrationFile;
|
|
300
|
+
if (previous !== void 0) {
|
|
301
|
+
const changes = diffSchemas(schemaFromCanonical(previous.schemaJson), schema);
|
|
302
|
+
const breaking = changes.filter((c) => c.kind === "breaking");
|
|
303
|
+
const additive = changes.filter((c) => c.kind === "additive");
|
|
304
|
+
if (breaking.length > 0) {
|
|
305
|
+
if (!allowBreaking) {
|
|
306
|
+
const hint = "run: irtio migrate create <name>";
|
|
307
|
+
printBreaking(log, breaking, hint);
|
|
308
|
+
throw new DeployRefusedError({ changes: breaking, hint });
|
|
309
|
+
}
|
|
310
|
+
migrationFile = await findMigrationFile(cwd, nextVersion);
|
|
311
|
+
if (migrationFile === void 0) {
|
|
312
|
+
const hint = `--allow-breaking needs a migration for v${nextVersion}. Run: irtio migrate create <name>`;
|
|
313
|
+
printBreaking(log, breaking, hint);
|
|
314
|
+
throw new DeployRefusedError({ changes: breaking, hint });
|
|
315
|
+
}
|
|
316
|
+
log(pc.yellow(`${breaking.length} breaking change(s), proceeding with --allow-breaking:`));
|
|
317
|
+
for (const change of breaking) log(` ${change.message}`);
|
|
318
|
+
} else {
|
|
319
|
+
allowBreaking = false;
|
|
320
|
+
}
|
|
321
|
+
if (additive.length > 0) {
|
|
322
|
+
log(pc.cyan(`${additive.length} additive change(s):`));
|
|
323
|
+
for (const change of additive) log(` ${change.message}`);
|
|
324
|
+
}
|
|
325
|
+
if (changes.length === 0) log(pc.dim("no schema changes"));
|
|
326
|
+
} else {
|
|
327
|
+
log(pc.dim("no previous deployment, so this is the first deploy for this project"));
|
|
328
|
+
}
|
|
329
|
+
let migrationKey;
|
|
330
|
+
if (migrationFile !== void 0) {
|
|
331
|
+
const migrationBundle = await bundleRoom({
|
|
332
|
+
entry: migrationFile,
|
|
333
|
+
outDir,
|
|
334
|
+
verify: false,
|
|
335
|
+
...options.irtioPackages !== void 0 ? { irtioPackages: options.irtioPackages } : {}
|
|
336
|
+
});
|
|
337
|
+
const uploaded = await client.postBytes(
|
|
338
|
+
`/v1/projects/${projectId}/artifacts`,
|
|
339
|
+
new Uint8Array(await readFile(migrationBundle.file))
|
|
340
|
+
);
|
|
341
|
+
migrationKey = uploaded.key;
|
|
342
|
+
log(pc.green(`bundled and uploaded migration ${path.basename(migrationFile)}`));
|
|
343
|
+
}
|
|
344
|
+
let origin;
|
|
345
|
+
if (previous === void 0) {
|
|
346
|
+
const answer = (await ask("production origin (localhost is always allowed) \u2014 leave blank to skip: ")).trim();
|
|
347
|
+
if (answer.length > 0) origin = answer;
|
|
348
|
+
}
|
|
349
|
+
let created;
|
|
350
|
+
try {
|
|
351
|
+
created = await client.post(`/v1/projects/${projectId}/deployments`, {
|
|
352
|
+
// No `bundleHash` and no `bundleKey`: that absence IS the schema-only deploy, and it is
|
|
353
|
+
// what makes control write a row `isRelayOnly` reads as relay-only.
|
|
354
|
+
schemaJson: schema.canonical,
|
|
355
|
+
schemaHash: schema.hash,
|
|
356
|
+
...migrationKey !== void 0 ? { migrationKey } : {},
|
|
357
|
+
...allowBreaking ? { allowBreaking: true } : {},
|
|
358
|
+
...origin !== void 0 ? { origin } : {}
|
|
359
|
+
});
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (err instanceof ApiClientError && err.code === "E_BREAKING_SCHEMA" && err.changes) {
|
|
362
|
+
const changes = err.changes;
|
|
363
|
+
const hint = err.hint ?? "run: irtio migrate create <name>";
|
|
364
|
+
printBreaking(log, changes, hint);
|
|
365
|
+
throw new DeployRefusedError({ changes, hint });
|
|
366
|
+
}
|
|
367
|
+
if (isLoginRequired(err)) throw err;
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
await rm(outDir, { recursive: true, force: true }).catch(() => {
|
|
371
|
+
});
|
|
372
|
+
log(pc.green(`deployed v${created.version}`));
|
|
373
|
+
log(pc.dim("schema-only deploy: this project runs as a relay tenant"));
|
|
374
|
+
log(pc.dim(`reachable at ${created.url}`));
|
|
375
|
+
const result = {
|
|
376
|
+
project: projectId,
|
|
377
|
+
version: created.version,
|
|
378
|
+
url: created.url,
|
|
379
|
+
draining: created.draining,
|
|
380
|
+
...created.applied !== void 0 ? { applied: created.applied } : {}
|
|
381
|
+
};
|
|
382
|
+
if (config.static === void 0 || options.noStatic === true) return result;
|
|
383
|
+
const site = await runStaticDeploy({
|
|
384
|
+
cwd,
|
|
385
|
+
...options.config !== void 0 ? { config: options.config } : {},
|
|
386
|
+
...options.controlUrl !== void 0 ? { controlUrl: options.controlUrl } : {},
|
|
387
|
+
...options.client !== void 0 ? { client: options.client } : {},
|
|
388
|
+
log
|
|
389
|
+
});
|
|
390
|
+
return { ...result, site };
|
|
391
|
+
}
|
|
238
392
|
async function runDeploy(options = {}) {
|
|
239
393
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
240
394
|
const log = options.log ?? ((line) => console.log(line));
|
|
241
395
|
const ask = options.ask ?? defaultAsk;
|
|
242
396
|
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
243
397
|
const config = await readProjectConfig(cwd, "irtio deploy", options.config);
|
|
398
|
+
if (isSchemaOnly(cwd, options.room)) {
|
|
399
|
+
return runSchemaOnlyDeploy(options, cwd, log, ask, controlUrl, config);
|
|
400
|
+
}
|
|
244
401
|
const roomTypes = findRoomTypes(cwd, options.room);
|
|
245
402
|
const entry = roomTypes.default;
|
|
246
403
|
await warnIfClientImportsRoom(cwd, entry, config, log);
|
|
@@ -562,6 +719,7 @@ export {
|
|
|
562
719
|
deploy,
|
|
563
720
|
findRoomEntry,
|
|
564
721
|
findRoomTypes,
|
|
722
|
+
findSchemaEntry,
|
|
565
723
|
parseDeployArgs,
|
|
566
724
|
runDeploy
|
|
567
725
|
};
|
|
@@ -23,10 +23,10 @@ import {
|
|
|
23
23
|
ratingValueProblem,
|
|
24
24
|
saveKey,
|
|
25
25
|
verifyAssertion
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-WFMRNGO5.js";
|
|
27
27
|
import {
|
|
28
28
|
readProjectConfig
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-JL235KIE.js";
|
|
30
30
|
import {
|
|
31
31
|
BundleError,
|
|
32
32
|
bundleRoom
|
|
@@ -35,8 +35,8 @@ import {
|
|
|
35
35
|
HelpRequested,
|
|
36
36
|
helpFor,
|
|
37
37
|
helpRequested
|
|
38
|
-
} from "./chunk-
|
|
39
|
-
import "./chunk-
|
|
38
|
+
} from "./chunk-OCVALOGK.js";
|
|
39
|
+
import "./chunk-IDF46P7R.js";
|
|
40
40
|
import "./chunk-UPHQM6NZ.js";
|
|
41
41
|
|
|
42
42
|
// src/dev.ts
|
|
@@ -187,9 +187,11 @@ import {
|
|
|
187
187
|
isMailboxAlarm,
|
|
188
188
|
isOutboxAlarm,
|
|
189
189
|
isRelayHash8,
|
|
190
|
+
isTypedMsg,
|
|
190
191
|
isVoiceMsg,
|
|
191
192
|
relaySchema as relaySchema2,
|
|
192
193
|
schemaPayload,
|
|
194
|
+
typedMsgFromClientOk,
|
|
193
195
|
withBuiltins
|
|
194
196
|
} from "@irtio/protocol";
|
|
195
197
|
import { inspectState } from "@irtio/runtime";
|
|
@@ -402,10 +404,12 @@ function relayForward(room, peer, type, payload) {
|
|
|
402
404
|
}
|
|
403
405
|
let target;
|
|
404
406
|
let body;
|
|
407
|
+
let typed;
|
|
405
408
|
try {
|
|
406
409
|
const msg = decodeMsg(payload);
|
|
407
410
|
target = msg.target;
|
|
408
411
|
body = msg.payload;
|
|
412
|
+
typed = msg.typed;
|
|
409
413
|
} catch {
|
|
410
414
|
return { ok: false, reason: "malformed MSG" };
|
|
411
415
|
}
|
|
@@ -415,7 +419,11 @@ function relayForward(room, peer, type, payload) {
|
|
|
415
419
|
if (target.kind === "voice") return { ok: true, dropped: true };
|
|
416
420
|
const out = encodeFrame(
|
|
417
421
|
FrameType.MSG,
|
|
418
|
-
encodeMsg({
|
|
422
|
+
encodeMsg({
|
|
423
|
+
target: { kind: "client", clientId: peer.clientId },
|
|
424
|
+
payload: body,
|
|
425
|
+
...typed ? { typed } : {}
|
|
426
|
+
})
|
|
419
427
|
);
|
|
420
428
|
for (const other of room.peers()) {
|
|
421
429
|
if (other === peer || other.state !== "joined") continue;
|
|
@@ -3008,9 +3016,29 @@ var SupervisorImpl = class {
|
|
|
3008
3016
|
*
|
|
3009
3017
|
* They go into a ring under the reserved id `__tenant` (D38), so the whole existing pipeline
|
|
3010
3018
|
* carries them with no new route and no new frame.
|
|
3019
|
+
*
|
|
3020
|
+
* bug #53 (D67-g, which recorded the gap rather than closing it): on a **sharded** tenant every
|
|
3021
|
+
* ring line is tagged `[shard n]`. Every shard of a project ships its `__tenant` ring under the
|
|
3022
|
+
* same project and the same reserved room id, `log_events` has no shard column, and control
|
|
3023
|
+
* drops the frame's shard before the insert, so without the tag `irtio logs` shows two shards'
|
|
3024
|
+
* boot lines interleaved by time and calls them one restart. The tag is written here, at the
|
|
3025
|
+
* source, because this process is the only thing that knows which shard it is without being
|
|
3026
|
+
* told, and one string in the message beats a schema change on an append-only table.
|
|
3027
|
+
*
|
|
3028
|
+
* It is the mirror of `roomLog` below, and for the same reason: a ring line carries whatever its
|
|
3029
|
+
* stream's identity does not. A room's ring is keyed by the room, so its lines stay bare and the
|
|
3030
|
+
* console gets the `[room]` prefix; the tenant ring is keyed by `__tenant` alone, so its lines
|
|
3031
|
+
* carry the shard and the console stays bare — each shard is its own VM with its own console
|
|
3032
|
+
* file, which already says which shard is talking.
|
|
3033
|
+
*
|
|
3034
|
+
* A single-shard tenant (every tenant that exists today) takes the same bare `args` it always
|
|
3035
|
+
* did, byte for byte: `single-shard-pin.test.ts` holds that.
|
|
3011
3036
|
*/
|
|
3012
3037
|
log(level, ...args) {
|
|
3013
|
-
this.tenantLogs.push(
|
|
3038
|
+
this.tenantLogs.push(
|
|
3039
|
+
level,
|
|
3040
|
+
this.isSharded() ? [`[shard ${this.config.shard ?? 0}]`, ...args] : args
|
|
3041
|
+
);
|
|
3014
3042
|
this.emit(level, args);
|
|
3015
3043
|
}
|
|
3016
3044
|
/** The console/config sink, with no ring behind it. */
|
|
@@ -3047,9 +3075,20 @@ var SupervisorImpl = class {
|
|
|
3047
3075
|
* shape, so a dev tenant's existing snapshots stay readable.
|
|
3048
3076
|
*/
|
|
3049
3077
|
/**
|
|
3050
|
-
* The
|
|
3051
|
-
*
|
|
3052
|
-
*
|
|
3078
|
+
* The admin API the host agent polls from outside the VM (week 5). It exists only when
|
|
3079
|
+
* `adminToken` is configured.
|
|
3080
|
+
*
|
|
3081
|
+
* Its original rule was that every route is a projection of `SupervisorAdmin` and the agent must
|
|
3082
|
+
* not be able to *change* anything about a tenant it is shipping telemetry for. That rule now
|
|
3083
|
+
* has five exceptions, each added deliberately and each documented at its own `case`:
|
|
3084
|
+
* `/admin/resume` (D31, the post-restore wall clock), `/admin/deploy` (the in-place deploy),
|
|
3085
|
+
* `/admin/caps` (D48), `/admin/identity-keys` (D53), `/admin/bus/deliver` (D67-e) and
|
|
3086
|
+
* `/admin/rooms/evict` (M6 lane A, D68-d). The surviving half of the rule is the part that
|
|
3087
|
+
* matters and it is unchanged: every one of them is a **push from control**, arriving over the
|
|
3088
|
+
* fleet socket and relayed by the agent with the derived admin token. None of them is something
|
|
3089
|
+
* a guest, a tenant or anything else that can reach this port may originate.
|
|
3090
|
+
*
|
|
3091
|
+
* `/admin/rooms/evict` is the one that disconnects players, so it is the one to read carefully.
|
|
3053
3092
|
*/
|
|
3054
3093
|
serveAdmin(req, res) {
|
|
3055
3094
|
const token = this.config.adminToken;
|
|
@@ -3317,6 +3356,55 @@ var SupervisorImpl = class {
|
|
|
3317
3356
|
});
|
|
3318
3357
|
return true;
|
|
3319
3358
|
}
|
|
3359
|
+
// ---- M6 lane A: rooms API ----
|
|
3360
|
+
// D68-d: the FIFTH write on this surface, and the only one that disconnects players. A
|
|
3361
|
+
// `DELETE ?force=true` on the rooms API reached control, control authorized it, and this is
|
|
3362
|
+
// control telling the guest to let the room go before the objects are removed underneath it.
|
|
3363
|
+
//
|
|
3364
|
+
// Same `adminToken` gate as every other route here, which is the whole access story: the
|
|
3365
|
+
// token is derived per project from the agent's own fleet token and never leaves the box, so
|
|
3366
|
+
// a tenant cannot forge it, and the frame that triggers this is `ControlToAgent` — a
|
|
3367
|
+
// direction a guest cannot speak on the fleet socket at all.
|
|
3368
|
+
//
|
|
3369
|
+
// Answers `{ outcome }`, never an error for "not here": a room this tenant is not holding is
|
|
3370
|
+
// `absent`, and control deletes the family either way.
|
|
3371
|
+
case "/admin/rooms/evict": {
|
|
3372
|
+
if (req.method !== "POST") {
|
|
3373
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
3374
|
+
res.end('{"error":"POST only"}');
|
|
3375
|
+
return true;
|
|
3376
|
+
}
|
|
3377
|
+
let body = "";
|
|
3378
|
+
req.on("data", (d) => {
|
|
3379
|
+
body += d.toString();
|
|
3380
|
+
});
|
|
3381
|
+
req.on("end", () => {
|
|
3382
|
+
void (async () => {
|
|
3383
|
+
let parsed;
|
|
3384
|
+
try {
|
|
3385
|
+
parsed = JSON.parse(body || "{}");
|
|
3386
|
+
} catch {
|
|
3387
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3388
|
+
res.end('{"error":"body is not JSON"}');
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (typeof parsed.roomId !== "string" || parsed.roomId.length === 0) {
|
|
3392
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3393
|
+
res.end('{"error":"roomId must be a non-empty string"}');
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
try {
|
|
3397
|
+
json({ outcome: await this.evictRoom(parsed.roomId) });
|
|
3398
|
+
} catch (err) {
|
|
3399
|
+
this.log("error", "admin room evict failed", err);
|
|
3400
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
3401
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
3402
|
+
}
|
|
3403
|
+
})();
|
|
3404
|
+
});
|
|
3405
|
+
return true;
|
|
3406
|
+
}
|
|
3407
|
+
// ---- end M6 lane A ----
|
|
3320
3408
|
case "/admin/caps": {
|
|
3321
3409
|
if (req.method === "GET") {
|
|
3322
3410
|
const state = this.caps.current();
|
|
@@ -4403,7 +4491,7 @@ var SupervisorImpl = class {
|
|
|
4403
4491
|
message: `${call.playerId} is not a client of room ${room.id}; leaderboard submits are only accepted for players currently in the room`
|
|
4404
4492
|
};
|
|
4405
4493
|
}
|
|
4406
|
-
await this.requireLeaderboard().submit(call.board, call.playerId, call.score);
|
|
4494
|
+
await this.requireLeaderboard().submit(call.board, call.playerId, call.score, call.bucket);
|
|
4407
4495
|
return { ok: true };
|
|
4408
4496
|
}
|
|
4409
4497
|
case "ratingReport": {
|
|
@@ -5106,6 +5194,43 @@ var SupervisorImpl = class {
|
|
|
5106
5194
|
});
|
|
5107
5195
|
this.roomLog(room, "info", `room closed: ${message}`);
|
|
5108
5196
|
}
|
|
5197
|
+
// ---- M6 lane A: rooms API ----
|
|
5198
|
+
/**
|
|
5199
|
+
* M6 lane A (D68-d): stop one room and remove its stored keys, because control is about to
|
|
5200
|
+
* delete the rest of its family.
|
|
5201
|
+
*
|
|
5202
|
+
* Answers `'absent'` when this tenant is not holding the room, which is not a failure and is
|
|
5203
|
+
* most of the real cases: the room went to sleep, or this tenant restarted, and control's row
|
|
5204
|
+
* was a poll behind. Control deletes the family either way.
|
|
5205
|
+
*
|
|
5206
|
+
* **The race this exists to close, and why the `await` is the fix.** `closeRoom` already deletes
|
|
5207
|
+
* the live key and the alarm sidecar, and refuses to run twice. What it cannot do on its own is
|
|
5208
|
+
* stop a hibernation that is *already in flight*: by then `hibernateRoom` has read the worker's
|
|
5209
|
+
* snapshot and is inside `putSnapshotWithRetry`, so its `store.put` lands after `closeRoom`'s
|
|
5210
|
+
* `store.delete` and after control's `deleteRoomFamily` — and the room is resurrected as a live
|
|
5211
|
+
* key nothing will ever delete again, because its row is gone, so the reaper cannot see it and
|
|
5212
|
+
* `irtio rooms` cannot list it. Awaiting `room.transition` first removes that window: every path
|
|
5213
|
+
* that can write a snapshot (`hibernateRoom`, `hibernateRelay`, `migrateRoom`, `wakeRoom`)
|
|
5214
|
+
* publishes itself there and settles before the deletes run, and no NEW one can start, because
|
|
5215
|
+
* they all early-return on a state that is not `running` and `closeRoom` has set `closed`.
|
|
5216
|
+
*
|
|
5217
|
+
* The two deletes are awaited, unlike `closeRoom`'s own fire-and-forget pair, for the same
|
|
5218
|
+
* reason: control lists the bucket the instant this answers.
|
|
5219
|
+
*
|
|
5220
|
+
* `flushAll` (tenant idle) needs no guard of its own: `closeRoom` removes the record from the
|
|
5221
|
+
* registry and the flush walks the registry.
|
|
5222
|
+
*/
|
|
5223
|
+
async evictRoom(roomId) {
|
|
5224
|
+
const room = this.registry.get(roomId);
|
|
5225
|
+
if (room === void 0) return "absent";
|
|
5226
|
+
await room.transition.catch(() => void 0);
|
|
5227
|
+
this.closeRoom(room, "E_ROOM_DELETED", `room ${roomId} was deleted by an operator`);
|
|
5228
|
+
const key = this.storeKey(room);
|
|
5229
|
+
await this.store.delete(key).catch(() => void 0);
|
|
5230
|
+
await this.store.delete(alarmsKey(key)).catch(() => void 0);
|
|
5231
|
+
return "evicted";
|
|
5232
|
+
}
|
|
5233
|
+
// ---- end M6 lane A ----
|
|
5109
5234
|
// -------------------------------------------------------------------------
|
|
5110
5235
|
// Joining
|
|
5111
5236
|
// -------------------------------------------------------------------------
|
|
@@ -5351,6 +5476,9 @@ var SupervisorImpl = class {
|
|
|
5351
5476
|
this.handleVoiceFrame(room, session, payload.subarray(1));
|
|
5352
5477
|
return;
|
|
5353
5478
|
}
|
|
5479
|
+
if (type === FrameType3.MSG && isTypedMsg(payload) && !typedMsgFromClientOk(payload)) {
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5354
5482
|
if (room.relay) {
|
|
5355
5483
|
this.relayFrame(room, session, type, payload);
|
|
5356
5484
|
return;
|
|
@@ -5735,6 +5863,7 @@ var SupervisorImpl = class {
|
|
|
5735
5863
|
room.metrics.ticks = stats.ticks;
|
|
5736
5864
|
room.metrics.maxTickMs = stats.maxTickMs;
|
|
5737
5865
|
room.metrics.overruns = stats.overruns;
|
|
5866
|
+
room.metrics.messagesDropped = stats.messagesDropped;
|
|
5738
5867
|
if (msg.heap !== void 0) {
|
|
5739
5868
|
this.recordHeap(room, msg.heap.usedBytes, msg.heap.limitBytes);
|
|
5740
5869
|
}
|
package/dist/index.js
CHANGED
|
@@ -99,9 +99,21 @@ commands:
|
|
|
99
99
|
[retire] [--url <control>] retire finishes a rotation
|
|
100
100
|
keys jwt-mint --secret <s> --sub <player> sign a test token locally with the documented
|
|
101
101
|
[--room <id>] [--role <r>] [--ttl <sec>] recipe \u2014 the secret never leaves this machine
|
|
102
|
+
rooms [--active|--idle] list a project's rooms, with the retention
|
|
103
|
+
[--idle-longer-than <d>] window in force and when each will be deleted
|
|
104
|
+
rooms get <room> one room in full: type, size class, how many
|
|
105
|
+
saves it holds, how big its state is, deadline
|
|
106
|
+
rooms set <room> --retention <d|forever|clear> override the retention window for THIS room
|
|
102
107
|
rooms saves <room> list a room's save generations, newest first
|
|
103
108
|
rooms restore <room> --save <id> bring a room back from a save \u2014 DISCARDS its
|
|
104
109
|
current state; stops the tenant if it is live
|
|
110
|
+
rooms delete <room> [--force] delete a room's stored state. Refused while it
|
|
111
|
+
is awake unless --force, which disconnects its
|
|
112
|
+
clients first
|
|
113
|
+
api-keys mint --scopes <list> [--label <t>] create a scoped API key for this project and
|
|
114
|
+
print it once. Scopes: rooms:read, rooms:write
|
|
115
|
+
api-keys list this project's keys, revoked ones included
|
|
116
|
+
api-keys revoke <id> stop a key working; the row stays as a record
|
|
105
117
|
|
|
106
118
|
every command takes --help (or -h) and prints its own usage, which is the authority: the lines
|
|
107
119
|
above are a map, and the command's own help is generated next to the code that parses it.
|
|
@@ -112,7 +124,8 @@ default location (same as setting IRT_CREDENTIALS_FILE). Sign in once with
|
|
|
112
124
|
commands \u2014 the pattern for agents and sandboxes that cannot reach the home directory.
|
|
113
125
|
|
|
114
126
|
every command that reads the project file takes -c/--config <file> to read a different one
|
|
115
|
-
(dev, deploy, deploy --static, migrate create, rollback, logs, rooms, status, keys).
|
|
127
|
+
(dev, deploy, deploy --static, migrate create, rollback, logs, rooms, status, keys, api-keys).
|
|
128
|
+
Default irtio.json;
|
|
116
129
|
the named file must exist. That is the test.irtio.json / prod.irtio.json pattern.
|
|
117
130
|
`;
|
|
118
131
|
switch (command) {
|
|
@@ -122,7 +135,7 @@ switch (command) {
|
|
|
122
135
|
break;
|
|
123
136
|
}
|
|
124
137
|
case "dev": {
|
|
125
|
-
const { dev } = await import("./dev-
|
|
138
|
+
const { dev } = await import("./dev-AUZ4OLA3.js");
|
|
126
139
|
await dev(args);
|
|
127
140
|
break;
|
|
128
141
|
}
|
|
@@ -132,18 +145,18 @@ switch (command) {
|
|
|
132
145
|
break;
|
|
133
146
|
}
|
|
134
147
|
case "login": {
|
|
135
|
-
const { login } = await import("./login-
|
|
148
|
+
const { login } = await import("./login-3RVN5PPN.js");
|
|
136
149
|
await login(args);
|
|
137
150
|
break;
|
|
138
151
|
}
|
|
139
152
|
case "whoami": {
|
|
140
|
-
const { whoami } = await import("./whoami-
|
|
153
|
+
const { whoami } = await import("./whoami-UFSWPWK6.js");
|
|
141
154
|
await whoami(args);
|
|
142
155
|
break;
|
|
143
156
|
}
|
|
144
157
|
case "deploy": {
|
|
145
158
|
if (args.includes("--static")) {
|
|
146
|
-
const { staticDeploy } = await import("./static-deploy-
|
|
159
|
+
const { staticDeploy } = await import("./static-deploy-7UCYINJB.js");
|
|
147
160
|
await staticDeploy(args);
|
|
148
161
|
break;
|
|
149
162
|
}
|
|
@@ -152,52 +165,63 @@ switch (command) {
|
|
|
152
165
|
break;
|
|
153
166
|
}
|
|
154
167
|
case "migrate": {
|
|
155
|
-
const { migrate } = await import("./migrate-
|
|
168
|
+
const { migrate } = await import("./migrate-FMXTRVUV.js");
|
|
156
169
|
await migrate(args);
|
|
157
170
|
break;
|
|
158
171
|
}
|
|
159
172
|
case "rollback": {
|
|
160
|
-
const { rollback } = await import("./rollback-
|
|
173
|
+
const { rollback } = await import("./rollback-LI4TDLQA.js");
|
|
161
174
|
await rollback(args);
|
|
162
175
|
break;
|
|
163
176
|
}
|
|
164
177
|
case "keys": {
|
|
165
|
-
const { keys } = await import("./keys-
|
|
178
|
+
const { keys } = await import("./keys-NXRIBJZP.js");
|
|
166
179
|
await keys(args);
|
|
167
180
|
break;
|
|
168
181
|
}
|
|
182
|
+
// ---- M6 lane A: rooms API ----
|
|
183
|
+
// Deliberately not folded into `keys` above: that one manages the HMAC secret a game signs its
|
|
184
|
+
// own player tokens with, and this one manages a credential a developer's backend authenticates
|
|
185
|
+
// to irtio with. Two ideas, two names, so "rotate my signing secret" is never one typo away
|
|
186
|
+
// from "revoke my cron job's key".
|
|
187
|
+
case "api-keys": {
|
|
188
|
+
const { apiKeys } = await import("./api-keys-UTLYMZYN.js");
|
|
189
|
+
await apiKeys(args);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// ---- end M6 lane A ----
|
|
169
193
|
case "logs": {
|
|
170
|
-
const { logs } = await import("./logs-
|
|
194
|
+
const { logs } = await import("./logs-AT7G6YRH.js");
|
|
171
195
|
await logs(args);
|
|
172
196
|
break;
|
|
173
197
|
}
|
|
174
198
|
case "rooms": {
|
|
175
|
-
const { rooms } = await import("./rooms-
|
|
199
|
+
const { rooms } = await import("./rooms-52Q5KBUS.js");
|
|
176
200
|
await rooms(args);
|
|
177
201
|
break;
|
|
178
202
|
}
|
|
179
203
|
case "status": {
|
|
180
|
-
const { status } = await import("./status-
|
|
204
|
+
const { status } = await import("./status-JZGKH2P6.js");
|
|
181
205
|
await status(args);
|
|
182
206
|
break;
|
|
183
207
|
}
|
|
184
208
|
case "delete-project": {
|
|
185
|
-
const { deleteProject } = await import("./delete-project-
|
|
209
|
+
const { deleteProject } = await import("./delete-project-MXUYNGAO.js");
|
|
186
210
|
await deleteProject(args);
|
|
187
211
|
break;
|
|
188
212
|
}
|
|
189
213
|
case "usage": {
|
|
190
|
-
const { usage } = await import("./usage-
|
|
214
|
+
const { usage } = await import("./usage-7S447INI.js");
|
|
191
215
|
await usage(args);
|
|
192
216
|
break;
|
|
193
217
|
}
|
|
194
218
|
case "ratings": {
|
|
195
|
-
const { ratings } = await import("./ratings-
|
|
219
|
+
const { ratings } = await import("./ratings-XBLX2MUW.js");
|
|
196
220
|
await ratings(args);
|
|
197
221
|
break;
|
|
198
222
|
}
|
|
199
223
|
case "leaderboard": {
|
|
200
|
-
const { leaderboard } = await import("./leaderboard-
|
|
224
|
+
const { leaderboard } = await import("./leaderboard-ZBJBKETM.js");
|
|
201
225
|
await leaderboard(args);
|
|
202
226
|
break;
|
|
203
227
|
}
|
package/dist/init.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readProjectConfig
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JL235KIE.js";
|
|
4
4
|
import {
|
|
5
5
|
helpRequested
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-OCVALOGK.js";
|
|
7
7
|
import {
|
|
8
8
|
createApiClient,
|
|
9
9
|
isLoginRequired
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IDF46P7R.js";
|
|
11
11
|
import {
|
|
12
12
|
resolveControlUrlForUser
|
|
13
13
|
} from "./chunk-UPHQM6NZ.js";
|