@productcraft/heimdall 0.1.0 → 0.2.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 CHANGED
@@ -206,11 +206,12 @@ import { jwtVerify } from "jose";
206
206
 
207
207
  const scope = heimdall.consumer("my-app-slug");
208
208
  const { payload } = await jwtVerify(token, scope.jwks.getKey, {
209
- issuer: scope.expectedIssuer,
210
- audience: "my-app",
209
+ issuer: scope.expectedIssuer, // the literal string "heimdall"
211
210
  });
212
211
  ```
213
212
 
213
+ Heimdall Consumer-API tokens are not minted with an `aud` claim — the per-app boundary is enforced by the JWKS, not the issuer string or the audience. If you mint custom tokens with the heimdall key and want to enforce an audience, pass `audience` to `jose.jwtVerify` or `scope.verifyToken(token, { audience: "..." })` per-call.
214
+
214
215
  ### Passport integration
215
216
 
216
217
  Use the companion package [`@productcraft/heimdall-passport`](../heimdall-passport):
@@ -263,8 +264,6 @@ new Heimdall({
263
264
  baseUrl: "https://api.heimdall.example.test",
264
265
  // Custom fetch (undici with retry, mock in tests, ...)
265
266
  fetch: customFetch,
266
- // Expected JWT `aud` claim for `consumer(slug).verifyToken(...)`. Optional.
267
- expectedAudience: "my-app",
268
267
  // JWKS cache TTL — default 10 minutes
269
268
  jwksTtlMs: 10 * 60 * 1000,
270
269
  });
package/dist/index.cjs CHANGED
@@ -1416,7 +1416,8 @@ function getConsumerVerifyControllerVerifyUrl({
1416
1416
  }
1417
1417
  async function consumerVerifyControllerVerify({
1418
1418
  appSlug,
1419
- data
1419
+ data,
1420
+ headers
1420
1421
  }, config = {}) {
1421
1422
  const { client: request = client, ...requestConfig } = config;
1422
1423
  const requestData = data;
@@ -1424,7 +1425,8 @@ async function consumerVerifyControllerVerify({
1424
1425
  method: "POST",
1425
1426
  url: getConsumerVerifyControllerVerifyUrl({ appSlug }).url.toString(),
1426
1427
  data: requestData,
1427
- ...requestConfig
1428
+ ...requestConfig,
1429
+ headers: { ...headers, ...requestConfig.headers }
1428
1430
  });
1429
1431
  return res.data;
1430
1432
  }
@@ -1439,7 +1441,8 @@ function getConsumerVerifyControllerAuthorizeUrl({
1439
1441
  }
1440
1442
  async function consumerVerifyControllerAuthorize({
1441
1443
  appSlug,
1442
- data
1444
+ data,
1445
+ headers
1443
1446
  }, config = {}) {
1444
1447
  const { client: request = client, ...requestConfig } = config;
1445
1448
  const requestData = data;
@@ -1447,7 +1450,8 @@ async function consumerVerifyControllerAuthorize({
1447
1450
  method: "POST",
1448
1451
  url: getConsumerVerifyControllerAuthorizeUrl({ appSlug }).url.toString(),
1449
1452
  data: requestData,
1450
- ...requestConfig
1453
+ ...requestConfig,
1454
+ headers: { ...headers, ...requestConfig.headers }
1451
1455
  });
1452
1456
  return res.data;
1453
1457
  }
@@ -1465,7 +1469,8 @@ function getConsumerVerifyControllerAuthorizeBatchUrl({
1465
1469
  }
1466
1470
  async function consumerVerifyControllerAuthorizeBatch({
1467
1471
  appSlug,
1468
- data
1472
+ data,
1473
+ headers
1469
1474
  }, config = {}) {
1470
1475
  const { client: request = client, ...requestConfig } = config;
1471
1476
  const requestData = data;
@@ -1475,7 +1480,8 @@ async function consumerVerifyControllerAuthorizeBatch({
1475
1480
  appSlug
1476
1481
  }).url.toString(),
1477
1482
  data: requestData,
1478
- ...requestConfig
1483
+ ...requestConfig,
1484
+ headers: { ...headers, ...requestConfig.headers }
1479
1485
  });
1480
1486
  return res.data;
1481
1487
  }
@@ -1656,21 +1662,49 @@ function translateJoseError(err) {
1656
1662
  }
1657
1663
 
1658
1664
  // src/scopes/consumer.ts
1665
+ var HEIMDALL_LEGACY_ISSUER = "heimdall";
1659
1666
  var ConsumerScope = class {
1660
1667
  /** The appSlug bound to this scope. */
1661
1668
  appSlug;
1662
- /** Default iss claim expected on tokens issued by this app's Heimdall instance. */
1669
+ /**
1670
+ * Issuer the Heimdall Consumer API stamps on every token for this
1671
+ * app — the public Heimdall API base joined with the app slug
1672
+ * (e.g. `https://api.heimdall.productcraft.co/acme`). Pin it in
1673
+ * your local verifier so a token minted for another app on the
1674
+ * platform cannot pass.
1675
+ *
1676
+ * `scope.verifyToken` already enforces this for you. Pass it as a
1677
+ * second-position issuer if you're wiring `jose.jwtVerify`,
1678
+ * `passport-jwt`, or PyJWT yourself.
1679
+ */
1663
1680
  expectedIssuer;
