@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 +3 -4
- package/dist/index.cjs +69 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +123 -65
- package/dist/index.d.ts +123 -65
- package/dist/index.js +69 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
1673
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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;
|