@ambasada/sdk 0.7.4 → 0.8.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/dist/index.js CHANGED
@@ -1,3 +1,190 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // ../protocol/src/index.ts
8
+ var src_exports = {};
9
+ __export(src_exports, {
10
+ APP_EVENT_TYPES: () => APP_EVENT_TYPES,
11
+ APP_NOTIFY_PATH: () => APP_NOTIFY_PATH,
12
+ APP_SIGNATURE_HEADER: () => APP_SIGNATURE_HEADER,
13
+ APP_TIMESTAMP_HEADER: () => APP_TIMESTAMP_HEADER,
14
+ APP_WEBHOOK_TOLERANCE_S: () => APP_WEBHOOK_TOLERANCE_S,
15
+ AppWebhookError: () => AppWebhookError,
16
+ DEFAULT_HUB_URL: () => DEFAULT_HUB_URL,
17
+ DEFAULT_ISSUER: () => DEFAULT_ISSUER,
18
+ EMPTY_ROOT: () => EMPTY_ROOT,
19
+ ERROR_STATUS: () => ERROR_STATUS,
20
+ FETCH_TIMEOUT_MS: () => FETCH_TIMEOUT_MS,
21
+ HEADERS: () => HEADERS,
22
+ HOSTED_MANIFEST: () => HOSTED_MANIFEST,
23
+ ICON_SIZE: () => ICON_SIZE,
24
+ MANIFEST_CATEGORIES: () => MANIFEST_CATEGORIES,
25
+ MANIFEST_ID: () => MANIFEST_ID,
26
+ MANIFEST_KINDS: () => MANIFEST_KINDS,
27
+ MANIFEST_LIMITS: () => MANIFEST_LIMITS,
28
+ MANIFEST_SCHEMA: () => MANIFEST_SCHEMA,
29
+ MANIFEST_URL_PATH: () => MANIFEST_URL_PATH,
30
+ MANIFEST_VISIBILITY: () => MANIFEST_VISIBILITY,
31
+ MAX_DEPTH: () => MAX_DEPTH,
32
+ MAX_PRICE_NANO: () => MAX_PRICE_NANO,
33
+ MIN_PRICE_NANO: () => MIN_PRICE_NANO,
34
+ NANO_PER_TON: () => NANO_PER_TON,
35
+ NOTIFY_LIMITS: () => NOTIFY_LIMITS,
36
+ RECEIPT_HEADER: () => RECEIPT_HEADER,
37
+ ReceiptError: () => ReceiptError,
38
+ SHELL_TOKEN_TTL_S: () => SHELL_TOKEN_TTL_S,
39
+ SLOW_ENTRY_MS: () => SLOW_ENTRY_MS,
40
+ SYSTEM_ACCOUNT: () => SYSTEM_ACCOUNT,
41
+ VillageError: () => VillageError,
42
+ WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
43
+ WebhookError: () => WebhookError,
44
+ appNotifyBody: () => appNotifyBody,
45
+ appNotifyResult: () => appNotifyResult,
46
+ appWebhookHeaders: () => appWebhookHeaders,
47
+ base64url: () => base64url,
48
+ checkHtml: () => checkHtml,
49
+ checkIcon: () => checkIcon,
50
+ checkRemote: () => checkRemote,
51
+ createReceiptSigner: () => createReceiptSigner,
52
+ decodeLeaf: () => decodeLeaf,
53
+ encodeLeaf: () => encodeLeaf,
54
+ equalBytes: () => equalBytes,
55
+ findingLabel: () => findingLabel,
56
+ formatCheckpoint: () => formatCheckpoint,
57
+ fromBase64: () => fromBase64,
58
+ fromBase64url: () => fromBase64url,
59
+ fromNano: () => fromNano,
60
+ generateShellKey: () => generateShellKey,
61
+ generateSigningKey: () => generateSigningKey,
62
+ generateWebhookSecret: () => generateWebhookSecret,
63
+ inclusionProof: () => inclusionProof,
64
+ isErrorBody: () => isErrorBody,
65
+ jwksLoader: () => jwksLoader,
66
+ leafHash: () => leafHash,
67
+ listingLine: () => listingLine,
68
+ nodeHash: () => nodeHash,
69
+ parseCheckpoint: () => parseCheckpoint,
70
+ parseVerifierKey: () => parseVerifierKey,
71
+ pngInfo: () => pngInfo,
72
+ publicJwkOf: () => publicJwkOf,
73
+ randomId: () => randomId,
74
+ rootFromLeafHashes: () => rootFromLeafHashes,
75
+ sha256Hex: () => sha256Hex,
76
+ signAppWebhook: () => signAppWebhook,
77
+ signNote: () => signNote,
78
+ signShellToken: () => signShellToken,
79
+ signWebhook: () => signWebhook,
80
+ splitPoint: () => splitPoint,
81
+ summarizeCheck: () => summarizeCheck,
82
+ toBase64: () => toBase64,
83
+ toNano: () => toNano,
84
+ validateManifest: () => validateManifest,
85
+ verifierKey: () => verifierKey,
86
+ verifyAppWebhook: () => verifyAppWebhook,
87
+ verifyCheckoutReceipt: () => verifyCheckoutReceipt,
88
+ verifyInclusion: () => verifyInclusion,
89
+ verifyNote: () => verifyNote,
90
+ verifyReceipt: () => verifyReceipt,
91
+ verifyWebhook: () => verifyWebhook,
92
+ webhookHeaders: () => webhookHeaders
93
+ });
94
+
95
+ // ../protocol/src/money.ts
96
+ var NANO_PER_TON = 1e9;
97
+ var MIN_PRICE_NANO = NANO_PER_TON / 10;
98
+ var MAX_PRICE_NANO = 500 * NANO_PER_TON;
99
+ var DECIMAL = /^(\d+)(?:\.(\d{1,9}))?$/;
100
+ function toNano(value) {
101
+ let text;
102
+ if (typeof value === "number") {
103
+ if (!Number.isFinite(value) || value < 0) return null;
104
+ text = value.toFixed(9);
105
+ } else if (typeof value === "string") {
106
+ text = value.trim();
107
+ } else {
108
+ return null;
109
+ }
110
+ const match = DECIMAL.exec(text);
111
+ if (!match) return null;
112
+ const whole = Number(match[1]);
113
+ const frac = Number((match[2] ?? "").padEnd(9, "0"));
114
+ const nano = whole * NANO_PER_TON + frac;
115
+ return Number.isSafeInteger(nano) ? nano : null;
116
+ }
117
+ function fromNano(nano) {
118
+ return Number((nano / NANO_PER_TON).toFixed(9));
119
+ }
120
+
121
+ // ../protocol/src/errors.ts
122
+ var ERROR_STATUS = {
123
+ // EXCHANGE.md §5 vocabulary
124
+ INSUFFICIENT_FUNDS: 402,
125
+ OFFLINE: 503,
126
+ TIMEOUT: 504,
127
+ SCHEMA_INVALID: 502,
128
+ CAP_BUYER_HOURLY: 429,
129
+ // the company's spending limit for the last hour
130
+ CAP_SELLER_RATE: 429,
131
+ // reserved: seller rate limits land with the rules milestone (day 11)
132
+ DEAL_EXHAUSTED: 409,
133
+ // reserved: deals land with the registry/exchange work
134
+ DEPTH_EXCEEDED: 422,
135
+ BUDGET_EXCEEDED: 402,
136
+ CAP_INBOX_RATE: 429,
137
+ // inbox texts: 20 an hour, 1 unsolicited per counterparty an hour (EXCHANGE.md §8)
138
+ // Additions needed by the gateway and bank
139
+ UPSTREAM_ERROR: 502,
140
+ PRICE_ABOVE_MAX: 409,
141
+ SELF_PURCHASE: 403,
142
+ INVALID_REQUEST: 400,
143
+ UNAUTHORIZED: 401,
144
+ NOT_FOUND: 404,
145
+ CONFLICT: 409,
146
+ IDEMPOTENCY_CONFLICT: 422,
147
+ IDEMPOTENCY_IN_PROGRESS: 409,
148
+ HOLD_NOT_ACTIVE: 409,
149
+ INTERNAL: 500,
150
+ // Spending limits a team sets for its company wallet (DESIGN.md §0 "Controlling agent spending")
151
+ CAP_PER_PAYMENT: 403,
152
+ CAP_BUYER_DAILY: 429,
153
+ CAP_PAYEE_DAILY: 429,
154
+ // A person's spend at one company per venue day (DESIGN.md §5 "Visitor rules")
155
+ CAP_VISITOR_DAILY: 429,
156
+ // Agent payments over the automatic limit wait for the team (DESIGN.md §5 "Agent approvals")
157
+ APPROVAL_REQUIRED: 403,
158
+ APPROVAL_INVALID: 403,
159
+ APPROVALS_PENDING: 429,
160
+ // Model and tool provider (LLMOK, docs/LLMOK.md): the village's provider budget is spent, or the
161
+ // provider refused the model/tool/key (a configuration problem on our side). Never the caller's fault.
162
+ PROVIDER_BUDGET: 503,
163
+ PROVIDER_DENIED: 502,
164
+ // Deploy-freeze and unattended windows: agents keep running, but config and steer are closed (DESIGN.md §5)
165
+ FROZEN: 403
166
+ };
167
+ var VillageError = class extends Error {
168
+ code;
169
+ status;
170
+ details;
171
+ constructor(code, message, details = {}) {
172
+ super(message);
173
+ this.name = "VillageError";
174
+ this.code = code;
175
+ this.status = ERROR_STATUS[code];
176
+ this.details = details;
177
+ }
178
+ toBody() {
179
+ return { error: { code: this.code, message: this.message, ...this.details } };
180
+ }
181
+ };
182
+ function isErrorBody(value) {
183
+ if (typeof value !== "object" || value === null) return false;
184
+ const error = value.error;
185
+ return typeof error === "object" && error !== null && typeof error.code === "string";
186
+ }
187
+
1
188
  // ../protocol/src/encoding.ts
2
189
  var encoder = new TextEncoder();
3
190
  var decoder = new TextDecoder();
