@eventuras/fides-auth 0.6.0 → 0.7.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.
@@ -1,120 +1,206 @@
1
- import { createLogger as e } from "../../logger.js";
2
- import { l as t, n, o as r, t as i } from "../../build-nL2--iMi.js";
3
- import { buildAuthorizationUrl as a, buildPKCEOptions as o } from "../../oauth.js";
1
+ import { createLogger } from "../../logger.js";
2
+ import { buildAuthorizationUrl, buildPKCEOptions } from "../../oauth.js";
3
+ import * as openid from "openid-client";
4
4
  //#region src/providers/vipps/types.ts
5
- var s = {
5
+ /**
6
+ * Available Vipps Login scopes
7
+ */
8
+ var VippsLoginScopes = {
9
+ /** Required: OpenID Connect base scope */
6
10
  OpenId: "openid",
11
+ /** User's name (given_name, family_name, name) */
7
12
  Name: "name",
13
+ /** Phone number */
8
14
  PhoneNumber: "phoneNumber",
15
+ /** Physical address */
9
16
  Address: "address",
17
+ /** Birth date */
10
18
  BirthDate: "birthDate",
19
+ /** Email address */
11
20
  Email: "email"
12
- }, c = [
13
- s.OpenId,
14
- s.Name,
15
- s.PhoneNumber,
16
- s.Address,
17
- s.Email
18
- ].join(" "), l = {
21
+ };
22
+ /**
23
+ * Default scopes for Vipps Login
24
+ */
25
+ var defaultVippsLoginScope = [
26
+ VippsLoginScopes.OpenId,
27
+ VippsLoginScopes.Name,
28
+ VippsLoginScopes.PhoneNumber,
29
+ VippsLoginScopes.Address,
30
+ VippsLoginScopes.Email
31
+ ].join(" ");
32
+ /**
33
+ * Vipps API environments
34
+ */
35
+ var VippsEnvironments = {
36
+ /** Test environment */
19
37
  Test: "https://apitest.vipps.no",
38
+ /** Production environment */
20
39
  Production: "https://api.vipps.no"
21
40
  };
22
- function u(e) {
23
- return `${e}/access-management-1.0/access/`;
41
+ /**
42
+ * Get Vipps OIDC issuer URL for environment
43
+ */
44
+ function getVippsIssuer(environment) {
45
+ return `${environment}/access-management-1.0/access/`;
24
46
  }
25
47
  //#endregion
26
48
  //#region src/providers/vipps/client.ts
27
- var d = e({
49
+ /**
50
+ * Vipps Login Client
51
+ *
52
+ * Client for Vipps Login (OIDC) authentication flow.
53
+ * Implements OAuth/OIDC operations with PKCE for Vipps.
54
+ */
55
+ var logger = createLogger({
28
56
  namespace: "fides-auth:vipps",
29
57
  context: { module: "VippsLoginClient" }
30
- }), f = class {
58
+ });
59
+ /**
60
+ * Vipps Login Client
61
+ *
62
+ * Handles Vipps Login (OIDC) authentication flow using PKCE.
63
+ */
64
+ var VippsLoginClient = class {
31
65
  config;
32
66
  oauthConfig;
33
67
  configPromise = null;
34
- constructor(e) {
68
+ constructor(config) {
35
69
  this.config = {
36
- ...e,
37
- scope: e.scope ?? c
38
- }, this.oauthConfig = {
39
- issuer: e.issuer,
40
- clientId: e.clientId,
41
- clientSecret: e.clientSecret,
42
- redirect_uri: e.redirectUri,
70
+ ...config,
71
+ scope: config.scope ?? defaultVippsLoginScope
72
+ };
73
+ this.oauthConfig = {
74
+ issuer: config.issuer,
75
+ clientId: config.clientId,
76
+ clientSecret: config.clientSecret,
77
+ redirect_uri: config.redirectUri,
43
78
  scope: this.config.scope
44
- }, d.info({
45
- issuer: e.issuer,
46
- redirectUri: e.redirectUri,
79
+ };
80
+ logger.info({
81
+ issuer: config.issuer,
82
+ redirectUri: config.redirectUri,
47
83
  scope: this.config.scope
48
84
  }, "VippsLoginClient initialized");
49
85
  }
86
+ /**
87
+ * Get OpenID Configuration (cached)
88
+ * This ensures we use the same Configuration instance across all operations
89
+ */
50
90
  async getConfig() {
51
- return this.configPromise ||= r(new URL(this.oauthConfig.issuer), this.oauthConfig.clientId, this.oauthConfig.clientSecret, i(this.oauthConfig.clientSecret)), this.configPromise;
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;
52
93
  }
94
+ /**
95
+ * Build PKCE options for authorization flow
96
+ *
97
+ * @returns PKCE options including code_verifier, code_challenge, and state
98
+ */
53
99
  async buildPKCEOptions() {
54
- return d.debug("Building PKCE options"), o(this.oauthConfig);
100
+ logger.debug("Building PKCE options");
101
+ return buildPKCEOptions(this.oauthConfig);
55
102
  }
56
- async buildAuthorizationUrl(e) {
57
- return d.debug("Building authorization URL"), a(await this.getConfig(), e);
103
+ /**
104
+ * Build authorization URL for Vipps Login
105
+ *
106
+ * @param pkceOptions - PKCE options from buildPKCEOptions()
107
+ * @returns Authorization URL to redirect user to
108
+ */
109
+ async buildAuthorizationUrl(pkceOptions) {
110
+ logger.debug("Building authorization URL");
111
+ return buildAuthorizationUrl(await this.getConfig(), pkceOptions);
58
112
  }
59
- async exchangeCodeForTokens(e, t, r) {
60
- d.debug("Exchanging authorization code for tokens");
113
+ /**
114
+ * Exchange authorization code for tokens
115
+ *
116
+ * @param callbackUrl - The full callback URL with code parameter from Vipps
117
+ * @param codeVerifier - PKCE code verifier from initial request
118
+ * @param expectedState - Expected state value for validation (optional)
119
+ * @returns Token response with access_token, id_token, and refresh_token
120
+ */
121
+ async exchangeCodeForTokens(callbackUrl, codeVerifier, expectedState) {
122
+ logger.debug("Exchanging authorization code for tokens");
61
123
  try {
62
- let i = await n(await this.getConfig(), typeof e == "string" ? new URL(e) : e, {
63
- pkceCodeVerifier: t,
64
- expectedState: r,
65
- idTokenExpected: !0
124
+ const config = await this.getConfig();
125
+ const currentUrl = typeof callbackUrl === "string" ? new URL(callbackUrl) : callbackUrl;
126
+ const tokens = await openid.authorizationCodeGrant(config, currentUrl, {
127
+ pkceCodeVerifier: codeVerifier,
128
+ expectedState,
129
+ idTokenExpected: true
66
130
  });
67
- return d.info({
68
- hasAccessToken: !!i.access_token,
69
- hasRefreshToken: !!i.refresh_token,
70
- hasIdToken: !!i.id_token,
71
- expiresIn: i.expires_in
72
- }, "Successfully exchanged code for tokens"), i;
73
- } catch (e) {
74
- throw d.error({ error: e }, "Failed to exchange code for tokens"), e;
131
+ logger.info({
132
+ hasAccessToken: !!tokens.access_token,
133
+ hasRefreshToken: !!tokens.refresh_token,
134
+ hasIdToken: !!tokens.id_token,
135
+ expiresIn: tokens.expires_in
136
+ }, "Successfully exchanged code for tokens");
137
+ return tokens;
138
+ } catch (error) {
139
+ logger.error({ error }, "Failed to exchange code for tokens");
140
+ throw error;
75
141
  }
76
142
  }
77
- async getUserInfo(e) {
78
- d.debug("Fetching user info from Vipps");
143
+ /**
144
+ * Get user information from Vipps userinfo endpoint
145
+ *
146
+ * @param accessToken - Access token from token exchange
147
+ * @returns Vipps user information
148
+ */
149
+ async getUserInfo(accessToken) {
150
+ logger.debug("Fetching user info from Vipps");
79
151
  try {
80
- let t = (await this.getConfig()).serverMetadata().userinfo_endpoint;
81
- if (!t) throw Error("Userinfo endpoint not found in OpenID configuration");
82
- let n = await fetch(t, { headers: {
83
- Authorization: `Bearer ${e}`,
152
+ const userinfoUrl = (await this.getConfig()).serverMetadata().userinfo_endpoint;
153
+ if (!userinfoUrl) throw new Error("Userinfo endpoint not found in OpenID configuration");
154
+ const response = await fetch(userinfoUrl, { headers: {
155
+ Authorization: `Bearer ${accessToken}`,
84
156
  ...this.config.subscriptionKey && { "Ocp-Apim-Subscription-Key": this.config.subscriptionKey },
85
157
  ...this.config.merchantSerialNumber && { "Merchant-Serial-Number": this.config.merchantSerialNumber }
86
158
  } });
87
- if (!n.ok) {
88
- let e = await n.text();
89
- throw d.error({
90
- status: n.status,
91
- error: e
92
- }, "Failed to fetch user info"), Error(`Failed to fetch user info: ${n.status} - ${e}`);
159
+ if (!response.ok) {
160
+ const errorText = await response.text();
161
+ logger.error({
162
+ status: response.status,
163
+ error: errorText
164
+ }, "Failed to fetch user info");
165
+ throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);
93
166
  }
94
- let r = await n.json();
95
- return d.info({
96
- sub: r.sub,
97
- hasEmail: !!r.email,
98
- hasPhone: !!r.phone_number,
99
- hasAddress: !!r.addresses?.length
100
- }, "Successfully fetched user info"), r;
101
- } catch (e) {
102
- throw d.error({ error: e }, "Error fetching user info"), e;
167
+ const userInfo = await response.json();
168
+ logger.info({
169
+ sub: userInfo.sub,
170
+ hasEmail: !!userInfo.email,
171
+ hasPhone: !!userInfo.phone_number,
172
+ hasAddress: !!userInfo.addresses?.length
173
+ }, "Successfully fetched user info");
174
+ return userInfo;
175
+ } catch (error) {
176
+ logger.error({ error }, "Error fetching user info");
177
+ throw error;
103
178
  }
104
179
  }
105
- async refreshTokens(e) {
106
- d.debug("Refreshing access token");
180
+ /**
181
+ * Refresh access token using refresh token
182
+ *
183
+ * @param refreshToken - Refresh token from initial token exchange
184
+ * @returns New token response
185
+ */
186
+ async refreshTokens(refreshToken) {
187
+ logger.debug("Refreshing access token");
107
188
  try {
108
- let n = await t(await this.getConfig(), e);
109
- return d.info({
110
- hasAccessToken: !!n.access_token,
111
- hasRefreshToken: !!n.refresh_token,
112
- expiresIn: n.expires_in
113
- }, "Successfully refreshed tokens"), n;
114
- } catch (e) {
115
- throw d.error({ error: e }, "Failed to refresh tokens"), e;
189
+ const config = await this.getConfig();
190
+ const tokens = await openid.refreshTokenGrant(config, refreshToken);
191
+ logger.info({
192
+ hasAccessToken: !!tokens.access_token,
193
+ hasRefreshToken: !!tokens.refresh_token,
194
+ expiresIn: tokens.expires_in
195
+ }, "Successfully refreshed tokens");
196
+ return tokens;
197
+ } catch (error) {
198
+ logger.error({ error }, "Failed to refresh tokens");
199
+ throw error;
116
200
  }
117
201
  }
118
202
  };
119
203
  //#endregion
120
- export { l as VippsEnvironments, f as VippsLoginClient, s as VippsLoginScopes, c as defaultVippsLoginScope, u as getVippsIssuer };
204
+ export { VippsEnvironments, VippsLoginClient, VippsLoginScopes, defaultVippsLoginScope, getVippsIssuer };
205
+
206
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,26 +1,64 @@
1
1
  //#region src/rate-limit.ts
2
- var e = class {
2
+ /**
3
+ * Token bucket rate limiter for protecting against brute-force attacks.
4
+ *
5
+ * Each key (e.g. IP address, user ID) gets its own bucket. Tokens refill
6
+ * over time at a fixed interval. When a bucket is empty, requests are denied.
7
+ *
8
+ * @typeParam _Key - The type of key used to identify rate-limited entities
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const limiter = new TokenBucket<string>(10, 60); // 10 tokens, refill 1/min
13
+ *
14
+ * if (!limiter.consume(ipAddress, 1)) {
15
+ * throw new Error('Rate limit exceeded');
16
+ * }
17
+ * ```
18
+ */
19
+ var TokenBucket = class {
3
20
  max;
4
21
  refillIntervalSeconds;
5
- constructor(e, t) {
6
- this.max = e, this.refillIntervalSeconds = t;
22
+ constructor(max, refillIntervalSeconds) {
23
+ this.max = max;
24
+ this.refillIntervalSeconds = refillIntervalSeconds;
7
25
  }
8
26
  storage = /* @__PURE__ */ new Map();
9
- check(e, t) {
10
- let n = this.storage.get(e) ?? null;
11
- if (n === null) return !0;
12
- let r = Math.floor((Date.now() - n.refilledAt) / (this.refillIntervalSeconds * 1e3));
13
- return r > 0 ? Math.min(n.count + r, this.max) >= t : n.count >= t;
27
+ /** Check whether the bucket has enough tokens without consuming any. */
28
+ check(key, cost) {
29
+ const bucket = this.storage.get(key) ?? null;
30
+ if (bucket === null) return true;
31
+ const refill = Math.floor((Date.now() - bucket.refilledAt) / (this.refillIntervalSeconds * 1e3));
32
+ if (refill > 0) return Math.min(bucket.count + refill, this.max) >= cost;
33
+ return bucket.count >= cost;
14
34
  }
15
- consume(e, t) {
16
- let n = this.storage.get(e) ?? null, r = Date.now();
17
- if (n === null) return n = {
18
- count: this.max - t,
19
- refilledAt: r
20
- }, this.storage.set(e, n), !0;
21
- let i = Math.floor((r - n.refilledAt) / (this.refillIntervalSeconds * 1e3));
22
- return i > 0 && (n.count = Math.min(n.count + i, this.max), n.refilledAt = r), n.count < t ? (this.storage.set(e, n), !1) : (n.count -= t, this.storage.set(e, n), !0);
35
+ /** Consume tokens from the bucket. Returns `true` if allowed, `false` if rate-limited. */
36
+ consume(key, cost) {
37
+ let bucket = this.storage.get(key) ?? null;
38
+ const now = Date.now();
39
+ if (bucket === null) {
40
+ bucket = {
41
+ count: this.max - cost,
42
+ refilledAt: now
43
+ };
44
+ this.storage.set(key, bucket);
45
+ return true;
46
+ }
47
+ const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1e3));
48
+ if (refill > 0) {
49
+ bucket.count = Math.min(bucket.count + refill, this.max);
50
+ bucket.refilledAt = now;
51
+ }
52
+ if (bucket.count < cost) {
53
+ this.storage.set(key, bucket);
54
+ return false;
55
+ }
56
+ bucket.count -= cost;
57
+ this.storage.set(key, bucket);
58
+ return true;
23
59
  }
24
60
  };
25
61
  //#endregion
26
- export { e as TokenBucket };
62
+ export { TokenBucket };
63
+
64
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","names":[],"sources":["../src/rate-limit.ts"],"sourcesContent":["/**\n * Token bucket rate limiter for protecting against brute-force attacks.\n *\n * Each key (e.g. IP address, user ID) gets its own bucket. Tokens refill\n * over time at a fixed interval. When a bucket is empty, requests are denied.\n *\n * @typeParam _Key - The type of key used to identify rate-limited entities\n *\n * @example\n * ```typescript\n * const limiter = new TokenBucket<string>(10, 60); // 10 tokens, refill 1/min\n *\n * if (!limiter.consume(ipAddress, 1)) {\n * throw new Error('Rate limit exceeded');\n * }\n * ```\n */\nexport class TokenBucket<_Key> {\n public max: number;\n public refillIntervalSeconds: number;\n\n constructor(max: number, refillIntervalSeconds: number) {\n this.max = max;\n this.refillIntervalSeconds = refillIntervalSeconds;\n }\n\n private readonly storage = new Map<_Key, Bucket>();\n\n /** Check whether the bucket has enough tokens without consuming any. */\n public check(key: _Key, cost: number): boolean {\n const bucket = this.storage.get(key) ?? null;\n if (bucket === null) {\n return true;\n }\n const now = Date.now();\n const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000));\n if (refill > 0) {\n return Math.min(bucket.count + refill, this.max) >= cost;\n }\n return bucket.count >= cost;\n }\n\n /** Consume tokens from the bucket. Returns `true` if allowed, `false` if rate-limited. */\n public consume(key: _Key, cost: number): boolean {\n let bucket = this.storage.get(key) ?? null;\n const now = Date.now();\n if (bucket === null) {\n bucket = {\n count: this.max - cost,\n refilledAt: now,\n };\n this.storage.set(key, bucket);\n return true;\n }\n const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000));\n if (refill > 0) {\n bucket.count = Math.min(bucket.count + refill, this.max);\n bucket.refilledAt = now;\n }\n if (bucket.count < cost) {\n this.storage.set(key, bucket);\n return false;\n }\n bucket.count -= cost;\n this.storage.set(key, bucket);\n return true;\n }\n}\ninterface Bucket {\n count: number;\n refilledAt: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,IAAa,cAAb,MAA+B;CAC7B;CACA;CAEA,YAAY,KAAa,uBAA+B;AACtD,OAAK,MAAM;AACX,OAAK,wBAAwB;;CAG/B,0BAA2B,IAAI,KAAmB;;CAGlD,MAAa,KAAW,MAAuB;EAC7C,MAAM,SAAS,KAAK,QAAQ,IAAI,IAAI,IAAI;AACxC,MAAI,WAAW,KACb,QAAO;EAGT,MAAM,SAAS,KAAK,OADR,KAAK,KACU,GAAM,OAAO,eAAe,KAAK,wBAAwB,KAAM;AAC1F,MAAI,SAAS,EACX,QAAO,KAAK,IAAI,OAAO,QAAQ,QAAQ,KAAK,IAAI,IAAI;AAEtD,SAAO,OAAO,SAAS;;;CAIzB,QAAe,KAAW,MAAuB;EAC/C,IAAI,SAAS,KAAK,QAAQ,IAAI,IAAI,IAAI;EACtC,MAAM,MAAM,KAAK,KAAK;AACtB,MAAI,WAAW,MAAM;AACnB,YAAS;IACP,OAAO,KAAK,MAAM;IAClB,YAAY;IACb;AACD,QAAK,QAAQ,IAAI,KAAK,OAAO;AAC7B,UAAO;;EAET,MAAM,SAAS,KAAK,OAAO,MAAM,OAAO,eAAe,KAAK,wBAAwB,KAAM;AAC1F,MAAI,SAAS,GAAG;AACd,UAAO,QAAQ,KAAK,IAAI,OAAO,QAAQ,QAAQ,KAAK,IAAI;AACxD,UAAO,aAAa;;AAEtB,MAAI,OAAO,QAAQ,MAAM;AACvB,QAAK,QAAQ,IAAI,KAAK,OAAO;AAC7B,UAAO;;AAET,SAAO,SAAS;AAChB,OAAK,QAAQ,IAAI,KAAK,OAAO;AAC7B,SAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,iBAAiB,OAAO,EACxB,QAAQ,WAAW,EACnB,UAAS,oBAAyB,KACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CA6CxB,CAAC"}
1
+ {"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,iBAAiB,OAAO,EACxB,QAAQ,WAAW,EACnB,UAAS,oBAAyB,KACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CA8CxB,CAAC"}
@@ -1,27 +1,48 @@
1
- import { createLogger as e } from "./logger.js";
2
- import { refreshAccessToken as t } from "./oauth.js";
1
+ import { createLogger } from "./logger.js";
2
+ import { getOAuthErrorLogContext } from "./oauth-logging.js";
3
+ import { refreshAccessToken } from "./oauth.js";
3
4
  //#region src/session-refresh.ts
4
- var n = e({ namespace: "fides-auth:session-refresh" }), r = async (e, r, i = {}) => {
5
- if (n.debug("Starting session refresh"), !e.tokens?.refreshToken) throw n.error("No refresh token available in session"), Error("No refresh token available");
5
+ var logger = createLogger({ namespace: "fides-auth:session-refresh" });
6
+ /**
7
+ * Refreshes an existing session by exchanging its refresh token for new tokens.
8
+ *
9
+ * @param current_session - The current session containing a refresh token
10
+ * @param config - OAuth configuration for the identity provider
11
+ * @param options - Optional session creation options
12
+ * @returns The updated session with new tokens, or throws on failure
13
+ * @throws Error if no refresh token is available or the token exchange fails
14
+ */
15
+ var refreshSession = async (current_session, config, options = {}) => {
16
+ logger.debug("Starting session refresh");
17
+ if (!current_session.tokens?.refreshToken) {
18
+ logger.error("No refresh token available in session");
19
+ throw new Error("No refresh token available");
20
+ }
6
21
  try {
7
- let i = await t(r, e.tokens.refreshToken), a = {
8
- ...e,
22
+ const newtokens = await refreshAccessToken(config, current_session.tokens.refreshToken);
23
+ const updatedSession = {
24
+ ...current_session,
9
25
  tokens: {
10
- ...e.tokens,
11
- accessToken: i.access_token,
12
- accessTokenExpiresAt: i.expires_in ? new Date(Date.now() + i.expires_in * 1e3) : void 0,
13
- refreshToken: i.refresh_token ?? e.tokens?.refreshToken
26
+ ...current_session.tokens,
27
+ accessToken: newtokens.access_token,
28
+ accessTokenExpiresAt: newtokens.expires_in ? new Date(Date.now() + newtokens.expires_in * 1e3) : void 0,
29
+ refreshToken: newtokens.refresh_token ?? current_session.tokens?.refreshToken
14
30
  }
15
31
  };
16
- return n.info({
17
- hasNewAccessToken: !!a.tokens.accessToken,
18
- hasNewRefreshToken: !!i.refresh_token,
19
- expiresIn: i.expires_in
20
- }, "Session refreshed successfully"), a;
21
- } catch (e) {
22
- let t = e;
23
- throw t?.code === "OAUTH_RESPONSE_BODY_ERROR" && t?.error === "invalid_grant" ? n.info("Session refresh failed - refresh token expired or invalid") : n.error({ error: e }, "Session refresh failed"), e;
32
+ logger.info({
33
+ hasNewAccessToken: !!updatedSession.tokens.accessToken,
34
+ hasNewRefreshToken: !!newtokens.refresh_token,
35
+ expiresIn: newtokens.expires_in
36
+ }, "Session refreshed successfully");
37
+ return updatedSession;
38
+ } catch (error) {
39
+ const errorContext = getOAuthErrorLogContext(error);
40
+ if (errorContext.code === "OAUTH_RESPONSE_BODY_ERROR" && errorContext.error === "invalid_grant") logger.info("Session refresh failed - refresh token expired or invalid");
41
+ else logger.error({ error: errorContext }, "Session refresh failed");
42
+ throw error;
24
43
  }
25
44
  };
26
45
  //#endregion
27
- export { r as refreshSession };
46
+ export { refreshSession };
47
+
48
+ //# sourceMappingURL=session-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-refresh.js","names":[],"sources":["../src/session-refresh.ts"],"sourcesContent":["import { createLogger } from './logger';\nimport { OAuthConfig, refreshAccessToken } from \"./oauth\";\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport { CreateSessionOptions, Session } from \"./types\";\n\nconst logger = createLogger({ namespace: 'fides-auth:session-refresh' });\n\n/**\n * Refreshes an existing session by exchanging its refresh token for new tokens.\n *\n * @param current_session - The current session containing a refresh token\n * @param config - OAuth configuration for the identity provider\n * @param options - Optional session creation options\n * @returns The updated session with new tokens, or throws on failure\n * @throws Error if no refresh token is available or the token exchange fails\n */\nexport const refreshSession = async (\n current_session: Session,\n config: OAuthConfig,\n options: CreateSessionOptions = {}\n): Promise<Session | null> => {\n logger.debug('Starting session refresh');\n\n if (!current_session.tokens?.refreshToken) {\n logger.error('No refresh token available in session');\n throw new Error('No refresh token available');\n }\n\n try {\n const newtokens = await refreshAccessToken(config, current_session.tokens.refreshToken);\n\n const updatedSession = {\n ...current_session,\n tokens: {\n ...current_session.tokens,\n accessToken: newtokens.access_token,\n accessTokenExpiresAt: newtokens.expires_in\n ? new Date(Date.now() + newtokens.expires_in * 1000)\n : undefined,\n refreshToken: newtokens.refresh_token ?? current_session.tokens?.refreshToken,\n },\n };\n\n logger.info({\n hasNewAccessToken: !!updatedSession.tokens.accessToken,\n hasNewRefreshToken: !!newtokens.refresh_token,\n expiresIn: newtokens.expires_in,\n }, 'Session refreshed successfully');\n\n return updatedSession;\n } catch (error) {\n // Check if this is an expected invalid_grant error\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Expected error during logout/session expiry - log at info level\n logger.info('Session refresh failed - refresh token expired or invalid');\n } else {\n // Unexpected error - log at error level\n logger.error({ error: errorContext }, 'Session refresh failed');\n }\n\n throw error;\n }\n};\n\n"],"mappings":";;;;AAKA,IAAM,SAAS,aAAa,EAAE,WAAW,8BAA8B,CAAC;;;;;;;;;;AAWxE,IAAa,iBAAiB,OAC5B,iBACA,QACA,UAAgC,EAAE,KACN;AAC5B,QAAO,MAAM,2BAA2B;AAExC,KAAI,CAAC,gBAAgB,QAAQ,cAAc;AACzC,SAAO,MAAM,wCAAwC;AACrD,QAAM,IAAI,MAAM,6BAA6B;;AAG/C,KAAI;EACF,MAAM,YAAY,MAAM,mBAAmB,QAAQ,gBAAgB,OAAO,aAAa;EAEvF,MAAM,iBAAiB;GACrB,GAAG;GACH,QAAQ;IACN,GAAG,gBAAgB;IACnB,aAAa,UAAU;IACvB,sBAAsB,UAAU,aAC5B,IAAI,KAAK,KAAK,KAAK,GAAG,UAAU,aAAa,IAAK,GAClD,KAAA;IACJ,cAAc,UAAU,iBAAiB,gBAAgB,QAAQ;IAClE;GACF;AAED,SAAO,KAAK;GACV,mBAAmB,CAAC,CAAC,eAAe,OAAO;GAC3C,oBAAoB,CAAC,CAAC,UAAU;GAChC,WAAW,UAAU;GACtB,EAAE,iCAAiC;AAEpC,SAAO;UACA,OAAO;EAEd,MAAM,eAAe,wBAAwB,MAAM;AAInD,MAFE,aAAa,SAAS,+BAA+B,aAAa,UAAU,gBAI5E,QAAO,KAAK,4DAA4D;MAGxE,QAAO,MAAM,EAAE,OAAO,cAAc,EAAE,yBAAyB;AAGjE,QAAM"}
@@ -1,2 +1,77 @@
1
- import { n as e, t } from "./session-validation-CHSsh4q_.js";
2
- export { t as isSession, e as validateSessionJwt };
1
+ import { createLogger } from "./logger.js";
2
+ import { hexToUint8Array } from "./utils.js";
3
+ import { decodeJwt, jwtDecrypt } from "jose";
4
+ //#region src/session-validation.ts
5
+ var logger = createLogger({ namespace: "fides-auth:session-validation" });
6
+ /**
7
+ * Validates an encrypted JWT and checks for expiration.
8
+ *
9
+ * @param encryptedJwt The encrypted JWT string to validate
10
+ * @param secret The decryption key as a hex string or Uint8Array (32 bytes for A256GCM)
11
+ * @returns Session and SessionValidationResult indicating whether it's valid or not,
12
+ * plus accessTokenExpiresIn if valid
13
+ */
14
+ async function validateSessionJwt(encryptedJwt, secret) {
15
+ logger.debug("Starting session JWT validation");
16
+ const secretBytes = typeof secret === "string" ? hexToUint8Array(secret) : secret;
17
+ let payload;
18
+ try {
19
+ ({payload} = await jwtDecrypt(encryptedJwt, secretBytes));
20
+ } catch (error) {
21
+ const errorMessage = error instanceof Error ? error.message : "Failed decrypting JWT";
22
+ logger.warn({ reason: errorMessage }, "Session JWT decryption failed");
23
+ return {
24
+ status: "INVALID",
25
+ reason: `Decryption error: ${errorMessage}`
26
+ };
27
+ }
28
+ if (!isSession(payload)) {
29
+ logger.warn("JWT payload does not match Session structure");
30
+ return {
31
+ status: "INVALID",
32
+ reason: "jwt does not seem to include a session"
33
+ };
34
+ }
35
+ logger.debug("Session validated successfully");
36
+ const expiresIn = computeAccessTokenExpiresIn(payload);
37
+ if (expiresIn !== void 0 && expiresIn <= 0) {
38
+ logger.info({ accessTokenExpiresIn: expiresIn }, "Session access token has expired");
39
+ return {
40
+ session: payload,
41
+ status: "EXPIRED",
42
+ accessTokenExpiresIn: expiresIn,
43
+ reason: "Access token has expired"
44
+ };
45
+ }
46
+ return {
47
+ session: payload,
48
+ status: "VALID",
49
+ accessTokenExpiresIn: expiresIn
50
+ };
51
+ }
52
+ /**
53
+ * Computes the number of seconds until the access token expires.
54
+ * Returns undefined if no expiry can be determined.
55
+ */
56
+ function computeAccessTokenExpiresIn(session) {
57
+ const accessToken = session.tokens?.accessToken;
58
+ if (!accessToken) return;
59
+ try {
60
+ const { exp } = decodeJwt(accessToken);
61
+ if (typeof exp !== "number") return;
62
+ return Math.floor(exp - Date.now() / 1e3);
63
+ } catch {
64
+ return;
65
+ }
66
+ }
67
+ /** Type guard that checks whether an unknown value conforms to the {@link Session} shape. */
68
+ function isSession(object) {
69
+ if (typeof object !== "object" || object === null) return false;
70
+ if ("tokens" in object && typeof object.tokens !== "object") return false;
71
+ if ("user" in object && typeof object.user !== "object") return false;
72
+ return true;
73
+ }
74
+ //#endregion
75
+ export { isSession, validateSessionJwt };
76
+
77
+ //# sourceMappingURL=session-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-validation.js","names":[],"sources":["../src/session-validation.ts"],"sourcesContent":["// session-validation.ts\n\nimport { jwtDecrypt, decodeJwt } from 'jose';\n\nimport { createLogger } from './logger';\nimport { hexToUint8Array } from './utils';\nimport { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:session-validation' });\n\n/** Result of validating an encrypted session JWT. */\nexport interface SessionValidationResult {\n /** The decoded session, if valid. */\n session?: Session;\n /** Validation status: VALID, INVALID (decryption/format error), or EXPIRED. */\n status: 'VALID' | 'INVALID' | 'EXPIRED';\n /** Seconds until the access token expires (negative means already expired). */\n accessTokenExpiresIn?: number;\n /** Human-readable reason when status is not VALID. */\n reason?: string;\n}\n\n/**\n * Validates an encrypted JWT and checks for expiration.\n *\n * @param encryptedJwt The encrypted JWT string to validate\n * @param secret The decryption key as a hex string or Uint8Array (32 bytes for A256GCM)\n * @returns Session and SessionValidationResult indicating whether it's valid or not,\n * plus accessTokenExpiresIn if valid\n */\nexport async function validateSessionJwt(\n encryptedJwt: string,\n secret: string | Uint8Array,\n): Promise<SessionValidationResult> {\n logger.debug('Starting session JWT validation');\n\n const secretBytes = typeof secret === 'string'\n ? hexToUint8Array(secret)\n : secret;\n\n let payload: unknown;\n try {\n ({ payload } = await jwtDecrypt(encryptedJwt, secretBytes));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed decrypting JWT';\n logger.warn({ reason: errorMessage }, 'Session JWT decryption failed');\n return {\n status: 'INVALID',\n reason: `Decryption error: ${errorMessage}`\n };\n }\n\n // Check if payload has the shape of a Session\n if (!isSession(payload)) {\n logger.warn('JWT payload does not match Session structure');\n return {\n status: 'INVALID',\n reason: 'jwt does not seem to include a session'\n };\n }\n\n logger.debug('Session validated successfully');\n\n // Check access token expiry if available\n const expiresIn = computeAccessTokenExpiresIn(payload);\n\n if (expiresIn !== undefined && expiresIn <= 0) {\n logger.info({ accessTokenExpiresIn: expiresIn }, 'Session access token has expired');\n return {\n session: payload,\n status: 'EXPIRED',\n accessTokenExpiresIn: expiresIn,\n reason: 'Access token has expired',\n };\n }\n\n return {\n session: payload,\n status: 'VALID',\n accessTokenExpiresIn: expiresIn,\n };\n}\n\n/**\n * Computes the number of seconds until the access token expires.\n * Returns undefined if no expiry can be determined.\n */\nfunction computeAccessTokenExpiresIn(session: Session): number | undefined {\n const accessToken = session.tokens?.accessToken;\n if (!accessToken) {\n return undefined;\n }\n\n try {\n const { exp } = decodeJwt(accessToken);\n if (typeof exp !== 'number') {\n return undefined;\n }\n return Math.floor(exp - Date.now() / 1000);\n } catch {\n // Not a valid JWT or can't decode — ignore\n return undefined;\n }\n}\n\n\n/** Type guard that checks whether an unknown value conforms to the {@link Session} shape. */\nexport function isSession(object: unknown): object is Session {\n if (typeof object !== 'object' || object === null) {\n return false;\n }\n\n if (\n 'tokens' in object &&\n typeof (object as any).tokens !== 'object'\n ) {\n return false;\n }\n\n if (\n 'user' in object &&\n typeof (object as any).user !== 'object'\n ) {\n return false;\n }\n\n // Seems to be a real session\n return true;\n}\n"],"mappings":";;;;AAQA,IAAM,SAAS,aAAa,EAAE,WAAW,iCAAiC,CAAC;;;;;;;;;AAsB3E,eAAsB,mBACpB,cACA,QACkC;AAClC,QAAO,MAAM,kCAAkC;CAE/C,MAAM,cAAc,OAAO,WAAW,WAClC,gBAAgB,OAAO,GACvB;CAEJ,IAAI;AACJ,KAAI;AACF,GAAC,CAAE,WAAY,MAAM,WAAW,cAAc,YAAY;UACnD,OAAO;EACd,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU;AAC9D,SAAO,KAAK,EAAE,QAAQ,cAAc,EAAE,gCAAgC;AACtE,SAAO;GACL,QAAQ;GACR,QAAQ,qBAAqB;GAC9B;;AAIH,KAAI,CAAC,UAAU,QAAQ,EAAE;AACvB,SAAO,KAAK,+CAA+C;AAC3D,SAAO;GACL,QAAQ;GACR,QAAQ;GACT;;AAGH,QAAO,MAAM,iCAAiC;CAG9C,MAAM,YAAY,4BAA4B,QAAQ;AAEtD,KAAI,cAAc,KAAA,KAAa,aAAa,GAAG;AAC7C,SAAO,KAAK,EAAE,sBAAsB,WAAW,EAAE,mCAAmC;AACpF,SAAO;GACL,SAAS;GACT,QAAQ;GACR,sBAAsB;GACtB,QAAQ;GACT;;AAGH,QAAO;EACL,SAAS;EACT,QAAQ;EACR,sBAAsB;EACvB;;;;;;AAOH,SAAS,4BAA4B,SAAsC;CACzE,MAAM,cAAc,QAAQ,QAAQ;AACpC,KAAI,CAAC,YACH;AAGF,KAAI;EACF,MAAM,EAAE,QAAQ,UAAU,YAAY;AACtC,MAAI,OAAO,QAAQ,SACjB;AAEF,SAAO,KAAK,MAAM,MAAM,KAAK,KAAK,GAAG,IAAK;SACpC;AAEN;;;;AAMJ,SAAgB,UAAU,QAAoC;AAC5D,KAAI,OAAO,WAAW,YAAY,WAAW,KAC3C,QAAO;AAGT,KACE,YAAY,UACZ,OAAQ,OAAe,WAAW,SAElC,QAAO;AAGT,KACE,UAAU,UACV,OAAQ,OAAe,SAAS,SAEhC,QAAO;AAIT,QAAO"}