@thorprovider/adapters 4.2.1 → 5.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.
Files changed (60) hide show
  1. package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
  2. package/dist/index.d.mts +225 -387
  3. package/dist/index.d.ts +225 -387
  4. package/dist/index.js +613 -3062
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +609 -3052
  7. package/dist/index.mjs.map +1 -1
  8. package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
  9. package/docs/authentication.md +115 -62
  10. package/docs/capabilities-system.md +4 -2
  11. package/docs/category-customization.md +37 -29
  12. package/docs/fetch-reliability-solution.md +127 -278
  13. package/docs/getting-started.md +43 -29
  14. package/docs/profile-management.md +68 -38
  15. package/docs/services-overview.md +4 -2
  16. package/package.json +1 -1
  17. package/quick-reference.md +33 -0
  18. package/src/backend/factory.ts +1 -11
  19. package/src/factory/index.ts +74 -52
  20. package/src/index.ts +5 -15
  21. package/src/providers/index.ts +1 -16
  22. package/src/providers/medusa/admin.test.ts +404 -0
  23. package/src/providers/medusa/admin.ts +442 -0
  24. package/src/providers/medusa/auth.test.ts +137 -0
  25. package/src/providers/medusa/auth.ts +96 -1
  26. package/src/providers/medusa/index.ts +21 -1
  27. package/src/resilience/README.md +70 -48
  28. package/src/types/capabilities.types.ts +0 -250
  29. package/src/core/errors.js +0 -161
  30. package/src/core/fetch-utils.js +0 -240
  31. package/src/core/logger.js +0 -63
  32. package/src/metadata/channel-scope-validation.js +0 -136
  33. package/src/metadata/channel-scope.js +0 -174
  34. package/src/metadata/index.js +0 -31
  35. package/src/providers/bigcommerce/bigcommerce.ts +0 -6
  36. package/src/providers/bigcommerce/index.ts +0 -315
  37. package/src/providers/bigcommerce/transforms.ts +0 -227
  38. package/src/providers/magento/index.ts +0 -387
  39. package/src/providers/magento/magento.ts +0 -5
  40. package/src/providers/magento/queries.ts +0 -267
  41. package/src/providers/magento/transforms.ts +0 -249
  42. package/src/providers/medusa/auth.js +0 -601
  43. package/src/providers/medusa/index.js +0 -2485
  44. package/src/providers/medusa/payment.js +0 -265
  45. package/src/providers/medusa/transforms.js +0 -505
  46. package/src/providers/shopify/auth.ts +0 -402
  47. package/src/providers/shopify/index.ts +0 -444
  48. package/src/providers/shopify/mutations.ts +0 -247
  49. package/src/providers/shopify/queries.ts +0 -324
  50. package/src/providers/shopify/shopify.ts +0 -7
  51. package/src/providers/shopify/transforms.ts +0 -275
  52. package/src/providers/spree/index.ts +0 -344
  53. package/src/providers/spree/spree.ts +0 -5
  54. package/src/providers/spree/transforms.ts +0 -254
  55. package/src/providers/woocommerce/index.ts +0 -310
  56. package/src/providers/woocommerce/transforms.ts +0 -254
  57. package/src/providers/woocommerce/woocommerce.ts +0 -5
  58. package/src/types/capabilities.types.js +0 -141
  59. package/src/types/provider.js +0 -9
  60. package/src/utils/payment-utils.js +0 -152
@@ -49,6 +49,7 @@ import { validatePaymentId } from '../../utils/payment-utils';
49
49
  import { transformProduct, transformCart, transformCollection, transformCategory, transformOrder, transformCustomer, transformStockLocation, transformInventoryLevel, transformAddress, transformStorefrontConfig } from './transforms';
50
50
  import { MedusaAuthProvider } from './auth';
51
51
  import { getPaymentMethods } from './payment';
