@eventuras/fides-auth 0.12.0 → 0.14.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.
Files changed (49) hide show
  1. package/dist/heartbeat.d.ts +20 -2
  2. package/dist/heartbeat.d.ts.map +1 -1
  3. package/dist/heartbeat.js +37 -3
  4. package/dist/heartbeat.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -2
  8. package/dist/oauth-logging.d.ts +15 -0
  9. package/dist/oauth-logging.d.ts.map +1 -1
  10. package/dist/oauth-logging.js +63 -1
  11. package/dist/oauth-logging.js.map +1 -1
  12. package/dist/oauth.d.ts +2 -2
  13. package/dist/oauth.d.ts.map +1 -1
  14. package/dist/oauth.js +9 -6
  15. package/dist/oauth.js.map +1 -1
  16. package/dist/oidc-discovery.d.ts +42 -0
  17. package/dist/oidc-discovery.d.ts.map +1 -0
  18. package/dist/oidc-discovery.js +82 -0
  19. package/dist/oidc-discovery.js.map +1 -0
  20. package/dist/providers/vipps/client.d.ts +6 -3
  21. package/dist/providers/vipps/client.d.ts.map +1 -1
  22. package/dist/providers/vipps/index.js +8 -5
  23. package/dist/providers/vipps/index.js.map +1 -1
  24. package/dist/server/heartbeat-handler.d.ts +10 -3
  25. package/dist/server/heartbeat-handler.d.ts.map +1 -1
  26. package/dist/server/index.d.ts +1 -1
  27. package/dist/server/index.d.ts.map +1 -1
  28. package/dist/server/index.js +182 -53
  29. package/dist/server/index.js.map +1 -1
  30. package/dist/server/oidc-callback.d.ts.map +1 -1
  31. package/dist/server/oidc-logout.d.ts.map +1 -1
  32. package/dist/server/session.d.ts +75 -3
  33. package/dist/server/session.d.ts.map +1 -1
  34. package/dist/session-cookies.d.ts +25 -0
  35. package/dist/session-cookies.d.ts.map +1 -1
  36. package/dist/session-cookies.js +35 -14
  37. package/dist/session-cookies.js.map +1 -1
  38. package/dist/session-events.d.ts +149 -0
  39. package/dist/session-events.d.ts.map +1 -0
  40. package/dist/session-events.js +96 -0
  41. package/dist/session-events.js.map +1 -0
  42. package/dist/silent-login.d.ts.map +1 -1
  43. package/dist/silent-login.js +2 -1
  44. package/dist/silent-login.js.map +1 -1
  45. package/dist/types.d.ts +7 -0
  46. package/dist/types.d.ts.map +1 -1
  47. package/docs/discovery-cache.md +53 -0
  48. package/docs/session-events.md +207 -0
  49. package/package.json +15 -5
