@forestadmin/agent-bff 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = createOAuthRoutes;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ const bff_token_1 = require("./bff-token");
10
+ const forest_server_client_1 = require("./forest-server-client");
11
+ const oauth_error_1 = require("./oauth-error");
12
+ const session_lifecycle_1 = __importDefault(require("./session-lifecycle"));
13
+ function hashPresentedToken(token) {
14
+ return crypto_1.default.createHash('sha256').update(token).digest('base64url');
15
+ }
16
+ const CODE_VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]{43,128}$/;
17
+ const S256_CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
18
+ const SAFE_EXCHANGE_ERRORS = new Set([
19
+ 'invalid_request',
20
+ 'invalid_client',
21
+ 'invalid_grant',
22
+ 'unauthorized_client',
23
+ 'unsupported_grant_type',
24
+ 'invalid_scope',
25
+ ]);
26
+ function getQueryParam(ctx, key) {
27
+ const value = ctx.query[key];
28
+ if (Array.isArray(value)) {
29
+ throw (0, oauth_error_1.invalidRequest)(`Parameter must appear at most once: ${key}`);
30
+ }
31
+ return value;
32
+ }
33
+ function requireBodyString(body, key) {
34
+ const value = body[key];
35
+ if (typeof value !== 'string' || value === '') {
36
+ throw (0, oauth_error_1.invalidRequest)(`Missing required parameter: ${key}`);
37
+ }
38
+ return value;
39
+ }
40
+ function assertValidCodeVerifier(codeVerifier) {
41
+ if (!CODE_VERIFIER_PATTERN.test(codeVerifier)) {
42
+ throw (0, oauth_error_1.invalidRequest)('code_verifier is malformed');
43
+ }
44
+ }
45
+ function assertRegisteredRedirectUri(redirectUris, redirectUri, onMismatch) {
46
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
47
+ throw (0, oauth_error_1.invalidRequest)('Client has no registered redirect_uri');
48
+ }
49
+ if (!redirectUris.includes(redirectUri)) {
50
+ throw onMismatch('redirect_uri does not match the registered client');
51
+ }
52
+ }
53
+ function toSafeExchangeError(saasError) {
54
+ if (SAFE_EXCHANGE_ERRORS.has(saasError)) {
55
+ return new oauth_error_1.OAuthRequestError(400, saasError, 'Authorization code exchange failed');
56
+ }
57
+ return new oauth_error_1.OAuthRequestError(502, 'server_error', 'Forest server rejected the authorization code');
58
+ }
59
+ function mapIdentityError(error) {
60
+ const name = error instanceof Error ? error.name : '';
61
+ if (name === 'ForbiddenError') {
62
+ return (0, oauth_error_1.forestIdentityNotAllowed)('Caller is not an active user for this rendering');
63
+ }
64
+ return new oauth_error_1.OAuthRequestError(502, 'identity_resolution_failed', 'Failed to resolve identity');
65
+ }
66
+ function redirectAuthorizeError(ctx, redirectUri, state, error) {
67
+ const target = new URL(redirectUri);
68
+ target.searchParams.set('error', error.type);
69
+ target.searchParams.set('error_description', error.message);
70
+ if (state !== undefined && state !== '')
71
+ target.searchParams.set('state', state);
72
+ ctx.redirect(target.toString());
73
+ }
74
+ async function handleAuthorize(ctx, options) {
75
+ // Validate client_id and redirect_uri first: until redirect_uri is confirmed
76
+ // against the registered client, errors must not redirect (RFC 6749 §10.15).
77
+ const clientId = getQueryParam(ctx, 'client_id');
78
+ if (clientId === undefined || clientId === '') {
79
+ throw (0, oauth_error_1.invalidRequest)('Missing required parameter: client_id');
80
+ }
81
+ const redirectUri = getQueryParam(ctx, 'redirect_uri');
82
+ if (redirectUri === undefined || redirectUri === '') {
83
+ throw (0, oauth_error_1.invalidRequest)('Missing required parameter: redirect_uri');
84
+ }
85
+ const client = await options.serverClient.getRegisteredClient(clientId);
86
+ if (!client) {
87
+ throw (0, oauth_error_1.invalidRequest)('Unknown client_id');
88
+ }
89
+ assertRegisteredRedirectUri(client.redirect_uris, redirectUri, oauth_error_1.invalidRequest);
90
+ // redirect_uri is now trusted: remaining validation errors redirect back to
91
+ // the client with error + state (RFC 6749 §4.1.2.1).
92
+ let state;
93
+ try {
94
+ state = getQueryParam(ctx, 'state');
95
+ const responseType = getQueryParam(ctx, 'response_type');
96
+ if (responseType !== 'code') {
97
+ throw (0, oauth_error_1.invalidRequest)('response_type must be "code"');
98
+ }
99
+ if (getQueryParam(ctx, 'code_challenge_method') !== 'S256') {
100
+ throw (0, oauth_error_1.invalidRequest)('code_challenge_method must be S256');
101
+ }
102
+ const codeChallenge = getQueryParam(ctx, 'code_challenge');
103
+ if (codeChallenge === undefined || !S256_CODE_CHALLENGE_PATTERN.test(codeChallenge)) {
104
+ throw (0, oauth_error_1.invalidRequest)('code_challenge is missing or malformed');
105
+ }
106
+ if (state === undefined || state === '') {
107
+ throw (0, oauth_error_1.invalidRequest)('Missing required parameter: state');
108
+ }
109
+ const appBase = new URL(options.forestAppUrl);
110
+ const authorizeUrl = new URL(`${appBase.pathname.replace(/\/$/, '')}/oauth/authorize`, appBase);
111
+ authorizeUrl.searchParams.set('client_id', clientId);
112
+ authorizeUrl.searchParams.set('redirect_uri', redirectUri);
113
+ authorizeUrl.searchParams.set('response_type', 'code');
114
+ authorizeUrl.searchParams.set('code_challenge', codeChallenge);
115
+ authorizeUrl.searchParams.set('code_challenge_method', 'S256');
116
+ authorizeUrl.searchParams.set('state', state);
117
+ authorizeUrl.searchParams.set('environmentId', String(options.environmentId));
118
+ ctx.redirect(authorizeUrl.toString());
119
+ }
120
+ catch (error) {
121
+ if (error instanceof oauth_error_1.OAuthRequestError) {
122
+ redirectAuthorizeError(ctx, redirectUri, state, error);
123
+ return;
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ function computeExpiresIn(serverTokens) {
129
+ const saasRemaining = serverTokens.expiresAt - Math.floor(Date.now() / 1000);
130
+ if (saasRemaining <= 0) {
131
+ throw (0, oauth_error_1.invalidGrant)('The Forest server access token is already expired');
132
+ }
133
+ return Math.min(bff_token_1.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN, saasRemaining);
134
+ }
135
+ function parseBody(ctx) {
136
+ const { body } = ctx.request;
137
+ if (typeof body !== 'object' || body === null) {
138
+ throw (0, oauth_error_1.invalidRequest)('Missing request body');
139
+ }
140
+ return body;
141
+ }
142
+ async function parseTokenRequest(ctx, options) {
143
+ const params = parseBody(ctx);
144
+ const request = {
145
+ code: requireBodyString(params, 'code'),
146
+ clientId: requireBodyString(params, 'client_id'),
147
+ codeVerifier: requireBodyString(params, 'code_verifier'),
148
+ redirectUri: requireBodyString(params, 'redirect_uri'),
149
+ };
150
+ assertValidCodeVerifier(request.codeVerifier);
151
+ const client = await options.serverClient.getRegisteredClient(request.clientId);
152
+ if (!client) {
153
+ throw (0, oauth_error_1.invalidClient)('Unknown client_id');
154
+ }
155
+ assertRegisteredRedirectUri(client.redirect_uris, request.redirectUri, oauth_error_1.invalidGrant);
156
+ return request;
157
+ }
158
+ async function exchangeForServerTokens(request, options) {
159
+ try {
160
+ return await options.serverClient.exchangeCode(request);
161
+ }
162
+ catch (error) {
163
+ if (error instanceof forest_server_client_1.OAuthExchangeError) {
164
+ options.logger('Warn', 'Forest server code exchange rejected', { saasError: error.error });
165
+ throw toSafeExchangeError(error.error);
166
+ }
167
+ throw error;
168
+ }
169
+ }
170
+ function isRetriableExchangeError(error) {
171
+ return !(error instanceof oauth_error_1.OAuthRequestError) || error.status >= 500;
172
+ }
173
+ async function resolveIdentity(renderingId, saasAccessToken, options) {
174
+ try {
175
+ return await options.serverClient.getUserInfo(renderingId, saasAccessToken);
176
+ }
177
+ catch (error) {
178
+ options.logger('Warn', 'Failed to resolve user info after exchange', { renderingId });
179
+ throw mapIdentityError(error);
180
+ }
181
+ }
182
+ function writeTokenResponse(ctx, accessToken, expiresInSeconds, refreshToken) {
183
+ ctx.set('Cache-Control', 'no-store');
184
+ ctx.set('Pragma', 'no-cache');
185
+ ctx.status = 200;
186
+ ctx.body = {
187
+ access_token: accessToken,
188
+ token_type: 'Bearer',
189
+ expires_in: expiresInSeconds,
190
+ refresh_token: refreshToken,
191
+ };
192
+ }
193
+ async function handleAuthorizationCodeGrant(ctx, options) {
194
+ const request = await parseTokenRequest(ctx, options);
195
+ if (!options.sessionStore.claimAuthorizationCode(request.code)) {
196
+ throw (0, oauth_error_1.invalidGrant)('Authorization code has already been used');
197
+ }
198
+ let serverTokens;
199
+ try {
200
+ serverTokens = await exchangeForServerTokens(request, options);
201
+ }
202
+ catch (error) {
203
+ if (isRetriableExchangeError(error)) {
204
+ options.sessionStore.releaseAuthorizationCode(request.code);
205
+ }
206
+ throw error;
207
+ }
208
+ const expiresInSeconds = computeExpiresIn(serverTokens);
209
+ const user = await resolveIdentity(serverTokens.renderingId, serverTokens.saasAccessToken, options);
210
+ const { sid, refreshToken } = options.sessionStore.create({
211
+ saasAccessToken: serverTokens.saasAccessToken,
212
+ saasRefreshToken: serverTokens.saasRefreshToken,
213
+ renderingId: serverTokens.renderingId,
214
+ userId: user.id,
215
+ });
216
+ const accessToken = (0, bff_token_1.issueBffAccessToken)({
217
+ sid,
218
+ user,
219
+ renderingId: serverTokens.renderingId,
220
+ authSecret: options.authSecret,
221
+ expiresInSeconds,
222
+ });
223
+ options.logger('Info', 'Issued BFF session token', {
224
+ renderingId: serverTokens.renderingId,
225
+ userId: user.id,
226
+ });
227
+ writeTokenResponse(ctx, accessToken, expiresInSeconds, refreshToken);
228
+ }
229
+ function expiresInFromAccessToken(saasAccessToken) {
230
+ const decoded = jsonwebtoken_1.default.decode(saasAccessToken);
231
+ if (typeof decoded?.exp !== 'number' || !Number.isFinite(decoded.exp)) {
232
+ return bff_token_1.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN;
233
+ }
234
+ const saasRemaining = decoded.exp - Math.floor(Date.now() / 1000);
235
+ if (saasRemaining <= 0) {
236
+ throw (0, oauth_error_1.sessionExpired)('The Forest server access token is already expired');
237
+ }
238
+ return Math.min(bff_token_1.BFF_ACCESS_TOKEN_MAX_EXPIRES_IN, saasRemaining);
239
+ }
240
+ async function issueAccessForSession(sid, renderingId, refreshToken, options) {
241
+ const saasAccessToken = await (0, session_lifecycle_1.default)({
242
+ sid,
243
+ store: options.sessionStore,
244
+ serverClient: options.serverClient,
245
+ });
246
+ const expiresInSeconds = expiresInFromAccessToken(saasAccessToken);
247
+ const user = await resolveIdentity(renderingId, saasAccessToken, options);
248
+ const accessToken = (0, bff_token_1.issueBffAccessToken)({
249
+ sid,
250
+ user,
251
+ renderingId,
252
+ authSecret: options.authSecret,
253
+ expiresInSeconds,
254
+ });
255
+ return { sid, accessToken, expiresInSeconds, newRefreshToken: refreshToken };
256
+ }
257
+ async function reissueThenCommitRotation(presentedToken, sid, renderingId, newRefreshToken, options) {
258
+ const result = await issueAccessForSession(sid, renderingId, newRefreshToken, options);
259
+ if (!options.sessionStore.commitRotation(presentedToken, newRefreshToken)) {
260
+ throw (0, oauth_error_1.sessionExpired)('Session expired during refresh');
261
+ }
262
+ return result;
263
+ }
264
+ async function handleRefreshGrant(ctx, options) {
265
+ const params = parseBody(ctx);
266
+ const presentedToken = requireBodyString(params, 'refresh_token');
267
+ const presentedHash = hashPresentedToken(presentedToken);
268
+ const inFlight = options.inFlightRefreshByPresentedHash;
269
+ let pending = inFlight.get(presentedHash);
270
+ if (!pending) {
271
+ const rotation = options.sessionStore.prepareRotation(presentedToken);
272
+ if (rotation.outcome === 'reuse') {
273
+ options.sessionStore.destroy(rotation.sid);
274
+ options.logger('Warn', 'Rotated-out refresh token replayed; session invalidated', {
275
+ sid: rotation.sid,
276
+ });
277
+ throw (0, oauth_error_1.sessionInvalidated)('Refresh token was already used; the session is invalidated');
278
+ }
279
+ if (rotation.outcome === 'unknown') {
280
+ throw (0, oauth_error_1.invalidGrant)('Unknown or malformed refresh token');
281
+ }
282
+ if (rotation.outcome === 'replay') {
283
+ options.logger('Info', 'Replayed the last issued pair for a benign retry', {
284
+ sid: rotation.sid,
285
+ });
286
+ pending = issueAccessForSession(rotation.sid, rotation.renderingId, rotation.lastIssuedRefreshToken, options).finally(() => {
287
+ inFlight.delete(presentedHash);
288
+ });
289
+ }
290
+ else {
291
+ pending = reissueThenCommitRotation(presentedToken, rotation.sid, rotation.renderingId, rotation.newRefreshToken, options).finally(() => {
292
+ inFlight.delete(presentedHash);
293
+ });
294
+ }
295
+ inFlight.set(presentedHash, pending);
296
+ }
297
+ const result = await pending;
298
+ options.logger('Info', 'Refreshed BFF session token', { sid: result.sid });
299
+ writeTokenResponse(ctx, result.accessToken, result.expiresInSeconds, result.newRefreshToken);
300
+ }
301
+ async function handleToken(ctx, options) {
302
+ const params = parseBody(ctx);
303
+ if (params.grant_type === 'refresh_token') {
304
+ await handleRefreshGrant(ctx, options);
305
+ return;
306
+ }
307
+ if (params.grant_type === 'authorization_code') {
308
+ await handleAuthorizationCodeGrant(ctx, options);
309
+ return;
310
+ }
311
+ throw (0, oauth_error_1.unsupportedGrantType)('Only authorization_code and refresh_token are supported');
312
+ }
313
+ function matchRoute(ctx) {
314
+ if (ctx.method === 'GET' && ctx.path === '/oauth/authorize')
315
+ return handleAuthorize;
316
+ if (ctx.method === 'POST' && ctx.path === '/oauth/token')
317
+ return handleToken;
318
+ return undefined;
319
+ }
320
+ function writeError(ctx, error, options) {
321
+ if (error instanceof oauth_error_1.OAuthRequestError) {
322
+ ctx.status = error.status;
323
+ ctx.body = (0, oauth_error_1.toErrorBody)(error);
324
+ return;
325
+ }
326
+ options.logger('Error', 'OAuth route failure', {
327
+ path: ctx.path,
328
+ cause: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
329
+ });
330
+ ctx.status = 500;
331
+ ctx.body = { error: 'server_error', error_description: 'OAuth processing failed' };
332
+ }
333
+ function createOAuthRoutes(options) {
334
+ const runtimeOptions = {
335
+ ...options,
336
+ inFlightRefreshByPresentedHash: new Map(),
337
+ };
338
+ return async function oauthRoutes(ctx, next) {
339
+ const handler = matchRoute(ctx);
340
+ if (!handler) {
341
+ await next();
342
+ return;
343
+ }
344
+ try {
345
+ await handler(ctx, runtimeOptions);
346
+ }
347
+ catch (error) {
348
+ writeError(ctx, error, runtimeOptions);
349
+ }
350
+ };
351
+ }
352
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2F1dGgtcm91dGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL29hdXRoL29hdXRoLXJvdXRlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQXFnQkEsb0NBcUJDO0FBbmhCRCxvREFBNEI7QUFDNUIsZ0VBQXdDO0FBRXhDLDJDQUFtRjtBQUNuRixpRUFBNEQ7QUFDNUQsK0NBVXVCO0FBQ3ZCLDRFQUEwRDtBQUUxRCxTQUFTLGtCQUFrQixDQUFDLEtBQWE7SUFDdkMsT0FBTyxnQkFBTSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0FBQ3ZFLENBQUM7QUFXRCxNQUFNLHFCQUFxQixHQUFHLDRCQUE0QixDQUFDO0FBQzNELE1BQU0sMkJBQTJCLEdBQUcscUJBQXFCLENBQUM7QUFFMUQsTUFBTSxvQkFBb0IsR0FBRyxJQUFJLEdBQUcsQ0FBQztJQUNuQyxpQkFBaUI7SUFDakIsZ0JBQWdCO0lBQ2hCLGVBQWU7SUFDZixxQkFBcUI7SUFDckIsd0JBQXdCO0lBQ3hCLGVBQWU7Q0FDaEIsQ0FBQyxDQUFDO0FBRUgsU0FBUyxhQUFhLENBQUMsR0FBWSxFQUFFLEdBQVc7SUFDOUMsTUFBTSxLQUFLLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUU3QixJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN6QixNQUFNLElBQUEsNEJBQWMsRUFBQyx1Q0FBdUMsR0FBRyxFQUFFLENBQUMsQ0FBQztJQUNyRSxDQUFDO0lBRUQsT0FBTyxLQUFLLENBQUM7QUFDZixDQUFDO0FBRUQsU0FBUyxpQkFBaUIsQ0FBQyxJQUE2QixFQUFFLEdBQVc7SUFDbkUsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRXhCLElBQUksT0FBTyxLQUFLLEtBQUss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
@@ -0,0 +1,8 @@
1
+ export interface PkcePair {
2
+ codeVerifier: string;
3
+ codeChallenge: string;
4
+ }
5
+ export declare function createVerifier(): string;
6
+ export declare function challengeFromVerifier(verifier: string): string;
7
+ export declare function createPkcePair(): PkcePair;
8
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createVerifier = createVerifier;
7
+ exports.challengeFromVerifier = challengeFromVerifier;
8
+ exports.createPkcePair = createPkcePair;
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ function base64url(buffer) {
11
+ return buffer.toString('base64url');
12
+ }
13
+ function createVerifier() {
14
+ return base64url(crypto_1.default.randomBytes(64));
15
+ }
16
+ function challengeFromVerifier(verifier) {
17
+ return base64url(crypto_1.default.createHash('sha256').update(verifier).digest());
18
+ }
19
+ function createPkcePair() {
20
+ const codeVerifier = createVerifier();
21
+ return { codeVerifier, codeChallenge: challengeFromVerifier(codeVerifier) };
22
+ }
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGtjZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vYXV0aC9wa2NlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBV0Esd0NBRUM7QUFFRCxzREFFQztBQUVELHdDQUlDO0FBdkJELG9EQUE0QjtBQU81QixTQUFTLFNBQVMsQ0FBQyxNQUFjO0lBQy9CLE9BQU8sTUFBTSxDQUFDLFFBQVEsQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUN0QyxDQUFDO0FBRUQsU0FBZ0IsY0FBYztJQUM1QixPQUFPLFNBQVMsQ0FBQyxnQkFBTSxDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO0FBQzNDLENBQUM7QUFFRCxTQUFnQixxQkFBcUIsQ0FBQyxRQUFnQjtJQUNwRCxPQUFPLFNBQVMsQ0FBQyxnQkFBTSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztBQUMxRSxDQUFDO0FBRUQsU0FBZ0IsY0FBYztJQUM1QixNQUFNLFlBQVksR0FBRyxjQUFjLEVBQUUsQ0FBQztJQUV0QyxPQUFPLEVBQUUsWUFBWSxFQUFFLGFBQWEsRUFBRSxxQkFBcUIsQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDO0FBQzlFLENBQUMifQ==
@@ -0,0 +1,9 @@
1
+ import type ForestServerClient from './forest-server-client';
2
+ import type { SessionStore } from './session-store';
3
+ export interface EnsureFreshServerAccessParams {
4
+ sid: string;
5
+ store: SessionStore;
6
+ serverClient: ForestServerClient;
7
+ }
8
+ export default function ensureFreshServerAccess({ sid, store, serverClient, }: EnsureFreshServerAccessParams): Promise<string>;
9
+ //# sourceMappingURL=session-lifecycle.d.ts.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = ensureFreshServerAccess;
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const forest_server_client_1 = require("./forest-server-client");
9
+ const oauth_error_1 = require("./oauth-error");
10
+ const CLIENT_ERROR_CODES = new Set(['invalid_grant', 'invalid_request', 'invalid_client']);
11
+ const inFlightRefreshesBySid = new Map();
12
+ function accessTokenExpiry(saasAccessToken) {
13
+ const decoded = jsonwebtoken_1.default.decode(saasAccessToken);
14
+ return decoded?.exp ?? 0;
15
+ }
16
+ async function refreshAndPersist(sid, store, serverClient) {
17
+ const currentRefresh = store.getSaasRefreshToken(sid);
18
+ if (currentRefresh === undefined) {
19
+ throw (0, oauth_error_1.sessionExpired)('Session not found or expired');
20
+ }
21
+ let rotated;
22
+ try {
23
+ rotated = await serverClient.refreshServerToken(currentRefresh);
24
+ }
25
+ catch (error) {
26
+ if (error instanceof forest_server_client_1.OAuthExchangeError && CLIENT_ERROR_CODES.has(error.error)) {
27
+ throw (0, oauth_error_1.sessionExpired)('The Forest server rejected the refresh token');
28
+ }
29
+ throw (0, oauth_error_1.serverError)('Failed to reach the Forest server to refresh the session');
30
+ }
31
+ if (!store.get(sid)) {
32
+ throw (0, oauth_error_1.sessionExpired)('Session expired during token refresh');
33
+ }
34
+ store.updateSaasTokens(sid, {
35
+ saasAccessToken: rotated.saasAccessToken,
36
+ saasRefreshToken: rotated.saasRefreshToken,
37
+ });
38
+ return rotated.saasAccessToken;
39
+ }
40
+ async function ensureFreshServerAccess({ sid, store, serverClient, }) {
41
+ const session = store.get(sid);
42
+ if (!session) {
43
+ throw (0, oauth_error_1.sessionExpired)('Session not found or expired');
44
+ }
45
+ if (accessTokenExpiry(session.saasAccessToken) > Math.floor(Date.now() / 1000)) {
46
+ return session.saasAccessToken;
47
+ }
48
+ const existing = inFlightRefreshesBySid.get(sid);
49
+ if (existing)
50
+ return existing;
51
+ const refresh = refreshAndPersist(sid, store, serverClient).finally(() => {
52
+ inFlightRefreshesBySid.delete(sid);
53
+ });
54
+ inFlightRefreshesBySid.set(sid, refresh);
55
+ return refresh;
56
+ }
57
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vzc2lvbi1saWZlY3ljbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvb2F1dGgvc2Vzc2lvbi1saWZlY3ljbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUEyREEsMENBd0JDO0FBaEZELGdFQUF3QztBQUV4QyxpRUFBNEQ7QUFDNUQsK0NBQTREO0FBUTVELE1BQU0sa0JBQWtCLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxlQUFlLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDO0FBRTNGLE1BQU0sc0JBQXNCLEdBQUcsSUFBSSxHQUFHLEVBQTJCLENBQUM7QUFFbEUsU0FBUyxpQkFBaUIsQ0FBQyxlQUF1QjtJQUNoRCxNQUFNLE9BQU8sR0FBRyxzQkFBWSxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQTRCLENBQUM7SUFFaEYsT0FBTyxPQUFPLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUMzQixDQUFDO0FBRUQsS0FBSyxVQUFVLGlCQUFpQixDQUM5QixHQUFXLEVBQ1gsS0FBbUIsRUFDbkIsWUFBZ0M7SUFFaEMsTUFBTSxjQUFjLEdBQUcsS0FBSyxDQUFDLG1CQUFtQixDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRXRELElBQUksY0FBYyxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQ2pDLE1BQU0sSUFBQSw0QkFBYyxFQUFDLDhCQUE4QixDQUFDLENBQUM7SUFDdkQsQ0FBQztJQUVELElBQUksT0FBc0UsQ0FBQztJQUUzRSxJQUFJLENBQUM7UUFDSCxPQUFPLEdBQUcsTUFBTSxZQUFZLENBQUMsa0JBQWtCLENBQUMsY0FBYyxDQUFDLENBQUM7SUFDbEUsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixJQUFJLEtBQUssWUFBWSx5Q0FBa0IsSUFBSSxrQkFBa0IsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDL0UsTUFBTSxJQUFBLDRCQUFjLEVBQUMsOENBQThDLENBQUMsQ0FBQztRQUN2RSxDQUFDO1FBRUQsTUFBTSxJQUFBLHlCQUFXLEVBQUMsMERBQTBELENBQUMsQ0FBQztJQUNoRixDQUFDO0lBRUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUNwQixNQUFNLElBQUEsNEJBQWMsRUFBQyxzQ0FBc0MsQ0FBQyxDQUFDO0lBQy9ELENBQUM7SUFFRCxLQUFLLENBQUMsZ0JBQWdCLENBQUMsR0FBRyxFQUFFO1FBQzFCLGVBQWUsRUFBRSxPQUFPLENBQUMsZUFBZTtRQUN4QyxnQkFBZ0IsRUFBRSxPQUFPLENBQUMsZ0JBQWdCO0tBQzNDLENBQUMsQ0FBQztJQUVILE9BQU8sT0FBTyxDQUFDLGVBQWUsQ0FBQztBQUNqQyxDQUFDO0FBRWMsS0FBSyxVQUFVLHVCQUF1QixDQUFDLEVBQ3BELEdBQUcsRUFDSCxLQUFLLEVBQ0wsWUFBWSxHQUNrQjtJQUM5QixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRS9CLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNiLE1BQU0sSUFBQSw0QkFBYyxFQUFDLDhCQUE4QixDQUFDLENBQUM7SUFDdkQsQ0FBQztJQUVELElBQUksaUJBQWlCLENBQUMsT0FBTyxDQUFDLGVBQWUsQ0FBQyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDL0UsT0FBTyxPQUFPLENBQUMsZUFBZSxDQUFDO0lBQ2pDLENBQUM7SUFFRCxNQUFNLFFBQVEsR0FBRyxzQkFBc0IsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDakQsSUFBSSxRQUFRO1FBQUUsT0FBTyxRQUFRLENBQUM7SUFFOUIsTUFBTSxPQUFPLEdBQUcsaUJBQWlCLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxZQUFZLENBQUMsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFO1FBQ3ZFLHNCQUFzQixDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNyQyxDQUFDLENBQUMsQ0FBQztJQUNILHNCQUFzQixDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFFekMsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQyJ9
@@ -0,0 +1,61 @@
1
+ import type { EncryptedBlob, TokenCipher } from './token-cipher';
2
+ export interface CreateSessionInput {
3
+ saasAccessToken: string;
4
+ saasRefreshToken: string;
5
+ renderingId: number;
6
+ userId: number;
7
+ }
8
+ export interface StoredSession {
9
+ saasAccessToken: string;
10
+ saasRefreshTokenBlob: EncryptedBlob;
11
+ refreshTokenHash: string;
12
+ renderingId: number;
13
+ userId: number;
14
+ expiresAt: number;
15
+ }
16
+ export interface CreatedSession {
17
+ sid: string;
18
+ refreshToken: string;
19
+ }
20
+ export type PrepareRotationResult = {
21
+ outcome: 'ready';
22
+ sid: string;
23
+ renderingId: number;
24
+ newRefreshToken: string;
25
+ } | {
26
+ outcome: 'replay';
27
+ sid: string;
28
+ renderingId: number;
29
+ lastIssuedRefreshToken: string;
30
+ } | {
31
+ outcome: 'reuse';
32
+ sid: string;
33
+ } | {
34
+ outcome: 'unknown';
35
+ };
36
+ export interface UpdateSaasTokensInput {
37
+ saasAccessToken: string;
38
+ saasRefreshToken: string;
39
+ }
40
+ export interface SessionStore {
41
+ create(input: CreateSessionInput): CreatedSession;
42
+ get(sid: string): StoredSession | undefined;
43
+ getSaasRefreshToken(sid: string): string | undefined;
44
+ updateSaasTokens(sid: string, tokens: UpdateSaasTokensInput): void;
45
+ prepareRotation(presentedToken: string): PrepareRotationResult;
46
+ commitRotation(presentedToken: string, newRefreshToken: string): boolean;
47
+ destroy(sid: string): void;
48
+ claimAuthorizationCode(code: string): boolean;
49
+ releaseAuthorizationCode(code: string): void;
50
+ pendingClaimCount(): number;
51
+ }
52
+ export interface SessionStoreOptions {
53
+ cipher: TokenCipher;
54
+ now: () => number;
55
+ sessionTtlSeconds: number;
56
+ authCodeTtlSeconds?: number;
57
+ maxPendingCodes?: number;
58
+ }
59
+ export declare function createSid(): string;
60
+ export default function createInMemorySessionStore({ cipher, now, sessionTtlSeconds, authCodeTtlSeconds, maxPendingCodes, }: SessionStoreOptions): SessionStore;
61
+ //# sourceMappingURL=session-store.d.ts.map