@scrthq/runlog 0.0.24

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 ADDED
@@ -0,0 +1,23 @@
1
+ # @scrthq/runlog
2
+
3
+ The command line for [Runlog](https://github.com/SCRT-HQ/runlog) packs:
4
+ validate, lint, bundle and test a rule pack; make a signing key, sign a
5
+ release, and issue sealed copies to buyers.
6
+
7
+ ```bash
8
+ npx @scrthq/runlog validate my-game.yaml --strict
9
+ npx @scrthq/runlog test my-game.yaml
10
+ npx @scrthq/runlog keygen -o my-key.json
11
+ npx @scrthq/runlog sign my-game.yaml --key my-key.json --as "Your Name"
12
+ npx @scrthq/runlog issue my-game.yaml --to "Buyer" --ref order-1 --key my-key.json --seal
13
+ ```
14
+
15
+ `npx @scrthq/runlog help` lists everything.
16
+
17
+ The same package is a library for a seller's own backend: `seal`, `open`,
18
+ `readHeader`, `isSealed` and `generateLicenseKey`, with no dependencies.
19
+
20
+ ```js
21
+ import { seal, generateLicenseKey } from "@scrthq/runlog";
22
+ ``` The authoring guide, the format
23
+ reference and the selling notes live in the repository's `docs/`.
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ // packages/container/src/base64url.ts
2
+ function toBase64Url(bytes2) {
3
+ let binary = "";
4
+ for (const byte of bytes2) binary += String.fromCharCode(byte);
5
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }
7
+ function fromBase64Url(text) {
8
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/");
9
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
10
+ const bytes2 = new Uint8Array(new ArrayBuffer(binary.length));
11
+ for (let i = 0; i < binary.length; i++) bytes2[i] = binary.charCodeAt(i);
12
+ return bytes2;
13
+ }
14
+
15
+ // packages/container/src/container.ts
16
+ var MAGIC = "RLPACK";
17
+ var KDF_ITERATIONS = 6e5;
18
+ var encoder = new TextEncoder();
19
+ var decoder = new TextDecoder();
20
+ function bytes(n) {
21
+ const out = new Uint8Array(new ArrayBuffer(n));
22
+ crypto.getRandomValues(out);
23
+ return out;
24
+ }
25
+ function generateLicenseKey() {
26
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
27
+ const raw = bytes(20);
28
+ const chars = [...raw].map((b) => alphabet[b % alphabet.length]).join("");
29
+ return (chars.match(/.{5}/g) ?? []).join("-");
30
+ }
31
+ var normalizeKey = (key) => key.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
32
+ async function deriveKey(licenseKey, salt, iterations) {
33
+ const material = await crypto.subtle.importKey(
34
+ "raw",
35
+ encoder.encode(normalizeKey(licenseKey)),
36
+ "PBKDF2",
37
+ false,
38
+ ["deriveKey"]
39
+ );
40
+ return crypto.subtle.deriveKey(
41
+ { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
42
+ material,
43
+ { name: "AES-GCM", length: 256 },
44
+ false,
45
+ ["encrypt", "decrypt"]
46
+ );
47
+ }
48
+ async function seal(document, licenseKey, extra = {}) {
49
+ const salt = bytes(16);
50
+ const iv = bytes(12);
51
+ const key = await deriveKey(licenseKey, salt, KDF_ITERATIONS);
52
+ const plaintext = encoder.encode(JSON.stringify(document));
53
+ const sealed = await crypto.subtle.encrypt(
54
+ { name: "AES-GCM", iv },
55
+ key,
56
+ plaintext
57
+ );
58
+ const header = {
59
+ v: 1,
60
+ alg: "aes-256-gcm",
61
+ kdf: "pbkdf2-sha256",
62
+ iterations: KDF_ITERATIONS,
63
+ salt: toBase64Url(salt),
64
+ iv: toBase64Url(iv),
65
+ ...extra.ref ? { ref: extra.ref } : {},
66
+ ...extra.title ? { title: extra.title } : {}
67
+ };
68
+ const headerBytes = encoder.encode(JSON.stringify(header));
69
+ const body = new Uint8Array(sealed);
70
+ const magic = encoder.encode(MAGIC);
71
+ const out = new Uint8Array(new ArrayBuffer(magic.length + 4 + headerBytes.length + body.length));
72
+ let at = 0;
73
+ out.set(magic, at);
74
+ at += magic.length;
75
+ new DataView(out.buffer).setUint32(at, headerBytes.length, false);
76
+ at += 4;
77
+ out.set(headerBytes, at);
78
+ at += headerBytes.length;
79
+ out.set(body, at);
80
+ return out;
81
+ }
82
+ function isSealed(data) {
83
+ const magic = encoder.encode(MAGIC);
84
+ if (data.length < magic.length) return false;
85
+ return magic.every((b, i) => data[i] === b);
86
+ }
87
+ function readHeader(data) {
88
+ if (!isSealed(data)) return null;
89
+ try {
90
+ const start = encoder.encode(MAGIC).length;
91
+ const length = new DataView(data.buffer, data.byteOffset).getUint32(start, false);
92
+ const header = JSON.parse(
93
+ decoder.decode(data.subarray(start + 4, start + 4 + length))
94
+ );
95
+ return header.v === 1 ? header : null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ async function open(data, licenseKey) {
101
+ const header = readHeader(data);
102
+ if (!header) {
103
+ return { ok: false, reason: "not-sealed", message: "this file is not a sealed pack" };
104
+ }
105
+ if (header.alg !== "aes-256-gcm" || header.kdf !== "pbkdf2-sha256") {
106
+ return {
107
+ ok: false,
108
+ reason: "unsupported",
109
+ message: "this copy was sealed by a newer version of the app"
110
+ };
111
+ }
112
+ try {
113
+ const start = encoder.encode(MAGIC).length;
114
+ const length = new DataView(data.buffer, data.byteOffset).getUint32(start, false);
115
+ const view = data.subarray(start + 4 + length);
116
+ const body = new Uint8Array(new ArrayBuffer(view.length));
117
+ body.set(view);
118
+ const key = await deriveKey(licenseKey, fromBase64Url(header.salt), header.iterations);
119
+ const plain = await crypto.subtle.decrypt(
120
+ { name: "AES-GCM", iv: fromBase64Url(header.iv) },
121
+ key,
122
+ body
123
+ );
124
+ return { ok: true, document: JSON.parse(decoder.decode(plain)) };
125
+ } catch {
126
+ return {
127
+ ok: false,
128
+ reason: "wrong-key",
129
+ message: "that license key does not open this copy"
130
+ };
131
+ }
132
+ }
133
+ export {
134
+ fromBase64Url,
135
+ generateLicenseKey,
136
+ isSealed,
137
+ open,
138
+ readHeader,
139
+ seal,
140
+ toBase64Url
141
+ };