52
+ import { MedusaAdminClient } from './admin';
52
53
  import {
53
54
  buildChannelScopeMetadata,
54
55
  filterByChannel,
@@ -163,6 +164,19 @@ export class MedusaProvider implements CommerceProvider {
163
164
  * Only created when adminConfig.apiKey is provided.
164
165
  */
165
166
  private sdkAdmin?: Medusa;
167
+
168
+ /**
169
+ * Admin client for Thor multi-tenant endpoints (`/admin/thor/`).
170
+ * Available when `adminConfig.apiKey` is provided.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const { customers } = await commerce.admin.getChannelCustomers('sc_01JXXXXX');
175
+ * const { api_keys } = await commerce.admin.getChannelApiKeys('sc_01JXXXXX');
176
+ * await commerce.admin.updateStorefrontConfig('sc_01JXXXXX', { theme_accent_color: '#EF4444', ... });
177
+ * ```
178
+ */
179
+ public readonly admin?: MedusaAdminClient;
166
180
  /** Cached distinct product status values — rarely changes, cached for process lifetime */
167
181
  private _cachedProductStatuses: string[] | null = null;
168
182
 
@@ -253,12 +267,18 @@ export class MedusaProvider implements CommerceProvider {
253
267
  debug: config.debug ?? false,
254
268
  // No publishableKey or auth config: this instance is admin-only, no JWT needed
255
269
  });
270
+ // Admin client for Thor multi-tenant custom endpoints (/admin/thor/)
271
+ (this as any).admin = new MedusaAdminClient({
272
+ baseUrl: config.baseUrl,
273
+ apiKey: config.adminConfig.apiKey,
274
+ debug: config.debug,
275
+ });
256
276
  this.logger.info('[Admin Auth] Dedicated admin SDK instance created with API key');
257
277
  }
258
278
 
259
279
  // Initialize auth provider if config is provided
260
280
  if (config.auth) {
261
- this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug);
281
+ this.auth = new MedusaAuthProvider(this.sdk, config.auth, config.debug, config.baseUrl, config.publishableKey);
262
282
  this.logger.info('Auth provider initialized');
263
283
  }
264
284
 
@@ -1,3 +1,14 @@
1
+ ---
2
+ title: Resilience Patterns
3
+ purpose: Documents the Circuit Breaker and retry patterns for robust backend integration
4
+ type: reference
5
+ tags: [resilience, circuit-breaker, retry, fetch, patterns]
6
+ related:
7
+ - ../../docs/fetch-reliability-solution.md
8
+ - ../../docs/RETRY_SOLUTION_SUMMARY.md
9
+ updated: 2026-05-03
10
+ ---
11
+
1
12
  # Resilience Patterns
2
13
 
3
14
  **Framework-agnostic resilience patterns for robust backend integration.**
@@ -10,10 +21,17 @@ This module provides enterprise-grade resilience patterns for handling backend f
10
21
 
11
22
  ---
12
23
 
13
- ## 📦 Export
24
+ ## 📦 Exports
14
25
 
15
26
  ```typescript
16
- import { CircuitBreaker, backendCircuitBreaker } from '@thorprovider/adapters/resilience';
27
+ import {
28
+ CircuitBreaker,
29
+ backendCircuitBreaker,
30
+ CircuitBreakerOpenError,
31
+ type CircuitState,
32
+ type CircuitBreakerConfig,
33
+ type CircuitBreakerStats,
34
+ } from '@thorprovider/adapters';
17
35
  ```
18
36
 
19
37
  ---
@@ -53,7 +71,7 @@ The Circuit Breaker pattern prevents cascading failures when the backend is unav
53
71
  ### Basic Usage
54
72
 
55
73
  ```typescript
56
- import { CircuitBreaker } from '@thorprovider/adapters/resilience';
74
+ import { CircuitBreaker } from '@thorprovider/adapters';
57
75
 
58
76
  const breaker = new CircuitBreaker({
59
77
  failureThreshold: 5, // Open after 5 failures
@@ -63,7 +81,7 @@ const breaker = new CircuitBreaker({
63
81
 
64
82
  // With fallback
65
83
  const data = await breaker.execute(
66
- () => fetch('/api/data'),
84
+ () => fetch('/api/data').then(r => r.json()),
67
85
  { default: 'value' } // Fallback if circuit is open
68
86
  );
69
87
 
@@ -81,11 +99,11 @@ try {
81
99
  interface CircuitBreakerConfig {
82
100
  /** Number of failures before opening circuit */
83
101
  failureThreshold: number;
84
-
102
+
85
103
  /** Time in ms to wait before attempting recovery */
86
104
  resetTimeout: number;
87
-
88
- /** Name for logging/监控 */
105
+
106
+ /** Name for logging/monitoring */
89
107
  name?: string;
90
108
  }
91
109
  ```