1664
- /** Default aud claim. Undefined unless the caller sets it (skip aud check). */
1681
+ /**
1682
+ * Audience the Consumer API stamps on every token for this app —
1683
+ * literally the app slug (e.g. `"acme"`). Pin it in your verifier
1684
+ * the same way as `expectedIssuer`. `scope.verifyToken` enforces
1685
+ * it by default.
1686
+ */
1665
1687
  expectedAudience;
1688
+ /**
1689
+ * Both accepted issuer strings (`expectedIssuer` + the legacy
1690
+ * `'heimdall'` literal). `verifyToken` passes this to jose so tokens
1691
+ * minted before the 2026-05-24 per-app-issuer migration keep
1692
+ * verifying alongside fresh ones — useful for the ~1-hour transition
1693
+ * window per access-token TTL, and the longer session TTL on
1694
+ * refresh tokens.
1695
+ */
1696
+ acceptedIssuers;
1666
1697
  /** jose-compatible JWKS resolver. Drop into `jose.jwtVerify`, passport-jwt, etc. */
1667
1698
  jwks;
1668
1699
  client;
1669
1700
  constructor(appSlug, internals, opts = {}) {
1670
1701
  this.appSlug = appSlug;
1671
1702
  this.client = internals.client;
1672
- this.expectedIssuer = `${internals.baseUrl}/${appSlug}`;
1673
- this.expectedAudience = opts.audience;
1703
+ const apiOrigin = new URL(internals.baseUrl);
1704
+ apiOrigin.pathname = `/${appSlug}`;
1705
+ this.expectedIssuer = apiOrigin.toString().replace(/\/$/, "");
1706
+ this.expectedAudience = appSlug;
1707
+ this.acceptedIssuers = [this.expectedIssuer, HEIMDALL_LEGACY_ISSUER];
1674
1708
  this.jwks = new JwksCache({
1675
1709
  url: new URL(`/${appSlug}/v1/.well-known/jwks.json`, internals.baseUrl),
1676
1710
  ttlMs: opts.jwksTtlMs,
@@ -1686,10 +1720,20 @@ var ConsumerScope = class {
1686
1720
  const res = await this.client({ method, url, data: body, params });
1687
1721
  return res.data;
1688
1722
  }
1689
- /** Verify a Heimdall-issued JWT against this app's JWKS. */
1723
+ /**
1724
+ * Verify a Heimdall-issued JWT against this app's JWKS.
1725
+ *
1726
+ * Checks the signature, expiry, `iss`, and `aud`. Accepts both the
1727
+ * per-app issuer URL (`expectedIssuer`) and the legacy `'heimdall'`
1728
+ * literal during the issuer-migration transition window — callers
1729
+ * who want to refuse legacy tokens can override with
1730
+ * `{ issuer: scope.expectedIssuer }`. Audience defaults to the app
1731
+ * slug (`expectedAudience`); pass `{ audience: false }` (in an
1732
+ * options override) to skip the audience check entirely.
1733
+ */
1690
1734
  verifyToken(token, opts = {}) {
1691
1735
  return verifyHeimdallToken(token, this.jwks.getKey, {
1692
- issuer: this.expectedIssuer,
1736
+ issuer: this.acceptedIssuers,
1693
1737
  audience: this.expectedAudience,
1694
1738
  ...opts
1695
1739
  });
@@ -1802,16 +1846,23 @@ var ConsumerScope = class {
1802
1846
  // (typically called by the customer's backend, not the user agent)
1803
1847
  // ─────────────────────────────────────────────────────────────
1804
1848
  verify = {
1849
+ /**
1850
+ * The kubb-generated client makes `headers.authorization` a required
1851
+ * arg because the server controllers accept the bearer as a fallback
1852
+ * to `body.token`. We stub an empty string here — the HTTP client's
1853
+ * auth middleware overrides whatever's passed with the configured
1854
+ * credential (`PCAuth.apiKey` / `bearer` / `cookie`).
1855
+ */
1805
1856
  verify: (data) => consumerVerifyControllerVerify(
1806
- { appSlug: this.appSlug, data },
1857
+ { appSlug: this.appSlug, data, headers: { authorization: "" } },
1807
1858
  { client: this.client }
1808
1859
  ),
1809
1860
  authorize: (data) => consumerVerifyControllerAuthorize(
1810
- { appSlug: this.appSlug, data },
1861
+ { appSlug: this.appSlug, data, headers: { authorization: "" } },
1811
1862
  { client: this.client }
1812
1863
  ),
1813
1864
  authorizeBatch: (data) => consumerVerifyControllerAuthorizeBatch(
1814
- { appSlug: this.appSlug, data },
1865
+ { appSlug: this.appSlug, data, headers: { authorization: "" } },
1815
1866
  { client: this.client }
1816
1867
  )
1817
1868
  };
@@ -1831,7 +1882,7 @@ function getAppControllerListMyAppsUrl() {
1831
1882
  const res = { method: "GET", url: `/v1/apps` };
1832
1883
  return res;
1833
1884
  }
1834
- async function appControllerListMyApps({ params }, config = {}) {
1885
+ async function appControllerListMyApps({ params } = {}, config = {}) {
1835
1886
  const { client: request = client, ...requestConfig } = config;
1836
1887
  const mappedParams = params ? {
1837
1888
  limit: params.limit,
@@ -1891,7 +1942,7 @@ function getStatsControllerGetMyStatsUrl() {
1891
1942
  const res = { method: "GET", url: `/v1/stats/me` };
1892
1943
  return res;
1893
1944
  }
1894
- async function statsControllerGetMyStats({ params }, config = {}) {
1945
+ async function statsControllerGetMyStats({ params } = {}, config = {}) {
1895
1946
  const { client: request = client, ...requestConfig } = config;
1896
1947
  const mappedParams = params ? { workspace_id: params.workspaceId } : void 0;
1897
1948
  const res = await request({
@@ -1918,7 +1969,6 @@ var Heimdall = class {
1918
1969
  fetch: this.fetch
1919
1970
  });
1920
1971
  this.jwtConfig = {
1921
- audience: config.expectedAudience,
1922
1972
  jwksTtlMs: config.jwksTtlMs
1923
1973
  };
1924
1974
  }
@@ -1981,6 +2031,7 @@ var Heimdall = class {
1981
2031
 
1982
2032
  exports.AppScope = AppScope;
1983
2033
  exports.ConsumerScope = ConsumerScope;
2034
+ exports.HEIMDALL_LEGACY_ISSUER = HEIMDALL_LEGACY_ISSUER;
1984
2035
  exports.Heimdall = Heimdall;
1985
2036
  exports.HeimdallHttpError = HeimdallHttpError;
1986
2037
  exports.JwksCache = JwksCache;