@irtio/cli 0.5.2 → 0.6.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.
Files changed (37) hide show
  1. package/dist/api.d.ts +52 -0
  2. package/dist/api.js +15 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/{chunk-GBNHBWES.js → chunk-3HQMVCYA.js} +10 -6
  5. package/dist/{chunk-32QTPKVT.js → chunk-DKWG7MGO.js} +1 -1
  6. package/dist/{chunk-KRQUAEN2.js → chunk-OTSFRVJN.js} +12 -6
  7. package/dist/chunk-RNAH5T4W.js +96 -0
  8. package/dist/chunk-RQSJZWQC.js +452 -0
  9. package/dist/chunk-UPHQM6NZ.js +72 -0
  10. package/dist/chunk-ZD4ND6X6.js +31 -0
  11. package/dist/chunk-ZK5JLUD4.js +94 -0
  12. package/dist/credentials.d.ts +61 -0
  13. package/dist/credentials.js +20 -0
  14. package/dist/delete-project-VENS2B44.js +118 -0
  15. package/dist/deploy.d.ts +149 -0
  16. package/dist/{deploy-3SABPL3T.js → deploy.js} +166 -44
  17. package/dist/{dev-QJOGXLKM.js → dev-QM26ONKS.js} +2957 -231
  18. package/dist/index.js +97 -25
  19. package/dist/init.d.ts +1 -1
  20. package/dist/init.js +20 -4
  21. package/dist/{keys-XBORZAPI.js → keys-JHLMEGRA.js} +8 -4
  22. package/dist/leaderboard-SYPSBPS3.js +352 -0
  23. package/dist/{login-3EXB4CGX.js → login-2M73HBZT.js} +12 -5
  24. package/dist/{logs-2EOXLNWF.js → logs-2W7CPZO5.js} +9 -5
  25. package/dist/{migrate-WUO2GBMX.js → migrate-T3DZJREY.js} +12 -8
  26. package/dist/ratings-VG32WFDG.js +297 -0
  27. package/dist/{rollback-GA6UY772.js → rollback-SO74MVZV.js} +9 -5
  28. package/dist/{rooms-B66LQIIF.js → rooms-VI33P4RA.js} +36 -10
  29. package/dist/simulate.d.ts +147 -4
  30. package/dist/simulate.js +680 -53
  31. package/dist/{static-deploy-5TBH4VNA.js → static-deploy-KOWFKWZA.js} +6 -4
  32. package/dist/status-HF3ZEKB7.js +219 -0
  33. package/dist/usage-4G23QXCH.js +213 -0
  34. package/dist/{whoami-CI5D5RCC.js → whoami-KTMTQNHM.js} +8 -4
  35. package/package.json +23 -7
  36. package/dist/chunk-BPE452KF.js +0 -180
  37. package/dist/chunk-TV66QHFP.js +0 -167
