@bitspark/archon-server 0.7.0 → 0.8.2
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/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/json.d.ts +101 -0
- package/dist/src/json.js +287 -0
- package/dist/src/json.js.map +1 -0
- package/dist/src/login.d.ts +84 -0
- package/dist/src/login.js +616 -0
- package/dist/src/login.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// archon-server — the service side of the archon login protocol (docs/login.md §4).
|
|
2
|
+
//
|
|
3
|
+
// A service adopts proof-of-possession sign-in by constructing a `Handler` and calling it from
|
|
4
|
+
// whatever already serves its requests. The handler is fetch-style —
|
|
5
|
+
// `(Request) => Promise<Response>` — so it mounts in Node, Deno, Bun, a Worker, or any
|
|
6
|
+
// framework that speaks the Web platform's own types, and this package depends on none of
|
|
7
|
+
// them. It opens no socket, starts no timer, persists nothing, and interprets no authority:
|
|
8
|
+
// that is ADR 0007 §B, and it is what makes the tier adoptable by a service that has already
|
|
9
|
+
// chosen its stack.
|
|
10
|
+
export * from "./login.js";
|
|
11
|
+
export { fromHex, MAX_BODY_BYTES, rfc3339, scanTopLevel, stripMount, toHex } from "./json.js";
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,EAAE;AACF,+FAA+F;AAC/F,qEAAqE;AACrE,uFAAuF;AACvF,0FAA0F;AAC1F,4FAA4F;AAC5F,6FAA6F;AAC7F,oBAAoB;AACpB,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Caps every request body. See the note on `MAX_BODY_BYTES` in the Rust lane: the authority
|
|
2
|
+
* payload is the only field with no natural size, and a law needing more than this should say
|
|
3
|
+
* so rather than have the handler guess high. */
|
|
4
|
+
export declare const MAX_BODY_BYTES: number;
|
|
5
|
+
/** The floor on an offer's code (§4.1): at least 16 bytes of the PROVER's entropy, spelled as
|
|
6
|
+
* lowercase hex — so 32 characters. The code is the address of an open offer and is
|
|
7
|
+
* confidential until the offer is taken; a short one is guessable. */
|
|
8
|
+
export declare const MIN_CODE_HEX = 32;
|
|
9
|
+
/**
|
|
10
|
+
* Throws on a code that is not what §4.1 says a code is: lowercase hex of even length, at
|
|
11
|
+
* least MIN_CODE_HEX characters. The offer route answers 400 to a refusal; begin's `offer`
|
|
12
|
+
* member and the read route answer 404, because a registered code is always well-formed and
|
|
13
|
+
* so a malformed one is unknown by construction — a stranger learns nothing from the
|
|
14
|
+
* difference (the same rule as an unknown versus an expired id).
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkCode(code: string): void;
|
|
17
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
18
|
+
/** Throws on anything that is not an even-length run of hex digits. Total by refusal, never
|
|
19
|
+
* by silently dropping a bad pair. */
|
|
20
|
+
export declare function fromHex(text: string): Uint8Array;
|
|
21
|
+
/**
|
|
22
|
+
* Parses a request body strictly: capped, an object, and with UNKNOWN FIELDS REFUSED.
|
|
23
|
+
*
|
|
24
|
+
* The refusal is deliberate and is the same rule the other two lanes apply. A caller sending
|
|
25
|
+
* a field this package does not know is speaking a different version of the protocol, and
|
|
26
|
+
* half-reading their message is how two peers come to disagree about what was agreed.
|
|
27
|
+
* Refusing says so at the door — and in particular refuses an `audience` field, which is
|
|
28
|
+
* exactly the value review Finding 1 forbids trusting.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseBody(raw: ArrayBuffer, allowed: readonly string[]): ParsedBody;
|
|
31
|
+
/** A parsed body, the exact text it was parsed from, and each top-level member's source span. */
|
|
32
|
+
export interface ParsedBody {
|
|
33
|
+
text: string;
|
|
34
|
+
body: Record<string, unknown>;
|
|
35
|
+
/** Member name to the exact source text of its value. */
|
|
36
|
+
members: Map<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Refuses what the CLI could not display faithfully: an empty entry, a control character
|
|
40
|
+
* (§3.1), or text that is not valid UTF-8.
|
|
41
|
+
*
|
|
42
|
+
* Checked HERE as well as in the scheme, and that is not redundancy for its own sake:
|
|
43
|
+
* checking at the door means a request that could lie on screen never exists to be shown.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkScopeEntry(entry: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Formats a Unix timestamp as RFC 3339 UTC, which is what §4's `expires` field carries.
|
|
48
|
+
*
|
|
49
|
+
* The milliseconds are dropped so all three lanes emit the SAME spelling: Go and Rust format
|
|
50
|
+
* whole seconds, and a `.000Z` here would be a difference visible to any client that compared
|
|
51
|
+
* two services.
|
|
52
|
+
*/
|
|
53
|
+
export declare function rfc3339(unixSeconds: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Strips `mount` from the front of a request path, or returns undefined if the path is not
|
|
56
|
+
* under it.
|
|
57
|
+
*
|
|
58
|
+
* EXPORTED SO IT CAN BE TESTED DIRECTLY, and that is not an accident of style. Through the
|
|
59
|
+
* handler this rule is nearly invisible: a path that should have been refused here goes on to
|
|
60
|
+
* miss the id lookup and produce the same 404 anyway, so a test driving it through `handle`
|
|
61
|
+
* passes whether the rule is present or not. The Rust lane has the same rule in its adapter
|
|
62
|
+
* and tests it the same way, on the function itself.
|
|
63
|
+
*
|
|
64
|
+
* The remainder must be empty or begin with `/`, so a handler mounted at `/api/login` does not
|
|
65
|
+
* also claim `/api/loginX` — a path that belongs to whatever else the service routes.
|
|
66
|
+
*
|
|
67
|
+
* Nothing is percent-decoded: ids are hex and never need it, and decoding is how a `%2f` turns
|
|
68
|
+
* into a path separator that routing has already finished looking at.
|
|
69
|
+
*/
|
|
70
|
+
export declare function stripMount(pathname: string, mount: string): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The exact source text of the top-level `authority` member of `text`, or undefined if the
|
|
73
|
+
* body has no such member.
|
|
74
|
+
*
|
|
75
|
+
* THE AUTHORITY IS OPAQUE BYTES (ADR 0007 §B), and `JSON.parse` followed by `JSON.stringify`
|
|
76
|
+
* is NOT a round trip. `9007199254740993` comes back `9007199254740992` — it does not fit a
|
|
77
|
+
* double. `1.10` comes back `1.1`. A `\\u00e9` escape comes back as the character. Key order is
|
|
78
|
+
* whatever the parser kept. A law that signs its payload, or carries a 64-bit id, would then
|
|
79
|
+
* be handed something the CLI never sent, and the browser something the law never saw — and
|
|
80
|
+
* the three server lanes would each produce a different payload from one answer.
|
|
81
|
+
*
|
|
82
|
+
* `JSON.parse`'s reviver cannot do this: its `context.source` is populated for primitives
|
|
83
|
+
* only, so an object- or array-valued authority would have no source to read. Hence the
|
|
84
|
+
* scanner. It runs on text that has ALREADY parsed, so it is measuring, not validating.
|
|
85
|
+
*
|
|
86
|
+
* A repeated top-level key is refused rather than resolved. `JSON.parse` keeps the last one,
|
|
87
|
+
* so a body with two `authority` members would have two candidate spans and no reason to
|
|
88
|
+
* prefer either; refusing is the only answer that cannot be argued with. (The Rust lane
|
|
89
|
+
* refuses duplicates already, via serde; Go accepts them and keeps the last.)
|
|
90
|
+
*/
|
|
91
|
+
export declare function authoritySpan(text: string): string | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Every top-level member of `text`, name to the exact source text of its value.
|
|
94
|
+
*
|
|
95
|
+
* A repeated name is REFUSED rather than resolved. `JSON.parse` keeps the last one, so a body
|
|
96
|
+
* with two `authority` members would have two candidate spans and no reason to prefer either;
|
|
97
|
+
* refusing is the only answer that cannot be argued with. That reasoning is about `authority`,
|
|
98
|
+
* but the refusal is applied to every member, because "which of the two did you mean" has no
|
|
99
|
+
* good answer for any of them.
|
|
100
|
+
*/
|
|
101
|
+
export declare function scanTopLevel(text: string): Map<string, string>;
|
package/dist/src/json.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Hex, strict body parsing and the door checks. Small on purpose: the wire format is §4's
|
|
2
|
+
// and is spelled where the routes are, not abstracted behind builders that would hide it.
|
|
3
|
+
/** Caps every request body. See the note on `MAX_BODY_BYTES` in the Rust lane: the authority
|
|
4
|
+
* payload is the only field with no natural size, and a law needing more than this should say
|
|
5
|
+
* so rather than have the handler guess high. */
|
|
6
|
+
export const MAX_BODY_BYTES = 64 * 1024;
|
|
7
|
+
/** The floor on an offer's code (§4.1): at least 16 bytes of the PROVER's entropy, spelled as
|
|
8
|
+
* lowercase hex — so 32 characters. The code is the address of an open offer and is
|
|
9
|
+
* confidential until the offer is taken; a short one is guessable. */
|
|
10
|
+
export const MIN_CODE_HEX = 32;
|
|
11
|
+
/**
|
|
12
|
+
* Throws on a code that is not what §4.1 says a code is: lowercase hex of even length, at
|
|
13
|
+
* least MIN_CODE_HEX characters. The offer route answers 400 to a refusal; begin's `offer`
|
|
14
|
+
* member and the read route answer 404, because a registered code is always well-formed and
|
|
15
|
+
* so a malformed one is unknown by construction — a stranger learns nothing from the
|
|
16
|
+
* difference (the same rule as an unknown versus an expired id).
|
|
17
|
+
*/
|
|
18
|
+
export function checkCode(code) {
|
|
19
|
+
if (code.length < MIN_CODE_HEX || code.length % 2 !== 0) {
|
|
20
|
+
throw new Error(`a code is lowercase hex of even length, at least ${MIN_CODE_HEX} characters`);
|
|
21
|
+
}
|
|
22
|
+
if (!/^[0-9a-f]*$/.test(code))
|
|
23
|
+
throw new Error("a code is lowercase hex");
|
|
24
|
+
}
|
|
25
|
+
export function toHex(bytes) {
|
|
26
|
+
let out = "";
|
|
27
|
+
for (const b of bytes)
|
|
28
|
+
out += b.toString(16).padStart(2, "0");
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** Throws on anything that is not an even-length run of hex digits. Total by refusal, never
|
|
32
|
+
* by silently dropping a bad pair. */
|
|
33
|
+
export function fromHex(text) {
|
|
34
|
+
if (text.length % 2 !== 0)
|
|
35
|
+
throw new Error(`hex has odd length (${text.length})`);
|
|
36
|
+
if (!/^[0-9a-fA-F]*$/.test(text))
|
|
37
|
+
throw new Error("hex carries a non-hex character");
|
|
38
|
+
const out = new Uint8Array(text.length / 2);
|
|
39
|
+
for (let i = 0; i < out.length; i++)
|
|
40
|
+
out[i] = Number.parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parses a request body strictly: capped, an object, and with UNKNOWN FIELDS REFUSED.
|
|
45
|
+
*
|
|
46
|
+
* The refusal is deliberate and is the same rule the other two lanes apply. A caller sending
|
|
47
|
+
* a field this package does not know is speaking a different version of the protocol, and
|
|
48
|
+
* half-reading their message is how two peers come to disagree about what was agreed.
|
|
49
|
+
* Refusing says so at the door — and in particular refuses an `audience` field, which is
|
|
50
|
+
* exactly the value review Finding 1 forbids trusting.
|
|
51
|
+
*/
|
|
52
|
+
export function parseBody(raw, allowed) {
|
|
53
|
+
if (raw.byteLength > MAX_BODY_BYTES) {
|
|
54
|
+
throw new Error(`body is ${raw.byteLength} bytes, over the ${MAX_BODY_BYTES} cap`);
|
|
55
|
+
}
|
|
56
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
57
|
+
const value = JSON.parse(text);
|
|
58
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
59
|
+
throw new Error("body is not a JSON object");
|
|
60
|
+
}
|
|
61
|
+
const body = value;
|
|
62
|
+
for (const key of Object.keys(body)) {
|
|
63
|
+
if (!allowed.includes(key))
|
|
64
|
+
throw new Error(`body carries the unknown field ${JSON.stringify(key)}`);
|
|
65
|
+
}
|
|
66
|
+
// Scanned once, HERE, so every route refuses a repeated member — not only the one that
|
|
67
|
+
// happens to want a span. go refuses duplicates in its shared decoder and serde refuses
|
|
68
|
+
// them for a struct; a lane that refused them on `answer` alone would be the same
|
|
69
|
+
// three-lanes-three-answers defect in miniature.
|
|
70
|
+
const members = scanTopLevel(text);
|
|
71
|
+
return { text, body, members };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* An unpaired surrogate — a string JavaScript will hold happily and UTF-8 cannot represent.
|
|
75
|
+
*
|
|
76
|
+
* Go and TypeScript both need this check; Rust does not, because a `String` cannot hold one.
|
|
77
|
+
* That asymmetry is why it is spelled out here rather than assumed: the scheme refuses these
|
|
78
|
+
* at `loginBinding`, but that happens when a proof is MADE, which is after the person has
|
|
79
|
+
* already read the statement.
|
|
80
|
+
*/
|
|
81
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
82
|
+
/**
|
|
83
|
+
* Refuses what the CLI could not display faithfully: an empty entry, a control character
|
|
84
|
+
* (§3.1), or text that is not valid UTF-8.
|
|
85
|
+
*
|
|
86
|
+
* Checked HERE as well as in the scheme, and that is not redundancy for its own sake:
|
|
87
|
+
* checking at the door means a request that could lie on screen never exists to be shown.
|
|
88
|
+
*/
|
|
89
|
+
export function checkScopeEntry(entry) {
|
|
90
|
+
if (entry.length === 0)
|
|
91
|
+
throw new Error("a scope entry is empty");
|
|
92
|
+
if (LONE_SURROGATE.test(entry))
|
|
93
|
+
throw new Error("a scope entry is not valid UTF-8");
|
|
94
|
+
for (const ch of entry) {
|
|
95
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
96
|
+
if (code < 0x20 || code === 0x7f) {
|
|
97
|
+
throw new Error(`a scope entry carries a control character (U+${code.toString(16).padStart(4, "0")})`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Formats a Unix timestamp as RFC 3339 UTC, which is what §4's `expires` field carries.
|
|
103
|
+
*
|
|
104
|
+
* The milliseconds are dropped so all three lanes emit the SAME spelling: Go and Rust format
|
|
105
|
+
* whole seconds, and a `.000Z` here would be a difference visible to any client that compared
|
|
106
|
+
* two services.
|
|
107
|
+
*/
|
|
108
|
+
export function rfc3339(unixSeconds) {
|
|
109
|
+
return new Date(unixSeconds * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Strips `mount` from the front of a request path, or returns undefined if the path is not
|
|
113
|
+
* under it.
|
|
114
|
+
*
|
|
115
|
+
* EXPORTED SO IT CAN BE TESTED DIRECTLY, and that is not an accident of style. Through the
|
|
116
|
+
* handler this rule is nearly invisible: a path that should have been refused here goes on to
|
|
117
|
+
* miss the id lookup and produce the same 404 anyway, so a test driving it through `handle`
|
|
118
|
+
* passes whether the rule is present or not. The Rust lane has the same rule in its adapter
|
|
119
|
+
* and tests it the same way, on the function itself.
|
|
120
|
+
*
|
|
121
|
+
* The remainder must be empty or begin with `/`, so a handler mounted at `/api/login` does not
|
|
122
|
+
* also claim `/api/loginX` — a path that belongs to whatever else the service routes.
|
|
123
|
+
*
|
|
124
|
+
* Nothing is percent-decoded: ids are hex and never need it, and decoding is how a `%2f` turns
|
|
125
|
+
* into a path separator that routing has already finished looking at.
|
|
126
|
+
*/
|
|
127
|
+
export function stripMount(pathname, mount) {
|
|
128
|
+
const prefix = mount.replace(/\/+$/, "");
|
|
129
|
+
if (prefix.length === 0)
|
|
130
|
+
return pathname;
|
|
131
|
+
if (!pathname.startsWith(prefix))
|
|
132
|
+
return undefined;
|
|
133
|
+
const rest = pathname.slice(prefix.length);
|
|
134
|
+
if (rest.length > 0 && !rest.startsWith("/"))
|
|
135
|
+
return undefined;
|
|
136
|
+
return rest;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The exact source text of the top-level `authority` member of `text`, or undefined if the
|
|
140
|
+
* body has no such member.
|
|
141
|
+
*
|
|
142
|
+
* THE AUTHORITY IS OPAQUE BYTES (ADR 0007 §B), and `JSON.parse` followed by `JSON.stringify`
|
|
143
|
+
* is NOT a round trip. `9007199254740993` comes back `9007199254740992` — it does not fit a
|
|
144
|
+
* double. `1.10` comes back `1.1`. A `\\u00e9` escape comes back as the character. Key order is
|
|
145
|
+
* whatever the parser kept. A law that signs its payload, or carries a 64-bit id, would then
|
|
146
|
+
* be handed something the CLI never sent, and the browser something the law never saw — and
|
|
147
|
+
* the three server lanes would each produce a different payload from one answer.
|
|
148
|
+
*
|
|
149
|
+
* `JSON.parse`'s reviver cannot do this: its `context.source` is populated for primitives
|
|
150
|
+
* only, so an object- or array-valued authority would have no source to read. Hence the
|
|
151
|
+
* scanner. It runs on text that has ALREADY parsed, so it is measuring, not validating.
|
|
152
|
+
*
|
|
153
|
+
* A repeated top-level key is refused rather than resolved. `JSON.parse` keeps the last one,
|
|
154
|
+
* so a body with two `authority` members would have two candidate spans and no reason to
|
|
155
|
+
* prefer either; refusing is the only answer that cannot be argued with. (The Rust lane
|
|
156
|
+
* refuses duplicates already, via serde; Go accepts them and keeps the last.)
|
|
157
|
+
*/
|
|
158
|
+
export function authoritySpan(text) {
|
|
159
|
+
return scanTopLevel(text).get("authority");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Every top-level member of `text`, name to the exact source text of its value.
|
|
163
|
+
*
|
|
164
|
+
* A repeated name is REFUSED rather than resolved. `JSON.parse` keeps the last one, so a body
|
|
165
|
+
* with two `authority` members would have two candidate spans and no reason to prefer either;
|
|
166
|
+
* refusing is the only answer that cannot be argued with. That reasoning is about `authority`,
|
|
167
|
+
* but the refusal is applied to every member, because "which of the two did you mean" has no
|
|
168
|
+
* good answer for any of them.
|
|
169
|
+
*/
|
|
170
|
+
export function scanTopLevel(text) {
|
|
171
|
+
const members = new Map();
|
|
172
|
+
const cur = new Cursor(text);
|
|
173
|
+
cur.ws();
|
|
174
|
+
cur.expect("{");
|
|
175
|
+
cur.ws();
|
|
176
|
+
if (cur.peek() === "}") {
|
|
177
|
+
cur.take();
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
for (;;) {
|
|
181
|
+
cur.ws();
|
|
182
|
+
const key = cur.str();
|
|
183
|
+
if (members.has(key))
|
|
184
|
+
throw new Error(`the body repeats the top-level key ${JSON.stringify(key)}`);
|
|
185
|
+
cur.ws();
|
|
186
|
+
cur.expect(":");
|
|
187
|
+
cur.ws();
|
|
188
|
+
const start = cur.i;
|
|
189
|
+
cur.skip();
|
|
190
|
+
members.set(key, text.slice(start, cur.i));
|
|
191
|
+
cur.ws();
|
|
192
|
+
const next = cur.take();
|
|
193
|
+
if (next === ",")
|
|
194
|
+
continue;
|
|
195
|
+
if (next === "}")
|
|
196
|
+
break;
|
|
197
|
+
throw new Error("expected , or } after a member");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
cur.ws();
|
|
201
|
+
if (cur.peek() !== undefined)
|
|
202
|
+
throw new Error("trailing content after the body");
|
|
203
|
+
return members;
|
|
204
|
+
}
|
|
205
|
+
/** Just enough JSON to walk a top-level object and measure one member's value. */
|
|
206
|
+
class Cursor {
|
|
207
|
+
text;
|
|
208
|
+
i = 0;
|
|
209
|
+
constructor(text) {
|
|
210
|
+
this.text = text;
|
|
211
|
+
}
|
|
212
|
+
ws() {
|
|
213
|
+
while (this.i < this.text.length && " \t\n\r".includes(this.text[this.i]))
|
|
214
|
+
this.i += 1;
|
|
215
|
+
}
|
|
216
|
+
peek() {
|
|
217
|
+
return this.text[this.i];
|
|
218
|
+
}
|
|
219
|
+
take() {
|
|
220
|
+
const c = this.text[this.i];
|
|
221
|
+
if (c === undefined)
|
|
222
|
+
throw new Error("unexpected end of body");
|
|
223
|
+
this.i += 1;
|
|
224
|
+
return c;
|
|
225
|
+
}
|
|
226
|
+
expect(c) {
|
|
227
|
+
if (this.take() !== c)
|
|
228
|
+
throw new Error(`expected ${c}`);
|
|
229
|
+
}
|
|
230
|
+
/** Consumes a string literal and returns its value. Used for member names only — the
|
|
231
|
+
* authority is never decoded. */
|
|
232
|
+
str() {
|
|
233
|
+
const start = this.i;
|
|
234
|
+
this.expect('"');
|
|
235
|
+
for (;;) {
|
|
236
|
+
const c = this.take();
|
|
237
|
+
if (c === '"')
|
|
238
|
+
return JSON.parse(this.text.slice(start, this.i));
|
|
239
|
+
// Skip whatever the backslash escapes. For \\uXXXX the four hex digits that follow are
|
|
240
|
+
// ordinary characters — none of them can be a quote or a backslash — so no special case.
|
|
241
|
+
if (c === "\\")
|
|
242
|
+
this.i += 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Advances past one value of any type WITHOUT interpreting it. */
|
|
246
|
+
skip() {
|
|
247
|
+
this.ws();
|
|
248
|
+
const c = this.peek();
|
|
249
|
+
if (c === '"') {
|
|
250
|
+
this.str();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (c === "{" || c === "[") {
|
|
254
|
+
const close = c === "{" ? "}" : "]";
|
|
255
|
+
this.take();
|
|
256
|
+
this.ws();
|
|
257
|
+
if (this.peek() === close) {
|
|
258
|
+
this.take();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (;;) {
|
|
262
|
+
this.ws();
|
|
263
|
+
if (close === "}") {
|
|
264
|
+
this.str();
|
|
265
|
+
this.ws();
|
|
266
|
+
this.expect(":");
|
|
267
|
+
}
|
|
268
|
+
this.skip();
|
|
269
|
+
this.ws();
|
|
270
|
+
const next = this.take();
|
|
271
|
+
if (next === ",")
|
|
272
|
+
continue;
|
|
273
|
+
if (next === close)
|
|
274
|
+
return;
|
|
275
|
+
throw new Error(`expected , or ${close}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// A number, true, false or null: up to the next structural character.
|
|
279
|
+
const start = this.i;
|
|
280
|
+
while (this.i < this.text.length && !",}] \t\n\r".includes(this.text[this.i])) {
|
|
281
|
+
this.i += 1;
|
|
282
|
+
}
|
|
283
|
+
if (this.i === start)
|
|
284
|
+
throw new Error("expected a value");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/json.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** The header carrying the browser's collect proof. Exported because a browser client has to
|
|
2
|
+
* spell it, and one spelling in one place is how the two halves stay agreed. */
|
|
3
|
+
export declare const COLLECT_HEADER = "archon-collect";
|
|
4
|
+
/** Five minutes, from §4. */
|
|
5
|
+
export declare const DEFAULT_TTL_SECONDS = 300;
|
|
6
|
+
/** Five seconds, from §4. */
|
|
7
|
+
export declare const DEFAULT_INTERVAL_SECONDS = 5;
|
|
8
|
+
/**
|
|
9
|
+
* Interprets the authority payload — the ONLY place one is interpreted, and archon ships no
|
|
10
|
+
* implementation. THROWING refuses the answer with `403 invalid_grant` and stores nothing.
|
|
11
|
+
* Omitting it means the proof alone suffices, which is right for a service whose law needs
|
|
12
|
+
* nothing beyond "this key holder was here and approved this scope".
|
|
13
|
+
*
|
|
14
|
+
* It may be async, and the handler awaits it. See the note on `answer` for why every check it
|
|
15
|
+
* depends on is repeated afterwards.
|
|
16
|
+
*/
|
|
17
|
+
export type AdmitAuthority = (browser: Uint8Array, principal: Uint8Array, authority: Uint8Array) => void | Promise<void>;
|
|
18
|
+
/** Seconds since the Unix epoch. A config field rather than a call to `Date.now()`, which is
|
|
19
|
+
* the sdk's rule and the reason the suite never sleeps. */
|
|
20
|
+
export type Clock = () => number;
|
|
21
|
+
/** Fills `n` bytes. A config field for the same reason as `Clock`: a test predicts it,
|
|
22
|
+
* production draws from the platform's CSPRNG. */
|
|
23
|
+
export type Entropy = (n: number) => Uint8Array;
|
|
24
|
+
/** What a service supplies. Only the audience is required. */
|
|
25
|
+
export interface Config {
|
|
26
|
+
/** The service's public identity, and a fixed point of §2.1 — see the constructor. */
|
|
27
|
+
audience: string;
|
|
28
|
+
/** The path prefix the routes live under, e.g. `/api/login`. Default: none. */
|
|
29
|
+
mount?: string;
|
|
30
|
+
admit?: AdmitAuthority;
|
|
31
|
+
/** The address a person opens to finish a login the CLI started (§4.1 — the offers form).
|
|
32
|
+
* Optional. When set, an offer's response carries `<page>#<code>`: the code travels in the
|
|
33
|
+
* FRAGMENT, which a browser never sends to any server, so it reaches the page's script and
|
|
34
|
+
* no log. A page that already carries a fragment is refused by the constructor. The CLI
|
|
35
|
+
* prints the address and never opens it (§4.1 rule 1). */
|
|
36
|
+
page?: string;
|
|
37
|
+
/** How long a pending login lives — and how long an open offer lives. */
|
|
38
|
+
ttlSeconds?: number;
|
|
39
|
+
intervalSeconds?: number;
|
|
40
|
+
clock?: Clock;
|
|
41
|
+
entropy?: Entropy;
|
|
42
|
+
}
|
|
43
|
+
export declare class Handler {
|
|
44
|
+
#private;
|
|
45
|
+
readonly audience: string;
|
|
46
|
+
readonly mount: string;
|
|
47
|
+
readonly page: string | undefined;
|
|
48
|
+
readonly ttlSeconds: number;
|
|
49
|
+
readonly intervalSeconds: number;
|
|
50
|
+
/**
|
|
51
|
+
* Builds a handler, or throws explaining why the configuration cannot work.
|
|
52
|
+
*
|
|
53
|
+
* THE AUDIENCE MUST BE A FIXED POINT OF THE §2.1 GRAMMAR, and that is checked with the
|
|
54
|
+
* grammar itself rather than with a list of rules restated here. `https://Dawn.example/api`,
|
|
55
|
+
* `…:443` and `wss://…` each DERIVE to something else, so a handler configured with one
|
|
56
|
+
* would bind the spelling in the config file while the CLI bound the derived one, and every
|
|
57
|
+
* proof would fail with nothing on either side saying why. Feeding the audience back through
|
|
58
|
+
* `deriveAudience` asks the only question that matters: is this the string the CLI will
|
|
59
|
+
* produce?
|
|
60
|
+
*
|
|
61
|
+
* The `/login/00` is the shortest invocation URL the grammar admits — a minimum-length id —
|
|
62
|
+
* and exists only to make the audience parseable as one.
|
|
63
|
+
*/
|
|
64
|
+
constructor(config: Config);
|
|
65
|
+
/**
|
|
66
|
+
* The four shapes of §4 and the two of §4.1, relative to the mount:
|
|
67
|
+
*
|
|
68
|
+
* POST "" begin the browser opens a request ("offer" names an offer)
|
|
69
|
+
* POST "/offers" offer the CLI registers what it will delegate, under its code
|
|
70
|
+
* GET "/offers/<code>" read offer the page reads it once; the CLI polls it until taken
|
|
71
|
+
* GET "/<id>" read the CLI reads what it is being asked to sign
|
|
72
|
+
* POST "/<id>/answer" answer the CLI delivers the proof
|
|
73
|
+
* GET "/<id>/answer" collect the browser takes the answer, once
|
|
74
|
+
*
|
|
75
|
+
* The `offers` segment is matched before the id routes. There is no ambiguity to resolve —
|
|
76
|
+
* an id is hex and `offers` is not — but the order says which family owns the segment.
|
|
77
|
+
*/
|
|
78
|
+
handle(request: Request): Promise<Response>;
|
|
79
|
+
/** Drops expired records and dead offers and reports how many went. Optional: both also
|
|
80
|
+
* expire on read, so a handler that is never swept is correct, just less tidy. This package
|
|
81
|
+
* starts no timer of its own — a long-lived service calls this from its own maintenance
|
|
82
|
+
* loop. */
|
|
83
|
+
sweep(): number;
|
|
84
|
+
}
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// The service side of the archon login protocol (docs/login.md §4 and §4.1): the four routes a
|
|
2
|
+
// browser and a CLI need to rendezvous, and the two by which the CLI may start instead (an
|
|
3
|
+
// offer, read and taken by the page), so a service adopts proof-of-possession sign-in by
|
|
4
|
+
// mounting a handler rather than by building a session system.
|
|
5
|
+
//
|
|
6
|
+
// FETCH-STYLE, WHICH IS THIS LANE'S WHOLE ADAPTER STORY. `handle` is
|
|
7
|
+
// `(Request) => Promise<Response>` over the Web platform's own types, so the same handler
|
|
8
|
+
// mounts in Node 18+, Deno, Bun, Cloudflare Workers and any framework that speaks fetch —
|
|
9
|
+
// without this package depending on any of them. Express and friends need about ten lines of
|
|
10
|
+
// glue, which belongs beside this package rather than inside it (`examples/serve.ts` is the
|
|
11
|
+
// node:http version).
|
|
12
|
+
//
|
|
13
|
+
// Three things this package deliberately is not (ADR 0007 §B):
|
|
14
|
+
//
|
|
15
|
+
// - Not a server. It opens no socket and starts no timer.
|
|
16
|
+
// - Not a law. The authority payload is opaque bytes, interpreted only inside the
|
|
17
|
+
// AdmitAuthority the service supplies. archon ships no implementation of one.
|
|
18
|
+
// - Not a store. One in-memory record per pending login, dropped at expiry or on collection.
|
|
19
|
+
//
|
|
20
|
+
// THE AUDIENCE IS CONFIGURED, NEVER READ FROM THE WIRE. Every binding is recomputed from the
|
|
21
|
+
// string the service configured. That is the WebAuthn rule and review Finding 1: a relying
|
|
22
|
+
// party's identity comes from its own configuration, not from a value an attacker can put in
|
|
23
|
+
// a message. No code path here reads an audience from a request.
|
|
24
|
+
//
|
|
25
|
+
// The scheme itself — the binding layout, the domain, the proofs — is @bitspark/archon-sdk's
|
|
26
|
+
// and is consumed, never reimplemented: a binding written twice is a binding that drifts.
|
|
27
|
+
import { decodeKey, encodeKey } from "@bitspark/archon";
|
|
28
|
+
import { deriveAudience, loginBinding, LOGIN_ROLE_LOGIN, verifyCollect, verifyLogin } from "@bitspark/archon-sdk";
|
|
29
|
+
import { checkCode, checkScopeEntry, fromHex, parseBody, rfc3339, stripMount, toHex } from "./json.js";
|
|
30
|
+
/** The RFC 8628 / RFC 6749 codes §4 adopts verbatim. Their bearer-token result is not ours,
|
|
31
|
+
* but their vocabulary is, so a client that already speaks device flow behaves well. */
|
|
32
|
+
const ERR_INVALID_REQUEST = "invalid_request";
|
|
33
|
+
const ERR_INVALID_GRANT = "invalid_grant";
|
|
34
|
+
const ERR_EXPIRED_TOKEN = "expired_token";
|
|
35
|
+
const ERR_PENDING = "authorization_pending";
|
|
36
|
+
const ERR_SLOW_DOWN = "slow_down";
|
|
37
|
+
/** The header carrying the browser's collect proof. Exported because a browser client has to
|
|
38
|
+
* spell it, and one spelling in one place is how the two halves stay agreed. */
|
|
39
|
+
export const COLLECT_HEADER = "archon-collect";
|
|
40
|
+
/** Five minutes, from §4. */
|
|
41
|
+
export const DEFAULT_TTL_SECONDS = 300;
|
|
42
|
+
/** Five seconds, from §4. */
|
|
43
|
+
export const DEFAULT_INTERVAL_SECONDS = 5;
|
|
44
|
+
/** The floor on both the generated id and the generated nonce. §3.1 requires it of the nonce
|
|
45
|
+
* and recommends it for the id; this package applies it to both, since an id a stranger can
|
|
46
|
+
* guess is the address of a pending login. */
|
|
47
|
+
const MIN_ENTROPY = 16;
|
|
48
|
+
/** §4.1's "differs in any way", stated once: the same number of entries, each equal to its
|
|
49
|
+
* counterpart IN ORDER, and the same validity. A set comparison would let a page reorder
|
|
50
|
+
* what the person typed; a length comparison would let it swap an entry. */
|
|
51
|
+
function matchesOffer(offer, scope, validFor) {
|
|
52
|
+
return validFor === offer.validFor && scope.length === offer.scope.length && scope.every((s, i) => s === offer.scope[i]);
|
|
53
|
+
}
|
|
54
|
+
const JSON_HEADERS = { "content-type": "application/json", "cache-control": "no-store" };
|
|
55
|
+
/** The error body of §4: exactly `{"error": "<code>"}` and nothing else. No description, no
|
|
56
|
+
* echoed id — a client that guessed an id learns only that it is not pending, which is the
|
|
57
|
+
* same thing it learns for an id that never existed. */
|
|
58
|
+
function fail(status, code) {
|
|
59
|
+
return new Response(JSON.stringify({ error: code }), { status, headers: JSON_HEADERS });
|
|
60
|
+
}
|
|
61
|
+
function ok(status, body) {
|
|
62
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
63
|
+
}
|
|
64
|
+
/** `checkCode` as a predicate, for the two routes that answer a malformed code with 404. */
|
|
65
|
+
function isCode(code) {
|
|
66
|
+
try {
|
|
67
|
+
checkCode(code);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function toRequest(record) {
|
|
75
|
+
return {
|
|
76
|
+
id: record.id,
|
|
77
|
+
nonce: record.nonce,
|
|
78
|
+
browser: record.browser,
|
|
79
|
+
scope: record.scope,
|
|
80
|
+
validFor: record.validFor,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export class Handler {
|
|
84
|
+
audience;
|
|
85
|
+
mount;
|
|
86
|
+
page;
|
|
87
|
+
ttlSeconds;
|
|
88
|
+
intervalSeconds;
|
|
89
|
+
#admit;
|
|
90
|
+
#clock;
|
|
91
|
+
#entropy;
|
|
92
|
+
#records = new Map();
|
|
93
|
+
#offers = new Map();
|
|
94
|
+
/**
|
|
95
|
+
* Builds a handler, or throws explaining why the configuration cannot work.
|
|
96
|
+
*
|
|
97
|
+
* THE AUDIENCE MUST BE A FIXED POINT OF THE §2.1 GRAMMAR, and that is checked with the
|
|
98
|
+
* grammar itself rather than with a list of rules restated here. `https://Dawn.example/api`,
|
|
99
|
+
* `…:443` and `wss://…` each DERIVE to something else, so a handler configured with one
|
|
100
|
+
* would bind the spelling in the config file while the CLI bound the derived one, and every
|
|
101
|
+
* proof would fail with nothing on either side saying why. Feeding the audience back through
|
|
102
|
+
* `deriveAudience` asks the only question that matters: is this the string the CLI will
|
|
103
|
+
* produce?
|
|
104
|
+
*
|
|
105
|
+
* The `/login/00` is the shortest invocation URL the grammar admits — a minimum-length id —
|
|
106
|
+
* and exists only to make the audience parseable as one.
|
|
107
|
+
*/
|
|
108
|
+
constructor(config) {
|
|
109
|
+
if (config.audience.length === 0) {
|
|
110
|
+
throw new Error("login: an audience is required — the handler binds to it and never reads one from the wire");
|
|
111
|
+
}
|
|
112
|
+
let derived;
|
|
113
|
+
try {
|
|
114
|
+
derived = deriveAudience(`${config.audience}/login/00`).audience;
|
|
115
|
+
}
|
|
116
|
+
catch (cause) {
|
|
117
|
+
throw new Error(`login: audience ${JSON.stringify(config.audience)} is not valid: ${String(cause)} (docs/login.md §2.1)`);
|
|
118
|
+
}
|
|
119
|
+
if (derived !== config.audience) {
|
|
120
|
+
throw new Error(`login: audience ${JSON.stringify(config.audience)} is not canonical — the CLI will derive ` +
|
|
121
|
+
`${JSON.stringify(derived)} and bind THAT, so every proof would fail. ` +
|
|
122
|
+
`Configure ${JSON.stringify(derived)} (docs/login.md §2.1)`);
|
|
123
|
+
}
|
|
124
|
+
// The code goes in the page's fragment (§4.1), so a page that already has one is a
|
|
125
|
+
// configuration mistake — refused here, where the operator can read it, rather than
|
|
126
|
+
// producing an address with two fragments that no browser parses the way anyone meant.
|
|
127
|
+
if (config.page !== undefined && config.page.includes("#")) {
|
|
128
|
+
throw new Error(`login: page ${JSON.stringify(config.page)} carries a fragment — the offer's code goes there (docs/login.md §4.1)`);
|
|
129
|
+
}
|
|
130
|
+
this.audience = config.audience;
|
|
131
|
+
this.mount = (config.mount ?? "").replace(/\/+$/, "");
|
|
132
|
+
this.page = config.page;
|
|
133
|
+
this.ttlSeconds = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
134
|
+
this.intervalSeconds = config.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS;
|
|
135
|
+
this.#admit = config.admit;
|
|
136
|
+
this.#clock = config.clock ?? (() => Math.floor(Date.now() / 1000));
|
|
137
|
+
this.#entropy = config.entropy ?? ((n) => crypto.getRandomValues(new Uint8Array(n)));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The four shapes of §4 and the two of §4.1, relative to the mount:
|
|
141
|
+
*
|
|
142
|
+
* POST "" begin the browser opens a request ("offer" names an offer)
|
|
143
|
+
* POST "/offers" offer the CLI registers what it will delegate, under its code
|
|
144
|
+
* GET "/offers/<code>" read offer the page reads it once; the CLI polls it until taken
|
|
145
|
+
* GET "/<id>" read the CLI reads what it is being asked to sign
|
|
146
|
+
* POST "/<id>/answer" answer the CLI delivers the proof
|
|
147
|
+
* GET "/<id>/answer" collect the browser takes the answer, once
|
|
148
|
+
*
|
|
149
|
+
* The `offers` segment is matched before the id routes. There is no ambiguity to resolve —
|
|
150
|
+
* an id is hex and `offers` is not — but the order says which family owns the segment.
|
|
151
|
+
*/
|
|
152
|
+
async handle(request) {
|
|
153
|
+
const path = stripMount(new URL(request.url).pathname, this.mount);
|
|
154
|
+
if (path === undefined)
|
|
155
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
156
|
+
const parts = path.split("/").filter((p) => p.length > 0);
|
|
157
|
+
const method = request.method.toUpperCase();
|
|
158
|
+
if (parts.length === 0) {
|
|
159
|
+
return method === "POST" ? this.#begin(request) : fail(405, ERR_INVALID_REQUEST);
|
|
160
|
+
}
|
|
161
|
+
if (parts[0] === "offers") {
|
|
162
|
+
if (parts.length === 1)
|
|
163
|
+
return method === "POST" ? this.#offer(request) : fail(405, ERR_INVALID_REQUEST);
|
|
164
|
+
if (parts.length === 2)
|
|
165
|
+
return method === "GET" ? this.#readOffer(parts[1]) : fail(405, ERR_INVALID_REQUEST);
|
|
166
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
167
|
+
}
|
|
168
|
+
const id = parts[0];
|
|
169
|
+
if (parts.length === 1) {
|
|
170
|
+
return method === "GET" ? this.#read(id) : fail(405, ERR_INVALID_REQUEST);
|
|
171
|
+
}
|
|
172
|
+
if (parts.length === 2 && parts[1] === "answer") {
|
|
173
|
+
if (method === "POST")
|
|
174
|
+
return this.#answer(request, id);
|
|
175
|
+
if (method === "GET")
|
|
176
|
+
return this.#collect(request, id);
|
|
177
|
+
return fail(405, ERR_INVALID_REQUEST);
|
|
178
|
+
}
|
|
179
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
180
|
+
}
|
|
181
|
+
/** Drops expired records and dead offers and reports how many went. Optional: both also
|
|
182
|
+
* expire on read, so a handler that is never swept is correct, just less tidy. This package
|
|
183
|
+
* starts no timer of its own — a long-lived service calls this from its own maintenance
|
|
184
|
+
* loop. */
|
|
185
|
+
sweep() {
|
|
186
|
+
const now = this.#clock();
|
|
187
|
+
let dropped = 0;
|
|
188
|
+
for (const [key, record] of this.#records) {
|
|
189
|
+
if (now >= record.expires) {
|
|
190
|
+
this.#records.delete(key);
|
|
191
|
+
dropped += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Offers after records, so an offer whose request just went is seen as orphaned.
|
|
195
|
+
for (const [code, offer] of this.#offers) {
|
|
196
|
+
if (!this.#offerIsLive(offer, now)) {
|
|
197
|
+
this.#offers.delete(code);
|
|
198
|
+
dropped += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return dropped;
|
|
202
|
+
}
|
|
203
|
+
/** Creates a pending record: the server's id and nonce, the browser's key, the scope and
|
|
204
|
+
* validity it asked for. The nonce is the SERVER's, never the browser's — a nonce a caller
|
|
205
|
+
* chooses is a nonce a caller can replay (§3.1).
|
|
206
|
+
*
|
|
207
|
+
* On an offer, the request must be what the prover offered — scope entry for entry, in
|
|
208
|
+
* order, and validity equal — and the offer must be open. That is checked FIRST, before the
|
|
209
|
+
* browser key is decoded and before any entropy is drawn (§4.1: refused before anything is
|
|
210
|
+
* stored, before any key is involved), and again when the request is stored and the offer
|
|
211
|
+
* taken — with NO `await` between the check and the take, which is this lane's critical
|
|
212
|
+
* section (see `#takeOffer`). */
|
|
213
|
+
async #begin(request) {
|
|
214
|
+
let body;
|
|
215
|
+
try {
|
|
216
|
+
({ body } = parseBody(await request.arrayBuffer(), ["browser", "scope", "valid_for", "offer"]));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
220
|
+
}
|
|
221
|
+
// `valid_for`, and ONLY that spelling. A `validFor` alias here would be comfortable for a
|
|
222
|
+
// TypeScript caller and would let them write a client that go and rs refuse -- the exact
|
|
223
|
+
// cross-lane divergence the shared wire fixture exists to prevent. One wire, three lanes.
|
|
224
|
+
const validFor = body["valid_for"];
|
|
225
|
+
const browserText = body["browser"];
|
|
226
|
+
const scopeRaw = body["scope"] ?? [];
|
|
227
|
+
if (typeof browserText !== "string" || !Array.isArray(scopeRaw))
|
|
228
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
229
|
+
if (typeof validFor !== "number" || !Number.isInteger(validFor) || validFor <= 0 || validFor > 0xffffffff) {
|
|
230
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
231
|
+
}
|
|
232
|
+
const scope = [];
|
|
233
|
+
for (const entry of scopeRaw) {
|
|
234
|
+
if (typeof entry !== "string")
|
|
235
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
236
|
+
try {
|
|
237
|
+
checkScopeEntry(entry);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
241
|
+
}
|
|
242
|
+
scope.push(entry);
|
|
243
|
+
}
|
|
244
|
+
// THE OFFER IS CHECKED HERE — before the browser key is decoded, before any entropy is
|
|
245
|
+
// drawn (§4.1: refused before anything is stored, before any key is involved).
|
|
246
|
+
let offerCode;
|
|
247
|
+
if ("offer" in body) {
|
|
248
|
+
const code = body["offer"];
|
|
249
|
+
// A member that is present must be a code STRING: null or any other type is a malformed
|
|
250
|
+
// body (400), the same answer go and rs give — refused, not read as "no offer" (the
|
|
251
|
+
// page-started form leaves the member out) and not mistaken for an unknown code.
|
|
252
|
+
if (typeof code !== "string")
|
|
253
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
254
|
+
// A malformed code is an UNKNOWN one: a registered code is always well-formed, so this
|
|
255
|
+
// names nothing, and the answer is the poll route's 404 — not a 400 that would tell a
|
|
256
|
+
// prober which of its guesses were at least the right shape.
|
|
257
|
+
if (!isCode(code))
|
|
258
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
259
|
+
const refused = this.#checkOffer(code, scope, validFor);
|
|
260
|
+
if (refused !== undefined)
|
|
261
|
+
return refused;
|
|
262
|
+
offerCode = code;
|
|
263
|
+
}
|
|
264
|
+
let browser;
|
|
265
|
+
try {
|
|
266
|
+
browser = decodeKey(browserText);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
270
|
+
}
|
|
271
|
+
const record = {
|
|
272
|
+
id: this.#entropy(MIN_ENTROPY),
|
|
273
|
+
nonce: this.#entropy(MIN_ENTROPY),
|
|
274
|
+
browser,
|
|
275
|
+
scope,
|
|
276
|
+
validFor,
|
|
277
|
+
expires: this.#clock() + this.ttlSeconds,
|
|
278
|
+
answered: undefined,
|
|
279
|
+
lastPoll: undefined,
|
|
280
|
+
};
|
|
281
|
+
// A request that cannot produce a binding must not be handed out: the CLI would fetch it,
|
|
282
|
+
// show it, and fail at signing time with nothing to explain.
|
|
283
|
+
try {
|
|
284
|
+
loginBinding(LOGIN_ROLE_LOGIN, this.audience, toRequest(record));
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
288
|
+
}
|
|
289
|
+
const idHex = toHex(record.id);
|
|
290
|
+
if (offerCode !== undefined) {
|
|
291
|
+
const refused = this.#takeOffer(offerCode, idHex, record);
|
|
292
|
+
if (refused !== undefined)
|
|
293
|
+
return refused;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.#records.set(idHex, record);
|
|
297
|
+
}
|
|
298
|
+
// The same body whether or not an offer was named: the offer is not echoed, so a browser
|
|
299
|
+
// client needs one shape for begin (pinned as begin_on_offer in login-wire.json).
|
|
300
|
+
return ok(201, {
|
|
301
|
+
id: idHex,
|
|
302
|
+
nonce: toHex(record.nonce),
|
|
303
|
+
browser: browserText,
|
|
304
|
+
scope,
|
|
305
|
+
valid_for: validFor,
|
|
306
|
+
expires_in: this.ttlSeconds,
|
|
307
|
+
interval: this.intervalSeconds,
|
|
308
|
+
verification_uri: `${this.audience}/login/${idHex}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** §4.1's refusals for a request naming an offer, or undefined when it may go on: unknown,
|
|
312
|
+
* expired or orphaned → 404 (one answer, indistinguishable); taken → 409 (one offer, one
|
|
313
|
+
* request); differing from the offer in any way → 400. `#begin` asks twice — before
|
|
314
|
+
* building the request and again when storing it. */
|
|
315
|
+
#checkOffer(code, scope, validFor) {
|
|
316
|
+
const offer = this.#liveOffer(code);
|
|
317
|
+
if (offer === undefined)
|
|
318
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
319
|
+
if (offer.request !== undefined)
|
|
320
|
+
return fail(409, ERR_INVALID_REQUEST);
|
|
321
|
+
if (!matchesOffer(offer, scope, validFor))
|
|
322
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
/** Stores the request AND marks the offer taken, repeating every check first and with NO
|
|
326
|
+
* `await` between check and take — in this lane that span is the critical section, and it
|
|
327
|
+
* is what makes "exactly one of two pages beginning on one code wins" a fact rather than a
|
|
328
|
+
* likelihood. The offer may have been taken, or died, while the request was being built. */
|
|
329
|
+
#takeOffer(code, idHex, record) {
|
|
330
|
+
const refused = this.#checkOffer(code, record.scope, record.validFor);
|
|
331
|
+
if (refused !== undefined)
|
|
332
|
+
return refused;
|
|
333
|
+
this.#records.set(idHex, record);
|
|
334
|
+
const offer = this.#offers.get(code);
|
|
335
|
+
if (offer !== undefined)
|
|
336
|
+
offer.request = idHex;
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
/** Registers what a prover is willing to delegate, under a code the PROVER minted. The
|
|
340
|
+
* server draws no entropy here: the code is the prover's own, which is what lets the prover
|
|
341
|
+
* know it before anyone else does and print it on its own terminal (§4.1 "The code").
|
|
342
|
+
*
|
|
343
|
+
* Everything that would make the offer impossible to begin on is refused now, at the door,
|
|
344
|
+
* rather than at the page's begin — where the refusal would reach the page and not the
|
|
345
|
+
* person who typed the offer: a malformed code, a validity of zero, a scope entry the CLI
|
|
346
|
+
* could not display, and a scope that cannot bind at all. */
|
|
347
|
+
async #offer(request) {
|
|
348
|
+
let body;
|
|
349
|
+
try {
|
|
350
|
+
({ body } = parseBody(await request.arrayBuffer(), ["code", "scope", "valid_for"]));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
354
|
+
}
|
|
355
|
+
const code = body["code"];
|
|
356
|
+
const validFor = body["valid_for"];
|
|
357
|
+
const scopeRaw = body["scope"] ?? [];
|
|
358
|
+
if (typeof code !== "string" || !isCode(code) || !Array.isArray(scopeRaw))
|
|
359
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
360
|
+
if (typeof validFor !== "number" || !Number.isInteger(validFor) || validFor <= 0 || validFor > 0xffffffff) {
|
|
361
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
362
|
+
}
|
|
363
|
+
const scope = [];
|
|
364
|
+
for (const entry of scopeRaw) {
|
|
365
|
+
if (typeof entry !== "string")
|
|
366
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
367
|
+
try {
|
|
368
|
+
checkScopeEntry(entry);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
372
|
+
}
|
|
373
|
+
scope.push(entry);
|
|
374
|
+
}
|
|
375
|
+
// THE SCOPE MUST BIND. A binding needs a key and an id the offer does not have yet, so the
|
|
376
|
+
// probe uses placeholders of the right size: what is being asked is only whether THESE
|
|
377
|
+
// scope entries and THIS validity fit the binding's fields (§3.2), which no key changes. An
|
|
378
|
+
// offer that passes here can always be begun on; one that failed would have sat open until
|
|
379
|
+
// it expired, every begin on it refused for a reason the page cannot see.
|
|
380
|
+
try {
|
|
381
|
+
loginBinding(LOGIN_ROLE_LOGIN, this.audience, {
|
|
382
|
+
id: new Uint8Array(MIN_ENTROPY),
|
|
383
|
+
nonce: new Uint8Array(MIN_ENTROPY),
|
|
384
|
+
browser: new Uint8Array(32),
|
|
385
|
+
scope,
|
|
386
|
+
validFor,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
391
|
+
}
|
|
392
|
+
// One code, one offer: a LIVE offer under this code already is a conflict. The prover
|
|
393
|
+
// minted the code from 16 bytes of its own entropy, so this is a broken prover or a
|
|
394
|
+
// replay, never a collision worth retrying silently. A dead offer under the code is simply
|
|
395
|
+
// replaced — the code is free again.
|
|
396
|
+
if (this.#liveOffer(code) !== undefined)
|
|
397
|
+
return fail(409, ERR_INVALID_REQUEST);
|
|
398
|
+
const now = this.#clock();
|
|
399
|
+
this.#offers.set(code, { code, scope, validFor, expires: now + this.ttlSeconds, request: undefined });
|
|
400
|
+
const response = {
|
|
401
|
+
code,
|
|
402
|
+
scope,
|
|
403
|
+
valid_for: validFor,
|
|
404
|
+
expires_in: this.ttlSeconds,
|
|
405
|
+
interval: this.intervalSeconds,
|
|
406
|
+
};
|
|
407
|
+
// The code rides in the fragment, which a browser keeps to itself: the page's script reads
|
|
408
|
+
// it, no server and no log ever sees it (§4.1 "The code").
|
|
409
|
+
if (this.page !== undefined)
|
|
410
|
+
response["page"] = `${this.page}#${code}`;
|
|
411
|
+
return ok(201, response);
|
|
412
|
+
}
|
|
413
|
+
/** Read once by the page — to learn what it is being offered — and polled by the prover
|
|
414
|
+
* until `request` names the request that took the offer (§4.1).
|
|
415
|
+
*
|
|
416
|
+
* There is deliberately NO pacing on this route (ADR 0007 §C.7, amendment #39). Two parties
|
|
417
|
+
* poll it, and one reference time would let the prover's period lock the page out on every
|
|
418
|
+
* retry; and there is nothing here to protect — no proof to verify, no answer to hand over.
|
|
419
|
+
* A stranger who holds the code already has everything this route returns. The prover paces
|
|
420
|
+
* itself by the `interval` the offer response advertised.
|
|
421
|
+
*
|
|
422
|
+
* A malformed code is 404, the same as an unknown one: a registered code is always
|
|
423
|
+
* well-formed, so a malformed one is unknown by construction. */
|
|
424
|
+
#readOffer(code) {
|
|
425
|
+
if (!isCode(code))
|
|
426
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
427
|
+
const offer = this.#liveOffer(code);
|
|
428
|
+
if (offer === undefined)
|
|
429
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
430
|
+
return ok(200, {
|
|
431
|
+
code: offer.code,
|
|
432
|
+
scope: offer.scope,
|
|
433
|
+
valid_for: offer.validFor,
|
|
434
|
+
// null until taken, then the id — the shape a poller switches on.
|
|
435
|
+
request: offer.request ?? null,
|
|
436
|
+
expires: rfc3339(offer.expires),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/** The one definition of a live offer: unexpired, and — if a request has taken it — that
|
|
440
|
+
* request still present and unexpired. "Dies with its request" (§4.1) is enforced here, on
|
|
441
|
+
* every read, rather than by anything that runs when a request is collected: an offer that
|
|
442
|
+
* consults the record it points at can never disagree with it. */
|
|
443
|
+
#offerIsLive(offer, now) {
|
|
444
|
+
if (now >= offer.expires)
|
|
445
|
+
return false;
|
|
446
|
+
if (offer.request === undefined)
|
|
447
|
+
return true;
|
|
448
|
+
const record = this.#records.get(offer.request);
|
|
449
|
+
return record !== undefined && now < record.expires;
|
|
450
|
+
}
|
|
451
|
+
/** The live offer under `code`, or undefined if it never existed, expired, or died with its
|
|
452
|
+
* request — one answer on the wire, so a stranger probing codes learns nothing. */
|
|
453
|
+
#liveOffer(code) {
|
|
454
|
+
const offer = this.#offers.get(code);
|
|
455
|
+
if (offer === undefined)
|
|
456
|
+
return undefined;
|
|
457
|
+
if (!this.#offerIsLive(offer, this.#clock())) {
|
|
458
|
+
this.#offers.delete(code);
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
return offer;
|
|
462
|
+
}
|
|
463
|
+
/** What the CLI fetches. It answers with the request and NOT with the audience: there is no
|
|
464
|
+
* audience field in this response by design, because a CLI that would read one is a CLI
|
|
465
|
+
* that can be told to sign for someone else (Finding 1). */
|
|
466
|
+
#read(idHex) {
|
|
467
|
+
const record = this.#live(idHex);
|
|
468
|
+
if (record === undefined)
|
|
469
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
470
|
+
return ok(200, {
|
|
471
|
+
id: idHex,
|
|
472
|
+
nonce: toHex(record.nonce),
|
|
473
|
+
browser: encodeKey(record.browser),
|
|
474
|
+
scope: record.scope,
|
|
475
|
+
valid_for: record.validFor,
|
|
476
|
+
expires: rfc3339(record.expires),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Verifies BEFORE storing, which is the whole point of the route (§4).
|
|
481
|
+
*
|
|
482
|
+
* THE AWAIT IS THE HAZARD. `AdmitAuthority` is the SERVICE's code and may be async — a
|
|
483
|
+
* database, a network call — and JavaScript hands the runtime to every other pending request
|
|
484
|
+
* at that `await`. So the state this decision rests on is RE-CHECKED afterwards: a second
|
|
485
|
+
* CLI answering the same login while the first was being admitted is ordinary (a person with
|
|
486
|
+
* two terminals), and the loser must get 409 rather than overwrite an answer the browser may
|
|
487
|
+
* already have collected.
|
|
488
|
+
*
|
|
489
|
+
* The three server lanes reach the same rule from opposite directions: go and rs must keep
|
|
490
|
+
* the law OUT of the store's mutex so one slow law cannot decide the throughput of every
|
|
491
|
+
* login; here there is no mutex to hold, and the danger is the interleaving that an `await`
|
|
492
|
+
* makes certain rather than merely possible.
|
|
493
|
+
*/
|
|
494
|
+
async #answer(request, idHex) {
|
|
495
|
+
let body;
|
|
496
|
+
let authority;
|
|
497
|
+
try {
|
|
498
|
+
const parsed = parseBody(await request.arrayBuffer(), ["principal", "possession", "authority"]);
|
|
499
|
+
body = parsed.body;
|
|
500
|
+
// The authority is carried as the bytes the CLI sent, never rebuilt from the parse.
|
|
501
|
+
// See `authoritySpan`: JSON.parse + JSON.stringify silently rewrites large integers,
|
|
502
|
+
// trailing zeros, escapes and key order.
|
|
503
|
+
authority = parsed.members.get("authority");
|
|
504
|
+
if (authority === undefined && Object.prototype.hasOwnProperty.call(body, "authority")) {
|
|
505
|
+
// The parser saw a member the scanner did not. They read the same bytes, so this is
|
|
506
|
+
// not a request problem — but guessing which one is right is how a payload gets
|
|
507
|
+
// rebuilt behind the law's back. Refuse instead.
|
|
508
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
513
|
+
}
|
|
514
|
+
const principalText = body["principal"];
|
|
515
|
+
const possessionText = body["possession"];
|
|
516
|
+
if (typeof principalText !== "string" || typeof possessionText !== "string") {
|
|
517
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
518
|
+
}
|
|
519
|
+
let principal;
|
|
520
|
+
let possession;
|
|
521
|
+
try {
|
|
522
|
+
principal = decodeKey(principalText);
|
|
523
|
+
possession = fromHex(possessionText);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return fail(400, ERR_INVALID_REQUEST);
|
|
527
|
+
}
|
|
528
|
+
const record = this.#live(idHex);
|
|
529
|
+
if (record === undefined)
|
|
530
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
531
|
+
if (record.answered !== undefined)
|
|
532
|
+
return fail(409, ERR_INVALID_REQUEST);
|
|
533
|
+
if (!verifyLogin(principal, this.audience, toRequest(record), possession)) {
|
|
534
|
+
return fail(403, ERR_INVALID_GRANT);
|
|
535
|
+
}
|
|
536
|
+
if (this.#admit !== undefined) {
|
|
537
|
+
try {
|
|
538
|
+
await this.#admit(record.browser, principal, new TextEncoder().encode(authority ?? ""));
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return fail(403, ERR_INVALID_GRANT);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Everything the decision rested on, re-read after the await.
|
|
545
|
+
const still = this.#live(idHex);
|
|
546
|
+
if (still === undefined)
|
|
547
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
548
|
+
if (still.answered !== undefined) {
|
|
549
|
+
// Another answer won while this one was being admitted. Storing here would discard a
|
|
550
|
+
// proof the browser may already have collected.
|
|
551
|
+
return fail(409, ERR_INVALID_REQUEST);
|
|
552
|
+
}
|
|
553
|
+
still.answered = { principal: principalText, possession: possessionText, authority };
|
|
554
|
+
return new Response(null, { status: 204 });
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Hands the answer to the browser ONCE and drops the record.
|
|
558
|
+
*
|
|
559
|
+
* THE ORDER IS caa's FIX: the interval is checked WITHOUT writing the timer, the proof is
|
|
560
|
+
* verified, and only then does the timer advance — immediately before the answer is taken,
|
|
561
|
+
* with NO `await` in between, which is what makes the take-and-drop atomic in this lane.
|
|
562
|
+
* Advancing the timer before verification let a stranger polling junk hold the real browser
|
|
563
|
+
* at 429 indefinitely, turning slow_down into a denial of service handed to anyone who saw
|
|
564
|
+
* the id.
|
|
565
|
+
*
|
|
566
|
+
* A stranger is therefore not rate-limited here at all. That is correct: they get 403 every
|
|
567
|
+
* time, and §B says to add no rate limiting beyond slow_down, which exists for the legitimate
|
|
568
|
+
* client rather than as a guard.
|
|
569
|
+
*/
|
|
570
|
+
#collect(request, idHex) {
|
|
571
|
+
const header = request.headers.get(COLLECT_HEADER);
|
|
572
|
+
if (header === null || header.length === 0)
|
|
573
|
+
return fail(403, ERR_INVALID_GRANT);
|
|
574
|
+
let proof;
|
|
575
|
+
try {
|
|
576
|
+
proof = fromHex(header);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return fail(403, ERR_INVALID_GRANT);
|
|
580
|
+
}
|
|
581
|
+
const record = this.#live(idHex);
|
|
582
|
+
if (record === undefined)
|
|
583
|
+
return fail(404, ERR_EXPIRED_TOKEN);
|
|
584
|
+
const now = this.#clock();
|
|
585
|
+
if (record.lastPoll !== undefined && now - record.lastPoll < this.intervalSeconds) {
|
|
586
|
+
return fail(429, ERR_SLOW_DOWN);
|
|
587
|
+
}
|
|
588
|
+
if (!verifyCollect(this.audience, toRequest(record), proof))
|
|
589
|
+
return fail(403, ERR_INVALID_GRANT);
|
|
590
|
+
record.lastPoll = now;
|
|
591
|
+
const answer = record.answered;
|
|
592
|
+
if (answer === undefined)
|
|
593
|
+
return fail(202, ERR_PENDING);
|
|
594
|
+
this.#records.delete(idHex);
|
|
595
|
+
// Built as TEXT, not as an object handed to JSON.stringify: the authority is spliced in
|
|
596
|
+
// exactly as the CLI wrote it, which is the whole point. `principal` and `possession` are
|
|
597
|
+
// this handler's own key text and hex, so encoding those is safe.
|
|
598
|
+
const authority = answer.authority === undefined ? "" : `,"authority":${answer.authority}`;
|
|
599
|
+
const body = `{"principal":${JSON.stringify(answer.principal)},` +
|
|
600
|
+
`"possession":${JSON.stringify(answer.possession)}${authority}}`;
|
|
601
|
+
return new Response(body, { status: 200, headers: JSON_HEADERS });
|
|
602
|
+
}
|
|
603
|
+
/** The record for `idHex`, or undefined if it never existed or has expired — which are the
|
|
604
|
+
* same answer on the wire, so a stranger probing ids learns nothing from the difference. */
|
|
605
|
+
#live(idHex) {
|
|
606
|
+
const record = this.#records.get(idHex);
|
|
607
|
+
if (record === undefined)
|
|
608
|
+
return undefined;
|
|
609
|
+
if (this.#clock() >= record.expires) {
|
|
610
|
+
this.#records.delete(idHex);
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
return record;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/login.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitspark/archon-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "archon login: the service-side handler — four routes, law-free, store-minimal. A fetch-style handler that opens no socket.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"prepack": "npm run build",
|
|
21
22
|
"test": "tsc -p tsconfig.json && node --test dist/test/*.test.js"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@bitspark/archon": "^0.
|
|
25
|
-
"@bitspark/archon-sdk": "^0.
|
|
25
|
+
"@bitspark/archon": "^0.8.2",
|
|
26
|
+
"@bitspark/archon-sdk": "^0.8.2"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^26",
|