@@ -103,7 +121,7 @@ console.log({
103
121
  failureCount: stats.failureCount,
104
122
  totalRequests: stats.totalRequests,
105
123
  totalFailures: stats.totalFailures,
106
- totalSuccesses: stats.totalSuccesses
124
+ totalSuccesses: stats.totalSuccesses,
107
125
  });
108
126
 
109
127
  // Manual reset (force recovery)
@@ -117,35 +135,34 @@ breaker.manualReset();
117
135
  A pre-configured global instance for backend API calls:
118
136
 
119
137
  ```typescript
120
- import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
138
+ import { backendCircuitBreaker } from '@thorprovider/adapters';
121
139
 
122
140
  // Use in adapters
123
141
  async function getProducts() {
124
142
  return backendCircuitBreaker.execute(
125
- () => fetch('/api/products'),
143
+ () => commerce.getProducts(),
126
144
  [] // Empty array fallback
127
145
  );
128
146
  }
129
147
  ```
130
148
 
131
- **Configuration:**
149
+ **Default configuration:**
132
150
  - `failureThreshold`: 5 failures
133
151
  - `resetTimeout`: 60,000ms (1 minute)
134
- - `name`: 'backend-api'
152
+ - `name`: `'backend-api'`
135
153
 
136
154
  ---
137
155
 
138
156
  ## 🎨 Usage Patterns
139
157
 
140
- ### Pattern 1: L2 Adapter with Graceful Degradation
158
+ ### Pattern 1: Adapter with Graceful Degradation
141
159
 
142
160
  ```typescript
143
- // packages/adapters/src/medusa/product-adapter.ts
144
- import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
161
+ import { backendCircuitBreaker } from '@thorprovider/adapters';
145
162
 
146
163
  export async function getProducts(options: GetProductsOptions) {
147
164
  return backendCircuitBreaker.execute(
148
- () => medusaClient.products.list(options),
165
+ () => commerce.getProducts(options),
149
166
  [] // Return empty array if backend is down
150
167
  );
151
168
  }
@@ -156,23 +173,23 @@ export async function getProducts(options: GetProductsOptions) {
156
173
  - ✅ User sees "No products" instead of error page
157
174
  - ✅ Backend gets time to recover
158
175
 
159
- ### Pattern 2: L5 Server Component with Fallback UI
176
+ ### Pattern 2: Server Component with Fallback UI
160
177
 
161
178
  ```typescript
162
- // packages/core/app/products/page.tsx
163
- import { backendCircuitBreaker } from '@thorprovider/adapters/resilience';
164
- import { getProducts } from '@thorprovider/adapters/medusa';
179
+ // app/products/page.tsx
180
+ import { backendCircuitBreaker } from '@thorprovider/adapters';
181
+ import { commerce } from '@/lib/commerce';
165
182
 
166
183
  export default async function ProductsPage() {
167
184
  const products = await backendCircuitBreaker.execute(
168
- () => getProducts(),
169
- [] // Fallback to empty array
185
+ () => commerce.getProducts(),
186
+ []
170
187
  );
171
-
188
+
172
189
  if (products.length === 0) {
173
- return <EmptyState message="Products temporarily unavailable" />;
190
+ return <p>Products temporarily unavailable</p>;
174
191
  }
175
-
192
+
176
193
  return <ProductsGrid products={products} />;
177
194
  }
178
195
  ```
@@ -181,14 +198,13 @@ export default async function ProductsPage() {
181
198
 
182
199
  ```typescript
183
200
  // Critical operations should fail explicitly
