@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,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.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin typed `fetch` wrapper around `@irtio/control`'s `/v1` API (contract.ts). Two things
|
|
3
|
+
* earn this its own module rather than inlined `fetch` calls in every command:
|
|
4
|
+
*
|
|
5
|
+
* 1. **`ApiError` becomes a real thrown error.** The control plane's error bodies (`code`,
|
|
6
|
+
* `message`, and — on `E_BREAKING_SCHEMA` — `changes`/`hint`) are exactly what `irtio deploy`
|
|
7
|
+
* needs to print verbatim; `ApiClientError` carries them through instead of a generic
|
|
8
|
+
* "request failed" the command layer would have to re-parse.
|
|
9
|
+
* 2. **401 is a command, not an error.** Every command that hits the control plane wants the same
|
|
10
|
+
* "run: irtio login" line and exit code 1, so it lives here once.
|
|
11
|
+
*/
|
|
12
|
+
declare class ApiClientError extends Error {
|
|
13
|
+
readonly name = "ApiClientError";
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly changes: readonly unknown[] | undefined;
|
|
17
|
+
readonly hint: string | undefined;
|
|
18
|
+
constructor(status: number, body: {
|
|
19
|
+
code: string;
|
|
20
|
+
message: string;
|
|
21
|
+
changes?: readonly unknown[];
|
|
22
|
+
hint?: string;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Thrown by `requireApiClient`/callers when there is no stored token: the login nudge. */
|
|
26
|
+
declare class NotLoggedInError extends Error {
|
|
27
|
+
readonly controlUrl: string;
|
|
28
|
+
readonly name = "NotLoggedInError";
|
|
29
|
+
constructor(controlUrl: string);
|
|
30
|
+
}
|
|
31
|
+
interface ApiClient {
|
|
32
|
+
readonly controlUrl: string;
|
|
33
|
+
get<T>(path: string, query?: Record<string, string | undefined>): Promise<T>;
|
|
34
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
35
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
36
|
+
/**
|
|
37
|
+
* `DELETE`, with the same query-string handling as `get`. The one route that needs it today is
|
|
38
|
+
* `DELETE /v1/projects/:id/origins`, which names the origin to remove in the query rather than
|
|
39
|
+
* the body, so a client without this method cannot remove an origin at all.
|
|
40
|
+
*/
|
|
41
|
+
del<T>(path: string, query?: Record<string, string | undefined>): Promise<T>;
|
|
42
|
+
/** Uploads raw bytes (a bundle) rather than JSON. */
|
|
43
|
+
postBytes<T>(path: string, bytes: Uint8Array): Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
/** Builds a client bound to one control URL, using whatever token `irtio login` last stored. */
|
|
46
|
+
declare function createApiClient(controlUrl: string): Promise<ApiClient>;
|
|
47
|
+
/** Same as `createApiClient`, but with an explicit token — what tests use. */
|
|
48
|
+
declare function createApiClientWithToken(controlUrl: string, token: string): ApiClient;
|
|
49
|
+
/** True when `err` is the "please log in" case — the shape every command checks for. */
|
|
50
|
+
declare function isLoginRequired(err: unknown): boolean;
|
|
51
|
+
|
|
52
|
+
export { type ApiClient, ApiClientError, NotLoggedInError, createApiClient, createApiClientWithToken, isLoginRequired };
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiClientError,
|
|
3
|
+
NotLoggedInError,
|
|
4
|
+
createApiClient,
|
|
5
|
+
createApiClientWithToken,
|
|
6
|
+
isLoginRequired
|
|
7
|
+
} from "./chunk-IDF46P7R.js";
|
|
8
|
+
import "./chunk-UPHQM6NZ.js";
|
|
9
|
+
export {
|
|
10
|
+
ApiClientError,
|
|
11
|
+
NotLoggedInError,
|
|
12
|
+
createApiClient,
|
|
13
|
+
createApiClientWithToken,
|
|
14
|
+
isLoginRequired
|
|
15
|
+
};
|
package/dist/bundle.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
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
|
-
createApiClient,
|
|
12
11
|
helpFor,
|
|
13
|
-
helpRequested
|
|
14
|
-
|
|
12
|
+
helpRequested
|
|
13
|
+
} from "./chunk-OCVALOGK.js";
|
|
14
|
+
import {
|
|
15
|
+
createApiClient,
|
|
16
|
+
isLoginRequired
|
|
17
|
+
} from "./chunk-IDF46P7R.js";
|
|
18
|
+
import {
|
|
15
19
|
resolveControlUrlForUser
|
|
16
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-UPHQM6NZ.js";
|
|
17
21
|
|
|
18
22
|
// src/static-deploy.ts
|
|
19
23
|
import { existsSync } from "fs";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readCredential
|
|
3
|
+
} from "./chunk-UPHQM6NZ.js";
|
|
4
|
+
|
|
5
|
+
// src/api-client.ts
|
|
6
|
+
var ApiClientError = class extends Error {
|
|
7
|
+
name = "ApiClientError";
|
|
8
|
+
status;
|
|
9
|
+
code;
|
|
10
|
+
changes;
|
|
11
|
+
hint;
|
|
12
|
+
constructor(status, body) {
|
|
13
|
+
super(body.message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.code = body.code;
|
|
16
|
+
this.changes = body.changes;
|
|
17
|
+
this.hint = body.hint;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var NotLoggedInError = class extends Error {
|
|
21
|
+
constructor(controlUrl) {
|
|
22
|
+
super(`not logged in to ${controlUrl} \u2014 run: irtio login`);
|
|
23
|
+
this.controlUrl = controlUrl;
|
|
24
|
+
}
|
|
25
|
+
controlUrl;
|
|
26
|
+
name = "NotLoggedInError";
|
|
27
|
+
};
|
|
28
|
+
async function createApiClient(controlUrl) {
|
|
29
|
+
const apiKey = process.env.IRT_API_KEY?.trim();
|
|
30
|
+
if (apiKey) return createApiClientWithToken(controlUrl, apiKey);
|
|
31
|
+
const credential = await readCredential(controlUrl);
|
|
32
|
+
if (!credential) throw new NotLoggedInError(controlUrl);
|
|
33
|
+
return createApiClientWithToken(controlUrl, credential.token);
|
|
34
|
+
}
|
|
35
|
+
function createApiClientWithToken(controlUrl, token) {
|
|
36
|
+
async function request(method, path, init = {}) {
|
|
37
|
+
const headers = {
|
|
38
|
+
Authorization: `Bearer ${token}`,
|
|
39
|
+
Accept: "application/json"
|
|
40
|
+
};
|
|
41
|
+
let requestBody;
|
|
42
|
+
if (init.bytes !== void 0) {
|
|
43
|
+
headers["content-type"] = "application/octet-stream";
|
|
44
|
+
requestBody = init.bytes;
|
|
45
|
+
} else if (init.body !== void 0) {
|
|
46
|
+
headers["content-type"] = "application/json";
|
|
47
|
+
requestBody = JSON.stringify(init.body);
|
|
48
|
+
}
|
|
49
|
+
const response = await fetch(`${controlUrl}${path}`, {
|
|
50
|
+
method,
|
|
51
|
+
headers,
|
|
52
|
+
...requestBody !== void 0 ? { body: requestBody } : {}
|
|
53
|
+
});
|
|
54
|
+
const text = await response.text();
|
|
55
|
+
const parsed = text.length > 0 ? JSON.parse(text) : void 0;
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const body = parsed !== void 0 && typeof parsed === "object" && parsed !== null && "code" in parsed && "message" in parsed ? parsed : {
|
|
58
|
+
code: "E_UNKNOWN",
|
|
59
|
+
message: text || `${method} ${path} failed with ${response.status}`
|
|
60
|
+
};
|
|
61
|
+
throw new ApiClientError(response.status, body);
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
function withQuery(path, query) {
|
|
66
|
+
const qs = query ? Object.entries(query).filter((e) => e[1] !== void 0).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&") : "";
|
|
67
|
+
return qs ? `${path}?${qs}` : path;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
controlUrl,
|
|
71
|
+
get(path, query) {
|
|
72
|
+
return request("GET", withQuery(path, query));
|
|
73
|
+
},
|
|
74
|
+
del(path, query) {
|
|
75
|
+
return request("DELETE", withQuery(path, query));
|
|
76
|
+
},
|
|
77
|
+
post(path, body) {
|
|
78
|
+
return request("POST", path, { body });
|
|
79
|
+
},
|
|
80
|
+
patch(path, body) {
|
|
81
|
+
return request("PATCH", path, { body });
|
|
82
|
+
},
|
|
83
|
+
postBytes(path, bytes) {
|
|
84
|
+
return request("POST", path, { bytes });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isLoginRequired(err) {
|
|
89
|
+
return err instanceof NotLoggedInError || err instanceof ApiClientError && err.status === 401;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
ApiClientError,
|
|
94
|
+
NotLoggedInError,
|
|
95
|
+
createApiClient,
|
|
96
|
+
createApiClientWithToken,
|
|
97
|
+
isLoginRequired
|
|
98
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiClientError
|
|
3
|
+
} from "./chunk-IDF46P7R.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
|
+
};
|
|
@@ -5,8 +5,14 @@ import * as path from "path";
|
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
import { Worker } from "worker_threads";
|
|
7
7
|
import * as esbuild from "esbuild";
|
|
8
|
-
var ALLOWED_IMPORTS = [
|
|
9
|
-
|
|
8
|
+
var ALLOWED_IMPORTS = [
|
|
9
|
+
"@irtio/server",
|
|
10
|
+
"@irtio/schema",
|
|
11
|
+
"@dimforge/rapier3d-compat",
|
|
12
|
+
// D45: the second blessed engine. Exactly one import added, as the decision says.
|
|
13
|
+
"matter-js"
|
|
14
|
+
];
|
|
15
|
+
var EXTERNAL_IMPORTS = ["@dimforge/rapier3d-compat", "matter-js"];
|
|
10
16
|
var BundleError = class extends Error {
|
|
11
17
|
name = "BundleError";
|
|
12
18
|
};
|
|
@@ -85,8 +91,8 @@ async function bundleRoom(options) {
|
|
|
85
91
|
const out = result.outputFiles[0];
|
|
86
92
|
if (!out) throw new BundleError("esbuild produced no output");
|
|
87
93
|
const warnings = result.warnings.map((w) => w.text);
|
|
88
|
-
const
|
|
89
|
-
(o) => o.imports.some((i) => i.path === "@dimforge/rapier3d-compat")
|
|
94
|
+
const importsEngine = Object.values(result.metafile?.outputs ?? {}).some(
|
|
95
|
+
(o) => o.imports.some((i) => i.path === "@dimforge/rapier3d-compat" || i.path === "matter-js")
|
|
90
96
|
);
|
|
91
97
|
const hash = createHash("sha256").update(out.contents).digest("hex");
|
|
92
98
|
const file = path.join(options.outDir, `room.${hash.slice(0, 16)}.mjs`);
|
|
@@ -108,9 +114,9 @@ async function bundleRoom(options) {
|
|
|
108
114
|
}
|
|
109
115
|
if (options.verify === false) return { file, hash, warnings };
|
|
110
116
|
const verified = await verifyBundle(file);
|
|
111
|
-
if (
|
|
117
|
+
if (importsEngine && !verified.physics) {
|
|
112
118
|
throw new BundleError(
|
|
113
|
-
"irtio: this room imports
|
|
119
|
+
"irtio: this room imports a physics engine but declares no physics. Add\n physics: { engine: 'rapier3d', gravity: { x: 0, y: -9.81, z: 0 }, bodies: { ... } }\nor, for the 2D engine,\n physics: { engine: 'matter2d', gravity: { x: 0, y: 1 }, bodies: { ... } }\nto defineRoom(...), and mark the body-backed fields with the schema's physics option. Without it the runtime never creates a world, and nothing you build with the engine steps."
|
|
114
120
|
);
|
|
115
121
|
}
|
|
116
122
|
const fullHash = createHash("sha256").update(out.contents).update(verified.canonical).digest("hex");
|
|
@@ -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
|
+
};
|