@ambasada/village 0.8.1 → 0.8.3
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/cli.mjs +78 -40
- 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 +85 -686
- 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,
|
|
@@ -69,6 +85,7 @@ __export(src_exports, {
|
|
|
69
85
|
parseCheckpoint: () => parseCheckpoint,
|
|
70
86
|
parseVerifierKey: () => parseVerifierKey,
|
|
71
87
|
pngInfo: () => pngInfo,
|
|
88
|
+
probeManifest: () => probeManifest,
|
|
72
89
|
publicJwkOf: () => publicJwkOf,
|
|
73
90
|
randomId: () => randomId,
|
|
74
91
|
rootFromLeafHashes: () => rootFromLeafHashes,
|
|
@@ -164,7 +181,7 @@ var ERROR_STATUS = {
|
|
|
164
181
|
// Deploy-freeze and unattended windows: agents keep running, but config and steer are closed (DESIGN.md §5)
|
|
165
182
|
FROZEN: 403
|
|
166
183
|
};
|
|
167
|
-
var
|
|
184
|
+
var VillageError2 = class extends Error {
|
|
168
185
|
code;
|
|
169
186
|
status;
|
|
170
187
|
details;
|
|
@@ -309,8 +326,8 @@ async function verifySigned(token, options) {
|
|
|
309
326
|
}
|
|
310
327
|
if (header3.alg !== "EdDSA") throw new ReceiptError("malformed", `unsupported alg ${header3.alg}`);
|
|
311
328
|
if (typeof claims !== "object" || claims === null) throw new ReceiptError("malformed", "receipt claims are not an object");
|
|
312
|
-
const
|
|
313
|
-
const jwk =
|
|
329
|
+
const keys2 = typeof options.keys === "function" ? await options.keys() : options.keys;
|
|
330
|
+
const jwk = keys2.find((k) => (k.kid ?? k.x.slice(0, 16)) === header3.kid) ?? (keys2.length === 1 && !header3.kid ? keys2[0] : void 0);
|
|
314
331
|
if (!jwk) throw new ReceiptError("unknown_key", `no village key with kid ${header3.kid}`);
|
|
315
332
|
const key = await crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, ED25519, false, ["verify"]);
|
|
316
333
|
const ok = await crypto.subtle.verify(ED25519, key, fromBase64url(signaturePart), utf8(`${headerPart}.${payloadPart}`));
|
|
@@ -328,9 +345,9 @@ function jwksLoader(url, { ttlMs = 10 * 6e4 } = {}) {
|
|
|
328
345
|
if (cached && Date.now() - cached.at < ttlMs) return cached.keys;
|
|
329
346
|
const res = await fetch(url);
|
|
330
347
|
if (!res.ok) throw new Error(`failed to load village keys from ${url}: ${res.status}`);
|
|
331
|
-
const { keys:
|
|
332
|
-
cached = { keys:
|
|
333
|
-
return
|
|
348
|
+
const { keys: keys2 } = await res.json();
|
|
349
|
+
cached = { keys: keys2, at: Date.now() };
|
|
350
|
+
return keys2;
|
|
334
351
|
};
|
|
335
352
|
}
|
|
336
353
|
|
|
@@ -1250,6 +1267,34 @@ var USER_AGENT = "ambasada-check/1 (+https://ambasada.id)";
|
|
|
1250
1267
|
async function get(fetchImpl, url) {
|
|
1251
1268
|
return fetchImpl(url, { redirect: "manual", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { "user-agent": USER_AGENT, accept: "*/*" } });
|
|
1252
1269
|
}
|
|
1270
|
+
async function probeManifest(origin, fetchImpl = fetch) {
|
|
1271
|
+
try {
|
|
1272
|
+
const res = await get(fetchImpl, new URL(MANIFEST_URL_PATH, origin).toString());
|
|
1273
|
+
if (res.status !== 200) {
|
|
1274
|
+
await res.body?.cancel();
|
|
1275
|
+
return { on: false, entry: null };
|
|
1276
|
+
}
|
|
1277
|
+
const m = JSON.parse(await res.text());
|
|
1278
|
+
if (!m || typeof m !== "object" || m.schema !== MANIFEST_SCHEMA && m.schema !== 1) return { on: false, entry: null };
|
|
1279
|
+
return { on: true, entry: typeof m.entry === "string" && m.entry.startsWith("/") ? m.entry : "/" };
|
|
1280
|
+
} catch {
|
|
1281
|
+
return { on: false, entry: null };
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
async function checkPage(fetchImpl, url, report, html) {
|
|
1285
|
+
try {
|
|
1286
|
+
const started = Date.now();
|
|
1287
|
+
const res = await get(fetchImpl, url);
|
|
1288
|
+
const text = res.status === 200 && html ? await res.text() : "";
|
|
1289
|
+
if (!html) await res.body?.cancel();
|
|
1290
|
+
const ms = Date.now() - started;
|
|
1291
|
+
if (res.status !== 200) report.problems.push({ code: "entry_status", message: `${url} answered ${res.status}${res.headers.get("location") ? ` \u2192 ${res.headers.get("location")}` : ""}; ${html ? "the entry" : "the app"} must answer 200 without redirects` });
|
|
1292
|
+
else if (html) merge(report, checkHtml(text));
|
|
1293
|
+
if (ms > SLOW_ENTRY_MS) report.warnings.push({ code: "slow_entry", message: `${url} took ${ms} ms; aim for under ${SLOW_ENTRY_MS} ms${html ? " (the splash stays until ready())" : ""}` });
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
report.problems.push({ code: "entry_fetch", message: `could not fetch ${url}: ${error.message}` });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1253
1298
|
async function checkRemote(url, fetchImpl = fetch, options = {}) {
|
|
1254
1299
|
const report = { ok: true, target: url, problems: [], warnings: [], notes: [] };
|
|
1255
1300
|
let origin;
|
|
@@ -1263,8 +1308,23 @@ async function checkRemote(url, fetchImpl = fetch, options = {}) {
|
|
|
1263
1308
|
if (origin.protocol !== "https:") report.problems.push({ code: "not_https", message: "the app must be served over https" });
|
|
1264
1309
|
let manifest = null;
|
|
1265
1310
|
const manifestUrl = new URL(MANIFEST_URL_PATH, origin).toString();
|
|
1311
|
+
if (options.miniapp === false) {
|
|
1312
|
+
report.miniapp = { on: false, entry: null };
|
|
1313
|
+
report.notes.push("mini app: off");
|
|
1314
|
+
await checkPage(fetchImpl, new URL("/", origin).toString(), report, false);
|
|
1315
|
+
report.ok = report.problems.length === 0;
|
|
1316
|
+
return report;
|
|
1317
|
+
}
|
|
1266
1318
|
try {
|
|
1267
1319
|
let res = await get(fetchImpl, manifestUrl);
|
|
1320
|
+
if (res.status === 404 && options.miniapp === void 0) {
|
|
1321
|
+
await res.body?.cancel();
|
|
1322
|
+
report.miniapp = { on: false, entry: null };
|
|
1323
|
+
report.notes.push(`mini app: off (no ${MANIFEST_URL_PATH})`);
|
|
1324
|
+
await checkPage(fetchImpl, new URL("/", origin).toString(), report, false);
|
|
1325
|
+
report.ok = report.problems.length === 0;
|
|
1326
|
+
return report;
|
|
1327
|
+
}
|
|
1268
1328
|
if (res.status >= 300 && res.status < 400) {
|
|
1269
1329
|
const location = res.headers.get("location") ?? "";
|
|
1270
1330
|
if (res.status === 307 && HOSTED_MANIFEST.test(location)) {
|
|
@@ -1286,18 +1346,8 @@ async function checkRemote(url, fetchImpl = fetch, options = {}) {
|
|
|
1286
1346
|
report.problems.push({ code: "manifest_fetch", message: `could not fetch ${manifestUrl}: ${error.message}` });
|
|
1287
1347
|
}
|
|
1288
1348
|
const entryPath = typeof manifest?.entry === "string" && manifest.entry.startsWith("/") ? manifest.entry : "/";
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const started = Date.now();
|
|
1292
|
-
const res = await get(fetchImpl, entryUrl);
|
|
1293
|
-
const html = res.status === 200 ? await res.text() : "";
|
|
1294
|
-
const ms = Date.now() - started;
|
|
1295
|
-
if (res.status !== 200) report.problems.push({ code: "entry_status", message: `${entryUrl} answered ${res.status}${res.headers.get("location") ? ` \u2192 ${res.headers.get("location")}` : ""}; the entry must answer 200 without redirects` });
|
|
1296
|
-
else merge(report, checkHtml(html));
|
|
1297
|
-
if (ms > SLOW_ENTRY_MS) report.warnings.push({ code: "slow_entry", message: `${entryUrl} took ${ms} ms; the splash stays until ready(), aim for under ${SLOW_ENTRY_MS} ms` });
|
|
1298
|
-
} catch (error) {
|
|
1299
|
-
report.problems.push({ code: "entry_fetch", message: `could not fetch ${entryUrl}: ${error.message}` });
|
|
1300
|
-
}
|
|
1349
|
+
report.miniapp = { on: true, entry: entryPath };
|
|
1350
|
+
await checkPage(fetchImpl, new URL(entryPath, origin).toString(), report, true);
|
|
1301
1351
|
if (typeof manifest?.icon === "string" && manifest.icon) {
|
|
1302
1352
|
const iconUrl = new URL(manifest.icon, origin).toString();
|
|
1303
1353
|
try {
|
|
@@ -1365,9 +1415,11 @@ function summarizeCheck(report, now = /* @__PURE__ */ new Date()) {
|
|
|
1365
1415
|
...report.warnings.map((w) => ({ ...w, level: "warning", label: findingLabel(w.code) }))
|
|
1366
1416
|
];
|
|
1367
1417
|
const needs = [...new Set(issues.filter((i) => i.level === "problem").map((i) => i.label))];
|
|
1368
|
-
|
|
1418
|
+
const miniapp = report.miniapp ? { miniapp: report.miniapp.on ? "on" : "off", entry: report.miniapp.entry } : {};
|
|
1419
|
+
return { ok: report.ok, issues, needs, checked_at: now.toISOString(), ...miniapp };
|
|
1369
1420
|
}
|
|
1370
1421
|
function listingLine(tier, check) {
|
|
1422
|
+
if (check?.miniapp === "off") return "Mini app: off";
|
|
1371
1423
|
if (tier === "verified") return "Verified";
|
|
1372
1424
|
if (tier === "listed") return "Listed for the event";
|
|
1373
1425
|
if (tier === "unlisted") return check?.needs.length ? `Unlisted \xB7 needs: ${check.needs.join(", ")}` : "Unlisted";
|
|
@@ -1411,663 +1463,8 @@ var HEADERS = {
|
|
|
1411
1463
|
};
|
|
1412
1464
|
var MAX_DEPTH = 4;
|
|
1413
1465
|
|
|
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
1466
|
// src/receipt.ts
|
|
1467
|
+
import { AmbasadaError as AmbasadaError2, getHeader } from "@ambasada/sdk/platform";
|
|
2071
1468
|
var RECEIPT_HEADER2 = "x-village-receipt";
|
|
2072
1469
|
async function readIncoming(req) {
|
|
2073
1470
|
if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
|
|
@@ -2078,7 +1475,7 @@ async function readIncoming(req) {
|
|
|
2078
1475
|
const raw = toBytes2(node.rawBody) ?? toBytes2(node.body);
|
|
2079
1476
|
if (raw) return { headers: node.headers, body: raw };
|
|
2080
1477
|
if (node.body !== void 0 && node.body !== null && !(typeof node.body === "object" && Object.keys(node.body).length === 0)) {
|
|
2081
|
-
throw new
|
|
1478
|
+
throw new AmbasadaError2(
|
|
2082
1479
|
"CONFIG",
|
|
2083
1480
|
"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
1481
|
);
|
|
@@ -2153,6 +1550,7 @@ function concat(chunks) {
|
|
|
2153
1550
|
}
|
|
2154
1551
|
|
|
2155
1552
|
// src/webhook.ts
|
|
1553
|
+
import { getHeader as getHeader2, WebhookInvalidError } from "@ambasada/sdk/platform";
|
|
2156
1554
|
var { WebhookError: WebhookError2, verifyWebhook: verifyProtocolWebhook } = src_exports;
|
|
2157
1555
|
var WEBHOOK_HEADERS2 = WEBHOOK_HEADERS;
|
|
2158
1556
|
var generateWebhookSecret2 = generateWebhookSecret;
|
|
@@ -2160,7 +1558,7 @@ var webhookHeaders2 = webhookHeaders;
|
|
|
2160
1558
|
function plainHeaders(headers) {
|
|
2161
1559
|
const out = {};
|
|
2162
1560
|
for (const name of ["webhook-id", "webhook-timestamp", "webhook-signature"]) {
|
|
2163
|
-
const v =
|
|
1561
|
+
const v = getHeader2(headers, name);
|
|
2164
1562
|
if (v !== null) out[name] = v;
|
|
2165
1563
|
}
|
|
2166
1564
|
return out;
|
|
@@ -2193,7 +1591,7 @@ function num(value) {
|
|
|
2193
1591
|
}
|
|
2194
1592
|
function splitOffering(offering2) {
|
|
2195
1593
|
const [company, name, ...rest] = offering2.split("/");
|
|
2196
|
-
if (!company || !name || rest.length) throw new
|
|
1594
|
+
if (!company || !name || rest.length) throw new AmbasadaError3("INVALID_REQUEST", `offering must be "company/name", got "${offering2}"`);
|
|
2197
1595
|
return [company, name];
|
|
2198
1596
|
}
|
|
2199
1597
|
var Village = class extends Ambasada {
|
|
@@ -2270,14 +1668,14 @@ var Village = class extends Ambasada {
|
|
|
2270
1668
|
try {
|
|
2271
1669
|
call = await this.verifyReceipt(req, { offering: offering2 });
|
|
2272
1670
|
} catch (error) {
|
|
2273
|
-
if (error instanceof
|
|
1671
|
+
if (error instanceof AmbasadaError3) return error.toResponse();
|
|
2274
1672
|
throw error;
|
|
2275
1673
|
}
|
|
2276
1674
|
try {
|
|
2277
1675
|
const out = await handler(call.input, call, req);
|
|
2278
1676
|
return out instanceof Response ? out : Response.json(out);
|
|
2279
1677
|
} catch (error) {
|
|
2280
|
-
if (error instanceof
|
|
1678
|
+
if (error instanceof AmbasadaError3) return error.toResponse();
|
|
2281
1679
|
console.error(error);
|
|
2282
1680
|
return Response.json({ error: { code: "INTERNAL", message: "handler failed" } }, { status: 500 });
|
|
2283
1681
|
}
|
|
@@ -2466,26 +1864,27 @@ function offering(def) {
|
|
|
2466
1864
|
}
|
|
2467
1865
|
|
|
2468
1866
|
// src/index.ts
|
|
1867
|
+
import { Ambasada as Ambasada2, AmbasadaError as AmbasadaError4, isAmbasadaError as isAmbasadaError2, defineApp, ERROR_STATUS as ERROR_STATUS2, WebhookInvalidError as WebhookInvalidError2 } from "@ambasada/sdk/platform";
|
|
2469
1868
|
var village = new Village();
|
|
2470
1869
|
var index_default = village;
|
|
2471
1870
|
export {
|
|
2472
|
-
Ambasada,
|
|
2473
|
-
AmbasadaError,
|
|
1871
|
+
Ambasada2 as Ambasada,
|
|
1872
|
+
AmbasadaError4 as AmbasadaError,
|
|
2474
1873
|
DEFAULT_MODEL,
|
|
2475
1874
|
DEFAULT_VILLAGE_API_URL,
|
|
2476
1875
|
ERROR_STATUS2 as ERROR_STATUS,
|
|
2477
1876
|
RECEIPT_HEADER2 as RECEIPT_HEADER,
|
|
2478
1877
|
ReceiptInvalidError,
|
|
2479
1878
|
Village,
|
|
2480
|
-
|
|
1879
|
+
VillageError,
|
|
2481
1880
|
WEBHOOK_HEADERS2 as WEBHOOK_HEADERS,
|
|
2482
|
-
WebhookInvalidError,
|
|
1881
|
+
WebhookInvalidError2 as WebhookInvalidError,
|
|
2483
1882
|
createVillage,
|
|
2484
1883
|
index_default as default,
|
|
2485
1884
|
defineApp,
|
|
2486
1885
|
defineCompany,
|
|
2487
1886
|
generateWebhookSecret2 as generateWebhookSecret,
|
|
2488
|
-
isAmbasadaError,
|
|
1887
|
+
isAmbasadaError2 as isAmbasadaError,
|
|
2489
1888
|
isVillageError,
|
|
2490
1889
|
offering,
|
|
2491
1890
|
readIncoming,
|