@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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/runlog.js +422 -47
  3. 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 import_yaml3 = __toESM(require_dist(), 1);
7372
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2 } from "node:fs";
7373
- import { basename, dirname, resolve as resolve2 } from "node:path";
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 { readFileSync, writeFileSync, existsSync, chmodSync } from "node:fs";
28208
- import { resolve } from "node:path";
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 flag(args, name) {
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 = resolve(flag(args, "-o") ?? flag(args, "--out") ?? "runlog-key.json");
28222
- if (existsSync(out)) {
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
- writeFileSync(out, `${JSON.stringify(key, null, 2)}
28577
+ writeFileSync2(out, `${JSON.stringify(key, null, 2)}
28236
28578
  `, "utf8");
28237
28579
  try {
28238
- chmodSync(out, 384);
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 = flag(args, "--key") ?? flag(args, "-k");
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(readFileSync(resolve(keyPath), "utf8"));
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
- const path = resolve(input2);
28273
- const text = readFileSync(path, "utf8");
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 = import_yaml2.default.parse(text);
28276
- const signature = await signPack(document, key.privateKey, flag(args, "--as"));
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
- writeFileSync(
28626
+ writeFileSync2(
28279
28627
  path,
28280
28628
  format === "json" ? `${JSON.stringify(signed, null, 2)}
28281
- ` : import_yaml2.default.stringify(signed, { lineWidth: 90 }),
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 = flag(args, "--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 = resolve(input2);
28303
- const text = readFileSync(path, "utf8");
28650
+ const path = resolve2(input2);
28651
+ const text = readFileSync2(path, "utf8");
28304
28652
  const format = detectFormat(path);
28305
- const document = import_yaml2.default.parse(text);
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
- ...flag(args, "--ref") ? { reference: flag(args, "--ref") } : {},
28659
+ ...flag2(args, "--ref") ? { reference: flag2(args, "--ref") } : {},
28312
28660
  issuedAt: (/* @__PURE__ */ new Date()).toISOString()
28313
28661
  }
28314
28662
  };
28315
- const keyPath = flag(args, "--key") ?? flag(args, "-k");
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(readFileSync(resolve(keyPath), "utf8"));
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
- stamped.signature = await signPack(stamped, key.privateKey, flag(args, "--as"));
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 ? flag(args, "--license") ?? generateLicenseKey() : null;
28683
+ const licenseKey = sealing ? flag2(args, "--license") ?? generateLicenseKey() : null;
28330
28684
  const extension = sealing ? "rlpack" : format;
28331
- const out = resolve(
28332
- flag(args, "-o") ?? flag(args, "--out") ?? defaultIssueName(document, to, extension)
28685
+ const out = resolve2(
28686
+ flag2(args, "-o") ?? flag2(args, "--out") ?? defaultIssueName(document, to, extension)
28333
28687
  );
28334
- if (existsSync(out)) {
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
- ...flag(args, "--ref") ? { ref: flag(args, "--ref") } : {},
28694
+ ...flag2(args, "--ref") ? { ref: flag2(args, "--ref") } : {},
28341
28695
  ...typeof stamped.title === "string" ? { title: stamped.title } : {}
28342
28696
  });
28343
- writeFileSync(out, bytes2);
28697
+ writeFileSync2(out, bytes2);
28344
28698
  } else {
28345
- writeFileSync(
28699
+ writeFileSync2(
28346
28700
  out,
28347
28701
  format === "json" ? `${JSON.stringify(stamped, null, 2)}
28348
- ` : import_yaml2.default.stringify(stamped, { lineWidth: 90 }),
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}${flag(args, "--ref") ? ` \xB7 ref ${flag(args, "--ref")}` : ""}`));
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 = resolve2(path);
28444
- if (!existsSync2(abs)) {
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 = readFileSync2(abs, "utf8");
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(import_yaml3.default.parse(readFileSync2(resolve2(file2), "utf8")));
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] ? resolve2(args[outIndex + 1]) : resolve2(`${pack.id}-${pack.version}.pack.json`);
28505
- mkdirSync(dirname(out), { recursive: true });
28506
- writeFileSync2(out, `${JSON.stringify(bundled, null, 2)}
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 = resolve2(`${name}.yaml`);
28560
- if (existsSync2(out)) {
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
- writeFileSync2(out, SKELETON.replaceAll("__NAME__", name), "utf8");
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
- --strict makes warnings fail, which is what you want in CI.
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":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrthq/runlog",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "description": "Validate, lint, bundle and test Runlog rule packs.",
5
5
  "type": "module",
6
6
  "license": "MIT",