@estiva-app/protocol 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/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/bridge.d.ts +91 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +138 -0
- package/dist/bridge.js.map +1 -0
- package/dist/events.d.ts +432 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +616 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/live.d.ts +205 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +398 -0
- package/dist/live.js.map +1 -0
- package/dist/nip19.d.ts +98 -0
- package/dist/nip19.d.ts.map +1 -0
- package/dist/nip19.js +320 -0
- package/dist/nip19.js.map +1 -0
- package/dist/nip98.d.ts +61 -0
- package/dist/nip98.d.ts.map +1 -0
- package/dist/nip98.js +134 -0
- package/dist/nip98.js.map +1 -0
- package/dist/sign.d.ts +67 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +58 -0
- package/dist/sign.js.map +1 -0
- package/dist/subscriptions.d.ts +120 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +68 -0
- package/dist/subscriptions.js.map +1 -0
- package/package.json +59 -0
- package/src/bridge.ts +198 -0
- package/src/events.ts +821 -0
- package/src/index.ts +159 -0
- package/src/live.ts +536 -0
- package/src/nip19.ts +354 -0
- package/src/nip98.ts +164 -0
- package/src/sign.ts +113 -0
- package/src/subscriptions.ts +200 -0
package/dist/nip19.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIP-19 `naddr` — bech32-encoded pointers to addressable events.
|
|
3
|
+
*
|
|
4
|
+
* This is how an object gets *out of* the app that owns it and *into* another:
|
|
5
|
+
* Ship encodes an issue as `nostr:naddr1…`, somebody pastes it into a Peek
|
|
6
|
+
* message (NIP-21 for the URI scheme, NIP-27 for the convention of embedding one
|
|
7
|
+
* in a text body), and Peek resolves it back to an address.
|
|
8
|
+
*
|
|
9
|
+
* ## Two implementations, and they already agreed
|
|
10
|
+
*
|
|
11
|
+
* Before SHA-3 this existed twice: Ship wrote an encoder, Peek wrote a decoder,
|
|
12
|
+
* and each grew the other half later. Their outputs were checked byte for byte
|
|
13
|
+
* during the extraction and were **identical** for the same pointer — so this
|
|
14
|
+
* merge is a deduplication and not a reconciliation. What each copy had that the
|
|
15
|
+
* other lacked is all kept: Ship's generic bech32 primitives and
|
|
16
|
+
* `addrToNaddr`/`naddrToAddr`, Peek's `pointerToAddress`/`addressToPointer`,
|
|
17
|
+
* `referenceToPointer` and `stripNaddrs`.
|
|
18
|
+
*
|
|
19
|
+
* ## Three things about the format that are easy to get wrong
|
|
20
|
+
*
|
|
21
|
+
* **Not bech32m.** NIP-19 predates bech32m and uses the original constant
|
|
22
|
+
* (`^ 1`). Encoding with bech32m produces a string that looks right, passes a
|
|
23
|
+
* casual eyeball, and fails every other implementation's checksum. A round-trip
|
|
24
|
+
* test cannot catch it — it would be wrong in both directions — which is why
|
|
25
|
+
* `bech32Encode`/`bech32Decode` are exported and pinned against the canonical
|
|
26
|
+
* NIP-19 `npub` vector from the specification itself.
|
|
27
|
+
*
|
|
28
|
+
* **The 90-character limit does not apply.** BIP-173 caps a bech32 string at 90
|
|
29
|
+
* characters for QR-code reasons; an `naddr` carrying a UUID `d` tag, a 32-byte
|
|
30
|
+
* pubkey and a relay hint runs to ~180. NIP-19 explicitly lifts the cap, so no
|
|
31
|
+
* length check appears here.
|
|
32
|
+
*
|
|
33
|
+
* **TLV order is not normative, and other implementations differ.** This encoder
|
|
34
|
+
* emits identifier, relays, author, kind. `nostr-tools` emits a different order,
|
|
35
|
+
* so the same pointer encodes to a *different string* — measured. Every decoder
|
|
36
|
+
* involved (this one, Ship's old one, Peek's old one, `nostr-tools`) reads TLVs
|
|
37
|
+
* by type and is order-tolerant, so the strings interoperate; but they are not
|
|
38
|
+
* comparable as strings, and nothing here should ever compare two naddrs for
|
|
39
|
+
* equality. Compare the addresses they decode to.
|
|
40
|
+
*/
|
|
41
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
|
|
42
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
43
|
+
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
44
|
+
function polymod(values) {
|
|
45
|
+
let chk = 1;
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
const top = chk >>> 25;
|
|
48
|
+
chk = ((chk & 0x1ffffff) << 5) ^ value;
|
|
49
|
+
for (let i = 0; i < 5; i++) {
|
|
50
|
+
if ((top >>> i) & 1)
|
|
51
|
+
chk ^= GENERATOR[i];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return chk >>> 0;
|
|
55
|
+
}
|
|
56
|
+
function hrpExpand(hrp) {
|
|
57
|
+
const high = [];
|
|
58
|
+
const low = [];
|
|
59
|
+
for (const char of hrp) {
|
|
60
|
+
high.push(char.charCodeAt(0) >>> 5);
|
|
61
|
+
low.push(char.charCodeAt(0) & 31);
|
|
62
|
+
}
|
|
63
|
+
return [...high, 0, ...low];
|
|
64
|
+
}
|
|
65
|
+
function checksum(hrp, data) {
|
|
66
|
+
const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
|
|
67
|
+
// `^ 1` is bech32. bech32m would use `^ 0x2bc830a3` — see the note above.
|
|
68
|
+
const mod = polymod(values) ^ 1;
|
|
69
|
+
return [0, 1, 2, 3, 4, 5].map((i) => (mod >>> (5 * (5 - i))) & 31);
|
|
70
|
+
}
|
|
71
|
+
/** Regroup bytes between bit widths, e.g. 8-bit bytes → 5-bit bech32 symbols. */
|
|
72
|
+
export function convertBits(data, from, to, pad) {
|
|
73
|
+
let acc = 0;
|
|
74
|
+
let bits = 0;
|
|
75
|
+
const out = [];
|
|
76
|
+
const maxv = (1 << to) - 1;
|
|
77
|
+
for (let i = 0; i < data.length; i++) {
|
|
78
|
+
const value = data[i];
|
|
79
|
+
if (value < 0 || value >> from !== 0)
|
|
80
|
+
throw new Error(`value out of range: ${value}`);
|
|
81
|
+
acc = (acc << from) | value;
|
|
82
|
+
bits += from;
|
|
83
|
+
while (bits >= to) {
|
|
84
|
+
bits -= to;
|
|
85
|
+
out.push((acc >>> bits) & maxv);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (pad) {
|
|
89
|
+
if (bits > 0)
|
|
90
|
+
out.push((acc << (to - bits)) & maxv);
|
|
91
|
+
}
|
|
92
|
+
else if (bits >= from || ((acc << (to - bits)) & maxv) !== 0) {
|
|
93
|
+
throw new Error('invalid padding');
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Exported for the bare NIP-19 types (`npub`, `note`) and for tests.
|
|
99
|
+
*
|
|
100
|
+
* The checksum layer is the part worth testing directly: a wrong constant still
|
|
101
|
+
* round-trips against itself, so only an external vector catches it.
|
|
102
|
+
*
|
|
103
|
+
* Strict about case, deliberately — BIP-173 forbids mixing. `decodeNaddr` is the
|
|
104
|
+
* forgiving entry point, because what reaches it was typed or pasted by a person.
|
|
105
|
+
*/
|
|
106
|
+
export function bech32Encode(hrp, data) {
|
|
107
|
+
const combined = [...data, ...checksum(hrp, data)];
|
|
108
|
+
return `${hrp}1${combined.map((d) => CHARSET[d]).join('')}`;
|
|
109
|
+
}
|
|
110
|
+
export function bech32Decode(encoded) {
|
|
111
|
+
const lower = encoded.toLowerCase();
|
|
112
|
+
if (lower !== encoded && encoded.toUpperCase() !== encoded) {
|
|
113
|
+
throw new Error('mixed case bech32 string');
|
|
114
|
+
}
|
|
115
|
+
const split = lower.lastIndexOf('1');
|
|
116
|
+
if (split < 1 || split + 7 > lower.length)
|
|
117
|
+
throw new Error('malformed bech32 string');
|
|
118
|
+
const hrp = lower.slice(0, split);
|
|
119
|
+
const data = [];
|
|
120
|
+
for (const char of lower.slice(split + 1)) {
|
|
121
|
+
const index = CHARSET.indexOf(char);
|
|
122
|
+
if (index === -1)
|
|
123
|
+
throw new Error(`invalid bech32 character: ${char}`);
|
|
124
|
+
data.push(index);
|
|
125
|
+
}
|
|
126
|
+
if (polymod([...hrpExpand(hrp), ...data]) !== 1)
|
|
127
|
+
throw new Error('bad bech32 checksum');
|
|
128
|
+
return { hrp, data: data.slice(0, -6) };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* TLV types for `naddr` (NIP-19 §"Shareable identifiers with extra metadata").
|
|
132
|
+
* The numbers are the protocol; the names are ours.
|
|
133
|
+
*/
|
|
134
|
+
const TLV_IDENTIFIER = 0;
|
|
135
|
+
const TLV_RELAY = 1;
|
|
136
|
+
const TLV_AUTHOR = 2;
|
|
137
|
+
const TLV_KIND = 3;
|
|
138
|
+
/**
|
|
139
|
+
* Encode an address as `naddr1…`.
|
|
140
|
+
*
|
|
141
|
+
* TLV order is identifier, relays, author, kind — see the header on why that is
|
|
142
|
+
* a choice rather than a rule.
|
|
143
|
+
*/
|
|
144
|
+
export function encodeNaddr(pointer) {
|
|
145
|
+
const bytes = [];
|
|
146
|
+
const push = (type, value) => {
|
|
147
|
+
if (value.length > 255)
|
|
148
|
+
throw new Error(`TLV value too long for type ${type}`);
|
|
149
|
+
bytes.push(type, value.length, ...value);
|
|
150
|
+
};
|
|
151
|
+
push(TLV_IDENTIFIER, utf8ToBytes(pointer.identifier));
|
|
152
|
+
for (const relay of pointer.relays)
|
|
153
|
+
push(TLV_RELAY, utf8ToBytes(relay));
|
|
154
|
+
// Peek's encoder checked the author's length and Ship's did not, and the
|
|
155
|
+
// difference is not cosmetic: `hexToBytes` is happy with any even-length hex,
|
|
156
|
+
// so a 20-byte author encoded into a *valid* naddr that every decoder then
|
|
157
|
+
// refused as "author must be 32 bytes". A pointer that cannot be read back is
|
|
158
|
+
// worse than an error, because it is produced silently and only fails at
|
|
159
|
+
// whoever pastes it. Peek's guard is the one that ships.
|
|
160
|
+
const author = hexToBytes(pointer.pubkey);
|
|
161
|
+
if (author.length !== 32)
|
|
162
|
+
throw new Error(`author must be 32 bytes, got ${author.length}`);
|
|
163
|
+
push(TLV_AUTHOR, author);
|
|
164
|
+
// Kind is a 4-byte big-endian integer, not a decimal string.
|
|
165
|
+
push(TLV_KIND, new Uint8Array([
|
|
166
|
+
(pointer.kind >>> 24) & 0xff,
|
|
167
|
+
(pointer.kind >>> 16) & 0xff,
|
|
168
|
+
(pointer.kind >>> 8) & 0xff,
|
|
169
|
+
pointer.kind & 0xff,
|
|
170
|
+
]));
|
|
171
|
+
return bech32Encode('naddr', convertBits(bytes, 8, 5, true));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Decode `naddr1…`, with or without a `nostr:` prefix, in any case.
|
|
175
|
+
*
|
|
176
|
+
* Forgiving on purpose: what arrives here was pasted by a person, sometimes out
|
|
177
|
+
* of an email client that capitalised the first letter. `bech32Decode` is the
|
|
178
|
+
* strict primitive underneath — this lowercases first, so a mixed-case string
|
|
179
|
+
* that would be refused there is accepted here.
|
|
180
|
+
*/
|
|
181
|
+
export function decodeNaddr(encoded) {
|
|
182
|
+
const { hrp, data } = bech32Decode(encoded.replace(/^nostr:/i, '').toLowerCase());
|
|
183
|
+
if (hrp !== 'naddr')
|
|
184
|
+
throw new Error(`expected an naddr, got ${hrp}`);
|
|
185
|
+
const bytes = convertBits(data, 5, 8, false);
|
|
186
|
+
let identifier;
|
|
187
|
+
let pubkey;
|
|
188
|
+
let kind;
|
|
189
|
+
const relays = [];
|
|
190
|
+
for (let i = 0; i < bytes.length;) {
|
|
191
|
+
const type = bytes[i];
|
|
192
|
+
const length = bytes[i + 1];
|
|
193
|
+
const value = bytes.slice(i + 2, i + 2 + length);
|
|
194
|
+
if (value.length !== length)
|
|
195
|
+
throw new Error('truncated TLV');
|
|
196
|
+
i += 2 + length;
|
|
197
|
+
switch (type) {
|
|
198
|
+
case TLV_IDENTIFIER:
|
|
199
|
+
identifier = new TextDecoder().decode(new Uint8Array(value));
|
|
200
|
+
break;
|
|
201
|
+
case TLV_RELAY:
|
|
202
|
+
relays.push(new TextDecoder().decode(new Uint8Array(value)));
|
|
203
|
+
break;
|
|
204
|
+
case TLV_AUTHOR:
|
|
205
|
+
if (length !== 32)
|
|
206
|
+
throw new Error(`author must be 32 bytes, got ${length}`);
|
|
207
|
+
pubkey = bytesToHex(new Uint8Array(value));
|
|
208
|
+
break;
|
|
209
|
+
case TLV_KIND:
|
|
210
|
+
if (length !== 4)
|
|
211
|
+
throw new Error(`kind must be 4 bytes, got ${length}`);
|
|
212
|
+
kind = ((value[0] << 24) | (value[1] << 16) | (value[2] << 8) | value[3]) >>> 0;
|
|
213
|
+
break;
|
|
214
|
+
// Unknown TLV types are skipped, not rejected — that is what lets the
|
|
215
|
+
// format gain fields without breaking existing decoders.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (identifier === undefined || pubkey === undefined || kind === undefined) {
|
|
219
|
+
throw new Error('naddr is missing identifier, author or kind');
|
|
220
|
+
}
|
|
221
|
+
return { identifier, pubkey, kind, relays };
|
|
222
|
+
}
|
|
223
|
+
/** `<kind>:<pubkey>:<d>` — the form used in `a` tags and relay filters. */
|
|
224
|
+
export function pointerToAddress(pointer) {
|
|
225
|
+
return `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The inverse: `<kind>:<pubkey>:<d>` back to a pointer.
|
|
229
|
+
*
|
|
230
|
+
* An address carries no relay hints — `naddr` has a TLV for them and an `a` tag
|
|
231
|
+
* does not — so the relays come back empty. That is a real loss of information,
|
|
232
|
+
* not an oversight: it is why the two forms are not interchangeable and why the
|
|
233
|
+
* encoder is not simply run in reverse.
|
|
234
|
+
*
|
|
235
|
+
* The `d` identifier may itself contain colons, so only the first two are
|
|
236
|
+
* separators.
|
|
237
|
+
*/
|
|
238
|
+
export function addressToPointer(address) {
|
|
239
|
+
const first = address.indexOf(':');
|
|
240
|
+
const second = address.indexOf(':', first + 1);
|
|
241
|
+
if (first < 1 || second < 0)
|
|
242
|
+
throw new Error(`not an address: ${address.slice(0, 40)}`);
|
|
243
|
+
const kind = Number(address.slice(0, first));
|
|
244
|
+
const pubkey = address.slice(first + 1, second);
|
|
245
|
+
const identifier = address.slice(second + 1);
|
|
246
|
+
if (!Number.isInteger(kind) || !/^[0-9a-f]{64}$/i.test(pubkey)) {
|
|
247
|
+
throw new Error(`not an address: ${address.slice(0, 40)}`);
|
|
248
|
+
}
|
|
249
|
+
return { kind, pubkey: pubkey.toLowerCase(), identifier, relays: [] };
|
|
250
|
+
}
|
|
251
|
+
/** `<kind>:<pubkey>:<d>` → `naddr1…`. */
|
|
252
|
+
export function addrToNaddr(address, relays = []) {
|
|
253
|
+
const [kind, pubkey, ...rest] = address.split(':');
|
|
254
|
+
return encodeNaddr({ kind: Number(kind), pubkey, identifier: rest.join(':'), relays });
|
|
255
|
+
}
|
|
256
|
+
/** `naddr1…` → `<kind>:<pubkey>:<d>`. */
|
|
257
|
+
export function naddrToAddr(encoded) {
|
|
258
|
+
return pointerToAddress(decodeNaddr(encoded));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A pointer from *either* form a reference arrives in.
|
|
262
|
+
*
|
|
263
|
+
* A message composed in an app carries `nostr:naddr1…` in its body. The same
|
|
264
|
+
* message read back off the relay carries the same reference as an `a` tag,
|
|
265
|
+
* which is a plain `<kind>:<pubkey>:<d>` address — that is what the NIP says a
|
|
266
|
+
* tag holds.
|
|
267
|
+
*
|
|
268
|
+
* Both name one object. Accepting only the first is what made a reference stop
|
|
269
|
+
* rendering the moment its own message came back from the relay (FEE-2).
|
|
270
|
+
*/
|
|
271
|
+
export function referenceToPointer(input) {
|
|
272
|
+
const trimmed = input.replace(/^nostr:/i, '');
|
|
273
|
+
return trimmed.toLowerCase().startsWith('naddr1')
|
|
274
|
+
? decodeNaddr(trimmed)
|
|
275
|
+
: addressToPointer(trimmed);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Every `nostr:naddr1…` in a body of text (NIP-27).
|
|
279
|
+
*
|
|
280
|
+
* The character class is bech32's own alphabet, which excludes `1`, `b`, `i`
|
|
281
|
+
* and `o`, so a match ends cleanly at punctuation without a lookahead.
|
|
282
|
+
*/
|
|
283
|
+
export const NADDR_RE = /nostr:(naddr1[023456789acdefghjklmnpqrstuvwxyz]+)/gi;
|
|
284
|
+
export function findNaddrs(text) {
|
|
285
|
+
return [...text.matchAll(NADDR_RE)].map((m) => m[1]);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The same text with every `nostr:naddr1…` taken out (PEEK-18).
|
|
289
|
+
*
|
|
290
|
+
* A reference that resolves into a widget should not also sit in the prose as
|
|
291
|
+
* sixty characters of bech32: the widget *is* the reference, rendered. Ship
|
|
292
|
+
* appends one to every thread it starts about an issue, so leaving it in means
|
|
293
|
+
* most cross-app messages open with a wall of noise nobody reads.
|
|
294
|
+
*
|
|
295
|
+
* Whitespace is repaired rather than merely removed. A pointer is usually
|
|
296
|
+
* trailing or on a line of its own, and deleting it in place otherwise leaves a
|
|
297
|
+
* double space mid-sentence or a hole between paragraphs — both of which look
|
|
298
|
+
* like the message itself is broken. Runs of blanks collapse *within* a line
|
|
299
|
+
* only, so indentation and paragraph breaks survive.
|
|
300
|
+
*
|
|
301
|
+
* Display-only. The stored body keeps the pointer, which is what lets the
|
|
302
|
+
* reference still be found, resolved and followed.
|
|
303
|
+
*/
|
|
304
|
+
export function stripNaddrs(text) {
|
|
305
|
+
// Text with no pointer comes back byte-identical. The repairs below are only
|
|
306
|
+
// ever justified by a removal, and a display helper that reflows somebody's
|
|
307
|
+
// indentation for free would be a bug wearing a tidy-up's clothes.
|
|
308
|
+
if (findNaddrs(text).length === 0)
|
|
309
|
+
return text;
|
|
310
|
+
return (text
|
|
311
|
+
// Take the blanks on either side along with the pointer, then put a single
|
|
312
|
+
// space back only when it stood between words on one line. Doing it in one
|
|
313
|
+
// pass is what keeps the repair local to the hole.
|
|
314
|
+
.replace(new RegExp(`([^\\S\\n]*)${NADDR_RE.source}([^\\S\\n]*)`, 'gi'), (_match, before, _addr, after) => (before && after ? ' ' : ''))
|
|
315
|
+
// A pointer alone on its line leaves the line's newlines behind on both
|
|
316
|
+
// sides, which reads as an unexplained gap.
|
|
317
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
318
|
+
.trim());
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=nip19.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nip19.js","sourceRoot":"","sources":["../src/nip19.ts"],"names":[],"mappings":"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"}
|
package/dist/nip98.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type SignedEvent, type UnsignedEvent } from './events.js';
|
|
2
|
+
import type { Signer } from './sign.js';
|
|
3
|
+
/** The relay rejects auth events outside ±60s (`TIMESTAMP_TOLERANCE_SECS`). */
|
|
4
|
+
export declare const TIMESTAMP_TOLERANCE_SECS = 60;
|
|
5
|
+
/**
|
|
6
|
+
* Buzz's `normalize_url` (nip98.rs:145): parse, strip trailing slashes from the
|
|
7
|
+
* path, re-serialize. We only need it to keep our own `u` tag canonical.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeUrl(raw: string): string;
|
|
10
|
+
/** What `buildUnsignedAuthEvent` needs. Named so callers can pass it around. */
|
|
11
|
+
export interface AuthEventArgs {
|
|
12
|
+
/** Left empty when a remote signer will overwrite it with the token's subject. */
|
|
13
|
+
pubkey: string;
|
|
14
|
+
url: string;
|
|
15
|
+
method: string;
|
|
16
|
+
/** Request body, when there is one — adds the `payload` tag. */
|
|
17
|
+
body?: string;
|
|
18
|
+
/** Override for tests; defaults to now. */
|
|
19
|
+
nowMs?: number;
|
|
20
|
+
/** Override for tests; defaults to random. Must differ per request. */
|
|
21
|
+
nonce?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the unsigned kind:27235 event backing an `Authorization: Nostr …` header.
|
|
25
|
+
*
|
|
26
|
+
* `url` must be the full request URL with the same host the relay will see —
|
|
27
|
+
* `nip98_expected_url` (bridge.rs:195) reconstructs it as
|
|
28
|
+
* `{http|https}://{host}{path}` from the request's own Host header.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildUnsignedAuthEvent(args: AuthEventArgs): UnsignedEvent;
|
|
31
|
+
/**
|
|
32
|
+
* Base64 without Node's `Buffer`, so this works in a browser, in Convex's
|
|
33
|
+
* default runtime and under Node from one build.
|
|
34
|
+
*/
|
|
35
|
+
export declare function base64(input: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* The full header value: `Nostr <base64(signed kind-27235 event JSON)>`
|
|
38
|
+
* (bridge.rs:81-93 strips the `Nostr ` prefix and base64-decodes the rest).
|
|
39
|
+
*
|
|
40
|
+
* Uses the utf8-aware `base64` above rather than a bare `btoa`, which throws on
|
|
41
|
+
* any code point above U+00FF.
|
|
42
|
+
*/
|
|
43
|
+
export declare function authorizationHeaderFor(signed: SignedEvent): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build, sign and encode the header in one step.
|
|
46
|
+
*
|
|
47
|
+
* **The auth event is signed by the same signer as the content**, which is what
|
|
48
|
+
* makes a keyless signer work at all: Buzz's bridge authenticates with NIP-98,
|
|
49
|
+
* so an app holding a perfectly signed issue and no way to sign a `27235` can
|
|
50
|
+
* still publish nothing. Signing content but not auth leaves the seam
|
|
51
|
+
* half-built, and it looks finished (PEEK-44).
|
|
52
|
+
*
|
|
53
|
+
* That rule has exactly one definition, here, and {@link Relay} uses it rather
|
|
54
|
+
* than inlining it — because an app with its own transport needs the same rule
|
|
55
|
+
* and would otherwise write it out again.
|
|
56
|
+
*
|
|
57
|
+
* Async for the same reason `Signer.sign` is: a remote signer or a browser
|
|
58
|
+
* extension cannot answer synchronously.
|
|
59
|
+
*/
|
|
60
|
+
export declare function authorizationHeader(signer: Signer, args: Omit<AuthEventArgs, 'pubkey'>): Promise<string>;
|
|
61
|
+
//# sourceMappingURL=nip98.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nip98.d.ts","sourceRoot":"","sources":["../src/nip98.ts"],"names":[],"mappings":"AAgDA,OAAO,EAAuB,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAW1C;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED,gFAAgF;AAChF,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CAoBzE;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAElE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAClC,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
package/dist/nip98.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIP-98 HTTP auth for Buzz's REST bridge.
|
|
3
|
+
*
|
|
4
|
+
* Buzz's bridge (`POST /events`, `POST /query`) authenticates with NIP-98, **not**
|
|
5
|
+
* NIP-42 — no challenge/response and no persistent connection, which is what
|
|
6
|
+
* makes it usable from a request-scoped server runtime that cannot hold a socket,
|
|
7
|
+
* and from a plain `fetch` in a browser.
|
|
8
|
+
*
|
|
9
|
+
* Written against the relay's actual verifier, `verify_nip98_event`
|
|
10
|
+
* (crates/buzz-auth/src/nip98.rs:55). Its rules, in order:
|
|
11
|
+
*
|
|
12
|
+
* 1. kind must be 27235
|
|
13
|
+
* 2. valid Schnorr signature + id hash
|
|
14
|
+
* 3. `created_at` within ±60s of the relay's clock
|
|
15
|
+
* 4. `u` tag must equal the expected URL after normalization
|
|
16
|
+
* 5. `method` tag must match (case-insensitive)
|
|
17
|
+
* 6. if a `payload` tag is present AND a body was sent, sha256(body) must match
|
|
18
|
+
*
|
|
19
|
+
* Two things that bite in practice:
|
|
20
|
+
*
|
|
21
|
+
* - **No loopback aliasing.** `localhost`, `127.0.0.1` and `::1` are distinct
|
|
22
|
+
* hosts to the verifier (deliberately — it is the row-zero community binding).
|
|
23
|
+
* The `u` tag must use the same host string as the request's Host header.
|
|
24
|
+
* - **Single use, and "fresh" is not enough.** Each auth event id is recorded in
|
|
25
|
+
* a Redis seen-set (`check_nip98_replay`, bridge.rs:135). Rebuilding the header
|
|
26
|
+
* per request does *not* guarantee a new id: `created_at` has one-second
|
|
27
|
+
* resolution, so two requests with the same URL, method and body inside the
|
|
28
|
+
* same second produce a byte-identical event and the second is rejected with
|
|
29
|
+
* `NIP-98: replay detected`. Verified against the live relay. Hence the
|
|
30
|
+
* `nonce` tag below — it is what makes each auth event unique.
|
|
31
|
+
*
|
|
32
|
+
* ## Why the builder is unsigned, and the signer is somebody else's problem
|
|
33
|
+
*
|
|
34
|
+
* Before SHA-3 this existed twice with a real divergence: Peek had split the
|
|
35
|
+
* builder into `buildUnsignedAuthEvent` plus a signing step, because Peek holds
|
|
36
|
+
* no keys and must sign through Estiva ID; Ship still had the combined
|
|
37
|
+
* build-and-sign form. **Peek's shape is what ships**, because it is the one that
|
|
38
|
+
* works for a keyless app — and a keyed app composes it with `signEvent` in one
|
|
39
|
+
* line. The tag layout then has exactly one definition, which is the point: it is
|
|
40
|
+
* part of the event id preimage, and two copies drifting would produce ids the
|
|
41
|
+
* relay computes differently.
|
|
42
|
+
*
|
|
43
|
+
* `nostr-tools/nip98` was evaluated and does **not** fit: its `getToken` emits
|
|
44
|
+
* `u`, `method` and `payload` and no nonce, so identical requests in one second
|
|
45
|
+
* collide on Buzz's replay set. Measured, not assumed — see the README.
|
|
46
|
+
*/
|
|
47
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
48
|
+
import { bytesToHex, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
|
|
49
|
+
import { KIND } from './events.js';
|
|
50
|
+
/** The relay rejects auth events outside ±60s (`TIMESTAMP_TOLERANCE_SECS`). */
|
|
51
|
+
export const TIMESTAMP_TOLERANCE_SECS = 60;
|
|
52
|
+
/**
|
|
53
|
+
* Buzz's `normalize_url` (nip98.rs:145): parse, strip trailing slashes from the
|
|
54
|
+
* path, re-serialize. We only need it to keep our own `u` tag canonical.
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeUrl(raw) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(raw);
|
|
59
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
|
|
60
|
+
return parsed.toString();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return raw.toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build the unsigned kind:27235 event backing an `Authorization: Nostr …` header.
|
|
68
|
+
*
|
|
69
|
+
* `url` must be the full request URL with the same host the relay will see —
|
|
70
|
+
* `nip98_expected_url` (bridge.rs:195) reconstructs it as
|
|
71
|
+
* `{http|https}://{host}{path}` from the request's own Host header.
|
|
72
|
+
*/
|
|
73
|
+
export function buildUnsignedAuthEvent(args) {
|
|
74
|
+
const tags = [
|
|
75
|
+
['u', normalizeUrl(args.url)],
|
|
76
|
+
['method', args.method.toUpperCase()],
|
|
77
|
+
// Uniqueness, not security: without it, two identical requests in the same
|
|
78
|
+
// second collide on the event id and the relay rejects the second as a
|
|
79
|
+
// replay. The verifier ignores tags it does not know (it looks up `u`,
|
|
80
|
+
// `method` and `payload` by name), so this is safe to add.
|
|
81
|
+
['nonce', args.nonce ?? bytesToHex(randomBytes(16))],
|
|
82
|
+
];
|
|
83
|
+
if (args.body !== undefined) {
|
|
84
|
+
tags.push(['payload', bytesToHex(sha256(utf8ToBytes(args.body)))]);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
pubkey: args.pubkey,
|
|
88
|
+
created_at: Math.floor((args.nowMs ?? Date.now()) / 1000),
|
|
89
|
+
kind: KIND.HTTP_AUTH,
|
|
90
|
+
tags,
|
|
91
|
+
content: '',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Base64 without Node's `Buffer`, so this works in a browser, in Convex's
|
|
96
|
+
* default runtime and under Node from one build.
|
|
97
|
+
*/
|
|
98
|
+
export function base64(input) {
|
|
99
|
+
const bytes = utf8ToBytes(input);
|
|
100
|
+
let binary = '';
|
|
101
|
+
for (const b of bytes)
|
|
102
|
+
binary += String.fromCharCode(b);
|
|
103
|
+
return btoa(binary);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The full header value: `Nostr <base64(signed kind-27235 event JSON)>`
|
|
107
|
+
* (bridge.rs:81-93 strips the `Nostr ` prefix and base64-decodes the rest).
|
|
108
|
+
*
|
|
109
|
+
* Uses the utf8-aware `base64` above rather than a bare `btoa`, which throws on
|
|
110
|
+
* any code point above U+00FF.
|
|
111
|
+
*/
|
|
112
|
+
export function authorizationHeaderFor(signed) {
|
|
113
|
+
return `Nostr ${base64(JSON.stringify(signed))}`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build, sign and encode the header in one step.
|
|
117
|
+
*
|
|
118
|
+
* **The auth event is signed by the same signer as the content**, which is what
|
|
119
|
+
* makes a keyless signer work at all: Buzz's bridge authenticates with NIP-98,
|
|
120
|
+
* so an app holding a perfectly signed issue and no way to sign a `27235` can
|
|
121
|
+
* still publish nothing. Signing content but not auth leaves the seam
|
|
122
|
+
* half-built, and it looks finished (PEEK-44).
|
|
123
|
+
*
|
|
124
|
+
* That rule has exactly one definition, here, and {@link Relay} uses it rather
|
|
125
|
+
* than inlining it — because an app with its own transport needs the same rule
|
|
126
|
+
* and would otherwise write it out again.
|
|
127
|
+
*
|
|
128
|
+
* Async for the same reason `Signer.sign` is: a remote signer or a browser
|
|
129
|
+
* extension cannot answer synchronously.
|
|
130
|
+
*/
|
|
131
|
+
export async function authorizationHeader(signer, args) {
|
|
132
|
+
return authorizationHeaderFor(await signer.sign(buildUnsignedAuthEvent({ ...args, pubkey: signer.pubkey })));
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=nip98.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nip98.js","sourceRoot":"","sources":["../src/nip98.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAE,IAAI,EAAuD,MAAM,aAAa,CAAA;AAGvF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAA;AAW1C;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QACrD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC,WAAW,EAAE,CAAA;IAC1B,CAAC;AACH,CAAC;AAgBD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAmB;IACxD,MAAM,IAAI,GAAe;QACvB,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QACrC,2EAA2E;QAC3E,uEAAuE;QACvE,uEAAuE;QACvE,2DAA2D;QAC3D,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;KACrD,CAAA;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACpE,CAAC;IACD,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,EAAE,IAAI,CAAC,SAAS;QACpB,IAAI;QACJ,OAAO,EAAE,EAAE;KACZ,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IAChC,IAAI,MAAM,GAAG,EAAE,CAAA;IACf,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IACvD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAmB;IACxD,OAAO,SAAS,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAc,EACd,IAAmC;IAEnC,OAAO,sBAAsB,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;AAC9G,CAAC"}
|
package/dist/sign.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type SignedEvent, type UnsignedEvent } from './events.js';
|
|
2
|
+
/** Derive the 64-char hex x-only public key for a secret key. */
|
|
3
|
+
export declare function publicKeyFromSecret(secretKeyHex: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Compute the event id and sign it, producing a relay-submittable event.
|
|
6
|
+
*
|
|
7
|
+
* Throws if `unsigned.pubkey` does not match the secret key — a mismatch would
|
|
8
|
+
* produce an event the relay silently rejects as an invalid signature, which is
|
|
9
|
+
* painful to debug from the other side.
|
|
10
|
+
*/
|
|
11
|
+
export declare function signEvent(unsigned: UnsignedEvent, secretKeyHex: string): SignedEvent;
|
|
12
|
+
/**
|
|
13
|
+
* Who signs, decided once and injected everywhere (PEEK-44).
|
|
14
|
+
*
|
|
15
|
+
* ## Why this exists at all
|
|
16
|
+
*
|
|
17
|
+
* The suite claims apps built by different people, sharing no code and no
|
|
18
|
+
* database, can work on the same data. Wiring an app directly to one login
|
|
19
|
+
* service would quietly undercut that — it would only work for people with an
|
|
20
|
+
* Estiva account.
|
|
21
|
+
*
|
|
22
|
+
* Depending on a *signer* does not. Separating the signer from the client is
|
|
23
|
+
* ordinary Nostr architecture (NIP-07, NIP-46), so an app stays honest for
|
|
24
|
+
* anyone who wants to point their own signer at it, and "sign in with Estiva ID"
|
|
25
|
+
* becomes one implementation among several rather than an assumption baked into
|
|
26
|
+
* the data layer. It is also the seam that lets this package stay ignorant of
|
|
27
|
+
* identity: `@estiva-app/identity` implements this interface, and nothing here
|
|
28
|
+
* knows that it exists.
|
|
29
|
+
*
|
|
30
|
+
* ## The two decisions in the interface
|
|
31
|
+
*
|
|
32
|
+
* **`pubkey` is synchronous.** Every event builder needs it before there is
|
|
33
|
+
* anything to sign — `buildMessage(pubkey, …)` — so making it a promise would
|
|
34
|
+
* put an `await` in front of every construction site for no benefit. You know
|
|
35
|
+
* who you are before you sign; implementations that must ask (NIP-07) resolve it
|
|
36
|
+
* once, at construction.
|
|
37
|
+
*
|
|
38
|
+
* **`sign` is asynchronous**, because two of the three implementations are: a
|
|
39
|
+
* remote call to `/sign` and a round trip to a browser extension. The local key
|
|
40
|
+
* is the odd one out, and it is cheaper for it to return a resolved promise than
|
|
41
|
+
* for the interface to pretend signing is always instant.
|
|
42
|
+
*
|
|
43
|
+
* ## What a signer deliberately cannot do
|
|
44
|
+
*
|
|
45
|
+
* Sign as somebody else. Every implementation attributes the event to its own
|
|
46
|
+
* `pubkey` and ignores whatever the caller put there, which is the same
|
|
47
|
+
* guarantee `/sign` makes server-side. Code that genuinely needs to produce a
|
|
48
|
+
* mismatched event — Ship's `scripts/verify.ts` forges one to prove the relay
|
|
49
|
+
* rejects it — uses {@link signEvent} directly and holds a raw key to do it.
|
|
50
|
+
*/
|
|
51
|
+
/** How the current signer authenticates, for anything that needs to say so. */
|
|
52
|
+
export type SignerKind = 'local' | 'estiva-id' | 'nip07';
|
|
53
|
+
export interface Signer {
|
|
54
|
+
/** The pubkey every event from this signer is attributed to. */
|
|
55
|
+
readonly pubkey: string;
|
|
56
|
+
readonly kind: SignerKind;
|
|
57
|
+
/** Attributes the event to {@link pubkey}, whatever the caller supplied. */
|
|
58
|
+
sign(unsigned: UnsignedEvent): Promise<SignedEvent>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A signer holding a raw secret key.
|
|
62
|
+
*
|
|
63
|
+
* The honest name for what a script has always done. Used directly by scripts,
|
|
64
|
+
* which have no `localStorage`, and underneath an app's per-browser identity.
|
|
65
|
+
*/
|
|
66
|
+
export declare function secretKeySigner(secretKeyHex: string, kind?: SignerKind): Signer;
|
|
67
|
+
//# sourceMappingURL=sign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAElF,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,GAAG,WAAW,CAUpF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,CAAA;AAExD,MAAM,WAAW,MAAM;IACrB,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,4EAA4E;IAC5E,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACpD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,GAAE,UAAoB,GAAG,MAAM,CASxF"}
|