@@ -7,6 +194,11 @@ function utf8(text) {
7
194
  function fromUtf8(bytes) {
8
195
  return decoder.decode(bytes);
9
196
  }
197
+ function base64url(bytes) {
198
+ let binary = "";
199
+ for (const byte of bytes) binary += String.fromCharCode(byte);
200
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
201
+ }
10
202
  function fromBase64url(text) {
11
203
  const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4);
12
204
  const binary = atob(padded);
@@ -21,9 +213,14 @@ function toBytes(data) {
21
213
  }
22
214
  async function sha256Hex(data) {
23
215
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", toBytes(data)));
24
- let hex2 = "";
25
- for (const byte of digest) hex2 += byte.toString(16).padStart(2, "0");
26
- return hex2;
216
+ let hex3 = "";
217
+ for (const byte of digest) hex3 += byte.toString(16).padStart(2, "0");
218
+ return hex3;
219
+ }
220
+ function randomId(prefix, bytes = 12) {
221
+ const raw = new Uint8Array(bytes);
222
+ crypto.getRandomValues(raw);
223
+ return `${prefix}_${base64url(raw)}`;
27
224
  }
28
225
 
29
226
  // ../protocol/src/receipt.ts
@@ -38,6 +235,38 @@ var ReceiptError = class extends Error {
38
235
  reason;
39
236
  };
40
237
  var ED25519 = { name: "Ed25519" };
238
+ function publicJwkOf(jwk) {
239
+ return { kty: "OKP", crv: "Ed25519", x: jwk.x, kid: jwk.kid ?? jwk.x.slice(0, 16), alg: "EdDSA", use: "sig" };
240
+ }
241
+ async function generateSigningKey() {
242
+ const pair = await crypto.subtle.generateKey(ED25519, true, ["sign", "verify"]);
243
+ const exported = await crypto.subtle.exportKey("jwk", pair.privateKey);
244
+ const kid = `vk-${(await sha256Hex(exported.x)).slice(0, 12)}`;
245
+ const privateJwk = { kty: "OKP", crv: "Ed25519", x: exported.x, d: exported.d, kid, alg: "EdDSA" };
246
+ return { privateJwk, publicJwk: publicJwkOf(privateJwk) };
247
+ }
248
+ async function createReceiptSigner(privateJwk) {
249
+ if (!privateJwk.d) throw new Error("receipt signing key must be a private Ed25519 JWK (missing d)");
250
+ const publicJwk = publicJwkOf(privateJwk);
251
+ const kid = publicJwk.kid;
252
+ const key = await crypto.subtle.importKey(
253
+ "jwk",
254
+ { kty: "OKP", crv: "Ed25519", x: privateJwk.x, d: privateJwk.d },
255
+ ED25519,
256
+ false,
257
+ ["sign"]
258
+ );
259
+ const header3 = base64url(utf8(JSON.stringify({ alg: "EdDSA", typ: "JWT", kid })));
260
+ return {
261
+ kid,
262
+ publicJwk,
263
+ async sign(claims) {
264
+ const signingInput = `${header3}.${base64url(utf8(JSON.stringify(claims)))}`;
265
+ const signature = new Uint8Array(await crypto.subtle.sign(ED25519, key, utf8(signingInput)));
266
+ return `${signingInput}.${base64url(signature)}`;
267
+ }
268
+ };
269
+ }
41
270
  async function verifyReceipt(token, options) {
42
271
  const claims = await verifySigned(token, options);
43
272
  if (claims.kind !== void 0 || typeof claims.call_id !== "string" || typeof claims.body_sha256 !== "string") {
@@ -93,6 +322,584 @@ async function verifySigned(token, options) {
93
322
  if (claims.iss !== (options.issuer ?? DEFAULT_ISSUER)) throw new ReceiptError("wrong_issuer", `unexpected issuer ${String(claims.iss)}`);
94
323
  return claims;
95
324
  }
325
+ function jwksLoader(url, { ttlMs = 10 * 6e4 } = {}) {
326
+ let cached;
327
+ return async () => {
328
+ if (cached && Date.now() - cached.at < ttlMs) return cached.keys;
329
+ const res = await fetch(url);
330
+ if (!res.ok) throw new Error(`failed to load village keys from ${url}: ${res.status}`);
331
+ const { keys: keys3 } = await res.json();
332
+ cached = { keys: keys3, at: Date.now() };
333
+ return keys3;
334
+ };
335
+ }
336
+
337
+ // ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/_u64.js
338
+ var fromNumH = (n) => n / 2 ** 32 | 0;
339
+ var fromNumL = (n) => n >>> 0;
340
+ function setU64FromNum(view, byteOffset, n, isLE) {
341
+ const h = fromNumH(n);
342
+ const l = fromNumL(n);
343
+ view.setUint32(byteOffset, isLE ? l : h, isLE);
344
+ view.setUint32(byteOffset + 4, isLE ? h : l, isLE);
345
+ }
346
+
347
+ // ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/utils.js
348
+ function isBytes(a) {
349
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
350
+ }
351
+ var atitle = (title) => title ? `"${title}" ` : "";
352
+ function anumber(n, title = "") {
353
+ if (typeof n !== "number")
354
+ throw new TypeError(atitle(title) + "expected number, got " + typeof n);
355
+ if (!Number.isSafeInteger(n) || n < 0)
356
+ throw new RangeError(atitle(title) + "expected integer >= 0, got " + n);
357
+ return n;
358
+ }
359
+ function abytes(value, length, title = "") {
360
+ if (isBytes(value) && (length === void 0 || value.length === length))
361
+ return value;
362
+ if (length !== void 0)
363
+ anumber(length, "length");
364
+ const bytes = isBytes(value);
365
+ const ofLen = length !== void 0 ? ` of length ${length}` : "";
366
+ const got = bytes ? `length=${value.length}` : `type=${typeof value}`;
367
+ const message = atitle(title) + "expected Uint8Array" + ofLen + ", got " + got;
368
+ if (!bytes)
369
+ throw new TypeError(message);
370
+ throw new RangeError(message);
371
+ }
372
+ var aobject = (value, label) => {
373
+ if (value === null || typeof value !== "object" || Array.isArray(value))
374
+ throw new TypeError((label === "object" ? "" : `"${label}" `) + "expected object, got type=" + typeof value);
375
+ };
376
+ var aopts = (value, label) => {
377
+ aobject(value, label);
378
+ const proto = Object.getPrototypeOf(value);
379
+ if (proto !== Object.prototype && proto !== null)
380
+ throw new TypeError(`"${label}" expected plain object`);
381
+ if (Object.hasOwn(value, "__proto__"))
382
+ throw new TypeError(`"${label}.__proto__" is not allowed`);
383
+ };
384
+ function aexists(instance, checkFinished = true) {
385
+ if (instance.destroyed)
386
+ throw new Error("hash was destroyed");
387
+ if (checkFinished && instance.finished)
388
+ throw new Error("digest() was already called");
389
+ }
390
+ function aoutput(out, instance) {
391
+ abytes(out, void 0, "output");
392
+ const min = instance.outputLen;
393
+ if (!(out.length >= min)) {
394
+ throw new RangeError('"output" expected length >= ' + min);
395
+ }
396
+ }
397
+ function clean(...arrays) {
398
+ for (let i = 0; i < arrays.length; i++) {
399
+ arrays[i].fill(0);
400
+ }
401
+ }
402
+ function createView(arr) {
403
+ return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
404
+ }
405
+ function rotr(word, shift) {
406
+ return word << 32 - shift | word >>> shift;
407
+ }
408
+ function checkOpts(defaults, opts, title = "opts") {
409
+ aopts(defaults, "defaults");
410
+ if (opts !== void 0)
411
+ aopts(opts, title);
412
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), defaults, opts);
413
+ return merged;
414
+ }
415
+ function createHasher(hashCons, info = {}) {
416
+ if (typeof hashCons !== "function")
417
+ throw new TypeError('"hashCons" expected function, got type=' + typeof hashCons);
418
+ info = checkOpts({}, info, "info");
419
+ const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
420
+ const tmp = hashCons(void 0);
421
+ hashC.outputLen = tmp.outputLen;
422
+ hashC.blockLen = tmp.blockLen;
423
+ hashC.canXOF = tmp.canXOF;
424
+ hashC.create = (opts) => hashCons(opts);
425
+ Object.assign(hashC, info);
426
+ return Object.freeze(hashC);
427
+ }
428
+ var oidNist = (suffix) => ({
429
+ // Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet.
430
+ // Larger suffix values would need base-128 OID encoding and a different length byte.
431
+ oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
432
+ });
433
+
434
+ // ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/_md.js
435
+ function Chi(a, b, c) {
436
+ return a & b ^ ~a & c;
437
+ }
438
+ function Maj(a, b, c) {
439
+ return a & b ^ a & c ^ b & c;
440
+ }
441
+ var HashMD = class {
442
+ blockLen;
443
+ outputLen;
444
+ canXOF = false;
445
+ padOffset;
446
+ isLE;
447
+ // For partial updates less than block size
448
+ buffer;
449
+ view;
450
+ finished = false;
451
+ length = 0;
452
+ pos = 0;
453
+ destroyed = false;
454
+ constructor(blockLen, outputLen, padOffset, isLE) {
455
+ this.blockLen = blockLen;
456
+ this.outputLen = outputLen;
457
+ this.padOffset = padOffset;
458
+ this.isLE = isLE;
459
+ this.buffer = new Uint8Array(blockLen);
460
+ this.view = createView(this.buffer);
461
+ }
462
+ update(data) {
463
+ aexists(this);
464
+ abytes(data);
465
+ const { view, buffer, blockLen } = this;
466
+ const len = data.length;
467
+ let processed = false;
468
+ for (let pos = 0; pos < len; ) {
469
+ const take = Math.min(blockLen - this.pos, len - pos);
470
+ if (take === blockLen) {
471
+ const dataView = createView(data);
472
+ for (; blockLen <= len - pos; pos += blockLen)
473
+ this.process(dataView, pos);
474
+ processed = true;
475
+ continue;
476
+ }
477
+ buffer.set(pos === 0 && take === len ? data : data.subarray(pos, pos + take), this.pos);
478
+ this.pos += take;
479
+ pos += take;
480
+ if (this.pos === blockLen) {
481
+ this.process(view, 0);
482
+ this.pos = 0;
483
+ processed = true;
484
+ }
485
+ }
486
+ this.length += data.length;
487
+ if (processed)
488
+ this.roundClean();
489
+ return this;
490
+ }
491
+ digestInto(out) {
492
+ aexists(this);
493
+ aoutput(out, this);
494
+ this.finished = true;
495
+ const { buffer, view, blockLen, isLE } = this;
496
+ let { pos } = this;
497
+ buffer[pos++] = 128;
498
+ buffer.fill(0, pos);
499
+ if (this.padOffset > blockLen - pos) {
500
+ this.process(view, 0);
501
+ buffer.fill(0);
502
+ }
503
+ setU64FromNum(view, blockLen - 8, this.length * 8, isLE);
504
+ this.process(view, 0);
505
+ this.roundClean();
506
+ const oview = out === buffer ? view : createView(out);
507
+ const len = this.outputLen;
508
+ const outLen = len / 4;
509
+ const state = this.get();
510
+ if (len % 4 || outLen > state.length)
511
+ throw new Error("invalid outputLen");
512
+ for (let i = 0; i < outLen; i++)
513
+ oview.setUint32(4 * i, state[i], isLE);
514
+ }
515
+ digest() {
516
+ const { buffer, outputLen } = this;
517
+ this.digestInto(buffer);
518
+ const res = buffer.slice(0, outputLen);
519
+ this.destroy();
520
+ return res;
521
+ }
522
+ _cloneIntoMeta(to) {
523
+ const { buffer, length, finished, destroyed, pos } = this;
524
+ to.destroyed = destroyed;
525
+ to.finished = finished;
526
+ to.length = length;
527
+ to.pos = pos;
528
+ if (pos)
529
+ to.buffer.set(buffer);
530
+ return to;
531
+ }
532
+ clone() {
533
+ return this._cloneInto();
534
+ }
535
+ };
536
+ var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
537
+ 1779033703,
538
+ 3144134277,
539
+ 1013904242,
540
+ 2773480762,
541
+ 1359893119,
542
+ 2600822924,
543
+ 528734635,
544
+ 1541459225
545
+ ]);
546
+
547
+ // ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/sha2.js
548
+ var SHA256_K = /* @__PURE__ */ Uint32Array.from([
549
+ 1116352408,
550
+ 1899447441,
551
+ 3049323471,
552
+ 3921009573,
553
+ 961987163,
554
+ 1508970993,
555
+ 2453635748,
556
+ 2870763221,
557
+ 3624381080,
558
+ 310598401,
559
+ 607225278,
560
+ 1426881987,
561
+ 1925078388,
562
+ 2162078206,
563
+ 2614888103,
564
+ 3248222580,
565
+ 3835390401,
566
+ 4022224774,
567
+ 264347078,
568
+ 604807628,
569
+ 770255983,
570
+ 1249150122,
571
+ 1555081692,
572
+ 1996064986,
573
+ 2554220882,
574
+ 2821834349,
575
+ 2952996808,
576
+ 3210313671,
577
+ 3336571891,
578
+ 3584528711,
579
+ 113926993,
580
+ 338241895,
581
+ 666307205,
582
+ 773529912,
583
+ 1294757372,
584
+ 1396182291,
585
+ 1695183700,
586
+ 1986661051,
587
+ 2177026350,
588
+ 2456956037,
589
+ 2730485921,
590
+ 2820302411,
591
+ 3259730800,
592
+ 3345764771,
593
+ 3516065817,
594
+ 3600352804,
595
+ 4094571909,
596
+ 275423344,
597
+ 430227734,
598
+ 506948616,
599
+ 659060556,
600
+ 883997877,
601
+ 958139571,
602
+ 1322822218,
603
+ 1537002063,
604
+ 1747873779,
605
+ 1955562222,
606
+ 2024104815,
607
+ 2227730452,
608
+ 2361852424,
609
+ 2428436474,
610
+ 2756734187,
611
+ 3204031479,
612
+ 3329325298
613
+ ]);
614
+ var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
615
+ var SHA2_32B = class extends HashMD {
616
+ // We cannot use array here since array allows indexing by variable
617
+ // which means optimizer/compiler cannot use registers.
618
+ // Numeric initializers matter: starting the fields as `undefined` changes
619
+ // V8's field representation and makes sha256 3x slower (measured).
620
+ A = 0;
621
+ B = 0;
622
+ C = 0;
623
+ D = 0;
624
+ E = 0;
625
+ F = 0;
626
+ G = 0;
627
+ H = 0;
628
+ constructor(outputLen, IV) {
629
+ super(64, outputLen, 8, false);
630
+ this.A = IV[0] | 0;
631
+ this.B = IV[1] | 0;
632
+ this.C = IV[2] | 0;
633
+ this.D = IV[3] | 0;
634
+ this.E = IV[4] | 0;
635
+ this.F = IV[5] | 0;
636
+ this.G = IV[6] | 0;
637
+ this.H = IV[7] | 0;
638
+ }
639
+ get() {
640
+ const { A, B, C, D, E, F, G, H } = this;
641
+ return [A, B, C, D, E, F, G, H];
642
+ }
643
+ // prettier-ignore
644
+ set(A, B, C, D, E, F, G, H) {
645
+ this.A = A | 0;
646
+ this.B = B | 0;
647
+ this.C = C | 0;
648
+ this.D = D | 0;
649
+ this.E = E | 0;
650
+ this.F = F | 0;
651
+ this.G = G | 0;
652
+ this.H = H | 0;
653
+ }
654
+ _cloneInto(to) {
655
+ (to ||= new this.constructor()).set(...this.get());
656
+ return this._cloneIntoMeta(to);
657
+ }
658
+ process(view, offset) {
659
+ for (let i = 0; i < 16; i++, offset += 4)
660
+ SHA256_W[i] = view.getUint32(offset, false);
661
+ for (let i = 16; i < 64; i++) {
662
+ const W15 = SHA256_W[i - 15];
663
+ const W2 = SHA256_W[i - 2];
664
+ const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
665
+ const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
666
+ SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
667
+ }
668
+ let { A, B, C, D, E, F, G, H } = this;
669
+ for (let i = 0; i < 64; i++) {
670
+ const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
671
+ const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
672
+ const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
673
+ const T2 = sigma0 + Maj(A, B, C) | 0;
674
+ H = G;
675
+ G = F;
676
+ F = E;
677
+ E = D + T1 | 0;
678
+ D = C;
679
+ C = B;
680
+ B = A;
681
+ A = T1 + T2 | 0;
682
+ }
683
+ A = A + this.A | 0;
684
+ B = B + this.B | 0;
685
+ C = C + this.C | 0;
686
+ D = D + this.D | 0;
687
+ E = E + this.E | 0;
688
+ F = F + this.F | 0;
689
+ G = G + this.G | 0;
690
+ H = H + this.H | 0;
691
+ this.set(A, B, C, D, E, F, G, H);
692
+ }
693
+ roundClean() {
694
+ clean(SHA256_W);
695
+ }
696
+ destroy() {
697
+ this.destroyed = true;
698
+ this.set(0, 0, 0, 0, 0, 0, 0, 0);
699
+ clean(this.buffer);
700
+ }
701
+ };
702
+ var _SHA256 = class extends SHA2_32B {
703
+ constructor() {
704
+ super(32, SHA256_IV);
705
+ }
706
+ };
707
+ var sha256 = /* @__PURE__ */ createHasher(
708
+ () => new _SHA256(),
709
+ /* @__PURE__ */ oidNist(1)
710
+ );
711
+
712
+ // ../protocol/src/merkle.ts
713
+ function leafHash(leaf) {
714
+ const bytes = typeof leaf === "string" ? utf8(leaf) : leaf;
715
+ const input = new Uint8Array(bytes.length + 1);
716
+ input[0] = 0;
717
+ input.set(bytes, 1);
718
+ return sha256(input);
719
+ }
720
+ function nodeHash(left, right) {
721
+ const input = new Uint8Array(65);
722
+ input[0] = 1;
723
+ input.set(left, 1);
724
+ input.set(right, 33);
725
+ return sha256(input);
726
+ }
727
+ var EMPTY_ROOT = sha256(new Uint8Array(0));
728
+ function splitPoint(n) {
729
+ let k = 1;
730
+ while (k * 2 < n) k *= 2;
731
+ return k;
732
+ }
733
+ function rootFromLeafHashes(hashes) {
734
+ const stack = [];
735
+ for (const hash of hashes) {
736
+ let node = { size: 1, hash };
737
+ while (stack.length > 0 && stack[stack.length - 1].size === node.size) {
738
+ const left = stack.pop();
739
+ node = { size: left.size * 2, hash: nodeHash(left.hash, node.hash) };
740
+ }
741
+ stack.push(node);
742
+ }
743
+ if (stack.length === 0) return EMPTY_ROOT;
744
+ let root = stack[stack.length - 1].hash;
745
+ for (let i = stack.length - 2; i >= 0; i--) root = nodeHash(stack[i].hash, root);
746
+ return root;
747
+ }
748
+ function inclusionProof(index, size, rangeHash) {
749
+ if (!(index >= 0 && index < size)) throw new RangeError(`index ${index} not in tree of size ${size}`);
750
+ const proof = [];
751
+ let lo = 0;
752
+ let hi = size;
753
+ while (hi - lo > 1) {
754
+ const k = splitPoint(hi - lo);
755
+ if (index < lo + k) {
756
+ proof.push(rangeHash(lo + k, hi));
757
+ hi = lo + k;
758
+ } else {
759
+ proof.push(rangeHash(lo, lo + k));
760
+ lo = lo + k;
761
+ }
762
+ }
763
+ return proof.reverse();
764
+ }
765
+ function verifyInclusion(index, size, leaf, proof, root) {
766
+ if (!(index >= 0 && index < size)) return false;
767
+ let fn = index;
768
+ let sn = size - 1;
769
+ let r = leaf;
770
+ for (const p of proof) {
771
+ if (sn === 0) return false;
772
+ if (fn % 2 === 1 || fn === sn) {
773
+ r = nodeHash(p, r);
774
+ while (fn % 2 === 0 && fn !== 0) {
775
+ fn = Math.floor(fn / 2);
776
+ sn = Math.floor(sn / 2);
777
+ }
778
+ } else {
779
+ r = nodeHash(r, p);
780
+ }
781
+ fn = Math.floor(fn / 2);
782
+ sn = Math.floor(sn / 2);
783
+ }
784
+ return sn === 0 && equalBytes(r, root);
785
+ }
786
+ function equalBytes(a, b) {
787
+ if (a.length !== b.length) return false;
788
+ let diff = 0;
789
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
790
+ return diff === 0;
791
+ }
792
+
793
+ // ../protocol/src/note.ts
794
+ var ED255192 = { name: "Ed25519" };
795
+ var ALG_ED25519 = 1;
796
+ var DASH = "\u2014";
797
+ function formatCheckpoint({ origin, size, root }) {
798
+ return `${origin}
799
+ ${size}
800
+ ${root}
801
+ `;
802
+ }
803
+ function parseCheckpoint(body) {
804
+ const [origin, sizeText, root] = body.split("\n");
805
+ const size = Number(sizeText);
806
+ if (!origin || !Number.isSafeInteger(size) || size < 0 || !root || fromBase64(root).length !== 32) {
807
+ throw new Error("malformed checkpoint");
808
+ }
809
+ return { origin, size, root };
810
+ }
811
+ function keyId(name, publicKey) {
812
+ const input = new Uint8Array([...utf8(name), 10, ALG_ED25519, ...publicKey]);
813
+ return sha256(input).slice(0, 4);
814
+ }
815
+ function verifierKey(name, publicJwk) {
816
+ const publicKey = fromBase64url(publicJwk.x);
817
+ const id = keyId(name, publicKey);
818
+ return `${name}+${hex(id)}+${toBase64(new Uint8Array([ALG_ED25519, ...publicKey]))}`;
819
+ }
820
+ function parseVerifierKey(vkey) {
821
+ const [name, idHex, ...key] = vkey.split("+");
822
+ const keyB64 = key.join("+");
823
+ if (!name || !idHex || !keyB64) throw new Error("malformed verifier key");
824
+ const raw = fromBase64(keyB64);
825
+ if (raw.length !== 33 || raw[0] !== ALG_ED25519) throw new Error("verifier key is not Ed25519");
826
+ const publicKey = raw.slice(1);
827
+ const id = keyId(name, publicKey);
828
+ if (hex(id) !== idHex) throw new Error("verifier key id does not match its key");
829
+ return { name, id, publicKey };
830
+ }
831
+ async function signNote(text, name, privateJwk) {
832
+ if (!text.endsWith("\n")) throw new Error("note text must end with a newline");
833
+ if (!privateJwk.d) throw new Error("signing key must be a private Ed25519 JWK");
834
+ const key = await crypto.subtle.importKey(
835
+ "jwk",
836
+ { kty: "OKP", crv: "Ed25519", x: privateJwk.x, d: privateJwk.d },
837
+ ED255192,
838
+ false,
839
+ ["sign"]
840
+ );
841
+ const signature = new Uint8Array(await crypto.subtle.sign(ED255192, key, utf8(text)));
842
+ const id = keyId(name, fromBase64url(privateJwk.x));
843
+ return `${text}
844
+ ${DASH} ${name} ${toBase64(new Uint8Array([...id, ...signature]))}
845
+ `;
846
+ }
847
+ async function verifyNote(note, vkey) {
848
+ const { name, id, publicKey } = parseVerifierKey(vkey);
849
+ const split = note.lastIndexOf("\n\n");
850
+ if (split < 0 || !note.endsWith("\n")) throw new Error("malformed note");
851
+ const text = note.slice(0, split + 1);
852
+ const lines = note.slice(split + 2, -1).split("\n");
853
+ const key = await crypto.subtle.importKey("raw", publicKey, ED255192, false, ["verify"]);
854
+ for (const line of lines) {
855
+ const match = new RegExp(`^${DASH} (\\S+) (\\S+)$`).exec(line);
856
+ if (!match || match[1] !== name) continue;
857
+ const raw = fromBase64(match[2]);
858
+ if (raw.length !== 68 || hex(raw.slice(0, 4)) !== hex(id)) continue;
859
+ if (await crypto.subtle.verify(ED255192, key, raw.slice(4), utf8(text))) return text;
860
+ }
861
+ throw new Error(`note is not signed by ${name}`);
862
+ }
863
+ function toBase64(bytes) {
864
+ let binary = "";
865
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
866
+ return btoa(binary);
867
+ }
868
+ function fromBase64(text) {
869
+ const binary = atob(text);
870
+ const bytes = new Uint8Array(binary.length);
871
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
872
+ return bytes;
873
+ }
874
+ function hex(bytes) {
875
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
876
+ }
877
+
878
+ // ../protocol/src/ledger-log.ts
879
+ var SYSTEM_ACCOUNT = "@village";
880
+ function encodeLeaf(leaf) {
881
+ const ordered = {
882
+ v: 1,
883
+ index: leaf.index,
884
+ id: leaf.id,
885
+ ts: leaf.ts,
886
+ kind: leaf.kind,
887
+ from: leaf.from,
888
+ to: leaf.to,
889
+ amount: leaf.amount,
890
+ from_balance: leaf.from_balance,
891
+ to_balance: leaf.to_balance,
892
+ ref: leaf.ref,
893
+ hold_id: leaf.hold_id,
894
+ memo: leaf.memo
895
+ };
896
+ return JSON.stringify(ordered);
897
+ }
898
+ function decodeLeaf(data) {
899
+ const leaf = JSON.parse(data);
900
+ if (leaf.v !== 1 || encodeLeaf(leaf) !== data) throw new Error("leaf is not in canonical form");
901
+ return leaf;
902
+ }
96
903
 
