@irtio/cli 0.5.2 → 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.d.ts +52 -0
- package/dist/api.js +15 -0
- package/dist/bundle.js +1 -1
- package/dist/{chunk-GBNHBWES.js → chunk-BQBOBFBO.js} +10 -6
- package/dist/chunk-IDF46P7R.js +98 -0
- package/dist/{chunk-32QTPKVT.js → chunk-JL235KIE.js} +1 -1
- package/dist/chunk-OCVALOGK.js +31 -0
- package/dist/{chunk-KRQUAEN2.js → chunk-OTSFRVJN.js} +12 -6
- package/dist/chunk-UPHQM6NZ.js +72 -0
- package/dist/chunk-WFMRNGO5.js +481 -0
- package/dist/chunk-ZK5JLUD4.js +94 -0
- package/dist/credentials.d.ts +61 -0
- package/dist/credentials.js +20 -0
- package/dist/delete-project-MXUYNGAO.js +118 -0
- package/dist/deploy.d.ts +151 -0
- package/dist/{deploy-3SABPL3T.js → deploy.js} +324 -44
- package/dist/{dev-QJOGXLKM.js → dev-AUZ4OLA3.js} +3066 -211
- package/dist/index.js +121 -25
- package/dist/init.d.ts +1 -1
- package/dist/init.js +20 -4
- package/dist/{keys-XBORZAPI.js → keys-NXRIBJZP.js} +8 -4
- package/dist/leaderboard-ZBJBKETM.js +406 -0
- package/dist/{login-3EXB4CGX.js → login-3RVN5PPN.js} +12 -5
- package/dist/{logs-2EOXLNWF.js → logs-AT7G6YRH.js} +9 -5
- package/dist/{migrate-WUO2GBMX.js → migrate-FMXTRVUV.js} +12 -8
- package/dist/ratings-XBLX2MUW.js +297 -0
- package/dist/{rollback-GA6UY772.js → rollback-LI4TDLQA.js} +9 -5
- package/dist/rooms-52Q5KBUS.js +411 -0
- package/dist/simulate.d.ts +254 -6
- package/dist/simulate.js +925 -64
- package/dist/{static-deploy-5TBH4VNA.js → static-deploy-7UCYINJB.js} +6 -4
- package/dist/status-JZGKH2P6.js +219 -0
- package/dist/usage-7S447INI.js +213 -0
- package/dist/{whoami-CI5D5RCC.js → whoami-UFSWPWK6.js} +8 -4
- package/package.json +23 -7
- package/dist/chunk-BPE452KF.js +0 -180
- package/dist/chunk-TV66QHFP.js +0 -167
- package/dist/rooms-B66LQIIF.js +0 -226
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// ../store/src/static.ts
|
|
2
|
+
var STATIC_LIMITS = {
|
|
3
|
+
/** Bytes per file. Big enough for a wasm build or a texture atlas; a video does not belong. */
|
|
4
|
+
maxFileBytes: 32 * 1024 * 1024,
|
|
5
|
+
/** Files per deploy. */
|
|
6
|
+
maxFiles: 2e3,
|
|
7
|
+
/** Total bytes per deploy. */
|
|
8
|
+
maxTotalBytes: 256 * 1024 * 1024
|
|
9
|
+
};
|
|
10
|
+
function staticPathProblem(path2) {
|
|
11
|
+
if (path2.length === 0 || path2.length > 512) return "path must be 1-512 characters";
|
|
12
|
+
if (path2.startsWith("/")) return "path must be relative (no leading slash)";
|
|
13
|
+
if (path2.includes("\\")) return "path must use forward slashes";
|
|
14
|
+
if (/[\x00-\x1f\x7f]/.test(path2)) return "path must not contain control characters";
|
|
15
|
+
for (const segment of path2.split("/")) {
|
|
16
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
17
|
+
return "path segments must be non-empty and not . or ..";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ../store/src/disk.ts
|
|
24
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
|
|
25
|
+
import * as path from "path";
|
|
26
|
+
var DiskStore = class {
|
|
27
|
+
constructor(dir) {
|
|
28
|
+
this.dir = dir;
|
|
29
|
+
}
|
|
30
|
+
dir;
|
|
31
|
+
fileFor(key) {
|
|
32
|
+
if (!/^[A-Za-z0-9_.@/-]+$/.test(key) || key.includes("..")) {
|
|
33
|
+
throw new Error(`invalid object store key ${JSON.stringify(key)}`);
|
|
34
|
+
}
|
|
35
|
+
return path.join(this.dir, `${key}.snap`);
|
|
36
|
+
}
|
|
37
|
+
async put(key, bytes) {
|
|
38
|
+
const file = this.fileFor(key);
|
|
39
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
40
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
41
|
+
await writeFile(tmp, bytes);
|
|
42
|
+
for (let attempt = 0; ; attempt++) {
|
|
43
|
+
try {
|
|
44
|
+
await rename(tmp, file);
|
|
45
|
+
return;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const code = err.code;
|
|
48
|
+
if ((code === "EPERM" || code === "EBUSY") && attempt < 10) {
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 5 * (attempt + 1)));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async get(key) {
|
|
57
|
+
try {
|
|
58
|
+
return new Uint8Array(await readFile(this.fileFor(key)));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code === "ENOENT") return void 0;
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async delete(key) {
|
|
65
|
+
await rm(this.fileFor(key), { force: true });
|
|
66
|
+
}
|
|
67
|
+
async list(prefix) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const walk = async (dir, rel) => {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.code === "ENOENT") return;
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
79
|
+
if (e.isDirectory()) await walk(path.join(dir, e.name), r);
|
|
80
|
+
else if (e.name.endsWith(".snap")) out.push(r.slice(0, -".snap".length));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
await walk(this.dir, "");
|
|
84
|
+
return out.filter((k) => k.startsWith(prefix)).sort();
|
|
85
|
+
}
|
|
86
|
+
async sizes(prefix) {
|
|
87
|
+
const keys = await this.list(prefix);
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
try {
|
|
91
|
+
const st = await stat(this.fileFor(key));
|
|
92
|
+
out.push({ key, size: st.size, lastModified: Math.floor(st.mtimeMs) });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err.code === "ENOENT") continue;
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ../store/src/kv.ts
|
|
103
|
+
var KV_LIMITS = {
|
|
104
|
+
/** Max UTF-8 bytes in one value. */
|
|
105
|
+
valueBytes: 16 * 1024,
|
|
106
|
+
/** Max UTF-8 bytes in one key. */
|
|
107
|
+
keyBytes: 256,
|
|
108
|
+
/** Max UTF-8 bytes in one player id. */
|
|
109
|
+
playerIdBytes: 256,
|
|
110
|
+
/** Max distinct keys one player may hold within one project. */
|
|
111
|
+
keysPerPlayer: 128,
|
|
112
|
+
/** Max rows one project may hold across all its players. */
|
|
113
|
+
rowsPerProject: 1e6
|
|
114
|
+
};
|
|
115
|
+
var PLAYER_ISSUER_RE = /^[a-z0-9._-]{1,64}$/;
|
|
116
|
+
var KV_ERRORS = {
|
|
117
|
+
badKey: "E_KV_BAD_KEY",
|
|
118
|
+
badPlayer: "E_KV_BAD_PLAYER",
|
|
119
|
+
valueTooLarge: "E_KV_VALUE_TOO_LARGE",
|
|
120
|
+
tooManyKeys: "E_KV_TOO_MANY_KEYS",
|
|
121
|
+
projectFull: "E_KV_PROJECT_FULL",
|
|
122
|
+
forbidden: "E_KV_FORBIDDEN",
|
|
123
|
+
unavailable: "E_KV_UNAVAILABLE"
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ../store/src/identity.ts
|
|
127
|
+
import {
|
|
128
|
+
createHash,
|
|
129
|
+
createHmac,
|
|
130
|
+
createPublicKey,
|
|
131
|
+
generateKeyPairSync,
|
|
132
|
+
sign,
|
|
133
|
+
verify
|
|
134
|
+
} from "crypto";
|
|
135
|
+
var IRT_IDENTITY_ISSUER = "irt";
|
|
136
|
+
var ASSERTION_TYP = "IRTA";
|
|
137
|
+
var ASSERTION_ALG = "EdDSA";
|
|
138
|
+
var ASSERTION_TTL_MS = 5 * 6e4;
|
|
139
|
+
var ASSERTION_CLOCK_SKEW_MS = 6e4;
|
|
140
|
+
var SUB_RE = /^[\x21-\x7e]{1,128}$/;
|
|
141
|
+
function verifyAssertion(keys, token, projectId, now = Date.now()) {
|
|
142
|
+
const malformed = (reason) => ({
|
|
143
|
+
ok: false,
|
|
144
|
+
code: "E_TOKEN_MALFORMED",
|
|
145
|
+
reason
|
|
146
|
+
});
|
|
147
|
+
if (keys.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
code: "E_ASSERTION_UNVERIFIABLE",
|
|
151
|
+
reason: "this tenant holds no platform identity key yet"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const parts = token.split(".");
|
|
155
|
+
if (parts.length !== 3) return malformed("not three dot-separated segments");
|
|
156
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
157
|
+
let header;
|
|
158
|
+
let claims;
|
|
159
|
+
try {
|
|
160
|
+
header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
|
|
161
|
+
claims = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
162
|
+
} catch {
|
|
163
|
+
return malformed("header or payload is not base64url JSON");
|
|
164
|
+
}
|
|
165
|
+
if (header.typ !== ASSERTION_TYP) return malformed(`typ must be ${ASSERTION_TYP}`);
|
|
166
|
+
if (claims.iss !== IRT_IDENTITY_ISSUER) return malformed(`iss must be ${IRT_IDENTITY_ISSUER}`);
|
|
167
|
+
if (typeof claims.sub !== "string" || !SUB_RE.test(claims.sub)) {
|
|
168
|
+
return malformed("sub is required: 1-128 printable ASCII characters, no spaces");
|
|
169
|
+
}
|
|
170
|
+
if (typeof claims.aud !== "string") return malformed("aud (the project id) is required");
|
|
171
|
+
if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) {
|
|
172
|
+
return malformed("exp (unix seconds) is required");
|
|
173
|
+
}
|
|
174
|
+
if (claims.aud !== projectId) {
|
|
175
|
+
return { ok: false, code: "E_TOKEN_WRONG_PROJECT", reason: "aud is another project" };
|
|
176
|
+
}
|
|
177
|
+
if (header.alg !== ASSERTION_ALG) {
|
|
178
|
+
return { ok: false, code: "E_TOKEN_BAD_ALG", reason: String(header.alg) };
|
|
179
|
+
}
|
|
180
|
+
const signed = Buffer.from(`${headerB64}.${payloadB64}`, "utf8");
|
|
181
|
+
let signature;
|
|
182
|
+
try {
|
|
183
|
+
signature = Buffer.from(signatureB64, "base64url");
|
|
184
|
+
} catch {
|
|
185
|
+
return malformed("signature is not base64url");
|
|
186
|
+
}
|
|
187
|
+
const kid = typeof header.kid === "string" ? header.kid : void 0;
|
|
188
|
+
const candidates = kid !== void 0 ? keys.filter((k) => k.kid === kid) : [];
|
|
189
|
+
const tried = candidates.length > 0 ? candidates : keys;
|
|
190
|
+
let matched;
|
|
191
|
+
for (const key of tried) {
|
|
192
|
+
let ok = false;
|
|
193
|
+
try {
|
|
194
|
+
ok = verify(null, signed, key.publicKeyPem, signature);
|
|
195
|
+
} catch {
|
|
196
|
+
ok = false;
|
|
197
|
+
}
|
|
198
|
+
if (ok) {
|
|
199
|
+
matched = key;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!matched) return { ok: false, code: "E_TOKEN_INVALID", reason: "signature mismatch" };
|
|
204
|
+
const expMs = claims.exp * 1e3;
|
|
205
|
+
if (expMs + ASSERTION_CLOCK_SKEW_MS <= now) {
|
|
206
|
+
return { ok: false, code: "E_TOKEN_EXPIRED", reason: "exp is in the past" };
|
|
207
|
+
}
|
|
208
|
+
if (typeof claims.iat === "number" && Number.isFinite(claims.iat) && claims.iat * 1e3 - ASSERTION_CLOCK_SKEW_MS > now) {
|
|
209
|
+
return { ok: false, code: "E_TOKEN_EXPIRED", reason: "iat is in the future" };
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
sub: claims.sub,
|
|
214
|
+
playerId: `${IRT_IDENTITY_ISSUER}:${claims.sub}`,
|
|
215
|
+
kid: matched.kid,
|
|
216
|
+
expMs
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function parseIdentityKeys(value) {
|
|
220
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
221
|
+
const raw = value.keys;
|
|
222
|
+
if (!Array.isArray(raw)) return void 0;
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const entry of raw) {
|
|
225
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
226
|
+
const k = entry;
|
|
227
|
+
if (typeof k.kid !== "string" || k.kid === "") continue;
|
|
228
|
+
if (typeof k.publicKeyPem !== "string" || !k.publicKeyPem.includes("BEGIN PUBLIC KEY")) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
out.push({ kid: k.kid, publicKeyPem: k.publicKeyPem });
|
|
232
|
+
}
|
|
233
|
+
return out.slice(0, 2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ../store/src/leaderboard.ts
|
|
237
|
+
var LEADERBOARD_ERRORS = {
|
|
238
|
+
badBoard: "E_LB_BAD_BOARD",
|
|
239
|
+
badScore: "E_LB_BAD_SCORE",
|
|
240
|
+
badPlayer: "E_LB_BAD_PLAYER",
|
|
241
|
+
notInRoom: "E_LB_NOT_IN_ROOM",
|
|
242
|
+
projectFull: "E_LB_PROJECT_FULL",
|
|
243
|
+
unavailable: "E_LB_UNAVAILABLE",
|
|
244
|
+
/** D62-a: a `top` cursor that is not a cursor for the board and direction it arrived on. Its
|
|
245
|
+
* own code because the only thing to do about it is start again from the first page, which no
|
|
246
|
+
* other code in this table implies. */
|
|
247
|
+
badCursor: "E_LB_BAD_CURSOR",
|
|
248
|
+
// ---- M6 lane B: boards (D69) ----
|
|
249
|
+
/** A `?period=` that is not shaped like a period key. */
|
|
250
|
+
badPeriod: "E_LB_BAD_PERIOD",
|
|
251
|
+
/** A bucket name that breaks the board-name rules it shares. */
|
|
252
|
+
badBucket: "E_LB_BAD_BUCKET",
|
|
253
|
+
/** A bucketed board read or written without a bucket. Refused rather than merged or defaulted:
|
|
254
|
+
* a read that silently answered every cohort at once would be a ranking nobody asked for. */
|
|
255
|
+
bucketRequired: "E_LB_BUCKET_REQUIRED",
|
|
256
|
+
/** A bucket on a board that declares none. Refused rather than ignored, because a submit whose
|
|
257
|
+
* bucket was quietly dropped would put a cohort's scores on the wrong ranking. */
|
|
258
|
+
noBuckets: "E_LB_NO_BUCKETS"
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// ../store/src/rating.ts
|
|
262
|
+
var RATING_LIMITS = {
|
|
263
|
+
/** Max players named in one report. A report is one rating period and decomposes into pairs,
|
|
264
|
+
* so the work is quadratic in this number; 64 is `MAX_QUEUE_SIZE`, which is the largest party
|
|
265
|
+
* any queue may declare and therefore the largest honest report a matched room can make. */
|
|
266
|
+
maxResults: 64,
|
|
267
|
+
/** Max UTF-8 bytes in a queue name. Matches the queue-name shape below. */
|
|
268
|
+
queueBytes: 32,
|
|
269
|
+
/** Max UTF-8 bytes in a player id. The leaderboard's number, for the same column shape. */
|
|
270
|
+
playerIdBytes: 256,
|
|
271
|
+
/** Max distinct rating rows one project may hold across all its queues. */
|
|
272
|
+
rowsPerProject: 1e6,
|
|
273
|
+
/** `place` is a 1-based finishing position. Ties are equal places, so places need not be dense
|
|
274
|
+
* and need not start at 1 — only be whole, positive and inside a party's plausible size. */
|
|
275
|
+
maxPlace: 64,
|
|
276
|
+
/** The bounds a game-supplied rating (`ratings.set`) must sit inside. Wide enough for any
|
|
277
|
+
* scale a game might use, narrow enough that a NaN or an infinity cannot be stored. */
|
|
278
|
+
minRating: -1e6,
|
|
279
|
+
maxRating: 1e6,
|
|
280
|
+
/** Deviation is a standard error on the same scale, so it is positive and bounded. */
|
|
281
|
+
minDeviation: 1,
|
|
282
|
+
maxDeviation: 1e3
|
|
283
|
+
};
|
|
284
|
+
var RATING_QUEUE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
|
|
285
|
+
var RATING_ERRORS = {
|
|
286
|
+
badQueue: "E_RATING_BAD_QUEUE",
|
|
287
|
+
badResults: "E_RATING_BAD_RESULTS",
|
|
288
|
+
badPlayer: "E_RATING_BAD_PLAYER",
|
|
289
|
+
badRating: "E_RATING_BAD_RATING",
|
|
290
|
+
notInRoom: "E_RATING_NOT_IN_ROOM",
|
|
291
|
+
projectFull: "E_RATING_PROJECT_FULL",
|
|
292
|
+
unavailable: "E_RATING_UNAVAILABLE"
|
|
293
|
+
};
|
|
294
|
+
function ratingQueueProblem(queue) {
|
|
295
|
+
if (typeof queue !== "string" || queue.length === 0) {
|
|
296
|
+
return { code: RATING_ERRORS.badQueue, message: "queue must be a non-empty string" };
|
|
297
|
+
}
|
|
298
|
+
if (!RATING_QUEUE_NAME_RE.test(queue)) {
|
|
299
|
+
return {
|
|
300
|
+
code: RATING_ERRORS.badQueue,
|
|
301
|
+
message: `queue must be 1-32 characters of a-z 0-9 . _ - and start with a letter or digit (got ${JSON.stringify(queue)})`
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return void 0;
|
|
305
|
+
}
|
|
306
|
+
function ratingResultsProblem(results) {
|
|
307
|
+
if (!Array.isArray(results)) {
|
|
308
|
+
return { code: RATING_ERRORS.badResults, message: "results must be an array" };
|
|
309
|
+
}
|
|
310
|
+
if (results.length < 2) {
|
|
311
|
+
return {
|
|
312
|
+
code: RATING_ERRORS.badResults,
|
|
313
|
+
message: "a result needs at least two players; a rating is a comparison"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (results.length > RATING_LIMITS.maxResults) {
|
|
317
|
+
return {
|
|
318
|
+
code: RATING_ERRORS.badResults,
|
|
319
|
+
message: `a report may name at most ${RATING_LIMITS.maxResults} players`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const raw of results) {
|
|
324
|
+
if (typeof raw !== "object" || raw === null) {
|
|
325
|
+
return { code: RATING_ERRORS.badResults, message: "each result must be an object" };
|
|
326
|
+
}
|
|
327
|
+
const { playerId, place } = raw;
|
|
328
|
+
const bad = ratingPlayerProblem(playerId);
|
|
329
|
+
if (bad) return bad;
|
|
330
|
+
if (typeof place !== "number" || !Number.isInteger(place)) {
|
|
331
|
+
return {
|
|
332
|
+
code: RATING_ERRORS.badResults,
|
|
333
|
+
message: "place must be a whole number, 1 for first (ties share a place)"
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (place < 1 || place > RATING_LIMITS.maxPlace) {
|
|
337
|
+
return {
|
|
338
|
+
code: RATING_ERRORS.badResults,
|
|
339
|
+
message: `place must be between 1 and ${RATING_LIMITS.maxPlace}`
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (seen.has(playerId)) {
|
|
343
|
+
return {
|
|
344
|
+
code: RATING_ERRORS.badResults,
|
|
345
|
+
message: `${JSON.stringify(playerId)} appears twice; a player cannot play themselves`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
seen.add(playerId);
|
|
349
|
+
}
|
|
350
|
+
return void 0;
|
|
351
|
+
}
|
|
352
|
+
function ratingPlayerProblem(playerId) {
|
|
353
|
+
if (typeof playerId !== "string" || playerId.length === 0) {
|
|
354
|
+
return { code: RATING_ERRORS.badPlayer, message: "playerId must be a non-empty string" };
|
|
355
|
+
}
|
|
356
|
+
if (new TextEncoder().encode(playerId).length > RATING_LIMITS.playerIdBytes) {
|
|
357
|
+
return {
|
|
358
|
+
code: RATING_ERRORS.badPlayer,
|
|
359
|
+
message: `playerId must be at most ${RATING_LIMITS.playerIdBytes} bytes`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return void 0;
|
|
363
|
+
}
|
|
364
|
+
function ratingValueProblem(value) {
|
|
365
|
+
if (typeof value !== "object" || value === null) {
|
|
366
|
+
return { code: RATING_ERRORS.badRating, message: "a rating must be an object" };
|
|
367
|
+
}
|
|
368
|
+
const { rating, deviation } = value;
|
|
369
|
+
if (typeof rating !== "number" || !Number.isFinite(rating)) {
|
|
370
|
+
return { code: RATING_ERRORS.badRating, message: "rating must be a finite number" };
|
|
371
|
+
}
|
|
372
|
+
if (rating < RATING_LIMITS.minRating || rating > RATING_LIMITS.maxRating) {
|
|
373
|
+
return {
|
|
374
|
+
code: RATING_ERRORS.badRating,
|
|
375
|
+
message: `rating must be between ${RATING_LIMITS.minRating} and ${RATING_LIMITS.maxRating}`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (deviation !== void 0) {
|
|
379
|
+
if (typeof deviation !== "number" || !Number.isFinite(deviation)) {
|
|
380
|
+
return { code: RATING_ERRORS.badRating, message: "deviation must be a finite number" };
|
|
381
|
+
}
|
|
382
|
+
if (deviation < RATING_LIMITS.minDeviation || deviation > RATING_LIMITS.maxDeviation) {
|
|
383
|
+
return {
|
|
384
|
+
code: RATING_ERRORS.badRating,
|
|
385
|
+
message: `deviation must be between ${RATING_LIMITS.minDeviation} and ${RATING_LIMITS.maxDeviation}`
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ../store/src/saves.ts
|
|
393
|
+
var SAVES_SEGMENT = "/saves/";
|
|
394
|
+
var SAVE_ID_TIME_DIGITS = 17;
|
|
395
|
+
var DEFAULT_SAVE_RETAIN = 10;
|
|
396
|
+
var PRE_MIGRATION_SAVE_ID = "premigrate";
|
|
397
|
+
function mintSaveId(nowMs, rand) {
|
|
398
|
+
const t = Math.max(0, Math.floor(nowMs));
|
|
399
|
+
const suffix = (Math.floor(Math.abs(rand)) & 65535).toString(16).padStart(4, "0");
|
|
400
|
+
return `${String(t).padStart(SAVE_ID_TIME_DIGITS, "0")}-${suffix}`;
|
|
401
|
+
}
|
|
402
|
+
function saveIdCreatedAt(saveId) {
|
|
403
|
+
if (saveId.length !== SAVE_ID_TIME_DIGITS + 5) return void 0;
|
|
404
|
+
if (saveId[SAVE_ID_TIME_DIGITS] !== "-") return void 0;
|
|
405
|
+
const time = saveId.slice(0, SAVE_ID_TIME_DIGITS);
|
|
406
|
+
if (!/^[0-9]+$/.test(time)) return void 0;
|
|
407
|
+
if (!/^[0-9a-f]{4}$/.test(saveId.slice(SAVE_ID_TIME_DIGITS + 1))) return void 0;
|
|
408
|
+
return Number(time);
|
|
409
|
+
}
|
|
410
|
+
function savesPrefix(liveKey) {
|
|
411
|
+
return `${liveKey}${SAVES_SEGMENT}`;
|
|
412
|
+
}
|
|
413
|
+
function saveKey(liveKey, saveId) {
|
|
414
|
+
return `${savesPrefix(liveKey)}${saveId}`;
|
|
415
|
+
}
|
|
416
|
+
function saveIdOf(liveKey, key) {
|
|
417
|
+
const prefix = savesPrefix(liveKey);
|
|
418
|
+
if (!key.startsWith(prefix)) return void 0;
|
|
419
|
+
const rest = key.slice(prefix.length);
|
|
420
|
+
if (rest === "" || rest.includes("/")) return void 0;
|
|
421
|
+
return rest;
|
|
422
|
+
}
|
|
423
|
+
async function listSaves(store, liveKey) {
|
|
424
|
+
const keys = await store.list(savesPrefix(liveKey));
|
|
425
|
+
const out = [];
|
|
426
|
+
for (const key of keys) {
|
|
427
|
+
const saveId = saveIdOf(liveKey, key);
|
|
428
|
+
if (saveId === void 0) continue;
|
|
429
|
+
const createdAt = saveIdCreatedAt(saveId);
|
|
430
|
+
if (createdAt === void 0) continue;
|
|
431
|
+
out.push({ saveId, key, createdAt });
|
|
432
|
+
}
|
|
433
|
+
out.sort((a, b) => a.saveId < b.saveId ? 1 : a.saveId > b.saveId ? -1 : 0);
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
function selectForPruning(saves, retain) {
|
|
437
|
+
const keep = Number.isFinite(retain) ? Math.max(1, Math.floor(retain)) : 1;
|
|
438
|
+
return saves.length <= keep ? [] : saves.slice(keep);
|
|
439
|
+
}
|
|
440
|
+
async function pruneSaves(store, saves, retain, log) {
|
|
441
|
+
const doomed = selectForPruning(saves, retain);
|
|
442
|
+
let deleted = 0;
|
|
443
|
+
for (const save of doomed) {
|
|
444
|
+
try {
|
|
445
|
+
await store.delete(save.key);
|
|
446
|
+
deleted++;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
log("warn", `save retention: could not delete ${save.key}`, err);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return deleted;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ../store/src/s3.ts
|
|
455
|
+
import { createHash as createHash3 } from "crypto";
|
|
456
|
+
|
|
457
|
+
// ../store/src/sigv4.ts
|
|
458
|
+
import { createHash as createHash2, createHmac as createHmac2 } from "crypto";
|
|
459
|
+
|
|
460
|
+
export {
|
|
461
|
+
PLAYER_ISSUER_RE,
|
|
462
|
+
KV_ERRORS,
|
|
463
|
+
IRT_IDENTITY_ISSUER,
|
|
464
|
+
verifyAssertion,
|
|
465
|
+
parseIdentityKeys,
|
|
466
|
+
LEADERBOARD_ERRORS,
|
|
467
|
+
RATING_ERRORS,
|
|
468
|
+
ratingQueueProblem,
|
|
469
|
+
ratingResultsProblem,
|
|
470
|
+
ratingPlayerProblem,
|
|
471
|
+
ratingValueProblem,
|
|
472
|
+
DEFAULT_SAVE_RETAIN,
|
|
473
|
+
PRE_MIGRATION_SAVE_ID,
|
|
474
|
+
mintSaveId,
|
|
475
|
+
saveKey,
|
|
476
|
+
listSaves,
|
|
477
|
+
pruneSaves,
|
|
478
|
+
STATIC_LIMITS,
|
|
479
|
+
staticPathProblem,
|
|
480
|
+
DiskStore
|
|
481
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's on-disk credential store. One JSON file, keyed by control URL so a machine
|
|
3
|
+
* that talks to more than one control plane (a local dev instance and the real one) keeps both
|
|
4
|
+
* tokens without clobbering each other.
|
|
5
|
+
*
|
|
6
|
+
* Location: `~/.config/irtio/credentials.json` on macOS/Linux, `%APPDATA%/irtio/credentials.json`
|
|
7
|
+
* on Windows. This follows the platform's own convention for CLI config rather than XDG
|
|
8
|
+
* everywhere (`os.homedir()` alone gets the wrong answer on Windows, where `~/.config` is not a
|
|
9
|
+
* thing anyone looks for) — `%APPDATA%` is what `git`, `npm` and most Windows CLIs already use.
|
|
10
|
+
*
|
|
11
|
+
* The file is written mode 0600 (owner read/write only): it holds a bearer token that is exactly
|
|
12
|
+
* as sensitive as a password for whatever it authorizes. `chmod` runs after `writeFile` too,
|
|
13
|
+
* belt-and-suspenders against an umask or an existing file with looser permissions.
|
|
14
|
+
*/
|
|
15
|
+
/** One control plane's worth of stored auth. `email` is best-effort (see `login.ts`). */
|
|
16
|
+
interface StoredCredential {
|
|
17
|
+
readonly token: string;
|
|
18
|
+
readonly expiresAt: string;
|
|
19
|
+
readonly email?: string;
|
|
20
|
+
}
|
|
21
|
+
type CredentialsFile = Record<string, StoredCredential>;
|
|
22
|
+
/**
|
|
23
|
+
* `~/.config/irtio/credentials.json`, or `%APPDATA%/irtio/credentials.json` on Windows.
|
|
24
|
+
*
|
|
25
|
+
* `IRT_CREDENTIALS_FILE` overrides this outright — the seam the test suite uses so `login`'s
|
|
26
|
+
* end-to-end tests never touch the real machine's home directory.
|
|
27
|
+
*/
|
|
28
|
+
declare function credentialsPath(): string;
|
|
29
|
+
/** Reads the whole file. Missing or corrupt ⇒ empty (a corrupt file is not fatal, just unlucky). */
|
|
30
|
+
declare function readCredentials(): Promise<CredentialsFile>;
|
|
31
|
+
/** Reads the stored credential for one control URL, or `undefined` if there is none. */
|
|
32
|
+
declare function readCredential(controlUrl: string): Promise<StoredCredential | undefined>;
|
|
33
|
+
/** Merges one control URL's credential into the file and writes it back, mode 0600. */
|
|
34
|
+
declare function writeCredential(controlUrl: string, credential: StoredCredential): Promise<string>;
|
|
35
|
+
/** Removes one control URL's credential (used by tests; not currently wired to a command). */
|
|
36
|
+
declare function deleteCredential(controlUrl: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* The production control plane, used when nothing more specific is known. Since the single-origin
|
|
39
|
+
* merge the control API lives at the apex; `control.irt.io` has DNS but no certificate, so the
|
|
40
|
+
* old default died at the TLS handshake with a bare "fetch failed".
|
|
41
|
+
*/
|
|
42
|
+
declare const DEFAULT_CONTROL_URL = "https://irt.io";
|
|
43
|
+
/** `--url`, else `IRT_CONTROL_URL`, else the production control plane. */
|
|
44
|
+
declare function resolveControlUrl(flag: string | undefined): string;
|
|
45
|
+
/**
|
|
46
|
+
* `resolveControlUrl`, plus the one thing a user always means: **if you signed in somewhere, talk
|
|
47
|
+
* to that somewhere.**
|
|
48
|
+
*
|
|
49
|
+
* `irtio login --url https://example.test` stores the credential under that URL, but every later
|
|
50
|
+
* command used to default to `https://control.irt.io` and answer `not logged in` while a perfectly
|
|
51
|
+
* good credential sat in the file under a different key. Real time was lost to exactly
|
|
52
|
+
* that during testing, escaped only by reading the CLI's compiled source.
|
|
53
|
+
*
|
|
54
|
+
* So: an explicit `--url` or `IRT_CONTROL_URL` still wins, and a file holding credentials for
|
|
55
|
+
* several control planes stays ambiguous and falls back to the default (guessing between two
|
|
56
|
+
* logins would be worse than the default). But the overwhelmingly common case — exactly one
|
|
57
|
+
* stored login — now resolves to that one.
|
|
58
|
+
*/
|
|
59
|
+
declare function resolveControlUrlForUser(flag: string | undefined): Promise<string>;
|
|
60
|
+
|
|
61
|
+
export { type CredentialsFile, DEFAULT_CONTROL_URL, type StoredCredential, credentialsPath, deleteCredential, readCredential, readCredentials, resolveControlUrl, resolveControlUrlForUser, writeCredential };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONTROL_URL,
|
|
3
|
+
credentialsPath,
|
|
4
|
+
deleteCredential,
|
|
5
|
+
readCredential,
|
|
6
|
+
readCredentials,
|
|
7
|
+
resolveControlUrl,
|
|
8
|
+
resolveControlUrlForUser,
|
|
9
|
+
writeCredential
|
|
10
|
+
} from "./chunk-UPHQM6NZ.js";
|
|
11
|
+
export {
|
|
12
|
+
DEFAULT_CONTROL_URL,
|
|
13
|
+
credentialsPath,
|
|
14
|
+
deleteCredential,
|
|
15
|
+
readCredential,
|
|
16
|
+
readCredentials,
|
|
17
|
+
resolveControlUrl,
|
|
18
|
+
resolveControlUrlForUser,
|
|
19
|
+
writeCredential
|
|
20
|
+
};
|