@ambasada/sdk 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -25
- package/dist/apps.d.ts +2 -2
- package/dist/auth.d.ts +9 -9
- package/dist/client.d.ts +45 -145
- package/dist/define.d.ts +10 -35
- package/dist/errors.d.ts +12 -15
- package/dist/headers.d.ts +3 -0
- package/dist/index.d.ts +126 -23
- package/dist/index.js +1470 -277
- package/dist/platform.d.ts +20 -0
- package/dist/types.d.ts +2 -308
- package/dist/village/client.d.ts +148 -0
- package/dist/village/define.d.ts +44 -0
- package/dist/village/errors.d.ts +19 -0
- package/dist/village/index.d.ts +27 -0
- package/dist/{receipt.d.ts → village/receipt.d.ts} +2 -2
- package/dist/village/types.d.ts +307 -0
- package/dist/{webhook.d.ts → village/webhook.d.ts} +11 -3
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,3 +1,190 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// ../protocol/src/index.ts
|
|
8
|
+
var src_exports = {};
|
|
9
|
+
__export(src_exports, {
|
|
10
|
+
APP_EVENT_TYPES: () => APP_EVENT_TYPES,
|
|
11
|
+
APP_NOTIFY_PATH: () => APP_NOTIFY_PATH,
|
|
12
|
+
APP_SIGNATURE_HEADER: () => APP_SIGNATURE_HEADER,
|
|
13
|
+
APP_TIMESTAMP_HEADER: () => APP_TIMESTAMP_HEADER,
|
|
14
|
+
APP_WEBHOOK_TOLERANCE_S: () => APP_WEBHOOK_TOLERANCE_S,
|
|
15
|
+
AppWebhookError: () => AppWebhookError,
|
|
16
|
+
DEFAULT_HUB_URL: () => DEFAULT_HUB_URL,
|
|
17
|
+
DEFAULT_ISSUER: () => DEFAULT_ISSUER,
|
|
18
|
+
EMPTY_ROOT: () => EMPTY_ROOT,
|
|
19
|
+
ERROR_STATUS: () => ERROR_STATUS,
|
|
20
|
+
FETCH_TIMEOUT_MS: () => FETCH_TIMEOUT_MS,
|
|
21
|
+
HEADERS: () => HEADERS,
|
|
22
|
+
HOSTED_MANIFEST: () => HOSTED_MANIFEST,
|
|
23
|
+
ICON_SIZE: () => ICON_SIZE,
|
|
24
|
+
MANIFEST_CATEGORIES: () => MANIFEST_CATEGORIES,
|
|
25
|
+
MANIFEST_ID: () => MANIFEST_ID,
|
|
26
|
+
MANIFEST_KINDS: () => MANIFEST_KINDS,
|
|
27
|
+
MANIFEST_LIMITS: () => MANIFEST_LIMITS,
|
|
28
|
+
MANIFEST_SCHEMA: () => MANIFEST_SCHEMA,
|
|
29
|
+
MANIFEST_URL_PATH: () => MANIFEST_URL_PATH,
|
|
30
|
+
MANIFEST_VISIBILITY: () => MANIFEST_VISIBILITY,
|
|
31
|
+
MAX_DEPTH: () => MAX_DEPTH,
|
|
32
|
+
MAX_PRICE_NANO: () => MAX_PRICE_NANO,
|
|
33
|
+
MIN_PRICE_NANO: () => MIN_PRICE_NANO,
|
|
34
|
+
NANO_PER_TON: () => NANO_PER_TON,
|
|
35
|
+
NOTIFY_LIMITS: () => NOTIFY_LIMITS,
|
|
36
|
+
RECEIPT_HEADER: () => RECEIPT_HEADER,
|
|
37
|
+
ReceiptError: () => ReceiptError,
|
|
38
|
+
SHELL_TOKEN_TTL_S: () => SHELL_TOKEN_TTL_S,
|
|
39
|
+
SLOW_ENTRY_MS: () => SLOW_ENTRY_MS,
|
|
40
|
+
SYSTEM_ACCOUNT: () => SYSTEM_ACCOUNT,
|
|
41
|
+
VillageError: () => VillageError,
|
|
42
|
+
WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
|
|
43
|
+
WebhookError: () => WebhookError,
|
|
44
|
+
appNotifyBody: () => appNotifyBody,
|
|
45
|
+
appNotifyResult: () => appNotifyResult,
|
|
46
|
+
appWebhookHeaders: () => appWebhookHeaders,
|
|
47
|
+
base64url: () => base64url,
|
|
48
|
+
checkHtml: () => checkHtml,
|
|
49
|
+
checkIcon: () => checkIcon,
|
|
50
|
+
checkRemote: () => checkRemote,
|
|
51
|
+
createReceiptSigner: () => createReceiptSigner,
|
|
52
|
+
decodeLeaf: () => decodeLeaf,
|
|
53
|
+
encodeLeaf: () => encodeLeaf,
|
|
54
|
+
equalBytes: () => equalBytes,
|
|
55
|
+
findingLabel: () => findingLabel,
|
|
56
|
+
formatCheckpoint: () => formatCheckpoint,
|
|
57
|
+
fromBase64: () => fromBase64,
|
|
58
|
+
fromBase64url: () => fromBase64url,
|
|
59
|
+
fromNano: () => fromNano,
|
|
60
|
+
generateShellKey: () => generateShellKey,
|
|
61
|
+
generateSigningKey: () => generateSigningKey,
|
|
62
|
+
generateWebhookSecret: () => generateWebhookSecret,
|
|
63
|
+
inclusionProof: () => inclusionProof,
|
|
64
|
+
isErrorBody: () => isErrorBody,
|
|
65
|
+
jwksLoader: () => jwksLoader,
|
|
66
|
+
leafHash: () => leafHash,
|
|
67
|
+
listingLine: () => listingLine,
|
|
68
|
+
nodeHash: () => nodeHash,
|
|
69
|
+
parseCheckpoint: () => parseCheckpoint,
|
|
70
|
+
parseVerifierKey: () => parseVerifierKey,
|
|
71
|
+
pngInfo: () => pngInfo,
|
|
72
|
+
publicJwkOf: () => publicJwkOf,
|
|
73
|
+
randomId: () => randomId,
|
|
74
|
+
rootFromLeafHashes: () => rootFromLeafHashes,
|
|
75
|
+
sha256Hex: () => sha256Hex,
|
|
76
|
+
signAppWebhook: () => signAppWebhook,
|
|
77
|
+
signNote: () => signNote,
|
|
78
|
+
signShellToken: () => signShellToken,
|
|
79
|
+
signWebhook: () => signWebhook,
|
|
80
|
+
splitPoint: () => splitPoint,
|
|
81
|
+
summarizeCheck: () => summarizeCheck,
|
|
82
|
+
toBase64: () => toBase64,
|
|
83
|
+
toNano: () => toNano,
|
|
84
|
+
validateManifest: () => validateManifest,
|
|
85
|
+
verifierKey: () => verifierKey,
|
|
86
|
+
verifyAppWebhook: () => verifyAppWebhook,
|
|
87
|
+
verifyCheckoutReceipt: () => verifyCheckoutReceipt,
|
|
88
|
+
verifyInclusion: () => verifyInclusion,
|
|
89
|
+
verifyNote: () => verifyNote,
|
|
90
|
+
verifyReceipt: () => verifyReceipt,
|
|
91
|
+
verifyWebhook: () => verifyWebhook,
|
|
92
|
+
webhookHeaders: () => webhookHeaders
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ../protocol/src/money.ts
|
|
96
|
+
var NANO_PER_TON = 1e9;
|
|
97
|
+
var MIN_PRICE_NANO = NANO_PER_TON / 10;
|
|
98
|
+
var MAX_PRICE_NANO = 500 * NANO_PER_TON;
|
|
99
|
+
var DECIMAL = /^(\d+)(?:\.(\d{1,9}))?$/;
|
|
100
|
+
function toNano(value) {
|
|
101
|
+
let text;
|
|
102
|
+
if (typeof value === "number") {
|
|
103
|
+
if (!Number.isFinite(value) || value < 0) return null;
|
|
104
|
+
text = value.toFixed(9);
|
|
105
|
+
} else if (typeof value === "string") {
|
|
106
|
+
text = value.trim();
|
|
107
|
+
} else {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const match = DECIMAL.exec(text);
|
|
111
|
+
if (!match) return null;
|
|
112
|
+
const whole = Number(match[1]);
|
|
113
|
+
const frac = Number((match[2] ?? "").padEnd(9, "0"));
|
|
114
|
+
const nano = whole * NANO_PER_TON + frac;
|
|
115
|
+
return Number.isSafeInteger(nano) ? nano : null;
|
|
116
|
+
}
|
|
117
|
+
function fromNano(nano) {
|
|
118
|
+
return Number((nano / NANO_PER_TON).toFixed(9));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ../protocol/src/errors.ts
|
|
122
|
+
var ERROR_STATUS = {
|
|
123
|
+
// EXCHANGE.md §5 vocabulary
|
|
124
|
+
INSUFFICIENT_FUNDS: 402,
|
|
125
|
+
OFFLINE: 503,
|
|
126
|
+
TIMEOUT: 504,
|
|
127
|
+
SCHEMA_INVALID: 502,
|
|
128
|
+
CAP_BUYER_HOURLY: 429,
|
|
129
|
+
// the company's spending limit for the last hour
|
|
130
|
+
CAP_SELLER_RATE: 429,
|
|
131
|
+
// reserved: seller rate limits land with the rules milestone (day 11)
|
|
132
|
+
DEAL_EXHAUSTED: 409,
|
|
133
|
+
// reserved: deals land with the registry/exchange work
|
|
134
|
+
DEPTH_EXCEEDED: 422,
|
|
135
|
+
BUDGET_EXCEEDED: 402,
|
|
136
|
+
CAP_INBOX_RATE: 429,
|
|
137
|
+
// inbox texts: 20 an hour, 1 unsolicited per counterparty an hour (EXCHANGE.md §8)
|
|
138
|
+
// Additions needed by the gateway and bank
|
|
139
|
+
UPSTREAM_ERROR: 502,
|
|
140
|
+
PRICE_ABOVE_MAX: 409,
|
|
141
|
+
SELF_PURCHASE: 403,
|
|
142
|
+
INVALID_REQUEST: 400,
|
|
143
|
+
UNAUTHORIZED: 401,
|
|
144
|
+
NOT_FOUND: 404,
|
|
145
|
+
CONFLICT: 409,
|
|
146
|
+
IDEMPOTENCY_CONFLICT: 422,
|
|
147
|
+
IDEMPOTENCY_IN_PROGRESS: 409,
|
|
148
|
+
HOLD_NOT_ACTIVE: 409,
|
|
149
|
+
INTERNAL: 500,
|
|
150
|
+
// Spending limits a team sets for its company wallet (DESIGN.md §0 "Controlling agent spending")
|
|
151
|
+
CAP_PER_PAYMENT: 403,
|
|
152
|
+
CAP_BUYER_DAILY: 429,
|
|
153
|
+
CAP_PAYEE_DAILY: 429,
|
|
154
|
+
// A person's spend at one company per venue day (DESIGN.md §5 "Visitor rules")
|
|
155
|
+
CAP_VISITOR_DAILY: 429,
|
|
156
|
+
// Agent payments over the automatic limit wait for the team (DESIGN.md §5 "Agent approvals")
|
|
157
|
+
APPROVAL_REQUIRED: 403,
|
|
158
|
+
APPROVAL_INVALID: 403,
|
|
159
|
+
APPROVALS_PENDING: 429,
|
|
160
|
+
// Model and tool provider (LLMOK, docs/LLMOK.md): the village's provider budget is spent, or the
|
|
161
|
+
// provider refused the model/tool/key (a configuration problem on our side). Never the caller's fault.
|
|
162
|
+
PROVIDER_BUDGET: 503,
|
|
163
|
+
PROVIDER_DENIED: 502,
|
|
164
|
+
// Deploy-freeze and unattended windows: agents keep running, but config and steer are closed (DESIGN.md §5)
|
|
165
|
+
FROZEN: 403
|
|
166
|
+
};
|
|
167
|
+
var VillageError = class extends Error {
|
|
168
|
+
code;
|
|
169
|
+
status;
|
|
170
|
+
details;
|
|
171
|
+
constructor(code, message, details = {}) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.name = "VillageError";
|
|
174
|
+
this.code = code;
|
|
175
|
+
this.status = ERROR_STATUS[code];
|
|
176
|
+
this.details = details;
|
|
177
|
+
}
|
|
178
|
+
toBody() {
|
|
179
|
+
return { error: { code: this.code, message: this.message, ...this.details } };
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
function isErrorBody(value) {
|
|
183
|
+
if (typeof value !== "object" || value === null) return false;
|
|
184
|
+
const error = value.error;
|
|
185
|
+
return typeof error === "object" && error !== null && typeof error.code === "string";
|
|
186
|
+
}
|
|
187
|
+
|
|
1
188
|
// ../protocol/src/encoding.ts
|
|
2
189
|
var encoder = new TextEncoder();
|
|
3
190
|
var decoder = new TextDecoder();
|
|
@@ -7,6 +194,11 @@ function utf8(text) {
|
|
|
7
194
|
function fromUtf8(bytes) {
|
|
8
195
|
return decoder.decode(bytes);
|
|
9
196
|
}
|
|
197
|
+
function base64url(bytes) {
|
|
198
|
+
let binary = "";
|
|
199
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
200
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
201
|
+
}
|
|
10
202
|
function fromBase64url(text) {
|
|
11
203
|
const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4);
|
|
12
204
|
const binary = atob(padded);
|
|
@@ -21,9 +213,14 @@ function toBytes(data) {
|
|
|
21
213
|
}
|
|
22
214
|
async function sha256Hex(data) {
|
|
23
215
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", toBytes(data)));
|
|
24
|
-
let
|
|
25
|
-
for (const byte of digest)
|
|
26
|
-
return
|
|
216
|
+
let hex3 = "";
|
|
217
|
+
for (const byte of digest) hex3 += byte.toString(16).padStart(2, "0");
|
|
218
|
+
return hex3;
|
|
219
|
+
}
|
|
220
|
+
function randomId(prefix, bytes = 12) {
|
|
221
|
+
const raw = new Uint8Array(bytes);
|
|
222
|
+
crypto.getRandomValues(raw);
|
|
223
|
+
return `${prefix}_${base64url(raw)}`;
|
|
27
224
|
}
|
|
28
225
|
|
|
29
226
|
// ../protocol/src/receipt.ts
|
|
@@ -38,6 +235,38 @@ var ReceiptError = class extends Error {
|
|
|
38
235
|
reason;
|
|
39
236
|
};
|
|
40
237
|
var ED25519 = { name: "Ed25519" };
|
|
238
|
+
function publicJwkOf(jwk) {
|
|
239
|
+
return { kty: "OKP", crv: "Ed25519", x: jwk.x, kid: jwk.kid ?? jwk.x.slice(0, 16), alg: "EdDSA", use: "sig" };
|
|
240
|
+
}
|
|
241
|
+
async function generateSigningKey() {
|
|
242
|
+
const pair = await crypto.subtle.generateKey(ED25519, true, ["sign", "verify"]);
|
|
243
|
+
const exported = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
244
|
+
const kid = `vk-${(await sha256Hex(exported.x)).slice(0, 12)}`;
|
|
245
|
+
const privateJwk = { kty: "OKP", crv: "Ed25519", x: exported.x, d: exported.d, kid, alg: "EdDSA" };
|
|
246
|
+
return { privateJwk, publicJwk: publicJwkOf(privateJwk) };
|
|
247
|
+
}
|
|
248
|
+
async function createReceiptSigner(privateJwk) {
|
|
249
|
+
if (!privateJwk.d) throw new Error("receipt signing key must be a private Ed25519 JWK (missing d)");
|
|
250
|
+
const publicJwk = publicJwkOf(privateJwk);
|
|
251
|
+
const kid = publicJwk.kid;
|
|
252
|
+
const key = await crypto.subtle.importKey(
|
|
253
|
+
"jwk",
|
|
254
|
+
{ kty: "OKP", crv: "Ed25519", x: privateJwk.x, d: privateJwk.d },
|
|
255
|
+
ED25519,
|
|
256
|
+
false,
|
|
257
|
+
["sign"]
|
|
258
|
+
);
|
|
259
|
+
const header3 = base64url(utf8(JSON.stringify({ alg: "EdDSA", typ: "JWT", kid })));
|
|
260
|
+
return {
|
|
261
|
+
kid,
|
|
262
|
+
publicJwk,
|
|
263
|
+
async sign(claims) {
|
|
264
|
+
const signingInput = `${header3}.${base64url(utf8(JSON.stringify(claims)))}`;
|
|
265
|
+
const signature = new Uint8Array(await crypto.subtle.sign(ED25519, key, utf8(signingInput)));
|
|
266
|
+
return `${signingInput}.${base64url(signature)}`;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
41
270
|
async function verifyReceipt(token, options) {
|
|
42
271
|
const claims = await verifySigned(token, options);
|
|
43
272
|
if (claims.kind !== void 0 || typeof claims.call_id !== "string" || typeof claims.body_sha256 !== "string") {
|
|
@@ -93,6 +322,584 @@ async function verifySigned(token, options) {
|
|
|
93
322
|
if (claims.iss !== (options.issuer ?? DEFAULT_ISSUER)) throw new ReceiptError("wrong_issuer", `unexpected issuer ${String(claims.iss)}`);
|
|
94
323
|
return claims;
|
|
95
324
|
}
|
|
325
|
+
function jwksLoader(url, { ttlMs = 10 * 6e4 } = {}) {
|
|
326
|
+
let cached;
|
|
327
|
+
return async () => {
|
|
328
|
+
if (cached && Date.now() - cached.at < ttlMs) return cached.keys;
|
|
329
|
+
const res = await fetch(url);
|
|
330
|
+
if (!res.ok) throw new Error(`failed to load village keys from ${url}: ${res.status}`);
|
|
331
|
+
const { keys: keys3 } = await res.json();
|
|
332
|
+
cached = { keys: keys3, at: Date.now() };
|
|
333
|
+
return keys3;
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/_u64.js
|
|
338
|
+
var fromNumH = (n) => n / 2 ** 32 | 0;
|
|
339
|
+
var fromNumL = (n) => n >>> 0;
|
|
340
|
+
function setU64FromNum(view, byteOffset, n, isLE) {
|
|
341
|
+
const h = fromNumH(n);
|
|
342
|
+
const l = fromNumL(n);
|
|
343
|
+
view.setUint32(byteOffset, isLE ? l : h, isLE);
|
|
344
|
+
view.setUint32(byteOffset + 4, isLE ? h : l, isLE);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/utils.js
|
|
348
|
+
function isBytes(a) {
|
|
349
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
350
|
+
}
|
|
351
|
+
var atitle = (title) => title ? `"${title}" ` : "";
|
|
352
|
+
function anumber(n, title = "") {
|
|
353
|
+
if (typeof n !== "number")
|
|
354
|
+
throw new TypeError(atitle(title) + "expected number, got " + typeof n);
|
|
355
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
356
|
+
throw new RangeError(atitle(title) + "expected integer >= 0, got " + n);
|
|
357
|
+
return n;
|
|
358
|
+
}
|
|
359
|
+
function abytes(value, length, title = "") {
|
|
360
|
+
if (isBytes(value) && (length === void 0 || value.length === length))
|
|
361
|
+
return value;
|
|
362
|
+
if (length !== void 0)
|
|
363
|
+
anumber(length, "length");
|
|
364
|
+
const bytes = isBytes(value);
|
|
365
|
+
const ofLen = length !== void 0 ? ` of length ${length}` : "";
|
|
366
|
+
const got = bytes ? `length=${value.length}` : `type=${typeof value}`;
|
|
367
|
+
const message = atitle(title) + "expected Uint8Array" + ofLen + ", got " + got;
|
|
368
|
+
if (!bytes)
|
|
369
|
+
throw new TypeError(message);
|
|
370
|
+
throw new RangeError(message);
|
|
371
|
+
}
|
|
372
|
+
var aobject = (value, label) => {
|
|
373
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
374
|
+
throw new TypeError((label === "object" ? "" : `"${label}" `) + "expected object, got type=" + typeof value);
|
|
375
|
+
};
|
|
376
|
+
var aopts = (value, label) => {
|
|
377
|
+
aobject(value, label);
|
|
378
|
+
const proto = Object.getPrototypeOf(value);
|
|
379
|
+
if (proto !== Object.prototype && proto !== null)
|
|
380
|
+
throw new TypeError(`"${label}" expected plain object`);
|
|
381
|
+
if (Object.hasOwn(value, "__proto__"))
|
|
382
|
+
throw new TypeError(`"${label}.__proto__" is not allowed`);
|
|
383
|
+
};
|
|
384
|
+
function aexists(instance, checkFinished = true) {
|
|
385
|
+
if (instance.destroyed)
|
|
386
|
+
throw new Error("hash was destroyed");
|
|
387
|
+
if (checkFinished && instance.finished)
|
|
388
|
+
throw new Error("digest() was already called");
|
|
389
|
+
}
|
|
390
|
+
function aoutput(out, instance) {
|
|
391
|
+
abytes(out, void 0, "output");
|
|
392
|
+
const min = instance.outputLen;
|
|
393
|
+
if (!(out.length >= min)) {
|
|
394
|
+
throw new RangeError('"output" expected length >= ' + min);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function clean(...arrays) {
|
|
398
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
399
|
+
arrays[i].fill(0);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function createView(arr) {
|
|
403
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
404
|
+
}
|
|
405
|
+
function rotr(word, shift) {
|
|
406
|
+
return word << 32 - shift | word >>> shift;
|
|
407
|
+
}
|
|
408
|
+
function checkOpts(defaults, opts, title = "opts") {
|
|
409
|
+
aopts(defaults, "defaults");
|
|
410
|
+
if (opts !== void 0)
|
|
411
|
+
aopts(opts, title);
|
|
412
|
+
const merged = Object.assign(/* @__PURE__ */ Object.create(null), defaults, opts);
|
|
413
|
+
return merged;
|
|
414
|
+
}
|
|
415
|
+
function createHasher(hashCons, info = {}) {
|
|
416
|
+
if (typeof hashCons !== "function")
|
|
417
|
+
throw new TypeError('"hashCons" expected function, got type=' + typeof hashCons);
|
|
418
|
+
info = checkOpts({}, info, "info");
|
|
419
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
420
|
+
const tmp = hashCons(void 0);
|
|
421
|
+
hashC.outputLen = tmp.outputLen;
|
|
422
|
+
hashC.blockLen = tmp.blockLen;
|
|
423
|
+
hashC.canXOF = tmp.canXOF;
|
|
424
|
+
hashC.create = (opts) => hashCons(opts);
|
|
425
|
+
Object.assign(hashC, info);
|
|
426
|
+
return Object.freeze(hashC);
|
|
427
|
+
}
|
|
428
|
+
var oidNist = (suffix) => ({
|
|
429
|
+
// Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet.
|
|
430
|
+
// Larger suffix values would need base-128 OID encoding and a different length byte.
|
|
431
|
+
oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/_md.js
|
|
435
|
+
function Chi(a, b, c) {
|
|
436
|
+
return a & b ^ ~a & c;
|
|
437
|
+
}
|
|
438
|
+
function Maj(a, b, c) {
|
|
439
|
+
return a & b ^ a & c ^ b & c;
|
|
440
|
+
}
|
|
441
|
+
var HashMD = class {
|
|
442
|
+
blockLen;
|
|
443
|
+
outputLen;
|
|
444
|
+
canXOF = false;
|
|
445
|
+
padOffset;
|
|
446
|
+
isLE;
|
|
447
|
+
// For partial updates less than block size
|
|
448
|
+
buffer;
|
|
449
|
+
view;
|
|
450
|
+
finished = false;
|
|
451
|
+
length = 0;
|
|
452
|
+
pos = 0;
|
|
453
|
+
destroyed = false;
|
|
454
|
+
constructor(blockLen, outputLen, padOffset, isLE) {
|
|
455
|
+
this.blockLen = blockLen;
|
|
456
|
+
this.outputLen = outputLen;
|
|
457
|
+
this.padOffset = padOffset;
|
|
458
|
+
this.isLE = isLE;
|
|
459
|
+
this.buffer = new Uint8Array(blockLen);
|
|
460
|
+
this.view = createView(this.buffer);
|
|
461
|
+
}
|
|
462
|
+
update(data) {
|
|
463
|
+
aexists(this);
|
|
464
|
+
abytes(data);
|
|
465
|
+
const { view, buffer, blockLen } = this;
|
|
466
|
+
const len = data.length;
|
|
467
|
+
let processed = false;
|
|
468
|
+
for (let pos = 0; pos < len; ) {
|
|
469
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
470
|
+
if (take === blockLen) {
|
|
471
|
+
const dataView = createView(data);
|
|
472
|
+
for (; blockLen <= len - pos; pos += blockLen)
|
|
473
|
+
this.process(dataView, pos);
|
|
474
|
+
processed = true;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
buffer.set(pos === 0 && take === len ? data : data.subarray(pos, pos + take), this.pos);
|
|
478
|
+
this.pos += take;
|
|
479
|
+
pos += take;
|
|
480
|
+
if (this.pos === blockLen) {
|
|
481
|
+
this.process(view, 0);
|
|
482
|
+
this.pos = 0;
|
|
483
|
+
processed = true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
this.length += data.length;
|
|
487
|
+
if (processed)
|
|
488
|
+
this.roundClean();
|
|
489
|
+
return this;
|
|
490
|
+
}
|
|
491
|
+
digestInto(out) {
|
|
492
|
+
aexists(this);
|
|
493
|
+
aoutput(out, this);
|
|
494
|
+
this.finished = true;
|
|
495
|
+
const { buffer, view, blockLen, isLE } = this;
|
|
496
|
+
let { pos } = this;
|
|
497
|
+
buffer[pos++] = 128;
|
|
498
|
+
buffer.fill(0, pos);
|
|
499
|
+
if (this.padOffset > blockLen - pos) {
|
|
500
|
+
this.process(view, 0);
|
|
501
|
+
buffer.fill(0);
|
|
502
|
+
}
|
|
503
|
+
setU64FromNum(view, blockLen - 8, this.length * 8, isLE);
|
|
504
|
+
this.process(view, 0);
|
|
505
|
+
this.roundClean();
|
|
506
|
+
const oview = out === buffer ? view : createView(out);
|
|
507
|
+
const len = this.outputLen;
|
|
508
|
+
const outLen = len / 4;
|
|
509
|
+
const state = this.get();
|
|
510
|
+
if (len % 4 || outLen > state.length)
|
|
511
|
+
throw new Error("invalid outputLen");
|
|
512
|
+
for (let i = 0; i < outLen; i++)
|
|
513
|
+
oview.setUint32(4 * i, state[i], isLE);
|
|
514
|
+
}
|
|
515
|
+
digest() {
|
|
516
|
+
const { buffer, outputLen } = this;
|
|
517
|
+
this.digestInto(buffer);
|
|
518
|
+
const res = buffer.slice(0, outputLen);
|
|
519
|
+
this.destroy();
|
|
520
|
+
return res;
|
|
521
|
+
}
|
|
522
|
+
_cloneIntoMeta(to) {
|
|
523
|
+
const { buffer, length, finished, destroyed, pos } = this;
|
|
524
|
+
to.destroyed = destroyed;
|
|
525
|
+
to.finished = finished;
|
|
526
|
+
to.length = length;
|
|
527
|
+
to.pos = pos;
|
|
528
|
+
if (pos)
|
|
529
|
+
to.buffer.set(buffer);
|
|
530
|
+
return to;
|
|
531
|
+
}
|
|
532
|
+
clone() {
|
|
533
|
+
return this._cloneInto();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
537
|
+
1779033703,
|
|
538
|
+
3144134277,
|
|
539
|
+
1013904242,
|
|
540
|
+
2773480762,
|
|
541
|
+
1359893119,
|
|
542
|
+
2600822924,
|
|
543
|
+
528734635,
|
|
544
|
+
1541459225
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
// ../../node_modules/.pnpm/@noble+hashes@2.4.0/node_modules/@noble/hashes/sha2.js
|
|
548
|
+
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
549
|
+
1116352408,
|
|
550
|
+
1899447441,
|
|
551
|
+
3049323471,
|
|
552
|
+
3921009573,
|
|
553
|
+
961987163,
|
|
554
|
+
1508970993,
|
|
555
|
+
2453635748,
|
|
556
|
+
2870763221,
|
|
557
|
+
3624381080,
|
|
558
|
+
310598401,
|
|
559
|
+
607225278,
|
|
560
|
+
1426881987,
|
|
561
|
+
1925078388,
|
|
562
|
+
2162078206,
|
|
563
|
+
2614888103,
|
|
564
|
+
3248222580,
|
|
565
|
+
3835390401,
|
|
566
|
+
4022224774,
|
|
567
|
+
264347078,
|
|
568
|
+
604807628,
|
|
569
|
+
770255983,
|
|
570
|
+
1249150122,
|
|
571
|
+
1555081692,
|
|
572
|
+
1996064986,
|
|
573
|
+
2554220882,
|
|
574
|
+
2821834349,
|
|
575
|
+
2952996808,
|
|
576
|
+
3210313671,
|
|
577
|
+
3336571891,
|
|
578
|
+
3584528711,
|
|
579
|
+
113926993,
|
|
580
|
+
338241895,
|
|
581
|
+
666307205,
|
|
582
|
+
773529912,
|
|
583
|
+
1294757372,
|
|
584
|
+
1396182291,
|
|
585
|
+
1695183700,
|
|
586
|
+
1986661051,
|
|
587
|
+
2177026350,
|
|
588
|
+
2456956037,
|
|
589
|
+
2730485921,
|
|
590
|
+
2820302411,
|
|
591
|
+
3259730800,
|
|
592
|
+
3345764771,
|
|
593
|
+
3516065817,
|
|
594
|
+
3600352804,
|
|
595
|
+
4094571909,
|
|
596
|
+
275423344,
|
|
597
|
+
430227734,
|
|
598
|
+
506948616,
|
|
599
|
+
659060556,
|
|
600
|
+
883997877,
|
|
601
|
+
958139571,
|
|
602
|
+
1322822218,
|
|
603
|
+
1537002063,
|
|
604
|
+
1747873779,
|
|
605
|
+
1955562222,
|
|
606
|
+
2024104815,
|
|
607
|
+
2227730452,
|
|
608
|
+
2361852424,
|
|
609
|
+
2428436474,
|
|
610
|
+
2756734187,
|
|
611
|
+
3204031479,
|
|
612
|
+
3329325298
|
|
613
|
+
]);
|
|
614
|
+
var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
615
|
+
var SHA2_32B = class extends HashMD {
|
|
616
|
+
// We cannot use array here since array allows indexing by variable
|
|
617
|
+
// which means optimizer/compiler cannot use registers.
|
|
618
|
+
// Numeric initializers matter: starting the fields as `undefined` changes
|
|
619
|
+
// V8's field representation and makes sha256 3x slower (measured).
|
|
620
|
+
A = 0;
|
|
621
|
+
B = 0;
|
|
622
|
+
C = 0;
|
|
623
|
+
D = 0;
|
|
624
|
+
E = 0;
|
|
625
|
+
F = 0;
|
|
626
|
+
G = 0;
|
|
627
|
+
H = 0;
|
|
628
|
+
constructor(outputLen, IV) {
|
|
629
|
+
super(64, outputLen, 8, false);
|
|
630
|
+
this.A = IV[0] | 0;
|
|
631
|
+
this.B = IV[1] | 0;
|
|
632
|
+
this.C = IV[2] | 0;
|
|
633
|
+
this.D = IV[3] | 0;
|
|
634
|
+
this.E = IV[4] | 0;
|
|
635
|
+
this.F = IV[5] | 0;
|
|
636
|
+
this.G = IV[6] | 0;
|
|
637
|
+
this.H = IV[7] | 0;
|
|
638
|
+
}
|
|
639
|
+
get() {
|
|
640
|
+
const { A, B, C, D, E, F, G, H } = this;
|
|
641
|
+
return [A, B, C, D, E, F, G, H];
|
|
642
|
+
}
|
|
643
|
+
// prettier-ignore
|
|
644
|
+
set(A, B, C, D, E, F, G, H) {
|
|
645
|
+
this.A = A | 0;
|
|
646
|
+
this.B = B | 0;
|
|
647
|
+
this.C = C | 0;
|
|
648
|
+
this.D = D | 0;
|
|
649
|
+
this.E = E | 0;
|
|
650
|
+
this.F = F | 0;
|
|
651
|
+
this.G = G | 0;
|
|
652
|
+
this.H = H | 0;
|
|
653
|
+
}
|
|
654
|
+
_cloneInto(to) {
|
|
655
|
+
(to ||= new this.constructor()).set(...this.get());
|
|
656
|
+
return this._cloneIntoMeta(to);
|
|
657
|
+
}
|
|
658
|
+
process(view, offset) {
|
|
659
|
+
for (let i = 0; i < 16; i++, offset += 4)
|
|
660
|
+
SHA256_W[i] = view.getUint32(offset, false);
|
|
661
|
+
for (let i = 16; i < 64; i++) {
|
|
662
|
+
const W15 = SHA256_W[i - 15];
|
|
663
|
+
const W2 = SHA256_W[i - 2];
|
|
664
|
+
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
665
|
+
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
666
|
+
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
667
|
+
}
|
|
668
|
+
let { A, B, C, D, E, F, G, H } = this;
|
|
669
|
+
for (let i = 0; i < 64; i++) {
|
|
670
|
+
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
671
|
+
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
672
|
+
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
|
673
|
+
const T2 = sigma0 + Maj(A, B, C) | 0;
|
|
674
|
+
H = G;
|
|
675
|
+
G = F;
|
|
676
|
+
F = E;
|
|
677
|
+
E = D + T1 | 0;
|
|
678
|
+
D = C;
|
|
679
|
+
C = B;
|
|
680
|
+
B = A;
|
|
681
|
+
A = T1 + T2 | 0;
|
|
682
|
+
}
|
|
683
|
+
A = A + this.A | 0;
|
|
684
|
+
B = B + this.B | 0;
|
|
685
|
+
C = C + this.C | 0;
|
|
686
|
+
D = D + this.D | 0;
|
|
687
|
+
E = E + this.E | 0;
|
|
688
|
+
F = F + this.F | 0;
|
|
689
|
+
G = G + this.G | 0;
|
|
690
|
+
H = H + this.H | 0;
|
|
691
|
+
this.set(A, B, C, D, E, F, G, H);
|
|
692
|
+
}
|
|
693
|
+
roundClean() {
|
|
694
|
+
clean(SHA256_W);
|
|
695
|
+
}
|
|
696
|
+
destroy() {
|
|
697
|
+
this.destroyed = true;
|
|
698
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
699
|
+
clean(this.buffer);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
var _SHA256 = class extends SHA2_32B {
|
|
703
|
+
constructor() {
|
|
704
|
+
super(32, SHA256_IV);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
var sha256 = /* @__PURE__ */ createHasher(
|
|
708
|
+
() => new _SHA256(),
|
|
709
|
+
/* @__PURE__ */ oidNist(1)
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
// ../protocol/src/merkle.ts
|
|
713
|
+
function leafHash(leaf) {
|
|
714
|
+
const bytes = typeof leaf === "string" ? utf8(leaf) : leaf;
|
|
715
|
+
const input = new Uint8Array(bytes.length + 1);
|
|
716
|
+
input[0] = 0;
|
|
717
|
+
input.set(bytes, 1);
|
|
718
|
+
return sha256(input);
|
|
719
|
+
}
|
|
720
|
+
function nodeHash(left, right) {
|
|
721
|
+
const input = new Uint8Array(65);
|
|
722
|
+
input[0] = 1;
|
|
723
|
+
input.set(left, 1);
|
|
724
|
+
input.set(right, 33);
|
|
725
|
+
return sha256(input);
|
|
726
|
+
}
|
|
727
|
+
var EMPTY_ROOT = sha256(new Uint8Array(0));
|
|
728
|
+
function splitPoint(n) {
|
|
729
|
+
let k = 1;
|
|
730
|
+
while (k * 2 < n) k *= 2;
|
|
731
|
+
return k;
|
|
732
|
+
}
|
|
733
|
+
function rootFromLeafHashes(hashes) {
|
|
734
|
+
const stack = [];
|
|
735
|
+
for (const hash of hashes) {
|
|
736
|
+
let node = { size: 1, hash };
|
|
737
|
+
while (stack.length > 0 && stack[stack.length - 1].size === node.size) {
|
|
738
|
+
const left = stack.pop();
|
|
739
|
+
node = { size: left.size * 2, hash: nodeHash(left.hash, node.hash) };
|
|
740
|
+
}
|
|
741
|
+
stack.push(node);
|
|
742
|
+
}
|
|
743
|
+
if (stack.length === 0) return EMPTY_ROOT;
|
|
744
|
+
let root = stack[stack.length - 1].hash;
|
|
745
|
+
for (let i = stack.length - 2; i >= 0; i--) root = nodeHash(stack[i].hash, root);
|
|
746
|
+
return root;
|
|
747
|
+
}
|
|
748
|
+
function inclusionProof(index, size, rangeHash) {
|
|
749
|
+
if (!(index >= 0 && index < size)) throw new RangeError(`index ${index} not in tree of size ${size}`);
|
|
750
|
+
const proof = [];
|
|
751
|
+
let lo = 0;
|
|
752
|
+
let hi = size;
|
|
753
|
+
while (hi - lo > 1) {
|
|
754
|
+
const k = splitPoint(hi - lo);
|
|
755
|
+
if (index < lo + k) {
|
|
756
|
+
proof.push(rangeHash(lo + k, hi));
|
|
757
|
+
hi = lo + k;
|
|
758
|
+
} else {
|
|
759
|
+
proof.push(rangeHash(lo, lo + k));
|
|
760
|
+
lo = lo + k;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return proof.reverse();
|
|
764
|
+
}
|
|
765
|
+
function verifyInclusion(index, size, leaf, proof, root) {
|
|
766
|
+
if (!(index >= 0 && index < size)) return false;
|
|
767
|
+
let fn = index;
|
|
768
|
+
let sn = size - 1;
|
|
769
|
+
let r = leaf;
|
|
770
|
+
for (const p of proof) {
|
|
771
|
+
if (sn === 0) return false;
|
|
772
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
773
|
+
r = nodeHash(p, r);
|
|
774
|
+
while (fn % 2 === 0 && fn !== 0) {
|
|
775
|
+
fn = Math.floor(fn / 2);
|
|
776
|
+
sn = Math.floor(sn / 2);
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
r = nodeHash(r, p);
|
|
780
|
+
}
|
|
781
|
+
fn = Math.floor(fn / 2);
|
|
782
|
+
sn = Math.floor(sn / 2);
|
|
783
|
+
}
|
|
784
|
+
return sn === 0 && equalBytes(r, root);
|
|
785
|
+
}
|
|
786
|
+
function equalBytes(a, b) {
|
|
787
|
+
if (a.length !== b.length) return false;
|
|
788
|
+
let diff = 0;
|
|
789
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
790
|
+
return diff === 0;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ../protocol/src/note.ts
|
|
794
|
+
var ED255192 = { name: "Ed25519" };
|
|
795
|
+
var ALG_ED25519 = 1;
|
|
796
|
+
var DASH = "\u2014";
|
|
797
|
+
function formatCheckpoint({ origin, size, root }) {
|
|
798
|
+
return `${origin}
|
|
799
|
+
${size}
|
|
800
|
+
${root}
|
|
801
|
+
`;
|
|
802
|
+
}
|
|
803
|
+
function parseCheckpoint(body) {
|
|
804
|
+
const [origin, sizeText, root] = body.split("\n");
|
|
805
|
+
const size = Number(sizeText);
|
|
806
|
+
if (!origin || !Number.isSafeInteger(size) || size < 0 || !root || fromBase64(root).length !== 32) {
|
|
807
|
+
throw new Error("malformed checkpoint");
|
|
808
|
+
}
|
|
809
|
+
return { origin, size, root };
|
|
810
|
+
}
|
|
811
|
+
function keyId(name, publicKey) {
|
|
812
|
+
const input = new Uint8Array([...utf8(name), 10, ALG_ED25519, ...publicKey]);
|
|
813
|
+
return sha256(input).slice(0, 4);
|
|
814
|
+
}
|
|
815
|
+
function verifierKey(name, publicJwk) {
|
|
816
|
+
const publicKey = fromBase64url(publicJwk.x);
|
|
817
|
+
const id = keyId(name, publicKey);
|
|
818
|
+
return `${name}+${hex(id)}+${toBase64(new Uint8Array([ALG_ED25519, ...publicKey]))}`;
|
|
819
|
+
}
|
|
820
|
+
function parseVerifierKey(vkey) {
|
|
821
|
+
const [name, idHex, ...key] = vkey.split("+");
|
|
822
|
+
const keyB64 = key.join("+");
|
|
823
|
+
if (!name || !idHex || !keyB64) throw new Error("malformed verifier key");
|
|
824
|
+
const raw = fromBase64(keyB64);
|
|
825
|
+
if (raw.length !== 33 || raw[0] !== ALG_ED25519) throw new Error("verifier key is not Ed25519");
|
|
826
|
+
const publicKey = raw.slice(1);
|
|
827
|
+
const id = keyId(name, publicKey);
|
|
828
|
+
if (hex(id) !== idHex) throw new Error("verifier key id does not match its key");
|
|
829
|
+
return { name, id, publicKey };
|
|
830
|
+
}
|
|
831
|
+
async function signNote(text, name, privateJwk) {
|
|
832
|
+
if (!text.endsWith("\n")) throw new Error("note text must end with a newline");
|
|
833
|
+
if (!privateJwk.d) throw new Error("signing key must be a private Ed25519 JWK");
|
|
834
|
+
const key = await crypto.subtle.importKey(
|
|
835
|
+
"jwk",
|
|
836
|
+
{ kty: "OKP", crv: "Ed25519", x: privateJwk.x, d: privateJwk.d },
|
|
837
|
+
ED255192,
|
|
838
|
+
false,
|
|
839
|
+
["sign"]
|
|
840
|
+
);
|
|
841
|
+
const signature = new Uint8Array(await crypto.subtle.sign(ED255192, key, utf8(text)));
|
|
842
|
+
const id = keyId(name, fromBase64url(privateJwk.x));
|
|
843
|
+
return `${text}
|
|
844
|
+
${DASH} ${name} ${toBase64(new Uint8Array([...id, ...signature]))}
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
async function verifyNote(note, vkey) {
|
|
848
|
+
const { name, id, publicKey } = parseVerifierKey(vkey);
|
|
849
|
+
const split = note.lastIndexOf("\n\n");
|
|
850
|
+
if (split < 0 || !note.endsWith("\n")) throw new Error("malformed note");
|
|
851
|
+
const text = note.slice(0, split + 1);
|
|
852
|
+
const lines = note.slice(split + 2, -1).split("\n");
|
|
853
|
+
const key = await crypto.subtle.importKey("raw", publicKey, ED255192, false, ["verify"]);
|
|
854
|
+
for (const line of lines) {
|
|
855
|
+
const match = new RegExp(`^${DASH} (\\S+) (\\S+)$`).exec(line);
|
|
856
|
+
if (!match || match[1] !== name) continue;
|
|
857
|
+
const raw = fromBase64(match[2]);
|
|
858
|
+
if (raw.length !== 68 || hex(raw.slice(0, 4)) !== hex(id)) continue;
|
|
859
|
+
if (await crypto.subtle.verify(ED255192, key, raw.slice(4), utf8(text))) return text;
|
|
860
|
+
}
|
|
861
|
+
throw new Error(`note is not signed by ${name}`);
|
|
862
|
+
}
|
|
863
|
+
function toBase64(bytes) {
|
|
864
|
+
let binary = "";
|
|
865
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
866
|
+
return btoa(binary);
|
|
867
|
+
}
|
|
868
|
+
function fromBase64(text) {
|
|
869
|
+
const binary = atob(text);
|
|
870
|
+
const bytes = new Uint8Array(binary.length);
|
|
871
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
872
|
+
return bytes;
|
|
873
|
+
}
|
|
874
|
+
function hex(bytes) {
|
|
875
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ../protocol/src/ledger-log.ts
|
|
879
|
+
var SYSTEM_ACCOUNT = "@village";
|
|
880
|
+
function encodeLeaf(leaf) {
|
|
881
|
+
const ordered = {
|
|
882
|
+
v: 1,
|
|
883
|
+
index: leaf.index,
|
|
884
|
+
id: leaf.id,
|
|
885
|
+
ts: leaf.ts,
|
|
886
|
+
kind: leaf.kind,
|
|
887
|
+
from: leaf.from,
|
|
888
|
+
to: leaf.to,
|
|
889
|
+
amount: leaf.amount,
|
|
890
|
+
from_balance: leaf.from_balance,
|
|
891
|
+
to_balance: leaf.to_balance,
|
|
892
|
+
ref: leaf.ref,
|
|
893
|
+
hold_id: leaf.hold_id,
|
|
894
|
+
memo: leaf.memo
|
|
895
|
+
};
|
|
896
|
+
return JSON.stringify(ordered);
|
|
897
|
+
}
|
|
898
|
+
function decodeLeaf(data) {
|
|
899
|
+
const leaf = JSON.parse(data);
|
|
900
|
+
if (leaf.v !== 1 || encodeLeaf(leaf) !== data) throw new Error("leaf is not in canonical form");
|
|
901
|
+
return leaf;
|
|
902
|
+
}
|
|
96
903
|
|
|
97
904
|
// ../protocol/src/webhook.ts
|
|
98
905
|
var WEBHOOK_HEADERS = {
|
|
@@ -180,6 +987,7 @@ var APP_TIMESTAMP_HEADER = "x-ambasada-timestamp";
|
|
|
180
987
|
var APP_WEBHOOK_TOLERANCE_S = 300;
|
|
181
988
|
var DEFAULT_HUB_URL = "https://hub.ambasada.id";
|
|
182
989
|
var APP_NOTIFY_PATH = "/api/apps/notify";
|
|
990
|
+
var APP_EVENT_TYPES = ["notifications_enabled", "notifications_disabled", "app_added", "app_removed", "ping"];
|
|
183
991
|
var AppWebhookError = class extends Error {
|
|
184
992
|
constructor(reason, message) {
|
|
185
993
|
super(message);
|
|
@@ -198,13 +1006,13 @@ function hmacKey2(secret) {
|
|
|
198
1006
|
}
|
|
199
1007
|
return key;
|
|
200
1008
|
}
|
|
201
|
-
var
|
|
1009
|
+
var hex2 = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
202
1010
|
async function mac(secret, timestamp, body) {
|
|
203
1011
|
return new Uint8Array(await crypto.subtle.sign("HMAC", await hmacKey2(secret), encoder3.encode(`${timestamp}.${body}`)));
|
|
204
1012
|
}
|
|
205
1013
|
async function signAppWebhook(secret, timestamp, body) {
|
|
206
1014
|
if (!secret) throw new AppWebhookError("bad_secret", "no app secret");
|
|
207
|
-
return `v1=${
|
|
1015
|
+
return `v1=${hex2(await mac(secret, String(timestamp), body))}`;
|
|
208
1016
|
}
|
|
209
1017
|
async function appWebhookHeaders(secret, body, timestamp = Math.floor(Date.now() / 1e3)) {
|
|
210
1018
|
return { [APP_TIMESTAMP_HEADER]: String(timestamp), [APP_SIGNATURE_HEADER]: await signAppWebhook(secret, timestamp, body), "content-type": "application/json" };
|
|
@@ -310,8 +1118,301 @@ function appNotifyResult(json) {
|
|
|
310
1118
|
return { sent: list(o.sent), invalid: list(o.invalid), rate_limited: list(o.rate_limited) };
|
|
311
1119
|
}
|
|
312
1120
|
|
|
1121
|
+
// ../protocol/src/check.ts
|
|
1122
|
+
var MANIFEST_URL_PATH = "/.well-known/ambasada.json";
|
|
1123
|
+
var MANIFEST_SCHEMA = 2;
|
|
1124
|
+
var MANIFEST_KINDS = ["product", "event", "tool", "assistant"];
|
|
1125
|
+
var MANIFEST_CATEGORIES = ["shopping", "food", "events", "tools", "learning", "community", "games", "finance", "other"];
|
|
1126
|
+
var MANIFEST_VISIBILITY = ["public", "builders", "unlisted"];
|
|
1127
|
+
var MANIFEST_LIMITS = { id: 32, name: 32, subtitle: 40, description: 170, tags: 5, screenshots: 3 };
|
|
1128
|
+
var MANIFEST_ID = /^[a-z0-9-]{3,32}$/;
|
|
1129
|
+
var HOSTED_MANIFEST = /^https:\/\/hub\.ambasada\.id\/api\/apps\/([a-z0-9-]+)\/manifest$/;
|
|
1130
|
+
var ICON_SIZE = 1024;
|
|
1131
|
+
var SLOW_ENTRY_MS = 3e3;
|
|
1132
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
1133
|
+
var HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
1134
|
+
var SDK_MARKERS = ["@ambasada/miniapp", "miniapp.js", "ambasadaShell"];
|
|
1135
|
+
var findings = () => ({ problems: [], warnings: [] });
|
|
1136
|
+
var merge = (into, from) => {
|
|
1137
|
+
into.problems.push(...from.problems);
|
|
1138
|
+
into.warnings.push(...from.warnings);
|
|
1139
|
+
};
|
|
1140
|
+
function validateManifest(json) {
|
|
1141
|
+
const f = findings();
|
|
1142
|
+
const problem = (code, message) => f.problems.push({ code, message });
|
|
1143
|
+
const warn = (code, message) => f.warnings.push({ code, message });
|
|
1144
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) {
|
|
1145
|
+
problem("manifest_not_object", "the manifest must be a JSON object");
|
|
1146
|
+
return f;
|
|
1147
|
+
}
|
|
1148
|
+
const m = json;
|
|
1149
|
+
const str = (key) => typeof m[key] === "string" ? m[key] : void 0;
|
|
1150
|
+
if (m.schema === 1) warn("schema_1", `schema 1 still works, but the app lists it as Unlisted until it is updated to schema ${MANIFEST_SCHEMA} (ambasada init writes the new one)`);
|
|
1151
|
+
else if (m.schema !== MANIFEST_SCHEMA) problem("schema", `"schema" must be ${MANIFEST_SCHEMA} (got ${JSON.stringify(m.schema)})`);
|
|
1152
|
+
const id = str("id");
|
|
1153
|
+
if (!id) problem("id", '"id" is required: 3\u201332 lowercase letters, digits and dashes');
|
|
1154
|
+
else if (!MANIFEST_ID.test(id)) problem("id", `"id" must match ${MANIFEST_ID} (got ${JSON.stringify(id)})`);
|
|
1155
|
+
const name = str("name");
|
|
1156
|
+
if (!name) problem("name", '"name" is required');
|
|
1157
|
+
else if (name.length > MANIFEST_LIMITS.name) problem("name_length", `"name" is ${name.length} characters; the limit is ${MANIFEST_LIMITS.name}`);
|
|
1158
|
+
const icon = str("icon");
|
|
1159
|
+
if (!icon) problem("icon", '"icon" is required: the path of a 1024\xD71024 opaque PNG, like "/icon.png"');
|
|
1160
|
+
const entry = str("entry");
|
|
1161
|
+
if (!entry) problem("entry", '"entry" is required: the path the app opens, like "/"');
|
|
1162
|
+
else if (!entry.startsWith("/")) problem("entry", `"entry" must be a path starting with / (got ${JSON.stringify(entry)})`);
|
|
1163
|
+
const subtitle = str("subtitle");
|
|
1164
|
+
if (subtitle && subtitle.length > MANIFEST_LIMITS.subtitle) problem("subtitle_length", `"subtitle" is ${subtitle.length} characters; the limit is ${MANIFEST_LIMITS.subtitle}`);
|
|
1165
|
+
const description = str("description");
|
|
1166
|
+
if (description && description.length > MANIFEST_LIMITS.description) problem("description_length", `"description" is ${description.length} characters; the limit is ${MANIFEST_LIMITS.description}`);
|
|
1167
|
+
if (Array.isArray(m.tags) && m.tags.length > MANIFEST_LIMITS.tags) problem("tags_count", `"tags" has ${m.tags.length} items; the limit is ${MANIFEST_LIMITS.tags}`);
|
|
1168
|
+
if (Array.isArray(m.screenshots) && m.screenshots.length > MANIFEST_LIMITS.screenshots) problem("screenshots_count", `"screenshots" has ${m.screenshots.length} items; the limit is ${MANIFEST_LIMITS.screenshots}`);
|
|
1169
|
+
const scopes = Array.isArray(m.scopes) ? m.scopes.filter((s) => typeof s === "string") : [];
|
|
1170
|
+
if (m.scopes !== void 0 && !Array.isArray(m.scopes)) problem("scopes", '"scopes" must be an array of strings');
|
|
1171
|
+
if (scopes.includes("notifications")) {
|
|
1172
|
+
const webhook = m.notifications && typeof m.notifications === "object" ? m.notifications.webhook : void 0;
|
|
1173
|
+
if (typeof webhook !== "string" || !webhook) problem("notifications_webhook", 'the "notifications" scope needs "notifications": { "webhook": "/api/webhook" }, where the app delivers notifications_enabled');
|
|
1174
|
+
}
|
|
1175
|
+
if (scopes.length && !str("privacy_url")) problem("privacy_url", `apps with scopes (${scopes.join(", ")}) need a "privacy_url"`);
|
|
1176
|
+
const visibility = str("visibility");
|
|
1177
|
+
if (m.visibility !== void 0 && (!visibility || !(MANIFEST_VISIBILITY.includes(visibility) || /^event:[a-z0-9-]+$/.test(visibility)))) {
|
|
1178
|
+
problem("visibility", `"visibility" must be one of ${MANIFEST_VISIBILITY.join(", ")} or event:<slug> (got ${JSON.stringify(m.visibility)})`);
|
|
1179
|
+
}
|
|
1180
|
+
const accent = str("accent");
|
|
1181
|
+
if (m.accent !== void 0 && (!accent || !HEX_COLOR.test(accent))) problem("accent", `"accent" must be a #RRGGBB colour (got ${JSON.stringify(m.accent)})`);
|
|
1182
|
+
if (m.splash !== void 0) {
|
|
1183
|
+
const splash = m.splash && typeof m.splash === "object" ? m.splash : null;
|
|
1184
|
+
const color = splash?.color;
|
|
1185
|
+
if (!splash || color !== void 0 && (typeof color !== "string" || !HEX_COLOR.test(color))) problem("splash_color", `"splash.color" must be a #RRGGBB colour (got ${JSON.stringify(color ?? m.splash)})`);
|
|
1186
|
+
}
|
|
1187
|
+
const kind = str("kind");
|
|
1188
|
+
if (m.kind !== void 0 && (!kind || !MANIFEST_KINDS.includes(kind))) warn("kind", `unknown "kind" ${JSON.stringify(m.kind)}: the app knows ${MANIFEST_KINDS.join(", ")} and shows this one as a product`);
|
|
1189
|
+
const category = str("category");
|
|
1190
|
+
if (m.category !== void 0 && (!category || !MANIFEST_CATEGORIES.includes(category))) warn("category", `unknown "category" ${JSON.stringify(m.category)}: the app knows ${MANIFEST_CATEGORIES.join(", ")} and files this one under other`);
|
|
1191
|
+
return f;
|
|
1192
|
+
}
|
|
1193
|
+
var PNG_SIGNATURE = "89504e470d0a1a0a";
|
|
1194
|
+
var hexOf = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
1195
|
+
var latin1 = (b) => String.fromCharCode(...b);
|
|
1196
|
+
function pngInfo(bytes) {
|
|
1197
|
+
if (bytes.length < 33) return null;
|
|
1198
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1199
|
+
if (hexOf(bytes.subarray(0, 8)) !== PNG_SIGNATURE) return null;
|
|
1200
|
+
if (latin1(bytes.subarray(12, 16)) !== "IHDR") return null;
|
|
1201
|
+
const width = view.getUint32(16);
|
|
1202
|
+
const height = view.getUint32(20);
|
|
1203
|
+
const colorType = bytes[25];
|
|
1204
|
+
let hasAlpha = colorType === 4 || colorType === 6;
|
|
1205
|
+
for (let pos = 8; pos + 8 <= bytes.length && !hasAlpha; ) {
|
|
1206
|
+
const length = view.getUint32(pos);
|
|
1207
|
+
const type = latin1(bytes.subarray(pos + 4, pos + 8));
|
|
1208
|
+
if (type === "IDAT" || type === "IEND") break;
|
|
1209
|
+
if (type === "tRNS") hasAlpha = true;
|
|
1210
|
+
pos += 12 + length;
|
|
1211
|
+
}
|
|
1212
|
+
return { width, height, hasAlpha };
|
|
1213
|
+
}
|
|
1214
|
+
function checkIcon(bytes) {
|
|
1215
|
+
const f = findings();
|
|
1216
|
+
const info = pngInfo(bytes);
|
|
1217
|
+
if (!info) f.problems.push({ code: "icon_not_png", message: "the icon must be a PNG" });
|
|
1218
|
+
else {
|
|
1219
|
+
if (info.width !== ICON_SIZE || info.height !== ICON_SIZE) f.problems.push({ code: "icon_size", message: `the icon is ${info.width}\xD7${info.height}; it must be ${ICON_SIZE}\xD7${ICON_SIZE}` });
|
|
1220
|
+
if (info.hasAlpha) f.problems.push({ code: "icon_alpha", message: "the icon has transparency; it must be opaque (the app rounds the corners itself)" });
|
|
1221
|
+
}
|
|
1222
|
+
return f;
|
|
1223
|
+
}
|
|
1224
|
+
function metaTags(html) {
|
|
1225
|
+
const tags = [];
|
|
1226
|
+
for (const [, attrs] of html.matchAll(/<meta\b([^>]*)>/gi)) {
|
|
1227
|
+
const out = {};
|
|
1228
|
+
for (const [, key, dq, sq, bare] of attrs.matchAll(/([a-zA-Z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g)) out[key.toLowerCase()] = (dq ?? sq ?? bare ?? "").trim();
|
|
1229
|
+
tags.push(out);
|
|
1230
|
+
}
|
|
1231
|
+
return tags;
|
|
1232
|
+
}
|
|
1233
|
+
function checkHtml(html) {
|
|
1234
|
+
const f = findings();
|
|
1235
|
+
const metas = metaTags(html);
|
|
1236
|
+
const viewport = metas.find((m) => m.name?.toLowerCase() === "viewport");
|
|
1237
|
+
if (!viewport || !/width\s*=\s*device-width/i.test(viewport.content ?? "")) {
|
|
1238
|
+
f.problems.push({ code: "no_viewport", message: 'the entry page needs <meta name="viewport" content="width=device-width, initial-scale=1">' });
|
|
1239
|
+
}
|
|
1240
|
+
const scheme = metas.find((m) => m.name?.toLowerCase() === "color-scheme")?.content?.toLowerCase().trim();
|
|
1241
|
+
const darkMeta = scheme !== void 0 && /\bdark\b/.test(scheme) && !/\blight\b/.test(scheme);
|
|
1242
|
+
const darkStyle = [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].concat([...html.matchAll(/\bstyle\s*=\s*"([^"]*)"/gi)]).some(([, css]) => [...css.matchAll(/color-scheme\s*:\s*([^;}"]+)/gi)].some(([, v]) => /\bdark\b/i.test(v) && !/\blight\b/i.test(v)));
|
|
1243
|
+
if (darkMeta || darkStyle) f.problems.push({ code: "dark_only", message: "the entry page declares a dark-only color-scheme; the Ambasada app is light (use light, or light dark)" });
|
|
1244
|
+
if (!SDK_MARKERS.some((m) => html.includes(m))) {
|
|
1245
|
+
f.warnings.push({ code: "no_miniapp_sdk", message: "no @ambasada/miniapp, miniapp.js or ambasadaShell in the entry HTML: a bundled import is fine too, the registry crawler confirms ready() is called at runtime" });
|
|
1246
|
+
}
|
|
1247
|
+
return f;
|
|
1248
|
+
}
|
|
1249
|
+
var USER_AGENT = "ambasada-check/1 (+https://ambasada.id)";
|
|
1250
|
+
async function get(fetchImpl, url) {
|
|
1251
|
+
return fetchImpl(url, { redirect: "manual", signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { "user-agent": USER_AGENT, accept: "*/*" } });
|
|
1252
|
+
}
|
|
1253
|
+
async function checkRemote(url, fetchImpl = fetch, options = {}) {
|
|
1254
|
+
const report = { ok: true, target: url, problems: [], warnings: [], notes: [] };
|
|
1255
|
+
let origin;
|
|
1256
|
+
try {
|
|
1257
|
+
origin = new URL(url);
|
|
1258
|
+
} catch {
|
|
1259
|
+
report.problems.push({ code: "bad_url", message: `${url} is not a URL` });
|
|
1260
|
+
report.ok = false;
|
|
1261
|
+
return report;
|
|
1262
|
+
}
|
|
1263
|
+
if (origin.protocol !== "https:") report.problems.push({ code: "not_https", message: "the app must be served over https" });
|
|
1264
|
+
let manifest = null;
|
|
1265
|
+
const manifestUrl = new URL(MANIFEST_URL_PATH, origin).toString();
|
|
1266
|
+
try {
|
|
1267
|
+
let res = await get(fetchImpl, manifestUrl);
|
|
1268
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1269
|
+
const location = res.headers.get("location") ?? "";
|
|
1270
|
+
if (res.status === 307 && HOSTED_MANIFEST.test(location)) {
|
|
1271
|
+
report.notes.push(`manifest hosted by the hub: ${location}`);
|
|
1272
|
+
res = await get(fetchImpl, location);
|
|
1273
|
+
} else report.problems.push({ code: "manifest_redirect", message: `${manifestUrl} redirects (${res.status} \u2192 ${location || "?"}); serve it directly` });
|
|
1274
|
+
}
|
|
1275
|
+
if (res.status !== 200) {
|
|
1276
|
+
if (!report.problems.some((p) => p.code === "manifest_redirect")) report.problems.push({ code: "manifest_status", message: `${manifestUrl} answered ${res.status}` });
|
|
1277
|
+
} else {
|
|
1278
|
+
try {
|
|
1279
|
+
manifest = JSON.parse(await res.text());
|
|
1280
|
+
merge(report, validateManifest(manifest));
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
report.problems.push({ code: "manifest_json", message: `${manifestUrl} is not valid JSON: ${error.message}` });
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
report.problems.push({ code: "manifest_fetch", message: `could not fetch ${manifestUrl}: ${error.message}` });
|
|
1287
|
+
}
|
|
1288
|
+
const entryPath = typeof manifest?.entry === "string" && manifest.entry.startsWith("/") ? manifest.entry : "/";
|
|
1289
|
+
const entryUrl = new URL(entryPath, origin).toString();
|
|
1290
|
+
try {
|
|
1291
|
+
const started = Date.now();
|
|
1292
|
+
const res = await get(fetchImpl, entryUrl);
|
|
1293
|
+
const html = res.status === 200 ? await res.text() : "";
|
|
1294
|
+
const ms = Date.now() - started;
|
|
1295
|
+
if (res.status !== 200) report.problems.push({ code: "entry_status", message: `${entryUrl} answered ${res.status}${res.headers.get("location") ? ` \u2192 ${res.headers.get("location")}` : ""}; the entry must answer 200 without redirects` });
|
|
1296
|
+
else merge(report, checkHtml(html));
|
|
1297
|
+
if (ms > SLOW_ENTRY_MS) report.warnings.push({ code: "slow_entry", message: `${entryUrl} took ${ms} ms; the splash stays until ready(), aim for under ${SLOW_ENTRY_MS} ms` });
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
report.problems.push({ code: "entry_fetch", message: `could not fetch ${entryUrl}: ${error.message}` });
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof manifest?.icon === "string" && manifest.icon) {
|
|
1302
|
+
const iconUrl = new URL(manifest.icon, origin).toString();
|
|
1303
|
+
try {
|
|
1304
|
+
const res = await get(fetchImpl, iconUrl);
|
|
1305
|
+
if (res.status !== 200) report.problems.push({ code: "icon_status", message: `${iconUrl} answered ${res.status}` });
|
|
1306
|
+
else merge(report, checkIcon(new Uint8Array(await res.arrayBuffer())));
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
report.problems.push({ code: "icon_fetch", message: `could not fetch ${iconUrl}: ${error.message}` });
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const scopes = Array.isArray(manifest?.scopes) ? manifest.scopes.filter((x) => typeof x === "string") : [];
|
|
1312
|
+
if (scopes.includes("identity")) {
|
|
1313
|
+
const callbackUrl = new URL("/api/auth/callback", origin).toString();
|
|
1314
|
+
try {
|
|
1315
|
+
const res = await get(fetchImpl, callbackUrl);
|
|
1316
|
+
await res.body?.cancel();
|
|
1317
|
+
if (res.status === 404 || res.status >= 500) report.problems.push({ code: "identity_callback", message: `the identity scope needs Sign in with Ambasada at /api/auth/callback; ${callbackUrl} answered ${res.status}` });
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
report.problems.push({ code: "identity_callback", message: `could not fetch ${callbackUrl}: ${error.message}` });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const webhook = manifest?.notifications && typeof manifest.notifications === "object" ? manifest.notifications.webhook : void 0;
|
|
1323
|
+
if (scopes.includes("notifications") && typeof webhook === "string" && webhook) {
|
|
1324
|
+
const webhookUrl = new URL(webhook, origin);
|
|
1325
|
+
if (webhookUrl.origin !== origin.origin) report.problems.push({ code: "webhook_origin", message: `the notifications webhook ${webhookUrl} must be on ${origin.origin}` });
|
|
1326
|
+
else if (!options.appSecret) report.notes.push(`webhook ${webhookUrl.pathname}: not pinged (needs the app secret; the registry pings it)`);
|
|
1327
|
+
else {
|
|
1328
|
+
const body = JSON.stringify({ event: "ping", app: options.appId ?? (typeof manifest?.id === "string" ? manifest.id : ""), at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1329
|
+
try {
|
|
1330
|
+
const res = await fetchImpl(webhookUrl.toString(), {
|
|
1331
|
+
method: "POST",
|
|
1332
|
+
redirect: "manual",
|
|
1333
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
1334
|
+
headers: { "user-agent": USER_AGENT, ...await appWebhookHeaders(options.appSecret, body) },
|
|
1335
|
+
body
|
|
1336
|
+
});
|
|
1337
|
+
await res.body?.cancel();
|
|
1338
|
+
if (res.status !== 200) report.problems.push({ code: "webhook_ping", message: `${webhookUrl} answered the signed ping with ${res.status}; it must answer 200 (verify with ambasada.apps.verifyWebhook)` });
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
report.problems.push({ code: "webhook_ping", message: `${webhookUrl} did not answer the signed ping: ${error.message}` });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
report.ok = report.problems.length === 0;
|
|
1345
|
+
return report;
|
|
1346
|
+
}
|
|
1347
|
+
var LABELS = [
|
|
1348
|
+
[/^icon/, "icon"],
|
|
1349
|
+
[/^manifest|^schema|^id$|^entry$/, "manifest"],
|
|
1350
|
+
[/^name/, "name"],
|
|
1351
|
+
[/^no_viewport$/, "viewport"],
|
|
1352
|
+
[/^dark_only$/, "light theme"],
|
|
1353
|
+
[/^no_miniapp_sdk$/, "ready()"],
|
|
1354
|
+
[/^entry_|^not_https$|^bad_url$/, "entry page"],
|
|
1355
|
+
[/^privacy_url$/, "privacy page"],
|
|
1356
|
+
[/^notifications_webhook$|^webhook_/, "webhook"],
|
|
1357
|
+
[/^identity_callback$/, "sign-in"]
|
|
1358
|
+
];
|
|
1359
|
+
function findingLabel(code) {
|
|
1360
|
+
return LABELS.find(([re]) => re.test(code))?.[1] ?? code.replace(/_/g, " ");
|
|
1361
|
+
}
|
|
1362
|
+
function summarizeCheck(report, now = /* @__PURE__ */ new Date()) {
|
|
1363
|
+
const issues = [
|
|
1364
|
+
...report.problems.map((p) => ({ ...p, level: "problem", label: findingLabel(p.code) })),
|
|
1365
|
+
...report.warnings.map((w) => ({ ...w, level: "warning", label: findingLabel(w.code) }))
|
|
1366
|
+
];
|
|
1367
|
+
const needs = [...new Set(issues.filter((i) => i.level === "problem").map((i) => i.label))];
|
|
1368
|
+
return { ok: report.ok, issues, needs, checked_at: now.toISOString() };
|
|
1369
|
+
}
|
|
1370
|
+
function listingLine(tier, check) {
|
|
1371
|
+
if (tier === "verified") return "Verified";
|
|
1372
|
+
if (tier === "listed") return "Listed for the event";
|
|
1373
|
+
if (tier === "unlisted") return check?.needs.length ? `Unlisted \xB7 needs: ${check.needs.join(", ")}` : "Unlisted";
|
|
1374
|
+
return check?.needs.length ? `Not registered yet \xB7 needs: ${check.needs.join(", ")}` : "Not registered yet";
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ../protocol/src/shell-token.ts
|
|
1378
|
+
var SHELL_TOKEN_TTL_S = 60;
|
|
1379
|
+
var RSA = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
1380
|
+
async function generateShellKey(kid = `ins_${crypto.randomUUID().slice(0, 8)}`) {
|
|
1381
|
+
const pair = await crypto.subtle.generateKey({ ...RSA, modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) }, true, ["sign", "verify"]);
|
|
1382
|
+
const pub = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
1383
|
+
return { kid, privateKey: pair.privateKey, jwk: { kty: "RSA", n: pub.n, e: pub.e, kid, alg: "RS256", use: "sig" } };
|
|
1384
|
+
}
|
|
1385
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
1386
|
+
async function signShellToken(key, claims) {
|
|
1387
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1388
|
+
const header3 = base64url(utf82(JSON.stringify({ alg: "RS256", typ: "JWT", kid: key.kid })));
|
|
1389
|
+
const payload = base64url(utf82(JSON.stringify({ iat: now, nbf: now, exp: now + SHELL_TOKEN_TTL_S, ...claims })));
|
|
1390
|
+
const sig = new Uint8Array(await crypto.subtle.sign(RSA.name, key.privateKey, utf82(`${header3}.${payload}`)));
|
|
1391
|
+
return `${header3}.${payload}.${base64url(sig)}`;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// ../protocol/src/index.ts
|
|
1395
|
+
var HEADERS = {
|
|
1396
|
+
receipt: "x-village-receipt",
|
|
1397
|
+
callId: "x-village-call-id",
|
|
1398
|
+
amount: "x-village-amount",
|
|
1399
|
+
/** Position of the payment in the public ledger log; fetch an inclusion proof with it. */
|
|
1400
|
+
logIndex: "x-village-log-index",
|
|
1401
|
+
depth: "x-village-depth",
|
|
1402
|
+
budget: "x-village-budget",
|
|
1403
|
+
maxPrice: "x-village-max-price",
|
|
1404
|
+
buyer: "x-village-buyer",
|
|
1405
|
+
idempotencyKey: "idempotency-key",
|
|
1406
|
+
/** An approved approval request id, to retry an agent payment that was over the automatic limit. */
|
|
1407
|
+
approval: "x-village-approval",
|
|
1408
|
+
/** The agent's own reason for a payment, shown to the team next to the Gateway's facts. */
|
|
1409
|
+
reason: "x-village-reason",
|
|
1410
|
+
idempotentReplay: "idempotent-replayed"
|
|
1411
|
+
};
|
|
1412
|
+
var MAX_DEPTH = 4;
|
|
1413
|
+
|
|
313
1414
|
// ../sdk/src/errors.ts
|
|
314
|
-
var
|
|
1415
|
+
var ERROR_STATUS2 = {
|
|
315
1416
|
// EXCHANGE.md §5 vocabulary
|
|
316
1417
|
INSUFFICIENT_FUNDS: 402,
|
|
317
1418
|
OFFLINE: 503,
|
|
@@ -355,17 +1456,24 @@ var ERROR_STATUS = {
|
|
|
355
1456
|
RECEIPT_INVALID: 402,
|
|
356
1457
|
WEBHOOK_INVALID: 400
|
|
357
1458
|
};
|
|
358
|
-
var
|
|
1459
|
+
var BRAND = /* @__PURE__ */ Symbol.for("ambasada.error");
|
|
1460
|
+
var AmbasadaError = class _AmbasadaError extends Error {
|
|
359
1461
|
code;
|
|
360
1462
|
/** HTTP status (0 when the request never got an answer). */
|
|
361
1463
|
status;
|
|
362
1464
|
details;
|
|
363
1465
|
constructor(code, message, status, details = {}) {
|
|
364
1466
|
super(message);
|
|
365
|
-
this.name = "
|
|
1467
|
+
this.name = "AmbasadaError";
|
|
366
1468
|
this.code = code;
|
|
367
|
-
this.status = status ??
|
|
1469
|
+
this.status = status ?? ERROR_STATUS2[code] ?? 500;
|
|
368
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;
|
|
369
1477
|
}
|
|
370
1478
|
get callId() {
|
|
371
1479
|
return this.details.call_id;
|
|
@@ -385,15 +1493,7 @@ var VillageError = class extends Error {
|
|
|
385
1493
|
return Response.json(this.toJSON(), { status: this.status || 500 });
|
|
386
1494
|
}
|
|
387
1495
|
};
|
|
388
|
-
var
|
|
389
|
-
reason;
|
|
390
|
-
constructor(reason, message) {
|
|
391
|
-
super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
|
|
392
|
-
this.name = "ReceiptInvalidError";
|
|
393
|
-
this.reason = reason;
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
var WebhookInvalidError = class extends VillageError {
|
|
1496
|
+
var WebhookInvalidError = class extends AmbasadaError {
|
|
397
1497
|
reason;
|
|
398
1498
|
constructor(reason, message) {
|
|
399
1499
|
super("WEBHOOK_INVALID", message, 400, { reason });
|
|
@@ -401,17 +1501,17 @@ var WebhookInvalidError = class extends VillageError {
|
|
|
401
1501
|
this.reason = reason;
|
|
402
1502
|
}
|
|
403
1503
|
};
|
|
404
|
-
function
|
|
405
|
-
return value instanceof
|
|
1504
|
+
function isAmbasadaError(value, code) {
|
|
1505
|
+
return value instanceof AmbasadaError && (code === void 0 || value.code === code);
|
|
406
1506
|
}
|
|
407
1507
|
function errorFromResponse(status, body, fallback) {
|
|
408
1508
|
const error = typeof body === "object" && body !== null ? body.error : void 0;
|
|
409
1509
|
if (typeof error === "object" && error !== null && typeof error.code === "string") {
|
|
410
1510
|
const { code: code2, message, ...details } = error;
|
|
411
|
-
return new
|
|
1511
|
+
return new AmbasadaError(code2, typeof message === "string" ? message : code2, status, details);
|
|
412
1512
|
}
|
|
413
1513
|
const code = status === 401 ? "UNAUTHORIZED" : status === 404 ? "NOT_FOUND" : status >= 500 ? "INTERNAL" : "INVALID_REQUEST";
|
|
414
|
-
return new
|
|
1514
|
+
return new AmbasadaError(code, fallback, status);
|
|
415
1515
|
}
|
|
416
1516
|
|
|
417
1517
|
// ../sdk/src/apps.ts
|
|
@@ -449,16 +1549,16 @@ var AmbasadaApps = class {
|
|
|
449
1549
|
/**
|
|
450
1550
|
* Sends a push to the people behind `tokens` (or `users`, for Verified apps with `identity`) through the hub.
|
|
451
1551
|
* Checks the limits first (title ≤ 40, body ≤ 140, id ≤ 128, ≤ 100 recipients, `url` on your origin) and
|
|
452
|
-
* throws `
|
|
1552
|
+
* throws `AmbasadaError("INVALID_REQUEST")` before sending anything. Tokens in `invalid` are gone: forget them.
|
|
453
1553
|
*/
|
|
454
1554
|
async notify(input) {
|
|
455
1555
|
const c = this.config();
|
|
456
|
-
if (!c.appSecret) throw new
|
|
1556
|
+
if (!c.appSecret) throw new AmbasadaError("CONFIG", "no app secret: set AMBASADA_APP_SECRET (injected on ambasada.dev; `ambasada dev` sets one for the mock hub)");
|
|
457
1557
|
let body;
|
|
458
1558
|
try {
|
|
459
1559
|
body = appNotifyBody(input, c.origin);
|
|
460
1560
|
} catch (error) {
|
|
461
|
-
throw new
|
|
1561
|
+
throw new AmbasadaError("INVALID_REQUEST", error.message, 400);
|
|
462
1562
|
}
|
|
463
1563
|
const url = `${(c.hubUrl ?? DEFAULT_HUB_URL).replace(/\/+$/, "")}${APP_NOTIFY_PATH}`;
|
|
464
1564
|
const doFetch = c.fetch ?? ((i, init) => fetch(i, init));
|
|
@@ -471,14 +1571,14 @@ var AmbasadaApps = class {
|
|
|
471
1571
|
signal: AbortSignal.timeout(15e3)
|
|
472
1572
|
});
|
|
473
1573
|
} catch (error) {
|
|
474
|
-
throw new
|
|
1574
|
+
throw new AmbasadaError("NETWORK", `POST ${url}: ${error.message}`, 0);
|
|
475
1575
|
}
|
|
476
1576
|
const json = await res.json().catch(() => null);
|
|
477
1577
|
if (!res.ok) {
|
|
478
1578
|
const err = json?.error;
|
|
479
1579
|
const code = typeof err === "object" && err?.code ? err.code : res.status === 401 || res.status === 403 ? "UNAUTHORIZED" : res.status === 429 ? "CAP_SELLER_RATE" : res.status >= 500 ? "UPSTREAM_ERROR" : "INVALID_REQUEST";
|
|
480
1580
|
const message = typeof err === "string" ? err : err?.message ?? `the hub answered ${res.status}`;
|
|
481
|
-
throw new
|
|
1581
|
+
throw new AmbasadaError(code, `notify: ${message}`, res.status);
|
|
482
1582
|
}
|
|
483
1583
|
return appNotifyResult(json);
|
|
484
1584
|
}
|
|
@@ -576,13 +1676,13 @@ async function jwks(issuer, doFetch, refresh = false) {
|
|
|
576
1676
|
if (cached && !refresh && Date.now() - cached.at < JWKS_TTL_MS) return cached.keys;
|
|
577
1677
|
if (cached && refresh && Date.now() - cached.at < 6e4) return cached.keys;
|
|
578
1678
|
const res = await doFetch(`${issuer}/.well-known/jwks.json`, { headers: { accept: "application/json" } });
|
|
579
|
-
if (!res.ok) throw new
|
|
1679
|
+
if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada ID keys (${res.status})`, 502);
|
|
580
1680
|
const body = await res.json();
|
|
581
1681
|
const keys3 = (body.keys ?? []).filter((k) => k.kty === "RSA" && k.n && k.e);
|
|
582
1682
|
jwksCache.set(issuer, { at: Date.now(), keys: keys3 });
|
|
583
1683
|
return keys3;
|
|
584
1684
|
}
|
|
585
|
-
var shellRefused = (message) => new
|
|
1685
|
+
var shellRefused = (message) => new AmbasadaError("UNAUTHORIZED", `shell token refused: ${message}`, 401);
|
|
586
1686
|
var AmbasadaAuth = class {
|
|
587
1687
|
constructor(config) {
|
|
588
1688
|
this.config = config;
|
|
@@ -596,7 +1696,7 @@ var AmbasadaAuth = class {
|
|
|
596
1696
|
settings(req) {
|
|
597
1697
|
const c = this.config();
|
|
598
1698
|
if (!c.clientId || !c.clientSecret || !c.issuer) {
|
|
599
|
-
throw new
|
|
1699
|
+
throw new AmbasadaError("CONFIG", "Sign in with Ambasada is not set up: it works on https://<slug>.ambasada.dev after `ambasada deploy`, and locally under `ambasada dev` (AMBASADA_AUTH_CLIENT_ID / _SECRET / _ISSUER)");
|
|
600
1700
|
}
|
|
601
1701
|
const origin = (c.publicUrl ? new URL(c.publicUrl) : new URL(req.url)).origin;
|
|
602
1702
|
return {
|
|
@@ -632,7 +1732,7 @@ var AmbasadaAuth = class {
|
|
|
632
1732
|
}
|
|
633
1733
|
/**
|
|
634
1734
|
* Ambasada ID sends the person back here: checks the state, exchanges the code (with the PKCE
|
|
635
|
-
* verifier and the client secret), reads their profile from the
|
|
1735
|
+
* verifier and the client secret), reads their profile from the Ambasada API, sets the session cookie
|
|
636
1736
|
* and redirects to `returnTo`. A refused consent goes back to `returnTo` signed out.
|
|
637
1737
|
*/
|
|
638
1738
|
async callback(req) {
|
|
@@ -641,12 +1741,12 @@ var AmbasadaAuth = class {
|
|
|
641
1741
|
const clear = setCookie(STATE_COOKIE, "", s.secure, 0);
|
|
642
1742
|
const saved = await verifyCookie(s.clientSecret, "state", readOurCookie(req, STATE_COOKIE));
|
|
643
1743
|
if (!saved || !params.get("state") || params.get("state") !== saved.state) {
|
|
644
|
-
throw new
|
|
1744
|
+
throw new AmbasadaError("UNAUTHORIZED", "sign-in expired or was started elsewhere: try again", 400);
|
|
645
1745
|
}
|
|
646
1746
|
const returnTo = safeReturnTo(saved.returnTo);
|
|
647
1747
|
if (params.get("error")) return redirect(returnTo, [clear]);
|
|
648
1748
|
const code = params.get("code");
|
|
649
|
-
if (!code) throw new
|
|
1749
|
+
if (!code) throw new AmbasadaError("INVALID_REQUEST", "no authorization code", 400);
|
|
650
1750
|
const token = await s.fetch(`${s.issuer}/oauth/token`, {
|
|
651
1751
|
method: "POST",
|
|
652
1752
|
headers: {
|
|
@@ -657,9 +1757,9 @@ var AmbasadaAuth = class {
|
|
|
657
1757
|
body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: s.redirectUri, code_verifier: saved.verifier })
|
|
658
1758
|
});
|
|
659
1759
|
const tokens = await token.json().catch(() => ({}));
|
|
660
|
-
if (!token.ok || !tokens.access_token) throw new
|
|
1760
|
+
if (!token.ok || !tokens.access_token) throw new AmbasadaError("UNAUTHORIZED", `Ambasada ID refused the sign-in (${tokens.error ?? token.status})`, 401);
|
|
661
1761
|
const res = await s.fetch(`${s.apiUrl}/v1/auth/profile`, { headers: { authorization: `Bearer ${tokens.access_token}`, accept: "application/json" } });
|
|
662
|
-
if (!res.ok) throw new
|
|
1762
|
+
if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada profile (${res.status})`, 502);
|
|
663
1763
|
const user = await res.json();
|
|
664
1764
|
const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
|
|
665
1765
|
const session = await signCookie(s.clientSecret, "session", { user, exp });
|
|
@@ -674,14 +1774,14 @@ var AmbasadaAuth = class {
|
|
|
674
1774
|
/**
|
|
675
1775
|
* Verifies a shell token (Clerk JWT template `ambasada-miniapp`, RS256, 60 s) against
|
|
676
1776
|
* `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json` (cached 1 h): signature, expiry, issuer, and that its `aud`
|
|
677
|
-
* is this app (its id or origin). Returns who it is, or throws `
|
|
1777
|
+
* is this app (its id or origin). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`.
|
|
678
1778
|
*/
|
|
679
1779
|
async verifyShellToken(token, options = {}) {
|
|
680
1780
|
const c = this.config();
|
|
681
|
-
if (!c.issuer) throw new
|
|
1781
|
+
if (!c.issuer) throw new AmbasadaError("CONFIG", "silent sign-in needs AMBASADA_AUTH_ISSUER (injected on ambasada.dev and by `ambasada dev`)");
|
|
682
1782
|
const trusted = [c.issuer, c.hubUrl].filter((v) => !!v).map((v) => v.replace(/\/+$/, ""));
|
|
683
1783
|
const audiences = [options.audience ?? c.audiences ?? []].flat().filter(Boolean).map((a) => a.replace(/\/+$/, ""));
|
|
684
|
-
if (!audiences.length) throw new
|
|
1784
|
+
if (!audiences.length) throw new AmbasadaError("CONFIG", "silent sign-in needs to know this app: set AMBASADA_APP_ORIGIN or PUBLIC_URL, or pass { audience }");
|
|
685
1785
|
const doFetch = c.fetch ?? ((input, init) => fetch(input, init));
|
|
686
1786
|
const parts = typeof token === "string" ? token.split(".") : [];
|
|
687
1787
|
if (parts.length !== 3) throw shellRefused("not a JWT");
|
|
@@ -725,14 +1825,14 @@ var AmbasadaAuth = class {
|
|
|
725
1825
|
*/
|
|
726
1826
|
async shell(req) {
|
|
727
1827
|
const s = this.settings(req);
|
|
728
|
-
if (req.method !== "POST") throw new
|
|
1828
|
+
if (req.method !== "POST") throw new AmbasadaError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
|
|
729
1829
|
let token = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "")?.[1]?.trim();
|
|
730
1830
|
if (!token) {
|
|
731
|
-
if (!(req.headers.get("content-type") ?? "").includes("application/json")) throw new
|
|
1831
|
+
if (!(req.headers.get("content-type") ?? "").includes("application/json")) throw new AmbasadaError("INVALID_REQUEST", "send the token as JSON { token }", 415);
|
|
732
1832
|
const body = await req.json().catch(() => null);
|
|
733
1833
|
token = typeof body?.token === "string" ? body.token : void 0;
|
|
734
1834
|
}
|
|
735
|
-
if (!token) throw new
|
|
1835
|
+
if (!token) throw new AmbasadaError("INVALID_REQUEST", "no shell token", 400);
|
|
736
1836
|
const audiences = [...this.config().audiences ?? [], s.origin];
|
|
737
1837
|
const who = await this.verifyShellToken(token, { audience: audiences });
|
|
738
1838
|
let user = { id: who.user_id, name: who.name, avatar_url: who.avatar, builder: false, builder_since: null, member_since: null, pass: null };
|
|
@@ -763,57 +1863,238 @@ var AmbasadaAuth = class {
|
|
|
763
1863
|
}
|
|
764
1864
|
};
|
|
765
1865
|
|
|
766
|
-
// ../sdk/src/
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
const value = key === void 0 ? void 0 : record[key];
|
|
772
|
-
return Array.isArray(value) ? value[0] ?? null : value ?? null;
|
|
1866
|
+
// ../sdk/src/client.ts
|
|
1867
|
+
var DEFAULT_API_URL = "https://api.ambasada.dev";
|
|
1868
|
+
function processEnv() {
|
|
1869
|
+
const p = globalThis.process;
|
|
1870
|
+
return p?.env ?? {};
|
|
773
1871
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1872
|
+
var Ambasada = class {
|
|
1873
|
+
options;
|
|
1874
|
+
keyLoader;
|
|
1875
|
+
projectPromise;
|
|
1876
|
+
constructor(options = {}) {
|
|
1877
|
+
this.options = options;
|
|
779
1878
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (!secret) throw new WebhookInvalidError("bad_secret", "no webhook secret: set AMBASADA_WEBHOOK_SECRET or pass it");
|
|
784
|
-
const plain = plainHeaders(headers);
|
|
785
|
-
let parsed;
|
|
786
|
-
try {
|
|
787
|
-
parsed = await verifyWebhook(secret, plain, body, options);
|
|
788
|
-
} catch (error) {
|
|
789
|
-
if (error instanceof WebhookError) throw new WebhookInvalidError(error.reason, error.message);
|
|
790
|
-
if (error instanceof SyntaxError) throw new WebhookInvalidError("bad_body", "webhook body is not JSON");
|
|
791
|
-
if (error instanceof DOMException || error instanceof TypeError) throw new WebhookInvalidError("bad_secret", "webhook secret is not whsec_<base64>");
|
|
792
|
-
throw error;
|
|
1879
|
+
env(name) {
|
|
1880
|
+
const v = this.options.env?.[name];
|
|
1881
|
+
return typeof v === "string" ? v : processEnv()[name];
|
|
793
1882
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
// ../sdk/src/receipt.ts
|
|
798
|
-
var RECEIPT_HEADER2 = "x-village-receipt";
|
|
799
|
-
async function readIncoming(req) {
|
|
800
|
-
if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
|
|
801
|
-
const r = req;
|
|
802
|
-
return { headers: r.headers, body: new Uint8Array(await r.clone().arrayBuffer()) };
|
|
1883
|
+
get apiUrl() {
|
|
1884
|
+
return (this.options.apiUrl ?? this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL") ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
803
1885
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
throw new VillageError(
|
|
809
|
-
"CONFIG",
|
|
810
|
-
"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)."
|
|
811
|
-
);
|
|
1886
|
+
get apiKey() {
|
|
1887
|
+
const key = this.options.apiKey ?? (this.env("AMBASADA_API_KEY") ?? this.env("VILLAGE_API_KEY"));
|
|
1888
|
+
if (!key) throw new AmbasadaError("CONFIG", "no API key: set AMBASADA_API_KEY (a project key from developer.ambasada.dev, or `ambasada login`) or pass { apiKey }");
|
|
1889
|
+
return key;
|
|
812
1890
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1891
|
+
/**
|
|
1892
|
+
* Sign in with Ambasada (docs/AUTH.md): `login(req)`, `callback(req)`, `user(req)`, `logout(req)`.
|
|
1893
|
+
* Credentials come with the deploy to ambasada.dev, and from `ambasada dev` locally.
|
|
1894
|
+
*/
|
|
1895
|
+
auth = new AmbasadaAuth(() => ({
|
|
1896
|
+
clientId: this.options.auth?.clientId ?? this.env("AMBASADA_AUTH_CLIENT_ID"),
|
|
1897
|
+
clientSecret: this.options.auth?.clientSecret ?? this.env("AMBASADA_AUTH_CLIENT_SECRET"),
|
|
1898
|
+
issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
|
|
1899
|
+
publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
|
|
1900
|
+
apiUrl: this.apiUrl,
|
|
1901
|
+
// Silent sign-in: a shell token must be for this app, by id or origin.
|
|
1902
|
+
audiences: [this.appOrigin, this.options.app?.id ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY")].filter((a) => !!a),
|
|
1903
|
+
hubUrl: this.hubUrl,
|
|
1904
|
+
fetch: this.options.fetch
|
|
1905
|
+
}));
|
|
1906
|
+
get appOrigin() {
|
|
1907
|
+
const url = this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL");
|
|
1908
|
+
return url && URL.canParse(url) ? new URL(url).origin : void 0;
|
|
1909
|
+
}
|
|
1910
|
+
get hubUrl() {
|
|
1911
|
+
return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* The mini-app platform (docs/MINI-APPS.md §7): `verifyWebhook(req)` for the hub's app events and
|
|
1915
|
+
* `notify({ … })` to push to people who turned notifications on. Uses `AMBASADA_APP_SECRET`.
|
|
1916
|
+
*/
|
|
1917
|
+
apps = new AmbasadaApps(() => ({
|
|
1918
|
+
appSecret: this.options.app?.secret ?? this.env("AMBASADA_APP_SECRET"),
|
|
1919
|
+
hubUrl: this.hubUrl,
|
|
1920
|
+
origin: this.appOrigin,
|
|
1921
|
+
fetch: this.options.fetch
|
|
1922
|
+
}));
|
|
1923
|
+
// ── HTTP ──────────────────────────────────────────────────────────────────
|
|
1924
|
+
async send(method, path, opts = {}) {
|
|
1925
|
+
const url = new URL(this.apiUrl + path);
|
|
1926
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
|
|
1927
|
+
const headers = { accept: "application/json" };
|
|
1928
|
+
if (opts.auth !== false) headers.authorization = `Bearer ${this.apiKey}`;
|
|
1929
|
+
let body = opts.raw;
|
|
1930
|
+
if (opts.body !== void 0) {
|
|
1931
|
+
body = JSON.stringify(opts.body);
|
|
1932
|
+
headers["content-type"] = "application/json";
|
|
1933
|
+
}
|
|
1934
|
+
for (const [k, v] of Object.entries(opts.headers ?? {})) if (v !== void 0) headers[k.toLowerCase()] = v;
|
|
1935
|
+
const doFetch = this.options.fetch ?? fetch;
|
|
1936
|
+
const attempt = () => {
|
|
1937
|
+
const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 45e3);
|
|
1938
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
|
|
1939
|
+
return doFetch(url, { method, headers, body, signal });
|
|
1940
|
+
};
|
|
1941
|
+
let res;
|
|
1942
|
+
try {
|
|
1943
|
+
res = await attempt();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (opts.signal?.aborted) throw error;
|
|
1946
|
+
if (!opts.retry) throw new AmbasadaError("NETWORK", `${method} ${url.pathname}: ${error.message}`, 0);
|
|
1947
|
+
try {
|
|
1948
|
+
res = await attempt();
|
|
1949
|
+
} catch (again) {
|
|
1950
|
+
throw new AmbasadaError("NETWORK", `${method} ${url.pathname}: ${again.message}`, 0);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
const text = await res.text();
|
|
1954
|
+
let json = text;
|
|
1955
|
+
if (text && (res.headers.get("content-type") ?? "").includes("json")) {
|
|
1956
|
+
try {
|
|
1957
|
+
json = JSON.parse(text);
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (!res.ok) throw errorFromResponse(res.status, json, `${method} ${url.pathname} \u2192 ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
|
|
1962
|
+
return { status: res.status, headers: res.headers, json };
|
|
1963
|
+
}
|
|
1964
|
+
async call(method, path, opts) {
|
|
1965
|
+
return (await this.send(method, path, opts)).json;
|
|
1966
|
+
}
|
|
1967
|
+
// ── identity, keys ────────────────────────────────────────────────────────
|
|
1968
|
+
/** Your app's id (from options, `AMBASADA_APP_ID`, or the project the API key belongs to). */
|
|
1969
|
+
project() {
|
|
1970
|
+
const known = this.options.project ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY");
|
|
1971
|
+
if (known) return Promise.resolve(known);
|
|
1972
|
+
this.projectPromise ??= this.call("GET", "/v1/key").then(
|
|
1973
|
+
(k) => k.id,
|
|
1974
|
+
(error) => {
|
|
1975
|
+
this.projectPromise = void 0;
|
|
1976
|
+
throw error;
|
|
1977
|
+
}
|
|
1978
|
+
);
|
|
1979
|
+
return this.projectPromise;
|
|
1980
|
+
}
|
|
1981
|
+
/** The API's signing keys (`/.well-known/jwks.json`, cached). */
|
|
1982
|
+
keys() {
|
|
1983
|
+
const given = this.options.keys;
|
|
1984
|
+
if (Array.isArray(given)) return Promise.resolve(given);
|
|
1985
|
+
if (typeof given === "function") return given();
|
|
1986
|
+
this.keyLoader ??= cachedKeys(() => this.call("GET", "/.well-known/jwks.json", { auth: false }).then((r) => r.keys));
|
|
1987
|
+
return this.keyLoader();
|
|
1988
|
+
}
|
|
1989
|
+
// ── storage ───────────────────────────────────────────────────────────────
|
|
1990
|
+
files = {
|
|
1991
|
+
/**
|
|
1992
|
+
* Stores bytes, a Blob, or a URL to copy (`POST /v1/files`). Returns a public URL. For an app's own
|
|
1993
|
+
* uploads prefer its bucket, `env.FILES` (docs/STORAGE.md).
|
|
1994
|
+
*/
|
|
1995
|
+
put: (data, options = {}) => {
|
|
1996
|
+
const query = { ttl_seconds: options.ttlSeconds };
|
|
1997
|
+
if (typeof data === "string") return this.call("POST", "/v1/files", { body: { url: data, content_type: options.contentType }, query });
|
|
1998
|
+
const contentType = options.contentType ?? (data instanceof Blob && data.type ? data.type : "application/octet-stream");
|
|
1999
|
+
return this.call("POST", "/v1/files", { raw: data, headers: { "content-type": contentType }, query });
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
/**
|
|
2003
|
+
* Your project's JSON key-value store (`/v1/store`): 1 MB per project (keys + values), free. For more,
|
|
2004
|
+
* use the app's own D1, KV and R2 (docs/STORAGE.md).
|
|
2005
|
+
*/
|
|
2006
|
+
store = {
|
|
2007
|
+
/** The value, or null when the key is not set. */
|
|
2008
|
+
get: async (key) => {
|
|
2009
|
+
try {
|
|
2010
|
+
return (await this.call("GET", `/v1/store/${encodeURIComponent(key)}`)).value;
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
if (error instanceof AmbasadaError && error.status === 404) return null;
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
/** Saves any JSON value (fails with INVALID_REQUEST when the store would pass 1 MB). */
|
|
2017
|
+
set: (key, value) => {
|
|
2018
|
+
if (value === void 0) throw new AmbasadaError("INVALID_REQUEST", "store.set needs a JSON value (use store.delete to remove a key)");
|
|
2019
|
+
return this.call("PUT", `/v1/store/${encodeURIComponent(key)}`, { body: value });
|
|
2020
|
+
},
|
|
2021
|
+
/** `set` under its other name. */
|
|
2022
|
+
put: (key, value) => this.store.set(key, value),
|
|
2023
|
+
/** True when the key existed. */
|
|
2024
|
+
delete: async (key) => (await this.call("DELETE", `/v1/store/${encodeURIComponent(key)}`)).deleted,
|
|
2025
|
+
/** Keys (up to 1000, optionally under a prefix) and how much of the 1 MB is used. */
|
|
2026
|
+
list: (prefix) => this.call("GET", "/v1/store", { query: { prefix } })
|
|
2027
|
+
};
|
|
2028
|
+
};
|
|
2029
|
+
function cachedKeys(load, ttlMs = 10 * 6e4) {
|
|
2030
|
+
let cached;
|
|
2031
|
+
return () => {
|
|
2032
|
+
if (!cached || Date.now() - cached.at > ttlMs) {
|
|
2033
|
+
const keys3 = load();
|
|
2034
|
+
const entry = { at: Date.now(), keys: keys3 };
|
|
2035
|
+
cached = entry;
|
|
2036
|
+
keys3.catch(() => {
|
|
2037
|
+
if (cached === entry) cached = void 0;
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
return cached.keys;
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function createAmbasada(options = {}) {
|
|
2044
|
+
return new Ambasada(options);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// ../sdk/src/headers.ts
|
|
2048
|
+
function getHeader(headers, name) {
|
|
2049
|
+
if (typeof headers.get === "function") return headers.get(name);
|
|
2050
|
+
const record = headers;
|
|
2051
|
+
const key = Object.keys(record).find((k) => k.toLowerCase() === name);
|
|
2052
|
+
const value = key === void 0 ? void 0 : record[key];
|
|
2053
|
+
return Array.isArray(value) ? value[0] ?? null : value ?? null;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// ../sdk/src/define.ts
|
|
2057
|
+
function defineApp(config) {
|
|
2058
|
+
if (typeof config.id !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(config.id)) throw new Error(`app id "${String(config.id)}" must be a lowercase slug (a-z, 0-9, -)`);
|
|
2059
|
+
return config;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// ../sdk/src/platform.ts
|
|
2063
|
+
var ambasada = new Ambasada();
|
|
2064
|
+
var platform_default = ambasada;
|
|
2065
|
+
|
|
2066
|
+
// ../village/src/errors.ts
|
|
2067
|
+
var VillageError2 = AmbasadaError;
|
|
2068
|
+
var isVillageError = isAmbasadaError;
|
|
2069
|
+
var ReceiptInvalidError = class extends AmbasadaError {
|
|
2070
|
+
reason;
|
|
2071
|
+
constructor(reason, message) {
|
|
2072
|
+
super("RECEIPT_INVALID", `payment required: ${message}`, 402, { reason });
|
|
2073
|
+
this.name = "ReceiptInvalidError";
|
|
2074
|
+
this.reason = reason;
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
// ../village/src/receipt.ts
|
|
2079
|
+
var RECEIPT_HEADER2 = "x-village-receipt";
|
|
2080
|
+
async function readIncoming(req) {
|
|
2081
|
+
if (typeof req.clone === "function" && typeof req.arrayBuffer === "function") {
|
|
2082
|
+
const r = req;
|
|
2083
|
+
return { headers: r.headers, body: new Uint8Array(await r.clone().arrayBuffer()) };
|
|
2084
|
+
}
|
|
2085
|
+
const node = req;
|
|
2086
|
+
const raw = toBytes2(node.rawBody) ?? toBytes2(node.body);
|
|
2087
|
+
if (raw) return { headers: node.headers, body: raw };
|
|
2088
|
+
if (node.body !== void 0 && node.body !== null && !(typeof node.body === "object" && Object.keys(node.body).length === 0)) {
|
|
2089
|
+
throw new AmbasadaError(
|
|
2090
|
+
"CONFIG",
|
|
2091
|
+
"the request body was already parsed as JSON; receipts commit to the raw bytes. Use express.raw({ type: '*/*' }) on paid routes (or keep req.rawBody)."
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
if (typeof node[Symbol.asyncIterator] === "function") {
|
|
2095
|
+
const chunks = [];
|
|
2096
|
+
for await (const chunk of node) chunks.push(toBytes2(chunk) ?? new Uint8Array());
|
|
2097
|
+
const body = concat(chunks);
|
|
817
2098
|
node.rawBody = body;
|
|
818
2099
|
return { headers: node.headers, body };
|
|
819
2100
|
}
|
|
@@ -879,13 +2160,37 @@ function concat(chunks) {
|
|
|
879
2160
|
return out;
|
|
880
2161
|
}
|
|
881
2162
|
|
|
882
|
-
// ../
|
|
883
|
-
var
|
|
884
|
-
var
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
2163
|
+
// ../village/src/webhook.ts
|
|
2164
|
+
var { WebhookError: WebhookError2, verifyWebhook: verifyProtocolWebhook } = src_exports;
|
|
2165
|
+
var WEBHOOK_HEADERS2 = WEBHOOK_HEADERS;
|
|
2166
|
+
var generateWebhookSecret2 = generateWebhookSecret;
|
|
2167
|
+
var webhookHeaders2 = webhookHeaders;
|
|
2168
|
+
function plainHeaders(headers) {
|
|
2169
|
+
const out = {};
|
|
2170
|
+
for (const name of ["webhook-id", "webhook-timestamp", "webhook-signature"]) {
|
|
2171
|
+
const v = getHeader(headers, name);
|
|
2172
|
+
if (v !== null) out[name] = v;
|
|
2173
|
+
}
|
|
2174
|
+
return out;
|
|
888
2175
|
}
|
|
2176
|
+
async function verifyWebhookPayload(secret, headers, body, options = {}) {
|
|
2177
|
+
if (!secret) throw new WebhookInvalidError("bad_secret", "no webhook secret: set AMBASADA_WEBHOOK_SECRET or pass it");
|
|
2178
|
+
const plain = plainHeaders(headers);
|
|
2179
|
+
let parsed;
|
|
2180
|
+
try {
|
|
2181
|
+
parsed = await verifyProtocolWebhook(secret, plain, body, options);
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
if (error instanceof WebhookError2) throw new WebhookInvalidError(error.reason, error.message);
|
|
2184
|
+
if (error instanceof SyntaxError) throw new WebhookInvalidError("bad_body", "webhook body is not JSON");
|
|
2185
|
+
if (error instanceof DOMException || error instanceof TypeError) throw new WebhookInvalidError("bad_secret", "webhook secret is not whsec_<base64>");
|
|
2186
|
+
throw error;
|
|
2187
|
+
}
|
|
2188
|
+
return { id: plain["webhook-id"], type: String(parsed.type), timestamp: String(parsed.timestamp ?? ""), data: parsed.data };
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// ../village/src/client.ts
|
|
2192
|
+
var DEFAULT_VILLAGE_API_URL = "https://hackaton.ambasada.id";
|
|
2193
|
+
var DEFAULT_MODEL = "deepseek-v4.1-flash";
|
|
889
2194
|
function randomKey() {
|
|
890
2195
|
return `sdk_${crypto.randomUUID()}`;
|
|
891
2196
|
}
|
|
@@ -894,111 +2199,28 @@ function num(value) {
|
|
|
894
2199
|
const n = Number(value);
|
|
895
2200
|
return Number.isFinite(n) ? n : null;
|
|
896
2201
|
}
|
|
897
|
-
function splitOffering(
|
|
898
|
-
const [company, name, ...rest] =
|
|
899
|
-
if (!company || !name || rest.length) throw new
|
|
2202
|
+
function splitOffering(offering3) {
|
|
2203
|
+
const [company, name, ...rest] = offering3.split("/");
|
|
2204
|
+
if (!company || !name || rest.length) throw new AmbasadaError("INVALID_REQUEST", `offering must be "company/name", got "${offering3}"`);
|
|
900
2205
|
return [company, name];
|
|
901
2206
|
}
|
|
902
|
-
var Village = class {
|
|
903
|
-
|
|
904
|
-
keyLoader;
|
|
2207
|
+
var Village = class extends Ambasada {
|
|
2208
|
+
villageOptions;
|
|
905
2209
|
companyPromise;
|
|
906
2210
|
replays = new ReplayGuard();
|
|
907
2211
|
constructor(options = {}) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
env(name) {
|
|
911
|
-
const v = this.options.env?.[name];
|
|
912
|
-
return typeof v === "string" ? v : processEnv()[name];
|
|
2212
|
+
super(options);
|
|
2213
|
+
this.villageOptions = options;
|
|
913
2214
|
}
|
|
914
2215
|
get apiUrl() {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const key = this.options.apiKey ?? (this.env("AMBASADA_API_KEY") ?? this.env("VILLAGE_API_KEY"));
|
|
919
|
-
if (!key) throw new VillageError("CONFIG", "no API key: set AMBASADA_API_KEY (from the village app, or `ambasada login`) or pass { apiKey }");
|
|
920
|
-
return key;
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Sign in with Ambasada (docs/AUTH.md): `login(req)`, `callback(req)`, `user(req)`, `logout(req)`.
|
|
924
|
-
* Credentials come with the deploy to ambasada.dev, and from `ambasada dev` locally.
|
|
925
|
-
*/
|
|
926
|
-
auth = new AmbasadaAuth(() => ({
|
|
927
|
-
clientId: this.options.auth?.clientId ?? this.env("AMBASADA_AUTH_CLIENT_ID"),
|
|
928
|
-
clientSecret: this.options.auth?.clientSecret ?? this.env("AMBASADA_AUTH_CLIENT_SECRET"),
|
|
929
|
-
issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
|
|
930
|
-
publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
|
|
931
|
-
apiUrl: this.apiUrl,
|
|
932
|
-
// Silent sign-in: a shell token must be for this app, by id or origin.
|
|
933
|
-
audiences: [this.appOrigin, this.options.app?.id ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY")].filter((a) => !!a),
|
|
934
|
-
hubUrl: this.hubUrl,
|
|
935
|
-
fetch: this.options.fetch
|
|
936
|
-
}));
|
|
937
|
-
get appOrigin() {
|
|
938
|
-
const url = this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL");
|
|
939
|
-
return url && URL.canParse(url) ? new URL(url).origin : void 0;
|
|
2216
|
+
const shared = this.env("AMBASADA_API_URL") ?? this.env("VILLAGE_API_URL");
|
|
2217
|
+
const legacy = shared && !/^https:\/\/api\.ambasada\.dev\/?$/.test(shared) ? shared : void 0;
|
|
2218
|
+
return (this.villageOptions.apiUrl ?? this.env("AMBASADA_VILLAGE_API_URL") ?? legacy ?? DEFAULT_VILLAGE_API_URL).replace(/\/+$/, "");
|
|
940
2219
|
}
|
|
941
|
-
|
|
942
|
-
return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* The mini-app platform (docs/MINI-APPS.md §7): `verifyWebhook(req)` for the hub's app events and
|
|
946
|
-
* `notify({ … })` to push to people who turned notifications on. Uses `AMBASADA_APP_SECRET`.
|
|
947
|
-
*/
|
|
948
|
-
apps = new AmbasadaApps(() => ({
|
|
949
|
-
appSecret: this.options.app?.secret ?? this.env("AMBASADA_APP_SECRET"),
|
|
950
|
-
hubUrl: this.hubUrl,
|
|
951
|
-
origin: this.appOrigin,
|
|
952
|
-
fetch: this.options.fetch
|
|
953
|
-
}));
|
|
954
|
-
// ── HTTP ──────────────────────────────────────────────────────────────────
|
|
955
|
-
async send(method, path, opts = {}) {
|
|
956
|
-
const url = new URL(this.apiUrl + path);
|
|
957
|
-
for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
|
|
958
|
-
const headers = { accept: "application/json" };
|
|
959
|
-
if (opts.auth !== false) headers.authorization = `Bearer ${this.apiKey}`;
|
|
960
|
-
let body = opts.raw;
|
|
961
|
-
if (opts.body !== void 0) {
|
|
962
|
-
body = JSON.stringify(opts.body);
|
|
963
|
-
headers["content-type"] = "application/json";
|
|
964
|
-
}
|
|
965
|
-
for (const [k, v] of Object.entries(opts.headers ?? {})) if (v !== void 0) headers[k.toLowerCase()] = v;
|
|
966
|
-
const doFetch = this.options.fetch ?? fetch;
|
|
967
|
-
const attempt = () => {
|
|
968
|
-
const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 45e3);
|
|
969
|
-
const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
|
|
970
|
-
return doFetch(url, { method, headers, body, signal });
|
|
971
|
-
};
|
|
972
|
-
let res;
|
|
973
|
-
try {
|
|
974
|
-
res = await attempt();
|
|
975
|
-
} catch (error) {
|
|
976
|
-
if (opts.signal?.aborted) throw error;
|
|
977
|
-
if (!opts.retry) throw new VillageError("NETWORK", `${method} ${url.pathname}: ${error.message}`, 0);
|
|
978
|
-
try {
|
|
979
|
-
res = await attempt();
|
|
980
|
-
} catch (again) {
|
|
981
|
-
throw new VillageError("NETWORK", `${method} ${url.pathname}: ${again.message}`, 0);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
const text = await res.text();
|
|
985
|
-
let json = text;
|
|
986
|
-
if (text && (res.headers.get("content-type") ?? "").includes("json")) {
|
|
987
|
-
try {
|
|
988
|
-
json = JSON.parse(text);
|
|
989
|
-
} catch {
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
if (!res.ok) throw errorFromResponse(res.status, json, `${method} ${url.pathname} \u2192 ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
|
|
993
|
-
return { status: res.status, headers: res.headers, json };
|
|
994
|
-
}
|
|
995
|
-
async call(method, path, opts) {
|
|
996
|
-
return (await this.send(method, path, opts)).json;
|
|
997
|
-
}
|
|
998
|
-
// ── identity, keys ────────────────────────────────────────────────────────
|
|
2220
|
+
// ── identity ──────────────────────────────────────────────────────────────
|
|
999
2221
|
/** Your company id (from options, `AMBASADA_COMPANY`, or the wallet the API key belongs to). */
|
|
1000
2222
|
company() {
|
|
1001
|
-
const known = this.
|
|
2223
|
+
const known = this.villageOptions.company ?? (this.env("AMBASADA_COMPANY") ?? this.env("VILLAGE_COMPANY"));
|
|
1002
2224
|
if (known) return Promise.resolve(known);
|
|
1003
2225
|
this.companyPromise ??= this.wallet().then(
|
|
1004
2226
|
(w) => w.account,
|
|
@@ -1009,14 +2231,6 @@ var Village = class {
|
|
|
1009
2231
|
);
|
|
1010
2232
|
return this.companyPromise;
|
|
1011
2233
|
}
|
|
1012
|
-
/** Village receipt keys (cached). */
|
|
1013
|
-
keys() {
|
|
1014
|
-
const given = this.options.keys;
|
|
1015
|
-
if (Array.isArray(given)) return Promise.resolve(given);
|
|
1016
|
-
if (typeof given === "function") return given();
|
|
1017
|
-
this.keyLoader ??= cachedKeys(() => this.call("GET", "/.well-known/jwks.json", { auth: false }).then((r) => r.keys));
|
|
1018
|
-
return this.keyLoader();
|
|
1019
|
-
}
|
|
1020
2234
|
keySource() {
|
|
1021
2235
|
return () => this.keys();
|
|
1022
2236
|
}
|
|
@@ -1052,26 +2266,26 @@ var Village = class {
|
|
|
1052
2266
|
}
|
|
1053
2267
|
/**
|
|
1054
2268
|
* Wraps a paid offering as a fetch handler: verifies the receipt (402 if missing or bad), runs
|
|
1055
|
-
* `handler(input, call, req)` and answers JSON. A thrown `
|
|
2269
|
+
* `handler(input, call, req)` and answers JSON. A thrown `AmbasadaError` keeps its status (the
|
|
1056
2270
|
* Gateway then refunds the buyer).
|
|
1057
2271
|
*
|
|
1058
2272
|
* export default village.sell("translate", async ({ text, to }) => ({ text: await village.llm(`…`) }));
|
|
1059
2273
|
*/
|
|
1060
|
-
sell(
|
|
2274
|
+
sell(offering3, handler) {
|
|
1061
2275
|
return async (req) => {
|
|
1062
2276
|
if (req.method !== "POST") return Response.json({ error: { code: "INVALID_REQUEST", message: "POST only" } }, { status: 405 });
|
|
1063
2277
|
let call;
|
|
1064
2278
|
try {
|
|
1065
|
-
call = await this.verifyReceipt(req, { offering:
|
|
2279
|
+
call = await this.verifyReceipt(req, { offering: offering3 });
|
|
1066
2280
|
} catch (error) {
|
|
1067
|
-
if (error instanceof
|
|
2281
|
+
if (error instanceof AmbasadaError) return error.toResponse();
|
|
1068
2282
|
throw error;
|
|
1069
2283
|
}
|
|
1070
2284
|
try {
|
|
1071
2285
|
const out = await handler(call.input, call, req);
|
|
1072
2286
|
return out instanceof Response ? out : Response.json(out);
|
|
1073
2287
|
} catch (error) {
|
|
1074
|
-
if (error instanceof
|
|
2288
|
+
if (error instanceof AmbasadaError) return error.toResponse();
|
|
1075
2289
|
console.error(error);
|
|
1076
2290
|
return Response.json({ error: { code: "INTERNAL", message: "handler failed" } }, { status: 500 });
|
|
1077
2291
|
}
|
|
@@ -1080,7 +2294,7 @@ var Village = class {
|
|
|
1080
2294
|
/** Verifies a signed webhook (`checkout.paid`, `webhook.test`) and returns the event. Throws `WebhookInvalidError`. */
|
|
1081
2295
|
async verifyWebhook(req, secret) {
|
|
1082
2296
|
const { headers, body } = await readIncoming(req);
|
|
1083
|
-
return verifyWebhookPayload(secret ?? this.
|
|
2297
|
+
return verifyWebhookPayload(secret ?? this.villageOptions.webhookSecret ?? (this.env("AMBASADA_WEBHOOK_SECRET") ?? this.env("VILLAGE_WEBHOOK_SECRET")) ?? "", headers, body);
|
|
1084
2298
|
}
|
|
1085
2299
|
/** Verifies a checkout receipt (pay.js `village:paid` → `e.detail.receipt`, or `session.receipt`). */
|
|
1086
2300
|
async verifyCheckoutReceipt(token, options = {}) {
|
|
@@ -1128,8 +2342,8 @@ var Village = class {
|
|
|
1128
2342
|
}
|
|
1129
2343
|
// ── buying (machines) ─────────────────────────────────────────────────────
|
|
1130
2344
|
/** Buys one call of `company/offering` through the Gateway. Money moves only if the seller answers 2xx with valid output. */
|
|
1131
|
-
async buy(
|
|
1132
|
-
const [company, name] = splitOffering(
|
|
2345
|
+
async buy(offering3, input, options = {}) {
|
|
2346
|
+
const [company, name] = splitOffering(offering3);
|
|
1133
2347
|
return this.paid(`/v1/pay/${encodeURIComponent(company)}/${encodeURIComponent(name)}`, input, options);
|
|
1134
2348
|
}
|
|
1135
2349
|
/**
|
|
@@ -1208,42 +2422,6 @@ var Village = class {
|
|
|
1208
2422
|
};
|
|
1209
2423
|
}
|
|
1210
2424
|
};
|
|
1211
|
-
files = {
|
|
1212
|
-
/**
|
|
1213
|
-
* Stores bytes, a Blob, or a URL to copy, for the event (`POST /v1/files`, 0.1 TON). Returns a
|
|
1214
|
-
* public URL other products can fetch.
|
|
1215
|
-
*/
|
|
1216
|
-
put: (data, options = {}) => {
|
|
1217
|
-
const query = { ttl_seconds: options.ttlSeconds };
|
|
1218
|
-
if (typeof data === "string") return this.call("POST", "/v1/files", { body: { url: data, content_type: options.contentType }, query });
|
|
1219
|
-
const contentType = options.contentType ?? (data instanceof Blob && data.type ? data.type : "application/octet-stream");
|
|
1220
|
-
return this.call("POST", "/v1/files", { raw: data, headers: { "content-type": contentType }, query });
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
|
-
/**
|
|
1224
|
-
* Your company's JSON key-value store (`/v1/store`): 1 MB per company (keys + values), free. State for
|
|
1225
|
-
* a hosted product that needs no database.
|
|
1226
|
-
*/
|
|
1227
|
-
store = {
|
|
1228
|
-
/** The value, or null when the key is not set. */
|
|
1229
|
-
get: async (key) => {
|
|
1230
|
-
try {
|
|
1231
|
-
return (await this.call("GET", `/v1/store/${encodeURIComponent(key)}`)).value;
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
if (error instanceof VillageError && error.status === 404) return null;
|
|
1234
|
-
throw error;
|
|
1235
|
-
}
|
|
1236
|
-
},
|
|
1237
|
-
/** Saves any JSON value (fails with INVALID_REQUEST when the store would pass 1 MB). */
|
|
1238
|
-
set: (key, value) => {
|
|
1239
|
-
if (value === void 0) throw new VillageError("INVALID_REQUEST", "store.set needs a JSON value (use store.delete to remove a key)");
|
|
1240
|
-
return this.call("PUT", `/v1/store/${encodeURIComponent(key)}`, { body: value });
|
|
1241
|
-
},
|
|
1242
|
-
/** True when the key existed. */
|
|
1243
|
-
delete: async (key) => (await this.call("DELETE", `/v1/store/${encodeURIComponent(key)}`)).deleted,
|
|
1244
|
-
/** Keys (up to 1000, optionally under a prefix) and how much of the 1 MB is used. */
|
|
1245
|
-
list: (prefix) => this.call("GET", "/v1/store", { query: { prefix } })
|
|
1246
|
-
};
|
|
1247
2425
|
// ── listings, wallet, ledger ──────────────────────────────────────────────
|
|
1248
2426
|
listings = {
|
|
1249
2427
|
/** Creates or updates an offering (price changes are versioned). */
|
|
@@ -1279,26 +2457,14 @@ var Village = class {
|
|
|
1279
2457
|
state: () => this.call("GET", "/v1/agent/state")
|
|
1280
2458
|
};
|
|
1281
2459
|
};
|
|
1282
|
-
function
|
|
1283
|
-
let cached;
|
|
1284
|
-
return () => {
|
|
1285
|
-
if (!cached || Date.now() - cached.at > ttlMs) {
|
|
1286
|
-
const keys3 = load();
|
|
1287
|
-
const entry = { at: Date.now(), keys: keys3 };
|
|
1288
|
-
cached = entry;
|
|
1289
|
-
keys3.catch(() => {
|
|
1290
|
-
if (cached === entry) cached = void 0;
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
return cached.keys;
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
function createAmbasada(options = {}) {
|
|
2460
|
+
function createVillage(options = {}) {
|
|
1297
2461
|
return new Village(options);
|
|
1298
2462
|
}
|
|
1299
2463
|
|
|
1300
|
-
// ../
|
|
1301
|
-
function defineCompany(
|
|
2464
|
+
// ../village/src/define.ts
|
|
2465
|
+
function defineCompany(app, village3) {
|
|
2466
|
+
const config = { ...app, ...village3 };
|
|
2467
|
+
if (!config.idea || typeof config.idea.title !== "string") throw new Error(`${config.id}: the village needs idea: { title }`);
|
|
1302
2468
|
return config;
|
|
1303
2469
|
}
|
|
1304
2470
|
function offering(def) {
|
|
@@ -1307,39 +2473,66 @@ function offering(def) {
|
|
|
1307
2473
|
return def;
|
|
1308
2474
|
}
|
|
1309
2475
|
|
|
1310
|
-
// ../
|
|
2476
|
+
// ../village/src/index.ts
|
|
1311
2477
|
var village = new Village();
|
|
1312
|
-
|
|
2478
|
+
|
|
2479
|
+
// ../sdk/src/index.ts
|
|
2480
|
+
var village2 = village;
|
|
2481
|
+
var createVillage2 = createVillage;
|
|
2482
|
+
var Village2 = Village;
|
|
2483
|
+
var VillageError3 = VillageError2;
|
|
2484
|
+
var isVillageError2 = isVillageError;
|
|
2485
|
+
var ReceiptInvalidError2 = ReceiptInvalidError;
|
|
2486
|
+
var DEFAULT_MODEL2 = DEFAULT_MODEL;
|
|
2487
|
+
var verifyWebhookPayload2 = verifyWebhookPayload;
|
|
2488
|
+
var webhookHeaders3 = webhookHeaders2;
|
|
2489
|
+
var generateWebhookSecret3 = generateWebhookSecret2;
|
|
2490
|
+
var WEBHOOK_HEADERS3 = WEBHOOK_HEADERS2;
|
|
2491
|
+
var RECEIPT_HEADER3 = RECEIPT_HEADER2;
|
|
2492
|
+
var readIncoming2 = readIncoming;
|
|
2493
|
+
var verifyCallReceipt2 = verifyCallReceipt;
|
|
2494
|
+
var verifyCheckoutReceiptToken2 = verifyCheckoutReceiptToken;
|
|
2495
|
+
var defineCompany2 = defineCompany;
|
|
2496
|
+
var offering2 = offering;
|
|
2497
|
+
var DEFAULT_VILLAGE_API_URL2 = DEFAULT_VILLAGE_API_URL;
|
|
1313
2498
|
export {
|
|
1314
2499
|
APP_SIGNATURE_HEADER2 as APP_SIGNATURE_HEADER,
|
|
1315
2500
|
APP_TIMESTAMP_HEADER2 as APP_TIMESTAMP_HEADER,
|
|
1316
2501
|
AUTH_PATHS,
|
|
2502
|
+
Ambasada,
|
|
1317
2503
|
AmbasadaApps,
|
|
1318
2504
|
AmbasadaAuth,
|
|
2505
|
+
AmbasadaError,
|
|
1319
2506
|
DEFAULT_API_URL,
|
|
1320
|
-
DEFAULT_MODEL,
|
|
1321
|
-
|
|
2507
|
+
DEFAULT_MODEL2 as DEFAULT_MODEL,
|
|
2508
|
+
DEFAULT_VILLAGE_API_URL2 as DEFAULT_VILLAGE_API_URL,
|
|
2509
|
+
ERROR_STATUS2 as ERROR_STATUS,
|
|
1322
2510
|
JWKS_TTL_MS,
|
|
1323
|
-
|
|
1324
|
-
ReceiptInvalidError,
|
|
2511
|
+
RECEIPT_HEADER3 as RECEIPT_HEADER,
|
|
2512
|
+
ReceiptInvalidError2 as ReceiptInvalidError,
|
|
1325
2513
|
SESSION_TTL_S,
|
|
1326
|
-
Village,
|
|
1327
|
-
VillageError,
|
|
1328
|
-
WEBHOOK_HEADERS,
|
|
2514
|
+
Village2 as Village,
|
|
2515
|
+
VillageError3 as VillageError,
|
|
2516
|
+
WEBHOOK_HEADERS3 as WEBHOOK_HEADERS,
|
|
1329
2517
|
WebhookInvalidError,
|
|
2518
|
+
ambasada,
|
|
1330
2519
|
appWebhookHeaders2 as appWebhookHeaders,
|
|
1331
2520
|
createAmbasada,
|
|
1332
|
-
|
|
1333
|
-
|
|
2521
|
+
createVillage2 as createVillage,
|
|
2522
|
+
platform_default as default,
|
|
2523
|
+
defineApp,
|
|
2524
|
+
defineCompany2 as defineCompany,
|
|
1334
2525
|
errorFromResponse,
|
|
1335
|
-
generateWebhookSecret,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
2526
|
+
generateWebhookSecret3 as generateWebhookSecret,
|
|
2527
|
+
getHeader,
|
|
2528
|
+
isAmbasadaError,
|
|
2529
|
+
isVillageError2 as isVillageError,
|
|
2530
|
+
offering2 as offering,
|
|
2531
|
+
readIncoming2 as readIncoming,
|
|
1339
2532
|
signAppWebhook2 as signAppWebhook,
|
|
1340
|
-
verifyCallReceipt,
|
|
1341
|
-
verifyCheckoutReceiptToken,
|
|
1342
|
-
verifyWebhookPayload,
|
|
1343
|
-
village,
|
|
1344
|
-
webhookHeaders
|
|
2533
|
+
verifyCallReceipt2 as verifyCallReceipt,
|
|
2534
|
+
verifyCheckoutReceiptToken2 as verifyCheckoutReceiptToken,
|
|
2535
|
+
verifyWebhookPayload2 as verifyWebhookPayload,
|
|
2536
|
+
village2 as village,
|
|
2537
|
+
webhookHeaders3 as webhookHeaders
|
|
1345
2538
|
};
|