@ambasada/sdk 0.8.10 → 0.8.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +6 -4
- package/dist/define.d.ts +15 -0
- package/dist/errors.d.ts +7 -1
- package/dist/platform.d.ts +1 -1
- package/dist/platform.js +44 -26
- package/package.json +2 -2
package/dist/auth.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface AuthConfig {
|
|
|
40
40
|
issuer?: string;
|
|
41
41
|
/** Who a shell token must be for (`aud`): your app id and origin. The request's origin is added in `shell()`. */
|
|
42
42
|
audiences?: string[];
|
|
43
|
-
/** The hub,
|
|
43
|
+
/** The hub (default https://hub.ambasada.id): signs shell tokens, and has the full profile behind one (`/api/apps/profile`). */
|
|
44
44
|
hubUrl?: string;
|
|
45
45
|
/** Where `/v1/auth/profile` lives (the Ambasada API). */
|
|
46
46
|
apiUrl: string;
|
|
@@ -110,9 +110,11 @@ export declare class AmbasadaAuth {
|
|
|
110
110
|
/** Whoever is signed in on this request, or null (also null when sign-in is not set up). */
|
|
111
111
|
user(req: WithHeaders): Promise<AmbasadaUser | null>;
|
|
112
112
|
/**
|
|
113
|
-
* Verifies a shell token (
|
|
114
|
-
* `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json
|
|
115
|
-
*
|
|
113
|
+
* Verifies a shell token (RS256, 60 s) from the hub (`<AMBASADA_HUB_URL>/.well-known/jwks.json`, default
|
|
114
|
+
* https://hub.ambasada.id) or Ambasada ID (`<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json`, the Clerk JWT template
|
|
115
|
+
* `ambasada-miniapp`), keys cached 1 h: signature, expiry, issuer, and that its `aud` is this app (its origin or
|
|
116
|
+
* id). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`; `NOT_CONFIGURED` (503) while the issuer
|
|
117
|
+
* publishes no keys.
|
|
116
118
|
*/
|
|
117
119
|
verifyShellToken(token: string, options?: {
|
|
118
120
|
audience?: string | string[];
|
package/dist/define.d.ts
CHANGED
|
@@ -31,6 +31,19 @@ export interface MiniAppConfig {
|
|
|
31
31
|
scopes?: ("identity" | "notifications")[];
|
|
32
32
|
visibility?: "public" | "builders" | "unlisted" | `event:${string}`;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* How the app is listed, whether or not it has a mini app (dry run 7 nit d: with the mini app off there was nowhere
|
|
36
|
+
* for these). The mini app's own `age_rating`, `kind` and `tags` win; these are its defaults, and `miniapp disable`
|
|
37
|
+
* keeps them here.
|
|
38
|
+
*/
|
|
39
|
+
export interface ListingConfig {
|
|
40
|
+
/** Who it is for: "4+", "9+", "12+" or "17+" (the mini app's default "4+"). */
|
|
41
|
+
age_rating?: MiniAppConfig["age_rating"];
|
|
42
|
+
/** What it is: "product", "event", "tool" or "assistant" (the mini app's default "tool"). */
|
|
43
|
+
kind?: MiniAppConfig["kind"];
|
|
44
|
+
/** Up to 5 search words. */
|
|
45
|
+
tags?: string[];
|
|
46
|
+
}
|
|
34
47
|
export interface AppConfig {
|
|
35
48
|
/** The app's slug (lowercase): it lives at https://<id>.ambasada.dev. */
|
|
36
49
|
id: string;
|
|
@@ -46,6 +59,8 @@ export interface AppConfig {
|
|
|
46
59
|
};
|
|
47
60
|
/** The mini app inside the Ambasada app, at a path of this one. Off without it. */
|
|
48
61
|
miniapp?: MiniAppConfig;
|
|
62
|
+
/** age_rating, kind and tags, for an app with or without a mini app (the mini app's own win). */
|
|
63
|
+
listing?: ListingConfig;
|
|
49
64
|
/** Add-ons to load besides the ones found in node_modules (package names, e.g. "@ambasada/village"). */
|
|
50
65
|
addons?: string[];
|
|
51
66
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare const ERROR_STATUS: {
|
|
|
47
47
|
readonly CAP_TOOLS_RATE: 429;
|
|
48
48
|
readonly NETWORK: 0;
|
|
49
49
|
readonly CONFIG: 0;
|
|
50
|
+
/** A platform feature this app relies on is not switched on yet (silent sign-in before the hub has keys). */
|
|
51
|
+
readonly NOT_CONFIGURED: 503;
|
|
50
52
|
readonly RECEIPT_INVALID: 402;
|
|
51
53
|
readonly WEBHOOK_INVALID: 400;
|
|
52
54
|
};
|
|
@@ -63,6 +65,8 @@ export interface ErrorDetails {
|
|
|
63
65
|
upstream_status?: number;
|
|
64
66
|
/** APPROVAL_REQUIRED: retry with `{ approval: approvalId }` once the team approved it. */
|
|
65
67
|
approval_id?: string;
|
|
68
|
+
/** CAP_TOOLS_RATE, TOOL_UNAVAILABLE, a busy seller: seconds until it is worth trying again. */
|
|
69
|
+
retry_after_s?: number;
|
|
66
70
|
[key: string]: unknown;
|
|
67
71
|
}
|
|
68
72
|
export declare class AmbasadaError extends Error {
|
|
@@ -76,6 +80,8 @@ export declare class AmbasadaError extends Error {
|
|
|
76
80
|
get callId(): string | undefined;
|
|
77
81
|
get refunded(): boolean;
|
|
78
82
|
get approvalId(): string | undefined;
|
|
83
|
+
/** Seconds until it is worth trying again (CAP_TOOLS_RATE, TOOL_UNAVAILABLE, a busy seller), when the error says. */
|
|
84
|
+
get retryAfterS(): number | undefined;
|
|
79
85
|
/** The wire shape, e.g. to pass an error on from your own endpoint. */
|
|
80
86
|
toJSON(): {
|
|
81
87
|
error: {
|
|
@@ -83,7 +89,7 @@ export declare class AmbasadaError extends Error {
|
|
|
83
89
|
message: string;
|
|
84
90
|
} & ErrorDetails;
|
|
85
91
|
};
|
|
86
|
-
/** A `Response` with the wire shape and status (402 for receipt problems). */
|
|
92
|
+
/** A `Response` with the wire shape and status (402 for receipt problems), and `Retry-After` when it has one. */
|
|
87
93
|
toResponse(): Response;
|
|
88
94
|
}
|
|
89
95
|
export type WebhookFailure = "missing_headers" | "bad_timestamp" | "too_old" | "too_new" | "bad_signature" | "bad_secret" | "bad_body";
|
package/dist/platform.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { getHeader } from "./headers.js";
|
|
|
11
11
|
export type { HeaderSource } from "./headers.js";
|
|
12
12
|
export { defineApp } from "./define.js";
|
|
13
13
|
export { MINIAPP_AGE_RATINGS, MINIAPP_ENTRY, MINIAPP_KINDS, supportUrl } from "./define.js";
|
|
14
|
-
export type { AppConfig, MiniAppConfig } from "./define.js";
|
|
14
|
+
export type { AppConfig, ListingConfig, MiniAppConfig } from "./define.js";
|
|
15
15
|
export { AmbasadaAuth, AUTH_PATHS, JWKS_TTL_MS, SESSION_TTL_S } from "./auth.js";
|
|
16
16
|
export type { AmbasadaUser, AuthConfig, LoginOptions, ShellUser, WithHeaders } from "./auth.js";
|
|
17
17
|
export { AmbasadaApps, APP_SIGNATURE_HEADER, APP_TIMESTAMP_HEADER, appWebhookHeaders, signAppWebhook } from "./apps.js";
|
package/dist/platform.js
CHANGED
|
@@ -180,6 +180,8 @@ var ERROR_STATUS = {
|
|
|
180
180
|
// SDK-side
|
|
181
181
|
NETWORK: 0,
|
|
182
182
|
CONFIG: 0,
|
|
183
|
+
/** A platform feature this app relies on is not switched on yet (silent sign-in before the hub has keys). */
|
|
184
|
+
NOT_CONFIGURED: 503,
|
|
183
185
|
RECEIPT_INVALID: 402,
|
|
184
186
|
WEBHOOK_INVALID: 400
|
|
185
187
|
};
|
|
@@ -211,13 +213,19 @@ var AmbasadaError = class _AmbasadaError extends Error {
|
|
|
211
213
|
get approvalId() {
|
|
212
214
|
return this.details.approval_id;
|
|
213
215
|
}
|
|
216
|
+
/** Seconds until it is worth trying again (CAP_TOOLS_RATE, TOOL_UNAVAILABLE, a busy seller), when the error says. */
|
|
217
|
+
get retryAfterS() {
|
|
218
|
+
const s = this.details.retry_after_s;
|
|
219
|
+
return typeof s === "number" && Number.isFinite(s) ? s : void 0;
|
|
220
|
+
}
|
|
214
221
|
/** The wire shape, e.g. to pass an error on from your own endpoint. */
|
|
215
222
|
toJSON() {
|
|
216
223
|
return { error: { code: this.code, message: this.message, ...this.details } };
|
|
217
224
|
}
|
|
218
|
-
/** A `Response` with the wire shape and status (402 for receipt problems). */
|
|
225
|
+
/** A `Response` with the wire shape and status (402 for receipt problems), and `Retry-After` when it has one. */
|
|
219
226
|
toResponse() {
|
|
220
|
-
|
|
227
|
+
const retry = this.details.retry_after_s;
|
|
228
|
+
return Response.json(this.toJSON(), { status: this.status || 500, ...typeof retry === "number" ? { headers: { "retry-after": String(Math.ceil(retry)) } } : {} });
|
|
221
229
|
}
|
|
222
230
|
};
|
|
223
231
|
var WebhookInvalidError = class extends AmbasadaError {
|
|
@@ -403,13 +411,17 @@ async function jwks(issuer, doFetch, refresh = false) {
|
|
|
403
411
|
if (cached && !refresh && Date.now() - cached.at < JWKS_TTL_MS) return cached.keys;
|
|
404
412
|
if (cached && refresh && Date.now() - cached.at < 6e4) return cached.keys;
|
|
405
413
|
const res = await doFetch(`${issuer}/.well-known/jwks.json`, { headers: { accept: "application/json" } });
|
|
406
|
-
if (
|
|
407
|
-
|
|
414
|
+
if (res.status === 404) throw notConfigured(`${issuer} publishes no signing keys yet`);
|
|
415
|
+
if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the signing keys of ${issuer} (${res.status})`, 502);
|
|
416
|
+
const body = await res.json().catch(() => ({}));
|
|
408
417
|
const keys3 = (body.keys ?? []).filter((k) => k.kty === "RSA" && k.n && k.e);
|
|
418
|
+
if (!keys3.length) throw notConfigured(`${issuer} publishes no RS256 signing keys yet`);
|
|
409
419
|
jwksCache.set(issuer, { at: Date.now(), keys: keys3 });
|
|
410
420
|
return keys3;
|
|
411
421
|
}
|
|
422
|
+
var hubOf = (c) => (c.hubUrl || DEFAULT_HUB_URL).replace(/\/+$/, "");
|
|
412
423
|
var shellRefused = (message) => new AmbasadaError("UNAUTHORIZED", `shell token refused: ${message}`, 401);
|
|
424
|
+
var notConfigured = (message) => new AmbasadaError("NOT_CONFIGURED", `silent sign-in is not configured: ${message}`, 503);
|
|
413
425
|
var AmbasadaAuth = class {
|
|
414
426
|
constructor(config) {
|
|
415
427
|
this.config = config;
|
|
@@ -499,14 +511,15 @@ var AmbasadaAuth = class {
|
|
|
499
511
|
return (await verifyCookie(secret, "session", readOurCookie(req, SESSION_COOKIE)))?.user ?? null;
|
|
500
512
|
}
|
|
501
513
|
/**
|
|
502
|
-
* Verifies a shell token (
|
|
503
|
-
* `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json
|
|
504
|
-
*
|
|
514
|
+
* Verifies a shell token (RS256, 60 s) from the hub (`<AMBASADA_HUB_URL>/.well-known/jwks.json`, default
|
|
515
|
+
* https://hub.ambasada.id) or Ambasada ID (`<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json`, the Clerk JWT template
|
|
516
|
+
* `ambasada-miniapp`), keys cached 1 h: signature, expiry, issuer, and that its `aud` is this app (its origin or
|
|
517
|
+
* id). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`; `NOT_CONFIGURED` (503) while the issuer
|
|
518
|
+
* publishes no keys.
|
|
505
519
|
*/
|
|
506
520
|
async verifyShellToken(token, options = {}) {
|
|
507
521
|
const c = this.config();
|
|
508
|
-
|
|
509
|
-
const trusted = [c.issuer, c.hubUrl].filter((v) => !!v).map((v) => v.replace(/\/+$/, ""));
|
|
522
|
+
const trusted = [...new Set([c.issuer, hubOf(c)].filter((v) => !!v).map((v) => v.replace(/\/+$/, "")))];
|
|
510
523
|
const audiences = [options.audience ?? c.audiences ?? []].flat().filter(Boolean).map((a) => a.replace(/\/+$/, ""));
|
|
511
524
|
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 }");
|
|
512
525
|
const doFetch = c.fetch ?? ((input, init) => fetch(input, init));
|
|
@@ -551,6 +564,7 @@ var AmbasadaAuth = class {
|
|
|
551
564
|
* Answers `{ user }`.
|
|
552
565
|
*/
|
|
553
566
|
async shell(req) {
|
|
567
|
+
if (!this.configured) throw notConfigured("this app has no Sign in with Ambasada credentials (AMBASADA_AUTH_*: on ambasada.dev after `ambasada deploy`, and under `ambasada dev`)");
|
|
554
568
|
const s = this.settings(req);
|
|
555
569
|
if (req.method !== "POST") throw new AmbasadaError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
|
|
556
570
|
let token = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "")?.[1]?.trim();
|
|
@@ -563,16 +577,13 @@ var AmbasadaAuth = class {
|
|
|
563
577
|
const audiences = [...this.config().audiences ?? [], s.origin];
|
|
564
578
|
const who = await this.verifyShellToken(token, { audience: audiences });
|
|
565
579
|
let user = { id: who.user_id, name: who.name, avatar_url: who.avatar, builder: false, builder_since: null, member_since: null, pass: null };
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
} else await res.body?.cancel();
|
|
574
|
-
} catch {
|
|
575
|
-
}
|
|
580
|
+
try {
|
|
581
|
+
const res = await s.fetch(`${hubOf(this.config())}/api/apps/profile`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } });
|
|
582
|
+
if (res.ok) {
|
|
583
|
+
const profile = await res.json();
|
|
584
|
+
if (profile.id === who.user_id) user = { ...user, ...profile, id: who.user_id };
|
|
585
|
+
} else await res.body?.cancel();
|
|
586
|
+
} catch {
|
|
576
587
|
}
|
|
577
588
|
const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
|
|
578
589
|
const session = await signCookie(s.clientSecret, "session", { user, exp });
|
|
@@ -592,6 +603,7 @@ var AmbasadaAuth = class {
|
|
|
592
603
|
|
|
593
604
|
// src/client.ts
|
|
594
605
|
var DEFAULT_API_URL = "https://api.ambasada.dev";
|
|
606
|
+
var originOf = (url) => url && URL.canParse(url) ? new URL(url).origin : void 0;
|
|
595
607
|
var isBinding = (v) => typeof v === "object" && v !== null && typeof v.fetch === "function";
|
|
596
608
|
function processEnv() {
|
|
597
609
|
const p = globalThis.process;
|
|
@@ -642,14 +654,13 @@ var Ambasada = class {
|
|
|
642
654
|
issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
|
|
643
655
|
publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
|
|
644
656
|
apiUrl: this.apiUrl,
|
|
645
|
-
// Silent sign-in: a shell token must be for this app, by
|
|
646
|
-
audiences: [this.appOrigin, this.options.app?.id
|
|
657
|
+
// Silent sign-in: a shell token must be for this app, by origin (the deploy's and PUBLIC_URL) or id.
|
|
658
|
+
audiences: [...new Set([this.appOrigin, originOf(this.env("PUBLIC_URL")), this.options.app?.id, this.env("AMBASADA_APP_ID"), this.env("AMBASADA_COMPANY")].filter((a) => !!a))],
|
|
647
659
|
hubUrl: this.hubUrl,
|
|
648
660
|
fetch: this.fetcher
|
|
649
661
|
}));
|
|
650
662
|
get appOrigin() {
|
|
651
|
-
|
|
652
|
-
return url && URL.canParse(url) ? new URL(url).origin : void 0;
|
|
663
|
+
return originOf(this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"));
|
|
653
664
|
}
|
|
654
665
|
get hubUrl() {
|
|
655
666
|
return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
|
|
@@ -817,6 +828,11 @@ function supportUrl(value) {
|
|
|
817
828
|
return void 0;
|
|
818
829
|
}
|
|
819
830
|
var MINIAPP_ENTRY = /^\/(?:[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*)?$/;
|
|
831
|
+
function checkListing(id, key, l) {
|
|
832
|
+
if (l.age_rating !== void 0 && !MINIAPP_AGE_RATINGS.includes(l.age_rating)) throw new Error(`${id}: ${key}.age_rating must be one of ${MINIAPP_AGE_RATINGS.join(", ")} (got ${JSON.stringify(l.age_rating)})`);
|
|
833
|
+
if (l.kind !== void 0 && !MINIAPP_KINDS.includes(l.kind)) throw new Error(`${id}: ${key}.kind must be one of ${MINIAPP_KINDS.join(", ")} (got ${JSON.stringify(l.kind)})`);
|
|
834
|
+
if (l.tags !== void 0 && (!Array.isArray(l.tags) || l.tags.length > 5 || l.tags.some((t) => typeof t !== "string"))) throw new Error(`${id}: ${key}.tags must be up to 5 strings`);
|
|
835
|
+
}
|
|
820
836
|
function defineApp(config) {
|
|
821
837
|
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, -)`);
|
|
822
838
|
const m = config.miniapp;
|
|
@@ -827,10 +843,12 @@ function defineApp(config) {
|
|
|
827
843
|
}
|
|
828
844
|
if (m.icon !== void 0 && (typeof m.icon !== "string" || !m.icon.startsWith("/"))) throw new Error(`${config.id}: miniapp.icon must be a path like "/icon.png"`);
|
|
829
845
|
if (m.accent !== void 0 && !/^#[0-9a-fA-F]{6}$/.test(m.accent)) throw new Error(`${config.id}: miniapp.accent must be a #RRGGBB colour`);
|
|
830
|
-
|
|
831
|
-
if (m.kind !== void 0 && !MINIAPP_KINDS.includes(m.kind)) throw new Error(`${config.id}: miniapp.kind must be one of ${MINIAPP_KINDS.join(", ")} (got ${JSON.stringify(m.kind)})`);
|
|
846
|
+
checkListing(config.id, "miniapp", m);
|
|
832
847
|
if (m.support !== void 0 && !supportUrl(m.support)) throw new Error(`${config.id}: miniapp.support must be an email or an https URL (got ${JSON.stringify(m.support)})`);
|
|
833
|
-
|
|
848
|
+
}
|
|
849
|
+
if (config.listing !== void 0) {
|
|
850
|
+
if (!config.listing || typeof config.listing !== "object") throw new Error(`${config.id}: listing must be { age_rating, kind, tags }`);
|
|
851
|
+
checkListing(config.id, "listing", config.listing);
|
|
834
852
|
}
|
|
835
853
|
if (config.support !== void 0 && !supportUrl(config.support)) throw new Error(`${config.id}: support must be an email or an https URL (got ${JSON.stringify(config.support)})`);
|
|
836
854
|
return config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ambasada/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.12",
|
|
4
4
|
"description": "The Ambasada platform SDK: Sign in with Ambasada, mini-app notifications, the project store, files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=22"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@ambasada/village": "^0.8.
|
|
30
|
+
"@ambasada/village": "^0.8.12"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.20.5",
|