@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.
- package/README.md +3 -0
- package/dist/runlog.js +188 -39
- 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.
|
|
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
|
-
|
|
28264
|
-
|
|
28265
|
-
|
|
28266
|
-
|
|
28267
|
-
|
|
28268
|
-
|
|
28269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28279
|
-
|
|
28280
|
-
|
|
28281
|
-
|
|
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
|
|
28286
|
-
const
|
|
28287
|
-
|
|
28288
|
-
|
|
28289
|
-
|
|
28290
|
-
|
|
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
|
-
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
|
|
28297
|
-
|
|
28298
|
-
|
|
28299
|
-
|
|
28300
|
-
|
|
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
|
|
28304
|
-
const
|
|
28305
|
-
|
|
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]
|
|
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
|
|
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
|