97
904
  // ../protocol/src/webhook.ts
98
905
  var WEBHOOK_HEADERS = {
@@ -180,6 +987,7 @@ var APP_TIMESTAMP_HEADER = "x-ambasada-timestamp";
180
987
  var APP_WEBHOOK_TOLERANCE_S = 300;
181
988
  var DEFAULT_HUB_URL = "https://hub.ambasada.id";
182
989
  var APP_NOTIFY_PATH = "/api/apps/notify";
990
+ var APP_EVENT_TYPES = ["notifications_enabled", "notifications_disabled", "app_added", "app_removed", "ping"];
183
991
  var AppWebhookError = class extends Error {
184
992
  constructor(reason, message) {
185
993
  super(message);
@@ -198,13 +1006,13 @@ function hmacKey2(secret) {
198
1006
  }
199
1007
  return key;
200
1008
  }
201
- var hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1009
+ var hex2 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
202
1010
  async function mac(secret, timestamp, body) {
203
1011
  return new Uint8Array(await crypto.subtle.sign("HMAC", await hmacKey2(secret), encoder3.encode(`${timestamp}.${body}`)));
204
1012
  }
205
1013
  async function signAppWebhook(secret, timestamp, body) {
206
1014
  if (!secret) throw new AppWebhookError("bad_secret", "no app secret");
207
- return `v1=${hex(await mac(secret, String(timestamp), body))}`;
1015
+ return `v1=${hex2(await mac(secret, String(timestamp), body))}`;
208
1016
  }
209
1017
  async function appWebhookHeaders(secret, body, timestamp = Math.floor(Date.now() / 1e3)) {
210
1018
  return { [APP_TIMESTAMP_HEADER]: String(timestamp), [APP_SIGNATURE_HEADER]: await signAppWebhook(secret, timestamp, body), "content-type": "application/json" };
@@ -310,8 +1118,301 @@ function appNotifyResult(json) {
310
1118
  return { sent: list(o.sent), invalid: list(o.invalid), rate_limited: list(o.rate_limited) };
311
1119
  }
312
1120
 
1121
+ // ../protocol/src/check.ts
1122
+ var MANIFEST_URL_PATH = "/.well-known/ambasada.json";
1123
+ var MANIFEST_SCHEMA = 2;
1124
+ var MANIFEST_KINDS = ["product", "event", "tool", "assistant"];
1125
+ var MANIFEST_CATEGORIES = ["shopping", "food", "events", "tools", "learning", "community", "games", "finance", "other"];
1126
+ var MANIFEST_VISIBILITY = ["public", "builders", "unlisted"];
1127
+ var MANIFEST_LIMITS = { id: 32, name: 32, subtitle: 40, description: 170, tags: 5, screenshots: 3 };
1128
+ var MANIFEST_ID = /^[a-z0-9-]{3,32}$/;
1129
+ var HOSTED_MANIFEST = /^https:\/\/hub\.ambasada\.id\/api\/apps\/([a-z0-9-]+)\/manifest$/;
1130
+ var ICON_SIZE = 1024;
1131
+ var SLOW_ENTRY_MS = 3e3;
1132
+ var FETCH_TIMEOUT_MS = 1e4;
1133
+ var HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
1134
+ var SDK_MARKERS = ["@ambasada/miniapp", "miniapp.js", "ambasadaShell"];
1135
+ var findings = () => ({ problems: [], warnings: [] });
1136
+ var merge = (into, from) => {
1137
+ into.problems.push(...from.problems);
1138
+ into.warnings.push(...from.warnings);
1139
+ };
1140
+ function validateManifest(json) {
1141
+ const f = findings();
1142
+ const problem = (code, message) => f.problems.push({ code, message });
1143
+ const warn = (code, message) => f.warnings.push({ code, message });
1144
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
1145
+ problem("manifest_not_object", "the manifest must be a JSON object");
1146
+ return f;
1147
+ }
1148
+ const m = json;
1149
+ const str = (key) => typeof m[key] === "string" ? m[key] : void 0;
1150
+ if (m.schema === 1) warn("schema_1", `schema 1 still works, but the app lists it as Unlisted until it is updated to schema ${MANIFEST_SCHEMA} (ambasada init writes the new one)`);
1151
+ else if (m.schema !== MANIFEST_SCHEMA) problem("schema", `"schema" must be ${MANIFEST_SCHEMA} (got ${JSON.stringify(m.schema)})`);
1152
+ const id = str("id");
1153
+ if (!id) problem("id", '"id" is required: 3\u201332 lowercase letters, digits and dashes');
1154
+ else if (!MANIFEST_ID.test(id)) problem("id", `"id" must match ${MANIFEST_ID} (got ${JSON.stringify(id)})`);
1155
+ const name = str("name");
1156
+ if (!name) problem("name", '"name" is required');
1157
+ else if (name.length > MANIFEST_LIMITS.name) problem("name_length", `"name" is ${name.length} characters; the limit is ${MANIFEST_LIMITS.name}`);
1158
+ const icon = str("icon");
1159
+ if (!icon) problem("icon", '"icon" is required: the path of a 1024\xD71024 opaque PNG, like "/icon.png"');
1160
+ const entry = str("entry");
1161
+ if (!entry) problem("entry", '"entry" is required: the path the app opens, like "/"');
1162
+ else if (!entry.startsWith("/")) problem("entry", `"entry" must be a path starting with / (got ${JSON.stringify(entry)})`);
1163
+ const subtitle = str("subtitle");
1164
+ if (subtitle && subtitle.length > MANIFEST_LIMITS.subtitle) problem("subtitle_length", `"subtitle" is ${subtitle.length} characters; the limit is ${MANIFEST_LIMITS.subtitle}`);
1165
+ const description = str("description");
1166
+ if (description && description.length > MANIFEST_LIMITS.description) problem("description_length", `"description" is ${description.length} characters; the limit is ${MANIFEST_LIMITS.description}`);
1167
+ if (Array.isArray(m.tags) && m.tags.length > MANIFEST_LIMITS.tags) problem("tags_count", `"tags" has ${m.tags.length} items; the limit is ${MANIFEST_LIMITS.tags}`);
1168
+ if (Array.isArray(m.screenshots) && m.screenshots.length > MANIFEST_LIMITS.screenshots) problem("screenshots_count", `"screenshots" has ${m.screenshots.length} items; the limit is ${MANIFEST_LIMITS.screenshots}`);
1169
+ const scopes = Array.isArray(m.scopes) ? m.scopes.filter((s) => typeof s === "string") : [];
1170
+ if (m.scopes !== void 0 && !Array.isArray(m.scopes)) problem("scopes", '"scopes" must be an array of strings');
1171
+ if (scopes.includes("notifications")) {
1172
+ const webhook = m.notifications && typeof m.notifications === "object" ? m.notifications.webhook : void 0;
1173
+ if (typeof webhook !== "string" || !webhook) problem("notifications_webhook", 'the "notifications" scope needs "notifications": { "webhook": "/api/webhook" }, where the app delivers notifications_enabled');
1174
+ }
1175
+ if (scopes.length && !str("privacy_url")) problem("privacy_url", `apps with scopes (${scopes.join(", ")}) need a "privacy_url"`);
1176
+ const visibility = str("visibility");
1177
+ if (m.visibility !== void 0 && (!visibility || !(MANIFEST_VISIBILITY.includes(visibility) || /^event:[a-z0-9-]+$/.test(visibility)))) {
1178
+ problem("visibility", `"visibility" must be one of ${MANIFEST_VISIBILITY.join(", ")} or event:<slug> (got ${JSON.stringify(m.visibility)})`);
1179
+ }
1180
+ const accent = str("accent");
1181
+ if (m.accent !== void 0 && (!accent || !HEX_COLOR.test(accent))) problem("accent", `"accent" must be a #RRGGBB colour (got ${JSON.stringify(m.accent)})`);
1182
+ if (m.splash !== void 0) {
1183
+ const splash = m.splash && typeof m.splash === "object" ? m.splash : null;
1184
+ const color = splash?.color;
1185
+ if (!splash || color !== void 0 && (typeof color !== "string" || !HEX_COLOR.test(color))) problem("splash_color", `"splash.color" must be a #RRGGBB colour (got ${JSON.stringify(color ?? m.splash)})`);
1186
+ }
1187
+ const kind = str("kind");
1188
+ if (m.kind !== void 0 && (!kind || !MANIFEST_KINDS.includes(kind))) warn("kind", `unknown "kind" ${JSON.stringify(m.kind)}: the app knows ${MANIFEST_KINDS.join(", ")} and shows this one as a product`);
1189
+ const category = str("category");
1190
+ if (m.category !== void 0 && (!category || !MANIFEST_CATEGORIES.includes(category))) warn("category", `unknown "category" ${JSON.stringify(m.category)}: the app knows ${MANIFEST_CATEGORIES.join(", ")} and files this one under other`);
1191
+ return f;
1192
+ }
1193
+ var PNG_SIGNATURE = "89504e470d0a1a0a";
1194
+ var hexOf = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
1195
+ var latin1 = (b) => String.fromCharCode(...b);
1196
+ function pngInfo(bytes) {
1197
+ if (bytes.length < 33) return null;
1198
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1199
+ if (hexOf(bytes.subarray(0, 8)) !== PNG_SIGNATURE) return null;
1200
+ if (latin1(bytes.subarray(12, 16)) !== "IHDR") return null;
1201
+ const width = view.getUint32(16);
1202
+ const height = view.getUint32(20);
1203
+ const colorType = bytes[25];
1204
+ let hasAlpha = colorType === 4 || colorType === 6;
1205
+ for (let pos = 8; pos + 8 <= bytes.length && !hasAlpha; ) {
1206
+ const length = view.getUint32(pos);
1207
+ const type = latin1(bytes.subarray(pos + 4, pos + 8));
1208
+ if (type === "IDAT" || type === "IEND") break;
1209
+ if (type === "tRNS") hasAlpha = true;
1210
+ pos += 12 + length;
1211
+ }
1212
+ return { width, height, hasAlpha };
1213
+ }
1214
+ function checkIcon(bytes) {
1215
+ const f = findings();
1216
+ const info = pngInfo(bytes);
1217
+ if (!info) f.problems.push({ code: "icon_not_png", message: "the icon must be a PNG" });
1218
+ else {
1219
+ if (info.width !== ICON_SIZE || info.height !== ICON_SIZE) f.problems.push({ code: "icon_size", message: `the icon is ${info.width}\xD7${info.height}; it must be ${ICON_SIZE}\xD7${ICON_SIZE}` });
1220
+ if (info.hasAlpha) f.problems.push({ code: "icon_alpha", message: "the icon has transparency; it must be opaque (the app rounds the corners itself)" });
1221
+ }
1222
+ return f;
1223
+ }
1224
+ function metaTags(html) {
1225
+ const tags = [];
1226
+ for (const [, attrs] of html.matchAll(/<meta\b([^>]*)>/gi)) {
1227
+ const out = {};
1228
+ for (const [, key, dq, sq, bare] of attrs.matchAll(/([a-zA-Z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g)) out[key.toLowerCase()] = (dq ?? sq ?? bare ?? "").trim();
1229
+ tags.push(out);
1230
+ }
1231
+ return tags;
1232
+ }
1233
+ function checkHtml(html) {
1234
+ const f = findings();
1235
+ const metas = metaTags(html);
1236
+ const viewport = metas.find((m) => m.name?.toLowerCase() === "viewport");
1237
+ if (!viewport || !/width\s*=\s*device-width/i.test(viewport.content ?? "")) {
1238
+ f.problems.push({ code: "no_viewport", message: 'the entry page needs <meta name="viewport" content="width=device-width, initial-scale=1">' });
1239
+ }
1240
+ const scheme = metas.find((m) => m.name?.toLowerCase() === "color-scheme")?.content?.toLowerCase().trim();
1241
+ const darkMeta = scheme !== void 0 && /\bdark\b/.test(scheme) && !/\blight\b/.test(scheme);
1242
+ const darkStyle = [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].concat([...html.matchAll(/\bstyle\s*=\s*"([^"]*)"/gi)]).some(([, css]) => [...css.matchAll(/color-scheme\s*:\s*([^;}"]+)/gi)].some(([, v]) => /\bdark\b/i.test(v) && !/\blight\b/i.test(v)));
1243
+ if (darkMeta || darkStyle) f.problems.push({ code: "dark_only", message: "the entry page declares a dark-only color-scheme; the Ambasada app is light (use light, or light dark)" });
1244
+ if (!SDK_MARKERS.some((m) => html.includes(m))) {
1245
+ f.warnings.push({ code: "no_miniapp_sdk", message: "no @ambasada/miniapp, miniapp.js or ambasadaShell in the entry HTML: a bundled import is fine too, the registry crawler confirms ready() is called at runtime" });
1246
+ }
1247
+ return f;
1248
+ }
1249
+ var USER_AGENT = "ambasada-check/1 (+https://ambasada.id)";
1250
+ async function get(fetchImpl, url) {
1251
+ return fetchImpl(url, { redirect: "manual", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { "user-agent": USER_AGENT, accept: "*/*" } });
1252
+ }
1253
+ async function checkRemote(url, fetchImpl = fetch, options = {}) {
1254
+ const report = { ok: true, target: url, problems: [], warnings: [], notes: [] };
1255
+ let origin;
1256
+ try {
1257
+ origin = new URL(url);
1258
+ } catch {
1259
+ report.problems.push({ code: "bad_url", message: `${url} is not a URL` });
1260
+ report.ok = false;
1261
+ return report;
1262
+ }
1263
+ if (origin.protocol !== "https:") report.problems.push({ code: "not_https", message: "the app must be served over https" });
1264
+ let manifest = null;
1265
+ const manifestUrl = new URL(MANIFEST_URL_PATH, origin).toString();
1266
+ try {
1267
+ let res = await get(fetchImpl, manifestUrl);
1268
+ if (res.status >= 300 && res.status < 400) {
1269
+ const location = res.headers.get("location") ?? "";
1270
+ if (res.status === 307 && HOSTED_MANIFEST.test(location)) {
1271
+ report.notes.push(`manifest hosted by the hub: ${location}`);
1272
+ res = await get(fetchImpl, location);
1273
+ } else report.problems.push({ code: "manifest_redirect", message: `${manifestUrl} redirects (${res.status} \u2192 ${location || "?"}); serve it directly` });
1274
+ }
1275
+ if (res.status !== 200) {
1276
+ if (!report.problems.some((p) => p.code === "manifest_redirect")) report.problems.push({ code: "manifest_status", message: `${manifestUrl} answered ${res.status}` });
1277
+ } else {
1278
+ try {
1279
+ manifest = JSON.parse(await res.text());
1280
+ merge(report, validateManifest(manifest));
1281
+ } catch (error) {
1282
+ report.problems.push({ code: "manifest_json", message: `${manifestUrl} is not valid JSON: ${error.message}` });
1283
+ }
1284
+ }
1285
+ } catch (error) {
1286
+ report.problems.push({ code: "manifest_fetch", message: `could not fetch ${manifestUrl}: ${error.message}` });
1287
+ }
1288
+ const entryPath = typeof manifest?.entry === "string" && manifest.entry.startsWith("/") ? manifest.entry : "/";
1289
+ const entryUrl = new URL(entryPath, origin).toString();
1290
+ try {
1291
+ const started = Date.now();
1292
+ const res = await get(fetchImpl, entryUrl);
1293
+ const html = res.status === 200 ? await res.text() : "";
1294
+ const ms = Date.now() - started;
1295
+ if (res.status !== 200) report.problems.push({ code: "entry_status", message: `${entryUrl} answered ${res.status}${res.headers.get("location") ? ` \u2192 ${res.headers.get("location")}` : ""}; the entry must answer 200 without redirects` });
1296
+ else merge(report, checkHtml(html));
1297
+ if (ms > SLOW_ENTRY_MS) report.warnings.push({ code: "slow_entry", message: `${entryUrl} took ${ms} ms; the splash stays until ready(), aim for under ${SLOW_ENTRY_MS} ms` });
1298
+ } catch (error) {
1299
+ report.problems.push({ code: "entry_fetch", message: `could not fetch ${entryUrl}: ${error.message}` });
1300
+ }
1301
+ if (typeof manifest?.icon === "string" && manifest.icon) {
1302
+ const iconUrl = new URL(manifest.icon, origin).toString();
1303
+ try {
1304
+ const res = await get(fetchImpl, iconUrl);
1305
+ if (res.status !== 200) report.problems.push({ code: "icon_status", message: `${iconUrl} answered ${res.status}` });
1306
+ else merge(report, checkIcon(new Uint8Array(await res.arrayBuffer())));
1307
+ } catch (error) {
1308
+ report.problems.push({ code: "icon_fetch", message: `could not fetch ${iconUrl}: ${error.message}` });
1309
+ }
1310
+ }
1311
+ const scopes = Array.isArray(manifest?.scopes) ? manifest.scopes.filter((x) => typeof x === "string") : [];
1312
+ if (scopes.includes("identity")) {
1313
+ const callbackUrl = new URL("/api/auth/callback", origin).toString();
1314
+ try {
1315
+ const res = await get(fetchImpl, callbackUrl);
1316
+ await res.body?.cancel();
1317
+ if (res.status === 404 || res.status >= 500) report.problems.push({ code: "identity_callback", message: `the identity scope needs Sign in with Ambasada at /api/auth/callback; ${callbackUrl} answered ${res.status}` });
1318
+ } catch (error) {
1319
+ report.problems.push({ code: "identity_callback", message: `could not fetch ${callbackUrl}: ${error.message}` });
1320
+ }
1321
+ }
1322
+ const webhook = manifest?.notifications && typeof manifest.notifications === "object" ? manifest.notifications.webhook : void 0;
1323
+ if (scopes.includes("notifications") && typeof webhook === "string" && webhook) {
1324
+ const webhookUrl = new URL(webhook, origin);
1325
+ if (webhookUrl.origin !== origin.origin) report.problems.push({ code: "webhook_origin", message: `the notifications webhook ${webhookUrl} must be on ${origin.origin}` });
1326
+ else if (!options.appSecret) report.notes.push(`webhook ${webhookUrl.pathname}: not pinged (needs the app secret; the registry pings it)`);
1327
+ else {
1328
+ const body = JSON.stringify({ event: "ping", app: options.appId ?? (typeof manifest?.id === "string" ? manifest.id : ""), at: (/* @__PURE__ */ new Date()).toISOString() });
1329
+ try {
1330
+ const res = await fetchImpl(webhookUrl.toString(), {
1331
+ method: "POST",
1332
+ redirect: "manual",
1333
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
1334
+ headers: { "user-agent": USER_AGENT, ...await appWebhookHeaders(options.appSecret, body) },
1335
+ body
1336
+ });
1337
+ await res.body?.cancel();
1338
+ if (res.status !== 200) report.problems.push({ code: "webhook_ping", message: `${webhookUrl} answered the signed ping with ${res.status}; it must answer 200 (verify with ambasada.apps.verifyWebhook)` });
1339
+ } catch (error) {
1340
+ report.problems.push({ code: "webhook_ping", message: `${webhookUrl} did not answer the signed ping: ${error.message}` });
1341
+ }
1342
+ }
1343
+ }
1344
+ report.ok = report.problems.length === 0;
1345
+ return report;
1346
+ }
1347
+ var LABELS = [
1348
+ [/^icon/, "icon"],
1349
+ [/^manifest|^schema|^id$|^entry$/, "manifest"],
1350
+ [/^name/, "name"],
1351
+ [/^no_viewport$/, "viewport"],
1352
+ [/^dark_only$/, "light theme"],
1353
+ [/^no_miniapp_sdk$/, "ready()"],
1354
+ [/^entry_|^not_https$|^bad_url$/, "entry page"],
1355
+ [/^privacy_url$/, "privacy page"],
1356
+ [/^notifications_webhook$|^webhook_/, "webhook"],
1357
+ [/^identity_callback$/, "sign-in"]
1358
+ ];
1359
+ function findingLabel(code) {
1360
+ return LABELS.find(([re]) => re.test(code))?.[1] ?? code.replace(/_/g, " ");
1361
+ }
1362
+ function summarizeCheck(report, now = /* @__PURE__ */ new Date()) {
1363
+ const issues = [
1364
+ ...report.problems.map((p) => ({ ...p, level: "problem", label: findingLabel(p.code) })),
1365
+ ...report.warnings.map((w) => ({ ...w, level: "warning", label: findingLabel(w.code) }))
1366
+ ];
1367
+ const needs = [...new Set(issues.filter((i) => i.level === "problem").map((i) => i.label))];
1368
+ return { ok: report.ok, issues, needs, checked_at: now.toISOString() };
1369
+ }
1370
+ function listingLine(tier, check) {
1371
+ if (tier === "verified") return "Verified";
1372
+ if (tier === "listed") return "Listed for the event";
1373
+ if (tier === "unlisted") return check?.needs.length ? `Unlisted \xB7 needs: ${check.needs.join(", ")}` : "Unlisted";
1374
+ return check?.needs.length ? `Not registered yet \xB7 needs: ${check.needs.join(", ")}` : "Not registered yet";
1375
+ }
1376
+
1377
+ // ../protocol/src/shell-token.ts
1378
+ var SHELL_TOKEN_TTL_S = 60;
1379
+ var RSA = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
1380
+ async function generateShellKey(kid = `ins_${crypto.randomUUID().slice(0, 8)}`) {
1381
+ const pair = await crypto.subtle.generateKey({ ...RSA, modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) }, true, ["sign", "verify"]);
1382
+ const pub = await crypto.subtle.exportKey("jwk", pair.publicKey);
1383
+ return { kid, privateKey: pair.privateKey, jwk: { kty: "RSA", n: pub.n, e: pub.e, kid, alg: "RS256", use: "sig" } };
1384
+ }
1385
+ var utf82 = (s) => new TextEncoder().encode(s);
1386
+ async function signShellToken(key, claims) {
1387
+ const now = Math.floor(Date.now() / 1e3);
1388
+ const header3 = base64url(utf82(JSON.stringify({ alg: "RS256", typ: "JWT", kid: key.kid })));
1389
+ const payload = base64url(utf82(JSON.stringify({ iat: now, nbf: now, exp: now + SHELL_TOKEN_TTL_S, ...claims })));
1390
+ const sig = new Uint8Array(await crypto.subtle.sign(RSA.name, key.privateKey, utf82(`${header3}.${payload}`)));
1391
+ return `${header3}.${payload}.${base64url(sig)}`;
1392
+ }
1393
+
1394
+ // ../protocol/src/index.ts
1395
+ var HEADERS = {
1396
+ receipt: "x-village-receipt",
1397
+ callId: "x-village-call-id",
1398
+ amount: "x-village-amount",
1399
+ /** Position of the payment in the public ledger log; fetch an inclusion proof with it. */
1400
+ logIndex: "x-village-log-index",
1401
+ depth: "x-village-depth",
1402
+ budget: "x-village-budget",
1403
+ maxPrice: "x-village-max-price",
1404
+ buyer: "x-village-buyer",
1405
+ idempotencyKey: "idempotency-key",
1406
+ /** An approved approval request id, to retry an agent payment that was over the automatic limit. */
1407
+ approval: "x-village-approval",
1408
+ /** The agent's own reason for a payment, shown to the team next to the Gateway's facts. */
1409
+ reason: "x-village-reason",
1410
+ idempotentReplay: "idempotent-replayed"
1411
+ };
1412
+ var MAX_DEPTH = 4;
1413
+
313
1414
  // ../sdk/src/errors.ts
