@scrthq/runlog 0.0.37 → 0.0.39
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/README.md +3 -0
- package/dist/runlog.js +422 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,9 @@ npx @scrthq/runlog sign my-game.yaml --key my-key.json --as "Your Name"
|
|
|
12
12
|
npx @scrthq/runlog issue my-game.yaml --to "Buyer" --ref order-1 --key my-key.json --seal
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
`npx @scrthq/runlog login` signs in through your browser, so that `claim`
|
|
16
|
+
can tie a signing key to your account and the app names you beside what you
|
|
17
|
+
sign. In CI, set `RUNLOG_API_KEY` to a key from your profile page instead.
|
|
15
18
|
`npx @scrthq/runlog help` lists everything.
|
|
16
19
|
|
|
17
20
|
The same package is a library for a seller's own backend: `seal`, `open`,
|
package/dist/runlog.js
CHANGED
|
@@ -7368,9 +7368,9 @@ var require_dist = __commonJS({
|
|
|
7368
7368
|
});
|
|
7369
7369
|
|
|
7370
7370
|
// packages/cli/src/bin.ts
|
|
7371
|
-
var
|
|
7372
|
-
import { readFileSync as
|
|
7373
|
-
import { basename, dirname, resolve as
|
|
7371
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
7372
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "node:fs";
|
|
7373
|
+
import { basename, dirname, resolve as resolve3 } from "node:path";
|
|
7374
7374
|
import { createHash } from "node:crypto";
|
|
7375
7375
|
|
|
7376
7376
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -27813,6 +27813,11 @@ async function signPack(document, privateKey, signedBy) {
|
|
|
27813
27813
|
...signedBy ? { signedBy } : {}
|
|
27814
27814
|
};
|
|
27815
27815
|
}
|
|
27816
|
+
async function signBytes(bytes2, privateKey) {
|
|
27817
|
+
const key = await crypto.subtle.importKey("pkcs8", fromBase64Url(privateKey), ALGORITHM, false, ["sign"]);
|
|
27818
|
+
const value = await crypto.subtle.sign(SIGN, key, bytes2);
|
|
27819
|
+
return toBase64Url(new Uint8Array(value));
|
|
27820
|
+
}
|
|
27816
27821
|
async function derivePublicKey(privateKey) {
|
|
27817
27822
|
const key = await crypto.subtle.importKey(
|
|
27818
27823
|
"pkcs8",
|
|
@@ -28203,9 +28208,346 @@ function runFixtures(pack) {
|
|
|
28203
28208
|
}
|
|
28204
28209
|
|
|
28205
28210
|
// packages/cli/src/sign.ts
|
|
28211
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
28212
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
28213
|
+
import { resolve as resolve2 } from "node:path";
|
|
28214
|
+
|
|
28215
|
+
// packages/cli/src/account.ts
|
|
28206
28216
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
28207
|
-
import {
|
|
28208
|
-
import {
|
|
28217
|
+
import { spawn } from "node:child_process";
|
|
28218
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
28219
|
+
import { homedir } from "node:os";
|
|
28220
|
+
import { join, resolve } from "node:path";
|
|
28221
|
+
import { createInterface } from "node:readline";
|
|
28222
|
+
var DEFAULT_API = "https://runlog.scrthq.com/api";
|
|
28223
|
+
var RENEW_MARGIN_MS = 6e4;
|
|
28224
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
28225
|
+
function configDir() {
|
|
28226
|
+
const base = process.env["RUNLOG_CONFIG_DIR"] ?? (process.platform === "win32" ? join(process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"), "runlog") : join(process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config"), "runlog"));
|
|
28227
|
+
return base;
|
|
28228
|
+
}
|
|
28229
|
+
var credentialsPath = () => join(configDir(), "credentials.json");
|
|
28230
|
+
function credentials() {
|
|
28231
|
+
const fromEnv = process.env["RUNLOG_API_KEY"];
|
|
28232
|
+
if (fromEnv) return { api: process.env["RUNLOG_API"] ?? DEFAULT_API, key: fromEnv, savedAt: "" };
|
|
28233
|
+
try {
|
|
28234
|
+
const raw = JSON.parse(readFileSync(credentialsPath(), "utf8"));
|
|
28235
|
+
if (typeof raw.api !== "string") return null;
|
|
28236
|
+
if (typeof raw.key === "string") return { api: raw.api, key: raw.key, savedAt: raw.savedAt ?? "" };
|
|
28237
|
+
const s = raw.session;
|
|
28238
|
+
if (s && typeof s.accessToken === "string" && typeof s.refreshToken === "string" && typeof s.clientId === "string") {
|
|
28239
|
+
return {
|
|
28240
|
+
api: raw.api,
|
|
28241
|
+
session: { clientId: s.clientId, issuer: s.issuer ?? WORKOS, accessToken: s.accessToken, refreshToken: s.refreshToken, expiresAt: s.expiresAt ?? "" },
|
|
28242
|
+
savedAt: raw.savedAt ?? ""
|
|
28243
|
+
};
|
|
28244
|
+
}
|
|
28245
|
+
return null;
|
|
28246
|
+
} catch {
|
|
28247
|
+
return null;
|
|
28248
|
+
}
|
|
28249
|
+
}
|
|
28250
|
+
function save(creds) {
|
|
28251
|
+
const dir = configDir();
|
|
28252
|
+
mkdirSync(dir, { recursive: true });
|
|
28253
|
+
writeFileSync(credentialsPath(), `${JSON.stringify(creds, null, 2)}
|
|
28254
|
+
`, "utf8");
|
|
28255
|
+
try {
|
|
28256
|
+
chmodSync(credentialsPath(), 384);
|
|
28257
|
+
} catch {
|
|
28258
|
+
}
|
|
28259
|
+
}
|
|
28260
|
+
async function ask(prompt, hidden = false) {
|
|
28261
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
28262
|
+
return new Promise((done) => {
|
|
28263
|
+
if (hidden) {
|
|
28264
|
+
const out = process.stdout;
|
|
28265
|
+
process.stdout.write(prompt);
|
|
28266
|
+
const original = out.write.bind(out);
|
|
28267
|
+
out.write = ((chunk, ...rest) => {
|
|
28268
|
+
if (typeof chunk === "string" && chunk !== "\n" && chunk !== "\r\n") return true;
|
|
28269
|
+
return original(chunk, ...rest);
|
|
28270
|
+
});
|
|
28271
|
+
rl.question("", (answer) => {
|
|
28272
|
+
out.write = original;
|
|
28273
|
+
process.stdout.write("\n");
|
|
28274
|
+
rl.close();
|
|
28275
|
+
done(answer.trim());
|
|
28276
|
+
});
|
|
28277
|
+
} else {
|
|
28278
|
+
rl.question(prompt, (answer) => {
|
|
28279
|
+
rl.close();
|
|
28280
|
+
done(answer.trim());
|
|
28281
|
+
});
|
|
28282
|
+
}
|
|
28283
|
+
});
|
|
28284
|
+
}
|
|
28285
|
+
var WORKOS = "https://api.workos.com";
|
|
28286
|
+
var realDeps = {
|
|
28287
|
+
fetch: (input2, init) => fetch(input2, init),
|
|
28288
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
28289
|
+
say: (line) => console.log(line),
|
|
28290
|
+
open: (url2) => {
|
|
28291
|
+
try {
|
|
28292
|
+
const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : process.platform === "darwin" ? ["open", [url2]] : ["xdg-open", [url2]];
|
|
28293
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).on("error", () => {
|
|
28294
|
+
}).unref();
|
|
28295
|
+
} catch {
|
|
28296
|
+
}
|
|
28297
|
+
},
|
|
28298
|
+
now: () => Date.now()
|
|
28299
|
+
};
|
|
28300
|
+
async function postForm(deps, url2, form) {
|
|
28301
|
+
const response = await deps.fetch(url2, {
|
|
28302
|
+
method: "POST",
|
|
28303
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
28304
|
+
body: new URLSearchParams(form).toString()
|
|
28305
|
+
});
|
|
28306
|
+
const text = await response.text();
|
|
28307
|
+
try {
|
|
28308
|
+
return { status: response.status, body: JSON.parse(text) };
|
|
28309
|
+
} catch {
|
|
28310
|
+
throw new Error(`WorkOS did not answer as expected (${response.status})`);
|
|
28311
|
+
}
|
|
28312
|
+
}
|
|
28313
|
+
function expiryOf(accessToken, now) {
|
|
28314
|
+
try {
|
|
28315
|
+
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
28316
|
+
if (typeof payload.exp === "number") return new Date(payload.exp * 1e3).toISOString();
|
|
28317
|
+
} catch {
|
|
28318
|
+
}
|
|
28319
|
+
return new Date(now + 5 * 6e4).toISOString();
|
|
28320
|
+
}
|
|
28321
|
+
async function deviceFlow(clientId, issuer, deps = realDeps) {
|
|
28322
|
+
const start = await postForm(deps, `${issuer}/user_management/authorize/device`, { client_id: clientId });
|
|
28323
|
+
const s = start.body;
|
|
28324
|
+
if (start.status >= 400 || !s.device_code || !s.user_code || !s.verification_uri) {
|
|
28325
|
+
throw new Error(s.error_description ?? `WorkOS would not start a sign-in (${start.status})`);
|
|
28326
|
+
}
|
|
28327
|
+
deps.say("");
|
|
28328
|
+
deps.say(` Open ${s.verification_uri}`);
|
|
28329
|
+
deps.say(` Code ${s.user_code}`);
|
|
28330
|
+
deps.say("");
|
|
28331
|
+
deps.say("Waiting for you to confirm it there. Ctrl-C gives up.");
|
|
28332
|
+
if (s.verification_uri_complete) deps.open(s.verification_uri_complete);
|
|
28333
|
+
let interval = Math.max(1, s.interval ?? 5);
|
|
28334
|
+
const deadline = deps.now() + (s.expires_in ?? 300) * 1e3;
|
|
28335
|
+
while (deps.now() < deadline) {
|
|
28336
|
+
await deps.sleep(interval * 1e3);
|
|
28337
|
+
const poll = await postForm(deps, `${issuer}/user_management/authenticate`, { grant_type: DEVICE_GRANT, device_code: s.device_code, client_id: clientId });
|
|
28338
|
+
const a = poll.body;
|
|
28339
|
+
if (a.access_token && a.refresh_token) {
|
|
28340
|
+
return { clientId, issuer, accessToken: a.access_token, refreshToken: a.refresh_token, expiresAt: expiryOf(a.access_token, deps.now()) };
|
|
28341
|
+
}
|
|
28342
|
+
switch (a.error) {
|
|
28343
|
+
case "authorization_pending":
|
|
28344
|
+
continue;
|
|
28345
|
+
case "slow_down":
|
|
28346
|
+
interval += 1;
|
|
28347
|
+
continue;
|
|
28348
|
+
case "access_denied":
|
|
28349
|
+
throw new Error("the sign-in was refused in the browser");
|
|
28350
|
+
case "expired_token":
|
|
28351
|
+
throw new Error("the code expired before it was confirmed; run `runlog login` again");
|
|
28352
|
+
default:
|
|
28353
|
+
throw new Error(a.error_description ?? a.error ?? `WorkOS answered ${poll.status}`);
|
|
28354
|
+
}
|
|
28355
|
+
}
|
|
28356
|
+
throw new Error("the code expired before it was confirmed; run `runlog login` again");
|
|
28357
|
+
}
|
|
28358
|
+
async function renew(session, deps = realDeps) {
|
|
28359
|
+
const answer = await postForm(deps, `${session.issuer}/user_management/authenticate`, {
|
|
28360
|
+
grant_type: "refresh_token",
|
|
28361
|
+
refresh_token: session.refreshToken,
|
|
28362
|
+
client_id: session.clientId
|
|
28363
|
+
});
|
|
28364
|
+
const a = answer.body;
|
|
28365
|
+
if (!a.access_token || !a.refresh_token) {
|
|
28366
|
+
throw new Error("your sign-in has lapsed; run `runlog login` again");
|
|
28367
|
+
}
|
|
28368
|
+
return { ...session, accessToken: a.access_token, refreshToken: a.refresh_token, expiresAt: expiryOf(a.access_token, deps.now()) };
|
|
28369
|
+
}
|
|
28370
|
+
async function bearer(creds, force = false, deps = realDeps) {
|
|
28371
|
+
if (creds.key) return creds.key;
|
|
28372
|
+
if (!creds.session) throw new Error("not signed in: run `runlog login`");
|
|
28373
|
+
const lapsing = !creds.session.expiresAt || Date.parse(creds.session.expiresAt) - deps.now() < RENEW_MARGIN_MS;
|
|
28374
|
+
if (!force && !lapsing) return creds.session.accessToken;
|
|
28375
|
+
const session = await renew(creds.session, deps);
|
|
28376
|
+
creds.session = session;
|
|
28377
|
+
if (!process.env["RUNLOG_API_KEY"]) save(creds);
|
|
28378
|
+
return session.accessToken;
|
|
28379
|
+
}
|
|
28380
|
+
async function api(method, path, body) {
|
|
28381
|
+
const creds = credentials();
|
|
28382
|
+
if (!creds) throw new Error("not signed in: run `runlog login`");
|
|
28383
|
+
const once = async (token) => {
|
|
28384
|
+
const response = await fetch(`${creds.api.replace(/\/$/, "")}${path}`, {
|
|
28385
|
+
method,
|
|
28386
|
+
headers: { authorization: `Bearer ${token}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
|
|
28387
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
28388
|
+
});
|
|
28389
|
+
const text = await response.text();
|
|
28390
|
+
let parsed = null;
|
|
28391
|
+
try {
|
|
28392
|
+
parsed = JSON.parse(text);
|
|
28393
|
+
} catch {
|
|
28394
|
+
throw new Error(`the API at ${creds.api} did not answer as expected (${response.status})`);
|
|
28395
|
+
}
|
|
28396
|
+
return { status: response.status, parsed };
|
|
28397
|
+
};
|
|
28398
|
+
let answer = await once(await bearer(creds));
|
|
28399
|
+
if (answer.status === 401 && creds.session) answer = await once(await bearer(creds, true));
|
|
28400
|
+
if (answer.status === 401) {
|
|
28401
|
+
throw new Error(creds.key ? "that key is not accepted; make a new one on your profile page and `runlog login --key` again" : "your sign-in has lapsed; run `runlog login` again");
|
|
28402
|
+
}
|
|
28403
|
+
if (answer.status >= 400) {
|
|
28404
|
+
const said = answer.parsed?.error;
|
|
28405
|
+
throw new Error(said ?? `the API said ${answer.status}`);
|
|
28406
|
+
}
|
|
28407
|
+
return answer.parsed;
|
|
28408
|
+
}
|
|
28409
|
+
async function greet(apiUrl) {
|
|
28410
|
+
const me = await api("GET", "/me");
|
|
28411
|
+
const who = me.profile?.name ?? me.profile?.email ?? "you";
|
|
28412
|
+
console.log(`signed in as ${who} at ${apiUrl}`);
|
|
28413
|
+
}
|
|
28414
|
+
async function cmdLogin(args, deps = realDeps) {
|
|
28415
|
+
const apiUrl = (flag(args, "--api") ?? process.env["RUNLOG_API"] ?? DEFAULT_API).replace(/\/$/, "");
|
|
28416
|
+
if (process.env["RUNLOG_API_KEY"]) {
|
|
28417
|
+
console.error("RUNLOG_API_KEY is set, so that is what every command will use; unset it to sign in as yourself");
|
|
28418
|
+
return 1;
|
|
28419
|
+
}
|
|
28420
|
+
if (args.includes("--key")) {
|
|
28421
|
+
console.log("Paste a command-line key from your profile page in Runlog. It is not shown as you type.");
|
|
28422
|
+
const key = await ask("key: ", true);
|
|
28423
|
+
if (!key.startsWith("rl_")) {
|
|
28424
|
+
console.error("that does not look like a Runlog key; they begin with rl_");
|
|
28425
|
+
return 1;
|
|
28426
|
+
}
|
|
28427
|
+
save({ api: apiUrl, key, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
28428
|
+
try {
|
|
28429
|
+
await greet(apiUrl);
|
|
28430
|
+
return 0;
|
|
28431
|
+
} catch (error61) {
|
|
28432
|
+
unlinkSync(credentialsPath());
|
|
28433
|
+
console.error(error61 instanceof Error ? error61.message : String(error61));
|
|
28434
|
+
return 1;
|
|
28435
|
+
}
|
|
28436
|
+
}
|
|
28437
|
+
try {
|
|
28438
|
+
const response = await deps.fetch(`${apiUrl}/auth/cli`, { headers: { accept: "application/json" } });
|
|
28439
|
+
const text = await response.text();
|
|
28440
|
+
let about = {};
|
|
28441
|
+
try {
|
|
28442
|
+
about = JSON.parse(text);
|
|
28443
|
+
} catch {
|
|
28444
|
+
throw new Error(`the API at ${apiUrl} did not answer as expected (${response.status})`);
|
|
28445
|
+
}
|
|
28446
|
+
if (!about.clientId) throw new Error(`the API at ${apiUrl} cannot sign in a terminal yet; use \`runlog login --key\` with a key from your profile page`);
|
|
28447
|
+
const session = await deviceFlow(about.clientId, about.issuer ?? WORKOS, deps);
|
|
28448
|
+
save({ api: apiUrl, session, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
28449
|
+
await greet(apiUrl);
|
|
28450
|
+
return 0;
|
|
28451
|
+
} catch (error61) {
|
|
28452
|
+
console.error(error61 instanceof Error ? error61.message : String(error61));
|
|
28453
|
+
return 1;
|
|
28454
|
+
}
|
|
28455
|
+
}
|
|
28456
|
+
function cmdLogout() {
|
|
28457
|
+
const creds = credentials();
|
|
28458
|
+
if (existsSync(credentialsPath())) {
|
|
28459
|
+
unlinkSync(credentialsPath());
|
|
28460
|
+
console.log(creds?.key ? "signed out; the key is still valid until you revoke it on your profile page" : "signed out");
|
|
28461
|
+
} else {
|
|
28462
|
+
console.log("not signed in");
|
|
28463
|
+
}
|
|
28464
|
+
return 0;
|
|
28465
|
+
}
|
|
28466
|
+
async function cmdWhoami() {
|
|
28467
|
+
try {
|
|
28468
|
+
const creds = credentials();
|
|
28469
|
+
const me = await api("GET", "/me");
|
|
28470
|
+
const claims = await api("GET", "/claims");
|
|
28471
|
+
const how = process.env["RUNLOG_API_KEY"] ? "with RUNLOG_API_KEY" : creds?.key ? "with a key from the profile page" : "from the browser";
|
|
28472
|
+
console.log(`${me.profile?.name ?? "(no name)"} <${me.profile?.email ?? "?"}> ${me.sub}`);
|
|
28473
|
+
console.log(`signed in ${how} at ${creds?.api ?? DEFAULT_API}`);
|
|
28474
|
+
console.log(claims.claims.length > 0 ? `claimed signing keys: ${claims.claims.map((c) => c.fingerprint).join(", ")}` : "no claimed signing keys yet: `runlog claim key.json`");
|
|
28475
|
+
return 0;
|
|
28476
|
+
} catch (error61) {
|
|
28477
|
+
console.error(error61 instanceof Error ? error61.message : String(error61));
|
|
28478
|
+
return 1;
|
|
28479
|
+
}
|
|
28480
|
+
}
|
|
28481
|
+
function readKey(path) {
|
|
28482
|
+
const key = JSON.parse(readFileSync(resolve(path), "utf8"));
|
|
28483
|
+
if (!key.privateKey || !key.publicKey) throw new Error(`${path} is not a runlog signing key`);
|
|
28484
|
+
return { publicKey: key.publicKey, privateKey: key.privateKey, fingerprint: key.fingerprint ?? "" };
|
|
28485
|
+
}
|
|
28486
|
+
async function cmdClaim(args) {
|
|
28487
|
+
const keyPath = args.filter((a) => !a.startsWith("-"))[0];
|
|
28488
|
+
if (!keyPath) {
|
|
28489
|
+
console.error("usage: runlog claim key.json");
|
|
28490
|
+
return 2;
|
|
28491
|
+
}
|
|
28492
|
+
try {
|
|
28493
|
+
const key = readKey(keyPath);
|
|
28494
|
+
const { nonce } = await api("POST", "/claims/nonce");
|
|
28495
|
+
const signature = await signBytes(new TextEncoder().encode(nonce), key.privateKey);
|
|
28496
|
+
const { claim: claim2 } = await api("POST", "/claims", { publicKey: key.publicKey, nonce, signature });
|
|
28497
|
+
console.log(`claimed ${claim2.fingerprint}${claim2.name ? ` as ${claim2.name}` : ""}`);
|
|
28498
|
+
console.log("packs signed with this key now show your name in the app");
|
|
28499
|
+
return 0;
|
|
28500
|
+
} catch (error61) {
|
|
28501
|
+
console.error(error61 instanceof Error ? error61.message : String(error61));
|
|
28502
|
+
return 1;
|
|
28503
|
+
}
|
|
28504
|
+
}
|
|
28505
|
+
async function requireClaimed(publicKey) {
|
|
28506
|
+
const fp = await fingerprint(publicKey);
|
|
28507
|
+
const { claims } = await api("GET", "/claims");
|
|
28508
|
+
if (!claims.some((c) => c.fingerprint === fp)) {
|
|
28509
|
+
throw new Error(`this key (${fp}) is not claimed by your account: run \`runlog claim key.json\` first, so the app can name you`);
|
|
28510
|
+
}
|
|
28511
|
+
}
|
|
28512
|
+
async function cmdPublish(args) {
|
|
28513
|
+
const input2 = args.filter((a) => !a.startsWith("-"))[0];
|
|
28514
|
+
if (!input2) {
|
|
28515
|
+
console.error("usage: runlog publish <pack.yaml|pack.json>");
|
|
28516
|
+
return 2;
|
|
28517
|
+
}
|
|
28518
|
+
try {
|
|
28519
|
+
const path = resolve(input2);
|
|
28520
|
+
const text = readFileSync(path, "utf8");
|
|
28521
|
+
const format = detectFormat(path);
|
|
28522
|
+
const doc = format === "json" ? JSON.parse(text) : import_yaml2.default.parse(text);
|
|
28523
|
+
const id = String(doc["id"] ?? "");
|
|
28524
|
+
if (!id) throw new Error("the pack has no id");
|
|
28525
|
+
const { createHash: createHash2 } = await import("node:crypto");
|
|
28526
|
+
const hash2 = createHash2("sha256").update(text).digest("hex").slice(0, 16);
|
|
28527
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
28528
|
+
await api("PUT", `/packs/${encodeURIComponent(id)}`, {
|
|
28529
|
+
title: String(doc["title"] ?? id),
|
|
28530
|
+
version: String(doc["version"] ?? ""),
|
|
28531
|
+
format,
|
|
28532
|
+
filename: input2.split(/[\\/]/).pop(),
|
|
28533
|
+
importedAt: at,
|
|
28534
|
+
updatedAt: at,
|
|
28535
|
+
hash: hash2,
|
|
28536
|
+
source: text
|
|
28537
|
+
});
|
|
28538
|
+
console.log(`published ${id} to your library; it reaches your devices on their next sync`);
|
|
28539
|
+
return 0;
|
|
28540
|
+
} catch (error61) {
|
|
28541
|
+
console.error(error61 instanceof Error ? error61.message : String(error61));
|
|
28542
|
+
return 1;
|
|
28543
|
+
}
|
|
28544
|
+
}
|
|
28545
|
+
function flag(args, name) {
|
|
28546
|
+
const at = args.indexOf(name);
|
|
28547
|
+
return at === -1 ? void 0 : args[at + 1];
|
|
28548
|
+
}
|
|
28549
|
+
|
|
28550
|
+
// packages/cli/src/sign.ts
|
|
28209
28551
|
var RED = "\x1B[31m";
|
|
28210
28552
|
var GREEN = "\x1B[32m";
|
|
28211
28553
|
var YELLOW = "\x1B[33m";
|
|
@@ -28213,13 +28555,13 @@ var DIM = "\x1B[2m";
|
|
|
28213
28555
|
var RESET = "\x1B[0m";
|
|
28214
28556
|
var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
28215
28557
|
var paint = (code, text) => useColor ? `${code}${text}${RESET}` : text;
|
|
28216
|
-
function
|
|
28558
|
+
function flag2(args, name) {
|
|
28217
28559
|
const at = args.indexOf(name);
|
|
28218
28560
|
return at === -1 ? void 0 : args[at + 1];
|
|
28219
28561
|
}
|
|
28220
28562
|
async function cmdKeygen(args) {
|
|
28221
|
-
const out =
|
|
28222
|
-
if (
|
|
28563
|
+
const out = resolve2(flag2(args, "-o") ?? flag2(args, "--out") ?? "runlog-key.json");
|
|
28564
|
+
if (existsSync2(out)) {
|
|
28223
28565
|
console.error(paint(RED, `refusing to overwrite ${out}`));
|
|
28224
28566
|
console.error(paint(DIM, " a signing key cannot be recovered; delete it yourself if you mean to"));
|
|
28225
28567
|
return 1;
|
|
@@ -28232,10 +28574,10 @@ async function cmdKeygen(args) {
|
|
|
28232
28574
|
fingerprint: await fingerprint(pair.publicKey),
|
|
28233
28575
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28234
28576
|
};
|
|
28235
|
-
|
|
28577
|
+
writeFileSync2(out, `${JSON.stringify(key, null, 2)}
|
|
28236
28578
|
`, "utf8");
|
|
28237
28579
|
try {
|
|
28238
|
-
|
|
28580
|
+
chmodSync2(out, 384);
|
|
28239
28581
|
} catch {
|
|
28240
28582
|
}
|
|
28241
28583
|
console.log(`wrote ${out}`);
|
|
@@ -28253,14 +28595,14 @@ async function cmdKeygen(args) {
|
|
|
28253
28595
|
}
|
|
28254
28596
|
async function cmdSign(args) {
|
|
28255
28597
|
const input2 = args.filter((a) => !a.startsWith("-"))[0];
|
|
28256
|
-
const keyPath =
|
|
28598
|
+
const keyPath = flag2(args, "--key") ?? flag2(args, "-k");
|
|
28257
28599
|
if (!input2 || !keyPath) {
|
|
28258
28600
|
console.error('usage: runlog sign <pack.yaml> --key runlog-key.json [--as "Your Name"]');
|
|
28259
28601
|
return 2;
|
|
28260
28602
|
}
|
|
28261
28603
|
let key;
|
|
28262
28604
|
try {
|
|
28263
|
-
key = JSON.parse(
|
|
28605
|
+
key = JSON.parse(readFileSync2(resolve2(keyPath), "utf8"));
|
|
28264
28606
|
} catch {
|
|
28265
28607
|
console.error(paint(RED, `could not read the key at ${keyPath}`));
|
|
28266
28608
|
return 1;
|
|
@@ -28269,16 +28611,22 @@ async function cmdSign(args) {
|
|
|
28269
28611
|
console.error(paint(RED, `${keyPath} has no private key in it`));
|
|
28270
28612
|
return 1;
|
|
28271
28613
|
}
|
|
28272
|
-
|
|
28273
|
-
|
|
28614
|
+
try {
|
|
28615
|
+
await requireClaimed(key.publicKey);
|
|
28616
|
+
} catch (error61) {
|
|
28617
|
+
console.error(paint(RED, error61 instanceof Error ? error61.message : String(error61)));
|
|
28618
|
+
return 1;
|
|
28619
|
+
}
|
|
28620
|
+
const path = resolve2(input2);
|
|
28621
|
+
const text = readFileSync2(path, "utf8");
|
|
28274
28622
|
const format = detectFormat(path);
|
|
28275
|
-
const document =
|
|
28276
|
-
const signature = await signPack(document, key.privateKey,
|
|
28623
|
+
const document = import_yaml3.default.parse(text);
|
|
28624
|
+
const signature = await signPack(document, key.privateKey, flag2(args, "--as"));
|
|
28277
28625
|
const signed = { ...document, signature };
|
|
28278
|
-
|
|
28626
|
+
writeFileSync2(
|
|
28279
28627
|
path,
|
|
28280
28628
|
format === "json" ? `${JSON.stringify(signed, null, 2)}
|
|
28281
|
-
` :
|
|
28629
|
+
` : import_yaml3.default.stringify(signed, { lineWidth: 90 }),
|
|
28282
28630
|
"utf8"
|
|
28283
28631
|
);
|
|
28284
28632
|
console.log(`${paint(GREEN, "signed")} ${input2}`);
|
|
@@ -28292,65 +28640,71 @@ async function cmdSign(args) {
|
|
|
28292
28640
|
}
|
|
28293
28641
|
async function cmdIssue(args) {
|
|
28294
28642
|
const input2 = args.filter((a) => !a.startsWith("-"))[0];
|
|
28295
|
-
const to =
|
|
28643
|
+
const to = flag2(args, "--to");
|
|
28296
28644
|
if (!input2 || !to) {
|
|
28297
28645
|
console.error(
|
|
28298
28646
|
'usage: runlog issue <pack.yaml> --to "Buyer Name" [--ref order-123] [--key key.json] [-o out.yaml]'
|
|
28299
28647
|
);
|
|
28300
28648
|
return 2;
|
|
28301
28649
|
}
|
|
28302
|
-
const path =
|
|
28303
|
-
const text =
|
|
28650
|
+
const path = resolve2(input2);
|
|
28651
|
+
const text = readFileSync2(path, "utf8");
|
|
28304
28652
|
const format = detectFormat(path);
|
|
28305
|
-
const document =
|
|
28653
|
+
const document = import_yaml3.default.parse(text);
|
|
28306
28654
|
const { signature: _old, ...base } = document;
|
|
28307
28655
|
const stamped = {
|
|
28308
28656
|
...base,
|
|
28309
28657
|
issue: {
|
|
28310
28658
|
to,
|
|
28311
|
-
...
|
|
28659
|
+
...flag2(args, "--ref") ? { reference: flag2(args, "--ref") } : {},
|
|
28312
28660
|
issuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28313
28661
|
}
|
|
28314
28662
|
};
|
|
28315
|
-
const keyPath =
|
|
28663
|
+
const keyPath = flag2(args, "--key") ?? flag2(args, "-k");
|
|
28316
28664
|
let fingerprintText = "";
|
|
28317
28665
|
if (keyPath) {
|
|
28318
28666
|
let key;
|
|
28319
28667
|
try {
|
|
28320
|
-
key = JSON.parse(
|
|
28668
|
+
key = JSON.parse(readFileSync2(resolve2(keyPath), "utf8"));
|
|
28321
28669
|
} catch {
|
|
28322
28670
|
console.error(paint(RED, `could not read the key at ${keyPath}`));
|
|
28323
28671
|
return 1;
|
|
28324
28672
|
}
|
|
28325
|
-
|
|
28673
|
+
try {
|
|
28674
|
+
await requireClaimed(key.publicKey);
|
|
28675
|
+
} catch (error61) {
|
|
28676
|
+
console.error(paint(RED, error61 instanceof Error ? error61.message : String(error61)));
|
|
28677
|
+
return 1;
|
|
28678
|
+
}
|
|
28679
|
+
stamped.signature = await signPack(stamped, key.privateKey, flag2(args, "--as"));
|
|
28326
28680
|
fingerprintText = key.fingerprint;
|
|
28327
28681
|
}
|
|
28328
28682
|
const sealing = args.includes("--seal");
|
|
28329
|
-
const licenseKey = sealing ?
|
|
28683
|
+
const licenseKey = sealing ? flag2(args, "--license") ?? generateLicenseKey() : null;
|
|
28330
28684
|
const extension = sealing ? "rlpack" : format;
|
|
28331
|
-
const out =
|
|
28332
|
-
|
|
28685
|
+
const out = resolve2(
|
|
28686
|
+
flag2(args, "-o") ?? flag2(args, "--out") ?? defaultIssueName(document, to, extension)
|
|
28333
28687
|
);
|
|
28334
|
-
if (
|
|
28688
|
+
if (existsSync2(out)) {
|
|
28335
28689
|
console.error(paint(RED, `refusing to overwrite ${out}`));
|
|
28336
28690
|
return 1;
|
|
28337
28691
|
}
|
|
28338
28692
|
if (licenseKey) {
|
|
28339
28693
|
const bytes2 = await seal(stamped, licenseKey, {
|
|
28340
|
-
...
|
|
28694
|
+
...flag2(args, "--ref") ? { ref: flag2(args, "--ref") } : {},
|
|
28341
28695
|
...typeof stamped.title === "string" ? { title: stamped.title } : {}
|
|
28342
28696
|
});
|
|
28343
|
-
|
|
28697
|
+
writeFileSync2(out, bytes2);
|
|
28344
28698
|
} else {
|
|
28345
|
-
|
|
28699
|
+
writeFileSync2(
|
|
28346
28700
|
out,
|
|
28347
28701
|
format === "json" ? `${JSON.stringify(stamped, null, 2)}
|
|
28348
|
-
` :
|
|
28702
|
+
` : import_yaml3.default.stringify(stamped, { lineWidth: 90 }),
|
|
28349
28703
|
"utf8"
|
|
28350
28704
|
);
|
|
28351
28705
|
}
|
|
28352
28706
|
console.log(`${paint(GREEN, "issued")} ${out}`);
|
|
28353
|
-
console.log(paint(DIM, ` to ${to}${
|
|
28707
|
+
console.log(paint(DIM, ` to ${to}${flag2(args, "--ref") ? ` \xB7 ref ${flag2(args, "--ref")}` : ""}`));
|
|
28354
28708
|
if (keyPath) {
|
|
28355
28709
|
console.log(paint(DIM, ` signed \xB7 fingerprint ${fingerprintText}`));
|
|
28356
28710
|
console.log(
|
|
@@ -28440,12 +28794,12 @@ function printDiagnostics(diagnostics, file2) {
|
|
|
28440
28794
|
console.error(` ${parts.join(", ")} in ${file2}`);
|
|
28441
28795
|
}
|
|
28442
28796
|
function read(path) {
|
|
28443
|
-
const abs =
|
|
28444
|
-
if (!
|
|
28797
|
+
const abs = resolve3(path);
|
|
28798
|
+
if (!existsSync3(abs)) {
|
|
28445
28799
|
console.error(paint2(RED2, `no such file: ${path}`));
|
|
28446
28800
|
process.exit(2);
|
|
28447
28801
|
}
|
|
28448
|
-
const text =
|
|
28802
|
+
const text = readFileSync3(abs, "utf8");
|
|
28449
28803
|
const result = loadPackText(text, detectFormat(abs));
|
|
28450
28804
|
return { text, pack: result.pack, diagnostics: result.diagnostics };
|
|
28451
28805
|
}
|
|
@@ -28476,7 +28830,7 @@ async function cmdValidate(args) {
|
|
|
28476
28830
|
console.log(
|
|
28477
28831
|
`${paint2(GREEN2, "ok")} ${paint2(BOLD, pack.title)} ${paint2(DIM2, `v${pack.version}`)} \u2014 ${tables} table${tables === 1 ? "" : "s"}, ${entries} entries, ${Object.keys(pack.modes).length} mode(s)` + (diagnostics.length ? paint2(YELLOW2, `, ${diagnostics.length} warning(s)`) : "")
|
|
28478
28832
|
);
|
|
28479
|
-
await reportSignature(
|
|
28833
|
+
await reportSignature(import_yaml4.default.parse(readFileSync3(resolve3(file2), "utf8")));
|
|
28480
28834
|
}
|
|
28481
28835
|
}
|
|
28482
28836
|
return worst;
|
|
@@ -28501,9 +28855,9 @@ function cmdBundle(args) {
|
|
|
28501
28855
|
const body = JSON.stringify(pack, null, 2);
|
|
28502
28856
|
const hash2 = createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
28503
28857
|
const bundled = { ...pack, bundledAt: (/* @__PURE__ */ new Date()).toISOString(), contentHash: hash2 };
|
|
28504
|
-
const out = outIndex >= 0 && args[outIndex + 1] ?
|
|
28505
|
-
|
|
28506
|
-
|
|
28858
|
+
const out = outIndex >= 0 && args[outIndex + 1] ? resolve3(args[outIndex + 1]) : resolve3(`${pack.id}-${pack.version}.pack.json`);
|
|
28859
|
+
mkdirSync2(dirname(out), { recursive: true });
|
|
28860
|
+
writeFileSync3(out, `${JSON.stringify(bundled, null, 2)}
|
|
28507
28861
|
`, "utf8");
|
|
28508
28862
|
console.log(`${paint2(GREEN2, "bundled")} ${out} ${paint2(DIM2, `sha256:${hash2}`)}`);
|
|
28509
28863
|
if (!pack.license.redistributable) {
|
|
@@ -28556,12 +28910,12 @@ function cmdTest(args) {
|
|
|
28556
28910
|
}
|
|
28557
28911
|
function cmdInit(args) {
|
|
28558
28912
|
const name = args.filter((a) => !a.startsWith("-"))[0] ?? "my-pack";
|
|
28559
|
-
const out =
|
|
28560
|
-
if (
|
|
28913
|
+
const out = resolve3(`${name}.yaml`);
|
|
28914
|
+
if (existsSync3(out)) {
|
|
28561
28915
|
console.error(paint2(RED2, `refusing to overwrite ${out}`));
|
|
28562
28916
|
return 1;
|
|
28563
28917
|
}
|
|
28564
|
-
|
|
28918
|
+
writeFileSync3(out, SKELETON.replaceAll("__NAME__", name), "utf8");
|
|
28565
28919
|
console.log(`${paint2(GREEN2, "created")} ${out}`);
|
|
28566
28920
|
console.log(paint2(DIM2, ` next: runlog validate ${name}.yaml`));
|
|
28567
28921
|
return 0;
|
|
@@ -28628,11 +28982,22 @@ usage:
|
|
|
28628
28982
|
runlog issue <pack> --to "Name" stamp a copy with a buyer's name and sign it
|
|
28629
28983
|
[--seal] \u2026and seal it, so it needs a license key to open
|
|
28630
28984
|
|
|
28631
|
-
--
|
|
28985
|
+
runlog login [--api URL] sign in: a code to confirm in your browser
|
|
28986
|
+
[--key] \u2026or paste a key from your profile page, for a machine with no browser
|
|
28987
|
+
runlog whoami who the command line is acting as
|
|
28988
|
+
runlog claim key.json prove a signing key is yours; the app then names you
|
|
28989
|
+
runlog publish <pack> put a pack in your library, on every device
|
|
28990
|
+
runlog logout forget the sign-in
|
|
28991
|
+
|
|
28992
|
+
--strict makes warnings fail, which is what you want in CI. In CI, set
|
|
28993
|
+
RUNLOG_API_KEY to a key from your profile page instead of running login;
|
|
28994
|
+
nobody is there to confirm a code.
|
|
28632
28995
|
|
|
28633
28996
|
Signing proves authorship. It does not restrict copying and cannot: the app
|
|
28634
28997
|
has to read every word of a pack to play it. What it gives you is that an
|
|
28635
|
-
altered copy can no longer claim to be yours
|
|
28998
|
+
altered copy can no longer claim to be yours \u2014 and, once the key is claimed
|
|
28999
|
+
by your account, that the app names you beside it. sign and issue refuse a
|
|
29000
|
+
key that is not claimed.
|
|
28636
29001
|
`;
|
|
28637
29002
|
async function main(argv) {
|
|
28638
29003
|
const [command, ...args] = argv;
|
|
@@ -28649,6 +29014,16 @@ async function main(argv) {
|
|
|
28649
29014
|
return cmdSign(args);
|
|
28650
29015
|
case "issue":
|
|
28651
29016
|
return cmdIssue(args);
|
|
29017
|
+
case "login":
|
|
29018
|
+
return cmdLogin(args);
|
|
29019
|
+
case "logout":
|
|
29020
|
+
return cmdLogout();
|
|
29021
|
+
case "whoami":
|
|
29022
|
+
return cmdWhoami();
|
|
29023
|
+
case "claim":
|
|
29024
|
+
return cmdClaim(args);
|
|
29025
|
+
case "publish":
|
|
29026
|
+
return cmdPublish(args);
|
|
28652
29027
|
case "init":
|
|
28653
29028
|
return cmdInit(args);
|
|
28654
29029
|
case "-h":
|