@irtio/cli 0.6.0 → 0.8.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/bundle.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-OTSFRVJN.js → chunk-NOYGY3HA.js} +10 -3
- 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 +164 -6
- package/dist/{dev-QM26ONKS.js → dev-VJ2ATJTF.js} +220 -14
- package/dist/index.js +39 -15
- package/dist/init.js +3 -3
- 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-5YCFT63L.js} +4 -4
- 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 +163 -10
- package/dist/simulate.js +345 -38
- 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 +8 -7
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readProjectConfig
|
|
3
|
+
} from "./chunk-JL235KIE.js";
|
|
4
|
+
import {
|
|
5
|
+
HelpRequested,
|
|
6
|
+
helpFor,
|
|
7
|
+
helpRequested
|
|
8
|
+
} from "./chunk-OCVALOGK.js";
|
|
9
|
+
import {
|
|
10
|
+
createApiClient,
|
|
11
|
+
isLoginRequired
|
|
12
|
+
} from "./chunk-IDF46P7R.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveControlUrlForUser
|
|
15
|
+
} from "./chunk-UPHQM6NZ.js";
|
|
16
|
+
|
|
17
|
+
// src/api-keys.ts
|
|
18
|
+
import pc from "picocolors";
|
|
19
|
+
var SCOPES = ["rooms:read", "rooms:write"];
|
|
20
|
+
var USAGE = `usage: irtio api-keys mint --scopes <list> [--label <text>] [options]
|
|
21
|
+
irtio api-keys list [options]
|
|
22
|
+
irtio api-keys revoke <id> [options]
|
|
23
|
+
|
|
24
|
+
Scoped API keys let a script you run yourself \u2014 a nightly cleanup job, a support tool, your own
|
|
25
|
+
backend \u2014 reach this project's rooms over HTTP, and nothing else. A key belongs to one project,
|
|
26
|
+
carries an explicit list of what it may do there, never expires, and can be revoked.
|
|
27
|
+
|
|
28
|
+
A key is NOT a login. It cannot deploy, cannot read your usage or billing, cannot see your other
|
|
29
|
+
projects, and cannot mint or list keys, including itself.
|
|
30
|
+
|
|
31
|
+
subcommands:
|
|
32
|
+
mint --scopes <list> create a key and print it once. Scopes are comma-separated:
|
|
33
|
+
rooms:read (list and read rooms and their saves) and rooms:write
|
|
34
|
+
(also set retention, delete, and force-delete). Write implies read
|
|
35
|
+
list this project's keys, newest first, revoked ones included
|
|
36
|
+
revoke <id> stop a key working. The row stays, so the listing is still an audit
|
|
37
|
+
|
|
38
|
+
options:
|
|
39
|
+
--label <text> a note for the listing, e.g. "nightly cleanup"
|
|
40
|
+
--project <id> project id (default: the project file)
|
|
41
|
+
-c, --config <f> the project file to read (default irtio.json)
|
|
42
|
+
--url <control> control plane (default: your stored login)
|
|
43
|
+
-h, --help print this
|
|
44
|
+
`;
|
|
45
|
+
function parseApiKeysArgs(args) {
|
|
46
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
47
|
+
const first = args[0];
|
|
48
|
+
if (first !== "mint" && first !== "list" && first !== "revoke") {
|
|
49
|
+
throw new Error("irtio api-keys: expected mint, list or revoke");
|
|
50
|
+
}
|
|
51
|
+
const parsed = { sub: first };
|
|
52
|
+
let rest = args.slice(1);
|
|
53
|
+
if (first === "revoke") {
|
|
54
|
+
const id = rest[0];
|
|
55
|
+
if (id === void 0 || id.startsWith("-")) {
|
|
56
|
+
throw new Error("irtio api-keys revoke: needs a key id \u2014 run `irtio api-keys list`");
|
|
57
|
+
}
|
|
58
|
+
parsed.id = id;
|
|
59
|
+
rest = rest.slice(1);
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i < rest.length; i++) {
|
|
62
|
+
const arg = rest[i];
|
|
63
|
+
switch (arg) {
|
|
64
|
+
case "--scopes": {
|
|
65
|
+
const value = rest[++i];
|
|
66
|
+
if (value === void 0) throw new Error("irtio api-keys: --scopes needs a value");
|
|
67
|
+
const scopes = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
68
|
+
for (const scope of scopes) {
|
|
69
|
+
if (!SCOPES.includes(scope)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`irtio api-keys: ${JSON.stringify(scope)} is not a scope; the scopes are ${SCOPES.join(" and ")}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (scopes.length === 0) throw new Error("irtio api-keys: --scopes needs at least one");
|
|
76
|
+
parsed.scopes = scopes;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "--label": {
|
|
80
|
+
const value = rest[++i];
|
|
81
|
+
if (value === void 0) throw new Error("irtio api-keys: --label needs a value");
|
|
82
|
+
parsed.label = value;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "--project": {
|
|
86
|
+
const value = rest[++i];
|
|
87
|
+
if (value === void 0) throw new Error("irtio api-keys: --project needs a value");
|
|
88
|
+
parsed.project = value;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "-c":
|
|
92
|
+
case "--config": {
|
|
93
|
+
const value = rest[++i];
|
|
94
|
+
if (value === void 0 || value === "") {
|
|
95
|
+
throw new Error("irtio api-keys: --config needs a value");
|
|
96
|
+
}
|
|
97
|
+
parsed.config = value;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "--url": {
|
|
101
|
+
const value = rest[++i];
|
|
102
|
+
if (value === void 0) throw new Error("irtio api-keys: --url needs a value");
|
|
103
|
+
parsed.url = value;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`irtio api-keys: unknown option ${JSON.stringify(arg)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (parsed.sub === "mint" && parsed.scopes === void 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"irtio api-keys mint: --scopes is required \u2014 rooms:read to look, rooms:write to change things too"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
function formatKeys(rows) {
|
|
118
|
+
if (rows.length === 0) {
|
|
119
|
+
return [
|
|
120
|
+
pc.dim("no API keys"),
|
|
121
|
+
pc.dim("mint one with: irtio api-keys mint --scopes rooms:read")
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
|
|
125
|
+
const scopeWidth = Math.max(6, ...rows.map((r) => r.scopes.join(",").length));
|
|
126
|
+
const out = [
|
|
127
|
+
pc.dim(`${"ID".padEnd(idWidth)} ${"SCOPES".padEnd(scopeWidth)} LAST USED LABEL`)
|
|
128
|
+
];
|
|
129
|
+
for (const r of rows) {
|
|
130
|
+
const used = r.lastUsed ?? "never";
|
|
131
|
+
const label = r.revokedAt ? pc.dim(`${r.label} (revoked)`) : r.label;
|
|
132
|
+
out.push(
|
|
133
|
+
`${r.id.padEnd(idWidth)} ${r.scopes.join(",").padEnd(scopeWidth)} ${pc.dim(used.padEnd(20))} ${label}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
async function runMint(options) {
|
|
139
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
140
|
+
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
141
|
+
const client = options.client ?? await createApiClient(controlUrl);
|
|
142
|
+
const minted = await client.post(`/v1/projects/${options.project}/keys`, {
|
|
143
|
+
scopes: options.scopes,
|
|
144
|
+
...options.label !== void 0 ? { label: options.label } : {}
|
|
145
|
+
});
|
|
146
|
+
log("");
|
|
147
|
+
log(pc.bold(minted.key));
|
|
148
|
+
log("");
|
|
149
|
+
log(pc.yellow("Copy this now. It will not be shown again, here or anywhere else."));
|
|
150
|
+
log(pc.dim(`id ${minted.id}, scopes ${minted.scopes.join(",")}`));
|
|
151
|
+
log(pc.dim("use it as: Authorization: Bearer <key>, or set IRT_API_KEY for the rooms commands"));
|
|
152
|
+
log(pc.dim(`revoke it with: irtio api-keys revoke ${minted.id}`));
|
|
153
|
+
return minted;
|
|
154
|
+
}
|
|
155
|
+
async function runList(options) {
|
|
156
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
157
|
+
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
158
|
+
const client = options.client ?? await createApiClient(controlUrl);
|
|
159
|
+
const rows = await client.get(`/v1/projects/${options.project}/keys`);
|
|
160
|
+
for (const line of formatKeys(rows)) log(line);
|
|
161
|
+
return rows;
|
|
162
|
+
}
|
|
163
|
+
async function runRevoke(options) {
|
|
164
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
165
|
+
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
166
|
+
const client = options.client ?? await createApiClient(controlUrl);
|
|
167
|
+
await client.del(`/v1/projects/${options.project}/keys/${encodeURIComponent(options.id)}`);
|
|
168
|
+
log(pc.green(`revoked ${options.id}`));
|
|
169
|
+
log(pc.dim("anything using it now gets a 401. The row stays in the listing as a record"));
|
|
170
|
+
}
|
|
171
|
+
async function apiKeys(args, deps = {}) {
|
|
172
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
173
|
+
const errorLog = deps.errorLog ?? ((line) => console.error(line));
|
|
174
|
+
try {
|
|
175
|
+
const parsed = parseApiKeysArgs(args);
|
|
176
|
+
const config = await readProjectConfig(process.cwd(), "irtio api-keys", parsed.config);
|
|
177
|
+
const project = parsed.project ?? config.project;
|
|
178
|
+
if (project === void 0) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"irtio api-keys: no project id \u2014 pass --project or run this from a project with irtio.json"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const common = {
|
|
184
|
+
project,
|
|
185
|
+
log,
|
|
186
|
+
...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
|
|
187
|
+
...deps.client !== void 0 ? { client: deps.client } : {}
|
|
188
|
+
};
|
|
189
|
+
if (parsed.sub === "mint") {
|
|
190
|
+
await runMint({
|
|
191
|
+
...common,
|
|
192
|
+
scopes: parsed.scopes,
|
|
193
|
+
...parsed.label !== void 0 ? { label: parsed.label } : {}
|
|
194
|
+
});
|
|
195
|
+
} else if (parsed.sub === "revoke") {
|
|
196
|
+
await runRevoke({ ...common, id: parsed.id });
|
|
197
|
+
} else {
|
|
198
|
+
await runList(common);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err instanceof HelpRequested) {
|
|
202
|
+
log(err.usage);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (isLoginRequired(err)) {
|
|
206
|
+
errorLog(pc.red("not logged in"));
|
|
207
|
+
errorLog("run: irtio login");
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
errorLog(pc.red(err instanceof Error ? err.message : String(err)));
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
USAGE,
|
|
217
|
+
apiKeys,
|
|
218
|
+
parseApiKeysArgs,
|
|
219
|
+
runList,
|
|
220
|
+
runMint,
|
|
221
|
+
runRevoke
|
|
222
|
+
};
|
package/dist/api.js
CHANGED
package/dist/bundle.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
STATIC_LIMITS,
|
|
3
3
|
staticPathProblem
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WFMRNGO5.js";
|
|
5
5
|
import {
|
|
6
6
|
ensureProject,
|
|
7
7
|
readProjectConfig
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-JL235KIE.js";
|
|
9
9
|
import {
|
|
10
10
|
HelpRequested,
|
|
11
11
|
helpFor,
|
|
12
12
|
helpRequested
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OCVALOGK.js";
|
|
14
14
|
import {
|
|
15
15
|
createApiClient,
|
|
16
16
|
isLoginRequired
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-IDF46P7R.js";
|
|
18
18
|
import {
|
|
19
19
|
resolveControlUrlForUser
|
|
20
20
|
} from "./chunk-UPHQM6NZ.js";
|
|
@@ -26,6 +26,8 @@ var NotLoggedInError = class extends Error {
|
|
|
26
26
|
name = "NotLoggedInError";
|
|
27
27
|
};
|
|
28
28
|
async function createApiClient(controlUrl) {
|
|
29
|
+
const apiKey = process.env.IRT_API_KEY?.trim();
|
|
30
|
+
if (apiKey) return createApiClientWithToken(controlUrl, apiKey);
|
|
29
31
|
const credential = await readCredential(controlUrl);
|
|
30
32
|
if (!credential) throw new NotLoggedInError(controlUrl);
|
|
31
33
|
return createApiClientWithToken(controlUrl, credential.token);
|
|
@@ -10,9 +10,16 @@ var ALLOWED_IMPORTS = [
|
|
|
10
10
|
"@irtio/schema",
|
|
11
11
|
"@dimforge/rapier3d-compat",
|
|
12
12
|
// D45: the second blessed engine. Exactly one import added, as the decision says.
|
|
13
|
-
"matter-js"
|
|
13
|
+
"matter-js",
|
|
14
|
+
// The third: Rapier held in a plane. A separate package from the 3D build, so a separate
|
|
15
|
+
// import — a room picks one and pays for one.
|
|
16
|
+
"@dimforge/rapier2d-compat"
|
|
17
|
+
];
|
|
18
|
+
var EXTERNAL_IMPORTS = [
|
|
19
|
+
"@dimforge/rapier3d-compat",
|
|
20
|
+
"matter-js",
|
|
21
|
+
"@dimforge/rapier2d-compat"
|
|
14
22
|
];
|
|
15
|
-
var EXTERNAL_IMPORTS = ["@dimforge/rapier3d-compat", "matter-js"];
|
|
16
23
|
var BundleError = class extends Error {
|
|
17
24
|
name = "BundleError";
|
|
18
25
|
};
|
|
@@ -92,7 +99,7 @@ async function bundleRoom(options) {
|
|
|
92
99
|
if (!out) throw new BundleError("esbuild produced no output");
|
|
93
100
|
const warnings = result.warnings.map((w) => w.text);
|
|
94
101
|
const importsEngine = Object.values(result.metafile?.outputs ?? {}).some(
|
|
95
|
-
(o) => o.imports.some((i) =>
|
|
102
|
+
(o) => o.imports.some((i) => EXTERNAL_IMPORTS.includes(i.path))
|
|
96
103
|
);
|
|
97
104
|
const hash = createHash("sha256").update(out.contents).digest("hex");
|
|
98
105
|
const file = path.join(options.outDir, `room.${hash.slice(0, 16)}.mjs`);
|
|
@@ -1,14 +1,105 @@
|
|
|
1
|
-
// ../store/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
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
|
|
10
24
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
|
|
11
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
|
|
12
103
|
var KV_LIMITS = {
|
|
13
104
|
/** Max UTF-8 bytes in one value. */
|
|
14
105
|
valueBytes: 16 * 1024,
|
|
@@ -31,6 +122,16 @@ var KV_ERRORS = {
|
|
|
31
122
|
forbidden: "E_KV_FORBIDDEN",
|
|
32
123
|
unavailable: "E_KV_UNAVAILABLE"
|
|
33
124
|
};
|
|
125
|
+
|
|
126
|
+
// ../store/src/identity.ts
|
|
127
|
+
import {
|
|
128
|
+
createHash,
|
|
129
|
+
createHmac,
|
|
130
|
+
createPublicKey,
|
|
131
|
+
generateKeyPairSync,
|
|
132
|
+
sign,
|
|
133
|
+
verify
|
|
134
|
+
} from "crypto";
|
|
34
135
|
var IRT_IDENTITY_ISSUER = "irt";
|
|
35
136
|
var ASSERTION_TYP = "IRTA";
|
|
36
137
|
var ASSERTION_ALG = "EdDSA";
|
|
@@ -131,6 +232,8 @@ function parseIdentityKeys(value) {
|
|
|
131
232
|
}
|
|
132
233
|
return out.slice(0, 2);
|
|
133
234
|
}
|
|
235
|
+
|
|
236
|
+
// ../store/src/leaderboard.ts
|
|
134
237
|
var LEADERBOARD_ERRORS = {
|
|
135
238
|
badBoard: "E_LB_BAD_BOARD",
|
|
136
239
|
badScore: "E_LB_BAD_SCORE",
|
|
@@ -141,8 +244,21 @@ var LEADERBOARD_ERRORS = {
|
|
|
141
244
|
/** D62-a: a `top` cursor that is not a cursor for the board and direction it arrived on. Its
|
|
142
245
|
* own code because the only thing to do about it is start again from the first page, which no
|
|
143
246
|
* other code in this table implies. */
|
|
144
|
-
badCursor: "E_LB_BAD_CURSOR"
|
|
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"
|
|
145
259
|
};
|
|
260
|
+
|
|
261
|
+
// ../store/src/rating.ts
|
|
146
262
|
var RATING_LIMITS = {
|
|
147
263
|
/** Max players named in one report. A report is one rating period and decomposes into pairs,
|
|
148
264
|
* so the work is quadratic in this number; 64 is `MAX_QUEUE_SIZE`, which is the largest party
|
|
@@ -272,6 +388,8 @@ function ratingValueProblem(value) {
|
|
|
272
388
|
}
|
|
273
389
|
return void 0;
|
|
274
390
|
}
|
|
391
|
+
|
|
392
|
+
// ../store/src/saves.ts
|
|
275
393
|
var SAVES_SEGMENT = "/saves/";
|
|
276
394
|
var SAVE_ID_TIME_DIGITS = 17;
|
|
277
395
|
var DEFAULT_SAVE_RETAIN = 10;
|
|
@@ -332,101 +450,12 @@ async function pruneSaves(store, saves, retain, log) {
|
|
|
332
450
|
}
|
|
333
451
|
return deleted;
|
|
334
452
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
};
|
|
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";
|
|
430
459
|
|
|
431
460
|
export {
|
|
432
461
|
PLAYER_ISSUER_RE,
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
HelpRequested,
|
|
3
3
|
helpFor,
|
|
4
4
|
helpRequested
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OCVALOGK.js";
|
|
6
6
|
import {
|
|
7
7
|
createApiClient,
|
|
8
8
|
isLoginRequired
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IDF46P7R.js";
|
|
10
10
|
import {
|
|
11
11
|
resolveControlUrlForUser
|
|
12
12
|
} from "./chunk-UPHQM6NZ.js";
|
package/dist/deploy.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ interface RunDeployResult {
|
|
|
90
90
|
/** The room entry, or undefined when the project has none. `--room` naming a missing file still
|
|
91
91
|
* throws: that is a typo, not a project without a room. */
|
|
92
92
|
declare function findRoomEntry(cwd: string, room: string | undefined): string | undefined;
|
|
93
|
+
/** D70: the schema entry of a schema-only project, or `undefined` when it has none. */
|
|
94
|
+
declare function findSchemaEntry(cwd: string): string | undefined;
|
|
93
95
|
/** Every room type a project declares, resolved to entry files. */
|
|
94
96
|
interface RoomTypeEntries {
|
|
95
97
|
/** The entry `resolveEntry` would have picked: what a plain roomId gets. */
|
|
@@ -146,4 +148,4 @@ declare function deploy(args: readonly string[], deps?: {
|
|
|
146
148
|
irtioPackages?: BundleOptions['irtioPackages'];
|
|
147
149
|
}): Promise<void>;
|
|
148
150
|
|
|
149
|
-
export { type DeployArgs, DeployRefusedError, type RoomTypeEntries, type RunDeployOptions, type RunDeployResult, USAGE, deploy, findRoomEntry, findRoomTypes, parseDeployArgs, runDeploy };
|
|
151
|
+
export { type DeployArgs, DeployRefusedError, type RoomTypeEntries, type RunDeployOptions, type RunDeployResult, USAGE, deploy, findRoomEntry, findRoomTypes, findSchemaEntry, parseDeployArgs, runDeploy };
|