@@ -0,0 +1,42 @@
1
+ import { OAuthConfig } from './oauth';
2
+ import * as openid from 'openid-client';
3
+ /** Options for {@link configureDiscoveryCache}. */
4
+ export interface DiscoveryCacheOptions {
5
+ /**
6
+ * How long a discovered configuration stays reusable, in milliseconds.
7
+ * Pass `0` to disable caching and discover on every call. Must be finite and
8
+ * non-negative; anything else throws rather than quietly disabling the cache.
9
+ * @default 300_000 (5 minutes)
10
+ */
11
+ ttlMs?: number;
12
+ }
13
+ /**
14
+ * Adjusts the discovery cache. Existing entries are dropped so the new setting
15
+ * takes effect immediately.
16
+ *
17
+ * @throws TypeError if `ttlMs` is not a finite, non-negative number. Rejecting
18
+ * loudly matters here because the alternative is silent: `NaN` — what
19
+ * `Number(process.env.DISCOVERY_TTL)` yields when the variable is unset — would
20
+ * otherwise turn the cache off and restore a discovery round-trip on every
21
+ * token operation, with nothing to show for it in the logs.
22
+ */
23
+ export declare function configureDiscoveryCache(options: DiscoveryCacheOptions): void;
24
+ /**
25
+ * Drops every cached configuration.
26
+ *
27
+ * Call this after changing provider configuration that discovery would not
28
+ * otherwise pick up until the TTL expires. Tests should call it between cases,
29
+ * since the cache is module state.
30
+ */
31
+ export declare function clearDiscoveryCache(): void;
32
+ /**
33
+ * Discovers the provider's OpenID configuration, reusing a recent result.
34
+ *
35
+ * Every fides-auth flow goes through here, so this is also the single place
36
+ * where the client authentication method is decided.
37
+ *
38
+ * @param config - OAuth configuration identifying the provider and client.
39
+ * @returns The openid-client `Configuration` for this provider and client.
40
+ */
41
+ export declare function discoverConfiguration(config: OAuthConfig): Promise<openid.Configuration>;
42
+ //# sourceMappingURL=oidc-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-discovery.d.ts","sourceRoot":"","sources":["../src/oidc-discovery.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAiC3C,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAU5E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAWD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAgC/B"}
@@ -0,0 +1,82 @@
1
+ import { createLogger } from "./logger.js";
2
+ import * as openid from "openid-client";
3
+ //#region src/oidc-discovery.ts
4
+ var logger = createLogger({ namespace: "fides-auth:oidc-discovery" });
5
+ /**
6
+ * Default lifetime of a cached configuration.
7
+ *
8
+ * Discovery documents change rarely — a new endpoint, a signing-key rollover —
9
+ * and openid-client refetches JWKS on its own schedule, so this only bounds how
10
+ * long a *metadata* change takes to be noticed. Five minutes keeps that window
11
+ * short while removing essentially every discovery request from the hot path.
12
+ */
13
+ var DEFAULT_TTL_MS = 3e5;
14
+ var cache = /* @__PURE__ */ new Map();
15
+ var ttlMs = DEFAULT_TTL_MS;
16
+ /**
17
+ * Adjusts the discovery cache. Existing entries are dropped so the new setting
18
+ * takes effect immediately.
19
+ *
20
+ * @throws TypeError if `ttlMs` is not a finite, non-negative number. Rejecting
21
+ * loudly matters here because the alternative is silent: `NaN` — what
22
+ * `Number(process.env.DISCOVERY_TTL)` yields when the variable is unset — would
23
+ * otherwise turn the cache off and restore a discovery round-trip on every
24
+ * token operation, with nothing to show for it in the logs.
25
+ */
26
+ function configureDiscoveryCache(options) {
27
+ if (options.ttlMs !== void 0) {
28
+ if (!Number.isFinite(options.ttlMs) || options.ttlMs < 0) throw new TypeError(`configureDiscoveryCache: ttlMs must be a finite number >= 0, got ${options.ttlMs}`);
29
+ ttlMs = options.ttlMs;
30
+ }
31
+ cache.clear();
32
+ }
33
+ /**
34
+ * Drops every cached configuration.
35
+ *
36
+ * Call this after changing provider configuration that discovery would not
37
+ * otherwise pick up until the TTL expires. Tests should call it between cases,
38
+ * since the cache is module state.
39
+ */
40
+ function clearDiscoveryCache() {
41
+ cache.clear();
42
+ }
43
+ /**
44
+ * Issuer and client identify a configuration; the secret is checked separately.
45
+ * JSON-encoded rather than concatenated so no separator can be forged by a
46
+ * value that happens to contain it.
47
+ */
48
+ function cacheKey(config) {
49
+ return JSON.stringify([config.issuer, config.clientId]);
50
+ }
51
+ /**
52
+ * Discovers the provider's OpenID configuration, reusing a recent result.
53
+ *
54
+ * Every fides-auth flow goes through here, so this is also the single place
55
+ * where the client authentication method is decided.
56
+ *
57
+ * @param config - OAuth configuration identifying the provider and client.
58
+ * @returns The openid-client `Configuration` for this provider and client.
59
+ */
60
+ async function discoverConfiguration(config) {
61
+ const key = cacheKey(config);
62
+ const now = Date.now();
63
+ const cached = cache.get(key);
64
+ if (cached && cached.expiresAt > now && cached.clientSecret === config.clientSecret) return cached.configuration;
65
+ logger.debug({ issuer: config.issuer }, "Discovering OpenID configuration");
66
+ const configuration = openid.discovery(new URL(config.issuer), config.clientId, config.clientSecret, openid.ClientSecretPost(config.clientSecret));
67
+ if (ttlMs > 0) {
68
+ cache.set(key, {
69
+ configuration,
70
+ expiresAt: now + ttlMs,
71
+ clientSecret: config.clientSecret
72
+ });
73
+ configuration.catch(() => {
74
+ if (cache.get(key)?.configuration === configuration) cache.delete(key);
75
+ });
76
+ }
77
+ return configuration;
78
+ }
79
+ //#endregion
80
+ export { clearDiscoveryCache, configureDiscoveryCache, discoverConfiguration };
81
+
82
+ //# sourceMappingURL=oidc-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc-discovery.js","names":[],"sources":["../src/oidc-discovery.ts"],"sourcesContent":["// oidc-discovery.ts\n//\n// One place where fides-auth turns an OAuthConfig into an openid-client\n// Configuration, and one cache in front of it.\n//\n// Discovery was previously performed inline at every call site, which meant an\n// HTTP GET to the provider's /.well-known/openid-configuration before *every*\n// token operation. On the heartbeat path that is one extra round-trip per user\n// every few minutes: it doubles the latency of a refresh, doubles the load on\n// the provider, and doubles the surface on which a refresh can fail — a\n// discovery blip is indistinguishable from a token-endpoint blip to the caller.\n//\n// Reusing a Configuration is openid-client's intended usage; its own examples\n// build one at startup and keep it.\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport type { OAuthConfig } from './oauth';\n\nconst logger = createLogger({ namespace: 'fides-auth:oidc-discovery' });\n\n/**\n * Default lifetime of a cached configuration.\n *\n * Discovery documents change rarely — a new endpoint, a signing-key rollover —\n * and openid-client refetches JWKS on its own schedule, so this only bounds how\n * long a *metadata* change takes to be noticed. Five minutes keeps that window\n * short while removing essentially every discovery request from the hot path.\n */\nconst DEFAULT_TTL_MS = 5 * 60_000;\n\ninterface CacheEntry {\n /**\n * Held as the promise, not the resolved value, so concurrent callers share one\n * round-trip. Caching only the result would let a burst of requests each start\n * their own discovery before the first finished — precisely what a fleet of\n * heartbeats produces.\n */\n configuration: Promise<openid.Configuration>;\n expiresAt: number;\n /**\n * The secret this entry was built with, compared on lookup so a rotated client\n * secret invalidates the entry without the secret going into the cache key.\n */\n clientSecret: string;\n}\n\nconst cache = new Map<string, CacheEntry>();\nlet ttlMs = DEFAULT_TTL_MS;\n\n/** Options for {@link configureDiscoveryCache}. */\nexport interface DiscoveryCacheOptions {\n /**\n * How long a discovered configuration stays reusable, in milliseconds.\n * Pass `0` to disable caching and discover on every call. Must be finite and\n * non-negative; anything else throws rather than quietly disabling the cache.\n * @default 300_000 (5 minutes)\n */\n ttlMs?: number;\n}\n\n/**\n * Adjusts the discovery cache. Existing entries are dropped so the new setting\n * takes effect immediately.\n *\n * @throws TypeError if `ttlMs` is not a finite, non-negative number. Rejecting\n * loudly matters here because the alternative is silent: `NaN` — what\n * `Number(process.env.DISCOVERY_TTL)` yields when the variable is unset — would\n * otherwise turn the cache off and restore a discovery round-trip on every\n * token operation, with nothing to show for it in the logs.\n */\nexport function configureDiscoveryCache(options: DiscoveryCacheOptions): void {\n if (options.ttlMs !== undefined) {\n if (!Number.isFinite(options.ttlMs) || options.ttlMs < 0) {\n throw new TypeError(\n `configureDiscoveryCache: ttlMs must be a finite number >= 0, got ${options.ttlMs}`,\n );\n }\n ttlMs = options.ttlMs;\n }\n cache.clear();\n}\n\n/**\n * Drops every cached configuration.\n *\n * Call this after changing provider configuration that discovery would not\n * otherwise pick up until the TTL expires. Tests should call it between cases,\n * since the cache is module state.\n */\nexport function clearDiscoveryCache(): void {\n cache.clear();\n}\n\n/**\n * Issuer and client identify a configuration; the secret is checked separately.\n * JSON-encoded rather than concatenated so no separator can be forged by a\n * value that happens to contain it.\n */\nfunction cacheKey(config: OAuthConfig): string {\n return JSON.stringify([config.issuer, config.clientId]);\n}\n\n/**\n * Discovers the provider's OpenID configuration, reusing a recent result.\n *\n * Every fides-auth flow goes through here, so this is also the single place\n * where the client authentication method is decided.\n *\n * @param config - OAuth configuration identifying the provider and client.\n * @returns The openid-client `Configuration` for this provider and client.\n */\nexport async function discoverConfiguration(\n config: OAuthConfig,\n): Promise<openid.Configuration> {\n const key = cacheKey(config);\n const now = Date.now();\n const cached = cache.get(key);\n\n if (cached && cached.expiresAt > now && cached.clientSecret === config.clientSecret) {\n return cached.configuration;\n }\n\n logger.debug({ issuer: config.issuer }, 'Discovering OpenID configuration');\n\n const configuration = openid.discovery(\n new URL(config.issuer),\n config.clientId,\n config.clientSecret,\n // openid-client's own default is client_secret_post; stating it keeps the\n // method visible at the one place that would grow a `clientAuth` option.\n openid.ClientSecretPost(config.clientSecret),\n );\n\n if (ttlMs > 0) {\n cache.set(key, { configuration, expiresAt: now + ttlMs, clientSecret: config.clientSecret });\n\n // Never cache a failure: a provider that was briefly unreachable must be\n // retried on the next call, not remembered as broken for the whole TTL.\n // Guarded so a slow failure cannot evict a newer entry that replaced it.\n configuration.catch(() => {\n if (cache.get(key)?.configuration === configuration) cache.delete(key);\n });\n }\n\n return configuration;\n}\n"],"mappings":";;;AAoBA,IAAM,SAAS,aAAa,EAAE,WAAW,4BAA4B,CAAC;;;;;;;;;AAUtE,IAAM,iBAAiB;AAkBvB,IAAM,wBAAQ,IAAI,IAAwB;AAC1C,IAAI,QAAQ;;;;;;;;;;;AAuBZ,SAAgB,wBAAwB,SAAsC;CAC5E,IAAI,QAAQ,UAAU,KAAA,GAAW;EAC/B,IAAI,CAAC,OAAO,SAAS,QAAQ,KAAK,KAAK,QAAQ,QAAQ,GACrD,MAAM,IAAI,UACR,oEAAoE,QAAQ,OAC9E;EAEF,QAAQ,QAAQ;CAClB;CACA,MAAM,MAAM;AACd;;;;;;;;AASA,SAAgB,sBAA4B;CAC1C,MAAM,MAAM;AACd;;;;;;AAOA,SAAS,SAAS,QAA6B;CAC7C,OAAO,KAAK,UAAU,CAAC,OAAO,QAAQ,OAAO,QAAQ,CAAC;AACxD;;;;;;;;;;AAWA,eAAsB,sBACpB,QAC+B;CAC/B,MAAM,MAAM,SAAS,MAAM;CAC3B,MAAM,MAAM,KAAK,IAAI;CACrB,MAAM,SAAS,MAAM,IAAI,GAAG;CAE5B,IAAI,UAAU,OAAO,YAAY,OAAO,OAAO,iBAAiB,OAAO,cACrE,OAAO,OAAO;CAGhB,OAAO,MAAM,EAAE,QAAQ,OAAO,OAAO,GAAG,kCAAkC;CAE1E,MAAM,gBAAgB,OAAO,UAC3B,IAAI,IAAI,OAAO,MAAM,GACrB,OAAO,UACP,OAAO,cAGP,OAAO,iBAAiB,OAAO,YAAY,CAC7C;CAEA,IAAI,QAAQ,GAAG;EACb,MAAM,IAAI,KAAK;GAAE;GAAe,WAAW,MAAM;GAAO,cAAc,OAAO;EAAa,CAAC;EAK3F,cAAc,YAAY;GACxB,IAAI,MAAM,IAAI,GAAG,CAAC,EAAE,kBAAkB,eAAe,MAAM,OAAO,GAAG;EACvE,CAAC;CACH;CAEA,OAAO;AACT"}
@@ -15,11 +15,14 @@ import * as openid from 'openid-client';
15
15
  export declare class VippsLoginClient {
16
16
  private readonly config;
17
17
  private readonly oauthConfig;
18
- private configPromise;
19
18
  constructor(config: VippsLoginConfig);
20
19
  /**
21
- * Get OpenID Configuration (cached)
22
- * This ensures we use the same Configuration instance across all operations
20
+ * Get the OpenID Configuration, shared with the rest of fides-auth.
21
+ *
22
+ * Previously memoized on the instance, which cached rejections too: one failed
23
+ * discovery — a restart during a provider blip — left every later call
24
+ * rejecting for the lifetime of the client. {@link discoverConfiguration}
25
+ * caches successes with a TTL and drops failures.
23
26
  */
24
27
  private getConfig;
25
28
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/vipps/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAOjB;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,aAAa,CAA8C;gBAEvD,MAAM,EAAE,gBAAgB;IAwBpC;;;OAGG;YACW,SAAS;IAYvB;;;;OAIG;IACG,gBAAgB,IAAI,OAAO,CAAC,WAAW,CAAC;IAK9C;;;;;OAKG;IACG,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAMnE;;;;;;;OAOG;IACG,qBAAqB,CACzB,WAAW,EAAE,MAAM,GAAG,GAAG,EACzB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC;IAgCxC;;;;;OAKG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsD9D;;;;;OAKG;IACG,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC;CAsBjF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/vipps/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAOjB;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;gBAE9B,MAAM,EAAE,gBAAgB;IAwBpC;;;;;;;OAOG;YACW,SAAS;IAIvB;;;;OAIG;IACG,gBAAgB,IAAI,OAAO,CAAC,WAAW,CAAC;IAK9C;;;;;OAKG;IACG,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAMnE;;;;;;;OAOG;IACG,qBAAqB,CACzB,WAAW,EAAE,MAAM,GAAG,GAAG,EACzB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC;IAgCxC;;;;;OAKG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsD9D;;;;;OAKG;IACG,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC;CAsBjF"}
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "../../logger.js";
2
+ import { discoverConfiguration } from "../../oidc-discovery.js";
2
3
  import { buildAuthorizationUrl, buildPKCEOptions } from "../../oauth.js";
3
4
  import * as openid from "openid-client";
4
5
  //#region src/providers/vipps/types.ts
@@ -64,7 +65,6 @@ var logger = createLogger({
64
65
  var VippsLoginClient = class {
65
66
  config;
66
67
  oauthConfig;
67
- configPromise = null;
68
68
  constructor(config) {
69
69
  this.config = {
70
70
  ...config,
@@ -84,12 +84,15 @@ var VippsLoginClient = class {
84
84
  }, "VippsLoginClient initialized");
85
85
  }
86
86
  /**
87
- * Get OpenID Configuration (cached)
88
- * This ensures we use the same Configuration instance across all operations
87
+ * Get the OpenID Configuration, shared with the rest of fides-auth.
88
+ *
89
+ * Previously memoized on the instance, which cached rejections too: one failed
90
+ * discovery — a restart during a provider blip — left every later call
91
+ * rejecting for the lifetime of the client. {@link discoverConfiguration}
92
+ * caches successes with a TTL and drops failures.
89
93
  */
90
94
  async getConfig() {
91
- if (!this.configPromise) this.configPromise = openid.discovery(new URL(this.oauthConfig.issuer), this.oauthConfig.clientId, this.oauthConfig.clientSecret, openid.ClientSecretPost(this.oauthConfig.clientSecret));
92
- return this.configPromise;
95
+ return discoverConfiguration(this.oauthConfig);
93
96
  }
94
97
  /**
95
98
  * Build PKCE options for authorization flow
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n private configPromise: Promise<openid.Configuration> | null = null;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get OpenID Configuration (cached)\n * This ensures we use the same Configuration instance across all operations\n */\n private async getConfig(): Promise<openid.Configuration> {\n if (!this.configPromise) {\n this.configPromise = openid.discovery(\n new URL(this.oauthConfig.issuer),\n this.oauthConfig.clientId,\n this.oauthConfig.clientSecret,\n openid.ClientSecretPost(this.oauthConfig.clientSecret)\n );\n }\n return this.configPromise;\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport { discoverConfiguration } from '../../oidc-discovery';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get the OpenID Configuration, shared with the rest of fides-auth.\n *\n * Previously memoized on the instance, which cached rejections too: one failed\n * discovery — a restart during a provider blip — left every later call\n * rejecting for the lifetime of the client. {@link discoverConfiguration}\n * caches successes with a TTL and drops failures.\n */\n private async getConfig(): Promise<openid.Configuration> {\n return discoverConfiguration(this.oauthConfig);\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
@@ -13,9 +13,16 @@ export interface HeartbeatHandlerConfig {
13
13
  /**
14
14
  * Handles a heartbeat request — refresh the active session if there is one.
15
15
  *
16
- * Returns 200 with `{ accessTokenExpiresAt }` on success, 401 when there is no
17
- * session or the refresh token is dead, 405 for non-POST methods, and 429 when
18
- * rate-limited.
16
+ * Returns 200 with `{ accessTokenExpiresAt }` on success, 401 with `{ reason }`
17
+ * when the session is genuinely over, 503 when the provider could not be
18
+ * reached, 405 for non-POST methods, and 429 when rate-limited.
19
+ *
20
+ * The 401/503 split matters: a 401 tells the client to log the user out, so it
21
+ * must be reserved for the cases where the session really is finished. A network
22
+ * blip or a provider 5xx says nothing about the session's validity — answering
23
+ * 401 there logs out users whose sessions were fine, which is precisely the
24
+ * "logged out every few minutes" symptom. The client already retries non-401
25
+ * failures with backoff.
19
26
  */
20
27
  export declare function handleHeartbeat(request: Request, config: HeartbeatHandlerConfig): Promise<Response>;
21
28
  //# sourceMappingURL=heartbeat-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-handler.d.ts","sourceRoot":"","sources":["../../src/server/heartbeat-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,WAAW,EAAE,WAAW,CAAC;IAEzB,wCAAwC;IACxC,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CA0CnB"}
1
+ {"version":3,"file":"heartbeat-handler.d.ts","sourceRoot":"","sources":["../../src/server/heartbeat-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,WAAW,EAAE,WAAW,CAAC;IAEzB,wCAAwC;IACxC,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAkDnB"}
@@ -1,5 +1,5 @@
1
1
  export type { CookieStore } from './cookie-store';
2
- export { persistSession, readSession, refreshSessionInStore, readIdToken, clearSession, } from './session';
2
+ export { persistSession, readSession, tryReadSession, refreshSessionInStore, tryRefreshSessionInStore, readIdToken, clearSession, type ClearSessionOptions, type PersistSessionOptions, type ReadSessionResult, type RefreshSessionResult, } from './session';
3
3
  export { handleOidcLogin, type OidcLoginConfig } from './oidc-login';
4
4
  export { handleOidcCallback, type OidcCallbackConfig } from './oidc-callback';
5
5
  export { handleOidcLogout, type OidcLogoutConfig } from './oidc-logout';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EACL,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,WAAW,EACX,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EACL,cAAc,EACd,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,WAAW,EACX,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC"}