astro-tokenkit 1.0.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,293 @@
1
+ // packages/astro-tokenkit/src/client/client.ts
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ import { APIError, AuthError, NetworkError, TimeoutError } from '../types';
12
+ import { TokenManager } from '../auth/manager';
13
+ import { getContext } from './context';
14
+ import { calculateDelay, shouldRetry, sleep } from '../utils/retry';
15
+ /**
16
+ * API Client
17
+ */
18
+ export class APIClient {
19
+ constructor(config) {
20
+ this.config = config;
21
+ this.contextOptions = {
22
+ context: config.context,
23
+ getContextStore: config.getContextStore,
24
+ };
25
+ // Initialize token manager if auth is configured
26
+ if (config.auth) {
27
+ this.tokenManager = new TokenManager(config.auth, config.baseURL);
28
+ }
29
+ }
30
+ /**
31
+ * GET request
32
+ */
33
+ get(url, options) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ return this.request(Object.assign({ method: 'GET', url }, options));
36
+ });
37
+ }
38
+ /**
39
+ * POST request
40
+ */
41
+ post(url, data, options) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ return this.request(Object.assign({ method: 'POST', url,
44
+ data }, options));
45
+ });
46
+ }
47
+ /**
48
+ * PUT request
49
+ */
50
+ put(url, data, options) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ return this.request(Object.assign({ method: 'PUT', url,
53
+ data }, options));
54
+ });
55
+ }
56
+ /**
57
+ * PATCH request
58
+ */
59
+ patch(url, data, options) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ return this.request(Object.assign({ method: 'PATCH', url,
62
+ data }, options));
63
+ });
64
+ }
65
+ /**
66
+ * DELETE request
67
+ */
68
+ delete(url, options) {
69
+ return __awaiter(this, void 0, void 0, function* () {
70
+ return this.request(Object.assign({ method: 'DELETE', url }, options));
71
+ });
72
+ }
73
+ /**
74
+ * Generic request method
75
+ */
76
+ request(config) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ const ctx = getContext(config.ctx, this.contextOptions);
79
+ let attempt = 0;
80
+ let lastError;
81
+ while (true) {
82
+ attempt++;
83
+ try {
84
+ const response = yield this.executeRequest(config, ctx, attempt);
85
+ return response.data;
86
+ }
87
+ catch (error) {
88
+ lastError = error;
89
+ // Check if we should retry
90
+ if (shouldRetry(error.status, attempt, this.config.retry)) {
91
+ const delay = calculateDelay(attempt, this.config.retry);
92
+ yield sleep(delay);
93
+ continue;
94
+ }
95
+ // No more retries
96
+ throw error;
97
+ }
98
+ }
99
+ });
100
+ }
101
+ /**
102
+ * Execute single request
103
+ */
104
+ executeRequest(config, ctx, attempt) {
105
+ return __awaiter(this, void 0, void 0, function* () {
106
+ var _a, _b, _c, _d, _e;
107
+ // Ensure valid session (if auth is enabled)
108
+ if (this.tokenManager && !config.skipAuth) {
109
+ yield this.tokenManager.ensure(ctx);
110
+ }
111
+ // Build full URL
112
+ const fullURL = this.buildURL(config.url, config.params);
113
+ // Build headers
114
+ const headers = this.buildHeaders(config, ctx);
115
+ // Build request init
116
+ const init = {
117
+ method: config.method,
118
+ headers,
119
+ signal: config.signal,
120
+ };
121
+ // Add body for non-GET requests
122
+ if (config.data && config.method !== 'GET') {
123
+ init.body = JSON.stringify(config.data);
124
+ }
125
+ // Apply request interceptors
126
+ let requestConfig = Object.assign({}, config);
127
+ if ((_a = this.config.interceptors) === null || _a === void 0 ? void 0 : _a.request) {
128
+ for (const interceptor of this.config.interceptors.request) {
129
+ requestConfig = yield interceptor(requestConfig, ctx);
130
+ }
131
+ }
132
+ // Execute fetch with timeout
133
+ const timeout = (_c = (_b = config.timeout) !== null && _b !== void 0 ? _b : this.config.timeout) !== null && _c !== void 0 ? _c : 30000;
134
+ const controller = new AbortController();
135
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
136
+ try {
137
+ const response = yield fetch(fullURL, Object.assign(Object.assign({}, init), { signal: controller.signal }));
138
+ clearTimeout(timeoutId);
139
+ // Handle 401 (try refresh and retry once)
140
+ if (response.status === 401 && this.tokenManager && !config.skipAuth && attempt === 1) {
141
+ // Clear and try fresh session
142
+ const session = yield this.tokenManager.ensure(ctx);
143
+ if (session) {
144
+ // Retry with new token
145
+ return this.executeRequest(config, ctx, attempt + 1);
146
+ }
147
+ }
148
+ // Parse response
149
+ const apiResponse = yield this.parseResponse(response, fullURL);
150
+ // Apply response interceptors
151
+ if ((_d = this.config.interceptors) === null || _d === void 0 ? void 0 : _d.response) {
152
+ let interceptedResponse = apiResponse;
153
+ for (const interceptor of this.config.interceptors.response) {
154
+ interceptedResponse = yield interceptor(interceptedResponse, ctx);
155
+ }
156
+ return interceptedResponse;
157
+ }
158
+ return apiResponse;
159
+ }
160
+ catch (error) {
161
+ clearTimeout(timeoutId);
162
+ // Apply error interceptors
163
+ if ((_e = this.config.interceptors) === null || _e === void 0 ? void 0 : _e.error) {
164
+ for (const interceptor of this.config.interceptors.error) {
165
+ yield interceptor(error, ctx);
166
+ }
167
+ }
168
+ // Transform errors
169
+ if (error instanceof Error && error.name === 'AbortError') {
170
+ throw new TimeoutError(`Request timeout after ${timeout}ms`, requestConfig);
171
+ }
172
+ if (error instanceof APIError) {
173
+ throw error;
174
+ }
175
+ throw new NetworkError(error.message, requestConfig);
176
+ }
177
+ });
178
+ }
179
+ /**
180
+ * Parse response
181
+ */
182
+ parseResponse(response, url) {
183
+ return __awaiter(this, void 0, void 0, function* () {
184
+ let data;
185
+ // Try to parse JSON
186
+ const contentType = response.headers.get('content-type');
187
+ if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')) {
188
+ try {
189
+ data = yield response.json();
190
+ }
191
+ catch (_a) {
192
+ data = (yield response.text());
193
+ }
194
+ }
195
+ else {
196
+ data = (yield response.text());
197
+ }
198
+ // Check if response is ok
199
+ if (!response.ok) {
200
+ if (response.status === 401 || response.status === 403) {
201
+ throw new AuthError(`Authentication failed: ${response.status} ${response.statusText}`, response.status, data);
202
+ }
203
+ throw new APIError(`Request failed: ${response.status} ${response.statusText}`, response.status, data);
204
+ }
205
+ return {
206
+ data,
207
+ status: response.status,
208
+ statusText: response.statusText,
209
+ headers: response.headers,
210
+ url,
211
+ };
212
+ });
213
+ }
214
+ /**
215
+ * Build full URL with query params
216
+ */
217
+ buildURL(url, params) {
218
+ const fullURL = url.startsWith('http') ? url : this.config.baseURL + url;
219
+ if (!params)
220
+ return fullURL;
221
+ const urlObj = new URL(fullURL);
222
+ Object.entries(params).forEach(([key, value]) => {
223
+ if (value !== undefined && value !== null) {
224
+ urlObj.searchParams.append(key, String(value));
225
+ }
226
+ });
227
+ return urlObj.toString();
228
+ }
229
+ /**
230
+ * Build request headers
231
+ */
232
+ buildHeaders(config, ctx) {
233
+ var _a, _b;
234
+ const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, this.config.headers), config.headers);
235
+ // Add auth token if available
236
+ if (this.tokenManager && !config.skipAuth) {
237
+ const session = this.tokenManager.getSession(ctx);
238
+ if (session === null || session === void 0 ? void 0 : session.accessToken) {
239
+ const injectFn = (_b = (_a = this.config.auth) === null || _a === void 0 ? void 0 : _a.injectToken) !== null && _b !== void 0 ? _b : ((token) => `Bearer ${token}`);
240
+ headers['Authorization'] = injectFn(session.accessToken);
241
+ }
242
+ }
243
+ return headers;
244
+ }
245
+ /**
246
+ * Login
247
+ */
248
+ login(credentials, ctx) {
249
+ return __awaiter(this, void 0, void 0, function* () {
250
+ if (!this.tokenManager) {
251
+ throw new Error('Auth is not configured for this client');
252
+ }
253
+ const context = getContext(ctx, this.contextOptions);
254
+ yield this.tokenManager.login(context, credentials);
255
+ });
256
+ }
257
+ /**
258
+ * Logout
259
+ */
260
+ logout(ctx) {
261
+ return __awaiter(this, void 0, void 0, function* () {
262
+ if (!this.tokenManager) {
263
+ throw new Error('Auth is not configured for this client');
264
+ }
265
+ const context = getContext(ctx, this.contextOptions);
266
+ yield this.tokenManager.logout(context);
267
+ });
268
+ }
269
+ /**
270
+ * Check if authenticated
271
+ */
272
+ isAuthenticated(ctx) {
273
+ if (!this.tokenManager)
274
+ return false;
275
+ const context = getContext(ctx, this.contextOptions);
276
+ return this.tokenManager.isAuthenticated(context);
277
+ }
278
+ /**
279
+ * Get current session
280
+ */
281
+ getSession(ctx) {
282
+ if (!this.tokenManager)
283
+ return null;
284
+ const context = getContext(ctx, this.contextOptions);
285
+ return this.tokenManager.getSession(context);
286
+ }
287
+ }
288
+ /**
289
+ * Create API client
290
+ */
291
+ export function createClient(config) {
292
+ return new APIClient(config);
293
+ }
@@ -0,0 +1,17 @@
1
+ import type { TokenKitContext } from '../types';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ /**
4
+ * Configure shared AsyncLocalStorage
5
+ *
6
+ * @param storage - Shared AsyncLocalStorage instance
7
+ * @param key - Key to access Astro context in the storage
8
+ */
9
+ export declare function setSharedContextStorage(storage: AsyncLocalStorage<any>, key?: string): void;
10
+ /**
11
+ * Get context from shared storage
12
+ */
13
+ export declare function getContext(explicitCtx?: TokenKitContext): TokenKitContext;
14
+ /**
15
+ * Bind context (only needed if not using shared storage)
16
+ */
17
+ export declare function bindContext<T>(ctx: TokenKitContext, fn: () => T): T;
@@ -0,0 +1,60 @@
1
+ // packages/astro-tokenkit/src/client/context-shared.ts
2
+ /**
3
+ * OPTION: Share AsyncLocalStorage with other libraries
4
+ *
5
+ * If you have another library (like SessionKit) that uses AsyncLocalStorage,
6
+ * you can share the same instance to avoid performance overhead.
7
+ *
8
+ * Usage:
9
+ *
10
+ * // In your shared context file:
11
+ * import { AsyncLocalStorage } from 'node:async_hooks';
12
+ * export const appContext = new AsyncLocalStorage<{ astro: TokenKitContext }>();
13
+ *
14
+ * // Then configure TokenKit to use it:
15
+ * import { setSharedContextStorage } from 'astro-tokenkit';
16
+ * import { appContext } from './shared-context';
17
+ *
18
+ * setSharedContextStorage(appContext, 'astro');
19
+ */
20
+ let sharedStorage = null;
21
+ let contextKey = null;
22
+ /**
23
+ * Configure shared AsyncLocalStorage
24
+ *
25
+ * @param storage - Shared AsyncLocalStorage instance
26
+ * @param key - Key to access Astro context in the storage
27
+ */
28
+ export function setSharedContextStorage(storage, key = 'astro') {
29
+ sharedStorage = storage;
30
+ contextKey = key;
31
+ }
32
+ /**
33
+ * Get context from shared storage
34
+ */
35
+ export function getContext(explicitCtx) {
36
+ if (explicitCtx) {
37
+ return explicitCtx;
38
+ }
39
+ if (sharedStorage && contextKey) {
40
+ const store = sharedStorage.getStore();
41
+ const ctx = store === null || store === void 0 ? void 0 : store[contextKey];
42
+ if (ctx) {
43
+ return ctx;
44
+ }
45
+ }
46
+ throw new Error('Astro context not found. Either:\n' +
47
+ '1. Pass context explicitly: api.get("/path", { ctx: Astro })\n' +
48
+ '2. Configure shared storage: setSharedContextStorage(storage, "key")');
49
+ }
50
+ /**
51
+ * Bind context (only needed if not using shared storage)
52
+ */
53
+ export function bindContext(ctx, fn) {
54
+ if (sharedStorage && contextKey) {
55
+ const currentStore = sharedStorage.getStore() || {};
56
+ return sharedStorage.run(Object.assign(Object.assign({}, currentStore), { [contextKey]: ctx }), fn);
57
+ }
58
+ // Fallback: context must be passed explicitly
59
+ return fn();
60
+ }
@@ -0,0 +1,21 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { TokenKitContext } from '../types';
3
+ /**
4
+ * Configuration for context handling
5
+ */
6
+ export interface ContextOptions {
7
+ context?: AsyncLocalStorage<any>;
8
+ getContextStore?: () => TokenKitContext | undefined | null;
9
+ }
10
+ /**
11
+ * Bind Astro context for the current async scope
12
+ */
13
+ export declare function bindContext<T>(ctx: TokenKitContext, fn: () => T, options?: ContextOptions): T;
14
+ /**
15
+ * Get current Astro context (from middleware binding or explicit)
16
+ */
17
+ export declare function getContext(explicitCtx?: TokenKitContext, options?: ContextOptions): TokenKitContext;
18
+ /**
19
+ * Check if context is available
20
+ */
21
+ export declare function hasContext(explicitCtx?: TokenKitContext, options?: ContextOptions): boolean;
@@ -0,0 +1,37 @@
1
+ // packages/astro-tokenkit/src/client/context.ts
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ /**
4
+ * Async local storage for Astro context
5
+ */
6
+ const defaultContextStorage = new AsyncLocalStorage();
7
+ /**
8
+ * Bind Astro context for the current async scope
9
+ */
10
+ export function bindContext(ctx, fn, options) {
11
+ const storage = (options === null || options === void 0 ? void 0 : options.context) || defaultContextStorage;
12
+ return storage.run(ctx, fn);
13
+ }
14
+ /**
15
+ * Get current Astro context (from middleware binding or explicit)
16
+ */
17
+ export function getContext(explicitCtx, options) {
18
+ const store = (options === null || options === void 0 ? void 0 : options.getContextStore)
19
+ ? options.getContextStore()
20
+ : ((options === null || options === void 0 ? void 0 : options.context) || defaultContextStorage).getStore();
21
+ const ctx = explicitCtx || store;
22
+ if (!ctx) {
23
+ throw new Error('Astro context not found. Either:\n' +
24
+ '1. Use api.middleware() to bind context automatically, or\n' +
25
+ '2. Pass context explicitly: api.get("/path", { ctx: Astro })');
26
+ }
27
+ return ctx;
28
+ }
29
+ /**
30
+ * Check if context is available
31
+ */
32
+ export function hasContext(explicitCtx, options) {
33
+ const store = (options === null || options === void 0 ? void 0 : options.getContextStore)
34
+ ? options.getContextStore()
35
+ : ((options === null || options === void 0 ? void 0 : options.context) || defaultContextStorage).getStore();
36
+ return !!(explicitCtx || store);
37
+ }