te.js 1.3.1 → 2.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 (80) hide show
  1. package/.cursor/plans/ai_native_framework_features_5bb1a20a.plan.md +234 -0
  2. package/.cursor/plans/auto_error_fix_agent_e68979c5.plan.md +356 -0
  3. package/.cursor/plans/tejas_framework_test_suite_5e3c6fad.plan.md +168 -0
  4. package/.prettierignore +31 -0
  5. package/README.md +156 -14
  6. package/auto-docs/analysis/handler-analyzer.js +58 -0
  7. package/auto-docs/analysis/source-resolver.js +101 -0
  8. package/auto-docs/constants.js +37 -0
  9. package/auto-docs/index.js +146 -0
  10. package/auto-docs/llm/index.js +6 -0
  11. package/auto-docs/llm/parse.js +88 -0
  12. package/auto-docs/llm/prompts.js +222 -0
  13. package/auto-docs/llm/provider.js +187 -0
  14. package/auto-docs/openapi/endpoint-processor.js +277 -0
  15. package/auto-docs/openapi/generator.js +107 -0
  16. package/auto-docs/openapi/level3.js +131 -0
  17. package/auto-docs/openapi/spec-builders.js +244 -0
  18. package/auto-docs/ui/docs-ui.js +186 -0
  19. package/auto-docs/utils/logger.js +17 -0
  20. package/auto-docs/utils/strip-usage.js +10 -0
  21. package/cli/docs-command.js +315 -0
  22. package/cli/fly-command.js +71 -0
  23. package/cli/index.js +57 -0
  24. package/database/index.js +163 -5
  25. package/database/mongodb.js +146 -0
  26. package/database/redis.js +201 -0
  27. package/docs/README.md +36 -0
  28. package/docs/ammo.md +362 -0
  29. package/docs/api-reference.md +489 -0
  30. package/docs/auto-docs.md +215 -0
  31. package/docs/cli.md +152 -0
  32. package/docs/configuration.md +233 -0
  33. package/docs/database.md +391 -0
  34. package/docs/error-handling.md +417 -0
  35. package/docs/file-uploads.md +334 -0
  36. package/docs/getting-started.md +181 -0
  37. package/docs/middleware.md +356 -0
  38. package/docs/rate-limiting.md +394 -0
  39. package/docs/routing.md +302 -0
  40. package/example/API_OVERVIEW.md +77 -0
  41. package/example/README.md +155 -0
  42. package/example/index.js +27 -2
  43. package/example/openapi.json +390 -0
  44. package/example/package.json +5 -2
  45. package/example/services/cache.service.js +25 -0
  46. package/example/services/user.service.js +42 -0
  47. package/example/start-redis.js +2 -0
  48. package/example/targets/cache.target.js +35 -0
  49. package/example/targets/index.target.js +11 -2
  50. package/example/targets/users.target.js +60 -0
  51. package/example/tejas.config.json +13 -1
  52. package/package.json +20 -5
  53. package/rate-limit/algorithms/fixed-window.js +141 -0
  54. package/rate-limit/algorithms/sliding-window.js +147 -0
  55. package/rate-limit/algorithms/token-bucket.js +115 -0
  56. package/rate-limit/base.js +165 -0
  57. package/rate-limit/index.js +147 -0
  58. package/rate-limit/storage/base.js +104 -0
  59. package/rate-limit/storage/memory.js +102 -0
  60. package/rate-limit/storage/redis.js +88 -0
  61. package/server/ammo/body-parser.js +152 -25
  62. package/server/ammo/enhancer.js +6 -2
  63. package/server/ammo.js +356 -327
  64. package/server/endpoint.js +21 -0
  65. package/server/handler.js +113 -87
  66. package/server/target.js +50 -9
  67. package/server/targets/registry.js +160 -57
  68. package/te.js +363 -137
  69. package/tests/auto-docs/handler-analyzer.test.js +44 -0
  70. package/tests/auto-docs/openapi-generator.test.js +103 -0
  71. package/tests/auto-docs/parse.test.js +63 -0
  72. package/tests/auto-docs/source-resolver.test.js +58 -0
  73. package/tests/helpers/index.js +37 -0
  74. package/tests/helpers/mock-http.js +342 -0
  75. package/tests/helpers/test-utils.js +446 -0
  76. package/tests/setup.test.js +148 -0
  77. package/utils/configuration.js +13 -10
  78. package/vitest.config.js +54 -0
  79. package/database/mongo.js +0 -67
  80. package/example/targets/user/user.target.js +0 -17
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Pure OpenAPI 3.0 schema and operation builders for auto-documentation.
3
+ * No LLM, file I/O, or registry dependencies.
4
+ */
5
+
6
+ import { ALL_METHODS } from '../analysis/handler-analyzer.js';
7
+ import { METHOD_KEYS } from '../constants.js';
8
+
9
+ /**
10
+ * Returns true if the object is method-keyed metadata (keys are HTTP methods, values are { summary?, description?, request?, response? }).
11
+ * @param {object} obj - Parsed LLM response
12
+ * @returns {boolean}
13
+ */
14
+ export function isMethodKeyed(obj) {
15
+ if (!obj || typeof obj !== 'object') return false;
16
+ for (const key of Object.keys(obj)) {
17
+ if (METHOD_KEYS.has(key.toLowerCase())) {
18
+ const val = obj[key];
19
+ if (val && typeof val === 'object' && (val.summary != null || val.response != null)) return true;
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+
25
+ /**
26
+ * Convert te.js path pattern to OpenAPI path (e.g. /users/:id -> /users/{id}).
27
+ * @param {string} path - Route path possibly containing :param segments
28
+ * @returns {string}
29
+ */
30
+ export function toOpenAPIPath(path) {
31
+ if (!path || typeof path !== 'string') return '/';
32
+ return path.replace(/:([^/]+)/g, '{$1}');
33
+ }
34
+
35
+ /**
36
+ * Extract path parameter definitions from a te.js path for OpenAPI.
37
+ * @param {string} path - Route path (e.g. /users/:id)
38
+ * @returns {Array<{ name: string, in: string, required: boolean, schema: object }>}
39
+ */
40
+ export function getPathParameters(path) {
41
+ if (!path || typeof path !== 'string') return [];
42
+ const params = [];
43
+ const segmentRegex = /:([^/]+)/g;
44
+ let match;
45
+ while ((match = segmentRegex.exec(path)) !== null) {
46
+ params.push({
47
+ name: match[1],
48
+ in: 'path',
49
+ required: true,
50
+ description: `Path parameter: ${match[1]}`,
51
+ schema: { type: 'string' },
52
+ });
53
+ }
54
+ return params;
55
+ }
56
+
57
+ /**
58
+ * Build OpenAPI query parameter list from request.query metadata.
59
+ * @param {object} queryMeta - e.g. { limit: { type: 'integer', required: false }, q: { type: 'string', required: true } }
60
+ * @returns {Array<{ name: string, in: string, required: boolean, description?: string, schema: object }>}
61
+ */
62
+ export function getQueryParameters(queryMeta) {
63
+ if (!queryMeta || typeof queryMeta !== 'object') return [];
64
+ const params = [];
65
+ for (const [name, spec] of Object.entries(queryMeta)) {
66
+ if (!spec || typeof spec !== 'object' || !spec.type) continue;
67
+ params.push({
68
+ name,
69
+ in: 'query',
70
+ required: spec.required === true || spec.required === 'true',
71
+ ...(spec.description && { description: spec.description }),
72
+ schema: { type: spec.type, ...(spec.format && { format: spec.format }) },
73
+ });
74
+ }
75
+ return params;
76
+ }
77
+
78
+ /**
79
+ * Convert simple metadata schema (field -> { type, description? }) to OpenAPI schema.
80
+ * @param {object} meta - e.g. { name: { type: 'string' }, email: { type: 'string' } }
81
+ * @returns {object} OpenAPI schema with properties
82
+ */
83
+ export function buildSchemaFromMetadata(meta) {
84
+ if (!meta || typeof meta !== 'object') return {};
85
+ const properties = {};
86
+ const required = [];
87
+ for (const [key, value] of Object.entries(meta)) {
88
+ if (value && typeof value === 'object' && value.type) {
89
+ properties[key] = {
90
+ type: value.type,
91
+ ...(value.description && { description: value.description }),
92
+ ...(value.format && { format: value.format }),
93
+ };
94
+ if (value.required === true || value.required === 'true') required.push(key);
95
+ }
96
+ }
97
+ if (Object.keys(properties).length === 0) return {};
98
+ return {
99
+ type: 'object',
100
+ properties,
101
+ ...(required.length > 0 && { required }),
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Build request body schema for an operation from metadata or empty.
107
+ * @param {object} requestMeta - metadata.request (body schema)
108
+ * @returns {object|undefined} OpenAPI requestBody or undefined
109
+ */
110
+ export function buildRequestBody(requestMeta) {
111
+ if (!requestMeta?.body || typeof requestMeta.body !== 'object') return undefined;
112
+ const schema = buildSchemaFromMetadata(requestMeta.body);
113
+ if (!schema || Object.keys(schema).length === 0) return undefined;
114
+ return {
115
+ required: true,
116
+ content: {
117
+ 'application/json': {
118
+ schema,
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Build response object for an operation from metadata.
126
+ * @param {object} responseMeta - metadata.response (e.g. { 200: { description, schema? }, 201: { ... } })
127
+ * @returns {object} OpenAPI responses
128
+ */
129
+ export function buildResponses(responseMeta) {
130
+ const responses = {};
131
+ if (!responseMeta || typeof responseMeta !== 'object') {
132
+ responses['200'] = { description: 'Success' };
133
+ return responses;
134
+ }
135
+ for (const [code, spec] of Object.entries(responseMeta)) {
136
+ if (!spec || typeof spec !== 'object') continue;
137
+ responses[String(code)] = {
138
+ description: spec.description || `Response ${code}`,
139
+ ...(spec.schema && {
140
+ content: {
141
+ 'application/json': {
142
+ schema: typeof spec.schema === 'object' && spec.schema.type
143
+ ? spec.schema
144
+ : { type: 'object' },
145
+ },
146
+ },
147
+ }),
148
+ };
149
+ }
150
+ if (Object.keys(responses).length === 0) {
151
+ responses['200'] = { description: 'Success' };
152
+ }
153
+ return responses;
154
+ }
155
+
156
+ /**
157
+ * Returns true when the endpoint accepts all standard HTTP methods (method-agnostic).
158
+ * @param {string[]} methods
159
+ * @returns {boolean}
160
+ */
161
+ export function isMethodAgnostic(methods) {
162
+ if (!Array.isArray(methods) || methods.length !== ALL_METHODS.length) return false;
163
+ const set = new Set(methods.map((m) => m.toUpperCase()));
164
+ return ALL_METHODS.every((m) => set.has(m));
165
+ }
166
+
167
+ /**
168
+ * Build a single OpenAPI operation for one HTTP method.
169
+ * @param {string} method - HTTP method (GET, POST, etc.)
170
+ * @param {object} meta - Merged metadata (explicit + LLM-enhanced): summary, description?, request?, response?
171
+ * @param {Array} pathParams - Path parameters for this path
172
+ * @param {object} [options] - { methodAgnostic?: boolean } - when true, description notes all accepted methods
173
+ * @returns {object} OpenAPI operation object
174
+ */
175
+ export function buildOperation(method, meta, pathParams, options = {}) {
176
+ const { methodAgnostic = false } = options;
177
+ let description = meta.description || '';
178
+ if (methodAgnostic) {
179
+ const methodList = ALL_METHODS.join(', ');
180
+ description = description
181
+ ? `${description}\n\nAccepts any HTTP method: ${methodList}.`
182
+ : `Accepts any HTTP method: ${methodList}.`;
183
+ }
184
+ const queryParams = getQueryParameters(meta.request?.query);
185
+ const allParams = [...pathParams, ...queryParams];
186
+ const op = {
187
+ summary: meta.summary || '',
188
+ ...(description && { description }),
189
+ parameters: allParams.length > 0 ? allParams : undefined,
190
+ };
191
+ const methodUpper = method.toUpperCase();
192
+ const body = buildRequestBody(meta.request);
193
+ if (body && (methodAgnostic || (methodUpper !== 'GET' && methodUpper !== 'HEAD'))) {
194
+ op.requestBody = body;
195
+ }
196
+ op.responses = buildResponses(meta.response);
197
+ return op;
198
+ }
199
+
200
+ /**
201
+ * Merge explicit endpoint metadata with LLM-enhanced result.
202
+ * @param {object} explicit - From endpoint.getMetadata()
203
+ * @param {object} enhanced - From llm.enhanceEndpointDocs() or per-method enhancer
204
+ * @param {object} [options] - { preferEnhanced?: boolean } - when true (level 2), LLM response wins over explicit
205
+ * @returns {object}
206
+ */
207
+ export function mergeMetadata(explicit, enhanced, options = {}) {
208
+ const preferEnhanced = options.preferEnhanced === true;
209
+ const summary = preferEnhanced
210
+ ? (enhanced?.summary ?? explicit?.summary ?? '')
211
+ : (explicit?.summary ?? enhanced?.summary ?? '');
212
+ const description = preferEnhanced
213
+ ? (enhanced?.description ?? explicit?.description ?? '')
214
+ : (explicit?.description ?? enhanced?.description ?? '');
215
+ const request = preferEnhanced
216
+ ? (enhanced?.request ?? explicit?.request)
217
+ : (explicit?.request ?? enhanced?.request);
218
+ const response = preferEnhanced
219
+ ? (enhanced?.response ?? explicit?.response)
220
+ : (explicit?.response ?? enhanced?.response);
221
+ return {
222
+ summary: summary || 'Endpoint',
223
+ description: description || undefined,
224
+ ...(request && { request }),
225
+ ...(response && { response }),
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Merge method-keyed metadata for a method-agnostic endpoint into a single meta (preferred order).
231
+ * @param {Map<string,object>} metaByMethod
232
+ * @param {string[]} methods
233
+ * @param {object} fallbackMeta
234
+ * @returns {object}
235
+ */
236
+ export function mergeMethodAgnosticMeta(metaByMethod, methods, fallbackMeta) {
237
+ const preferredOrder = ['post', 'put', 'patch', 'get', 'delete', 'head', 'options'];
238
+ for (const k of preferredOrder) {
239
+ const m = metaByMethod.get(k);
240
+ if (m && (m.summary || m.description)) return m;
241
+ }
242
+ const first = metaByMethod.values().next().value;
243
+ return first || fallbackMeta;
244
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Serves interactive API docs at /docs with try-out functionality.
3
+ * Uses Scalar API Reference (modern UI, try-it-out, themes). MIT.
4
+ * Registers internal routes: GET /docs (HTML page) and GET /docs/openapi.json (spec).
5
+ *
6
+ * @see https://scalar.com/products/api-references/integrations/html-js
7
+ * @see https://scalar.com/products/api-references/configuration
8
+ */
9
+
10
+ import Endpoint from '../../server/endpoint.js';
11
+ import targetRegistry from '../../server/targets/registry.js';
12
+
13
+ /** Scalar API Reference browser standalone (IIFE, sets window.Scalar). Pinned for stability. */
14
+ const SCALAR_VERSION = '1.46.0';
15
+ const SCALAR_STANDALONE = `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${SCALAR_VERSION}/dist/browser/standalone.js`;
16
+
17
+ /**
18
+ * Default Scalar API Reference config. Use layout: 'classic' to show the test request
19
+ * inline on the same page instead of opening a dialog (modern layout).
20
+ *
21
+ * @see https://scalar.com/products/api-references/configuration
22
+ */
23
+ const DEFAULT_SCALAR_CONFIG = {
24
+ layout: 'modern',
25
+ theme: 'default',
26
+ showSidebar: true,
27
+ hideDownloadButton: false,
28
+ hideModels: false,
29
+ hideSearch: false,
30
+ hideDarkModeToggle: false,
31
+ hideTestRequestButton: false,
32
+ showDeveloperTools: 'localhost',
33
+ documentDownloadType: 'both',
34
+ defaultOpenAllTags: false,
35
+ defaultOpenFirstTag: true,
36
+ expandAllModelSections: false,
37
+ expandAllResponses: false,
38
+ withDefaultFonts: true,
39
+ };
40
+
41
+ /**
42
+ * Build HTML shell for Scalar docs: script tag + inline config and mount logic.
43
+ * @param {string} scriptUrl - URL to Scalar standalone JS
44
+ * @param {string} configJson - JSON string (already escaped for embedding in JS)
45
+ * @returns {string} Full HTML document
46
+ */
47
+ function buildDocsHtmlShell(scriptUrl, configJson) {
48
+ return `<!DOCTYPE html>
49
+ <html lang="en">
50
+ <head>
51
+ <meta charset="UTF-8" />
52
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
53
+ <title>API Reference</title>
54
+ <style>
55
+ body { margin: 0; min-height: 100vh; font-family: system-ui, sans-serif; }
56
+ #scalar-app { min-height: 100vh; width: 100%; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div id="scalar-app"></div>
61
+ <script src="${scriptUrl}" id="scalar-script"><\/script>
62
+ <script>
63
+ (function() {
64
+ var config = JSON.parse('${configJson}');
65
+ function mount() {
66
+ if (typeof Scalar !== 'undefined' && typeof Scalar.createApiReference === 'function') {
67
+ Scalar.createApiReference('#scalar-app', config);
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+ var el = document.getElementById('scalar-script');
73
+ if (el) {
74
+ el.addEventListener('load', function() { mount(); });
75
+ if (el.readyState === 'complete') setTimeout(function() { mount(); }, 0);
76
+ }
77
+ if (!mount()) setTimeout(function() { mount(); }, 100);
78
+ })();
79
+ </script>
80
+ </body>
81
+ </html>`;
82
+ }
83
+
84
+ /**
85
+ * Builds the HTML page that embeds Scalar and points to the spec URL.
86
+ * @param {string} specUrl - URL to the OpenAPI spec (e.g. '/docs/openapi.json' or full URL).
87
+ * @param {object} [scalarConfig] - Optional Scalar API Reference config (merged with defaults).
88
+ * @returns {string} Full HTML document.
89
+ */
90
+ function buildDocsPage(specUrl, scalarConfig = {}) {
91
+ const url = specUrl || '/docs/openapi.json';
92
+ const config = { ...DEFAULT_SCALAR_CONFIG, ...scalarConfig, url };
93
+ const configJson = JSON.stringify(config)
94
+ .replace(/</g, '\\u003c')
95
+ .replace(/>/g, '\\u003e');
96
+ return buildDocsHtmlShell(SCALAR_STANDALONE, configJson);
97
+ }
98
+
99
+ /**
100
+ * Create endpoint that serves the docs HTML page at GET docsPath.
101
+ * @param {string} docsPath - e.g. '/docs'
102
+ * @param {string} htmlContent - Full HTML document
103
+ * @returns {Endpoint}
104
+ */
105
+ function createDocsHtmlEndpoint(docsPath, htmlContent) {
106
+ const endpoint = new Endpoint();
107
+ endpoint.setPath('', docsPath);
108
+ endpoint.setMiddlewares([]);
109
+ endpoint.setHandler((ammo) => {
110
+ if (!ammo.GET) return ammo.notAllowed();
111
+ ammo.fire(200, htmlContent, 'text/html');
112
+ });
113
+ return endpoint;
114
+ }
115
+
116
+ /**
117
+ * Create endpoint that serves the OpenAPI spec JSON at GET specPath.
118
+ * @param {string} specPath - e.g. '/docs/openapi.json'
119
+ * @param {() => object | Promise<object>} getSpec - Function that returns the current spec
120
+ * @returns {Endpoint}
121
+ */
122
+ function createSpecJsonEndpoint(specPath, getSpec) {
123
+ const endpoint = new Endpoint();
124
+ endpoint.setPath('', specPath);
125
+ endpoint.setMiddlewares([]);
126
+ endpoint.setHandler(async (ammo) => {
127
+ if (!ammo.GET) return ammo.notAllowed();
128
+ try {
129
+ const spec = await Promise.resolve(getSpec());
130
+ ammo.fire(200, spec);
131
+ } catch (err) {
132
+ ammo.fire(500, {
133
+ error: 'Failed to generate OpenAPI spec',
134
+ message: err?.message,
135
+ });
136
+ }
137
+ });
138
+ return endpoint;
139
+ }
140
+
141
+ /**
142
+ * Registers the docs and openapi.json routes (or returns endpoints when mutateRegistry is false).
143
+ * Call this after the OpenAPI spec is available (e.g. after generateOpenAPISpec).
144
+ *
145
+ * @param {object} [options]
146
+ * @param {() => object | Promise<object>} options.getSpec - Function that returns the current OpenAPI spec (sync or async). Used for GET {docsPath}/openapi.json.
147
+ * @param {string} [options.docsPath='/docs'] - Base path for docs (HTML page and spec URL). Routes: GET {docsPath}, GET {docsPath}/openapi.json.
148
+ * @param {string} [options.specUrl] - Override for the spec URL shown in the docs page (default: '{docsPath}/openapi.json'). Use when serving behind a proxy with a different base path.
149
+ * @param {object} [options.scalarConfig] - Optional Scalar API Reference config (e.g. { layout: 'classic' } for try-it on the same page).
150
+ * @param {boolean} [options.mutateRegistry=true] - If true, push endpoints to registry. If false, return [docsEndpoint, specEndpoint] without mutating.
151
+ * @param {object} [registry] - Target registry to register routes on when mutateRegistry is true. Defaults to the module's targetRegistry.
152
+ * @returns {undefined | [Endpoint, Endpoint]} When mutateRegistry is false, returns the two endpoints for the caller to register.
153
+ */
154
+ export function registerDocRoutes(options = {}, registry = targetRegistry) {
155
+ const {
156
+ getSpec,
157
+ docsPath = '/docs',
158
+ specUrl: specUrlOption,
159
+ scalarConfig,
160
+ mutateRegistry = true,
161
+ } = options;
162
+
163
+ if (typeof getSpec !== 'function') {
164
+ throw new Error(
165
+ 'registerDocRoutes requires options.getSpec (function returning the OpenAPI spec)',
166
+ );
167
+ }
168
+
169
+ const basePath = docsPath.replace(/\/$/, '');
170
+ const specUrl = specUrlOption ?? `${basePath}/openapi.json`;
171
+ const specPath = `${basePath}/openapi.json`;
172
+ const htmlContent = buildDocsPage(specUrl, scalarConfig);
173
+
174
+ const docsEndpoint = createDocsHtmlEndpoint(docsPath, htmlContent);
175
+ const specEndpoint = createSpecJsonEndpoint(specPath, getSpec);
176
+
177
+ if (mutateRegistry) {
178
+ registry.targets.push(docsEndpoint);
179
+ registry.targets.push(specEndpoint);
180
+ return;
181
+ }
182
+ return [docsEndpoint, specEndpoint];
183
+ }
184
+
185
+ export { buildDocsPage };
186
+ export default registerDocRoutes;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Verbose logging helper for auto-docs.
3
+ * Returns a no-op when verbose is false or logger has no info method.
4
+ */
5
+
6
+ /**
7
+ * Create a logger that only logs when verbose is true and logger.info exists.
8
+ * @param {object|null} logger - Logger with .info(msg) method
9
+ * @param {boolean} verbose - Whether to log
10
+ * @returns {(msg: string) => void} Function that logs or does nothing
11
+ */
12
+ export function createVerboseLogger(logger, verbose) {
13
+ if (!verbose || !logger || typeof logger.info !== 'function') {
14
+ return () => {};
15
+ }
16
+ return (msg) => logger.info(msg);
17
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Strip LLM-only fields from response so they are not merged into metadata.
3
+ * @param {object|null} obj - Raw LLM response (may contain _usage, _fallback)
4
+ * @returns {object|null} Same object without _usage and _fallback
5
+ */
6
+ export function stripLlmUsage(obj) {
7
+ if (!obj || typeof obj !== 'object') return obj;
8
+ const { _usage, _fallback, ...rest } = obj;
9
+ return rest;
10
+ }