314
- var ERROR_STATUS = {
1415
+ var ERROR_STATUS2 = {
315
1416
  // EXCHANGE.md §5 vocabulary
316
1417
  INSUFFICIENT_FUNDS: 402,
317
1418
  OFFLINE: 503,
@@ -355,17 +1456,24 @@ var ERROR_STATUS = {
355
1456
  RECEIPT_INVALID: 402,
356
1457
  WEBHOOK_INVALID: 400
357
1458
  };
358
- var VillageError = class extends Error {
1459
+ var BRAND = /* @__PURE__ */ Symbol.for("ambasada.error");
1460
+ var AmbasadaError = class _AmbasadaError extends Error {
359
1461
  code;
360
1462
  /** HTTP status (0 when the request never got an answer). */
361
1463
  status;
362
1464
  details;
363
1465
  constructor(code, message, status, details = {}) {
364
1466
  super(message);
365
- this.name = "VillageError";
1467
+ this.name = "AmbasadaError";
366
1468
  this.code = code;
367
- this.status = status ?? ERROR_STATUS[code] ?? 500;
1469
+ this.status = status ?? ERROR_STATUS2[code] ?? 500;
368
1470
  this.details = details;
1471
+ Object.defineProperty(this, BRAND, { value: true });
1472
+ }
1473
+ /** `instanceof AmbasadaError` holds across bundled copies; subclasses keep the usual prototype check. */
1474
+ static [Symbol.hasInstance](value) {
1475
+ if (this !== _AmbasadaError) return Function.prototype[Symbol.hasInstance].call(this, value);
1476
+ return typeof value === "object" && value !== null && value[BRAND] === true;
369
1477
  }
370
1478
  get callId() {
371
1479
  return this.details.call_id;
@@ -385,15 +1493,7 @@ var VillageError = class extends Error {
385
1493
  return Response.json(this.toJSON(), { status: this.status || 500 });
386
1494
  }
387
1495
  };
388
- var ReceiptInvalidError = class extends VillageError {
389
- reason;
390
- constructor(reason, message) {
391
- super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
392
- this.name = "ReceiptInvalidError";
393
- this.reason = reason;
394
- }
395
- };
396
- var WebhookInvalidError = class extends VillageError {
1496
+ var WebhookInvalidError = class extends AmbasadaError {
397
1497
  reason;
398
1498
  constructor(reason, message) {
399
1499
  super("WEBHOOK_INVALID", message, 400, { reason });
@@ -401,17 +1501,17 @@ var WebhookInvalidError = class extends VillageError {
401
1501
  this.reason = reason;
402
1502
  }
403
1503
  };
404
- function isVillageError(value, code) {
405
- return value instanceof VillageError && (code === void 0 || value.code === code);
1504
+ function isAmbasadaError(value, code) {
1505
+ return value instanceof AmbasadaError && (code === void 0 || value.code === code);
406
1506
  }
407
1507
  function errorFromResponse(status, body, fallback) {
408
1508
  const error = typeof body === "object" && body !== null ? body.error : void 0;
409
1509
  if (typeof error === "object" && error !== null && typeof error.code === "string") {
410
1510
  const { code: code2, message, ...details } = error;
411
- return new VillageError(code2, typeof message === "string" ? message : code2, status, details);
1511
+ return new AmbasadaError(code2, typeof message === "string" ? message : code2, status, details);
412
1512
  }
413
1513
  const code = status === 401 ? "UNAUTHORIZED" : status === 404 ? "NOT_FOUND" : status >= 500 ? "INTERNAL" : "INVALID_REQUEST";
414
- return new VillageError(code, fallback, status);
1514
+ return new AmbasadaError(code, fallback, status);
415
1515
  }
416
1516
 
417
1517
  // ../sdk/src/apps.ts
@@ -449,16 +1549,16 @@ var AmbasadaApps = class {
449
1549
  /**
450
1550
  * Sends a push to the people behind `tokens` (or `users`, for Verified apps with `identity`) through the hub.
451
1551
  * Checks the limits first (title ≤ 40, body ≤ 140, id ≤ 128, ≤ 100 recipients, `url` on your origin) and
452
- * throws `VillageError("INVALID_REQUEST")` before sending anything. Tokens in `invalid` are gone: forget them.
1552
+ * throws `AmbasadaError("INVALID_REQUEST")` before sending anything. Tokens in `invalid` are gone: forget them.
453
1553
  */
454
1554
  async notify(input) {
455
1555
  const c = this.config();
456
- if (!c.appSecret) throw new VillageError("CONFIG", "no app secret: set AMBASADA_APP_SECRET (injected on ambasada.dev; `ambasada dev` sets one for the mock hub)");
1556
+ if (!c.appSecret) throw new AmbasadaError("CONFIG", "no app secret: set AMBASADA_APP_SECRET (injected on ambasada.dev; `ambasada dev` sets one for the mock hub)");
457
1557
  let body;
458
1558
  try {
459
1559
  body = appNotifyBody(input, c.origin);
460
1560
  } catch (error) {
461
- throw new VillageError("INVALID_REQUEST", error.message, 400);
1561
+ throw new AmbasadaError("INVALID_REQUEST", error.message, 400);
462
1562
  }
463
1563
  const url = `${(c.hubUrl ?? DEFAULT_HUB_URL).replace(/\/+$/, "")}${APP_NOTIFY_PATH}`;
464
1564
  const doFetch = c.fetch ?? ((i, init) => fetch(i, init));
@@ -471,14 +1571,14 @@ var AmbasadaApps = class {
471
1571
  signal: AbortSignal.timeout(15e3)
472
1572
  });
473
1573
  } catch (error) {
474
- throw new VillageError("NETWORK", `POST ${url}: ${error.message}`, 0);
1574
+ throw new AmbasadaError("NETWORK", `POST ${url}: ${error.message}`, 0);
475
1575
  }
476
1576
  const json = await res.json().catch(() => null);
477
1577
  if (!res.ok) {
478
1578
  const err = json?.error;
479
1579
  const code = typeof err === "object" && err?.code ? err.code : res.status === 401 || res.status === 403 ? "UNAUTHORIZED" : res.status === 429 ? "CAP_SELLER_RATE" : res.status >= 500 ? "UPSTREAM_ERROR" : "INVALID_REQUEST";
480
1580
  const message = typeof err === "string" ? err : err?.message ?? `the hub answered ${res.status}`;
481
- throw new VillageError(code, `notify: ${message}`, res.status);
1581
+ throw new AmbasadaError(code, `notify: ${message}`, res.status);
482
1582
  }
483
1583
  return appNotifyResult(json);
484
1584
  }
@@ -576,13 +1676,13 @@ async function jwks(issuer, doFetch, refresh = false) {
576
1676
  if (cached && !refresh && Date.now() - cached.at < JWKS_TTL_MS) return cached.keys;
577
1677
  if (cached && refresh && Date.now() - cached.at < 6e4) return cached.keys;
578
1678
  const res = await doFetch(`${issuer}/.well-known/jwks.json`, { headers: { accept: "application/json" } });
579
- if (!res.ok) throw new VillageError("UPSTREAM_ERROR", `could not read the Ambasada ID keys (${res.status})`, 502);
1679
+ if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada ID keys (${res.status})`, 502);
580
1680
  const body = await res.json();
581
1681
  const keys3 = (body.keys ?? []).filter((k) => k.kty === "RSA" && k.n && k.e);
582
1682
  jwksCache.set(issuer, { at: Date.now(), keys: keys3 });
583
1683
  return keys3;
584
1684
  }
585
- var shellRefused = (message) => new VillageError("UNAUTHORIZED", `shell token refused: ${message}`, 401);
1685
+ var shellRefused = (message) => new AmbasadaError("UNAUTHORIZED", `shell token refused: ${message}`, 401);
586
1686
  var AmbasadaAuth = class {
587
1687
  constructor(config) {
588
1688
  this.config = config;
@@ -596,7 +1696,7 @@ var AmbasadaAuth = class {
596
1696
  settings(req) {
597
1697
  const c = this.config();
598
1698
  if (!c.clientId || !c.clientSecret || !c.issuer) {
599
- throw new VillageError("CONFIG", "Sign in with Ambasada is not set up: it works on https://<slug>.ambasada.dev after `ambasada deploy`, and locally under `ambasada dev` (AMBASADA_AUTH_CLIENT_ID / _SECRET / _ISSUER)");
1699
+ throw new AmbasadaError("CONFIG", "Sign in with Ambasada is not set up: it works on https://<slug>.ambasada.dev after `ambasada deploy`, and locally under `ambasada dev` (AMBASADA_AUTH_CLIENT_ID / _SECRET / _ISSUER)");
600
1700
  }
601
1701
  const origin = (c.publicUrl ? new URL(c.publicUrl) : new URL(req.url)).origin;
602
1702
  return {
@@ -632,7 +1732,7 @@ var AmbasadaAuth = class {
632
1732
  }
633
1733
  /**
634
1734
  * Ambasada ID sends the person back here: checks the state, exchanges the code (with the PKCE
635
- * verifier and the client secret), reads their profile from the village, sets the session cookie
1735
+ * verifier and the client secret), reads their profile from the Ambasada API, sets the session cookie
636
1736
  * and redirects to `returnTo`. A refused consent goes back to `returnTo` signed out.
637
1737
  */
638
1738
  async callback(req) {
@@ -641,12 +1741,12 @@ var AmbasadaAuth = class {
641
1741
  const clear = setCookie(STATE_COOKIE, "", s.secure, 0);
642
1742
  const saved = await verifyCookie(s.clientSecret, "state", readOurCookie(req, STATE_COOKIE));
643
1743
  if (!saved || !params.get("state") || params.get("state") !== saved.state) {
644
- throw new VillageError("UNAUTHORIZED", "sign-in expired or was started elsewhere: try again", 400);
1744
+ throw new AmbasadaError("UNAUTHORIZED", "sign-in expired or was started elsewhere: try again", 400);
645
1745
  }
646
1746
  const returnTo = safeReturnTo(saved.returnTo);
647
1747
  if (params.get("error")) return redirect(returnTo, [clear]);
648
1748
  const code = params.get("code");
649
- if (!code) throw new VillageError("INVALID_REQUEST", "no authorization code", 400);
1749
+ if (!code) throw new AmbasadaError("INVALID_REQUEST", "no authorization code", 400);
650
1750
  const token = await s.fetch(`${s.issuer}/oauth/token`, {
651
1751
  method: "POST",
652
1752
  headers: {
@@ -657,9 +1757,9 @@ var AmbasadaAuth = class {
657
1757
  body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: s.redirectUri, code_verifier: saved.verifier })
658
1758
  });
659
1759
  const tokens = await token.json().catch(() => ({}));
660
- if (!token.ok || !tokens.access_token) throw new VillageError("UNAUTHORIZED", `Ambasada ID refused the sign-in (${tokens.error ?? token.status})`, 401);
1760
+ if (!token.ok || !tokens.access_token) throw new AmbasadaError("UNAUTHORIZED", `Ambasada ID refused the sign-in (${tokens.error ?? token.status})`, 401);
661
1761
  const res = await s.fetch(`${s.apiUrl}/v1/auth/profile`, { headers: { authorization: `Bearer ${tokens.access_token}`, accept: "application/json" } });
662
- if (!res.ok) throw new VillageError("UPSTREAM_ERROR", `could not read the Ambasada profile (${res.status})`, 502);
1762
+ if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada profile (${res.status})`, 502);
663
1763
  const user = await res.json();
664
1764
  const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
665
1765
  const session = await signCookie(s.clientSecret, "session", { user, exp });
@@ -674,14 +1774,14 @@ var AmbasadaAuth = class {
674
1774
  /**
675
1775
  * Verifies a shell token (Clerk JWT template `ambasada-miniapp`, RS256, 60 s) against
676
1776
  * `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json` (cached 1 h): signature, expiry, issuer, and that its `aud`
677
- * is this app (its id or origin). Returns who it is, or throws `VillageError("UNAUTHORIZED")`.
1777
+ * is this app (its id or origin). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`.
678
1778
  */
679
1779
  async verifyShellToken(token, options = {}) {
680
1780
  const c = this.config();
681
- if (!c.issuer) throw new VillageError("CONFIG", "silent sign-in needs AMBASADA_AUTH_ISSUER (injected on ambasada.dev and by `ambasada dev`)");
1781
+ if (!c.issuer) throw new AmbasadaError("CONFIG", "silent sign-in needs AMBASADA_AUTH_ISSUER (injected on ambasada.dev and by `ambasada dev`)");
682
1782
  const trusted = [c.issuer, c.hubUrl].filter((v) => !!v).map((v) => v.replace(/\/+$/, ""));
683
1783
  const audiences = [options.audience ?? c.audiences ?? []].flat().filter(Boolean).map((a) => a.replace(/\/+$/, ""));
684
- if (!audiences.length) throw new VillageError("CONFIG", "silent sign-in needs to know this app: set AMBASADA_APP_ORIGIN or PUBLIC_URL, or pass { audience }");
1784
+ if (!audiences.length) throw new AmbasadaError("CONFIG", "silent sign-in needs to know this app: set AMBASADA_APP_ORIGIN or PUBLIC_URL, or pass { audience }");
685
1785
  const doFetch = c.fetch ?? ((input, init) => fetch(input, init));
686
1786
  const parts = typeof token === "string" ? token.split(".") : [];
687
1787
  if (parts.length !== 3) throw shellRefused("not a JWT");
@@ -725,14 +1825,14 @@ var AmbasadaAuth = class {
725
1825
  */
726
1826
  async shell(req) {
727
1827
  const s = this.settings(req);
728
- if (req.method !== "POST") throw new VillageError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
1828
+ if (req.method !== "POST") throw new AmbasadaError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
729
1829
  let token = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "")?.[1]?.trim();
730
1830
  if (!token) {
731
- if (!(req.headers.get("content-type") ?? "").includes("application/json")) throw new VillageError("INVALID_REQUEST", "send the token as JSON { token }", 415);
1831
+ if (!(req.headers.get("content-type") ?? "").includes("application/json")) throw new AmbasadaError("INVALID_REQUEST", "send the token as JSON { token }", 415);
732
1832
  const body = await req.json().catch(() => null);
733
1833
  token = typeof body?.token === "string" ? body.token : void 0;
734
1834
  }
735
- if (!token) throw new VillageError("INVALID_REQUEST", "no shell token", 400);
1835
+ if (!token) throw new AmbasadaError("INVALID_REQUEST", "no shell token", 400);
736
1836
  const audiences = [...this.config().audiences ?? [], s.origin];
737
1837
  const who = await this.verifyShellToken(token, { audience: audiences });
738
1838
  let user = { id: who.user_id, name: who.name, avatar_url: who.avatar, builder: false, builder_since: null, member_since: null, pass: null };
@@ -763,57 +1863,238 @@ var AmbasadaAuth = class {
763
1863
  }
764
1864
  };
765
1865
 
766
- // ../sdk/src/webhook.ts
767
- function getHeader(headers, name) {
768
- if (typeof headers.get === "function") return headers.get(name);
769
- const record = headers;
770
- const key = Object.keys(record).find((k) => k.toLowerCase() === name);
771
- const value = key === void 0 ? void 0 : record[key];
772
- return Array.isArray(value) ? value[0] ?? null : value ?? null;
1866
+ // ../sdk/src/client.ts
1867
+ var DEFAULT_API_URL = "https://api.ambasada.dev";
1868
+ function processEnv() {
1869
+ const p = globalThis.process;
1870
+ return p?.env ?? {};
773
1871
  }
774
- function plainHeaders(headers) {
775
- const out = {};
776
- for (const name of ["webhook-id", "webhook-timestamp", "webhook-signature"]) {
777
- const v = getHeader(headers, name);
778
- if (v !== null) out[name] = v;
1872
+ var Ambasada = class {
1873
+ options;
1874
+ keyLoader;
1875
+ projectPromise;
1876
+ constructor(options = {}) {
1877
+ this.options = options;
779
1878
  }
780
- return out;
781
- }
782
- async function verifyWebhookPayload(secret, headers, body, options = {}) {
783
- if (!secret) throw new WebhookInvalidError("bad_secret", "no webhook secret: set AMBASADA_WEBHOOK_SECRET or pass it");
784
- const plain = plainHeaders(headers);
785
- let parsed;
786
- try {
787
- parsed = await verifyWebhook(secret, plain, body, options);
788
- } catch (error) {
789
- if (error instanceof WebhookError) throw new WebhookInvalidError(error.reason, error.message);
790
- if (error instanceof SyntaxError) throw new WebhookInvalidError("bad_body", "webhook body is not JSON");
791
- if (error instanceof DOMException || error instanceof TypeError) throw new WebhookInvalidError("bad_secret", "webhook secret is not whsec_<base64>");
792
- throw error;
1879
+ env(name) {
1880
+ const v = this.options.env?.[name];
1881
+ return typeof v === "string" ? v : processEnv()[name];
793
1882
  }
794
- return { id: plain["webhook-id"], type: String(parsed.type), timestamp: String(parsed.timestamp ?? ""), data: parsed.data };
795
- }
796
-
797
- // ../sdk/src/receipt.ts
798
- var RECEIPT_HEADER2 = "x-village-receipt";
799
- async function readIncoming(req) {
800
- if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
801
- const r = req;
802
- return { headers: r.headers, body: new Uint8Array(await r.clone().arrayBuffer()) };
1883
+ get apiUrl() {
1884
+ return (this.options.apiUrl ?? this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL") ?? DEFAULT_API_URL).replace(/\/+$/, "");
803
1885
  }
804
- const node = req;
805
- const raw = toBytes2(node.rawBody) ?? toBytes2(node.body);
806
- if (raw) return { headers: node.headers, body: raw };
807
- if (node.body !== void 0 && node.body !== null && !(typeof node.body === "object" && Object.keys(node.body).length === 0)) {
808
- throw new VillageError(
809
- "CONFIG",
810
- "the request body was already parsed as JSON; receipts commit to the raw bytes. Use express.raw({ type: '*/*' }) on paid routes (or keep req.rawBody)."
811
- );
1886
+ get apiKey() {
1887
+ const key = this.options.apiKey ?? (this.env("AMBASADA_API_KEY") ?? this.env("VILLAGE_API_KEY"));
1888
+ if (!key) throw new AmbasadaError("CONFIG", "no API key: set AMBASADA_API_KEY (a project key from developer.ambasada.dev, or `ambasada login`) or pass { apiKey }");
1889
+ return key;
812
1890
  }
813
- if (typeof node[Symbol.asyncIterator] === "function") {
814
- const chunks = [];
815
- for await (const chunk of node) chunks.push(toBytes2(chunk) ?? new Uint8Array());
816
- const body = concat(chunks);
1891
+ /**
1892
+ * Sign in with Ambasada (docs/AUTH.md): `login(req)`, `callback(req)`, `user(req)`, `logout(req)`.
1893
+ * Credentials come with the deploy to ambasada.dev, and from `ambasada dev` locally.
1894
+ */
1895
+ auth = new AmbasadaAuth(() => ({
1896
+ clientId: this.options.auth?.clientId ?? this.env("AMBASADA_AUTH_CLIENT_ID"),
1897
+ clientSecret: this.options.auth?.clientSecret ?? this.env("AMBASADA_AUTH_CLIENT_SECRET"),
1898
+ issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
1899
+ publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
1900
+ apiUrl: this.apiUrl,
1901
+ // Silent sign-in: a shell token must be for this app, by id or origin.
1902
+ audiences: [this.appOrigin, this.options.app?.id ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY")].filter((a) => !!a),
1903
+ hubUrl: this.hubUrl,
1904
+ fetch: this.options.fetch
1905
+ }));
1906
+ get appOrigin() {
1907
+ const url = this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL");
1908
+ return url && URL.canParse(url) ? new URL(url).origin : void 0;
1909
+ }
1910
+ get hubUrl() {
1911
+ return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
1912
+ }
1913
+ /**
1914
+ * The mini-app platform (docs/MINI-APPS.md §7): `verifyWebhook(req)` for the hub's app events and
1915
+ * `notify({ … })` to push to people who turned notifications on. Uses `AMBASADA_APP_SECRET`.
1916
+ */
1917
+ apps = new AmbasadaApps(() => ({
1918
+ appSecret: this.options.app?.secret ?? this.env("AMBASADA_APP_SECRET"),
1919
+ hubUrl: this.hubUrl,
1920
+ origin: this.appOrigin,
1921
+ fetch: this.options.fetch
1922
+ }));
1923
+ // ── HTTP ──────────────────────────────────────────────────────────────────
1924
+ async send(method, path, opts = {}) {
1925
+ const url = new URL(this.apiUrl + path);
1926
+ for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
1927
+ const headers = { accept: "application/json" };
1928
+ if (opts.auth !== false) headers.authorization = `Bearer ${this.apiKey}`;
1929
+ let body = opts.raw;
1930
+ if (opts.body !== void 0) {
1931
+ body = JSON.stringify(opts.body);
1932
+ headers["content-type"] = "application/json";
1933
+ }
1934
+ for (const [k, v] of Object.entries(opts.headers ?? {})) if (v !== void 0) headers[k.toLowerCase()] = v;
1935
+ const doFetch = this.options.fetch ?? fetch;
1936
+ const attempt = () => {
1937
+ const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 45e3);
1938
+ const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
1939
+ return doFetch(url, { method, headers, body, signal });
1940
+ };
1941
+ let res;
1942
+ try {
1943
+ res = await attempt();
1944
+ } catch (error) {
1945
+ if (opts.signal?.aborted) throw error;
1946
+ if (!opts.retry) throw new AmbasadaError("NETWORK", `${method} ${url.pathname}: ${error.message}`, 0);
1947
+ try {
1948
+ res = await attempt();
1949
+ } catch (again) {
1950
+ throw new AmbasadaError("NETWORK", `${method} ${url.pathname}: ${again.message}`, 0);
1951
+ }
1952
+ }
1953
+ const text = await res.text();
1954
+ let json = text;
1955
+ if (text && (res.headers.get("content-type") ?? "").includes("json")) {
1956
+ try {
1957
+ json = JSON.parse(text);
1958
+ } catch {
1959
+ }
1960
+ }
1961
+ if (!res.ok) throw errorFromResponse(res.status, json, `${method} ${url.pathname} \u2192 ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
1962
+ return { status: res.status, headers: res.headers, json };
1963
+ }
1964
+ async call(method, path, opts) {
1965
+ return (await this.send(method, path, opts)).json;
1966
+ }
1967
+ // ── identity, keys ────────────────────────────────────────────────────────
1968
+ /** Your app's id (from options, `AMBASADA_APP_ID`, or the project the API key belongs to). */
1969
+ project() {
1970
+ const known = this.options.project ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY");
1971
+ if (known) return Promise.resolve(known);
1972
+ this.projectPromise ??= this.call("GET", "/v1/key").then(
1973
+ (k) => k.id,
1974
+ (error) => {
1975
+ this.projectPromise = void 0;
1976
+ throw error;
1977
+ }
1978
+ );
1979
+ return this.projectPromise;
1980
+ }
1981
+ /** The API's signing keys (`/.well-known/jwks.json`, cached). */
1982
+ keys() {
1983
+ const given = this.options.keys;
1984
+ if (Array.isArray(given)) return Promise.resolve(given);
1985
+ if (typeof given === "function") return given();
1986
+ this.keyLoader ??= cachedKeys(() => this.call("GET", "/.well-known/jwks.json", { auth: false }).then((r) => r.keys));
1987
+ return this.keyLoader();
1988
+ }
1989
+ // ── storage ───────────────────────────────────────────────────────────────
1990
+ files = {
1991
+ /**
1992
+ * Stores bytes, a Blob, or a URL to copy (`POST /v1/files`). Returns a public URL. For an app's own
1993
+ * uploads prefer its bucket, `env.FILES` (docs/STORAGE.md).
1994
+ */
1995
+ put: (data, options = {}) => {
1996
+ const query = { ttl_seconds: options.ttlSeconds };
1997
+ if (typeof data === "string") return this.call("POST", "/v1/files", { body: { url: data, content_type: options.contentType }, query });
1998
+ const contentType = options.contentType ?? (data instanceof Blob && data.type ? data.type : "application/octet-stream");
1999
+ return this.call("POST", "/v1/files", { raw: data, headers: { "content-type": contentType }, query });
2000
+ }
2001
+ };
2002
+ /**
2003
+ * Your project's JSON key-value store (`/v1/store`): 1 MB per project (keys + values), free. For more,
2004
+ * use the app's own D1, KV and R2 (docs/STORAGE.md).
2005
+ */
2006
+ store = {
2007
+ /** The value, or null when the key is not set. */
2008
+ get: async (key) => {
2009
+ try {
2010
+ return (await this.call("GET", `/v1/store/${encodeURIComponent(key)}`)).value;
2011
+ } catch (error) {
2012
+ if (error instanceof AmbasadaError && error.status === 404) return null;
2013
+ throw error;
2014
+ }
2015
+ },
2016
+ /** Saves any JSON value (fails with INVALID_REQUEST when the store would pass 1 MB). */
2017
+ set: (key, value) => {
2018
+ if (value === void 0) throw new AmbasadaError("INVALID_REQUEST", "store.set needs a JSON value (use store.delete to remove a key)");
2019
+ return this.call("PUT", `/v1/store/${encodeURIComponent(key)}`, { body: value });
2020
+ },
2021
+ /** `set` under its other name. */
2022
+ put: (key, value) => this.store.set(key, value),
2023
+ /** True when the key existed. */
2024
+ delete: async (key) => (await this.call("DELETE", `/v1/store/${encodeURIComponent(key)}`)).deleted,
2025
+ /** Keys (up to 1000, optionally under a prefix) and how much of the 1 MB is used. */
2026
+ list: (prefix) => this.call("GET", "/v1/store", { query: { prefix } })
2027
+ };
2028
+ };
2029
+ function cachedKeys(load, ttlMs = 10 * 6e4) {
2030
+ let cached;
2031
+ return () => {
2032
+ if (!cached || Date.now() - cached.at > ttlMs) {
2033
+ const keys3 = load();
2034
+ const entry = { at: Date.now(), keys: keys3 };
2035
+ cached = entry;
2036
+ keys3.catch(() => {
2037
+ if (cached === entry) cached = void 0;
2038
+ });
2039
+ }
2040
+ return cached.keys;
2041
+ };
2042
+ }
2043
+ function createAmbasada(options = {}) {
2044
+ return new Ambasada(options);
2045
+ }
2046
+
2047
+ // ../sdk/src/headers.ts
2048
+ function getHeader(headers, name) {
2049
+ if (typeof headers.get === "function") return headers.get(name);
2050
+ const record = headers;
2051
+ const key = Object.keys(record).find((k) => k.toLowerCase() === name);
2052
+ const value = key === void 0 ? void 0 : record[key];
2053
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
2054
+ }
2055
+
2056
+ // ../sdk/src/define.ts
2057
+ function defineApp(config) {
2058
+ if (typeof config.id !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(config.id)) throw new Error(`app id "${String(config.id)}" must be a lowercase slug (a-z, 0-9, -)`);
2059
+ return config;
2060
+ }
2061
+
2062
+ // ../sdk/src/platform.ts
2063
+ var ambasada = new Ambasada();
2064
+ var platform_default = ambasada;
2065
+
2066
+ // ../village/src/errors.ts
2067
+ var VillageError2 = AmbasadaError;
2068
+ var isVillageError = isAmbasadaError;
2069
+ var ReceiptInvalidError = class extends AmbasadaError {
2070
+ reason;
2071
+ constructor(reason, message) {
2072
+ super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
2073
+ this.name = "ReceiptInvalidError";
2074
+ this.reason = reason;
2075
+ }
2076
+ };
2077
+
2078
+ // ../village/src/receipt.ts
2079
+ var RECEIPT_HEADER2 = "x-village-receipt";
2080
+ async function readIncoming(req) {
2081
+ if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
2082
+ const r = req;
2083
+ return { headers: r.headers, body: new Uint8Array(await r.clone().arrayBuffer()) };
2084
+ }
2085
+ const node = req;
2086
+ const raw = toBytes2(node.rawBody) ?? toBytes2(node.body);
2087
+ if (raw) return { headers: node.headers, body: raw };
2088
+ if (node.body !== void 0 && node.body !== null && !(typeof node.body === "object" && Object.keys(node.body).length === 0)) {
2089
+ throw new AmbasadaError(
2090
+ "CONFIG",
2091
+ "the request body was already parsed as JSON; receipts commit to the raw bytes. Use express.raw({ type: '*/*' }) on paid routes (or keep req.rawBody)."
2092
+ );
2093
+ }
2094
+ if (typeof node[Symbol.asyncIterator] === "function") {
2095
+ const chunks = [];
2096
+ for await (const chunk of node) chunks.push(toBytes2(chunk) ?? new Uint8Array());
2097
+ const body = concat(chunks);
817
2098
  node.rawBody = body;
818
2099
  return { headers: node.headers, body };
819
2100
  }
@@ -879,13 +2160,37 @@ function concat(chunks) {
879
2160
  return out;
880
2161
  }
881
2162
 
882
- // ../sdk/src/client.ts
883
- var DEFAULT_API_URL = "https://hackaton.ambasada.id";
884
- var DEFAULT_MODEL = "deepseek-v4.1-flash";
885
- function processEnv() {
886
- const p = globalThis.process;
887
- return p?.env ?? {};
2163
+ // ../village/src/webhook.ts
2164
+ var { WebhookError: WebhookError2, verifyWebhook: verifyProtocolWebhook } = src_exports;
2165
+ var WEBHOOK_HEADERS2 = WEBHOOK_HEADERS;
2166
+ var generateWebhookSecret2 = generateWebhookSecret;
2167
+ var webhookHeaders2 = webhookHeaders;
2168
+ function plainHeaders(headers) {
2169
+ const out = {};
2170
+ for (const name of ["webhook-id", "webhook-timestamp", "webhook-signature"]) {
2171
+ const v = getHeader(headers, name);
2172
+ if (v !== null) out[name] = v;
2173
+ }
2174
+ return out;
888
2175
  }
2176
+ async function verifyWebhookPayload(secret, headers, body, options = {}) {
2177
+ if (!secret) throw new WebhookInvalidError("bad_secret", "no webhook secret: set AMBASADA_WEBHOOK_SECRET or pass it");
2178
+ const plain = plainHeaders(headers);
2179
+ let parsed;
2180
+ try {
2181
+ parsed = await verifyProtocolWebhook(secret, plain, body, options);
2182
+ } catch (error) {
2183
+ if (error instanceof WebhookError2) throw new WebhookInvalidError(error.reason, error.message);
2184
+ if (error instanceof SyntaxError) throw new WebhookInvalidError("bad_body", "webhook body is not JSON");
2185
+ if (error instanceof DOMException || error instanceof TypeError) throw new WebhookInvalidError("bad_secret", "webhook secret is not whsec_<base64>");
2186
+ throw error;
2187
+ }
2188
+ return { id: plain["webhook-id"], type: String(parsed.type), timestamp: String(parsed.timestamp ?? ""), data: parsed.data };
2189
+ }
2190
+
2191
+ // ../village/src/client.ts
2192
+ var DEFAULT_VILLAGE_API_URL = "https://hackaton.ambasada.id";
2193
+ var DEFAULT_MODEL = "deepseek-v4.1-flash";
889
2194
  function randomKey() {
890
2195
  return `sdk_${crypto.randomUUID()}`;
891
2196
  }
@@ -894,111 +2199,28 @@ function num(value) {
894
2199
  const n = Number(value);
895
2200
  return Number.isFinite(n) ? n : null;
896
2201
  }
897
- function splitOffering(offering2) {
898
- const [company, name, ...rest] = offering2.split("/");
899
- if (!company || !name || rest.length) throw new VillageError("INVALID_REQUEST", `offering must be "company/name", got "${offering2}"`);
2202
+ function splitOffering(offering3) {
2203
+ const [company, name, ...rest] = offering3.split("/");
2204
+ if (!company || !name || rest.length) throw new AmbasadaError("INVALID_REQUEST", `offering must be "company/name", got "${offering3}"`);
900
2205
  return [company, name];
901
2206
  }
902
- var Village = class {
903
- options;
904
- keyLoader;
2207
+ var Village = class extends Ambasada {
2208
+ villageOptions;
905
2209
  companyPromise;
906
2210
  replays = new ReplayGuard();
907
2211
  constructor(options = {}) {
908
- this.options = options;
909
- }
910
- env(name) {
911
- const v = this.options.env?.[name];
912
- return typeof v === "string" ? v : processEnv()[name];
2212
+ super(options);
2213
+ this.villageOptions = options;
913
2214
  }
914
2215
  get apiUrl() {
915
- return (this.options.apiUrl ?? (this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL")) ?? DEFAULT_API_URL).replace(/\/+$/, "");
916
- }
917
- get apiKey() {
918
- const key = this.options.apiKey ?? (this.env("AMBASADA_API_KEY") ?? this.env("VILLAGE_API_KEY"));
919
- if (!key) throw new VillageError("CONFIG", "no API key: set AMBASADA_API_KEY (from the village app, or `ambasada login`) or pass { apiKey }");
920
- return key;
921
- }
922
- /**
923
- * Sign in with Ambasada (docs/AUTH.md): `login(req)`, `callback(req)`, `user(req)`, `logout(req)`.
924
- * Credentials come with the deploy to ambasada.dev, and from `ambasada dev` locally.
925
- */
926
- auth = new AmbasadaAuth(() => ({
927
- clientId: this.options.auth?.clientId ?? this.env("AMBASADA_AUTH_CLIENT_ID"),
928
- clientSecret: this.options.auth?.clientSecret ?? this.env("AMBASADA_AUTH_CLIENT_SECRET"),
929
- issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
930
- publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
931
- apiUrl: this.apiUrl,
932
- // Silent sign-in: a shell token must be for this app, by id or origin.
933
- audiences: [this.appOrigin, this.options.app?.id ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY")].filter((a) => !!a),
934
- hubUrl: this.hubUrl,
935
- fetch: this.options.fetch
936
- }));
937
- get appOrigin() {
938
- const url = this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL");
939
- return url && URL.canParse(url) ? new URL(url).origin : void 0;
2216
+ const shared = this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL");
2217
+ const legacy = shared && !/^https:\/\/api\.ambasada\.dev\/?$/.test(shared) ? shared : void 0;
2218
+ return (this.villageOptions.apiUrl ?? this.env("AMBASADA_VILLAGE_API_URL") ?? legacy ?? DEFAULT_VILLAGE_API_URL).replace(/\/+$/, "");
940
2219
  }
941
- get hubUrl() {
942
- return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
943
- }
944
- /**
945
- * The mini-app platform (docs/MINI-APPS.md §7): `verifyWebhook(req)` for the hub's app events and
946
- * `notify({ … })` to push to people who turned notifications on. Uses `AMBASADA_APP_SECRET`.
947
- */
948
- apps = new AmbasadaApps(() => ({
949
- appSecret: this.options.app?.secret ?? this.env("AMBASADA_APP_SECRET"),
950
- hubUrl: this.hubUrl,
951
- origin: this.appOrigin,
952
- fetch: this.options.fetch
953
- }));
954
- // ── HTTP ──────────────────────────────────────────────────────────────────
955
- async send(method, path, opts = {}) {
956
- const url = new URL(this.apiUrl + path);
957
- for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
958
- const headers = { accept: "application/json" };
959
- if (opts.auth !== false) headers.authorization = `Bearer ${this.apiKey}`;
960
- let body = opts.raw;
961
- if (opts.body !== void 0) {
962
- body = JSON.stringify(opts.body);
963
- headers["content-type"] = "application/json";
964
- }
965
- for (const [k, v] of Object.entries(opts.headers ?? {})) if (v !== void 0) headers[k.toLowerCase()] = v;
966
- const doFetch = this.options.fetch ?? fetch;
967
- const attempt = () => {
968
- const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 45e3);
969
- const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
970
- return doFetch(url, { method, headers, body, signal });
971
- };
972
- let res;
973
- try {
974
- res = await attempt();
975
- } catch (error) {
976
- if (opts.signal?.aborted) throw error;
977
- if (!opts.retry) throw new VillageError("NETWORK", `${method} ${url.pathname}: ${error.message}`, 0);
978
- try {
979
- res = await attempt();
980
- } catch (again) {
981
- throw new VillageError("NETWORK", `${method} ${url.pathname}: ${again.message}`, 0);
982
- }
983
- }
984
- const text = await res.text();
985
- let json = text;
986
- if (text && (res.headers.get("content-type") ?? "").includes("json")) {
987
- try {
988
- json = JSON.parse(text);
989
- } catch {
990
- }
991
- }
992
- if (!res.ok) throw errorFromResponse(res.status, json, `${method} ${url.pathname} \u2192 ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
993
- return { status: res.status, headers: res.headers, json };
994
- }
995
- async call(method, path, opts) {
996
- return (await this.send(method, path, opts)).json;
997
- }
998
- // ── identity, keys ────────────────────────────────────────────────────────
2220
+ // ── identity ──────────────────────────────────────────────────────────────
999
2221
  /** Your company id (from options, `AMBASADA_COMPANY`, or the wallet the API key belongs to). */
1000
2222
  company() {
1001
- const known = this.options.company ?? (this.env("AMBASADA_COMPANY") ?? this.env("VILLAGE_COMPANY"));
2223
+ const known = this.villageOptions.company ?? (this.env("AMBASADA_COMPANY") ?? this.env("VILLAGE_COMPANY"));
1002
2224
  if (known) return Promise.resolve(known);
1003
2225
  this.companyPromise ??= this.wallet().then(
1004
2226
  (w) => w.account,
@@ -1009,14 +2231,6 @@ var Village = class {
1009
2231
  );
1010
2232
  return this.companyPromise;
1011
2233
  }
1012
- /** Village receipt keys (cached). */
1013
- keys() {
1014
- const given = this.options.keys;
1015
- if (Array.isArray(given)) return Promise.resolve(given);
1016
- if (typeof given === "function") return given();
1017
- this.keyLoader ??= cachedKeys(() => this.call("GET", "/.well-known/jwks.json", { auth: false }).then((r) => r.keys));
1018
- return this.keyLoader();
1019
- }
1020
2234
  keySource() {
1021
2235
  return () => this.keys();
1022
2236
  }
@@ -1052,26 +2266,26 @@ var Village = class {
1052
2266
  }
1053
2267
  /**
1054
2268
  * Wraps a paid offering as a fetch handler: verifies the receipt (402 if missing or bad), runs
1055
- * `handler(input, call, req)` and answers JSON. A thrown `VillageError` keeps its status (the
2269
+ * `handler(input, call, req)` and answers JSON. A thrown `AmbasadaError` keeps its status (the
1056
2270
  * Gateway then refunds the buyer).
1057
2271
  *
1058
2272
  * export default village.sell("translate", async ({ text, to }) => ({ text: await village.llm(`…`) }));
1059
2273
  */
1060
- sell(offering2, handler) {
2274
+ sell(offering3, handler) {
1061
2275
  return async (req) => {
1062
2276
  if (req.method !== "POST") return Response.json({ error: { code: "INVALID_REQUEST", message: "POST only" } }, { status: 405 });
1063
2277
  let call;
1064
2278
  try {
1065
- call = await this.verifyReceipt(req, { offering: offering2 });
2279
+ call = await this.verifyReceipt(req, { offering: offering3 });
1066
2280
  } catch (error) {
1067
- if (error instanceof VillageError) return error.toResponse();
2281
+ if (error instanceof AmbasadaError) return error.toResponse();
1068
2282
  throw error;
1069
2283
  }
1070
2284
  try {
1071
2285
  const out = await handler(call.input, call, req);
1072
2286
  return out instanceof Response ? out : Response.json(out);
1073
2287
  } catch (error) {
1074
- if (error instanceof VillageError) return error.toResponse();
2288
+ if (error instanceof AmbasadaError) return error.toResponse();
1075
2289
  console.error(error);
1076
2290
  return Response.json({ error: { code: "INTERNAL", message: "handler failed" } }, { status: 500 });
1077
2291
  }
@@ -1080,7 +2294,7 @@ var Village = class {
1080
2294
  /** Verifies a signed webhook (`checkout.paid`, `webhook.test`) and returns the event. Throws `WebhookInvalidError`. */
1081
2295
  async verifyWebhook(req, secret) {
1082
2296
  const { headers, body } = await readIncoming(req);
1083
- return verifyWebhookPayload(secret ?? this.options.webhookSecret ?? (this.env("AMBASADA_WEBHOOK_SECRET") ?? this.env("VILLAGE_WEBHOOK_SECRET")) ?? "", headers, body);
2297
+ return verifyWebhookPayload(secret ?? this.villageOptions.webhookSecret ?? (this.env("AMBASADA_WEBHOOK_SECRET") ?? this.env("VILLAGE_WEBHOOK_SECRET")) ?? "", headers, body);
1084
2298
  }
1085
2299
  /** Verifies a checkout receipt (pay.js `village:paid` → `e.detail.receipt`, or `session.receipt`). */
1086
2300
  async verifyCheckoutReceipt(token, options = {}) {
@@ -1128,8 +2342,8 @@ var Village = class {
1128
2342
  }
1129
2343
  // ── buying (machines) ─────────────────────────────────────────────────────
1130
2344
  /** Buys one call of `company/offering` through the Gateway. Money moves only if the seller answers 2xx with valid output. */
1131
- async buy(offering2, input, options = {}) {
1132
- const [company, name] = splitOffering(offering2);
2345
+ async buy(offering3, input, options = {}) {
2346
+ const [company, name] = splitOffering(offering3);
1133
2347
  return this.paid(`/v1/pay/${encodeURIComponent(company)}/${encodeURIComponent(name)}`, input, options);
1134
2348
  }
1135
2349
  /**
@@ -1208,42 +2422,6 @@ var Village = class {
1208
2422
  };
1209
2423
  }
1210
2424
  };
1211
- files = {
1212
- /**
1213
- * Stores bytes, a Blob, or a URL to copy, for the event (`POST /v1/files`, 0.1 TON). Returns a
1214
- * public URL other products can fetch.
1215
- */
1216
- put: (data, options = {}) => {
1217
- const query = { ttl_seconds: options.ttlSeconds };
1218
- if (typeof data === "string") return this.call("POST", "/v1/files", { body: { url: data, content_type: options.contentType }, query });
1219
- const contentType = options.contentType ?? (data instanceof Blob && data.type ? data.type : "application/octet-stream");
1220
- return this.call("POST", "/v1/files", { raw: data, headers: { "content-type": contentType }, query });
1221
- }
1222
- };
1223
- /**
1224
- * Your company's JSON key-value store (`/v1/store`): 1 MB per company (keys + values), free. State for
1225
- * a hosted product that needs no database.
1226
- */
1227
- store = {
1228
- /** The value, or null when the key is not set. */
1229
- get: async (key) => {
1230
- try {
1231
- return (await this.call("GET", `/v1/store/${encodeURIComponent(key)}`)).value;
1232
- } catch (error) {
1233
- if (error instanceof VillageError && error.status === 404) return null;
1234
- throw error;
1235
- }
1236
- },
1237
- /** Saves any JSON value (fails with INVALID_REQUEST when the store would pass 1 MB). */
1238
- set: (key, value) => {
1239
- if (value === void 0) throw new VillageError("INVALID_REQUEST", "store.set needs a JSON value (use store.delete to remove a key)");
1240
- return this.call("PUT", `/v1/store/${encodeURIComponent(key)}`, { body: value });
1241
- },
1242
- /** True when the key existed. */
1243
- delete: async (key) => (await this.call("DELETE", `/v1/store/${encodeURIComponent(key)}`)).deleted,
1244
- /** Keys (up to 1000, optionally under a prefix) and how much of the 1 MB is used. */
1245
- list: (prefix) => this.call("GET", "/v1/store", { query: { prefix } })
1246
- };
1247
2425
  // ── listings, wallet, ledger ──────────────────────────────────────────────
1248
2426
  listings = {
1249
2427
  /** Creates or updates an offering (price changes are versioned). */
@@ -1279,26 +2457,14 @@ var Village = class {
1279
2457
  state: () => this.call("GET", "/v1/agent/state")
1280
2458
  };
1281
2459
  };
1282
- function cachedKeys(load, ttlMs = 10 * 6e4) {
1283
- let cached;
1284
- return () => {
1285
- if (!cached || Date.now() - cached.at > ttlMs) {
1286
- const keys3 = load();
1287
- const entry = { at: Date.now(), keys: keys3 };
1288
- cached = entry;
1289
- keys3.catch(() => {
1290
- if (cached === entry) cached = void 0;
1291
- });
1292
- }
1293
- return cached.keys;
1294
- };
1295
- }
1296
- function createAmbasada(options = {}) {
2460
+ function createVillage(options = {}) {
1297
2461
  return new Village(options);
1298
2462
  }
1299
2463
 
1300
- // ../sdk/src/define.ts
1301
- function defineCompany(config) {
2464
+ // ../village/src/define.ts
2465
+ function defineCompany(app, village3) {
2466
+ const config = { ...app, ...village3 };
2467
+ if (!config.idea || typeof config.idea.title !== "string") throw new Error(`${config.id}: the village needs idea: { title }`);
1302
2468
  return config;
1303
2469
  }
1304
2470
  function offering(def) {
@@ -1307,39 +2473,66 @@ function offering(def) {
1307
2473
  return def;
1308
2474
  }
1309
2475
 
1310
- // ../sdk/src/index.ts
2476
+ // ../village/src/index.ts
1311
2477
  var village = new Village();
1312
- var index_default = village;
2478
+
2479
+ // ../sdk/src/index.ts
2480
+ var village2 = village;
2481
+ var createVillage2 = createVillage;
2482
+ var Village2 = Village;
2483
+ var VillageError3 = VillageError2;
2484
+ var isVillageError2 = isVillageError;
2485
+ var ReceiptInvalidError2 = ReceiptInvalidError;
2486
+ var DEFAULT_MODEL2 = DEFAULT_MODEL;
2487
+ var verifyWebhookPayload2 = verifyWebhookPayload;
2488
+ var webhookHeaders3 = webhookHeaders2;
2489
+ var generateWebhookSecret3 = generateWebhookSecret2;
2490
+ var WEBHOOK_HEADERS3 = WEBHOOK_HEADERS2;
2491
+ var RECEIPT_HEADER3 = RECEIPT_HEADER2;
2492
+ var readIncoming2 = readIncoming;
2493
+ var verifyCallReceipt2 = verifyCallReceipt;
2494
+ var verifyCheckoutReceiptToken2 = verifyCheckoutReceiptToken;
2495
+ var defineCompany2 = defineCompany;
2496
+ var offering2 = offering;
2497
+ var DEFAULT_VILLAGE_API_URL2 = DEFAULT_VILLAGE_API_URL;
1313
2498
  export {
1314
2499
  APP_SIGNATURE_HEADER2 as APP_SIGNATURE_HEADER,
1315
2500
  APP_TIMESTAMP_HEADER2 as APP_TIMESTAMP_HEADER,
1316
2501
  AUTH_PATHS,
2502
+ Ambasada,
1317
2503
  AmbasadaApps,
1318
2504
  AmbasadaAuth,
2505
+ AmbasadaError,
1319
2506
  DEFAULT_API_URL,
1320
- DEFAULT_MODEL,
1321
- ERROR_STATUS,
2507
+ DEFAULT_MODEL2 as DEFAULT_MODEL,
2508
+ DEFAULT_VILLAGE_API_URL2 as DEFAULT_VILLAGE_API_URL,
2509
+ ERROR_STATUS2 as ERROR_STATUS,
1322
2510
  JWKS_TTL_MS,
1323
- RECEIPT_HEADER2 as RECEIPT_HEADER,
1324
- ReceiptInvalidError,
2511
+ RECEIPT_HEADER3 as RECEIPT_HEADER,
2512
+ ReceiptInvalidError2 as ReceiptInvalidError,
1325
2513
  SESSION_TTL_S,
1326
- Village,
1327
- VillageError,
1328
- WEBHOOK_HEADERS,
2514
+ Village2 as Village,
2515
+ VillageError3 as VillageError,
2516
+ WEBHOOK_HEADERS3 as WEBHOOK_HEADERS,
1329
2517
  WebhookInvalidError,
2518
+ ambasada,
1330
2519
  appWebhookHeaders2 as appWebhookHeaders,
1331
2520
  createAmbasada,
1332
- index_default as default,
1333
- defineCompany,
2521
+ createVillage2 as createVillage,
2522
+ platform_default as default,
2523
+ defineApp,
2524
+ defineCompany2 as defineCompany,
1334
2525
  errorFromResponse,
1335
- generateWebhookSecret,
1336
- isVillageError,
1337
- offering,
1338
- readIncoming,
2526
+ generateWebhookSecret3 as generateWebhookSecret,
2527
+ getHeader,
2528
+ isAmbasadaError,
2529
+ isVillageError2 as isVillageError,
2530
+ offering2 as offering,
2531
+ readIncoming2 as readIncoming,
1339
2532
  signAppWebhook2 as signAppWebhook,
1340
- verifyCallReceipt,
1341
- verifyCheckoutReceiptToken,
1342
- verifyWebhookPayload,
1343
- village,
1344
- webhookHeaders
2533
+ verifyCallReceipt2 as verifyCallReceipt,
2534
+ verifyCheckoutReceiptToken2 as verifyCheckoutReceiptToken,
2535
+ verifyWebhookPayload2 as verifyWebhookPayload,
2536
+ village2 as village,
2537
+ webhookHeaders3 as webhookHeaders
1345
2538
  };