184
- async function completeCheckout(data: CheckoutData) {
201
+ async function completeCheckout(cartId: string) {
185
202
  try {
186
203
  return await backendCircuitBreaker.execute(
187
- () => medusaClient.carts.complete(data)
188
- // NO fallback - we want this to fail if backend is down
204
+ () => commerce.completeCheckout(cartId)
205
+ // NO fallback — we want this to fail if backend is down
189
206
  );
190
207
  } catch (error) {
191
- // Show error message to user
192
208
  throw new Error('Checkout unavailable. Please try again later.');
193
209
  }
194
210
  }
@@ -198,10 +214,9 @@ async function completeCheckout(data: CheckoutData) {
198
214
 
199
215
  ## 🧪 Testing
200
216
 
201
- ### Unit Test Example
202
-
203
217
  ```typescript
204
- import { CircuitBreaker } from '@thorprovider/adapters/resilience';
218
+ import { describe, it, expect, vi } from 'vitest';
219
+ import { CircuitBreaker } from '@thorprovider/adapters';
205
220
 
206
221
  describe('CircuitBreaker', () => {
207
222
  it('opens after threshold failures', async () => {
@@ -209,28 +224,28 @@ describe('CircuitBreaker', () => {
209
224
  failureThreshold: 3,
210
225
  resetTimeout: 1000,
211
226
  });
212
-
213
- const mockFn = jest.fn().mockRejectedValue(new Error('fail'));
214
-
227
+
228
+ const mockFn = vi.fn().mockRejectedValue(new Error('fail'));
229
+
215
230
  // 3 failures
216
231
  await expect(breaker.execute(mockFn)).rejects.toThrow();
217
232
  await expect(breaker.execute(mockFn)).rejects.toThrow();
218
233
  await expect(breaker.execute(mockFn)).rejects.toThrow();
219
-
234
+
220
235
  expect(breaker.state).toBe('OPEN');
221
236
  });
222
-
237
+
223
238
  it('uses fallback when circuit is open', async () => {
224
239
  const breaker = new CircuitBreaker({
225
240
  failureThreshold: 1,
226
241
  resetTimeout: 1000,
227
242
  });
228
-
229
- const mockFn = jest.fn().mockRejectedValue(new Error('fail'));
230
-
243
+
244
+ const mockFn = vi.fn().mockRejectedValue(new Error('fail'));
245
+
231
246
  // Open circuit
232
247
  await breaker.execute(mockFn, 'fallback').catch(() => {});
233
-
248
+
234
249
  // Should return fallback immediately
235
250
  const result = await breaker.execute(mockFn, 'fallback');
236
251
  expect(result).toBe('fallback');
@@ -239,6 +254,12 @@ describe('CircuitBreaker', () => {
239
254
  });
240
255
  ```
241
256
 
257
+ Run tests:
258
+
259
+ ```bash
260
+ yarn workspace @thorprovider/adapters test
261
+ ```
262
+
242
263
  ---
243
264
 
244
265
  ## 📐 Architecture Decisions
@@ -271,29 +292,30 @@ describe('CircuitBreaker', () => {
271
292
 
272
293
  - Don't use for client-side only operations
273
294
  - Don't ignore OPEN state errors silently
274
- - Don't set failureThreshold too low (causes flapping)
275
- - Don't set resetTimeout too high (slow recovery)
295
+ - Don't set `failureThreshold` too low (causes flapping)
296
+ - Don't set `resetTimeout` too high (slow recovery)
276
297
 
277
298
  ---
278
299
 
279
300
  ## 🔮 Future Enhancements
280
301
 
281
- - [ ] **Retry Pattern**: Exponential backoff for transient failures
282
- - [ ] **Bulkhead Pattern**: Isolate resource pools
302
+ - [ ] **Bulkhead Pattern**: Isolate resource pools per service
283
303
  - [ ] **Rate Limiting**: Prevent API abuse
284
304
  - [ ] **Metrics Export**: Prometheus/Datadog integration
285
305
  - [ ] **Health Check Integration**: Auto-open circuit on health check fail
286
306
 
307
+ > ✅ **Retry Pattern** with exponential backoff is already implemented in `src/core/fetch-utils.ts`.
308
+
287
309
  ---
288
310
 
289
311
  ## 📚 References
290
312
 
291
- - [Martin Fowler - Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
292
- - [Microsoft - Resilience Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)
313
+ - [Martin Fowler — Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
314
+ - [Microsoft — Resilience Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)
293
315
  - [Netflix Hystrix](https://github.com/Netflix/Hystrix/wiki)
294
316
 
295
317
  ---
296
318
 
297
- **Last Updated**: March 24, 2026
319
+ **Last Updated**: 2026-05-03
298
320
  **Status**: ✅ Active
299
321
  **Layer**: L2 (Adapters)
@@ -150,256 +150,6 @@ export const STELORDER_CAPABILITIES: Readonly<BackendCapabilities> = {
150
150
  loyaltyProgram: false,
151
151
  };
152
152
 
153
- /**
154
- * Shopify backend capabilities
155
- *
156
- * @see https://shopify.dev/docs/api/storefront
157
- */
158
- export const SHOPIFY_CAPABILITIES: Readonly<BackendCapabilities> = {
159
- // Profile Management
160
- updateProfile: true,
161
- updateEmail: true,
162
- changePasswordDirect: true,
163
- changePasswordViaEmail: true,
164
- deleteAccount: false, // Requires admin API
165
-
166
- // Avatar
167
- uploadAvatar: false,
168
- avatarViaMetadata: true,
169
-
170
- // Custom Attributes
171
- customAttributes: true,
172
-
173
- // Orders
174
- fetchOrders: true,
175
- cancelOrder: true,
176
-
177
- // Addresses
178
- manageAddresses: true,
179
- defaultAddress: true,
180
-
181
- // Cart/Checkout
182
- applyDiscounts: true,
183
- shippingCalculation: true,
184
- guestCheckout: true,
185
- savedPaymentMethods: true,
186
- listPaymentProviders: true,
187
- regionSpecificPayments: false, // Global payment methods
188
-
189
- // Wishlist
190
- wishlist: false, // Requires app
191
-
192
- // Search
193
- advancedSearch: true,
194
- facetedFilters: true,
195
-
196
- // Reviews
197
- productReviews: false, // Requires app
198
-
199
- // Loyalty
200
- loyaltyProgram: false, // Requires app
201
- };
202
-
203
- /**
204
- * BigCommerce backend capabilities
205
- *
206
- * @see https://developer.bigcommerce.com/docs/rest-storefront
207
- */
208
- export const BIGCOMMERCE_CAPABILITIES: Readonly<BackendCapabilities> = {
209
- // Profile Management
210
- updateProfile: true,
211
- updateEmail: true,
212
- changePasswordDirect: true,
213
- changePasswordViaEmail: true,
214
- deleteAccount: false,
215
-
216
- // Avatar
217
- uploadAvatar: false,
218
- avatarViaMetadata: false,
219
-
220
- // Custom Attributes
221
- customAttributes: true,
222
-
223
- // Orders
224
- fetchOrders: true,
225
- cancelOrder: false, // Admin API only
226
-
227
- // Addresses
228
- manageAddresses: true,
229
- defaultAddress: false,
230
-
231
- // Cart/Checkout
232
- applyDiscounts: true,
233
- shippingCalculation: true,
234
- guestCheckout: true,
235
- savedPaymentMethods: true,
236
- listPaymentProviders: true,
237
- regionSpecificPayments: false,
238
-
239
- // Wishlist
240
- wishlist: true, // Native support
241
-
242
- // Search
243
- advancedSearch: true,
244
- facetedFilters: true,
245
-
246
- // Reviews
247
- productReviews: true, // Native support
248
-
249
- // Loyalty
250
- loyaltyProgram: false,
251
- };
252
-
253
- /**
254
- * WooCommerce backend capabilities
255
- *
256
- * @see https://developer.woocommerce.com/docs/apis/store-api
257
- */
258
- export const WOOCOMMERCE_CAPABILITIES: Readonly<BackendCapabilities> = {
259
- // Profile Management
260
- updateProfile: true,
261
- updateEmail: true,
262
- changePasswordDirect: false, // WordPress handles auth
263
- changePasswordViaEmail: true,
264
- deleteAccount: false,
265
-
266
- // Avatar
267
- uploadAvatar: false,
268
- avatarViaMetadata: false, // Gravatar-based
269
-
270
- // Custom Attributes
271
- customAttributes: true, // Via meta_data
272
-
273
- // Orders
274
- fetchOrders: true,
275
- cancelOrder: true,
276
-
277
- // Addresses
278
- manageAddresses: true,
279
- defaultAddress: false, // Billing/shipping only
280
-
281
- // Cart/Checkout
282
- applyDiscounts: true, // Coupon codes
283
- shippingCalculation: true,
284
- guestCheckout: true,
285
- savedPaymentMethods: false,
286
- listPaymentProviders: true,
287
- regionSpecificPayments: false,
288
-
289
- // Wishlist
290
- wishlist: false, // Requires plugin
291
-
292
- // Search
293
- advancedSearch: false, // Basic WP search
294
- facetedFilters: false, // Requires plugin
295
-
296
- // Reviews
297
- productReviews: true, // Native WP reviews
298
-
299
- // Loyalty
300
- loyaltyProgram: false,
301
- };
302
-
303
- /**
304
- * Spree Commerce backend capabilities
305
- *
306
- * @see https://docs.spreecommerce.org/api-reference
307
- */
308
- export const SPREE_CAPABILITIES: Readonly<BackendCapabilities> = {
309
- // Profile Management
310
- updateProfile: true,
311
- updateEmail: true,
312
- changePasswordDirect: true,
313
- changePasswordViaEmail: true,
314
- deleteAccount: false,
315
-
316
- // Avatar
317
- uploadAvatar: false,
318
- avatarViaMetadata: false,
319
-
320
- // Custom Attributes
321
- customAttributes: false,
322
-
323
- // Orders
324
- fetchOrders: true,
325
- cancelOrder: true,
326
-
327
- // Addresses
328
- manageAddresses: true,
329
- defaultAddress: true,
330
-
331
- // Cart/Checkout
332
- applyDiscounts: true,
333
- shippingCalculation: true,
334
- guestCheckout: true,
335
- savedPaymentMethods: false,
336
- listPaymentProviders: true,
337
- regionSpecificPayments: false,
338
-
339
- // Wishlist
340
- wishlist: true, // Native support
341
-
342
- // Search
343
- advancedSearch: false,
344
- facetedFilters: true, // Taxon-based filtering
345
-
346
- // Reviews
347
- productReviews: false, // Requires extension
348
-
349
- // Loyalty
350
- loyaltyProgram: false,
351
- };
352
-
353
- /**
354
- * Adobe Commerce / Magento backend capabilities
355
- *
356
- * @see https://developer.adobe.com/commerce/webapi/graphql/
357
- */
358
- export const MAGENTO_CAPABILITIES: Readonly<BackendCapabilities> = {
359
- // Profile Management
360
- updateProfile: true,
361
- updateEmail: true,
362
- changePasswordDirect: true,
363
- changePasswordViaEmail: true,
364
- deleteAccount: false, // Admin only
365
-
366
- // Avatar
367
- uploadAvatar: false,
368
- avatarViaMetadata: false,
369
-
370
- // Custom Attributes
371
- customAttributes: true, // EAV model
372
-
373
- // Orders
374
- fetchOrders: true,
375
- cancelOrder: true,
376
-
377
- // Addresses
378
- manageAddresses: true,
379
- defaultAddress: true,
380
-
381
- // Cart/Checkout
382
- applyDiscounts: true,
383
- shippingCalculation: true,
384
- guestCheckout: true,
385
- savedPaymentMethods: true,
386
- listPaymentProviders: true,
387
- regionSpecificPayments: true, // Store-view scoped
388
-
389
- // Wishlist
390
- wishlist: true, // Native support
391
-
392
- // Search
393
- advancedSearch: true, // Elasticsearch/OpenSearch
394
- facetedFilters: true, // Layered navigation
395
-
396
- // Reviews
397
- productReviews: true, // Native support
398
-
399
- // Loyalty
400
- loyaltyProgram: false, // Requires extension
401
- };
402
-
403
153
  /**
404
154
  * Type guard to check if an object is a valid BackendCapabilities
405
155
  */