@scrthq/runlog 0.0.38 → 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 +188 -39
  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
@@ -28214,11 +28214,14 @@ import { resolve as resolve2 } from "node:path";
28214
28214
 
28215
28215
  // packages/cli/src/account.ts
28216
28216
  var import_yaml2 = __toESM(require_dist(), 1);
28217
+ import { spawn } from "node:child_process";
28217
28218
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
28218
28219
  import { homedir } from "node:os";
28219
28220
  import { join, resolve } from "node:path";
28220
28221
  import { createInterface } from "node:readline";
28221
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";
28222
28225
  function configDir() {
28223
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"));
28224
28227
  return base;
@@ -28229,12 +28232,31 @@ function credentials() {
28229
28232
  if (fromEnv) return { api: process.env["RUNLOG_API"] ?? DEFAULT_API, key: fromEnv, savedAt: "" };
28230
28233
  try {
28231
28234
  const raw = JSON.parse(readFileSync(credentialsPath(), "utf8"));
28232
- if (typeof raw.key !== "string" || typeof raw.api !== "string") return null;
28233
- return { api: raw.api, key: raw.key, savedAt: raw.savedAt ?? "" };
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;
28234
28246
  } catch {
28235
28247
  return null;
28236
28248
  }
28237
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
+ }
28238
28260
  async function ask(prompt, hidden = false) {
28239
28261
  const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
28240
28262
  return new Promise((done) => {
@@ -28260,60 +28282,182 @@ async function ask(prompt, hidden = false) {
28260
28282
  }
28261
28283
  });
28262
28284
  }
28263
- async function api(method, path, body) {
28264
- const creds = credentials();
28265
- if (!creds) throw new Error("not signed in: run `runlog login` with a key from your profile page");
28266
- const response = await fetch(`${creds.api.replace(/\/$/, "")}${path}`, {
28267
- method,
28268
- headers: { authorization: `Bearer ${creds.key}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
28269
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
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()
28270
28305
  });
28271
28306
  const text = await response.text();
28272
- let parsed = null;
28273
28307
  try {
28274
- parsed = JSON.parse(text);
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();
28275
28317
  } catch {
28276
- throw new Error(`the API at ${creds.api} did not answer as expected (${response.status})`);
28277
28318
  }
28278
- if (response.status === 401) throw new Error("that key is not accepted; make a new one on your profile page and `runlog login` again");
28279
- if (response.status >= 400) {
28280
- const said = parsed?.error;
28281
- throw new Error(said ?? `the API said ${response.status}`);
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}`);
28282
28406
  }
28283
- return parsed;
28407
+ return answer.parsed;
28284
28408
  }
28285
- async function cmdLogin(args) {
28286
- const apiUrl = flag(args, "--api") ?? process.env["RUNLOG_API"] ?? DEFAULT_API;
28287
- console.log("Paste a command-line key from your profile page in Runlog. It is not shown as you type.");
28288
- const key = await ask("key: ", true);
28289
- if (!key.startsWith("rl_")) {
28290
- console.error("that does not look like a Runlog key; they begin with rl_");
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");
28291
28418
  return 1;
28292
28419
  }
28293
- const dir = configDir();
28294
- mkdirSync(dir, { recursive: true });
28295
- const saved = { api: apiUrl, key, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
28296
- writeFileSync(credentialsPath(), `${JSON.stringify(saved, null, 2)}
28297
- `, "utf8");
28298
- try {
28299
- chmodSync(credentialsPath(), 384);
28300
- } catch {
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
+ }
28301
28436
  }
28302
28437
  try {
28303
- const me = await api("GET", "/me");
28304
- const who = me.profile?.name ?? me.profile?.email ?? "you";
28305
- console.log(`signed in as ${who} at ${apiUrl}`);
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);
28306
28450
  return 0;
28307
28451
  } catch (error61) {
28308
- unlinkSync(credentialsPath());
28309
28452
  console.error(error61 instanceof Error ? error61.message : String(error61));
28310
28453
  return 1;
28311
28454
  }
28312
28455
  }
28313
28456
  function cmdLogout() {
28457
+ const creds = credentials();
28314
28458
  if (existsSync(credentialsPath())) {
28315
28459
  unlinkSync(credentialsPath());
28316
- console.log("signed out; the key is still valid until you revoke it on your profile page");
28460
+ console.log(creds?.key ? "signed out; the key is still valid until you revoke it on your profile page" : "signed out");
28317
28461
  } else {
28318
28462
  console.log("not signed in");
28319
28463
  }
@@ -28321,9 +28465,12 @@ function cmdLogout() {
28321
28465
  }
28322
28466
  async function cmdWhoami() {
28323
28467
  try {
28468
+ const creds = credentials();
28324
28469
  const me = await api("GET", "/me");
28325
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";
28326
28472
  console.log(`${me.profile?.name ?? "(no name)"} <${me.profile?.email ?? "?"}> ${me.sub}`);
28473
+ console.log(`signed in ${how} at ${creds?.api ?? DEFAULT_API}`);
28327
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`");
28328
28475
  return 0;
28329
28476
  } catch (error61) {
@@ -28835,14 +28982,16 @@ usage:
28835
28982
  runlog issue <pack> --to "Name" stamp a copy with a buyer's name and sign it
28836
28983
  [--seal] \u2026and seal it, so it needs a license key to open
28837
28984
 
28838
- runlog login [--api URL] paste a key from your profile page, once
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
28839
28987
  runlog whoami who the command line is acting as
28840
28988
  runlog claim key.json prove a signing key is yours; the app then names you
28841
28989
  runlog publish <pack> put a pack in your library, on every device
28842
- runlog logout forget the saved key
28990
+ runlog logout forget the sign-in
28843
28991
 
28844
28992
  --strict makes warnings fail, which is what you want in CI. In CI, set
28845
- RUNLOG_API_KEY instead of running login.
28993
+ RUNLOG_API_KEY to a key from your profile page instead of running login;
28994
+ nobody is there to confirm a code.
28846
28995
 
28847
28996
  Signing proves authorship. It does not restrict copying and cannot: the app
28848
28997
  has to read every word of a pack to play it. What it gives you is that an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrthq/runlog",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "description": "Validate, lint, bundle and test Runlog rule packs.",
5
5
  "type": "module",
6
6
  "license": "MIT",