@ambasada/sdk 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2538 +1,43 @@
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
-
188
- // ../protocol/src/encoding.ts
189
- var encoder = new TextEncoder();
190
- var decoder = new TextDecoder();
191
- function utf8(text) {
192
- return encoder.encode(text);
193
- }
194
- function fromUtf8(bytes) {
195
- return decoder.decode(bytes);
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
- }
202
- function fromBase64url(text) {
203
- const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4);
204
- const binary = atob(padded);
205
- const bytes = new Uint8Array(binary.length);
206
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
207
- return bytes;
208
- }
209
- function toBytes(data) {
210
- if (typeof data === "string") return utf8(data);
211
- if (data instanceof Uint8Array) return new Uint8Array(data);
212
- return new Uint8Array(data);
213
- }
214
- async function sha256Hex(data) {
215
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", toBytes(data)));
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)}`;
224
- }
225
-
226
- // ../protocol/src/receipt.ts
227
- var RECEIPT_HEADER = "x-village-receipt";
228
- var DEFAULT_ISSUER = "village";
229
- var ReceiptError = class extends Error {
230
- constructor(reason, message) {
231
- super(message);
232
- this.reason = reason;
233
- this.name = "ReceiptError";
234
- }
235
- reason;
236
- };
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
- }
270
- async function verifyReceipt(token, options) {
271
- const claims = await verifySigned(token, options);
272
- if (claims.kind !== void 0 || typeof claims.call_id !== "string" || typeof claims.body_sha256 !== "string") {
273
- throw new ReceiptError("wrong_kind", "not a call receipt");
274
- }
275
- if (options.seller !== void 0 && claims.seller !== options.seller) {
276
- throw new ReceiptError("wrong_seller", `receipt is for seller ${claims.seller}`);
277
- }
278
- if (options.offering !== void 0 && claims.offering !== options.offering) {
279
- throw new ReceiptError("wrong_offering", `receipt is for offering ${claims.offering}`);
280
- }
281
- if (options.body !== void 0 && await sha256Hex(options.body) !== claims.body_sha256) {
282
- throw new ReceiptError("body_mismatch", "request body does not match the receipt");
283
- }
284
- return claims;
285
- }
286
- async function verifyCheckoutReceipt(token, options) {
287
- const claims = await verifySigned(token, options);
288
- if (claims.kind !== "checkout" || typeof claims.session !== "string") throw new ReceiptError("wrong_kind", "not a checkout receipt");
289
- if (options.seller !== void 0 && claims.seller !== options.seller) {
290
- throw new ReceiptError("wrong_seller", `receipt is for seller ${claims.seller}`);
291
- }
292
- if (options.session !== void 0 && claims.session !== options.session) {
293
- throw new ReceiptError("wrong_session", `receipt is for checkout ${claims.session}`);
294
- }
295
- return claims;
296
- }
297
- async function verifySigned(token, options) {
298
- if (!token) throw new ReceiptError("missing", `missing ${RECEIPT_HEADER}`);
299
- const parts = token.split(".");
300
- if (parts.length !== 3) throw new ReceiptError("malformed", "receipt is not a JWT");
301
- const [headerPart, payloadPart, signaturePart] = parts;
302
- let header3;
303
- let claims;
304
- try {
305
- header3 = JSON.parse(fromUtf8(fromBase64url(headerPart)));
306
- claims = JSON.parse(fromUtf8(fromBase64url(payloadPart)));
307
- } catch {
308
- throw new ReceiptError("malformed", "receipt is not valid base64url JSON");
309
- }
310
- if (header3.alg !== "EdDSA") throw new ReceiptError("malformed", `unsupported alg ${header3.alg}`);
311
- if (typeof claims !== "object" || claims === null) throw new ReceiptError("malformed", "receipt claims are not an object");
312
- const keys3 = typeof options.keys === "function" ? await options.keys() : options.keys;
313
- const jwk = keys3.find((k) => (k.kid ?? k.x.slice(0, 16)) === header3.kid) ?? (keys3.length === 1 && !header3.kid ? keys3[0] : void 0);
314
- if (!jwk) throw new ReceiptError("unknown_key", `no village key with kid ${header3.kid}`);
315
- const key = await crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, ED25519, false, ["verify"]);
316
- const ok = await crypto.subtle.verify(ED25519, key, fromBase64url(signaturePart), utf8(`${headerPart}.${payloadPart}`));
317
- if (!ok) throw new ReceiptError("bad_signature", "receipt signature is invalid");
318
- const now = options.now ?? Math.floor(Date.now() / 1e3);
319
- const skew = options.clockSkewSeconds ?? 30;
320
- if (typeof claims.exp !== "number" || claims.exp + skew < now) throw new ReceiptError("expired", "receipt expired");
321
- if (typeof claims.iat === "number" && claims.iat - skew > now) throw new ReceiptError("not_yet_valid", "receipt issued in the future");
322
- if (claims.iss !== (options.issuer ?? DEFAULT_ISSUER)) throw new ReceiptError("wrong_issuer", `unexpected issuer ${String(claims.iss)}`);
323
- return claims;
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
- }
903
-
904
- // ../protocol/src/webhook.ts
905
- var WEBHOOK_HEADERS = {
906
- id: "webhook-id",
907
- timestamp: "webhook-timestamp",
908
- signature: "webhook-signature"
909
- };
910
- var SECRET_PREFIX = "whsec_";
911
- var encoder2 = new TextEncoder();
912
- var WebhookError = class extends Error {
913
- constructor(reason, message) {
914
- super(message);
915
- this.reason = reason;
916
- this.name = "WebhookError";
917
- }
918
- reason;
919
- };
920
- function generateWebhookSecret() {
921
- const bytes = new Uint8Array(24);
922
- crypto.getRandomValues(bytes);
923
- return SECRET_PREFIX + toStdBase64(bytes);
924
- }
925
- async function hmacKey(secret) {
926
- const raw = secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret;
927
- let bytes;
928
- try {
929
- bytes = fromStdBase64(raw);
930
- } catch {
931
- throw new WebhookError("bad_secret", "webhook secret is not whsec_<base64>");
932
- }
933
- return crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
934
- }
935
- async function signWebhook(secret, id, timestamp, body) {
936
- const mac2 = await crypto.subtle.sign("HMAC", await hmacKey(secret), encoder2.encode(`${id}.${timestamp}.${body}`));
937
- return `v1,${toStdBase64(new Uint8Array(mac2))}`;
938
- }
939
- async function webhookHeaders(secret, id, timestamp, body) {
940
- return {
941
- [WEBHOOK_HEADERS.id]: id,
942
- [WEBHOOK_HEADERS.timestamp]: String(timestamp),
943
- [WEBHOOK_HEADERS.signature]: await signWebhook(secret, id, timestamp, body)
944
- };
945
- }
946
- function header(headers, name) {
947
- if (headers instanceof Headers) return headers.get(name);
948
- const value = headers[name] ?? headers[name.toLowerCase()];
949
- return Array.isArray(value) ? value[0] ?? null : value ?? null;
950
- }
951
- async function verifyWebhook(secret, headers, body, { toleranceSeconds = 300, now = Math.floor(Date.now() / 1e3) } = {}) {
952
- const id = header(headers, WEBHOOK_HEADERS.id);
953
- const ts = header(headers, WEBHOOK_HEADERS.timestamp);
954
- const signatures = header(headers, WEBHOOK_HEADERS.signature);
955
- if (!id || !ts || !signatures) throw new WebhookError("missing_headers", "missing webhook-id, webhook-timestamp or webhook-signature");
956
- const timestamp = Number(ts);
957
- if (!/^\d+$/.test(ts) || !Number.isSafeInteger(timestamp)) throw new WebhookError("bad_timestamp", "webhook-timestamp is not unix seconds");
958
- if (timestamp < now - toleranceSeconds) throw new WebhookError("too_old", "webhook timestamp is too old");
959
- if (timestamp > now + toleranceSeconds) throw new WebhookError("too_new", "webhook timestamp is in the future");
960
- const text = typeof body === "string" ? body : fromUtf8(body);
961
- const expected = (await signWebhook(secret, id, timestamp, text)).slice(3);
962
- const match = signatures.split(" ").filter((s) => s.startsWith("v1,")).some((s) => constantTimeEqual(s.slice(3), expected));
963
- if (!match) throw new WebhookError("bad_signature", "no matching webhook signature");
964
- return JSON.parse(text);
965
- }
966
- function constantTimeEqual(a, b) {
967
- if (a.length !== b.length) return false;
968
- let diff = 0;
969
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
970
- return diff === 0;
971
- }
972
- function toStdBase64(bytes) {
973
- let binary = "";
974
- for (const byte of bytes) binary += String.fromCharCode(byte);
975
- return btoa(binary);
976
- }
977
- function fromStdBase64(text) {
978
- const binary = atob(text);
979
- const bytes = new Uint8Array(binary.length);
980
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
981
- return bytes;
982
- }
983
-
984
- // ../protocol/src/apps.ts
985
- var APP_SIGNATURE_HEADER = "x-ambasada-signature";
986
- var APP_TIMESTAMP_HEADER = "x-ambasada-timestamp";
987
- var APP_WEBHOOK_TOLERANCE_S = 300;
988
- var DEFAULT_HUB_URL = "https://hub.ambasada.id";
989
- var APP_NOTIFY_PATH = "/api/apps/notify";
990
- var APP_EVENT_TYPES = ["notifications_enabled", "notifications_disabled", "app_added", "app_removed", "ping"];
991
- var AppWebhookError = class extends Error {
992
- constructor(reason, message) {
993
- super(message);
994
- this.reason = reason;
995
- this.name = "AppWebhookError";
996
- }
997
- reason;
998
- };
999
- var encoder3 = new TextEncoder();
1000
- var keys = /* @__PURE__ */ new Map();
1001
- function hmacKey2(secret) {
1002
- let key = keys.get(secret);
1003
- if (!key) {
1004
- key = crypto.subtle.importKey("raw", encoder3.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1005
- keys.set(secret, key);
1006
- }
1007
- return key;
1008
- }
1009
- var hex2 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1010
- async function mac(secret, timestamp, body) {
1011
- return new Uint8Array(await crypto.subtle.sign("HMAC", await hmacKey2(secret), encoder3.encode(`${timestamp}.${body}`)));
1012
- }
1013
- async function signAppWebhook(secret, timestamp, body) {
1014
- if (!secret) throw new AppWebhookError("bad_secret", "no app secret");
1015
- return `v1=${hex2(await mac(secret, String(timestamp), body))}`;
1016
- }
1017
- async function appWebhookHeaders(secret, body, timestamp = Math.floor(Date.now() / 1e3)) {
1018
- return { [APP_TIMESTAMP_HEADER]: String(timestamp), [APP_SIGNATURE_HEADER]: await signAppWebhook(secret, timestamp, body), "content-type": "application/json" };
1019
- }
1020
- function decodeSignature(value) {
1021
- if (/^[0-9a-f]{64}$/i.test(value)) return Uint8Array.from(value.match(/../g), (h) => parseInt(h, 16));
1022
- try {
1023
- const std = value.replace(/-/g, "+").replace(/_/g, "/");
1024
- const bin = atob(std + "===".slice((std.length + 3) % 4));
1025
- return bin.length === 32 ? Uint8Array.from(bin, (c) => c.charCodeAt(0)) : null;
1026
- } catch {
1027
- return null;
1028
- }
1029
- }
1030
- function equal(a, b) {
1031
- if (a.length !== b.length) return false;
1032
- let diff = 0;
1033
- for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
1034
- return diff === 0;
1035
- }
1036
- function header2(headers, name) {
1037
- if (typeof headers.get === "function") return headers.get(name);
1038
- const record = headers;
1039
- const key = Object.keys(record).find((k) => k.toLowerCase() === name);
1040
- const value = key === void 0 ? void 0 : record[key];
1041
- return Array.isArray(value) ? value[0] ?? null : value ?? null;
1042
- }
1043
- async function verifyAppWebhook(secret, headers, body, { toleranceSeconds = APP_WEBHOOK_TOLERANCE_S, now = Math.floor(Date.now() / 1e3) } = {}) {
1044
- if (!secret) throw new AppWebhookError("bad_secret", "no app secret: set AMBASADA_APP_SECRET (injected on ambasada.dev; from the hub dashboard elsewhere)");
1045
- const ts = header2(headers, APP_TIMESTAMP_HEADER);
1046
- const sig = header2(headers, APP_SIGNATURE_HEADER);
1047
- if (!ts || !sig) throw new AppWebhookError("missing_headers", `missing ${APP_TIMESTAMP_HEADER} or ${APP_SIGNATURE_HEADER}`);
1048
- if (!/^\d+$/.test(ts.trim())) throw new AppWebhookError("bad_timestamp", `${APP_TIMESTAMP_HEADER} is not unix seconds`);
1049
- let timestamp = Number(ts.trim());
1050
- if (timestamp > 1e12) timestamp = Math.floor(timestamp / 1e3);
1051
- if (timestamp < now - toleranceSeconds) throw new AppWebhookError("too_old", "webhook timestamp is too old");
1052
- if (timestamp > now + toleranceSeconds) throw new AppWebhookError("too_new", "webhook timestamp is in the future");
1053
- const expected = await mac(secret, ts.trim(), body);
1054
- const candidates = sig.split(/[\s,]+/).map((part) => /^v1=(.+)$/.exec(part.trim())?.[1]).filter((v) => !!v).map(decodeSignature).filter((v) => v !== null);
1055
- if (!candidates.some((c) => equal(c, expected))) throw new AppWebhookError("bad_signature", "webhook signature does not match: check AMBASADA_APP_SECRET");
1056
- let event;
1057
- try {
1058
- event = JSON.parse(body);
1059
- } catch {
1060
- throw new AppWebhookError("bad_body", "webhook body is not JSON");
1061
- }
1062
- if (!event || typeof event !== "object" || typeof event.event !== "string") throw new AppWebhookError("bad_body", 'webhook body has no "event"');
1063
- return event;
1064
- }
1065
- var NOTIFY_LIMITS = { notification_id: 128, title: 40, body: 140, recipients: 100, collapse_key: 128, ttl_seconds: 7 * 24 * 3600 };
1066
- var CATEGORIES = ["transactional", "social", "marketing"];
1067
- function appNotifyBody(input, origin) {
1068
- const fail = (message) => {
1069
- throw new Error(message);
1070
- };
1071
- const id = typeof input.notification_id === "string" ? input.notification_id : "";
1072
- if (!id || id.length > NOTIFY_LIMITS.notification_id) fail(`notification_id must be 1\u2013${NOTIFY_LIMITS.notification_id} characters`);
1073
- const title = typeof input.title === "string" ? input.title.trim() : "";
1074
- if (!title || title.length > NOTIFY_LIMITS.title) fail(`title must be 1\u2013${NOTIFY_LIMITS.title} characters (got ${title.length})`);
1075
- const body = typeof input.body === "string" ? input.body.trim() : "";
1076
- if (body.length > NOTIFY_LIMITS.body) fail(`body must be at most ${NOTIFY_LIMITS.body} characters (got ${body.length})`);
1077
- const url = typeof input.url === "string" ? input.url.trim() : "";
1078
- if (!url) fail("url is required: a path on your app (/orders/184) or a URL on its origin");
1079
- if (url.startsWith("/")) {
1080
- if (url.startsWith("//") || url.startsWith("/\\") || /[\u0000-\u001f\u007f]/.test(url)) fail(`url ${JSON.stringify(url)} is not a path on your app`);
1081
- } else {
1082
- let parsed = null;
1083
- try {
1084
- parsed = new URL(url);
1085
- } catch {
1086
- fail(`url ${JSON.stringify(url)} is not a URL or a path`);
1087
- }
1088
- if (parsed.protocol !== "https:" && !(origin && parsed.origin === new URL(origin).origin && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(parsed.origin))) fail("url must be https");
1089
- if (parsed.username || parsed.password) fail("url must not carry credentials");
1090
- if (origin && parsed.origin !== new URL(origin).origin) fail(`url must be on your app's origin ${new URL(origin).origin} (got ${parsed.origin})`);
1091
- }
1092
- const tokens = input.tokens ?? [];
1093
- const users = input.users ?? [];
1094
- if (!Array.isArray(tokens) || !Array.isArray(users)) fail("tokens and users must be arrays");
1095
- if (tokens.length && users.length) fail("send tokens or users, not both");
1096
- const recipients = tokens.length ? tokens : users;
1097
- if (!recipients.length) fail("no recipients: pass tokens (from notifications_enabled) or users");
1098
- if (recipients.length > NOTIFY_LIMITS.recipients) fail(`at most ${NOTIFY_LIMITS.recipients} recipients per call (got ${recipients.length})`);
1099
- if (recipients.some((r) => typeof r !== "string" || !r)) fail("recipients must be non-empty strings");
1100
- const category = input.category ?? "transactional";
1101
- if (!CATEGORIES.includes(category)) fail(`category must be one of ${CATEGORIES.join(", ")}`);
1102
- if (input.collapse_key !== void 0 && (typeof input.collapse_key !== "string" || !input.collapse_key || input.collapse_key.length > NOTIFY_LIMITS.collapse_key)) fail(`collapse_key must be 1\u2013${NOTIFY_LIMITS.collapse_key} characters`);
1103
- if (input.ttl_seconds !== void 0 && (!Number.isInteger(input.ttl_seconds) || input.ttl_seconds < 0 || input.ttl_seconds > NOTIFY_LIMITS.ttl_seconds)) fail(`ttl_seconds must be an integer 0\u2013${NOTIFY_LIMITS.ttl_seconds}`);
1104
- return {
1105
- notification_id: id,
1106
- title,
1107
- body,
1108
- url,
1109
- ...tokens.length ? { tokens } : { users },
1110
- category,
1111
- ...input.collapse_key !== void 0 ? { collapse_key: input.collapse_key } : {},
1112
- ...input.ttl_seconds !== void 0 ? { ttl_seconds: input.ttl_seconds } : {}
1113
- };
1114
- }
1115
- function appNotifyResult(json) {
1116
- const o = json && typeof json === "object" ? json : {};
1117
- const list = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
1118
- return { sent: list(o.sent), invalid: list(o.invalid), rate_limited: list(o.rate_limited) };
1119
- }
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
-
1414
- // ../sdk/src/errors.ts
1415
- var ERROR_STATUS2 = {
1416
- // EXCHANGE.md §5 vocabulary
1417
- INSUFFICIENT_FUNDS: 402,
1418
- OFFLINE: 503,
1419
- TIMEOUT: 504,
1420
- SCHEMA_INVALID: 502,
1421
- CAP_BUYER_HOURLY: 429,
1422
- CAP_SELLER_RATE: 429,
1423
- DEAL_EXHAUSTED: 409,
1424
- DEPTH_EXCEEDED: 422,
1425
- BUDGET_EXCEEDED: 402,
1426
- // Gateway and bank
1427
- UPSTREAM_ERROR: 502,
1428
- PRICE_ABOVE_MAX: 409,
1429
- SELF_PURCHASE: 403,
1430
- INVALID_REQUEST: 400,
1431
- UNAUTHORIZED: 401,
1432
- NOT_FOUND: 404,
1433
- CONFLICT: 409,
1434
- IDEMPOTENCY_CONFLICT: 422,
1435
- IDEMPOTENCY_IN_PROGRESS: 409,
1436
- HOLD_NOT_ACTIVE: 409,
1437
- INTERNAL: 500,
1438
- // Spending limits and approvals (checkout stream)
1439
- CAP_PER_PAYMENT: 403,
1440
- CAP_BUYER_DAILY: 429,
1441
- CAP_PAYEE_DAILY: 429,
1442
- CAP_VISITOR_DAILY: 429,
1443
- APPROVAL_REQUIRED: 403,
1444
- APPROVAL_INVALID: 403,
1445
- APPROVALS_PENDING: 429,
1446
- // LLMOK mapping (utilities stream, docs/LLMOK.md)
1447
- PROVIDER_BUDGET: 503,
1448
- PROVIDER_DENIED: 502,
1449
- // Clock windows (agent stream): deploys / steering refused while frozen
1450
- FROZEN: 403,
1451
- // Exchange stream: an agent sent too many inbox messages
1452
- CAP_INBOX_RATE: 429,
1453
- // SDK-side
1454
- NETWORK: 0,
1455
- CONFIG: 0,
1456
- RECEIPT_INVALID: 402,
1457
- WEBHOOK_INVALID: 400
1458
- };
1459
- var BRAND = /* @__PURE__ */ Symbol.for("ambasada.error");
1460
- var AmbasadaError = class _AmbasadaError extends Error {
1461
- code;
1462
- /** HTTP status (0 when the request never got an answer). */
1463
- status;
1464
- details;
1465
- constructor(code, message, status, details = {}) {
1466
- super(message);
1467
- this.name = "AmbasadaError";
1468
- this.code = code;
1469
- this.status = status ?? ERROR_STATUS2[code] ?? 500;
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;
1477
- }
1478
- get callId() {
1479
- return this.details.call_id;
1480
- }
1481
- get refunded() {
1482
- return this.details.refunded === true;
1483
- }
1484
- get approvalId() {
1485
- return this.details.approval_id;
1486
- }
1487
- /** The wire shape, e.g. to pass an error on from your own endpoint. */
1488
- toJSON() {
1489
- return { error: { code: this.code, message: this.message, ...this.details } };
1490
- }
1491
- /** A `Response` with the wire shape and status (402 for receipt problems). */
1492
- toResponse() {
1493
- return Response.json(this.toJSON(), { status: this.status || 500 });
1494
- }
1495
- };
1496
- var WebhookInvalidError = class extends AmbasadaError {
1497
- reason;
1498
- constructor(reason, message) {
1499
- super("WEBHOOK_INVALID", message, 400, { reason });
1500
- this.name = "WebhookInvalidError";
1501
- this.reason = reason;
1502
- }
1503
- };
1504
- function isAmbasadaError(value, code) {
1505
- return value instanceof AmbasadaError && (code === void 0 || value.code === code);
1506
- }
1507
- function errorFromResponse(status, body, fallback) {
1508
- const error = typeof body === "object" && body !== null ? body.error : void 0;
1509
- if (typeof error === "object" && error !== null && typeof error.code === "string") {
1510
- const { code: code2, message, ...details } = error;
1511
- return new AmbasadaError(code2, typeof message === "string" ? message : code2, status, details);
1512
- }
1513
- const code = status === 401 ? "UNAUTHORIZED" : status === 404 ? "NOT_FOUND" : status >= 500 ? "INTERNAL" : "INVALID_REQUEST";
1514
- return new AmbasadaError(code, fallback, status);
1515
- }
1516
-
1517
- // ../sdk/src/apps.ts
1518
- var APP_SIGNATURE_HEADER2 = "x-ambasada-signature";
1519
- var APP_TIMESTAMP_HEADER2 = "x-ambasada-timestamp";
1520
- var signAppWebhook2 = (secret, timestamp, body) => signAppWebhook(secret, timestamp, body);
1521
- var appWebhookHeaders2 = (secret, body, timestamp) => appWebhookHeaders(secret, body, timestamp);
1522
- var AmbasadaApps = class {
1523
- constructor(config) {
1524
- this.config = config;
1525
- }
1526
- config;
1527
- /** True when the app has its secret (hosted on ambasada.dev, `ambasada dev`, or set by hand). */
1528
- get configured() {
1529
- return !!this.config().appSecret;
1530
- }
1531
- /**
1532
- * Verifies a webhook from the hub (signature, 5-minute window) and returns the event, or throws
1533
- * `WebhookInvalidError` (answer it with `error.toResponse()`, a 400). Read nothing from the request before.
1534
- */
1535
- async verifyWebhook(input, secret, options = {}) {
1536
- const key = secret ?? this.config().appSecret;
1537
- if (!key) throw new WebhookInvalidError("bad_secret", "no app secret: set AMBASADA_APP_SECRET (injected on ambasada.dev, from the hub dashboard elsewhere) or pass it");
1538
- const isRequest = typeof input.text === "function" && input.headers instanceof Headers;
1539
- const headers = isRequest ? input.headers : input.headers;
1540
- const raw = isRequest ? await input.text() : input.body;
1541
- const body = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
1542
- try {
1543
- return await verifyAppWebhook(key, headers, body, options);
1544
- } catch (error) {
1545
- if (error instanceof AppWebhookError) throw new WebhookInvalidError(error.reason, error.message);
1546
- throw error;
1547
- }
1548
- }
1549
- /**
1550
- * Sends a push to the people behind `tokens` (or `users`, for Verified apps with `identity`) through the hub.
1551
- * Checks the limits first (title ≤ 40, body ≤ 140, id ≤ 128, ≤ 100 recipients, `url` on your origin) and
1552
- * throws `AmbasadaError("INVALID_REQUEST")` before sending anything. Tokens in `invalid` are gone: forget them.
1553
- */
1554
- async notify(input) {
1555
- const c = this.config();
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)");
1557
- let body;
1558
- try {
1559
- body = appNotifyBody(input, c.origin);
1560
- } catch (error) {
1561
- throw new AmbasadaError("INVALID_REQUEST", error.message, 400);
1562
- }
1563
- const url = `${(c.hubUrl ?? DEFAULT_HUB_URL).replace(/\/+$/, "")}${APP_NOTIFY_PATH}`;
1564
- const doFetch = c.fetch ?? ((i, init) => fetch(i, init));
1565
- let res;
1566
- try {
1567
- res = await doFetch(url, {
1568
- method: "POST",
1569
- headers: { authorization: `Bearer ${c.appSecret}`, "content-type": "application/json", accept: "application/json" },
1570
- body: JSON.stringify(body),
1571
- signal: AbortSignal.timeout(15e3)
1572
- });
1573
- } catch (error) {
1574
- throw new AmbasadaError("NETWORK", `POST ${url}: ${error.message}`, 0);
1575
- }
1576
- const json = await res.json().catch(() => null);
1577
- if (!res.ok) {
1578
- const err = json?.error;
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";
1580
- const message = typeof err === "string" ? err : err?.message ?? `the hub answered ${res.status}`;
1581
- throw new AmbasadaError(code, `notify: ${message}`, res.status);
1582
- }
1583
- return appNotifyResult(json);
1584
- }
1585
- };
1586
-
1587
- // ../sdk/src/auth.ts
1588
- var AUTH_PATHS = { login: "/api/auth/login", callback: "/api/auth/callback", logout: "/api/auth/logout", shell: "/api/auth/shell" };
1589
- var SESSION_TTL_S = 7 * 24 * 3600;
1590
- var STATE_TTL_S = 600;
1591
- var SESSION_COOKIE = "ambasada_session";
1592
- var STATE_COOKIE = "ambasada_auth";
1593
- var enc = new TextEncoder();
1594
- function b64url(bytes) {
1595
- let s = "";
1596
- for (const b of bytes) s += String.fromCharCode(b);
1597
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1598
- }
1599
- function fromB64url(text) {
1600
- const s = atob(text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4));
1601
- const out = new Uint8Array(s.length);
1602
- for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
1603
- return out;
1604
- }
1605
- var random = (n) => b64url(crypto.getRandomValues(new Uint8Array(n)));
1606
- async function pkce() {
1607
- const verifier = random(32);
1608
- return { verifier, challenge: await pkceChallenge(verifier) };
1609
- }
1610
- async function pkceChallenge(verifier) {
1611
- return b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(verifier))));
1612
- }
1613
- var keys2 = /* @__PURE__ */ new Map();
1614
- function cookieKey(secret, purpose) {
1615
- const id = `${purpose}
1616
- ${secret}`;
1617
- let key = keys2.get(id);
1618
- if (!key) {
1619
- key = crypto.subtle.importKey("raw", enc.encode(secret), "HKDF", false, ["deriveKey"]).then(
1620
- (ikm) => crypto.subtle.deriveKey(
1621
- { name: "HKDF", hash: "SHA-256", salt: enc.encode("ambasada auth cookie v1"), info: enc.encode(purpose) },
1622
- ikm,
1623
- { name: "HMAC", hash: "SHA-256", length: 256 },
1624
- false,
1625
- ["sign", "verify"]
1626
- )
1627
- );
1628
- keys2.set(id, key);
1629
- }
1630
- return key;
1631
- }
1632
- async function signCookie(secret, purpose, payload) {
1633
- const body = b64url(enc.encode(JSON.stringify(payload)));
1634
- const sig = await crypto.subtle.sign("HMAC", await cookieKey(secret, purpose), enc.encode(body));
1635
- return `${body}.${b64url(new Uint8Array(sig))}`;
1636
- }
1637
- async function verifyCookie(secret, purpose, value, now = Date.now()) {
1638
- if (!value) return null;
1639
- const [body, sig, extra] = value.split(".");
1640
- if (!body || !sig || extra !== void 0) return null;
1641
- try {
1642
- const ok = await crypto.subtle.verify("HMAC", await cookieKey(secret, purpose), fromB64url(sig), enc.encode(body));
1643
- if (!ok) return null;
1644
- const payload = JSON.parse(new TextDecoder().decode(fromB64url(body)));
1645
- return typeof payload.exp === "number" && payload.exp * 1e3 > now ? payload : null;
1646
- } catch {
1647
- return null;
1648
- }
1649
- }
1650
- function readCookie(req, name) {
1651
- for (const part of (req.headers.get("cookie") ?? "").split(";")) {
1652
- const i = part.indexOf("=");
1653
- if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
1654
- }
1655
- return null;
1656
- }
1657
- var cookieName = (base, secure) => secure ? `__Host-${base}` : base;
1658
- var readOurCookie = (req, base) => readCookie(req, cookieName(base, true)) ?? readCookie(req, base);
1659
- function setCookie(base, value, secure, maxAge) {
1660
- return `${cookieName(base, secure)}=${value}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`;
1661
- }
1662
- function safeReturnTo(value) {
1663
- if (!value || !value.startsWith("/") || value.startsWith("//") || value.startsWith("/\\") || /[\u0000-\u001f\u007f]/.test(value)) return "/";
1664
- return value;
1665
- }
1666
- function redirect(location, cookies = []) {
1667
- const headers = new Headers({ location, "cache-control": "no-store" });
1668
- for (const c of cookies) headers.append("set-cookie", c);
1669
- return new Response(null, { status: 303, headers });
1670
- }
1671
- var JWKS_TTL_MS = 36e5;
1672
- var LEEWAY_S = 30;
1673
- var jwksCache = /* @__PURE__ */ new Map();
1674
- async function jwks(issuer, doFetch, refresh = false) {
1675
- const cached = jwksCache.get(issuer);
1676
- if (cached && !refresh && Date.now() - cached.at < JWKS_TTL_MS) return cached.keys;
1677
- if (cached && refresh && Date.now() - cached.at < 6e4) return cached.keys;
1678
- const res = await doFetch(`${issuer}/.well-known/jwks.json`, { headers: { accept: "application/json" } });
1679
- if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada ID keys (${res.status})`, 502);
1680
- const body = await res.json();
1681
- const keys3 = (body.keys ?? []).filter((k) => k.kty === "RSA" && k.n && k.e);
1682
- jwksCache.set(issuer, { at: Date.now(), keys: keys3 });
1683
- return keys3;
1684
- }
1685
- var shellRefused = (message) => new AmbasadaError("UNAUTHORIZED", `shell token refused: ${message}`, 401);
1686
- var AmbasadaAuth = class {
1687
- constructor(config) {
1688
- this.config = config;
1689
- }
1690
- config;
1691
- /** True when the product has credentials (hosted on ambasada.dev, or `ambasada dev`). */
1692
- get configured() {
1693
- const c = this.config();
1694
- return !!(c.clientId && c.clientSecret && c.issuer);
1695
- }
1696
- settings(req) {
1697
- const c = this.config();
1698
- if (!c.clientId || !c.clientSecret || !c.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)");
1700
- }
1701
- const origin = (c.publicUrl ? new URL(c.publicUrl) : new URL(req.url)).origin;
1702
- return {
1703
- clientId: c.clientId,
1704
- clientSecret: c.clientSecret,
1705
- issuer: c.issuer.replace(/\/+$/, ""),
1706
- apiUrl: c.apiUrl.replace(/\/+$/, ""),
1707
- origin,
1708
- secure: origin.startsWith("https:"),
1709
- redirectUri: `${origin}${AUTH_PATHS.callback}`,
1710
- // Called as a plain function: workerd refuses `fetch` invoked as a method of another object.
1711
- fetch: c.fetch ?? ((input, init) => fetch(input, init))
1712
- };
1713
- }
1714
- /** Redirects to Ambasada ID (consent screen) with PKCE; a short-lived signed cookie holds the state. */
1715
- async login(req, options = {}) {
1716
- const s = this.settings(req);
1717
- const { verifier, challenge } = await pkce();
1718
- const state = random(16);
1719
- const returnTo = safeReturnTo(options.returnTo ?? new URL(req.url).searchParams.get("returnTo"));
1720
- const url = new URL(`${s.issuer}/oauth/authorize`);
1721
- url.search = new URLSearchParams({
1722
- response_type: "code",
1723
- client_id: s.clientId,
1724
- redirect_uri: s.redirectUri,
1725
- scope: options.email ? "openid profile email" : "openid profile",
1726
- state,
1727
- code_challenge: challenge,
1728
- code_challenge_method: "S256"
1729
- }).toString();
1730
- const value = await signCookie(s.clientSecret, "state", { state, verifier, returnTo, exp: Math.floor(Date.now() / 1e3) + STATE_TTL_S });
1731
- return redirect(url.toString(), [setCookie(STATE_COOKIE, value, s.secure, STATE_TTL_S)]);
1732
- }
1733
- /**
1734
- * Ambasada ID sends the person back here: checks the state, exchanges the code (with the PKCE
1735
- * verifier and the client secret), reads their profile from the Ambasada API, sets the session cookie
1736
- * and redirects to `returnTo`. A refused consent goes back to `returnTo` signed out.
1737
- */
1738
- async callback(req) {
1739
- const s = this.settings(req);
1740
- const params = new URL(req.url).searchParams;
1741
- const clear = setCookie(STATE_COOKIE, "", s.secure, 0);
1742
- const saved = await verifyCookie(s.clientSecret, "state", readOurCookie(req, STATE_COOKIE));
1743
- if (!saved || !params.get("state") || params.get("state") !== saved.state) {
1744
- throw new AmbasadaError("UNAUTHORIZED", "sign-in expired or was started elsewhere: try again", 400);
1745
- }
1746
- const returnTo = safeReturnTo(saved.returnTo);
1747
- if (params.get("error")) return redirect(returnTo, [clear]);
1748
- const code = params.get("code");
1749
- if (!code) throw new AmbasadaError("INVALID_REQUEST", "no authorization code", 400);
1750
- const token = await s.fetch(`${s.issuer}/oauth/token`, {
1751
- method: "POST",
1752
- headers: {
1753
- "content-type": "application/x-www-form-urlencoded",
1754
- accept: "application/json",
1755
- authorization: `Basic ${btoa(`${encodeURIComponent(s.clientId)}:${encodeURIComponent(s.clientSecret)}`)}`
1756
- },
1757
- body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: s.redirectUri, code_verifier: saved.verifier })
1758
- });
1759
- const tokens = await token.json().catch(() => ({}));
1760
- if (!token.ok || !tokens.access_token) throw new AmbasadaError("UNAUTHORIZED", `Ambasada ID refused the sign-in (${tokens.error ?? token.status})`, 401);
1761
- const res = await s.fetch(`${s.apiUrl}/v1/auth/profile`, { headers: { authorization: `Bearer ${tokens.access_token}`, accept: "application/json" } });
1762
- if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada profile (${res.status})`, 502);
1763
- const user = await res.json();
1764
- const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
1765
- const session = await signCookie(s.clientSecret, "session", { user, exp });
1766
- return redirect(returnTo, [clear, setCookie(SESSION_COOKIE, session, s.secure, SESSION_TTL_S)]);
1767
- }
1768
- /** Whoever is signed in on this request, or null (also null when sign-in is not set up). */
1769
- async user(req) {
1770
- const secret = this.config().clientSecret;
1771
- if (!this.configured || !secret) return null;
1772
- return (await verifyCookie(secret, "session", readOurCookie(req, SESSION_COOKIE)))?.user ?? null;
1773
- }
1774
- /**
1775
- * Verifies a shell token (Clerk JWT template `ambasada-miniapp`, RS256, 60 s) against
1776
- * `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json` (cached 1 h): signature, expiry, issuer, and that its `aud`
1777
- * is this app (its id or origin). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`.
1778
- */
1779
- async verifyShellToken(token, options = {}) {
1780
- const c = this.config();
1781
- if (!c.issuer) throw new AmbasadaError("CONFIG", "silent sign-in needs AMBASADA_AUTH_ISSUER (injected on ambasada.dev and by `ambasada dev`)");
1782
- const trusted = [c.issuer, c.hubUrl].filter((v) => !!v).map((v) => v.replace(/\/+$/, ""));
1783
- const audiences = [options.audience ?? c.audiences ?? []].flat().filter(Boolean).map((a) => a.replace(/\/+$/, ""));
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 }");
1785
- const doFetch = c.fetch ?? ((input, init) => fetch(input, init));
1786
- const parts = typeof token === "string" ? token.split(".") : [];
1787
- if (parts.length !== 3) throw shellRefused("not a JWT");
1788
- let header3;
1789
- let claims;
1790
- try {
1791
- header3 = JSON.parse(new TextDecoder().decode(fromB64url(parts[0])));
1792
- claims = JSON.parse(new TextDecoder().decode(fromB64url(parts[1])));
1793
- } catch {
1794
- throw shellRefused("not a JWT");
1795
- }
1796
- if (header3.alg !== "RS256") throw shellRefused(`alg ${String(header3.alg)} (only RS256)`);
1797
- const iss = typeof claims.iss === "string" ? claims.iss.replace(/\/+$/, "") : null;
1798
- if (iss && !trusted.includes(iss)) throw shellRefused(`issuer ${claims.iss}`);
1799
- const issuer = iss ?? trusted[0];
1800
- let keys3 = await jwks(issuer, doFetch);
1801
- let jwk = keys3.find((k) => k.kid === header3.kid) ?? (!header3.kid && keys3.length === 1 ? keys3[0] : void 0);
1802
- if (!jwk) {
1803
- keys3 = await jwks(issuer, doFetch, true);
1804
- jwk = keys3.find((k) => k.kid === header3.kid);
1805
- }
1806
- if (!jwk) throw shellRefused(`unknown key ${String(header3.kid)}`);
1807
- const key = await crypto.subtle.importKey("jwk", { kty: "RSA", n: jwk.n, e: jwk.e, alg: "RS256", ext: true }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
1808
- const ok = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, fromB64url(parts[2]), enc.encode(`${parts[0]}.${parts[1]}`));
1809
- if (!ok) throw shellRefused("bad signature");
1810
- const now = Math.floor((options.now ?? Date.now()) / 1e3);
1811
- if (typeof claims.exp !== "number" || claims.exp + LEEWAY_S < now) throw shellRefused("expired");
1812
- if (typeof claims.nbf === "number" && claims.nbf - LEEWAY_S > now) throw shellRefused("not valid yet");
1813
- if (typeof claims.iat === "number" && claims.iat - LEEWAY_S > now) throw shellRefused("issued in the future");
1814
- const aud = [claims.aud].flat().filter((a) => typeof a === "string").map((a) => a.replace(/\/+$/, ""));
1815
- if (!aud.some((a) => audiences.includes(a))) throw shellRefused(`it is for ${aud.join(", ") || "no app"}, not this one`);
1816
- if (typeof claims.sub !== "string" || !claims.sub) throw shellRefused("no subject");
1817
- const text = (v) => typeof v === "string" && v ? v : null;
1818
- return { user_id: claims.sub, name: text(claims.name), avatar: text(claims.avatar) ?? text(claims.picture) ?? text(claims.image_url) };
1819
- }
1820
- /**
1821
- * Silent sign-in: `POST /api/auth/shell` with `{ "token": "<auth.getToken() token>" }` (or `Authorization: Bearer`).
1822
- * Verifies it, reads the full profile from the hub (falling back to what the token carries) and sets the same
1823
- * session cookie as `callback()`, so a person signed in inside the app and one signed in on the web look the same.
1824
- * Answers `{ user }`.
1825
- */
1826
- async shell(req) {
1827
- const s = this.settings(req);
1828
- if (req.method !== "POST") throw new AmbasadaError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
1829
- let token = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "")?.[1]?.trim();
1830
- if (!token) {
1831
- if (!(req.headers.get("content-type") ?? "").includes("application/json")) throw new AmbasadaError("INVALID_REQUEST", "send the token as JSON { token }", 415);
1832
- const body = await req.json().catch(() => null);
1833
- token = typeof body?.token === "string" ? body.token : void 0;
1834
- }
1835
- if (!token) throw new AmbasadaError("INVALID_REQUEST", "no shell token", 400);
1836
- const audiences = [...this.config().audiences ?? [], s.origin];
1837
- const who = await this.verifyShellToken(token, { audience: audiences });
1838
- let user = { id: who.user_id, name: who.name, avatar_url: who.avatar, builder: false, builder_since: null, member_since: null, pass: null };
1839
- const hub = this.config().hubUrl;
1840
- if (hub) {
1841
- try {
1842
- const res = await s.fetch(`${hub.replace(/\/+$/, "")}/api/apps/profile`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } });
1843
- if (res.ok) {
1844
- const profile = await res.json();
1845
- if (profile.id === who.user_id) user = { ...user, ...profile, id: who.user_id };
1846
- } else await res.body?.cancel();
1847
- } catch {
1848
- }
1849
- }
1850
- const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
1851
- const session = await signCookie(s.clientSecret, "session", { user, exp });
1852
- const headers = new Headers({ "cache-control": "no-store", "content-type": "application/json" });
1853
- headers.append("set-cookie", setCookie(SESSION_COOKIE, session, s.secure, SESSION_TTL_S));
1854
- return new Response(JSON.stringify({ user }), { status: 200, headers });
1855
- }
1856
- /** Clears the session and redirects to `returnTo` (default "/"). Signs out of this product only. */
1857
- logout(req, options = {}) {
1858
- const url = new URL(req.url);
1859
- const publicUrl = this.config().publicUrl;
1860
- const secure = (publicUrl ? new URL(publicUrl) : url).protocol === "https:";
1861
- const returnTo = safeReturnTo(options.returnTo ?? url.searchParams.get("returnTo"));
1862
- return redirect(returnTo, [setCookie(SESSION_COOKIE, "", secure, 0)]);
1863
- }
1864
- };
1865
-
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 ?? {};
1871
- }
1872
- var Ambasada = class {
1873
- options;
1874
- keyLoader;
1875
- projectPromise;
1876
- constructor(options = {}) {
1877
- this.options = options;
1878
- }
1879
- env(name) {
1880
- const v = this.options.env?.[name];
1881
- return typeof v === "string" ? v : processEnv()[name];
1882
- }
1883
- get apiUrl() {
1884
- return (this.options.apiUrl ?? this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL") ?? DEFAULT_API_URL).replace(/\/+$/, "");
1885
- }
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;
1890
- }
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);
2098
- node.rawBody = body;
2099
- return { headers: node.headers, body };
2100
- }
2101
- return { headers: node.headers, body: new Uint8Array() };
2102
- }
2103
- async function verifyCallReceipt(token, body, check) {
2104
- let claims;
2105
- try {
2106
- claims = await verifyReceipt(token, {
2107
- keys: check.keys,
2108
- seller: check.seller,
2109
- offering: check.offering,
2110
- body,
2111
- issuer: check.issuer,
2112
- now: check.now
2113
- });
2114
- } catch (error) {
2115
- if (error instanceof ReceiptError) throw new ReceiptInvalidError(error.reason, error.message);
2116
- throw error;
2117
- }
2118
- if (claims.kind !== void 0 || typeof claims.call_id !== "string" || typeof claims.body_sha256 !== "string") {
2119
- throw new ReceiptInvalidError("wrong_kind", "not a call receipt");
2120
- }
2121
- return claims;
2122
- }
2123
- async function verifyCheckoutReceiptToken(token, check) {
2124
- try {
2125
- return await verifyCheckoutReceipt(token, check);
2126
- } catch (error) {
2127
- if (error instanceof ReceiptError) throw new ReceiptInvalidError(error.reason, error.message);
2128
- throw error;
2129
- }
2130
- }
2131
- var ReplayGuard = class {
2132
- seen = /* @__PURE__ */ new Map();
2133
- /** Returns false if the call id was already used. */
2134
- claim(callId, exp) {
2135
- const now = Math.floor(Date.now() / 1e3);
2136
- if (this.seen.size > 1e3) {
2137
- for (const [id, e] of this.seen) if (e < now) this.seen.delete(id);
2138
- }
2139
- if (this.seen.has(callId)) return false;
2140
- this.seen.set(callId, exp);
2141
- return true;
2142
- }
2143
- };
2144
- function receiptHeader(headers) {
2145
- return getHeader(headers, RECEIPT_HEADER2);
2146
- }
2147
- function toBytes2(value) {
2148
- if (value instanceof Uint8Array) return value;
2149
- if (value instanceof ArrayBuffer) return new Uint8Array(value);
2150
- if (typeof value === "string") return new TextEncoder().encode(value);
2151
- return null;
2152
- }
2153
- function concat(chunks) {
2154
- const out = new Uint8Array(chunks.reduce((n, c) => n + c.byteLength, 0));
2155
- let offset = 0;
2156
- for (const c of chunks) {
2157
- out.set(c, offset);
2158
- offset += c.byteLength;
2159
- }
2160
- return out;
2161
- }
2162
-
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;
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";
2194
- function randomKey() {
2195
- return `sdk_${crypto.randomUUID()}`;
2196
- }
2197
- function num(value) {
2198
- if (value === null || value === "") return null;
2199
- const n = Number(value);
2200
- return Number.isFinite(n) ? n : null;
2201
- }
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}"`);
2205
- return [company, name];
2206
- }
2207
- var Village = class extends Ambasada {
2208
- villageOptions;
2209
- companyPromise;
2210
- replays = new ReplayGuard();
2211
- constructor(options = {}) {
2212
- super(options);
2213
- this.villageOptions = options;
2214
- }
2215
- get apiUrl() {
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(/\/+$/, "");
2219
- }
2220
- // ── identity ──────────────────────────────────────────────────────────────
2221
- /** Your company id (from options, `AMBASADA_COMPANY`, or the wallet the API key belongs to). */
2222
- company() {
2223
- const known = this.villageOptions.company ?? (this.env("AMBASADA_COMPANY") ?? this.env("VILLAGE_COMPANY"));
2224
- if (known) return Promise.resolve(known);
2225
- this.companyPromise ??= this.wallet().then(
2226
- (w) => w.account,
2227
- (error) => {
2228
- this.companyPromise = void 0;
2229
- throw error;
2230
- }
2231
- );
2232
- return this.companyPromise;
2233
- }
2234
- keySource() {
2235
- return () => this.keys();
2236
- }
2237
- // ── selling: receipts, webhooks ───────────────────────────────────────────
2238
- /**
2239
- * Verifies that a request to your endpoint was paid through the Gateway and returns the receipt
2240
- * with the parsed JSON `input`. Throws `ReceiptInvalidError` (status 402) otherwise.
2241
- * Works with a fetch `Request` (not consumed) or a Node/Express request with a raw body.
2242
- */
2243
- async verifyReceipt(req, options = {}) {
2244
- const { headers, body } = await readIncoming(req);
2245
- const token = receiptHeader(headers);
2246
- if (!token) throw new ReceiptInvalidError("missing", "missing x-village-receipt (call this endpoint through the village Gateway)");
2247
- const seller = await this.company();
2248
- const claims = await verifyCallReceipt(token, body, {
2249
- keys: this.keySource(),
2250
- seller,
2251
- offering: options.offering === void 0 ? void 0 : `${seller}/${options.offering}`
2252
- });
2253
- if (options.replayGuard !== false && !this.replays.claim(claims.call_id, claims.exp)) {
2254
- throw new ReceiptInvalidError("replayed", "receipt already used");
2255
- }
2256
- let input = {};
2257
- if (body.byteLength) {
2258
- const text = new TextDecoder().decode(body);
2259
- try {
2260
- input = JSON.parse(text);
2261
- } catch {
2262
- input = text;
2263
- }
2264
- }
2265
- return { ...claims, input };
2266
- }
2267
- /**
2268
- * Wraps a paid offering as a fetch handler: verifies the receipt (402 if missing or bad), runs
2269
- * `handler(input, call, req)` and answers JSON. A thrown `AmbasadaError` keeps its status (the
2270
- * Gateway then refunds the buyer).
2271
- *
2272
- * export default village.sell("translate", async ({ text, to }) => ({ text: await village.llm(`…`) }));
2273
- */
2274
- sell(offering3, handler) {
2275
- return async (req) => {
2276
- if (req.method !== "POST") return Response.json({ error: { code: "INVALID_REQUEST", message: "POST only" } }, { status: 405 });
2277
- let call;
2278
- try {
2279
- call = await this.verifyReceipt(req, { offering: offering3 });
2280
- } catch (error) {
2281
- if (error instanceof AmbasadaError) return error.toResponse();
2282
- throw error;
2283
- }
2284
- try {
2285
- const out = await handler(call.input, call, req);
2286
- return out instanceof Response ? out : Response.json(out);
2287
- } catch (error) {
2288
- if (error instanceof AmbasadaError) return error.toResponse();
2289
- console.error(error);
2290
- return Response.json({ error: { code: "INTERNAL", message: "handler failed" } }, { status: 500 });
2291
- }
2292
- };
2293
- }
2294
- /** Verifies a signed webhook (`checkout.paid`, `webhook.test`) and returns the event. Throws `WebhookInvalidError`. */
2295
- async verifyWebhook(req, secret) {
2296
- const { headers, body } = await readIncoming(req);
2297
- return verifyWebhookPayload(secret ?? this.villageOptions.webhookSecret ?? (this.env("AMBASADA_WEBHOOK_SECRET") ?? this.env("VILLAGE_WEBHOOK_SECRET")) ?? "", headers, body);
2298
- }
2299
- /** Verifies a checkout receipt (pay.js `village:paid` → `e.detail.receipt`, or `session.receipt`). */
2300
- async verifyCheckoutReceipt(token, options = {}) {
2301
- return verifyCheckoutReceiptToken(token, { keys: this.keySource(), seller: await this.company(), session: options.session });
2302
- }
2303
- // ── checkout (humans) ─────────────────────────────────────────────────────
2304
- /** Creates a Checkout session; redirect the person to `session.url`. Deliver on the webhook or `getCheckout(id).status === "paid"`. */
2305
- checkout(session, options = {}) {
2306
- return this.call("POST", "/v1/checkout/sessions", { body: session, headers: { "idempotency-key": options.idempotencyKey } });
2307
- }
2308
- getCheckout(id) {
2309
- return this.call("GET", `/v1/checkout/sessions/${encodeURIComponent(id)}`);
2310
- }
2311
- /** A reusable payment link for static pages (`<button data-village-pay="pl_…">` with pay.js). */
2312
- createPaymentLink(link) {
2313
- return this.call("POST", "/v1/checkout/links", { body: link });
2314
- }
2315
- /** Cancels an open session (`POST /v1/checkout/sessions/:id/expire`). */
2316
- expireCheckout(id) {
2317
- return this.call("POST", `/v1/checkout/sessions/${encodeURIComponent(id)}/expire`);
2318
- }
2319
- async listPaymentLinks() {
2320
- return (await this.call("GET", "/v1/checkout/links")).links;
2321
- }
2322
- deactivatePaymentLink(id) {
2323
- return this.call("POST", `/v1/checkout/links/${encodeURIComponent(id)}/deactivate`);
2324
- }
2325
- /** Where checkout webhooks go. Returns the signing secret (the same one until `rotateSecret`). */
2326
- setWebhook(url, options = {}) {
2327
- return this.call("PUT", "/v1/webhook", { body: { url, rotate_secret: options.rotateSecret || void 0 } });
2328
- }
2329
- /** The webhook URL, its secret and the last deliveries. */
2330
- getWebhook() {
2331
- return this.call("GET", "/v1/webhook");
2332
- }
2333
- deleteWebhook() {
2334
- return this.call("DELETE", "/v1/webhook");
2335
- }
2336
- /** Sends a `webhook.test` event now and returns the delivery. */
2337
- testWebhook() {
2338
- return this.call("POST", "/v1/webhook/test");
2339
- }
2340
- retryWebhookDelivery(id) {
2341
- return this.call("POST", `/v1/webhook/deliveries/${encodeURIComponent(id)}/retry`);
2342
- }
2343
- // ── buying (machines) ─────────────────────────────────────────────────────
2344
- /** Buys one call of `company/offering` through the Gateway. Money moves only if the seller answers 2xx with valid output. */
2345
- async buy(offering3, input, options = {}) {
2346
- const [company, name] = splitOffering(offering3);
2347
- return this.paid(`/v1/pay/${encodeURIComponent(company)}/${encodeURIComponent(name)}`, input, options);
2348
- }
2349
- /**
2350
- * Buys whatever your agent routed the need `name` to (or the best offering within `maxPrice` if it
2351
- * has not routed it yet), with fallbacks (EXCHANGE.md §4.3). Prefer this over hardcoding a supplier.
2352
- */
2353
- need(name, input, options = {}) {
2354
- return this.paid(`/v1/need/${encodeURIComponent(name)}`, input, options);
2355
- }
2356
- async paid(path, input, options) {
2357
- const depth = options.parent?.depth;
2358
- const budget = options.budget ?? options.parent?.budget;
2359
- const res = await this.send("POST", path, {
2360
- body: input ?? {},
2361
- retry: true,
2362
- signal: options.signal,
2363
- headers: {
2364
- "idempotency-key": options.idempotencyKey ?? randomKey(),
2365
- "x-village-max-price": options.maxPrice === void 0 ? void 0 : String(options.maxPrice),
2366
- "x-village-depth": depth === void 0 ? void 0 : String(depth),
2367
- "x-village-budget": budget === void 0 ? void 0 : String(budget),
2368
- "x-village-approval": options.approval,
2369
- "x-village-reason": options.reason?.slice(0, 280)
2370
- }
2371
- });
2372
- const h = res.headers;
2373
- return {
2374
- output: res.json,
2375
- callId: h.get("x-village-call-id"),
2376
- amount: num(h.get("x-village-amount")),
2377
- receipt: h.get("x-village-receipt"),
2378
- logIndex: num(h.get("x-village-log-index")),
2379
- replayed: h.get("idempotent-replayed") === "true",
2380
- offering: h.get("x-village-offering")
2381
- };
2382
- }
2383
- // ── utilities (LLMOK, paid in TON from your wallet) ───────────────────────
2384
- /**
2385
- * `village.llm("prompt")` → the answer text; `village.llm.chat(req)` → the full OpenAI-style response.
2386
- * Default model: deepseek-v4.1-flash.
2387
- */
2388
- llm = Object.assign(
2389
- async (prompt, options = {}) => {
2390
- const { system, ...rest } = options;
2391
- const messages = system ? [{ role: "system", content: system }] : [];
2392
- const res = await this.llm.chat({ ...rest, messages: [...messages, { role: "user", content: prompt }] });
2393
- return res.choices[0]?.message.content ?? "";
2394
- },
2395
- {
2396
- chat: (req) => this.call("POST", "/v1/llm/chat", { body: { ...req, model: req.model ?? DEFAULT_MODEL } })
2397
- }
2398
- );
2399
- images = {
2400
- /** Generates an image; the URL is re-hosted in the village files store. Default model: nano_banana. */
2401
- generate: (req) => this.call("POST", "/v1/images", { body: { ...req, model: req.model ?? "nano_banana" } })
2402
- };
2403
- tools = {
2404
- /** The curated LLMOK tool catalog with TON prices. */
2405
- list: async () => {
2406
- const res = await this.call("GET", "/v1/tools");
2407
- return Array.isArray(res) ? res : res.tools;
2408
- },
2409
- /** Runs one tool; debits your wallet only on success. Replays with the same idempotency key are free. */
2410
- call: async (id, input, options = {}) => {
2411
- const res = await this.send("POST", `/v1/tools/${encodeURIComponent(id)}`, {
2412
- body: input ?? {},
2413
- retry: true,
2414
- headers: { "idempotency-key": options.idempotencyKey ?? randomKey() }
2415
- });
2416
- const json = res.json;
2417
- return {
2418
- output: json.output,
2419
- callId: json.call_id ?? res.headers.get("x-village-call-id"),
2420
- amount: json.cost ?? num(res.headers.get("x-village-amount")),
2421
- replayed: res.headers.get("idempotent-replayed") === "true"
2422
- };
2423
- }
2424
- };
2425
- // ── listings, wallet, ledger ──────────────────────────────────────────────
2426
- listings = {
2427
- /** Creates or updates an offering (price changes are versioned). */
2428
- put: (name, def) => this.call("PUT", `/v1/offerings/${encodeURIComponent(name)}`, { body: def }),
2429
- list: async () => (await this.call("GET", "/v1/offerings")).offerings
2430
- };
2431
- wallet() {
2432
- return this.call("GET", "/v1/wallet");
2433
- }
2434
- ledger(since = 0, limit = 100) {
2435
- return this.call("GET", "/v1/ledger", { query: { since, limit } });
2436
- }
2437
- transfer(to, amount, options = {}) {
2438
- return this.call("POST", "/v1/transfer", {
2439
- body: { to, amount, reason: options.reason, ref: options.ref },
2440
- retry: true,
2441
- headers: { "idempotency-key": options.idempotencyKey ?? randomKey() }
2442
- });
2443
- }
2444
- // ── market, agent (CLI, scripts) ──────────────────────────────────────────
2445
- market = {
2446
- search: async (query) => {
2447
- const res = await this.call("GET", "/v1/market/search", { query: { q: query } });
2448
- return res.offerings ?? res.results ?? [];
2449
- }
2450
- };
2451
- agent = {
2452
- /** Replaces the agent's prompt (`agent.md`) and settings (`settings.json`). */
2453
- config: (config) => this.call("POST", "/v1/agent/config", { body: config }),
2454
- /** A message the agent answers on its next wake. */
2455
- steer: (text) => this.call("POST", "/v1/agent/steer", { body: { text } }),
2456
- transcript: (since = 0, limit = 200) => this.call("GET", "/v1/agent/transcript", { query: { since, limit } }),
2457
- state: () => this.call("GET", "/v1/agent/state")
2458
- };
2459
- };
2460
- function createVillage(options = {}) {
2461
- return new Village(options);
2462
- }
2463
-
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 }`);
2468
- return config;
2469
- }
2470
- function offering(def) {
2471
- if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(def.name)) throw new Error(`offering name "${def.name}" must be a slug (a-z, 0-9, _, -)`);
2472
- if (!(def.price >= 0.1 && def.price <= 500)) throw new Error(`offering ${def.name}: price must be between 0.1 and 500 TON`);
2473
- return def;
2474
- }
2475
-
2476
- // ../village/src/index.ts
2477
- var village = new 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;
1
+ // src/index.ts
2
+ import * as V from "@ambasada/village";
3
+ export * from "./platform.js";
4
+ import { default as default2 } from "./platform.js";
5
+ var village2 = V.village;
6
+ var createVillage2 = V.createVillage;
7
+ var Village2 = V.Village;
8
+ var VillageError2 = V.VillageError;
9
+ var isVillageError2 = V.isVillageError;
10
+ var ReceiptInvalidError2 = V.ReceiptInvalidError;
11
+ var DEFAULT_MODEL2 = V.DEFAULT_MODEL;
12
+ var verifyWebhookPayload2 = V.verifyWebhookPayload;
13
+ var webhookHeaders2 = V.webhookHeaders;
14
+ var generateWebhookSecret2 = V.generateWebhookSecret;
15
+ var WEBHOOK_HEADERS2 = V.WEBHOOK_HEADERS;
16
+ var RECEIPT_HEADER2 = V.RECEIPT_HEADER;
17
+ var readIncoming2 = V.readIncoming;
18
+ var verifyCallReceipt2 = V.verifyCallReceipt;
19
+ var verifyCheckoutReceiptToken2 = V.verifyCheckoutReceiptToken;
20
+ var defineCompany2 = V.defineCompany;
21
+ var offering2 = V.offering;
22
+ var DEFAULT_VILLAGE_API_URL2 = V.DEFAULT_VILLAGE_API_URL;
2498
23
  export {
2499
- APP_SIGNATURE_HEADER2 as APP_SIGNATURE_HEADER,
2500
- APP_TIMESTAMP_HEADER2 as APP_TIMESTAMP_HEADER,
2501
- AUTH_PATHS,
2502
- Ambasada,
2503
- AmbasadaApps,
2504
- AmbasadaAuth,
2505
- AmbasadaError,
2506
- DEFAULT_API_URL,
2507
24
  DEFAULT_MODEL2 as DEFAULT_MODEL,
2508
25
  DEFAULT_VILLAGE_API_URL2 as DEFAULT_VILLAGE_API_URL,
2509
- ERROR_STATUS2 as ERROR_STATUS,
2510
- JWKS_TTL_MS,
2511
- RECEIPT_HEADER3 as RECEIPT_HEADER,
26
+ RECEIPT_HEADER2 as RECEIPT_HEADER,
2512
27
  ReceiptInvalidError2 as ReceiptInvalidError,
2513
- SESSION_TTL_S,
2514
28
  Village2 as Village,
2515
- VillageError3 as VillageError,
2516
- WEBHOOK_HEADERS3 as WEBHOOK_HEADERS,
2517
- WebhookInvalidError,
2518
- ambasada,
2519
- appWebhookHeaders2 as appWebhookHeaders,
2520
- createAmbasada,
29
+ VillageError2 as VillageError,
30
+ WEBHOOK_HEADERS2 as WEBHOOK_HEADERS,
2521
31
  createVillage2 as createVillage,
2522
- platform_default as default,
2523
- defineApp,
32
+ default2 as default,
2524
33
  defineCompany2 as defineCompany,
2525
- errorFromResponse,
2526
- generateWebhookSecret3 as generateWebhookSecret,
2527
- getHeader,
2528
- isAmbasadaError,
34
+ generateWebhookSecret2 as generateWebhookSecret,
2529
35
  isVillageError2 as isVillageError,
2530
36
  offering2 as offering,
2531
37
  readIncoming2 as readIncoming,
2532
- signAppWebhook2 as signAppWebhook,
2533
38
  verifyCallReceipt2 as verifyCallReceipt,
2534
39
  verifyCheckoutReceiptToken2 as verifyCheckoutReceiptToken,
2535
40
  verifyWebhookPayload2 as verifyWebhookPayload,
2536
41
  village2 as village,
2537
- webhookHeaders3 as webhookHeaders
42
+ webhookHeaders2 as webhookHeaders
2538
43
  };