rootcard 0.0.0-stage → 0.1.0

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/src/json.js ADDED
@@ -0,0 +1,280 @@
1
+ // Canonical JSON (SPEC §2): a strict parser and the RFC 8785 serialization of the rootcard subset of JSON.
2
+ // Values are plain objects, arrays, strings, safe integers and booleans. Parsed objects have no prototype.
3
+
4
+ import { fail } from "./errors.js";
5
+
6
+ export const MAX_SAFE = Number.MAX_SAFE_INTEGER;
7
+ export const MAX_DEPTH = 32;
8
+
9
+ const enc = new TextEncoder();
10
+ const dec = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
11
+
12
+ /** Whether `v` is a plain object (not an array). */
13
+ export function isObject(v) {
14
+ return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Uint8Array);
15
+ }
16
+
17
+ /** A new empty object without prototype. */
18
+ export function obj(entries) {
19
+ const o = Object.create(null);
20
+ if (entries) for (const [k, v] of Object.entries(entries)) o[k] = v;
21
+ return o;
22
+ }
23
+
24
+ /** Whether `name` sorts before `other` by UTF-16 code units (RFC 8785 §3.2.3). JS string order is exactly that. */
25
+ function sortedKeys(o) {
26
+ return Object.keys(o).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
27
+ }
28
+
29
+ /** Check that a value built in code is a rootcard JSON value ([CJ-1]–[CJ-5]). → bad_json */
30
+ export function validate(v, depth = 0) {
31
+ if (typeof v === "string") {
32
+ if (!v.isWellFormed()) fail("bad_json");
33
+ } else if (typeof v === "number") {
34
+ if (!Number.isSafeInteger(v) || Object.is(v, -0)) fail("bad_json");
35
+ } else if (typeof v === "boolean") {
36
+ // ok
37
+ } else if (Array.isArray(v)) {
38
+ if (depth >= MAX_DEPTH) fail("bad_json");
39
+ for (const x of v) validate(x, depth + 1);
40
+ } else if (isObject(v)) {
41
+ if (depth >= MAX_DEPTH) fail("bad_json");
42
+ for (const k of Object.keys(v)) {
43
+ if (!k.isWellFormed()) fail("bad_json");
44
+ if (v[k] === undefined) fail("bad_json");
45
+ validate(v[k], depth + 1);
46
+ }
47
+ } else {
48
+ fail("bad_json");
49
+ }
50
+ }
51
+
52
+ function writeString(s, out) {
53
+ out.push('"');
54
+ for (const ch of s) {
55
+ const c = ch.codePointAt(0);
56
+ if (ch === '"') out.push('\\"');
57
+ else if (ch === "\\") out.push("\\\\");
58
+ else if (c === 0x08) out.push("\\b");
59
+ else if (c === 0x09) out.push("\\t");
60
+ else if (c === 0x0a) out.push("\\n");
61
+ else if (c === 0x0c) out.push("\\f");
62
+ else if (c === 0x0d) out.push("\\r");
63
+ else if (c < 0x20) out.push("\\u00" + c.toString(16).padStart(2, "0"));
64
+ else out.push(ch);
65
+ }
66
+ out.push('"');
67
+ }
68
+
69
+ function write(v, out) {
70
+ if (typeof v === "string") writeString(v, out);
71
+ else if (typeof v === "number") out.push(String(v));
72
+ else if (typeof v === "boolean") out.push(v ? "true" : "false");
73
+ else if (Array.isArray(v)) {
74
+ out.push("[");
75
+ v.forEach((x, i) => {
76
+ if (i) out.push(",");
77
+ write(x, out);
78
+ });
79
+ out.push("]");
80
+ } else {
81
+ out.push("{");
82
+ sortedKeys(v).forEach((k, i) => {
83
+ if (i) out.push(",");
84
+ writeString(k, out);
85
+ out.push(":");
86
+ write(v[k], out);
87
+ });
88
+ out.push("}");
89
+ }
90
+ }
91
+
92
+ /** `canonical(x)` as a string ([CJ-9]). The value must be valid ([CJ-1]–[CJ-5]). */
93
+ export function canonicalString(v) {
94
+ validate(v);
95
+ const out = [];
96
+ write(v, out);
97
+ return out.join("");
98
+ }
99
+
100
+ /** `canonical(x)` as UTF-8 bytes ([CJ-9]). */
101
+ export function canonical(v) {
102
+ return enc.encode(canonicalString(v));
103
+ }
104
+
105
+ /** The object `o` without the member `name` (SPEC §0.1). */
106
+ export function without(o, ...names) {
107
+ const r = obj();
108
+ for (const k of Object.keys(o)) if (!names.includes(k)) r[k] = o[k];
109
+ return r;
110
+ }
111
+
112
+ /** Parse a JSON text strictly ([CJ-6]). → bad_json */
113
+ export function parseJson(bytes) {
114
+ if (typeof bytes === "string") bytes = enc.encode(bytes);
115
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) fail("bad_json");
116
+ let text;
117
+ try {
118
+ text = dec.decode(bytes);
119
+ } catch {
120
+ fail("bad_json");
121
+ }
122
+ const p = { s: text, i: 0 };
123
+ ws(p);
124
+ const v = value(p, 1);
125
+ ws(p);
126
+ if (p.i !== p.s.length) fail("bad_json");
127
+ return v;
128
+ }
129
+
130
+ /** Parse with a size limit ([CJ-8]). → too_large, bad_json */
131
+ export function parseLimited(bytes, limit) {
132
+ if (typeof bytes === "string") bytes = enc.encode(bytes);
133
+ if (bytes.length > limit + Math.floor(limit / 4)) fail("too_large");
134
+ const v = parseJson(bytes);
135
+ if (canonical(v).length > limit) fail("too_large");
136
+ return v;
137
+ }
138
+
139
+ function ws(p) {
140
+ while (p.i < p.s.length) {
141
+ const c = p.s.charCodeAt(p.i);
142
+ if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) p.i++;
143
+ else break;
144
+ }
145
+ }
146
+
147
+ function eat(p, lit) {
148
+ if (p.s.startsWith(lit, p.i)) p.i += lit.length;
149
+ else fail("bad_json");
150
+ }
151
+
152
+ function value(p, depth) {
153
+ const c = p.s[p.i];
154
+ if (c === "{") {
155
+ if (depth > MAX_DEPTH) fail("bad_json");
156
+ p.i++;
157
+ const o = obj();
158
+ ws(p);
159
+ if (p.s[p.i] === "}") {
160
+ p.i++;
161
+ return o;
162
+ }
163
+ for (;;) {
164
+ ws(p);
165
+ if (p.s[p.i] !== '"') fail("bad_json");
166
+ const k = string(p);
167
+ ws(p);
168
+ eat(p, ":");
169
+ ws(p);
170
+ const v = value(p, depth + 1);
171
+ if (Object.prototype.hasOwnProperty.call(o, k)) fail("bad_json");
172
+ o[k] = v;
173
+ ws(p);
174
+ if (p.s[p.i] === ",") p.i++;
175
+ else if (p.s[p.i] === "}") {
176
+ p.i++;
177
+ return o;
178
+ } else fail("bad_json");
179
+ }
180
+ }
181
+ if (c === "[") {
182
+ if (depth > MAX_DEPTH) fail("bad_json");
183
+ p.i++;
184
+ const a = [];
185
+ ws(p);
186
+ if (p.s[p.i] === "]") {
187
+ p.i++;
188
+ return a;
189
+ }
190
+ for (;;) {
191
+ ws(p);
192
+ a.push(value(p, depth + 1));
193
+ ws(p);
194
+ if (p.s[p.i] === ",") p.i++;
195
+ else if (p.s[p.i] === "]") {
196
+ p.i++;
197
+ return a;
198
+ } else fail("bad_json");
199
+ }
200
+ }
201
+ if (c === '"') return string(p);
202
+ if (c === "t") {
203
+ eat(p, "true");
204
+ return true;
205
+ }
206
+ if (c === "f") {
207
+ eat(p, "false");
208
+ return false;
209
+ }
210
+ if (c === "-" || (c >= "0" && c <= "9")) return number(p);
211
+ fail("bad_json");
212
+ }
213
+
214
+ function number(p) {
215
+ const m = /^-?(0|[1-9][0-9]*)/.exec(p.s.slice(p.i, p.i + 20));
216
+ if (!m) fail("bad_json");
217
+ const text = m[0];
218
+ p.i += text.length;
219
+ const next = p.s[p.i];
220
+ if (next === "." || next === "e" || next === "E" || (next >= "0" && next <= "9")) fail("bad_json");
221
+ if (text === "-0" || text.length > 17) fail("bad_json");
222
+ const n = Number(text);
223
+ if (!Number.isSafeInteger(n)) fail("bad_json");
224
+ return n;
225
+ }
226
+
227
+ function hex4(p) {
228
+ const h = p.s.slice(p.i, p.i + 4);
229
+ if (!/^[0-9a-fA-F]{4}$/.test(h)) fail("bad_json");
230
+ p.i += 4;
231
+ return parseInt(h, 16);
232
+ }
233
+
234
+ function string(p) {
235
+ p.i++;
236
+ let out = "";
237
+ for (;;) {
238
+ if (p.i >= p.s.length) fail("bad_json");
239
+ const c = p.s.charCodeAt(p.i);
240
+ if (c === 0x22) {
241
+ p.i++;
242
+ return out;
243
+ }
244
+ if (c === 0x5c) {
245
+ p.i++;
246
+ const e = p.s[p.i++];
247
+ if (e === '"') out += '"';
248
+ else if (e === "\\") out += "\\";
249
+ else if (e === "/") out += "/";
250
+ else if (e === "b") out += "\b";
251
+ else if (e === "f") out += "\f";
252
+ else if (e === "n") out += "\n";
253
+ else if (e === "r") out += "\r";
254
+ else if (e === "t") out += "\t";
255
+ else if (e === "u") {
256
+ const u = hex4(p);
257
+ if (u >= 0xd800 && u < 0xdc00) {
258
+ eat(p, "\\u");
259
+ const lo = hex4(p);
260
+ if (!(lo >= 0xdc00 && lo < 0xe000)) fail("bad_json");
261
+ out += String.fromCharCode(u, lo);
262
+ } else if (u >= 0xdc00 && u < 0xe000) {
263
+ fail("bad_json");
264
+ } else {
265
+ out += String.fromCharCode(u);
266
+ }
267
+ } else fail("bad_json");
268
+ continue;
269
+ }
270
+ if (c < 0x20) fail("bad_json");
271
+ // A surrogate pair in the decoded text is one scalar value; copy both halves.
272
+ if (c >= 0xd800 && c < 0xdc00) {
273
+ out += p.s.slice(p.i, p.i + 2);
274
+ p.i += 2;
275
+ } else {
276
+ out += p.s[p.i];
277
+ p.i++;
278
+ }
279
+ }
280
+ }
package/src/keys.js ADDED
@@ -0,0 +1,134 @@
1
+ // Keys in JSON (SPEC §3.3) and the schema helpers every structure uses ([CJ-11]–[CJ-13]).
2
+
3
+ import { fail } from "./errors.js";
4
+ import { isObject } from "./json.js";
5
+ import { base64urlDecode, base64urlEncode, base32Decode } from "./encoding.js";
6
+ import { ed25519PointOk } from "./crypto.js";
7
+
8
+ /** `"<type>:" + b64u(raw)` ([KEY-1]). */
9
+ export function formatKey(type, raw) {
10
+ return `${type}:${base64urlEncode(raw)}`;
11
+ }
12
+
13
+ /** Parse a key string of the required type ([KEY-1]–[KEY-3]). → bad_key */
14
+ export function parseKey(s, type) {
15
+ if (typeof s !== "string") fail("bad_key");
16
+ const i = s.indexOf(":");
17
+ if (i < 0 || s.slice(0, i) !== type) fail("bad_key");
18
+ let raw;
19
+ try {
20
+ raw = base64urlDecode(s.slice(i + 1));
21
+ } catch {
22
+ fail("bad_key");
23
+ }
24
+ if (raw.length !== 32) fail("bad_key");
25
+ if (type === "ed25519" && !ed25519PointOk(raw)) fail("bad_key");
26
+ return raw;
27
+ }
28
+
29
+ // --- schema helpers (each failure is bad_field unless stated otherwise) ---
30
+
31
+ export function object(v) {
32
+ if (!isObject(v)) fail("bad_field");
33
+ return v;
34
+ }
35
+
36
+ /** [CJ-12] */
37
+ export function version(m) {
38
+ const v = m.v;
39
+ if (!Object.hasOwn(m, "v") || typeof v !== "number") fail("bad_field");
40
+ if (v !== 1) fail("unsupported_version");
41
+ }
42
+
43
+ /** [CJ-11] */
44
+ export function members(m, required, optional = []) {
45
+ for (const k of required) if (!Object.hasOwn(m, k)) fail("bad_field");
46
+ for (const k of Object.keys(m)) if (!required.includes(k) && !optional.includes(k)) fail("bad_field");
47
+ }
48
+
49
+ export function get(m, k) {
50
+ if (!Object.hasOwn(m, k)) fail("bad_field");
51
+ return m[k];
52
+ }
53
+
54
+ export function string(m, k) {
55
+ const v = get(m, k);
56
+ if (typeof v !== "string") fail("bad_field");
57
+ return v;
58
+ }
59
+
60
+ export function int(m, k) {
61
+ const v = get(m, k);
62
+ if (typeof v !== "number") fail("bad_field");
63
+ return v;
64
+ }
65
+
66
+ export function intIn(m, k, lo, hi) {
67
+ const v = int(m, k);
68
+ if (v < lo || v > hi) fail("bad_field");
69
+ return v;
70
+ }
71
+
72
+ export function bool(m, k) {
73
+ const v = get(m, k);
74
+ if (typeof v !== "boolean") fail("bad_field");
75
+ return v;
76
+ }
77
+
78
+ export function member(m, k) {
79
+ return object(get(m, k));
80
+ }
81
+
82
+ export function array(m, k) {
83
+ const v = get(m, k);
84
+ if (!Array.isArray(v)) fail("bad_field");
85
+ return v;
86
+ }
87
+
88
+ /** A base64url string of exactly `n` bytes. */
89
+ export function bytes(m, k, n) {
90
+ const s = string(m, k);
91
+ let b;
92
+ try {
93
+ b = base64urlDecode(s);
94
+ } catch {
95
+ fail("bad_field");
96
+ }
97
+ if (b.length !== n) fail("bad_field");
98
+ return b;
99
+ }
100
+
101
+ /** A key string: not a string → bad_field, a bad key → bad_key ([CJ-13]). */
102
+ export function key(m, k, type) {
103
+ return parseKey(string(m, k), type);
104
+ }
105
+
106
+ /** A 64-byte signature ([KEY-4]). */
107
+ export function sig(m, k) {
108
+ return bytes(m, k, 64);
109
+ }
110
+
111
+ /** An account id in strict form ([ID-3]). */
112
+ export function account(m, k) {
113
+ const s = string(m, k);
114
+ checkAccountId(s);
115
+ return s;
116
+ }
117
+
118
+ /** [ID-3] → bad_field */
119
+ export function checkAccountId(s) {
120
+ try {
121
+ base32Decode(s, 32);
122
+ } catch {
123
+ fail("bad_field");
124
+ }
125
+ }
126
+
127
+ /** A version `{ seq, hash }` ([VER-1]). */
128
+ export function versionRef(v) {
129
+ const m = object(v);
130
+ members(m, ["seq", "hash"]);
131
+ intIn(m, "seq", 0, 999);
132
+ bytes(m, "hash", 32);
133
+ return { seq: m.seq, hash: m.hash };
134
+ }
package/src/labels.js ADDED
@@ -0,0 +1,17 @@
1
+ // Domain separation labels (SPEC §5).
2
+
3
+ /** Version of the wire and storage format described by the specification. */
4
+ export const FORMAT_VERSION = 1;
5
+
6
+ /** Prefix of every domain-separation label (`rootcard/<purpose>/v<n>`). */
7
+ export const LABEL_PREFIX = "rootcard/";
8
+
9
+ /**
10
+ * Build a domain-separation label, e.g. `label("id", 1) === "rootcard/id/v1"`.
11
+ * @param {string} purpose
12
+ * @param {number} [version]
13
+ * @returns {string}
14
+ */
15
+ export function label(purpose, version = FORMAT_VERSION) {
16
+ return `${LABEL_PREFIX}${purpose}/v${version}`;
17
+ }