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.
- package/LICENSE +21 -0
- package/dist/auth/detector.d.ts +9 -0
- package/dist/auth/detector.js +123 -0
- package/dist/auth/manager.d.ts +38 -0
- package/dist/auth/manager.js +216 -0
- package/dist/auth/policy.d.ts +21 -0
- package/dist/auth/policy.js +66 -0
- package/dist/auth/storage.d.ts +40 -0
- package/dist/auth/storage.js +72 -0
- package/dist/client/client.d.ts +72 -0
- package/dist/client/client.js +293 -0
- package/dist/client/context-shared.d.ts +17 -0
- package/dist/client/context-shared.js +60 -0
- package/dist/client/context.d.ts +21 -0
- package/dist/client/context.js +37 -0
- package/dist/index.cjs +995 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +983 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +12 -0
- package/dist/integration.js +22 -0
- package/dist/middleware.d.ts +6 -0
- package/dist/middleware.js +40 -0
- package/dist/types.d.ts +200 -0
- package/dist/types.js +40 -0
- package/dist/utils/retry.d.ts +17 -0
- package/dist/utils/retry.js +41 -0
- package/dist/utils/time.d.ts +9 -0
- package/dist/utils/time.js +35 -0
- package/package.json +65 -0
|
@@ -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
|
+
}
|