@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/client.d.ts +1 -1
- package/dist/define.d.ts +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +36 -673
- package/dist/receipt.d.ts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/package.json +5 -3
- package/dist/sdk/apps.d.ts +0 -110
- package/dist/sdk/auth.d.ts +0 -132
- package/dist/sdk/client.d.ts +0 -106
- package/dist/sdk/define.d.ts +0 -19
- package/dist/sdk/errors.d.ts +0 -94
- package/dist/sdk/headers.d.ts +0 -3
- package/dist/sdk/platform.d.ts +0 -20
- package/dist/sdk/types.d.ts +0 -40
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: () =>
|
|
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
|
|
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
|
|
313
|
-
const jwk =
|
|
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:
|
|
332
|
-
cached = { keys:
|
|
333
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|