@hearth-auth/node 0.0.1
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/admin.d.ts +83 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +184 -0
- package/dist/admin.js.map +1 -0
- package/dist/admin.test.d.ts +2 -0
- package/dist/admin.test.d.ts.map +1 -0
- package/dist/admin.test.js +239 -0
- package/dist/admin.test.js.map +1 -0
- package/dist/authorize.d.ts +35 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +68 -0
- package/dist/authorize.js.map +1 -0
- package/dist/authorize.test.d.ts +2 -0
- package/dist/authorize.test.d.ts.map +1 -0
- package/dist/authorize.test.js +93 -0
- package/dist/authorize.test.js.map +1 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +51 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +36 -0
- package/dist/config.test.js.map +1 -0
- package/dist/discovery.d.ts +22 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +60 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +2 -0
- package/dist/discovery.test.d.ts.map +1 -0
- package/dist/discovery.test.js +77 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/errors.d.ts +120 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +172 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +89 -0
- package/dist/errors.test.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect.d.ts +37 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +72 -0
- package/dist/introspect.js.map +1 -0
- package/dist/introspect.test.d.ts +2 -0
- package/dist/introspect.test.d.ts.map +1 -0
- package/dist/introspect.test.js +109 -0
- package/dist/introspect.test.js.map +1 -0
- package/dist/jwks.d.ts +26 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +106 -0
- package/dist/jwks.js.map +1 -0
- package/dist/jwks.test.d.ts +7 -0
- package/dist/jwks.test.d.ts.map +1 -0
- package/dist/jwks.test.js +154 -0
- package/dist/jwks.test.js.map +1 -0
- package/dist/middleware.d.ts +61 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +228 -0
- package/dist/middleware.js.map +1 -0
- package/dist/middleware.mode.test.d.ts +2 -0
- package/dist/middleware.mode.test.d.ts.map +1 -0
- package/dist/middleware.mode.test.js +203 -0
- package/dist/middleware.mode.test.js.map +1 -0
- package/dist/middleware.test.d.ts +2 -0
- package/dist/middleware.test.d.ts.map +1 -0
- package/dist/middleware.test.js +144 -0
- package/dist/middleware.test.js.map +1 -0
- package/dist/token.d.ts +68 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +111 -0
- package/dist/token.js.map +1 -0
- package/dist/token.test.d.ts +2 -0
- package/dist/token.test.d.ts.map +1 -0
- package/dist/token.test.js +135 -0
- package/dist/token.test.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { IntrospectionClient } from "./introspect.js";
|
|
3
|
+
import { IntrospectionError } from "./errors.js";
|
|
4
|
+
const CONFIG = {
|
|
5
|
+
issuer_url: "https://auth.example.com",
|
|
6
|
+
client_id: "client1",
|
|
7
|
+
client_secret: "secret1",
|
|
8
|
+
audience: [],
|
|
9
|
+
jwks_ttl: 300_000,
|
|
10
|
+
introspection_endpoint: "https://auth.example.com/introspect",
|
|
11
|
+
http_timeout: 10_000,
|
|
12
|
+
clock_skew_seconds: 60,
|
|
13
|
+
realm_id: null,
|
|
14
|
+
authorize_endpoint: null,
|
|
15
|
+
};
|
|
16
|
+
const DISCOVERY = {
|
|
17
|
+
issuer: "https://auth.example.com",
|
|
18
|
+
jwks_uri: "https://auth.example.com/jwks",
|
|
19
|
+
introspection_endpoint: "https://auth.example.com/introspect",
|
|
20
|
+
};
|
|
21
|
+
function makeClient(overrides = {}) {
|
|
22
|
+
return new IntrospectionClient({ ...CONFIG, ...overrides }, async () => DISCOVERY);
|
|
23
|
+
}
|
|
24
|
+
describe("IntrospectionClient", () => {
|
|
25
|
+
afterEach(() => vi.restoreAllMocks());
|
|
26
|
+
it("returns active IntrospectionResult with all required fields", async () => {
|
|
27
|
+
const raw = {
|
|
28
|
+
active: true,
|
|
29
|
+
sub: "user123",
|
|
30
|
+
iss: "https://auth.example.com",
|
|
31
|
+
aud: ["api.example.com"],
|
|
32
|
+
exp: 1_700_003_600,
|
|
33
|
+
iat: 1_700_000_000,
|
|
34
|
+
scope: "openid profile",
|
|
35
|
+
custom_field: "value",
|
|
36
|
+
};
|
|
37
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
38
|
+
ok: true,
|
|
39
|
+
json: async () => raw,
|
|
40
|
+
});
|
|
41
|
+
const result = await makeClient().introspect("token123");
|
|
42
|
+
expect(result.active).toBe(true);
|
|
43
|
+
expect(result.sub).toBe("user123");
|
|
44
|
+
expect(result.iss).toBe("https://auth.example.com");
|
|
45
|
+
expect(result.aud).toEqual(["api.example.com"]);
|
|
46
|
+
expect(result.exp).toBe(1_700_003_600);
|
|
47
|
+
expect(result.iat).toBe(1_700_000_000);
|
|
48
|
+
expect(result.scope).toBe("openid profile");
|
|
49
|
+
expect(result.extra.custom_field).toBe("value");
|
|
50
|
+
});
|
|
51
|
+
it("returns inactive result when active=false", async () => {
|
|
52
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
53
|
+
ok: true,
|
|
54
|
+
json: async () => ({ active: false }),
|
|
55
|
+
});
|
|
56
|
+
const result = await makeClient().introspect("dead-token");
|
|
57
|
+
expect(result.active).toBe(false);
|
|
58
|
+
expect(result.sub).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
it("uses configured introspection_endpoint without discovery", async () => {
|
|
61
|
+
const spy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
json: async () => ({ active: true }),
|
|
64
|
+
});
|
|
65
|
+
await makeClient().introspect("tok");
|
|
66
|
+
expect(spy).toHaveBeenCalledWith("https://auth.example.com/introspect", expect.objectContaining({ method: "POST" }));
|
|
67
|
+
});
|
|
68
|
+
it("discovers introspection endpoint when not configured", async () => {
|
|
69
|
+
const spy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: async () => ({ active: true }),
|
|
72
|
+
});
|
|
73
|
+
const client = new IntrospectionClient({ ...CONFIG, introspection_endpoint: null }, async () => DISCOVERY);
|
|
74
|
+
await client.introspect("tok");
|
|
75
|
+
expect(spy.mock.calls[0][0]).toBe("https://auth.example.com/introspect");
|
|
76
|
+
});
|
|
77
|
+
it("throws IntrospectionError on non-OK HTTP response", async () => {
|
|
78
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
79
|
+
ok: false,
|
|
80
|
+
status: 401,
|
|
81
|
+
});
|
|
82
|
+
await expect(makeClient().introspect("tok")).rejects.toBeInstanceOf(IntrospectionError);
|
|
83
|
+
});
|
|
84
|
+
it("throws IntrospectionError on network failure", async () => {
|
|
85
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("ECONNREFUSED"));
|
|
86
|
+
await expect(makeClient().introspect("tok")).rejects.toBeInstanceOf(IntrospectionError);
|
|
87
|
+
});
|
|
88
|
+
it("throws IntrospectionError on invalid JSON response", async () => {
|
|
89
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
90
|
+
ok: true,
|
|
91
|
+
json: async () => { throw new Error("bad json"); },
|
|
92
|
+
});
|
|
93
|
+
await expect(makeClient().introspect("tok")).rejects.toBeInstanceOf(IntrospectionError);
|
|
94
|
+
});
|
|
95
|
+
it("throws IntrospectionError when endpoint missing from discovery", async () => {
|
|
96
|
+
const client = new IntrospectionClient({ ...CONFIG, introspection_endpoint: null }, async () => ({ issuer: "https://auth.example.com", jwks_uri: "https://auth.example.com/jwks" }));
|
|
97
|
+
await expect(client.introspect("tok")).rejects.toBeInstanceOf(IntrospectionError);
|
|
98
|
+
});
|
|
99
|
+
it("passes token_type_hint when provided", async () => {
|
|
100
|
+
const spy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
101
|
+
ok: true,
|
|
102
|
+
json: async () => ({ active: true }),
|
|
103
|
+
});
|
|
104
|
+
await makeClient().introspect("tok", "refresh_token");
|
|
105
|
+
const body = spy.mock.calls[0][1]?.body;
|
|
106
|
+
expect(String(body)).toContain("token_type_hint=refresh_token");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=introspect.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspect.test.js","sourceRoot":"","sources":["../src/introspect.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAIjD,MAAM,MAAM,GAAmB;IAC7B,UAAU,EAAE,0BAA0B;IACtC,SAAS,EAAE,SAAS;IACpB,aAAa,EAAE,SAAS;IACxB,QAAQ,EAAE,EAAE;IACZ,QAAQ,EAAE,OAAO;IACjB,sBAAsB,EAAE,qCAAqC;IAC7D,YAAY,EAAE,MAAM;IACpB,kBAAkB,EAAE,EAAE;IACtB,QAAQ,EAAE,IAAI;IACd,kBAAkB,EAAE,IAAI;CACzB,CAAC;AAEF,MAAM,SAAS,GAAkB;IAC/B,MAAM,EAAE,0BAA0B;IAClC,QAAQ,EAAE,+BAA+B;IACzC,sBAAsB,EAAE,qCAAqC;CAC9D,CAAC;AAEF,SAAS,UAAU,CAAC,YAAqC,EAAE;IACzD,OAAO,IAAI,mBAAmB,CAC5B,EAAE,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,EAC3B,KAAK,IAAI,EAAE,CAAC,SAAS,CACtB,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;IAEtC,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,GAAG,GAAG;YACV,MAAM,EAAE,IAAI;YACZ,GAAG,EAAE,SAAS;YACd,GAAG,EAAE,0BAA0B;YAC/B,GAAG,EAAE,CAAC,iBAAiB,CAAC;YACxB,GAAG,EAAE,aAAa;YAClB,GAAG,EAAE,aAAa;YAClB,KAAK,EAAE,gBAAgB;YACvB,YAAY,EAAE,OAAO;SACtB,CAAC;QACF,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC9C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,GAAG;SACV,CAAC,CAAC;QAEf,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACzD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC9C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;SAC1B,CAAC,CAAC;QACf,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC1D,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SACzB,CAAC,CAAC;QACf,MAAM,UAAU,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,CAAC,oBAAoB,CAC9B,qCAAqC,EACrC,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAC5C,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC1D,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SACzB,CAAC,CAAC;QACf,MAAM,MAAM,GAAG,IAAI,mBAAmB,CACpC,EAAE,GAAG,MAAM,EAAE,sBAAsB,EAAE,IAAI,EAAE,EAC3C,KAAK,IAAI,EAAE,CAAC,SAAS,CACtB,CAAC;QACF,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC9C,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACA,CAAC,CAAC;QACf,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3E,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC9C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;SAC5B,CAAC,CAAC;QAC1B,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,MAAM,GAAG,IAAI,mBAAmB,CACpC,EAAE,GAAG,MAAM,EAAE,sBAAsB,EAAE,IAAI,EAAE,EAC3C,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,0BAA0B,EAAE,QAAQ,EAAE,+BAA+B,EAAE,CAAC,CAChG,CAAC;QACF,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC;YAC1D,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SACzB,CAAC,CAAC;QACf,MAAM,UAAU,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/jwks.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** §2 — JWKS-backed token verification with cache-control, background refresh, and 401 re-fetch. */
|
|
2
|
+
import type { JWSHeaderParameters, FlattenedJWSInput, GetKeyFunction } from "jose";
|
|
3
|
+
import { DiscoveryClient } from "./discovery.js";
|
|
4
|
+
import { VerifiedToken } from "./token.js";
|
|
5
|
+
import type { ResolvedConfig } from "./config.js";
|
|
6
|
+
/** Key resolver type — the common base accepted by jwtVerify. */
|
|
7
|
+
type JwkKeyArg = GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput>;
|
|
8
|
+
/** Testability hook: override how the JWK set is built (e.g. use createLocalJWKSet in tests). */
|
|
9
|
+
export type JwkSetFactory = (jwksUri: string, ttlMs: number) => JwkKeyArg;
|
|
10
|
+
export declare class JwksVerifier {
|
|
11
|
+
private readonly discovery;
|
|
12
|
+
private remoteJwkSet;
|
|
13
|
+
private readonly config;
|
|
14
|
+
private refreshTimer;
|
|
15
|
+
private readonly jwkSetFactory;
|
|
16
|
+
constructor(config: ResolvedConfig, discovery?: DiscoveryClient, jwkSetFactory?: JwkSetFactory);
|
|
17
|
+
private buildJwkSet;
|
|
18
|
+
private scheduleBackgroundRefresh;
|
|
19
|
+
private getJwkSet;
|
|
20
|
+
/** Verify a JWT using the JWKS endpoint. Supports RS256 and ES256. */
|
|
21
|
+
verifyToken(token: string): Promise<VerifiedToken>;
|
|
22
|
+
/** Force JWKS cache eviction (e.g. on receiving a 401 from a resource server). */
|
|
23
|
+
invalidateCache(): void;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=jwks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../src/jwks.ts"],"names":[],"mappings":"AAAA,oGAAoG;AAGpG,OAAO,KAAK,EAAyC,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAC1H,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,iEAAiE;AACjE,KAAK,SAAS,GAAG,cAAc,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC;AAExE,iGAAiG;AACjG,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;AAE1E,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;gBAElC,MAAM,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,eAAe,EAAE,aAAa,CAAC,EAAE,aAAa;YAQhF,WAAW;IAgBzB,OAAO,CAAC,yBAAyB;YAcnB,SAAS;IAOvB,sEAAsE;IAChE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAoDxD,kFAAkF;IAClF,eAAe,IAAI,IAAI;CAQxB"}
|
package/dist/jwks.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** §2 — JWKS-backed token verification with cache-control, background refresh, and 401 re-fetch. */
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify, errors as joseErrors } from "jose";
|
|
3
|
+
import { DiscoveryClient } from "./discovery.js";
|
|
4
|
+
import { JWKSFetchError, TokenVerificationError, TokenExpiredError, TokenClaimsError } from "./errors.js";
|
|
5
|
+
import { VerifiedToken } from "./token.js";
|
|
6
|
+
export class JwksVerifier {
|
|
7
|
+
discovery;
|
|
8
|
+
remoteJwkSet = null;
|
|
9
|
+
config;
|
|
10
|
+
refreshTimer = null;
|
|
11
|
+
jwkSetFactory;
|
|
12
|
+
constructor(config, discovery, jwkSetFactory) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.discovery = discovery ?? new DiscoveryClient(config.issuer_url, config.http_timeout);
|
|
15
|
+
this.jwkSetFactory = jwkSetFactory ?? ((uri, ttl) => createRemoteJWKSet(new URL(uri), { cacheMaxAge: ttl, cooldownDuration: 30_000 }));
|
|
16
|
+
}
|
|
17
|
+
async buildJwkSet() {
|
|
18
|
+
let jwksUri;
|
|
19
|
+
try {
|
|
20
|
+
const doc = await this.discovery.discover();
|
|
21
|
+
jwksUri = doc.jwks_uri;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof JWKSFetchError)
|
|
25
|
+
throw err;
|
|
26
|
+
throw new JWKSFetchError("Failed to discover JWKS URI", { cause: err });
|
|
27
|
+
}
|
|
28
|
+
const cacheMaxAge = Math.min(this.config.jwks_ttl, 24 * 60 * 60 * 1000);
|
|
29
|
+
const jwkSet = this.jwkSetFactory(jwksUri, cacheMaxAge);
|
|
30
|
+
this.scheduleBackgroundRefresh(cacheMaxAge);
|
|
31
|
+
return jwkSet;
|
|
32
|
+
}
|
|
33
|
+
scheduleBackgroundRefresh(ttlMs) {
|
|
34
|
+
if (this.refreshTimer)
|
|
35
|
+
clearTimeout(this.refreshTimer);
|
|
36
|
+
// Background refresh at 80% of TTL to warm cache before expiry
|
|
37
|
+
const delay = Math.max(ttlMs * 0.8, 60_000);
|
|
38
|
+
this.refreshTimer = setTimeout(() => {
|
|
39
|
+
this.remoteJwkSet = null;
|
|
40
|
+
// Fire-and-forget: re-prime the JWK set; errors are silently swallowed
|
|
41
|
+
// to avoid crashing background timers. The next verify() call will retry.
|
|
42
|
+
this.getJwkSet().catch(() => undefined);
|
|
43
|
+
}, delay);
|
|
44
|
+
// Don't block process exit
|
|
45
|
+
if (this.refreshTimer.unref)
|
|
46
|
+
this.refreshTimer.unref();
|
|
47
|
+
}
|
|
48
|
+
async getJwkSet() {
|
|
49
|
+
if (!this.remoteJwkSet) {
|
|
50
|
+
this.remoteJwkSet = await this.buildJwkSet();
|
|
51
|
+
}
|
|
52
|
+
return this.remoteJwkSet;
|
|
53
|
+
}
|
|
54
|
+
/** Verify a JWT using the JWKS endpoint. Supports RS256 and ES256. */
|
|
55
|
+
async verifyToken(token) {
|
|
56
|
+
const jwkSet = await this.getJwkSet();
|
|
57
|
+
const verifyOptions = {
|
|
58
|
+
issuer: this.config.issuer_url,
|
|
59
|
+
clockTolerance: this.config.clock_skew_seconds,
|
|
60
|
+
algorithms: ["RS256", "ES256", "RS384", "ES384", "RS512", "ES512", "EdDSA"],
|
|
61
|
+
};
|
|
62
|
+
if (this.config.audience.length > 0) {
|
|
63
|
+
verifyOptions.audience = this.config.audience;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const result = await jwtVerify(token, jwkSet, verifyOptions);
|
|
67
|
+
return new VerifiedToken(result.payload, result.protectedHeader);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof joseErrors.JWTExpired) {
|
|
71
|
+
const expiredAt = err.payload?.exp ? new Date(err.payload.exp * 1000) : new Date(0);
|
|
72
|
+
throw new TokenExpiredError(expiredAt, { cause: err });
|
|
73
|
+
}
|
|
74
|
+
if (err instanceof joseErrors.JWKSNoMatchingKey ||
|
|
75
|
+
err instanceof joseErrors.JWKSMultipleMatchingKeys) {
|
|
76
|
+
// JWKS key not found — re-fetch once and retry (handles key rotation / 401-like scenario)
|
|
77
|
+
this.remoteJwkSet = null;
|
|
78
|
+
const freshSet = await this.getJwkSet().catch((e) => {
|
|
79
|
+
throw new JWKSFetchError("JWKS re-fetch after key miss failed", { cause: e });
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const result = await jwtVerify(token, freshSet, verifyOptions);
|
|
83
|
+
return new VerifiedToken(result.payload, result.protectedHeader);
|
|
84
|
+
}
|
|
85
|
+
catch (retryErr) {
|
|
86
|
+
throw new TokenVerificationError("Token verification failed after JWKS refresh", { cause: retryErr });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (err instanceof joseErrors.JWTClaimValidationFailed ||
|
|
90
|
+
err instanceof joseErrors.JWTInvalid) {
|
|
91
|
+
throw new TokenClaimsError(`Token claim validation failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
92
|
+
}
|
|
93
|
+
throw new TokenVerificationError(`Token verification failed: ${err instanceof Error ? err.message : "unknown error"}`, { cause: err });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Force JWKS cache eviction (e.g. on receiving a 401 from a resource server). */
|
|
97
|
+
invalidateCache() {
|
|
98
|
+
this.remoteJwkSet = null;
|
|
99
|
+
this.discovery.reset();
|
|
100
|
+
if (this.refreshTimer) {
|
|
101
|
+
clearTimeout(this.refreshTimer);
|
|
102
|
+
this.refreshTimer = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=jwks.js.map
|
package/dist/jwks.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.js","sourceRoot":"","sources":["../src/jwks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.test.d.ts","sourceRoot":"","sources":["../src/jwks.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §9 — JWKS verification tests:
|
|
3
|
+
* - key rotation integration (re-fetch after key miss)
|
|
4
|
+
* - clock skew boundary (exp/iat at exact tolerance)
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import * as jose from "jose";
|
|
8
|
+
import { JwksVerifier } from "./jwks.js";
|
|
9
|
+
import { DiscoveryClient } from "./discovery.js";
|
|
10
|
+
import { TokenExpiredError, JWKSFetchError } from "./errors.js";
|
|
11
|
+
import { JWKS_TTL_DEFAULT_MS, HTTP_TIMEOUT_DEFAULT_MS, CLOCK_SKEW_DEFAULT_S } from "./config.js";
|
|
12
|
+
const ISSUER = "https://auth.example.com";
|
|
13
|
+
function makeConfig(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
issuer_url: ISSUER,
|
|
16
|
+
client_id: "test-client",
|
|
17
|
+
client_secret: "test-secret",
|
|
18
|
+
audience: [],
|
|
19
|
+
jwks_ttl: JWKS_TTL_DEFAULT_MS,
|
|
20
|
+
introspection_endpoint: null,
|
|
21
|
+
http_timeout: HTTP_TIMEOUT_DEFAULT_MS,
|
|
22
|
+
clock_skew_seconds: CLOCK_SKEW_DEFAULT_S,
|
|
23
|
+
realm_id: null,
|
|
24
|
+
authorize_endpoint: null,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function generateKeyPair(alg = "RS256") {
|
|
29
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg);
|
|
30
|
+
const kid = `key-${alg}-${Date.now()}`;
|
|
31
|
+
const jwk = await jose.exportJWK(publicKey);
|
|
32
|
+
return { privateKey, publicKey, kid, jwk: { ...jwk, kid, alg, use: "sig" } };
|
|
33
|
+
}
|
|
34
|
+
async function signToken(payload, privateKey, alg, kid) {
|
|
35
|
+
return new jose.SignJWT(payload)
|
|
36
|
+
.setProtectedHeader({ alg, kid })
|
|
37
|
+
.sign(privateKey);
|
|
38
|
+
}
|
|
39
|
+
/** Build a JwkSetFactory that serves a local (in-memory) JWKS, no network needed. */
|
|
40
|
+
function makeLocalFactory(jwks) {
|
|
41
|
+
return (_uri, _ttl) => jose.createLocalJWKSet(jwks);
|
|
42
|
+
}
|
|
43
|
+
/** Stub DiscoveryClient — never makes network calls. */
|
|
44
|
+
function stubDiscovery() {
|
|
45
|
+
const d = new DiscoveryClient(ISSUER, HTTP_TIMEOUT_DEFAULT_MS);
|
|
46
|
+
vi.spyOn(d, "discover").mockResolvedValue({
|
|
47
|
+
issuer: ISSUER,
|
|
48
|
+
jwks_uri: `${ISSUER}/.well-known/jwks.json`,
|
|
49
|
+
introspection_endpoint: `${ISSUER}/introspect`,
|
|
50
|
+
});
|
|
51
|
+
return d;
|
|
52
|
+
}
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Clock skew boundary tests (§9)
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
describe("JwksVerifier — clock skew boundary (§9)", () => {
|
|
57
|
+
const NOW = 1_700_000_000;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.useFakeTimers();
|
|
60
|
+
vi.setSystemTime(NOW * 1000);
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
vi.useRealTimers();
|
|
64
|
+
vi.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
it("accepts token with exp = now + skew_tolerance (boundary valid)", async () => {
|
|
67
|
+
const kp = await generateKeyPair("RS256");
|
|
68
|
+
// exp is now + clockSkew — still within tolerance (not yet expired)
|
|
69
|
+
const exp = NOW + CLOCK_SKEW_DEFAULT_S;
|
|
70
|
+
const token = await signToken({ sub: "u1", iss: ISSUER, exp, iat: NOW - 10 }, kp.privateKey, "RS256", kp.kid);
|
|
71
|
+
const verifier = new JwksVerifier(makeConfig(), stubDiscovery(), makeLocalFactory({ keys: [kp.jwk] }));
|
|
72
|
+
const verified = await verifier.verifyToken(token);
|
|
73
|
+
expect(verified.subject()).toBe("u1");
|
|
74
|
+
});
|
|
75
|
+
it("rejects token with exp = now - skew_tolerance - 1 (just outside tolerance)", async () => {
|
|
76
|
+
const kp = await generateKeyPair("RS256");
|
|
77
|
+
const exp = NOW - CLOCK_SKEW_DEFAULT_S - 1;
|
|
78
|
+
const token = await signToken({ sub: "u1", iss: ISSUER, exp, iat: NOW - 200 }, kp.privateKey, "RS256", kp.kid);
|
|
79
|
+
const verifier = new JwksVerifier(makeConfig(), stubDiscovery(), makeLocalFactory({ keys: [kp.jwk] }));
|
|
80
|
+
await expect(verifier.verifyToken(token)).rejects.toBeInstanceOf(TokenExpiredError);
|
|
81
|
+
});
|
|
82
|
+
it("accepts token with iat = now + skew_tolerance (future iat within tolerance)", async () => {
|
|
83
|
+
const kp = await generateKeyPair("RS256");
|
|
84
|
+
const iat = NOW + CLOCK_SKEW_DEFAULT_S;
|
|
85
|
+
const exp = NOW + 3600;
|
|
86
|
+
const token = await signToken({ sub: "u1", iss: ISSUER, exp, iat }, kp.privateKey, "RS256", kp.kid);
|
|
87
|
+
const verifier = new JwksVerifier(makeConfig(), stubDiscovery(), makeLocalFactory({ keys: [kp.jwk] }));
|
|
88
|
+
const verified = await verifier.verifyToken(token);
|
|
89
|
+
expect(verified.subject()).toBe("u1");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
// JWKS key rotation integration (§9)
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
describe("JwksVerifier — JWKS key rotation integration (§9)", () => {
|
|
96
|
+
afterEach(() => vi.restoreAllMocks());
|
|
97
|
+
it("re-fetches JWKS on key miss and succeeds with rotated key", async () => {
|
|
98
|
+
const oldKey = await generateKeyPair("RS256");
|
|
99
|
+
const newKey = await generateKeyPair("RS256");
|
|
100
|
+
const NOW_REAL = Math.floor(Date.now() / 1000);
|
|
101
|
+
const token = await signToken({ sub: "u1", iss: ISSUER, exp: NOW_REAL + 3600, iat: NOW_REAL }, newKey.privateKey, "RS256", newKey.kid);
|
|
102
|
+
// First factory call: stale JWKS with only old key; second: rotated JWKS
|
|
103
|
+
let factoryCallCount = 0;
|
|
104
|
+
const rotatingFactory = (_uri, _ttl) => {
|
|
105
|
+
factoryCallCount++;
|
|
106
|
+
const keys = factoryCallCount === 1 ? [oldKey.jwk] : [newKey.jwk];
|
|
107
|
+
return jose.createLocalJWKSet({ keys });
|
|
108
|
+
};
|
|
109
|
+
const verifier = new JwksVerifier(makeConfig(), stubDiscovery(), rotatingFactory);
|
|
110
|
+
// First call: miss on old key → JwksVerifier invalidates and calls factory again
|
|
111
|
+
const verified = await verifier.verifyToken(token);
|
|
112
|
+
expect(verified.subject()).toBe("u1");
|
|
113
|
+
// Should have called factory at least twice (once for old, once for new)
|
|
114
|
+
expect(factoryCallCount).toBeGreaterThanOrEqual(2);
|
|
115
|
+
});
|
|
116
|
+
it("throws JWKSFetchError when OIDC discovery is unreachable", async () => {
|
|
117
|
+
const discovery = stubDiscovery();
|
|
118
|
+
vi.spyOn(discovery, "discover").mockRejectedValue(new Error("ECONNREFUSED"));
|
|
119
|
+
const verifier = new JwksVerifier(makeConfig(), discovery);
|
|
120
|
+
// JWKSFetchError is thrown when discovery/JWKS cannot be reached; it's a HearthError subtype
|
|
121
|
+
await expect(verifier.verifyToken("dummy.token.here")).rejects.toBeInstanceOf(JWKSFetchError);
|
|
122
|
+
});
|
|
123
|
+
it("invalidateCache forces new factory call on next verifyToken", async () => {
|
|
124
|
+
const kp = await generateKeyPair("ES256");
|
|
125
|
+
const NOW_REAL = Math.floor(Date.now() / 1000);
|
|
126
|
+
const token = await signToken({ sub: "u2", iss: ISSUER, exp: NOW_REAL + 3600, iat: NOW_REAL }, kp.privateKey, "ES256", kp.kid);
|
|
127
|
+
let factoryCallCount = 0;
|
|
128
|
+
const countingFactory = (_uri, _ttl) => {
|
|
129
|
+
factoryCallCount++;
|
|
130
|
+
return jose.createLocalJWKSet({ keys: [kp.jwk] });
|
|
131
|
+
};
|
|
132
|
+
const verifier = new JwksVerifier(makeConfig(), stubDiscovery(), countingFactory);
|
|
133
|
+
await verifier.verifyToken(token);
|
|
134
|
+
const afterFirst = factoryCallCount;
|
|
135
|
+
verifier.invalidateCache();
|
|
136
|
+
await verifier.verifyToken(token);
|
|
137
|
+
expect(factoryCallCount).toBeGreaterThan(afterFirst);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Algorithm support
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
describe("JwksVerifier — algorithm support", () => {
|
|
144
|
+
afterEach(() => vi.restoreAllMocks());
|
|
145
|
+
it.each(["RS256", "ES256"])("verifies %s tokens", async (alg) => {
|
|
146
|
+
const kp = await generateKeyPair(alg);
|
|
147
|
+
const NOW_REAL = Math.floor(Date.now() / 1000);
|
|
148
|
+
const token = await signToken({ sub: `user-${alg}`, iss: ISSUER, exp: NOW_REAL + 3600, iat: NOW_REAL }, kp.privateKey, alg, kp.kid);
|
|
149
|
+
const verifier = new JwksVerifier(makeConfig(), stubDiscovery(), makeLocalFactory({ keys: [kp.jwk] }));
|
|
150
|
+
const verified = await verifier.verifyToken(token);
|
|
151
|
+
expect(verified.subject()).toBe(`user-${alg}`);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
//# sourceMappingURL=jwks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.test.js","sourceRoot":"","sources":["../src/jwks.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** §6 — Framework-decoupled Express and Fastify middleware for JWT verification. */
|
|
2
|
+
import type { HearthConfig } from "./config.js";
|
|
3
|
+
import type { VerifiedToken } from "./token.js";
|
|
4
|
+
import type { AccessTokenAuthorizationMode } from "./token.js";
|
|
5
|
+
export interface MiddlewareOptions extends HearthConfig {
|
|
6
|
+
/** If true (default), return 401 when no Bearer token is present. */
|
|
7
|
+
required?: boolean;
|
|
8
|
+
/** If provided, return 403 when the verified token is missing this scope. */
|
|
9
|
+
requiredScope?: string;
|
|
10
|
+
/** If provided, return 403 when the verified token is missing this role. */
|
|
11
|
+
requiredRole?: string;
|
|
12
|
+
/** If provided, return 403 when the verified token is missing this permission. */
|
|
13
|
+
requiredPermission?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Authorization mode to enforce. Defaults to `"embedded"` (JWT claims only).
|
|
16
|
+
*
|
|
17
|
+
* - `"embedded"`: check permissions/roles from JWT claims — no network calls.
|
|
18
|
+
* - `"introspection"`: call `/introspect` for live RBAC data; reject on mode mismatch.
|
|
19
|
+
* - `"decision"`: call `POST /oauth/authorize` per-request; fail-closed on network errors.
|
|
20
|
+
*
|
|
21
|
+
* IMPORTANT: absence of `permissions` in a token MUST NOT silently fall back to
|
|
22
|
+
* a different mode. Set `expectedMode` explicitly for any non-embedded behavior.
|
|
23
|
+
*/
|
|
24
|
+
expectedMode?: AccessTokenAuthorizationMode;
|
|
25
|
+
}
|
|
26
|
+
interface MinimalRequest {
|
|
27
|
+
headers: Record<string, string | string[] | undefined>;
|
|
28
|
+
}
|
|
29
|
+
interface MinimalResponse {
|
|
30
|
+
status(code: number): MinimalResponse;
|
|
31
|
+
setHeader?(name: string, value: string): void;
|
|
32
|
+
json(body: unknown): unknown;
|
|
33
|
+
send?(body: unknown): unknown;
|
|
34
|
+
header?(name: string, value: string): void;
|
|
35
|
+
}
|
|
36
|
+
declare global {
|
|
37
|
+
namespace Express {
|
|
38
|
+
interface Request {
|
|
39
|
+
hearthToken?: VerifiedToken;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
type ExpressRequest = MinimalRequest & {
|
|
44
|
+
hearthToken?: VerifiedToken;
|
|
45
|
+
};
|
|
46
|
+
type NextFn = (err?: unknown) => void;
|
|
47
|
+
/** Express-compatible middleware factory. Attaches verified token to `req.hearthToken`. */
|
|
48
|
+
export declare function hearthMiddleware(options: MiddlewareOptions): (req: ExpressRequest, res: MinimalResponse, next: NextFn) => Promise<void>;
|
|
49
|
+
interface FastifyRequest {
|
|
50
|
+
headers: Record<string, string | undefined>;
|
|
51
|
+
hearthToken?: VerifiedToken;
|
|
52
|
+
}
|
|
53
|
+
interface FastifyReply {
|
|
54
|
+
code(statusCode: number): FastifyReply;
|
|
55
|
+
header(name: string, value: string): FastifyReply;
|
|
56
|
+
send(body: unknown): void;
|
|
57
|
+
}
|
|
58
|
+
/** Fastify hook/plugin factory. Attaches verified token to `request.hearthToken`. */
|
|
59
|
+
export declare function hearthFastifyHook(options: MiddlewareOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,oFAAoF;AAGpF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAK/D,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,4BAA4B,CAAC;CAC7C;AAKD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,UAAU,eAAe;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,SAAS,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7B,IAAI,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5C;AAGD,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,WAAW,CAAC,EAAE,aAAa,CAAC;SAC7B;KACF;CACF;AAED,KAAK,cAAc,GAAG,cAAc,GAAG;IAAE,WAAW,CAAC,EAAE,aAAa,CAAA;CAAE,CAAC;AACvE,KAAK,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AA6FtC,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,IAU3C,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC,CAoDtF;AAID,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,WAAW,CAAC,EAAE,aAAa,CAAC;CAC7B;AAED,UAAU,YAAY;IACpB,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,CAAC;IACvC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IAClD,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3B;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,IAU5C,SAAS,cAAc,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAoE3E"}
|