@@ -0,0 +1,452 @@
1
+ // ../store/dist/index.js
2
+ import {
3
+ createHash,
4
+ createHmac,
5
+ createPublicKey,
6
+ generateKeyPairSync,
7
+ sign,
8
+ verify
9
+ } from "crypto";
10
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
11
+ import * as path from "path";
12
+ var KV_LIMITS = {
13
+ /** Max UTF-8 bytes in one value. */
14
+ valueBytes: 16 * 1024,
15
+ /** Max UTF-8 bytes in one key. */
16
+ keyBytes: 256,
17
+ /** Max UTF-8 bytes in one player id. */
18
+ playerIdBytes: 256,
19
+ /** Max distinct keys one player may hold within one project. */
20
+ keysPerPlayer: 128,
21
+ /** Max rows one project may hold across all its players. */
22
+ rowsPerProject: 1e6
23
+ };
24
+ var PLAYER_ISSUER_RE = /^[a-z0-9._-]{1,64}$/;
25
+ var KV_ERRORS = {
26
+ badKey: "E_KV_BAD_KEY",
27
+ badPlayer: "E_KV_BAD_PLAYER",
28
+ valueTooLarge: "E_KV_VALUE_TOO_LARGE",
29
+ tooManyKeys: "E_KV_TOO_MANY_KEYS",
30
+ projectFull: "E_KV_PROJECT_FULL",
31
+ forbidden: "E_KV_FORBIDDEN",
32
+ unavailable: "E_KV_UNAVAILABLE"
33
+ };
34
+ var IRT_IDENTITY_ISSUER = "irt";
35
+ var ASSERTION_TYP = "IRTA";
36
+ var ASSERTION_ALG = "EdDSA";
37
+ var ASSERTION_TTL_MS = 5 * 6e4;
38
+ var ASSERTION_CLOCK_SKEW_MS = 6e4;
39
+ var SUB_RE = /^[\x21-\x7e]{1,128}$/;
40
+ function verifyAssertion(keys, token, projectId, now = Date.now()) {
41
+ const malformed = (reason) => ({
42
+ ok: false,
43
+ code: "E_TOKEN_MALFORMED",
44
+ reason
45
+ });
46
+ if (keys.length === 0) {
47
+ return {
48
+ ok: false,
49
+ code: "E_ASSERTION_UNVERIFIABLE",
50
+ reason: "this tenant holds no platform identity key yet"
51
+ };
52
+ }
53
+ const parts = token.split(".");
54
+ if (parts.length !== 3) return malformed("not three dot-separated segments");
55
+ const [headerB64, payloadB64, signatureB64] = parts;
56
+ let header;
57
+ let claims;
58
+ try {
59
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
60
+ claims = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
61
+ } catch {
62
+ return malformed("header or payload is not base64url JSON");
63
+ }
64
+ if (header.typ !== ASSERTION_TYP) return malformed(`typ must be ${ASSERTION_TYP}`);
65
+ if (claims.iss !== IRT_IDENTITY_ISSUER) return malformed(`iss must be ${IRT_IDENTITY_ISSUER}`);
66
+ if (typeof claims.sub !== "string" || !SUB_RE.test(claims.sub)) {
67
+ return malformed("sub is required: 1-128 printable ASCII characters, no spaces");
68
+ }
69
+ if (typeof claims.aud !== "string") return malformed("aud (the project id) is required");
70
+ if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) {
71
+ return malformed("exp (unix seconds) is required");
72
+ }
73
+ if (claims.aud !== projectId) {
74
+ return { ok: false, code: "E_TOKEN_WRONG_PROJECT", reason: "aud is another project" };
75
+ }
76
+ if (header.alg !== ASSERTION_ALG) {
77
+ return { ok: false, code: "E_TOKEN_BAD_ALG", reason: String(header.alg) };
78
+ }
79
+ const signed = Buffer.from(`${headerB64}.${payloadB64}`, "utf8");
80
+ let signature;
81
+ try {
82
+ signature = Buffer.from(signatureB64, "base64url");
83
+ } catch {
84
+ return malformed("signature is not base64url");
85
+ }
86
+ const kid = typeof header.kid === "string" ? header.kid : void 0;
87
+ const candidates = kid !== void 0 ? keys.filter((k) => k.kid === kid) : [];
88
+ const tried = candidates.length > 0 ? candidates : keys;
89
+ let matched;
90
+ for (const key of tried) {
91
+ let ok = false;
92
+ try {
93
+ ok = verify(null, signed, key.publicKeyPem, signature);
94
+ } catch {
95
+ ok = false;
96
+ }
97
+ if (ok) {
98
+ matched = key;
99
+ break;
100
+ }
101
+ }
102
+ if (!matched) return { ok: false, code: "E_TOKEN_INVALID", reason: "signature mismatch" };
103
+ const expMs = claims.exp * 1e3;
104
+ if (expMs + ASSERTION_CLOCK_SKEW_MS <= now) {
105
+ return { ok: false, code: "E_TOKEN_EXPIRED", reason: "exp is in the past" };
106
+ }
107
+ if (typeof claims.iat === "number" && Number.isFinite(claims.iat) && claims.iat * 1e3 - ASSERTION_CLOCK_SKEW_MS > now) {
108
+ return { ok: false, code: "E_TOKEN_EXPIRED", reason: "iat is in the future" };
109
+ }
110
+ return {
111
+ ok: true,
112
+ sub: claims.sub,
113
+ playerId: `${IRT_IDENTITY_ISSUER}:${claims.sub}`,
114
+ kid: matched.kid,
115
+ expMs
116
+ };
117
+ }
118
+ function parseIdentityKeys(value) {
119
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
120
+ const raw = value.keys;
121
+ if (!Array.isArray(raw)) return void 0;
122
+ const out = [];
123
+ for (const entry of raw) {
124
+ if (typeof entry !== "object" || entry === null) continue;
125
+ const k = entry;
126
+ if (typeof k.kid !== "string" || k.kid === "") continue;
127
+ if (typeof k.publicKeyPem !== "string" || !k.publicKeyPem.includes("BEGIN PUBLIC KEY")) {
128
+ continue;
129
+ }
130
+ out.push({ kid: k.kid, publicKeyPem: k.publicKeyPem });
131
+ }
132
+ return out.slice(0, 2);
133
+ }
134
+ var LEADERBOARD_ERRORS = {
135
+ badBoard: "E_LB_BAD_BOARD",
136
+ badScore: "E_LB_BAD_SCORE",
137
+ badPlayer: "E_LB_BAD_PLAYER",
138
+ notInRoom: "E_LB_NOT_IN_ROOM",
139
+ projectFull: "E_LB_PROJECT_FULL",
140
+ unavailable: "E_LB_UNAVAILABLE",
141
+ /** D62-a: a `top` cursor that is not a cursor for the board and direction it arrived on. Its
142
+ * own code because the only thing to do about it is start again from the first page, which no
143
+ * other code in this table implies. */
144
+ badCursor: "E_LB_BAD_CURSOR"
145
+ };
146
+ var RATING_LIMITS = {
147
+ /** Max players named in one report. A report is one rating period and decomposes into pairs,
148
+ * so the work is quadratic in this number; 64 is `MAX_QUEUE_SIZE`, which is the largest party
149
+ * any queue may declare and therefore the largest honest report a matched room can make. */
150
+ maxResults: 64,
151
+ /** Max UTF-8 bytes in a queue name. Matches the queue-name shape below. */
152
+ queueBytes: 32,
153
+ /** Max UTF-8 bytes in a player id. The leaderboard's number, for the same column shape. */
154
+ playerIdBytes: 256,
155
+ /** Max distinct rating rows one project may hold across all its queues. */
156
+ rowsPerProject: 1e6,
157
+ /** `place` is a 1-based finishing position. Ties are equal places, so places need not be dense
158
+ * and need not start at 1 — only be whole, positive and inside a party's plausible size. */
159
+ maxPlace: 64,
160
+ /** The bounds a game-supplied rating (`ratings.set`) must sit inside. Wide enough for any
161
+ * scale a game might use, narrow enough that a NaN or an infinity cannot be stored. */
162
+ minRating: -1e6,
163
+ maxRating: 1e6,
164
+ /** Deviation is a standard error on the same scale, so it is positive and bounded. */
165
+ minDeviation: 1,
166
+ maxDeviation: 1e3
167
+ };
168
+ var RATING_QUEUE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
169
+ var RATING_ERRORS = {
170
+ badQueue: "E_RATING_BAD_QUEUE",
171
+ badResults: "E_RATING_BAD_RESULTS",
172
+ badPlayer: "E_RATING_BAD_PLAYER",
173
+ badRating: "E_RATING_BAD_RATING",
174
+ notInRoom: "E_RATING_NOT_IN_ROOM",
175
+ projectFull: "E_RATING_PROJECT_FULL",
176
+ unavailable: "E_RATING_UNAVAILABLE"
177
+ };
178
+ function ratingQueueProblem(queue) {
179
+ if (typeof queue !== "string" || queue.length === 0) {
180
+ return { code: RATING_ERRORS.badQueue, message: "queue must be a non-empty string" };
181
+ }
182
+ if (!RATING_QUEUE_NAME_RE.test(queue)) {
183
+ return {
184
+ code: RATING_ERRORS.badQueue,
185
+ message: `queue must be 1-32 characters of a-z 0-9 . _ - and start with a letter or digit (got ${JSON.stringify(queue)})`
186
+ };
187
+ }
188
+ return void 0;
189
+ }
190
+ function ratingResultsProblem(results) {
191
+ if (!Array.isArray(results)) {
192
+ return { code: RATING_ERRORS.badResults, message: "results must be an array" };
193
+ }
194
+ if (results.length < 2) {
195
+ return {
196
+ code: RATING_ERRORS.badResults,
197
+ message: "a result needs at least two players; a rating is a comparison"
198
+ };
199
+ }
200
+ if (results.length > RATING_LIMITS.maxResults) {
201
+ return {
202
+ code: RATING_ERRORS.badResults,
203
+ message: `a report may name at most ${RATING_LIMITS.maxResults} players`
204
+ };
205
+ }
206
+ const seen = /* @__PURE__ */ new Set();
207
+ for (const raw of results) {
208
+ if (typeof raw !== "object" || raw === null) {
209
+ return { code: RATING_ERRORS.badResults, message: "each result must be an object" };
210
+ }
211
+ const { playerId, place } = raw;
212
+ const bad = ratingPlayerProblem(playerId);
213
+ if (bad) return bad;
214
+ if (typeof place !== "number" || !Number.isInteger(place)) {
215
+ return {
216
+ code: RATING_ERRORS.badResults,
217
+ message: "place must be a whole number, 1 for first (ties share a place)"
218
+ };
219
+ }
220
+ if (place < 1 || place > RATING_LIMITS.maxPlace) {
221
+ return {
222
+ code: RATING_ERRORS.badResults,
223
+ message: `place must be between 1 and ${RATING_LIMITS.maxPlace}`
224
+ };
225
+ }
226
+ if (seen.has(playerId)) {
227
+ return {
228
+ code: RATING_ERRORS.badResults,
229
+ message: `${JSON.stringify(playerId)} appears twice; a player cannot play themselves`
230
+ };
231
+ }
232
+ seen.add(playerId);
233
+ }
234
+ return void 0;
235
+ }
236
+ function ratingPlayerProblem(playerId) {
237
+ if (typeof playerId !== "string" || playerId.length === 0) {
238
+ return { code: RATING_ERRORS.badPlayer, message: "playerId must be a non-empty string" };
239
+ }
240
+ if (new TextEncoder().encode(playerId).length > RATING_LIMITS.playerIdBytes) {
241
+ return {
242
+ code: RATING_ERRORS.badPlayer,
243
+ message: `playerId must be at most ${RATING_LIMITS.playerIdBytes} bytes`
244
+ };
245
+ }
246
+ return void 0;
247
+ }
248
+ function ratingValueProblem(value) {
249
+ if (typeof value !== "object" || value === null) {
250
+ return { code: RATING_ERRORS.badRating, message: "a rating must be an object" };
251
+ }
252
+ const { rating, deviation } = value;
253
+ if (typeof rating !== "number" || !Number.isFinite(rating)) {
254
+ return { code: RATING_ERRORS.badRating, message: "rating must be a finite number" };
255
+ }
256
+ if (rating < RATING_LIMITS.minRating || rating > RATING_LIMITS.maxRating) {
257
+ return {
258
+ code: RATING_ERRORS.badRating,
259
+ message: `rating must be between ${RATING_LIMITS.minRating} and ${RATING_LIMITS.maxRating}`
260
+ };
261
+ }
262
+ if (deviation !== void 0) {
263
+ if (typeof deviation !== "number" || !Number.isFinite(deviation)) {
264
+ return { code: RATING_ERRORS.badRating, message: "deviation must be a finite number" };
265
+ }
266
+ if (deviation < RATING_LIMITS.minDeviation || deviation > RATING_LIMITS.maxDeviation) {
267
+ return {
268
+ code: RATING_ERRORS.badRating,
269
+ message: `deviation must be between ${RATING_LIMITS.minDeviation} and ${RATING_LIMITS.maxDeviation}`
270
+ };
271
+ }
272
+ }
273
+ return void 0;
274
+ }
275
+ var SAVES_SEGMENT = "/saves/";
276
+ var SAVE_ID_TIME_DIGITS = 17;
277
+ var DEFAULT_SAVE_RETAIN = 10;
278
+ var PRE_MIGRATION_SAVE_ID = "premigrate";
279
+ function mintSaveId(nowMs, rand) {
280
+ const t = Math.max(0, Math.floor(nowMs));
281
+ const suffix = (Math.floor(Math.abs(rand)) & 65535).toString(16).padStart(4, "0");
282
+ return `${String(t).padStart(SAVE_ID_TIME_DIGITS, "0")}-${suffix}`;
283
+ }
284
+ function saveIdCreatedAt(saveId) {
285
+ if (saveId.length !== SAVE_ID_TIME_DIGITS + 5) return void 0;
286
+ if (saveId[SAVE_ID_TIME_DIGITS] !== "-") return void 0;
287
+ const time = saveId.slice(0, SAVE_ID_TIME_DIGITS);
288
+ if (!/^[0-9]+$/.test(time)) return void 0;
289
+ if (!/^[0-9a-f]{4}$/.test(saveId.slice(SAVE_ID_TIME_DIGITS + 1))) return void 0;
290
+ return Number(time);
291
+ }
292
+ function savesPrefix(liveKey) {
293
+ return `${liveKey}${SAVES_SEGMENT}`;
294
+ }
295
+ function saveKey(liveKey, saveId) {
296
+ return `${savesPrefix(liveKey)}${saveId}`;
297
+ }
298
+ function saveIdOf(liveKey, key) {
299
+ const prefix = savesPrefix(liveKey);
300
+ if (!key.startsWith(prefix)) return void 0;
301
+ const rest = key.slice(prefix.length);
302
+ if (rest === "" || rest.includes("/")) return void 0;
303
+ return rest;
304
+ }
305
+ async function listSaves(store, liveKey) {
306
+ const keys = await store.list(savesPrefix(liveKey));
307
+ const out = [];
308
+ for (const key of keys) {
309
+ const saveId = saveIdOf(liveKey, key);
310
+ if (saveId === void 0) continue;
311
+ const createdAt = saveIdCreatedAt(saveId);
312
+ if (createdAt === void 0) continue;
313
+ out.push({ saveId, key, createdAt });
314
+ }
315
+ out.sort((a, b) => a.saveId < b.saveId ? 1 : a.saveId > b.saveId ? -1 : 0);
316
+ return out;
317
+ }
318
+ function selectForPruning(saves, retain) {
319
+ const keep = Number.isFinite(retain) ? Math.max(1, Math.floor(retain)) : 1;
320
+ return saves.length <= keep ? [] : saves.slice(keep);
321
+ }
322
+ async function pruneSaves(store, saves, retain, log) {
323
+ const doomed = selectForPruning(saves, retain);
324
+ let deleted = 0;
325
+ for (const save of doomed) {
326
+ try {
327
+ await store.delete(save.key);
328
+ deleted++;
329
+ } catch (err) {
330
+ log("warn", `save retention: could not delete ${save.key}`, err);
331
+ }
332
+ }
333
+ return deleted;
334
+ }
335
+ var STATIC_LIMITS = {
336
+ /** Bytes per file. Big enough for a wasm build or a texture atlas; a video does not belong. */
337
+ maxFileBytes: 32 * 1024 * 1024,
338
+ /** Files per deploy. */
339
+ maxFiles: 2e3,
340
+ /** Total bytes per deploy. */
341
+ maxTotalBytes: 256 * 1024 * 1024
342
+ };
343
+ function staticPathProblem(path2) {
344
+ if (path2.length === 0 || path2.length > 512) return "path must be 1-512 characters";
345
+ if (path2.startsWith("/")) return "path must be relative (no leading slash)";
346
+ if (path2.includes("\\")) return "path must use forward slashes";
347
+ if (/[\x00-\x1f\x7f]/.test(path2)) return "path must not contain control characters";
348
+ for (const segment of path2.split("/")) {
349
+ if (segment === "" || segment === "." || segment === "..") {
350
+ return "path segments must be non-empty and not . or ..";
351
+ }
352
+ }
353
+ return void 0;
354
+ }
355
+ var DiskStore = class {
356
+ constructor(dir) {
357
+ this.dir = dir;
358
+ }
359
+ dir;
360
+ fileFor(key) {
361
+ if (!/^[A-Za-z0-9_.@/-]+$/.test(key) || key.includes("..")) {
362
+ throw new Error(`invalid object store key ${JSON.stringify(key)}`);
363
+ }
364
+ return path.join(this.dir, `${key}.snap`);
365
+ }
366
+ async put(key, bytes) {
367
+ const file = this.fileFor(key);
368
+ await mkdir(path.dirname(file), { recursive: true });
369
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
370
+ await writeFile(tmp, bytes);
371
+ for (let attempt = 0; ; attempt++) {
372
+ try {
373
+ await rename(tmp, file);
374
+ return;
375
+ } catch (err) {
376
+ const code = err.code;
377
+ if ((code === "EPERM" || code === "EBUSY") && attempt < 10) {
378
+ await new Promise((resolve) => setTimeout(resolve, 5 * (attempt + 1)));
379
+ continue;
380
+ }
381
+ throw err;
382
+ }
383
+ }
384
+ }
385
+ async get(key) {
386
+ try {
387
+ return new Uint8Array(await readFile(this.fileFor(key)));
388
+ } catch (err) {
389
+ if (err.code === "ENOENT") return void 0;
390
+ throw err;
391
+ }
392
+ }
393
+ async delete(key) {
394
+ await rm(this.fileFor(key), { force: true });
395
+ }
396
+ async list(prefix) {
397
+ const out = [];
398
+ const walk = async (dir, rel) => {
399
+ let entries;
400
+ try {
401
+ entries = await readdir(dir, { withFileTypes: true });
402
+ } catch (err) {
403
+ if (err.code === "ENOENT") return;
404
+ throw err;
405
+ }
406
+ for (const e of entries) {
407
+ const r = rel ? `${rel}/${e.name}` : e.name;
408
+ if (e.isDirectory()) await walk(path.join(dir, e.name), r);
409
+ else if (e.name.endsWith(".snap")) out.push(r.slice(0, -".snap".length));
410
+ }
411
+ };
412
+ await walk(this.dir, "");
413
+ return out.filter((k) => k.startsWith(prefix)).sort();
414
+ }
415
+ async sizes(prefix) {
416
+ const keys = await this.list(prefix);
417
+ const out = [];
418
+ for (const key of keys) {
419
+ try {
420
+ const st = await stat(this.fileFor(key));
421
+ out.push({ key, size: st.size, lastModified: Math.floor(st.mtimeMs) });
422
+ } catch (err) {
423
+ if (err.code === "ENOENT") continue;
424
+ throw err;
425
+ }
426
+ }
427
+ return out;
428
+ }
429
+ };
430
+
431
+ export {
432
+ PLAYER_ISSUER_RE,
433
+ KV_ERRORS,
434
+ IRT_IDENTITY_ISSUER,
435
+ verifyAssertion,
436
+ parseIdentityKeys,
437
+ LEADERBOARD_ERRORS,
438
+ RATING_ERRORS,
439
+ ratingQueueProblem,
440
+ ratingResultsProblem,
441
+ ratingPlayerProblem,
442
+ ratingValueProblem,
443
+ DEFAULT_SAVE_RETAIN,
444
+ PRE_MIGRATION_SAVE_ID,
445
+ mintSaveId,
446
+ saveKey,
447
+ listSaves,
448
+ pruneSaves,
449
+ STATIC_LIMITS,
450
+ staticPathProblem,
451
+ DiskStore
452
+ };
@@ -0,0 +1,72 @@
1
+ // src/credentials.ts
2
+ import { existsSync } from "fs";
3
+ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ function credentialsPath() {
7
+ if (process.env.IRT_CREDENTIALS_FILE) return process.env.IRT_CREDENTIALS_FILE;
8
+ if (process.platform === "win32") {
9
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
10
+ return path.join(appData, "irtio", "credentials.json");
11
+ }
12
+ return path.join(os.homedir(), ".config", "irtio", "credentials.json");
13
+ }
14
+ async function readCredentials() {
15
+ const file = credentialsPath();
16
+ if (!existsSync(file)) return {};
17
+ try {
18
+ const raw = await readFile(file, "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
21
+ return parsed;
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+ async function readCredential(controlUrl) {
27
+ const all = await readCredentials();
28
+ return all[controlUrl];
29
+ }
30
+ async function writeCredential(controlUrl, credential) {
31
+ const file = credentialsPath();
32
+ await mkdir(path.dirname(file), { recursive: true });
33
+ const all = await readCredentials();
34
+ all[controlUrl] = credential;
35
+ await writeFile(file, `${JSON.stringify(all, null, 2)}
36
+ `, { mode: 384 });
37
+ await chmod(file, 384).catch(() => {
38
+ });
39
+ return file;
40
+ }
41
+ async function deleteCredential(controlUrl) {
42
+ const file = credentialsPath();
43
+ if (!existsSync(file)) return;
44
+ const all = await readCredentials();
45
+ delete all[controlUrl];
46
+ await writeFile(file, `${JSON.stringify(all, null, 2)}
47
+ `, { mode: 384 });
48
+ }
49
+ var DEFAULT_CONTROL_URL = "https://irt.io";
50
+ function resolveControlUrl(flag) {
51
+ return flag ?? process.env.IRT_CONTROL_URL ?? DEFAULT_CONTROL_URL;
52
+ }
53
+ async function resolveControlUrlForUser(flag) {
54
+ const explicit = flag ?? process.env.IRT_CONTROL_URL;
55
+ if (explicit !== void 0) return explicit;
56
+ const all = await readCredentials();
57
+ const now = Date.now();
58
+ const live = Object.entries(all).filter(([, cred]) => new Date(cred.expiresAt).getTime() > now).map(([url]) => url);
59
+ const urls = live.length > 0 ? live : Object.keys(all);
60
+ return urls.length === 1 ? urls[0] : DEFAULT_CONTROL_URL;
61
+ }
62
+
63
+ export {
64
+ credentialsPath,
65
+ readCredentials,
66
+ readCredential,
67
+ writeCredential,
68
+ deleteCredential,
69
+ DEFAULT_CONTROL_URL,
70
+ resolveControlUrl,
71
+ resolveControlUrlForUser
72
+ };
@@ -0,0 +1,31 @@
1
+ import {
2
+ ApiClientError
3
+ } from "./chunk-RNAH5T4W.js";
4
+
5
+ // src/help.ts
6
+ function helpRequested(args) {
7
+ return args.some((arg) => arg === "--help" || arg === "-h");
8
+ }
9
+ var HelpRequested = class extends Error {
10
+ constructor(usage) {
11
+ super(usage);
12
+ this.usage = usage;
13
+ }
14
+ usage;
15
+ name = "HelpRequested";
16
+ };
17
+ function helpFor(usage) {
18
+ return new HelpRequested(usage);
19
+ }
20
+ function trailerFor(err, hints = []) {
21
+ if (err instanceof ApiClientError && err.hint !== void 0 && err.hint !== "") return err.hint;
22
+ const grounded = hints.filter((h) => h !== "");
23
+ return grounded.length > 0 ? grounded.join("\n") : void 0;
24
+ }
25
+
26
+ export {
27
+ helpRequested,
28
+ HelpRequested,
29
+ helpFor,
30
+ trailerFor
31
+ };
@@ -0,0 +1,94 @@
1
+ // src/profile.ts
2
+ import { diffProfiles } from "@irtio/protocol";
3
+ import pc from "picocolors";
4
+ var PROFILE_TOP_DEFAULT = 12;
5
+ function rateOf(bytes, seconds) {
6
+ const v = seconds > 0 ? bytes / seconds : bytes;
7
+ const unit = seconds > 0 ? "/s" : "";
8
+ if (v >= 1e6) return `${(v / 1e6).toFixed(1)} MB${unit}`;
9
+ if (v >= 1e3) return `${(v / 1e3).toFixed(1)} kB${unit}`;
10
+ return `${Math.round(v)} B${unit}`;
11
+ }
12
+ function pct(part, whole) {
13
+ if (whole <= 0) return "";
14
+ const p = 100 * part / whole;
15
+ return p >= 10 ? `${Math.round(p)}%` : `${p.toFixed(1)}%`;
16
+ }
17
+ function formatProfileTable(window, options = {}) {
18
+ const top = options.top ?? PROFILE_TOP_DEFAULT;
19
+ const seconds = options.seconds ?? 0;
20
+ const per = options.per && options.per > 1 ? options.per : 1;
21
+ const dim = options.color === false ? (s) => s : pc.dim;
22
+ const rows = [...window.rows].sort((a, b) => b.out + b.in - (a.out + a.in)).filter((r) => r.out !== 0 || r.in !== 0).slice(0, top);
23
+ const cells = rows.map((r) => ({
24
+ kind: r.kind,
25
+ key: r.key,
26
+ out: rateOf(r.out / per, seconds),
27
+ in: rateOf(r.in / per, seconds),
28
+ share: pct(r.out, window.bytesOut)
29
+ }));
30
+ const totalOut = rateOf(window.bytesOut / per, seconds);
31
+ const totalIn = rateOf(window.bytesIn / per, seconds);
32
+ const w = {
33
+ kind: Math.max(4, ...cells.map((c) => c.kind.length)),
34
+ key: Math.max(3, ...cells.map((c) => c.key.length)),
35
+ out: Math.max(3, totalOut.length, ...cells.map((c) => c.out.length)),
36
+ in: Math.max(2, totalIn.length, ...cells.map((c) => c.in.length))
37
+ };
38
+ const lines = [];
39
+ lines.push(
40
+ dim(
41
+ ` ${"KIND".padEnd(w.kind)} ${"KEY".padEnd(w.key)} ${"OUT".padStart(w.out)} ${"IN".padStart(w.in)} SHARE`
42
+ )
43
+ );
44
+ if (cells.length === 0) lines.push(dim(" (nothing moved in this window)"));
45
+ for (const c of cells) {
46
+ lines.push(
47
+ ` ${c.kind.padEnd(w.kind)} ${c.key.padEnd(w.key)} ${c.out.padStart(w.out)} ${c.in.padStart(w.in)} ${c.share.padStart(5)}`
48
+ );
49
+ }
50
+ lines.push(
51
+ ` ${"total".padEnd(w.kind)} ${"".padEnd(w.key)} ${totalOut.padStart(w.out)} ${totalIn.padStart(w.in)}`
52
+ );
53
+ if (options.against) {
54
+ lines.push(
55
+ dim(
56
+ ` ${options.against.label.padEnd(w.kind)} ${"".padEnd(w.key)} ${rateOf(options.against.out / per, seconds).padStart(w.out)} ${rateOf(options.against.in / per, seconds).padStart(w.in)}`
57
+ )
58
+ );
59
+ }
60
+ return lines;
61
+ }
62
+ function profileTick(readings, previous, options = {}) {
63
+ const lines = [];
64
+ const history = /* @__PURE__ */ new Map();
65
+ const dim = options.color === false ? (s) => s : pc.dim;
66
+ for (const reading of readings) {
67
+ history.set(reading.id, reading);
68
+ if (!reading.profile) continue;
69
+ const before = previous.get(reading.id);
70
+ if (!before?.profile) continue;
71
+ const seconds = Math.max(0, reading.at - before.at) / 1e3;
72
+ if (seconds <= 0) continue;
73
+ const window = diffProfiles(before.profile, reading.profile);
74
+ lines.push(`room ${reading.id} ${dim(`\xB7 ${seconds.toFixed(1)} s window`)}`);
75
+ lines.push(
76
+ ...formatProfileTable(window, {
77
+ ...options,
78
+ seconds,
79
+ against: {
80
+ out: reading.egressBytes - before.egressBytes,
81
+ in: reading.ingressBytes - before.ingressBytes,
82
+ label: "socket"
83
+ }
84
+ })
85
+ );
86
+ lines.push("");
87
+ }
88
+ return { lines, history };
89
+ }
90
+
91
+ export {
92
+ formatProfileTable,
93
+ profileTick
94
+ };