@ambasada/village 0.8.1 → 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
@@ -4,6 +4,22 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/client.ts
8
+ import { Ambasada, AmbasadaError as AmbasadaError3 } from "@ambasada/sdk/platform";
9
+
10
+ // src/errors.ts
11
+ import { AmbasadaError, isAmbasadaError } from "@ambasada/sdk/platform";
12
+ var VillageError = AmbasadaError;
13
+ var isVillageError = isAmbasadaError;
14
+ var ReceiptInvalidError = class extends AmbasadaError {
15
+ reason;
16
+ constructor(reason, message) {
17
+ super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
18
+ this.name = "ReceiptInvalidError";
19
+ this.reason = reason;
20
+ }
21
+ };
22
+
7
23
  // ../protocol/src/index.ts
8
24
  var src_exports = {};
9
25
  __export(src_exports, {
@@ -38,7 +54,7 @@ __export(src_exports, {
38
54
  SHELL_TOKEN_TTL_S: () => SHELL_TOKEN_TTL_S,
39
55
  SLOW_ENTRY_MS: () => SLOW_ENTRY_MS,
40
56
  SYSTEM_ACCOUNT: () => SYSTEM_ACCOUNT,
41
- VillageError: () => VillageError,
57
+ VillageError: () => VillageError2,
42
58
  WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
43
59
  WebhookError: () => WebhookError,
44
60
  appNotifyBody: () => appNotifyBody,
@@ -164,7 +180,7 @@ var ERROR_STATUS = {
164
180
  // Deploy-freeze and unattended windows: agents keep running, but config and steer are closed (DESIGN.md §5)
165
181
  FROZEN: 403
166
182
  };
167
- var VillageError = class extends Error {
183
+ var VillageError2 = class extends Error {
168
184
  code;
169
185
  status;
170
186
  details;
@@ -309,8 +325,8 @@ async function verifySigned(token, options) {
309
325
  }
310
326
  if (header3.alg !== "EdDSA") throw new ReceiptError("malformed", `unsupported alg ${header3.alg}`);
311
327
  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);
328
+ const keys2 = typeof options.keys === "function" ? await options.keys() : options.keys;
329
+ const jwk = keys2.find((k) => (k.kid ?? k.x.slice(0, 16)) === header3.kid) ?? (keys2.length === 1 && !header3.kid ? keys2[0] : void 0);
314
330
  if (!jwk) throw new ReceiptError("unknown_key", `no village key with kid ${header3.kid}`);
315
331
  const key = await crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, ED25519, false, ["verify"]);
316
332
  const ok = await crypto.subtle.verify(ED25519, key, fromBase64url(signaturePart), utf8(`${headerPart}.${payloadPart}`));
@@ -328,9 +344,9 @@ function jwksLoader(url, { ttlMs = 10 * 6e4 } = {}) {
328
344
  if (cached && Date.now() - cached.at < ttlMs) return cached.keys;
329
345
  const res = await fetch(url);
330
346
  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;
347
+ const { keys: keys2 } = await res.json();
348
+ cached = { keys: keys2, at: Date.now() };
349
+ return keys2;
334
350
  };
335
351
  }
336
352
 
@@ -1411,663 +1427,8 @@ var HEADERS = {
1411
1427
  };
1412
1428
  var MAX_DEPTH = 4;
1413
1429
 
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 AmbasadaApps = class {
1519
- constructor(config) {
1520
- this.config = config;
1521
- }
1522
- config;
1523
- /** True when the app has its secret (hosted on ambasada.dev, `ambasada dev`, or set by hand). */
1524
- get configured() {
1525
- return !!this.config().appSecret;
1526
- }
1527
- /**
1528
- * Verifies a webhook from the hub (signature, 5-minute window) and returns the event, or throws
1529
- * `WebhookInvalidError` (answer it with `error.toResponse()`, a 400). Read nothing from the request before.
1530
- */
1531
- async verifyWebhook(input, secret, options = {}) {
1532
- const key = secret ?? this.config().appSecret;
1533
- 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");
1534
- const isRequest = typeof input.text === "function" && input.headers instanceof Headers;
1535
- const headers = isRequest ? input.headers : input.headers;
1536
- const raw = isRequest ? await input.text() : input.body;
1537
- const body = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
1538
- try {
1539
- return await verifyAppWebhook(key, headers, body, options);
1540
- } catch (error) {
1541
- if (error instanceof AppWebhookError) throw new WebhookInvalidError(error.reason, error.message);
1542
- throw error;
1543
- }
1544
- }
1545
- /**
1546
- * Sends a push to the people behind `tokens` (or `users`, for Verified apps with `identity`) through the hub.
1547
- * Checks the limits first (title ≤ 40, body ≤ 140, id ≤ 128, ≤ 100 recipients, `url` on your origin) and
1548
- * throws `AmbasadaError("INVALID_REQUEST")` before sending anything. Tokens in `invalid` are gone: forget them.
1549
- */
1550
- async notify(input) {
1551
- const c = this.config();
1552
- 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)");
1553
- let body;
1554
- try {
1555
- body = appNotifyBody(input, c.origin);
1556
- } catch (error) {
1557
- throw new AmbasadaError("INVALID_REQUEST", error.message, 400);
1558
- }
1559
- const url = `${(c.hubUrl ?? DEFAULT_HUB_URL).replace(/\/+$/, "")}${APP_NOTIFY_PATH}`;
1560
- const doFetch = c.fetch ?? ((i, init) => fetch(i, init));
1561
- let res;
1562
- try {
1563
- res = await doFetch(url, {
1564
- method: "POST",
1565
- headers: { authorization: `Bearer ${c.appSecret}`, "content-type": "application/json", accept: "application/json" },
1566
- body: JSON.stringify(body),
1567
- signal: AbortSignal.timeout(15e3)
1568
- });
1569
- } catch (error) {
1570
- throw new AmbasadaError("NETWORK", `POST ${url}: ${error.message}`, 0);
1571
- }
1572
- const json = await res.json().catch(() => null);
1573
- if (!res.ok) {
1574
- const err = json?.error;
1575
- 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";
1576
- const message = typeof err === "string" ? err : err?.message ?? `the hub answered ${res.status}`;
1577
- throw new AmbasadaError(code, `notify: ${message}`, res.status);
1578
- }
1579
- return appNotifyResult(json);
1580
- }
1581
- };
1582
-
1583
- // ../sdk/src/auth.ts
1584
- var AUTH_PATHS = { login: "/api/auth/login", callback: "/api/auth/callback", logout: "/api/auth/logout", shell: "/api/auth/shell" };
1585
- var SESSION_TTL_S = 7 * 24 * 3600;
1586
- var STATE_TTL_S = 600;
1587
- var SESSION_COOKIE = "ambasada_session";
1588
- var STATE_COOKIE = "ambasada_auth";
1589
- var enc = new TextEncoder();
1590
- function b64url(bytes) {
1591
- let s = "";
1592
- for (const b of bytes) s += String.fromCharCode(b);
1593
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1594
- }
1595
- function fromB64url(text) {
1596
- const s = atob(text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4));
1597
- const out = new Uint8Array(s.length);
1598
- for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
1599
- return out;
1600
- }
1601
- var random = (n) => b64url(crypto.getRandomValues(new Uint8Array(n)));
1602
- async function pkce() {
1603
- const verifier = random(32);
1604
- return { verifier, challenge: await pkceChallenge(verifier) };
1605
- }
1606
- async function pkceChallenge(verifier) {
1607
- return b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(verifier))));
1608
- }
1609
- var keys2 = /* @__PURE__ */ new Map();
1610
- function cookieKey(secret, purpose) {
1611
- const id = `${purpose}
1612
- ${secret}`;
1613
- let key = keys2.get(id);
1614
- if (!key) {
1615
- key = crypto.subtle.importKey("raw", enc.encode(secret), "HKDF", false, ["deriveKey"]).then(
1616
- (ikm) => crypto.subtle.deriveKey(
1617
- { name: "HKDF", hash: "SHA-256", salt: enc.encode("ambasada auth cookie v1"), info: enc.encode(purpose) },
1618
- ikm,
1619
- { name: "HMAC", hash: "SHA-256", length: 256 },
1620
- false,
1621
- ["sign", "verify"]
1622
- )
1623
- );
1624
- keys2.set(id, key);
1625
- }
1626
- return key;
1627
- }
1628
- async function signCookie(secret, purpose, payload) {
1629
- const body = b64url(enc.encode(JSON.stringify(payload)));
1630
- const sig = await crypto.subtle.sign("HMAC", await cookieKey(secret, purpose), enc.encode(body));
1631
- return `${body}.${b64url(new Uint8Array(sig))}`;
1632
- }
1633
- async function verifyCookie(secret, purpose, value, now = Date.now()) {
1634
- if (!value) return null;
1635
- const [body, sig, extra] = value.split(".");
1636
- if (!body || !sig || extra !== void 0) return null;
1637
- try {
1638
- const ok = await crypto.subtle.verify("HMAC", await cookieKey(secret, purpose), fromB64url(sig), enc.encode(body));
1639
- if (!ok) return null;
1640
- const payload = JSON.parse(new TextDecoder().decode(fromB64url(body)));
1641
- return typeof payload.exp === "number" && payload.exp * 1e3 > now ? payload : null;
1642
- } catch {
1643
- return null;
1644
- }
1645
- }
1646
- function readCookie(req, name) {
1647
- for (const part of (req.headers.get("cookie") ?? "").split(";")) {
1648
- const i = part.indexOf("=");
1649
- if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
1650
- }
1651
- return null;
1652
- }
1653
- var cookieName = (base, secure) => secure ? `__Host-${base}` : base;
1654
- var readOurCookie = (req, base) => readCookie(req, cookieName(base, true)) ?? readCookie(req, base);
1655
- function setCookie(base, value, secure, maxAge) {
1656
- return `${cookieName(base, secure)}=${value}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`;
1657
- }
1658
- function safeReturnTo(value) {
1659
- if (!value || !value.startsWith("/") || value.startsWith("//") || value.startsWith("/\\") || /[\u0000-\u001f\u007f]/.test(value)) return "/";
1660
- return value;
1661
- }
1662
- function redirect(location, cookies = []) {
1663
- const headers = new Headers({ location, "cache-control": "no-store" });
1664
- for (const c of cookies) headers.append("set-cookie", c);
1665
- return new Response(null, { status: 303, headers });
1666
- }
1667
- var JWKS_TTL_MS = 36e5;
1668
- var LEEWAY_S = 30;
1669
- var jwksCache = /* @__PURE__ */ new Map();
1670
- async function jwks(issuer, doFetch, refresh = false) {
1671
- const cached = jwksCache.get(issuer);
1672
- if (cached && !refresh && Date.now() - cached.at < JWKS_TTL_MS) return cached.keys;
1673
- if (cached && refresh && Date.now() - cached.at < 6e4) return cached.keys;
1674
- const res = await doFetch(`${issuer}/.well-known/jwks.json`, { headers: { accept: "application/json" } });
1675
- if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada ID keys (${res.status})`, 502);
1676
- const body = await res.json();
1677
- const keys3 = (body.keys ?? []).filter((k) => k.kty === "RSA" && k.n && k.e);
1678
- jwksCache.set(issuer, { at: Date.now(), keys: keys3 });
1679
- return keys3;
1680
- }
1681
- var shellRefused = (message) => new AmbasadaError("UNAUTHORIZED", `shell token refused: ${message}`, 401);
1682
- var AmbasadaAuth = class {
1683
- constructor(config) {
1684
- this.config = config;
1685
- }
1686
- config;
1687
- /** True when the product has credentials (hosted on ambasada.dev, or `ambasada dev`). */
1688
- get configured() {
1689
- const c = this.config();
1690
- return !!(c.clientId && c.clientSecret && c.issuer);
1691
- }
1692
- settings(req) {
1693
- const c = this.config();
1694
- if (!c.clientId || !c.clientSecret || !c.issuer) {
1695
- 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)");
1696
- }
1697
- const origin = (c.publicUrl ? new URL(c.publicUrl) : new URL(req.url)).origin;
1698
- return {
1699
- clientId: c.clientId,
1700
- clientSecret: c.clientSecret,
1701
- issuer: c.issuer.replace(/\/+$/, ""),
1702
- apiUrl: c.apiUrl.replace(/\/+$/, ""),
1703
- origin,
1704
- secure: origin.startsWith("https:"),
1705
- redirectUri: `${origin}${AUTH_PATHS.callback}`,
1706
- // Called as a plain function: workerd refuses `fetch` invoked as a method of another object.
1707
- fetch: c.fetch ?? ((input, init) => fetch(input, init))
1708
- };
1709
- }
1710
- /** Redirects to Ambasada ID (consent screen) with PKCE; a short-lived signed cookie holds the state. */
1711
- async login(req, options = {}) {
1712
- const s = this.settings(req);
1713
- const { verifier, challenge } = await pkce();
1714
- const state = random(16);
1715
- const returnTo = safeReturnTo(options.returnTo ?? new URL(req.url).searchParams.get("returnTo"));
1716
- const url = new URL(`${s.issuer}/oauth/authorize`);
1717
- url.search = new URLSearchParams({
1718
- response_type: "code",
1719
- client_id: s.clientId,
1720
- redirect_uri: s.redirectUri,
1721
- scope: options.email ? "openid profile email" : "openid profile",
1722
- state,
1723
- code_challenge: challenge,
1724
- code_challenge_method: "S256"
1725
- }).toString();
1726
- const value = await signCookie(s.clientSecret, "state", { state, verifier, returnTo, exp: Math.floor(Date.now() / 1e3) + STATE_TTL_S });
1727
- return redirect(url.toString(), [setCookie(STATE_COOKIE, value, s.secure, STATE_TTL_S)]);
1728
- }
1729
- /**
1730
- * Ambasada ID sends the person back here: checks the state, exchanges the code (with the PKCE
1731
- * verifier and the client secret), reads their profile from the Ambasada API, sets the session cookie
1732
- * and redirects to `returnTo`. A refused consent goes back to `returnTo` signed out.
1733
- */
1734
- async callback(req) {
1735
- const s = this.settings(req);
1736
- const params = new URL(req.url).searchParams;
1737
- const clear = setCookie(STATE_COOKIE, "", s.secure, 0);
1738
- const saved = await verifyCookie(s.clientSecret, "state", readOurCookie(req, STATE_COOKIE));
1739
- if (!saved || !params.get("state") || params.get("state") !== saved.state) {
1740
- throw new AmbasadaError("UNAUTHORIZED", "sign-in expired or was started elsewhere: try again", 400);
1741
- }
1742
- const returnTo = safeReturnTo(saved.returnTo);
1743
- if (params.get("error")) return redirect(returnTo, [clear]);
1744
- const code = params.get("code");
1745
- if (!code) throw new AmbasadaError("INVALID_REQUEST", "no authorization code", 400);
1746
- const token = await s.fetch(`${s.issuer}/oauth/token`, {
1747
- method: "POST",
1748
- headers: {
1749
- "content-type": "application/x-www-form-urlencoded",
1750
- accept: "application/json",
1751
- authorization: `Basic ${btoa(`${encodeURIComponent(s.clientId)}:${encodeURIComponent(s.clientSecret)}`)}`
1752
- },
1753
- body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: s.redirectUri, code_verifier: saved.verifier })
1754
- });
1755
- const tokens = await token.json().catch(() => ({}));
1756
- if (!token.ok || !tokens.access_token) throw new AmbasadaError("UNAUTHORIZED", `Ambasada ID refused the sign-in (${tokens.error ?? token.status})`, 401);
1757
- const res = await s.fetch(`${s.apiUrl}/v1/auth/profile`, { headers: { authorization: `Bearer ${tokens.access_token}`, accept: "application/json" } });
1758
- if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada profile (${res.status})`, 502);
1759
- const user = await res.json();
1760
- const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
1761
- const session = await signCookie(s.clientSecret, "session", { user, exp });
1762
- return redirect(returnTo, [clear, setCookie(SESSION_COOKIE, session, s.secure, SESSION_TTL_S)]);
1763
- }
1764
- /** Whoever is signed in on this request, or null (also null when sign-in is not set up). */
1765
- async user(req) {
1766
- const secret = this.config().clientSecret;
1767
- if (!this.configured || !secret) return null;
1768
- return (await verifyCookie(secret, "session", readOurCookie(req, SESSION_COOKIE)))?.user ?? null;
1769
- }
1770
- /**
1771
- * Verifies a shell token (Clerk JWT template `ambasada-miniapp`, RS256, 60 s) against
1772
- * `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json` (cached 1 h): signature, expiry, issuer, and that its `aud`
1773
- * is this app (its id or origin). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`.
1774
- */
1775
- async verifyShellToken(token, options = {}) {
1776
- const c = this.config();
1777
- if (!c.issuer) throw new AmbasadaError("CONFIG", "silent sign-in needs AMBASADA_AUTH_ISSUER (injected on ambasada.dev and by `ambasada dev`)");
1778
- const trusted = [c.issuer, c.hubUrl].filter((v) => !!v).map((v) => v.replace(/\/+$/, ""));
1779
- const audiences = [options.audience ?? c.audiences ?? []].flat().filter(Boolean).map((a) => a.replace(/\/+$/, ""));
1780
- 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 }");
1781
- const doFetch = c.fetch ?? ((input, init) => fetch(input, init));
1782
- const parts = typeof token === "string" ? token.split(".") : [];
1783
- if (parts.length !== 3) throw shellRefused("not a JWT");
1784
- let header3;
1785
- let claims;
1786
- try {
1787
- header3 = JSON.parse(new TextDecoder().decode(fromB64url(parts[0])));
1788
- claims = JSON.parse(new TextDecoder().decode(fromB64url(parts[1])));
1789
- } catch {
1790
- throw shellRefused("not a JWT");
1791
- }
1792
- if (header3.alg !== "RS256") throw shellRefused(`alg ${String(header3.alg)} (only RS256)`);
1793
- const iss = typeof claims.iss === "string" ? claims.iss.replace(/\/+$/, "") : null;
1794
- if (iss && !trusted.includes(iss)) throw shellRefused(`issuer ${claims.iss}`);
1795
- const issuer = iss ?? trusted[0];
1796
- let keys3 = await jwks(issuer, doFetch);
1797
- let jwk = keys3.find((k) => k.kid === header3.kid) ?? (!header3.kid && keys3.length === 1 ? keys3[0] : void 0);
1798
- if (!jwk) {
1799
- keys3 = await jwks(issuer, doFetch, true);
1800
- jwk = keys3.find((k) => k.kid === header3.kid);
1801
- }
1802
- if (!jwk) throw shellRefused(`unknown key ${String(header3.kid)}`);
1803
- 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"]);
1804
- const ok = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, fromB64url(parts[2]), enc.encode(`${parts[0]}.${parts[1]}`));
1805
- if (!ok) throw shellRefused("bad signature");
1806
- const now = Math.floor((options.now ?? Date.now()) / 1e3);
1807
- if (typeof claims.exp !== "number" || claims.exp + LEEWAY_S < now) throw shellRefused("expired");
1808
- if (typeof claims.nbf === "number" && claims.nbf - LEEWAY_S > now) throw shellRefused("not valid yet");
1809
- if (typeof claims.iat === "number" && claims.iat - LEEWAY_S > now) throw shellRefused("issued in the future");
1810
- const aud = [claims.aud].flat().filter((a) => typeof a === "string").map((a) => a.replace(/\/+$/, ""));
1811
- if (!aud.some((a) => audiences.includes(a))) throw shellRefused(`it is for ${aud.join(", ") || "no app"}, not this one`);
1812
- if (typeof claims.sub !== "string" || !claims.sub) throw shellRefused("no subject");
1813
- const text = (v) => typeof v === "string" && v ? v : null;
1814
- return { user_id: claims.sub, name: text(claims.name), avatar: text(claims.avatar) ?? text(claims.picture) ?? text(claims.image_url) };
1815
- }
1816
- /**
1817
- * Silent sign-in: `POST /api/auth/shell` with `{ "token": "<auth.getToken() token>" }` (or `Authorization: Bearer`).
1818
- * Verifies it, reads the full profile from the hub (falling back to what the token carries) and sets the same
1819
- * session cookie as `callback()`, so a person signed in inside the app and one signed in on the web look the same.
1820
- * Answers `{ user }`.
1821
- */
1822
- async shell(req) {
1823
- const s = this.settings(req);
1824
- if (req.method !== "POST") throw new AmbasadaError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
1825
- let token = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "")?.[1]?.trim();
1826
- if (!token) {
1827
- if (!(req.headers.get("content-type") ?? "").includes("application/json")) throw new AmbasadaError("INVALID_REQUEST", "send the token as JSON { token }", 415);
1828
- const body = await req.json().catch(() => null);
1829
- token = typeof body?.token === "string" ? body.token : void 0;
1830
- }
1831
- if (!token) throw new AmbasadaError("INVALID_REQUEST", "no shell token", 400);
1832
- const audiences = [...this.config().audiences ?? [], s.origin];
1833
- const who = await this.verifyShellToken(token, { audience: audiences });
1834
- let user = { id: who.user_id, name: who.name, avatar_url: who.avatar, builder: false, builder_since: null, member_since: null, pass: null };
1835
- const hub = this.config().hubUrl;
1836
- if (hub) {
1837
- try {
1838
- const res = await s.fetch(`${hub.replace(/\/+$/, "")}/api/apps/profile`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } });
1839
- if (res.ok) {
1840
- const profile = await res.json();
1841
- if (profile.id === who.user_id) user = { ...user, ...profile, id: who.user_id };
1842
- } else await res.body?.cancel();
1843
- } catch {
1844
- }
1845
- }
1846
- const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
1847
- const session = await signCookie(s.clientSecret, "session", { user, exp });
1848
- const headers = new Headers({ "cache-control": "no-store", "content-type": "application/json" });
1849
- headers.append("set-cookie", setCookie(SESSION_COOKIE, session, s.secure, SESSION_TTL_S));
1850
- return new Response(JSON.stringify({ user }), { status: 200, headers });
1851
- }
1852
- /** Clears the session and redirects to `returnTo` (default "/"). Signs out of this product only. */
1853
- logout(req, options = {}) {
1854
- const url = new URL(req.url);
1855
- const publicUrl = this.config().publicUrl;
1856
- const secure = (publicUrl ? new URL(publicUrl) : url).protocol === "https:";
1857
- const returnTo = safeReturnTo(options.returnTo ?? url.searchParams.get("returnTo"));
1858
- return redirect(returnTo, [setCookie(SESSION_COOKIE, "", secure, 0)]);
1859
- }
1860
- };
1861
-
1862
- // ../sdk/src/client.ts
1863
- var DEFAULT_API_URL = "https://api.ambasada.dev";
1864
- function processEnv() {
1865
- const p = globalThis.process;
1866
- return p?.env ?? {};
1867
- }
1868
- var Ambasada = class {
1869
- options;
1870
- keyLoader;
1871
- projectPromise;
1872
- constructor(options = {}) {
1873
- this.options = options;
1874
- }
1875
- env(name) {
1876
- const v = this.options.env?.[name];
1877
- return typeof v === "string" ? v : processEnv()[name];
1878
- }
1879
- get apiUrl() {
1880
- return (this.options.apiUrl ?? this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL") ?? DEFAULT_API_URL).replace(/\/+$/, "");
1881
- }
1882
- get apiKey() {
1883
- const key = this.options.apiKey ?? (this.env("AMBASADA_API_KEY") ?? this.env("VILLAGE_API_KEY"));
1884
- 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 }");
1885
- return key;
1886
- }
1887
- /**
1888
- * Sign in with Ambasada (docs/AUTH.md): `login(req)`, `callback(req)`, `user(req)`, `logout(req)`.
1889
- * Credentials come with the deploy to ambasada.dev, and from `ambasada dev` locally.
1890
- */
1891
- auth = new AmbasadaAuth(() => ({
1892
- clientId: this.options.auth?.clientId ?? this.env("AMBASADA_AUTH_CLIENT_ID"),
1893
- clientSecret: this.options.auth?.clientSecret ?? this.env("AMBASADA_AUTH_CLIENT_SECRET"),
1894
- issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
1895
- publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
1896
- apiUrl: this.apiUrl,
1897
- // Silent sign-in: a shell token must be for this app, by id or origin.
1898
- audiences: [this.appOrigin, this.options.app?.id ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY")].filter((a) => !!a),
1899
- hubUrl: this.hubUrl,
1900
- fetch: this.options.fetch
1901
- }));
1902
- get appOrigin() {
1903
- const url = this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL");
1904
- return url && URL.canParse(url) ? new URL(url).origin : void 0;
1905
- }
1906
- get hubUrl() {
1907
- return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
1908
- }
1909
- /**
1910
- * The mini-app platform (docs/MINI-APPS.md §7): `verifyWebhook(req)` for the hub's app events and
1911
- * `notify({ … })` to push to people who turned notifications on. Uses `AMBASADA_APP_SECRET`.
1912
- */
1913
- apps = new AmbasadaApps(() => ({
1914
- appSecret: this.options.app?.secret ?? this.env("AMBASADA_APP_SECRET"),
1915
- hubUrl: this.hubUrl,
1916
- origin: this.appOrigin,
1917
- fetch: this.options.fetch
1918
- }));
1919
- // ── HTTP ──────────────────────────────────────────────────────────────────
1920
- async send(method, path, opts = {}) {
1921
- const url = new URL(this.apiUrl + path);
1922
- for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
1923
- const headers = { accept: "application/json" };
1924
- if (opts.auth !== false) headers.authorization = `Bearer ${this.apiKey}`;
1925
- let body = opts.raw;
1926
- if (opts.body !== void 0) {
1927
- body = JSON.stringify(opts.body);
1928
- headers["content-type"] = "application/json";
1929
- }
1930
- for (const [k, v] of Object.entries(opts.headers ?? {})) if (v !== void 0) headers[k.toLowerCase()] = v;
1931
- const doFetch = this.options.fetch ?? fetch;
1932
- const attempt = () => {
1933
- const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 45e3);
1934
- const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
1935
- return doFetch(url, { method, headers, body, signal });
1936
- };
1937
- let res;
1938
- try {
1939
- res = await attempt();
1940
- } catch (error) {
1941
- if (opts.signal?.aborted) throw error;
1942
- if (!opts.retry) throw new AmbasadaError("NETWORK", `${method} ${url.pathname}: ${error.message}`, 0);
1943
- try {
1944
- res = await attempt();
1945
- } catch (again) {
1946
- throw new AmbasadaError("NETWORK", `${method} ${url.pathname}: ${again.message}`, 0);
1947
- }
1948
- }
1949
- const text = await res.text();
1950
- let json = text;
1951
- if (text && (res.headers.get("content-type") ?? "").includes("json")) {
1952
- try {
1953
- json = JSON.parse(text);
1954
- } catch {
1955
- }
1956
- }
1957
- if (!res.ok) throw errorFromResponse(res.status, json, `${method} ${url.pathname} \u2192 ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
1958
- return { status: res.status, headers: res.headers, json };
1959
- }
1960
- async call(method, path, opts) {
1961
- return (await this.send(method, path, opts)).json;
1962
- }
1963
- // ── identity, keys ────────────────────────────────────────────────────────
1964
- /** Your app's id (from options, `AMBASADA_APP_ID`, or the project the API key belongs to). */
1965
- project() {
1966
- const known = this.options.project ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY");
1967
- if (known) return Promise.resolve(known);
1968
- this.projectPromise ??= this.call("GET", "/v1/key").then(
1969
- (k) => k.id,
1970
- (error) => {
1971
- this.projectPromise = void 0;
1972
- throw error;
1973
- }
1974
- );
1975
- return this.projectPromise;
1976
- }
1977
- /** The API's signing keys (`/.well-known/jwks.json`, cached). */
1978
- keys() {
1979
- const given = this.options.keys;
1980
- if (Array.isArray(given)) return Promise.resolve(given);
1981
- if (typeof given === "function") return given();
1982
- this.keyLoader ??= cachedKeys(() => this.call("GET", "/.well-known/jwks.json", { auth: false }).then((r) => r.keys));
1983
- return this.keyLoader();
1984
- }
1985
- // ── storage ───────────────────────────────────────────────────────────────
1986
- files = {
1987
- /**
1988
- * Stores bytes, a Blob, or a URL to copy (`POST /v1/files`). Returns a public URL. For an app's own
1989
- * uploads prefer its bucket, `env.FILES` (docs/STORAGE.md).
1990
- */
1991
- put: (data, options = {}) => {
1992
- const query = { ttl_seconds: options.ttlSeconds };
1993
- if (typeof data === "string") return this.call("POST", "/v1/files", { body: { url: data, content_type: options.contentType }, query });
1994
- const contentType = options.contentType ?? (data instanceof Blob && data.type ? data.type : "application/octet-stream");
1995
- return this.call("POST", "/v1/files", { raw: data, headers: { "content-type": contentType }, query });
1996
- }
1997
- };
1998
- /**
1999
- * Your project's JSON key-value store (`/v1/store`): 1 MB per project (keys + values), free. For more,
2000
- * use the app's own D1, KV and R2 (docs/STORAGE.md).
2001
- */
2002
- store = {
2003
- /** The value, or null when the key is not set. */
2004
- get: async (key) => {
2005
- try {
2006
- return (await this.call("GET", `/v1/store/${encodeURIComponent(key)}`)).value;
2007
- } catch (error) {
2008
- if (error instanceof AmbasadaError && error.status === 404) return null;
2009
- throw error;
2010
- }
2011
- },
2012
- /** Saves any JSON value (fails with INVALID_REQUEST when the store would pass 1 MB). */
2013
- set: (key, value) => {
2014
- if (value === void 0) throw new AmbasadaError("INVALID_REQUEST", "store.set needs a JSON value (use store.delete to remove a key)");
2015
- return this.call("PUT", `/v1/store/${encodeURIComponent(key)}`, { body: value });
2016
- },
2017
- /** `set` under its other name. */
2018
- put: (key, value) => this.store.set(key, value),
2019
- /** True when the key existed. */
2020
- delete: async (key) => (await this.call("DELETE", `/v1/store/${encodeURIComponent(key)}`)).deleted,
2021
- /** Keys (up to 1000, optionally under a prefix) and how much of the 1 MB is used. */
2022
- list: (prefix) => this.call("GET", "/v1/store", { query: { prefix } })
2023
- };
2024
- };
2025
- function cachedKeys(load, ttlMs = 10 * 6e4) {
2026
- let cached;
2027
- return () => {
2028
- if (!cached || Date.now() - cached.at > ttlMs) {
2029
- const keys3 = load();
2030
- const entry = { at: Date.now(), keys: keys3 };
2031
- cached = entry;
2032
- keys3.catch(() => {
2033
- if (cached === entry) cached = void 0;
2034
- });
2035
- }
2036
- return cached.keys;
2037
- };
2038
- }
2039
-
2040
- // ../sdk/src/headers.ts
2041
- function getHeader(headers, name) {
2042
- if (typeof headers.get === "function") return headers.get(name);
2043
- const record = headers;
2044
- const key = Object.keys(record).find((k) => k.toLowerCase() === name);
2045
- const value = key === void 0 ? void 0 : record[key];
2046
- return Array.isArray(value) ? value[0] ?? null : value ?? null;
2047
- }
2048
-
2049
- // ../sdk/src/define.ts
2050
- function defineApp(config) {
2051
- 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, -)`);
2052
- return config;
2053
- }
2054
-
2055
- // ../sdk/src/platform.ts
2056
- var ambasada = new Ambasada();
2057
-
2058
- // src/errors.ts
2059
- var VillageError2 = AmbasadaError;
2060
- var isVillageError = isAmbasadaError;
2061
- var ReceiptInvalidError = class extends AmbasadaError {
2062
- reason;
2063
- constructor(reason, message) {
2064
- super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
2065
- this.name = "ReceiptInvalidError";
2066
- this.reason = reason;
2067
- }
2068
- };
2069
-
2070
1430
  // src/receipt.ts
1431
+ import { AmbasadaError as AmbasadaError2, getHeader } from "@ambasada/sdk/platform";
2071
1432
  var RECEIPT_HEADER2 = "x-village-receipt";
2072
1433
  async function readIncoming(req) {
2073
1434
  if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
@@ -2078,7 +1439,7 @@ async function readIncoming(req) {
2078
1439
  const raw = toBytes2(node.rawBody) ?? toBytes2(node.body);
2079
1440
  if (raw) return { headers: node.headers, body: raw };
2080
1441
  if (node.body !== void 0 && node.body !== null && !(typeof node.body === "object" && Object.keys(node.body).length === 0)) {
2081
- throw new AmbasadaError(
1442
+ throw new AmbasadaError2(
2082
1443
  "CONFIG",
2083
1444
  "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)."
2084
1445
  );
@@ -2153,6 +1514,7 @@ function concat(chunks) {
2153
1514
  }
2154
1515
 
2155
1516
  // src/webhook.ts
1517
+ import { getHeader as getHeader2, WebhookInvalidError } from "@ambasada/sdk/platform";
2156
1518
  var { WebhookError: WebhookError2, verifyWebhook: verifyProtocolWebhook } = src_exports;
2157
1519
  var WEBHOOK_HEADERS2 = WEBHOOK_HEADERS;
2158
1520
  var generateWebhookSecret2 = generateWebhookSecret;
@@ -2160,7 +1522,7 @@ var webhookHeaders2 = webhookHeaders;
2160
1522
  function plainHeaders(headers) {
2161
1523
  const out = {};
2162
1524
  for (const name of ["webhook-id", "webhook-timestamp", "webhook-signature"]) {
2163
- const v = getHeader(headers, name);
1525
+ const v = getHeader2(headers, name);
2164
1526
  if (v !== null) out[name] = v;
2165
1527
  }
2166
1528
  return out;
@@ -2193,7 +1555,7 @@ function num(value) {
2193
1555
  }
2194
1556
  function splitOffering(offering2) {
2195
1557
  const [company, name, ...rest] = offering2.split("/");
2196
- if (!company || !name || rest.length) throw new AmbasadaError("INVALID_REQUEST", `offering must be "company/name", got "${offering2}"`);
1558
+ if (!company || !name || rest.length) throw new AmbasadaError3("INVALID_REQUEST", `offering must be "company/name", got "${offering2}"`);
2197
1559
  return [company, name];
2198
1560
  }
2199
1561
  var Village = class extends Ambasada {
@@ -2270,14 +1632,14 @@ var Village = class extends Ambasada {
2270
1632
  try {
2271
1633
  call = await this.verifyReceipt(req, { offering: offering2 });
2272
1634
  } catch (error) {
2273
- if (error instanceof AmbasadaError) return error.toResponse();
1635
+ if (error instanceof AmbasadaError3) return error.toResponse();
2274
1636
  throw error;
2275
1637
  }
2276
1638
  try {
2277
1639
  const out = await handler(call.input, call, req);
2278
1640
  return out instanceof Response ? out : Response.json(out);
2279
1641
  } catch (error) {
2280
- if (error instanceof AmbasadaError) return error.toResponse();
1642
+ if (error instanceof AmbasadaError3) return error.toResponse();
2281
1643
  console.error(error);
2282
1644
  return Response.json({ error: { code: "INTERNAL", message: "handler failed" } }, { status: 500 });
2283
1645
  }
@@ -2466,26 +1828,27 @@ function offering(def) {
2466
1828
  }
2467
1829
 
2468
1830
  // src/index.ts
1831
+ import { Ambasada as Ambasada2, AmbasadaError as AmbasadaError4, isAmbasadaError as isAmbasadaError2, defineApp, ERROR_STATUS as ERROR_STATUS2, WebhookInvalidError as WebhookInvalidError2 } from "@ambasada/sdk/platform";
2469
1832
  var village = new Village();
2470
1833
  var index_default = village;
2471
1834
  export {
2472
- Ambasada,
2473
- AmbasadaError,
1835
+ Ambasada2 as Ambasada,
1836
+ AmbasadaError4 as AmbasadaError,
2474
1837
  DEFAULT_MODEL,
2475
1838
  DEFAULT_VILLAGE_API_URL,
2476
1839
  ERROR_STATUS2 as ERROR_STATUS,
2477
1840
  RECEIPT_HEADER2 as RECEIPT_HEADER,
2478
1841
  ReceiptInvalidError,
2479
1842
  Village,
2480
- VillageError2 as VillageError,
1843
+ VillageError,
2481
1844
  WEBHOOK_HEADERS2 as WEBHOOK_HEADERS,
2482
- WebhookInvalidError,
1845
+ WebhookInvalidError2 as WebhookInvalidError,
2483
1846
  createVillage,
2484
1847
  index_default as default,
2485
1848
  defineApp,
2486
1849
  defineCompany,
2487
1850
  generateWebhookSecret2 as generateWebhookSecret,
2488
- isAmbasadaError,
1851
+ isAmbasadaError2 as isAmbasadaError,
2489
1852
  isVillageError,
2490
1853
  offering,
2491
1854
  readIncoming,