@ambasada/sdk 0.8.11 → 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 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, for the full profile behind a shell token (`/api/apps/profile`). */
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 (Clerk JWT template `ambasada-miniapp`, RS256, 60 s) against
114
- * `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json` (cached 1 h): signature, expiry, issuer, and that its `aud`
115
- * is this app (its id or origin). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`.
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/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
  };
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
  };
@@ -409,13 +411,17 @@ async function jwks(issuer, doFetch, refresh = false) {
409
411
  if (cached && !refresh && Date.now() - cached.at < JWKS_TTL_MS) return cached.keys;
410
412
  if (cached && refresh && Date.now() - cached.at < 6e4) return cached.keys;
411
413
  const res = await doFetch(`${issuer}/.well-known/jwks.json`, { headers: { accept: "application/json" } });
412
- if (!res.ok) throw new AmbasadaError("UPSTREAM_ERROR", `could not read the Ambasada ID keys (${res.status})`, 502);
413
- const body = await res.json();
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(() => ({}));
414
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`);
415
419
  jwksCache.set(issuer, { at: Date.now(), keys: keys3 });
416
420
  return keys3;
417
421
  }
422
+ var hubOf = (c) => (c.hubUrl || DEFAULT_HUB_URL).replace(/\/+$/, "");
418
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);
419
425
  var AmbasadaAuth = class {
420
426
  constructor(config) {
421
427
  this.config = config;
@@ -505,14 +511,15 @@ var AmbasadaAuth = class {
505
511
  return (await verifyCookie(secret, "session", readOurCookie(req, SESSION_COOKIE)))?.user ?? null;
506
512
  }
507
513
  /**
508
- * Verifies a shell token (Clerk JWT template `ambasada-miniapp`, RS256, 60 s) against
509
- * `<AMBASADA_AUTH_ISSUER>/.well-known/jwks.json` (cached 1 h): signature, expiry, issuer, and that its `aud`
510
- * is this app (its id or origin). Returns who it is, or throws `AmbasadaError("UNAUTHORIZED")`.
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.
511
519
  */
512
520
  async verifyShellToken(token, options = {}) {
513
521
  const c = this.config();
514
- if (!c.issuer) throw new AmbasadaError("CONFIG", "silent sign-in needs AMBASADA_AUTH_ISSUER (injected on ambasada.dev and by `ambasada dev`)");
515
- 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(/\/+$/, "")))];
516
523
  const audiences = [options.audience ?? c.audiences ?? []].flat().filter(Boolean).map((a) => a.replace(/\/+$/, ""));
517
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 }");
518
525
  const doFetch = c.fetch ?? ((input, init) => fetch(input, init));
@@ -557,6 +564,7 @@ var AmbasadaAuth = class {
557
564
  * Answers `{ user }`.
558
565
  */
559
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`)");
560
568
  const s = this.settings(req);
561
569
  if (req.method !== "POST") throw new AmbasadaError("INVALID_REQUEST", "POST the shell token as JSON { token }", 405);
562
570
  let token = /^Bearer\s+(.+)$/i.exec(req.headers.get("authorization") ?? "")?.[1]?.trim();
@@ -569,16 +577,13 @@ var AmbasadaAuth = class {
569
577
  const audiences = [...this.config().audiences ?? [], s.origin];
570
578
  const who = await this.verifyShellToken(token, { audience: audiences });
571
579
  let user = { id: who.user_id, name: who.name, avatar_url: who.avatar, builder: false, builder_since: null, member_since: null, pass: null };
572
- const hub = this.config().hubUrl;
573
- if (hub) {
574
- try {
575
- const res = await s.fetch(`${hub.replace(/\/+$/, "")}/api/apps/profile`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } });
576
- if (res.ok) {
577
- const profile = await res.json();
578
- if (profile.id === who.user_id) user = { ...user, ...profile, id: who.user_id };
579
- } else await res.body?.cancel();
580
- } catch {
581
- }
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 {
582
587
  }
583
588
  const exp = Math.floor(Date.now() / 1e3) + SESSION_TTL_S;
584
589
  const session = await signCookie(s.clientSecret, "session", { user, exp });
@@ -598,6 +603,7 @@ var AmbasadaAuth = class {
598
603
 
599
604
  // src/client.ts
600
605
  var DEFAULT_API_URL = "https://api.ambasada.dev";
606
+ var originOf = (url) => url && URL.canParse(url) ? new URL(url).origin : void 0;
601
607
  var isBinding = (v) => typeof v === "object" && v !== null && typeof v.fetch === "function";
602
608
  function processEnv() {
603
609
  const p = globalThis.process;
@@ -648,14 +654,13 @@ var Ambasada = class {
648
654
  issuer: this.options.auth?.issuer ?? this.env("AMBASADA_AUTH_ISSUER"),
649
655
  publicUrl: this.options.auth?.publicUrl ?? this.env("PUBLIC_URL"),
650
656
  apiUrl: this.apiUrl,
651
- // Silent sign-in: a shell token must be for this app, by id or origin.
652
- audiences: [this.appOrigin, this.options.app?.id ?? this.env("AMBASADA_APP_ID") ?? this.env("AMBASADA_COMPANY")].filter((a) => !!a),
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))],
653
659
  hubUrl: this.hubUrl,
654
660
  fetch: this.fetcher
655
661
  }));
656
662
  get appOrigin() {
657
- const url = this.options.app?.origin ?? this.env("AMBASADA_APP_ORIGIN") ?? this.options.auth?.publicUrl ?? this.env("PUBLIC_URL");
658
- 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"));
659
664
  }
660
665
  get hubUrl() {
661
666
  return this.options.app?.hubUrl ?? this.env("AMBASADA_HUB_URL");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ambasada/sdk",
3
- "version": "0.8.11",
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.11"
30
+ "@ambasada/village": "^0.8.12"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.20.5",