@ambasada/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,795 @@
1
+ // src/errors.ts
2
+ var ERROR_STATUS = {
3
+ // EXCHANGE.md §5 vocabulary
4
+ INSUFFICIENT_FUNDS: 402,
5
+ OFFLINE: 503,
6
+ TIMEOUT: 504,
7
+ SCHEMA_INVALID: 502,
8
+ CAP_BUYER_HOURLY: 429,
9
+ CAP_SELLER_RATE: 429,
10
+ DEAL_EXHAUSTED: 409,
11
+ DEPTH_EXCEEDED: 422,
12
+ BUDGET_EXCEEDED: 402,
13
+ // Gateway and bank
14
+ UPSTREAM_ERROR: 502,
15
+ PRICE_ABOVE_MAX: 409,
16
+ SELF_PURCHASE: 403,
17
+ INVALID_REQUEST: 400,
18
+ UNAUTHORIZED: 401,
19
+ NOT_FOUND: 404,
20
+ CONFLICT: 409,
21
+ IDEMPOTENCY_CONFLICT: 422,
22
+ IDEMPOTENCY_IN_PROGRESS: 409,
23
+ HOLD_NOT_ACTIVE: 409,
24
+ INTERNAL: 500,
25
+ // Spending limits and approvals (checkout stream)
26
+ CAP_PER_PAYMENT: 403,
27
+ CAP_BUYER_DAILY: 429,
28
+ CAP_PAYEE_DAILY: 429,
29
+ CAP_VISITOR_DAILY: 429,
30
+ APPROVAL_REQUIRED: 403,
31
+ APPROVAL_INVALID: 403,
32
+ APPROVALS_PENDING: 429,
33
+ // LLMOK mapping (utilities stream, docs/LLMOK.md)
34
+ PROVIDER_BUDGET: 503,
35
+ PROVIDER_DENIED: 502,
36
+ // Clock windows (agent stream): deploys / steering refused while frozen
37
+ FROZEN: 403,
38
+ // Exchange stream: an agent sent too many inbox messages
39
+ CAP_INBOX_RATE: 429,
40
+ // SDK-side
41
+ NETWORK: 0,
42
+ CONFIG: 0,
43
+ RECEIPT_INVALID: 402,
44
+ WEBHOOK_INVALID: 400
45
+ };
46
+ var VillageError = class extends Error {
47
+ code;
48
+ /** HTTP status (0 when the request never got an answer). */
49
+ status;
50
+ details;
51
+ constructor(code, message, status, details = {}) {
52
+ super(message);
53
+ this.name = "VillageError";
54
+ this.code = code;
55
+ this.status = status ?? ERROR_STATUS[code] ?? 500;
56
+ this.details = details;
57
+ }
58
+ get callId() {
59
+ return this.details.call_id;
60
+ }
61
+ get refunded() {
62
+ return this.details.refunded === true;
63
+ }
64
+ get approvalId() {
65
+ return this.details.approval_id;
66
+ }
67
+ /** The wire shape, e.g. to pass an error on from your own endpoint. */
68
+ toJSON() {
69
+ return { error: { code: this.code, message: this.message, ...this.details } };
70
+ }
71
+ /** A `Response` with the wire shape and status (402 for receipt problems). */
72
+ toResponse() {
73
+ return Response.json(this.toJSON(), { status: this.status || 500 });
74
+ }
75
+ };
76
+ var ReceiptInvalidError = class extends VillageError {
77
+ reason;
78
+ constructor(reason, message) {
79
+ super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
80
+ this.name = "ReceiptInvalidError";
81
+ this.reason = reason;
82
+ }
83
+ };
84
+ var WebhookInvalidError = class extends VillageError {
85
+ reason;
86
+ constructor(reason, message) {
87
+ super("WEBHOOK_INVALID", message, 400, { reason });
88
+ this.name = "WebhookInvalidError";
89
+ this.reason = reason;
90
+ }
91
+ };
92
+ function isVillageError(value, code) {
93
+ return value instanceof VillageError && (code === void 0 || value.code === code);
94
+ }
95
+ function errorFromResponse(status, body, fallback) {
96
+ const error = typeof body === "object" && body !== null ? body.error : void 0;
97
+ if (typeof error === "object" && error !== null && typeof error.code === "string") {
98
+ const { code: code2, message, ...details } = error;
99
+ return new VillageError(code2, typeof message === "string" ? message : code2, status, details);
100
+ }
101
+ const code = status === 401 ? "UNAUTHORIZED" : status === 404 ? "NOT_FOUND" : status >= 500 ? "INTERNAL" : "INVALID_REQUEST";
102
+ return new VillageError(code, fallback, status);
103
+ }
104
+
105
+ // ../protocol/src/encoding.ts
106
+ var encoder = new TextEncoder();
107
+ var decoder = new TextDecoder();
108
+ function utf8(text) {
109
+ return encoder.encode(text);
110
+ }
111
+ function fromUtf8(bytes) {
112
+ return decoder.decode(bytes);
113
+ }
114
+ function fromBase64url(text) {
115
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4);
116
+ const binary = atob(padded);
117
+ const bytes = new Uint8Array(binary.length);
118
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
119
+ return bytes;
120
+ }
121
+ function toBytes(data) {
122
+ if (typeof data === "string") return utf8(data);
123
+ if (data instanceof Uint8Array) return new Uint8Array(data);
124
+ return new Uint8Array(data);
125
+ }
126
+ async function sha256Hex(data) {
127
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", toBytes(data)));
128
+ let hex = "";
129
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
130
+ return hex;
131
+ }
132
+
133
+ // ../protocol/src/receipt.ts
134
+ var RECEIPT_HEADER = "x-village-receipt";
135
+ var DEFAULT_ISSUER = "village";
136
+ var ReceiptError = class extends Error {
137
+ constructor(reason, message) {
138
+ super(message);
139
+ this.reason = reason;
140
+ this.name = "ReceiptError";
141
+ }
142
+ reason;
143
+ };
144
+ var ED25519 = { name: "Ed25519" };
145
+ async function verifyReceipt(token, options) {
146
+ const claims = await verifySigned(token, options);
147
+ if (claims.kind !== void 0 || typeof claims.call_id !== "string" || typeof claims.body_sha256 !== "string") {
148
+ throw new ReceiptError("wrong_kind", "not a call receipt");
149
+ }
150
+ if (options.seller !== void 0 && claims.seller !== options.seller) {
151
+ throw new ReceiptError("wrong_seller", `receipt is for seller ${claims.seller}`);
152
+ }
153
+ if (options.offering !== void 0 && claims.offering !== options.offering) {
154
+ throw new ReceiptError("wrong_offering", `receipt is for offering ${claims.offering}`);
155
+ }
156
+ if (options.body !== void 0 && await sha256Hex(options.body) !== claims.body_sha256) {
157
+ throw new ReceiptError("body_mismatch", "request body does not match the receipt");
158
+ }
159
+ return claims;
160
+ }
161
+ async function verifyCheckoutReceipt(token, options) {
162
+ const claims = await verifySigned(token, options);
163
+ if (claims.kind !== "checkout" || typeof claims.session !== "string") throw new ReceiptError("wrong_kind", "not a checkout receipt");
164
+ if (options.seller !== void 0 && claims.seller !== options.seller) {
165
+ throw new ReceiptError("wrong_seller", `receipt is for seller ${claims.seller}`);
166
+ }
167
+ if (options.session !== void 0 && claims.session !== options.session) {
168
+ throw new ReceiptError("wrong_session", `receipt is for checkout ${claims.session}`);
169
+ }
170
+ return claims;
171
+ }
172
+ async function verifySigned(token, options) {
173
+ if (!token) throw new ReceiptError("missing", `missing ${RECEIPT_HEADER}`);
174
+ const parts = token.split(".");
175
+ if (parts.length !== 3) throw new ReceiptError("malformed", "receipt is not a JWT");
176
+ const [headerPart, payloadPart, signaturePart] = parts;
177
+ let header2;
178
+ let claims;
179
+ try {
180
+ header2 = JSON.parse(fromUtf8(fromBase64url(headerPart)));
181
+ claims = JSON.parse(fromUtf8(fromBase64url(payloadPart)));
182
+ } catch {
183
+ throw new ReceiptError("malformed", "receipt is not valid base64url JSON");
184
+ }
185
+ if (header2.alg !== "EdDSA") throw new ReceiptError("malformed", `unsupported alg ${header2.alg}`);
186
+ if (typeof claims !== "object" || claims === null) throw new ReceiptError("malformed", "receipt claims are not an object");
187
+ const keys = typeof options.keys === "function" ? await options.keys() : options.keys;
188
+ const jwk = keys.find((k) => (k.kid ?? k.x.slice(0, 16)) === header2.kid) ?? (keys.length === 1 && !header2.kid ? keys[0] : void 0);
189
+ if (!jwk) throw new ReceiptError("unknown_key", `no village key with kid ${header2.kid}`);
190
+ const key = await crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, ED25519, false, ["verify"]);
191
+ const ok = await crypto.subtle.verify(ED25519, key, fromBase64url(signaturePart), utf8(`${headerPart}.${payloadPart}`));
192
+ if (!ok) throw new ReceiptError("bad_signature", "receipt signature is invalid");
193
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
194
+ const skew = options.clockSkewSeconds ?? 30;
195
+ if (typeof claims.exp !== "number" || claims.exp + skew < now) throw new ReceiptError("expired", "receipt expired");
196
+ if (typeof claims.iat === "number" && claims.iat - skew > now) throw new ReceiptError("not_yet_valid", "receipt issued in the future");
197
+ if (claims.iss !== (options.issuer ?? DEFAULT_ISSUER)) throw new ReceiptError("wrong_issuer", `unexpected issuer ${String(claims.iss)}`);
198
+ return claims;
199
+ }
200
+
201
+ // ../protocol/src/webhook.ts
202
+ var WEBHOOK_HEADERS = {
203
+ id: "webhook-id",
204
+ timestamp: "webhook-timestamp",
205
+ signature: "webhook-signature"
206
+ };
207
+ var SECRET_PREFIX = "whsec_";
208
+ var encoder2 = new TextEncoder();
209
+ var WebhookError = class extends Error {
210
+ constructor(reason, message) {
211
+ super(message);
212
+ this.reason = reason;
213
+ this.name = "WebhookError";
214
+ }
215
+ reason;
216
+ };
217
+ function generateWebhookSecret() {
218
+ const bytes = new Uint8Array(24);
219
+ crypto.getRandomValues(bytes);
220
+ return SECRET_PREFIX + toStdBase64(bytes);
221
+ }
222
+ async function hmacKey(secret) {
223
+ const raw = secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret;
224
+ let bytes;
225
+ try {
226
+ bytes = fromStdBase64(raw);
227
+ } catch {
228
+ throw new WebhookError("bad_secret", "webhook secret is not whsec_<base64>");
229
+ }
230
+ return crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
231
+ }
232
+ async function signWebhook(secret, id, timestamp, body) {
233
+ const mac = await crypto.subtle.sign("HMAC", await hmacKey(secret), encoder2.encode(`${id}.${timestamp}.${body}`));
234
+ return `v1,${toStdBase64(new Uint8Array(mac))}`;
235
+ }
236
+ async function webhookHeaders(secret, id, timestamp, body) {
237
+ return {
238
+ [WEBHOOK_HEADERS.id]: id,
239
+ [WEBHOOK_HEADERS.timestamp]: String(timestamp),
240
+ [WEBHOOK_HEADERS.signature]: await signWebhook(secret, id, timestamp, body)
241
+ };
242
+ }
243
+ function header(headers, name) {
244
+ if (headers instanceof Headers) return headers.get(name);
245
+ const value = headers[name] ?? headers[name.toLowerCase()];
246
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
247
+ }
248
+ async function verifyWebhook(secret, headers, body, { toleranceSeconds = 300, now = Math.floor(Date.now() / 1e3) } = {}) {
249
+ const id = header(headers, WEBHOOK_HEADERS.id);
250
+ const ts = header(headers, WEBHOOK_HEADERS.timestamp);
251
+ const signatures = header(headers, WEBHOOK_HEADERS.signature);
252
+ if (!id || !ts || !signatures) throw new WebhookError("missing_headers", "missing webhook-id, webhook-timestamp or webhook-signature");
253
+ const timestamp = Number(ts);
254
+ if (!/^\d+$/.test(ts) || !Number.isSafeInteger(timestamp)) throw new WebhookError("bad_timestamp", "webhook-timestamp is not unix seconds");
255
+ if (timestamp < now - toleranceSeconds) throw new WebhookError("too_old", "webhook timestamp is too old");
256
+ if (timestamp > now + toleranceSeconds) throw new WebhookError("too_new", "webhook timestamp is in the future");
257
+ const text = typeof body === "string" ? body : fromUtf8(body);
258
+ const expected = (await signWebhook(secret, id, timestamp, text)).slice(3);
259
+ const match = signatures.split(" ").filter((s) => s.startsWith("v1,")).some((s) => constantTimeEqual(s.slice(3), expected));
260
+ if (!match) throw new WebhookError("bad_signature", "no matching webhook signature");
261
+ return JSON.parse(text);
262
+ }
263
+ function constantTimeEqual(a, b) {
264
+ if (a.length !== b.length) return false;
265
+ let diff = 0;
266
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
267
+ return diff === 0;
268
+ }
269
+ function toStdBase64(bytes) {
270
+ let binary = "";
271
+ for (const byte of bytes) binary += String.fromCharCode(byte);
272
+ return btoa(binary);
273
+ }
274
+ function fromStdBase64(text) {
275
+ const binary = atob(text);
276
+ const bytes = new Uint8Array(binary.length);
277
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
278
+ return bytes;
279
+ }
280
+
281
+ // src/webhook.ts
282
+ function getHeader(headers, name) {
283
+ if (typeof headers.get === "function") return headers.get(name);
284
+ const record = headers;
285
+ const key = Object.keys(record).find((k) => k.toLowerCase() === name);
286
+ const value = key === void 0 ? void 0 : record[key];
287
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
288
+ }
289
+ function plainHeaders(headers) {
290
+ const out = {};
291
+ for (const name of ["webhook-id", "webhook-timestamp", "webhook-signature"]) {
292
+ const v = getHeader(headers, name);
293
+ if (v !== null) out[name] = v;
294
+ }
295
+ return out;
296
+ }
297
+ async function verifyWebhookPayload(secret, headers, body, options = {}) {
298
+ if (!secret) throw new WebhookInvalidError("bad_secret", "no webhook secret: set AMBASADA_WEBHOOK_SECRET or pass it");
299
+ const plain = plainHeaders(headers);
300
+ let parsed;
301
+ try {
302
+ parsed = await verifyWebhook(secret, plain, body, options);
303
+ } catch (error) {
304
+ if (error instanceof WebhookError) throw new WebhookInvalidError(error.reason, error.message);
305
+ if (error instanceof SyntaxError) throw new WebhookInvalidError("bad_body", "webhook body is not JSON");
306
+ if (error instanceof DOMException || error instanceof TypeError) throw new WebhookInvalidError("bad_secret", "webhook secret is not whsec_<base64>");
307
+ throw error;
308
+ }
309
+ return { id: plain["webhook-id"], type: String(parsed.type), timestamp: String(parsed.timestamp ?? ""), data: parsed.data };
310
+ }
311
+
312
+ // src/receipt.ts
313
+ var RECEIPT_HEADER2 = "x-village-receipt";
314
+ async function readIncoming(req) {
315
+ if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
316
+ const r = req;
317
+ return { headers: r.headers, body: new Uint8Array(await r.clone().arrayBuffer()) };
318
+ }
319
+ const node = req;
320
+ const raw = toBytes2(node.rawBody) ?? toBytes2(node.body);
321
+ if (raw) return { headers: node.headers, body: raw };
322
+ if (node.body !== void 0 && node.body !== null && !(typeof node.body === "object" && Object.keys(node.body).length === 0)) {
323
+ throw new VillageError(
324
+ "CONFIG",
325
+ "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)."
326
+ );
327
+ }
328
+ if (typeof node[Symbol.asyncIterator] === "function") {
329
+ const chunks = [];
330
+ for await (const chunk of node) chunks.push(toBytes2(chunk) ?? new Uint8Array());
331
+ const body = concat(chunks);
332
+ node.rawBody = body;
333
+ return { headers: node.headers, body };
334
+ }
335
+ return { headers: node.headers, body: new Uint8Array() };
336
+ }
337
+ async function verifyCallReceipt(token, body, check) {
338
+ let claims;
339
+ try {
340
+ claims = await verifyReceipt(token, {
341
+ keys: check.keys,
342
+ seller: check.seller,
343
+ offering: check.offering,
344
+ body,
345
+ issuer: check.issuer,
346
+ now: check.now
347
+ });
348
+ } catch (error) {
349
+ if (error instanceof ReceiptError) throw new ReceiptInvalidError(error.reason, error.message);
350
+ throw error;
351
+ }
352
+ if (claims.kind !== void 0 || typeof claims.call_id !== "string" || typeof claims.body_sha256 !== "string") {
353
+ throw new ReceiptInvalidError("wrong_kind", "not a call receipt");
354
+ }
355
+ return claims;
356
+ }
357
+ async function verifyCheckoutReceiptToken(token, check) {
358
+ try {
359
+ return await verifyCheckoutReceipt(token, check);
360
+ } catch (error) {
361
+ if (error instanceof ReceiptError) throw new ReceiptInvalidError(error.reason, error.message);
362
+ throw error;
363
+ }
364
+ }
365
+ var ReplayGuard = class {
366
+ seen = /* @__PURE__ */ new Map();
367
+ /** Returns false if the call id was already used. */
368
+ claim(callId, exp) {
369
+ const now = Math.floor(Date.now() / 1e3);
370
+ if (this.seen.size > 1e3) {
371
+ for (const [id, e] of this.seen) if (e < now) this.seen.delete(id);
372
+ }
373
+ if (this.seen.has(callId)) return false;
374
+ this.seen.set(callId, exp);
375
+ return true;
376
+ }
377
+ };
378
+ function receiptHeader(headers) {
379
+ return getHeader(headers, RECEIPT_HEADER2);
380
+ }
381
+ function toBytes2(value) {
382
+ if (value instanceof Uint8Array) return value;
383
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
384
+ if (typeof value === "string") return new TextEncoder().encode(value);
385
+ return null;
386
+ }
387
+ function concat(chunks) {
388
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.byteLength, 0));
389
+ let offset = 0;
390
+ for (const c of chunks) {
391
+ out.set(c, offset);
392
+ offset += c.byteLength;
393
+ }
394
+ return out;
395
+ }
396
+
397
+ // src/client.ts
398
+ var DEFAULT_API_URL = "https://hackaton.ambasada.id";
399
+ var DEFAULT_MODEL = "deepseek-v4.1-flash";
400
+ function processEnv() {
401
+ const p = globalThis.process;
402
+ return p?.env ?? {};
403
+ }
404
+ function randomKey() {
405
+ return `sdk_${crypto.randomUUID()}`;
406
+ }
407
+ function num(value) {
408
+ if (value === null || value === "") return null;
409
+ const n = Number(value);
410
+ return Number.isFinite(n) ? n : null;
411
+ }
412
+ function splitOffering(offering2) {
413
+ const [company, name, ...rest] = offering2.split("/");
414
+ if (!company || !name || rest.length) throw new VillageError("INVALID_REQUEST", `offering must be "company/name", got "${offering2}"`);
415
+ return [company, name];
416
+ }
417
+ var Village = class {
418
+ options;
419
+ keyLoader;
420
+ companyPromise;
421
+ replays = new ReplayGuard();
422
+ constructor(options = {}) {
423
+ this.options = options;
424
+ }
425
+ env(name) {
426
+ const v = this.options.env?.[name];
427
+ return typeof v === "string" ? v : processEnv()[name];
428
+ }
429
+ get apiUrl() {
430
+ return (this.options.apiUrl ?? (this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL")) ?? DEFAULT_API_URL).replace(/\/+$/, "");
431
+ }
432
+ get apiKey() {
433
+ const key = this.options.apiKey ?? (this.env("AMBASADA_API_KEY") ?? this.env("VILLAGE_API_KEY"));
434
+ if (!key) throw new VillageError("CONFIG", "no API key: set AMBASADA_API_KEY (from the village app, or `ambasada login`) or pass { apiKey }");
435
+ return key;
436
+ }
437
+ // ── HTTP ──────────────────────────────────────────────────────────────────
438
+ async send(method, path, opts = {}) {
439
+ const url = new URL(this.apiUrl + path);
440
+ for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
441
+ const headers = { accept: "application/json" };
442
+ if (opts.auth !== false) headers.authorization = `Bearer ${this.apiKey}`;
443
+ let body = opts.raw;
444
+ if (opts.body !== void 0) {
445
+ body = JSON.stringify(opts.body);
446
+ headers["content-type"] = "application/json";
447
+ }
448
+ for (const [k, v] of Object.entries(opts.headers ?? {})) if (v !== void 0) headers[k.toLowerCase()] = v;
449
+ const doFetch = this.options.fetch ?? fetch;
450
+ const attempt = () => {
451
+ const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 45e3);
452
+ const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
453
+ return doFetch(url, { method, headers, body, signal });
454
+ };
455
+ let res;
456
+ try {
457
+ res = await attempt();
458
+ } catch (error) {
459
+ if (opts.signal?.aborted) throw error;
460
+ if (!opts.retry) throw new VillageError("NETWORK", `${method} ${url.pathname}: ${error.message}`, 0);
461
+ try {
462
+ res = await attempt();
463
+ } catch (again) {
464
+ throw new VillageError("NETWORK", `${method} ${url.pathname}: ${again.message}`, 0);
465
+ }
466
+ }
467
+ const text = await res.text();
468
+ let json = text;
469
+ if (text && (res.headers.get("content-type") ?? "").includes("json")) {
470
+ try {
471
+ json = JSON.parse(text);
472
+ } catch {
473
+ }
474
+ }
475
+ if (!res.ok) throw errorFromResponse(res.status, json, `${method} ${url.pathname} \u2192 ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
476
+ return { status: res.status, headers: res.headers, json };
477
+ }
478
+ async call(method, path, opts) {
479
+ return (await this.send(method, path, opts)).json;
480
+ }
481
+ // ── identity, keys ────────────────────────────────────────────────────────
482
+ /** Your company id (from options, `AMBASADA_COMPANY`, or the wallet the API key belongs to). */
483
+ company() {
484
+ const known = this.options.company ?? (this.env("AMBASADA_COMPANY") ?? this.env("VILLAGE_COMPANY"));
485
+ if (known) return Promise.resolve(known);
486
+ this.companyPromise ??= this.wallet().then(
487
+ (w) => w.account,
488
+ (error) => {
489
+ this.companyPromise = void 0;
490
+ throw error;
491
+ }
492
+ );
493
+ return this.companyPromise;
494
+ }
495
+ /** Village receipt keys (cached). */
496
+ keys() {
497
+ const given = this.options.keys;
498
+ if (Array.isArray(given)) return Promise.resolve(given);
499
+ if (typeof given === "function") return given();
500
+ this.keyLoader ??= cachedKeys(() => this.call("GET", "/.well-known/jwks.json", { auth: false }).then((r) => r.keys));
501
+ return this.keyLoader();
502
+ }
503
+ keySource() {
504
+ return () => this.keys();
505
+ }
506
+ // ── selling: receipts, webhooks ───────────────────────────────────────────
507
+ /**
508
+ * Verifies that a request to your endpoint was paid through the Gateway and returns the receipt
509
+ * with the parsed JSON `input`. Throws `ReceiptInvalidError` (status 402) otherwise.
510
+ * Works with a fetch `Request` (not consumed) or a Node/Express request with a raw body.
511
+ */
512
+ async verifyReceipt(req, options = {}) {
513
+ const { headers, body } = await readIncoming(req);
514
+ const token = receiptHeader(headers);
515
+ if (!token) throw new ReceiptInvalidError("missing", "missing x-village-receipt (call this endpoint through the village Gateway)");
516
+ const seller = await this.company();
517
+ const claims = await verifyCallReceipt(token, body, {
518
+ keys: this.keySource(),
519
+ seller,
520
+ offering: options.offering === void 0 ? void 0 : `${seller}/${options.offering}`
521
+ });
522
+ if (options.replayGuard !== false && !this.replays.claim(claims.call_id, claims.exp)) {
523
+ throw new ReceiptInvalidError("replayed", "receipt already used");
524
+ }
525
+ let input = {};
526
+ if (body.byteLength) {
527
+ const text = new TextDecoder().decode(body);
528
+ try {
529
+ input = JSON.parse(text);
530
+ } catch {
531
+ input = text;
532
+ }
533
+ }
534
+ return { ...claims, input };
535
+ }
536
+ /**
537
+ * Wraps a paid offering as a fetch handler: verifies the receipt (402 if missing or bad), runs
538
+ * `handler(input, call, req)` and answers JSON. A thrown `VillageError` keeps its status (the
539
+ * Gateway then refunds the buyer).
540
+ *
541
+ * export default village.sell("translate", async ({ text, to }) => ({ text: await village.llm(`…`) }));
542
+ */
543
+ sell(offering2, handler) {
544
+ return async (req) => {
545
+ if (req.method !== "POST") return Response.json({ error: { code: "INVALID_REQUEST", message: "POST only" } }, { status: 405 });
546
+ let call;
547
+ try {
548
+ call = await this.verifyReceipt(req, { offering: offering2 });
549
+ } catch (error) {
550
+ if (error instanceof VillageError) return error.toResponse();
551
+ throw error;
552
+ }
553
+ try {
554
+ const out = await handler(call.input, call, req);
555
+ return out instanceof Response ? out : Response.json(out);
556
+ } catch (error) {
557
+ if (error instanceof VillageError) return error.toResponse();
558
+ console.error(error);
559
+ return Response.json({ error: { code: "INTERNAL", message: "handler failed" } }, { status: 500 });
560
+ }
561
+ };
562
+ }
563
+ /** Verifies a signed webhook (`checkout.paid`, `webhook.test`) and returns the event. Throws `WebhookInvalidError`. */
564
+ async verifyWebhook(req, secret) {
565
+ const { headers, body } = await readIncoming(req);
566
+ return verifyWebhookPayload(secret ?? this.options.webhookSecret ?? (this.env("AMBASADA_WEBHOOK_SECRET") ?? this.env("VILLAGE_WEBHOOK_SECRET")) ?? "", headers, body);
567
+ }
568
+ /** Verifies a checkout receipt (pay.js `village:paid` → `e.detail.receipt`, or `session.receipt`). */
569
+ async verifyCheckoutReceipt(token, options = {}) {
570
+ return verifyCheckoutReceiptToken(token, { keys: this.keySource(), seller: await this.company(), session: options.session });
571
+ }
572
+ // ── checkout (humans) ─────────────────────────────────────────────────────
573
+ /** Creates a Checkout session; redirect the person to `session.url`. Deliver on the webhook or `getCheckout(id).status === "paid"`. */
574
+ checkout(session, options = {}) {
575
+ return this.call("POST", "/v1/checkout/sessions", { body: session, headers: { "idempotency-key": options.idempotencyKey } });
576
+ }
577
+ getCheckout(id) {
578
+ return this.call("GET", `/v1/checkout/sessions/${encodeURIComponent(id)}`);
579
+ }
580
+ /** A reusable payment link for static pages (`<button data-village-pay="pl_…">` with pay.js). */
581
+ createPaymentLink(link) {
582
+ return this.call("POST", "/v1/checkout/links", { body: link });
583
+ }
584
+ /** Cancels an open session (`POST /v1/checkout/sessions/:id/expire`). */
585
+ expireCheckout(id) {
586
+ return this.call("POST", `/v1/checkout/sessions/${encodeURIComponent(id)}/expire`);
587
+ }
588
+ async listPaymentLinks() {
589
+ return (await this.call("GET", "/v1/checkout/links")).links;
590
+ }
591
+ deactivatePaymentLink(id) {
592
+ return this.call("POST", `/v1/checkout/links/${encodeURIComponent(id)}/deactivate`);
593
+ }
594
+ /** Where checkout webhooks go. Returns the signing secret (the same one until `rotateSecret`). */
595
+ setWebhook(url, options = {}) {
596
+ return this.call("PUT", "/v1/webhook", { body: { url, rotate_secret: options.rotateSecret || void 0 } });
597
+ }
598
+ /** The webhook URL, its secret and the last deliveries. */
599
+ getWebhook() {
600
+ return this.call("GET", "/v1/webhook");
601
+ }
602
+ deleteWebhook() {
603
+ return this.call("DELETE", "/v1/webhook");
604
+ }
605
+ /** Sends a `webhook.test` event now and returns the delivery. */
606
+ testWebhook() {
607
+ return this.call("POST", "/v1/webhook/test");
608
+ }
609
+ retryWebhookDelivery(id) {
610
+ return this.call("POST", `/v1/webhook/deliveries/${encodeURIComponent(id)}/retry`);
611
+ }
612
+ // ── buying (machines) ─────────────────────────────────────────────────────
613
+ /** Buys one call of `company/offering` through the Gateway. Money moves only if the seller answers 2xx with valid output. */
614
+ async buy(offering2, input, options = {}) {
615
+ const [company, name] = splitOffering(offering2);
616
+ return this.paid(`/v1/pay/${encodeURIComponent(company)}/${encodeURIComponent(name)}`, input, options);
617
+ }
618
+ /**
619
+ * Buys whatever your agent routed the need `name` to (or the best offering within `maxPrice` if it
620
+ * has not routed it yet), with fallbacks (EXCHANGE.md §4.3). Prefer this over hardcoding a supplier.
621
+ */
622
+ need(name, input, options = {}) {
623
+ return this.paid(`/v1/need/${encodeURIComponent(name)}`, input, options);
624
+ }
625
+ async paid(path, input, options) {
626
+ const depth = options.parent?.depth;
627
+ const budget = options.budget ?? options.parent?.budget;
628
+ const res = await this.send("POST", path, {
629
+ body: input ?? {},
630
+ retry: true,
631
+ signal: options.signal,
632
+ headers: {
633
+ "idempotency-key": options.idempotencyKey ?? randomKey(),
634
+ "x-village-max-price": options.maxPrice === void 0 ? void 0 : String(options.maxPrice),
635
+ "x-village-depth": depth === void 0 ? void 0 : String(depth),
636
+ "x-village-budget": budget === void 0 ? void 0 : String(budget),
637
+ "x-village-approval": options.approval,
638
+ "x-village-reason": options.reason?.slice(0, 280)
639
+ }
640
+ });
641
+ const h = res.headers;
642
+ return {
643
+ output: res.json,
644
+ callId: h.get("x-village-call-id"),
645
+ amount: num(h.get("x-village-amount")),
646
+ receipt: h.get("x-village-receipt"),
647
+ logIndex: num(h.get("x-village-log-index")),
648
+ replayed: h.get("idempotent-replayed") === "true",
649
+ offering: h.get("x-village-offering")
650
+ };
651
+ }
652
+ // ── utilities (LLMOK, paid in TON from your wallet) ───────────────────────
653
+ /**
654
+ * `village.llm("prompt")` → the answer text; `village.llm.chat(req)` → the full OpenAI-style response.
655
+ * Default model: deepseek-v4.1-flash.
656
+ */
657
+ llm = Object.assign(
658
+ async (prompt, options = {}) => {
659
+ const { system, ...rest } = options;
660
+ const messages = system ? [{ role: "system", content: system }] : [];
661
+ const res = await this.llm.chat({ ...rest, messages: [...messages, { role: "user", content: prompt }] });
662
+ return res.choices[0]?.message.content ?? "";
663
+ },
664
+ {
665
+ chat: (req) => this.call("POST", "/v1/llm/chat", { body: { ...req, model: req.model ?? DEFAULT_MODEL } })
666
+ }
667
+ );
668
+ images = {
669
+ /** Generates an image; the URL is re-hosted in the village files store. Default model: nano_banana. */
670
+ generate: (req) => this.call("POST", "/v1/images", { body: { ...req, model: req.model ?? "nano_banana" } })
671
+ };
672
+ tools = {
673
+ /** The curated LLMOK tool catalog with TON prices. */
674
+ list: async () => {
675
+ const res = await this.call("GET", "/v1/tools");
676
+ return Array.isArray(res) ? res : res.tools;
677
+ },
678
+ /** Runs one tool; debits your wallet only on success. Replays with the same idempotency key are free. */
679
+ call: async (id, input, options = {}) => {
680
+ const res = await this.send("POST", `/v1/tools/${encodeURIComponent(id)}`, {
681
+ body: input ?? {},
682
+ retry: true,
683
+ headers: { "idempotency-key": options.idempotencyKey ?? randomKey() }
684
+ });
685
+ const json = res.json;
686
+ return {
687
+ output: json.output,
688
+ callId: json.call_id ?? res.headers.get("x-village-call-id"),
689
+ amount: json.cost ?? num(res.headers.get("x-village-amount")),
690
+ replayed: res.headers.get("idempotent-replayed") === "true"
691
+ };
692
+ }
693
+ };
694
+ files = {
695
+ /**
696
+ * Stores bytes, a Blob, or a URL to copy, for the event. Returns a public URL other products can fetch.
697
+ * (`POST /v1/files`: assumed route; the utilities stream serves `GET /files/:id`.)
698
+ */
699
+ put: (data, options = {}) => {
700
+ const query = { ttl_seconds: options.ttlSeconds };
701
+ if (typeof data === "string") return this.call("POST", "/v1/files", { body: { url: data, content_type: options.contentType }, query });
702
+ const contentType = options.contentType ?? (data instanceof Blob && data.type ? data.type : "application/octet-stream");
703
+ return this.call("POST", "/v1/files", { raw: data, headers: { "content-type": contentType }, query });
704
+ }
705
+ };
706
+ // ── listings, wallet, ledger ──────────────────────────────────────────────
707
+ listings = {
708
+ /** Creates or updates an offering (price changes are versioned). */
709
+ put: (name, def) => this.call("PUT", `/v1/offerings/${encodeURIComponent(name)}`, { body: def }),
710
+ list: async () => (await this.call("GET", "/v1/offerings")).offerings
711
+ };
712
+ wallet() {
713
+ return this.call("GET", "/v1/wallet");
714
+ }
715
+ ledger(since = 0, limit = 100) {
716
+ return this.call("GET", "/v1/ledger", { query: { since, limit } });
717
+ }
718
+ transfer(to, amount, options = {}) {
719
+ return this.call("POST", "/v1/transfer", {
720
+ body: { to, amount, reason: options.reason, ref: options.ref },
721
+ retry: true,
722
+ headers: { "idempotency-key": options.idempotencyKey ?? randomKey() }
723
+ });
724
+ }
725
+ // ── market, agent (CLI, scripts) ──────────────────────────────────────────
726
+ market = {
727
+ search: async (query) => {
728
+ const res = await this.call("GET", "/v1/market/search", { query: { q: query } });
729
+ return res.offerings ?? res.results ?? [];
730
+ }
731
+ };
732
+ agent = {
733
+ /** Replaces the agent's prompt (`agent.md`) and settings (`settings.json`). */
734
+ config: (config) => this.call("POST", "/v1/agent/config", { body: config }),
735
+ /** A message the agent answers on its next wake. */
736
+ steer: (text) => this.call("POST", "/v1/agent/steer", { body: { text } }),
737
+ transcript: (since = 0, limit = 200) => this.call("GET", "/v1/agent/transcript", { query: { since, limit } }),
738
+ state: () => this.call("GET", "/v1/agent/state")
739
+ };
740
+ };
741
+ function cachedKeys(load, ttlMs = 10 * 6e4) {
742
+ let cached;
743
+ return () => {
744
+ if (!cached || Date.now() - cached.at > ttlMs) {
745
+ const keys = load();
746
+ const entry = { at: Date.now(), keys };
747
+ cached = entry;
748
+ keys.catch(() => {
749
+ if (cached === entry) cached = void 0;
750
+ });
751
+ }
752
+ return cached.keys;
753
+ };
754
+ }
755
+ function createAmbasada(options = {}) {
756
+ return new Village(options);
757
+ }
758
+
759
+ // src/define.ts
760
+ function defineCompany(config) {
761
+ return config;
762
+ }
763
+ function offering(def) {
764
+ 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, _, -)`);
765
+ if (!(def.price >= 0.1 && def.price <= 500)) throw new Error(`offering ${def.name}: price must be between 0.1 and 500 TON`);
766
+ return def;
767
+ }
768
+
769
+ // src/index.ts
770
+ var village = new Village();
771
+ var index_default = village;
772
+ export {
773
+ DEFAULT_API_URL,
774
+ DEFAULT_MODEL,
775
+ ERROR_STATUS,
776
+ RECEIPT_HEADER2 as RECEIPT_HEADER,
777
+ ReceiptInvalidError,
778
+ Village,
779
+ VillageError,
780
+ WEBHOOK_HEADERS,
781
+ WebhookInvalidError,
782
+ createAmbasada,
783
+ index_default as default,
784
+ defineCompany,
785
+ errorFromResponse,
786
+ generateWebhookSecret,
787
+ isVillageError,
788
+ offering,
789
+ readIncoming,
790
+ verifyCallReceipt,
791
+ verifyCheckoutReceiptToken,
792
+ verifyWebhookPayload,
793
+ village,
794
+ webhookHeaders
795
+ };