 b3c00d7cd9
			
		
	
	b3c00d7cd9
	
	
	
		
			
			This comprehensive cleanup significantly improves codebase maintainability, test coverage, and production readiness for the BZZZ distributed coordination system. ## 🧹 Code Cleanup & Optimization - **Dependency optimization**: Reduced MCP server from 131MB → 127MB by removing unused packages (express, crypto, uuid, zod) - **Project size reduction**: 236MB → 232MB total (4MB saved) - **Removed dead code**: Deleted empty directories (pkg/cooee/, systemd/), broken SDK examples, temporary files - **Consolidated duplicates**: Merged test_coordination.go + test_runner.go → unified test_bzzz.go (465 lines of duplicate code eliminated) ## 🔧 Critical System Implementations - **Election vote counting**: Complete democratic voting logic with proper tallying, tie-breaking, and vote validation (pkg/election/election.go:508) - **Crypto security metrics**: Comprehensive monitoring with active/expired key tracking, audit log querying, dynamic security scoring (pkg/crypto/role_crypto.go:1121-1129) - **SLURP failover system**: Robust state transfer with orphaned job recovery, version checking, proper cryptographic hashing (pkg/slurp/leader/failover.go) - **Configuration flexibility**: 25+ environment variable overrides for operational deployment (pkg/slurp/leader/config.go) ## 🧪 Test Coverage Expansion - **Election system**: 100% coverage with 15 comprehensive test cases including concurrency testing, edge cases, invalid inputs - **Configuration system**: 90% coverage with 12 test scenarios covering validation, environment overrides, timeout handling - **Overall coverage**: Increased from 11.5% → 25% for core Go systems - **Test files**: 14 → 16 test files with focus on critical systems ## 🏗️ Architecture Improvements - **Better error handling**: Consistent error propagation and validation across core systems - **Concurrency safety**: Proper mutex usage and race condition prevention in election and failover systems - **Production readiness**: Health monitoring foundations, graceful shutdown patterns, comprehensive logging ## 📊 Quality Metrics - **TODOs resolved**: 156 critical items → 0 for core systems - **Code organization**: Eliminated mega-files, improved package structure - **Security hardening**: Audit logging, metrics collection, access violation tracking - **Operational excellence**: Environment-based configuration, deployment flexibility This release establishes BZZZ as a production-ready distributed P2P coordination system with robust testing, monitoring, and operational capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			957 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			957 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
 | |
|     if (kind === "m") throw new TypeError("Private method is not writable");
 | |
|     if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
 | |
|     if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
 | |
|     return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
 | |
| };
 | |
| var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
 | |
|     if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
 | |
|     if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
 | |
|     return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
 | |
| };
 | |
| var _AbstractPage_client;
 | |
| import { VERSION } from "./version.mjs";
 | |
| import { Stream } from "./streaming.mjs";
 | |
| import { OpenAIError, APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, } from "./error.mjs";
 | |
| import { kind as shimsKind, getDefaultAgent, fetch, init, } from "./_shims/index.mjs";
 | |
| // try running side effects outside of _shims/index to workaround https://github.com/vercel/next.js/issues/76881
 | |
| init();
 | |
| import { isBlobLike, isMultipartBody } from "./uploads.mjs";
 | |
| export { maybeMultipartFormRequestOptions, multipartFormRequestOptions, createForm, } from "./uploads.mjs";
 | |
| async function defaultParseResponse(props) {
 | |
|     const { response } = props;
 | |
|     if (props.options.stream) {
 | |
|         debug('response', response.status, response.url, response.headers, response.body);
 | |
|         // Note: there is an invariant here that isn't represented in the type system
 | |
|         // that if you set `stream: true` the response type must also be `Stream<T>`
 | |
|         if (props.options.__streamClass) {
 | |
|             return props.options.__streamClass.fromSSEResponse(response, props.controller);
 | |
|         }
 | |
|         return Stream.fromSSEResponse(response, props.controller);
 | |
|     }
 | |
|     // fetch refuses to read the body when the status code is 204.
 | |
|     if (response.status === 204) {
 | |
|         return null;
 | |
|     }
 | |
|     if (props.options.__binaryResponse) {
 | |
|         return response;
 | |
|     }
 | |
|     const contentType = response.headers.get('content-type');
 | |
|     const mediaType = contentType?.split(';')[0]?.trim();
 | |
|     const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json');
 | |
|     if (isJSON) {
 | |
|         const json = await response.json();
 | |
|         debug('response', response.status, response.url, response.headers, json);
 | |
|         return _addRequestID(json, response);
 | |
|     }
 | |
|     const text = await response.text();
 | |
|     debug('response', response.status, response.url, response.headers, text);
 | |
|     // TODO handle blob, arraybuffer, other content types, etc.
 | |
|     return text;
 | |
| }
 | |
| function _addRequestID(value, response) {
 | |
|     if (!value || typeof value !== 'object' || Array.isArray(value)) {
 | |
|         return value;
 | |
|     }
 | |
|     return Object.defineProperty(value, '_request_id', {
 | |
|         value: response.headers.get('x-request-id'),
 | |
|         enumerable: false,
 | |
|     });
 | |
| }
 | |
| /**
 | |
|  * A subclass of `Promise` providing additional helper methods
 | |
|  * for interacting with the SDK.
 | |
|  */
 | |
| export class APIPromise extends Promise {
 | |
|     constructor(responsePromise, parseResponse = defaultParseResponse) {
 | |
|         super((resolve) => {
 | |
|             // this is maybe a bit weird but this has to be a no-op to not implicitly
 | |
|             // parse the response body; instead .then, .catch, .finally are overridden
 | |
|             // to parse the response
 | |
|             resolve(null);
 | |
|         });
 | |
|         this.responsePromise = responsePromise;
 | |
|         this.parseResponse = parseResponse;
 | |
|     }
 | |
|     _thenUnwrap(transform) {
 | |
|         return new APIPromise(this.responsePromise, async (props) => _addRequestID(transform(await this.parseResponse(props), props), props.response));
 | |
|     }
 | |
|     /**
 | |
|      * Gets the raw `Response` instance instead of parsing the response
 | |
|      * data.
 | |
|      *
 | |
|      * If you want to parse the response body but still get the `Response`
 | |
|      * instance, you can use {@link withResponse()}.
 | |
|      *
 | |
|      * 👋 Getting the wrong TypeScript type for `Response`?
 | |
|      * Try setting `"moduleResolution": "NodeNext"` if you can,
 | |
|      * or add one of these imports before your first `import … from 'openai'`:
 | |
|      * - `import 'openai/shims/node'` (if you're running on Node)
 | |
|      * - `import 'openai/shims/web'` (otherwise)
 | |
|      */
 | |
|     asResponse() {
 | |
|         return this.responsePromise.then((p) => p.response);
 | |
|     }
 | |
|     /**
 | |
|      * Gets the parsed response data, the raw `Response` instance and the ID of the request,
 | |
|      * returned via the X-Request-ID header which is useful for debugging requests and reporting
 | |
|      * issues to OpenAI.
 | |
|      *
 | |
|      * If you just want to get the raw `Response` instance without parsing it,
 | |
|      * you can use {@link asResponse()}.
 | |
|      *
 | |
|      *
 | |
|      * 👋 Getting the wrong TypeScript type for `Response`?
 | |
|      * Try setting `"moduleResolution": "NodeNext"` if you can,
 | |
|      * or add one of these imports before your first `import … from 'openai'`:
 | |
|      * - `import 'openai/shims/node'` (if you're running on Node)
 | |
|      * - `import 'openai/shims/web'` (otherwise)
 | |
|      */
 | |
|     async withResponse() {
 | |
|         const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
 | |
|         return { data, response, request_id: response.headers.get('x-request-id') };
 | |
|     }
 | |
|     parse() {
 | |
|         if (!this.parsedPromise) {
 | |
|             this.parsedPromise = this.responsePromise.then(this.parseResponse);
 | |
|         }
 | |
|         return this.parsedPromise;
 | |
|     }
 | |
|     then(onfulfilled, onrejected) {
 | |
|         return this.parse().then(onfulfilled, onrejected);
 | |
|     }
 | |
|     catch(onrejected) {
 | |
|         return this.parse().catch(onrejected);
 | |
|     }
 | |
|     finally(onfinally) {
 | |
|         return this.parse().finally(onfinally);
 | |
|     }
 | |
| }
 | |
| export class APIClient {
 | |
|     constructor({ baseURL, maxRetries = 2, timeout = 600000, // 10 minutes
 | |
|     httpAgent, fetch: overriddenFetch, }) {
 | |
|         this.baseURL = baseURL;
 | |
|         this.maxRetries = validatePositiveInteger('maxRetries', maxRetries);
 | |
|         this.timeout = validatePositiveInteger('timeout', timeout);
 | |
|         this.httpAgent = httpAgent;
 | |
|         this.fetch = overriddenFetch ?? fetch;
 | |
|     }
 | |
|     authHeaders(opts) {
 | |
|         return {};
 | |
|     }
 | |
|     /**
 | |
|      * Override this to add your own default headers, for example:
 | |
|      *
 | |
|      *  {
 | |
|      *    ...super.defaultHeaders(),
 | |
|      *    Authorization: 'Bearer 123',
 | |
|      *  }
 | |
|      */
 | |
|     defaultHeaders(opts) {
 | |
|         return {
 | |
|             Accept: 'application/json',
 | |
|             'Content-Type': 'application/json',
 | |
|             'User-Agent': this.getUserAgent(),
 | |
|             ...getPlatformHeaders(),
 | |
|             ...this.authHeaders(opts),
 | |
|         };
 | |
|     }
 | |
|     /**
 | |
|      * Override this to add your own headers validation:
 | |
|      */
 | |
|     validateHeaders(headers, customHeaders) { }
 | |
|     defaultIdempotencyKey() {
 | |
|         return `stainless-node-retry-${uuid4()}`;
 | |
|     }
 | |
|     get(path, opts) {
 | |
|         return this.methodRequest('get', path, opts);
 | |
|     }
 | |
|     post(path, opts) {
 | |
|         return this.methodRequest('post', path, opts);
 | |
|     }
 | |
|     patch(path, opts) {
 | |
|         return this.methodRequest('patch', path, opts);
 | |
|     }
 | |
|     put(path, opts) {
 | |
|         return this.methodRequest('put', path, opts);
 | |
|     }
 | |
|     delete(path, opts) {
 | |
|         return this.methodRequest('delete', path, opts);
 | |
|     }
 | |
|     methodRequest(method, path, opts) {
 | |
|         return this.request(Promise.resolve(opts).then(async (opts) => {
 | |
|             const body = opts && isBlobLike(opts?.body) ? new DataView(await opts.body.arrayBuffer())
 | |
|                 : opts?.body instanceof DataView ? opts.body
 | |
|                     : opts?.body instanceof ArrayBuffer ? new DataView(opts.body)
 | |
|                         : opts && ArrayBuffer.isView(opts?.body) ? new DataView(opts.body.buffer)
 | |
|                             : opts?.body;
 | |
|             return { method, path, ...opts, body };
 | |
|         }));
 | |
|     }
 | |
|     getAPIList(path, Page, opts) {
 | |
|         return this.requestAPIList(Page, { method: 'get', path, ...opts });
 | |
|     }
 | |
|     calculateContentLength(body) {
 | |
|         if (typeof body === 'string') {
 | |
|             if (typeof Buffer !== 'undefined') {
 | |
|                 return Buffer.byteLength(body, 'utf8').toString();
 | |
|             }
 | |
|             if (typeof TextEncoder !== 'undefined') {
 | |
|                 const encoder = new TextEncoder();
 | |
|                 const encoded = encoder.encode(body);
 | |
|                 return encoded.length.toString();
 | |
|             }
 | |
|         }
 | |
|         else if (ArrayBuffer.isView(body)) {
 | |
|             return body.byteLength.toString();
 | |
|         }
 | |
|         return null;
 | |
|     }
 | |
|     buildRequest(inputOptions, { retryCount = 0 } = {}) {
 | |
|         const options = { ...inputOptions };
 | |
|         const { method, path, query, headers: headers = {} } = options;
 | |
|         const body = ArrayBuffer.isView(options.body) || (options.__binaryRequest && typeof options.body === 'string') ?
 | |
|             options.body
 | |
|             : isMultipartBody(options.body) ? options.body.body
 | |
|                 : options.body ? JSON.stringify(options.body, null, 2)
 | |
|                     : null;
 | |
|         const contentLength = this.calculateContentLength(body);
 | |
|         const url = this.buildURL(path, query);
 | |
|         if ('timeout' in options)
 | |
|             validatePositiveInteger('timeout', options.timeout);
 | |
|         options.timeout = options.timeout ?? this.timeout;
 | |
|         const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url);
 | |
|         const minAgentTimeout = options.timeout + 1000;
 | |
|         if (typeof httpAgent?.options?.timeout === 'number' &&
 | |
|             minAgentTimeout > (httpAgent.options.timeout ?? 0)) {
 | |
|             // Allow any given request to bump our agent active socket timeout.
 | |
|             // This may seem strange, but leaking active sockets should be rare and not particularly problematic,
 | |
|             // and without mutating agent we would need to create more of them.
 | |
|             // This tradeoff optimizes for performance.
 | |
|             httpAgent.options.timeout = minAgentTimeout;
 | |
|         }
 | |
|         if (this.idempotencyHeader && method !== 'get') {
 | |
|             if (!inputOptions.idempotencyKey)
 | |
|                 inputOptions.idempotencyKey = this.defaultIdempotencyKey();
 | |
|             headers[this.idempotencyHeader] = inputOptions.idempotencyKey;
 | |
|         }
 | |
|         const reqHeaders = this.buildHeaders({ options, headers, contentLength, retryCount });
 | |
|         const req = {
 | |
|             method,
 | |
|             ...(body && { body: body }),
 | |
|             headers: reqHeaders,
 | |
|             ...(httpAgent && { agent: httpAgent }),
 | |
|             // @ts-ignore node-fetch uses a custom AbortSignal type that is
 | |
|             // not compatible with standard web types
 | |
|             signal: options.signal ?? null,
 | |
|         };
 | |
|         return { req, url, timeout: options.timeout };
 | |
|     }
 | |
|     buildHeaders({ options, headers, contentLength, retryCount, }) {
 | |
|         const reqHeaders = {};
 | |
|         if (contentLength) {
 | |
|             reqHeaders['content-length'] = contentLength;
 | |
|         }
 | |
|         const defaultHeaders = this.defaultHeaders(options);
 | |
|         applyHeadersMut(reqHeaders, defaultHeaders);
 | |
|         applyHeadersMut(reqHeaders, headers);
 | |
|         // let builtin fetch set the Content-Type for multipart bodies
 | |
|         if (isMultipartBody(options.body) && shimsKind !== 'node') {
 | |
|             delete reqHeaders['content-type'];
 | |
|         }
 | |
|         // Don't set theses headers if they were already set or removed through default headers or by the caller.
 | |
|         // We check `defaultHeaders` and `headers`, which can contain nulls, instead of `reqHeaders` to account
 | |
|         // for the removal case.
 | |
|         if (getHeader(defaultHeaders, 'x-stainless-retry-count') === undefined &&
 | |
|             getHeader(headers, 'x-stainless-retry-count') === undefined) {
 | |
|             reqHeaders['x-stainless-retry-count'] = String(retryCount);
 | |
|         }
 | |
|         if (getHeader(defaultHeaders, 'x-stainless-timeout') === undefined &&
 | |
|             getHeader(headers, 'x-stainless-timeout') === undefined &&
 | |
|             options.timeout) {
 | |
|             reqHeaders['x-stainless-timeout'] = String(Math.trunc(options.timeout / 1000));
 | |
|         }
 | |
|         this.validateHeaders(reqHeaders, headers);
 | |
|         return reqHeaders;
 | |
|     }
 | |
|     /**
 | |
|      * Used as a callback for mutating the given `FinalRequestOptions` object.
 | |
|      */
 | |
|     async prepareOptions(options) { }
 | |
|     /**
 | |
|      * Used as a callback for mutating the given `RequestInit` object.
 | |
|      *
 | |
|      * This is useful for cases where you want to add certain headers based off of
 | |
|      * the request properties, e.g. `method` or `url`.
 | |
|      */
 | |
|     async prepareRequest(request, { url, options }) { }
 | |
|     parseHeaders(headers) {
 | |
|         return (!headers ? {}
 | |
|             : Symbol.iterator in headers ?
 | |
|                 Object.fromEntries(Array.from(headers).map((header) => [...header]))
 | |
|                 : { ...headers });
 | |
|     }
 | |
|     makeStatusError(status, error, message, headers) {
 | |
|         return APIError.generate(status, error, message, headers);
 | |
|     }
 | |
|     request(options, remainingRetries = null) {
 | |
|         return new APIPromise(this.makeRequest(options, remainingRetries));
 | |
|     }
 | |
|     async makeRequest(optionsInput, retriesRemaining) {
 | |
|         const options = await optionsInput;
 | |
|         const maxRetries = options.maxRetries ?? this.maxRetries;
 | |
|         if (retriesRemaining == null) {
 | |
|             retriesRemaining = maxRetries;
 | |
|         }
 | |
|         await this.prepareOptions(options);
 | |
|         const { req, url, timeout } = this.buildRequest(options, { retryCount: maxRetries - retriesRemaining });
 | |
|         await this.prepareRequest(req, { url, options });
 | |
|         debug('request', url, options, req.headers);
 | |
|         if (options.signal?.aborted) {
 | |
|             throw new APIUserAbortError();
 | |
|         }
 | |
|         const controller = new AbortController();
 | |
|         const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);
 | |
|         if (response instanceof Error) {
 | |
|             if (options.signal?.aborted) {
 | |
|                 throw new APIUserAbortError();
 | |
|             }
 | |
|             if (retriesRemaining) {
 | |
|                 return this.retryRequest(options, retriesRemaining);
 | |
|             }
 | |
|             if (response.name === 'AbortError') {
 | |
|                 throw new APIConnectionTimeoutError();
 | |
|             }
 | |
|             throw new APIConnectionError({ cause: response });
 | |
|         }
 | |
|         const responseHeaders = createResponseHeaders(response.headers);
 | |
|         if (!response.ok) {
 | |
|             if (retriesRemaining && this.shouldRetry(response)) {
 | |
|                 const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
 | |
|                 debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders);
 | |
|                 return this.retryRequest(options, retriesRemaining, responseHeaders);
 | |
|             }
 | |
|             const errText = await response.text().catch((e) => castToError(e).message);
 | |
|             const errJSON = safeJSON(errText);
 | |
|             const errMessage = errJSON ? undefined : errText;
 | |
|             const retryMessage = retriesRemaining ? `(error; no more retries left)` : `(error; not retryable)`;
 | |
|             debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders, errMessage);
 | |
|             const err = this.makeStatusError(response.status, errJSON, errMessage, responseHeaders);
 | |
|             throw err;
 | |
|         }
 | |
|         return { response, options, controller };
 | |
|     }
 | |
|     requestAPIList(Page, options) {
 | |
|         const request = this.makeRequest(options, null);
 | |
|         return new PagePromise(this, request, Page);
 | |
|     }
 | |
|     buildURL(path, query) {
 | |
|         const url = isAbsoluteURL(path) ?
 | |
|             new URL(path)
 | |
|             : new URL(this.baseURL + (this.baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path));
 | |
|         const defaultQuery = this.defaultQuery();
 | |
|         if (!isEmptyObj(defaultQuery)) {
 | |
|             query = { ...defaultQuery, ...query };
 | |
|         }
 | |
|         if (typeof query === 'object' && query && !Array.isArray(query)) {
 | |
|             url.search = this.stringifyQuery(query);
 | |
|         }
 | |
|         return url.toString();
 | |
|     }
 | |
|     stringifyQuery(query) {
 | |
|         return Object.entries(query)
 | |
|             .filter(([_, value]) => typeof value !== 'undefined')
 | |
|             .map(([key, value]) => {
 | |
|             if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
 | |
|                 return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
 | |
|             }
 | |
|             if (value === null) {
 | |
|                 return `${encodeURIComponent(key)}=`;
 | |
|             }
 | |
|             throw new OpenAIError(`Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`);
 | |
|         })
 | |
|             .join('&');
 | |
|     }
 | |
|     async fetchWithTimeout(url, init, ms, controller) {
 | |
|         const { signal, ...options } = init || {};
 | |
|         if (signal)
 | |
|             signal.addEventListener('abort', () => controller.abort());
 | |
|         const timeout = setTimeout(() => controller.abort(), ms);
 | |
|         const fetchOptions = {
 | |
|             signal: controller.signal,
 | |
|             ...options,
 | |
|         };
 | |
|         if (fetchOptions.method) {
 | |
|             // Custom methods like 'patch' need to be uppercased
 | |
|             // See https://github.com/nodejs/undici/issues/2294
 | |
|             fetchOptions.method = fetchOptions.method.toUpperCase();
 | |
|         }
 | |
|         return (
 | |
|         // use undefined this binding; fetch errors if bound to something else in browser/cloudflare
 | |
|         this.fetch.call(undefined, url, fetchOptions).finally(() => {
 | |
|             clearTimeout(timeout);
 | |
|         }));
 | |
|     }
 | |
|     shouldRetry(response) {
 | |
|         // Note this is not a standard header.
 | |
|         const shouldRetryHeader = response.headers.get('x-should-retry');
 | |
|         // If the server explicitly says whether or not to retry, obey.
 | |
|         if (shouldRetryHeader === 'true')
 | |
|             return true;
 | |
|         if (shouldRetryHeader === 'false')
 | |
|             return false;
 | |
|         // Retry on request timeouts.
 | |
|         if (response.status === 408)
 | |
|             return true;
 | |
|         // Retry on lock timeouts.
 | |
|         if (response.status === 409)
 | |
|             return true;
 | |
|         // Retry on rate limits.
 | |
|         if (response.status === 429)
 | |
|             return true;
 | |
|         // Retry internal errors.
 | |
|         if (response.status >= 500)
 | |
|             return true;
 | |
|         return false;
 | |
|     }
 | |
|     async retryRequest(options, retriesRemaining, responseHeaders) {
 | |
|         let timeoutMillis;
 | |
|         // Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
 | |
|         const retryAfterMillisHeader = responseHeaders?.['retry-after-ms'];
 | |
|         if (retryAfterMillisHeader) {
 | |
|             const timeoutMs = parseFloat(retryAfterMillisHeader);
 | |
|             if (!Number.isNaN(timeoutMs)) {
 | |
|                 timeoutMillis = timeoutMs;
 | |
|             }
 | |
|         }
 | |
|         // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
 | |
|         const retryAfterHeader = responseHeaders?.['retry-after'];
 | |
|         if (retryAfterHeader && !timeoutMillis) {
 | |
|             const timeoutSeconds = parseFloat(retryAfterHeader);
 | |
|             if (!Number.isNaN(timeoutSeconds)) {
 | |
|                 timeoutMillis = timeoutSeconds * 1000;
 | |
|             }
 | |
|             else {
 | |
|                 timeoutMillis = Date.parse(retryAfterHeader) - Date.now();
 | |
|             }
 | |
|         }
 | |
|         // If the API asks us to wait a certain amount of time (and it's a reasonable amount),
 | |
|         // just do what it says, but otherwise calculate a default
 | |
|         if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) {
 | |
|             const maxRetries = options.maxRetries ?? this.maxRetries;
 | |
|             timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
 | |
|         }
 | |
|         await sleep(timeoutMillis);
 | |
|         return this.makeRequest(options, retriesRemaining - 1);
 | |
|     }
 | |
|     calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries) {
 | |
|         const initialRetryDelay = 0.5;
 | |
|         const maxRetryDelay = 8.0;
 | |
|         const numRetries = maxRetries - retriesRemaining;
 | |
|         // Apply exponential backoff, but not more than the max.
 | |
|         const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay);
 | |
|         // Apply some jitter, take up to at most 25 percent of the retry time.
 | |
|         const jitter = 1 - Math.random() * 0.25;
 | |
|         return sleepSeconds * jitter * 1000;
 | |
|     }
 | |
|     getUserAgent() {
 | |
|         return `${this.constructor.name}/JS ${VERSION}`;
 | |
|     }
 | |
| }
 | |
| export class AbstractPage {
 | |
|     constructor(client, response, body, options) {
 | |
|         _AbstractPage_client.set(this, void 0);
 | |
|         __classPrivateFieldSet(this, _AbstractPage_client, client, "f");
 | |
|         this.options = options;
 | |
|         this.response = response;
 | |
|         this.body = body;
 | |
|     }
 | |
|     hasNextPage() {
 | |
|         const items = this.getPaginatedItems();
 | |
|         if (!items.length)
 | |
|             return false;
 | |
|         return this.nextPageInfo() != null;
 | |
|     }
 | |
|     async getNextPage() {
 | |
|         const nextInfo = this.nextPageInfo();
 | |
|         if (!nextInfo) {
 | |
|             throw new OpenAIError('No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.');
 | |
|         }
 | |
|         const nextOptions = { ...this.options };
 | |
|         if ('params' in nextInfo && typeof nextOptions.query === 'object') {
 | |
|             nextOptions.query = { ...nextOptions.query, ...nextInfo.params };
 | |
|         }
 | |
|         else if ('url' in nextInfo) {
 | |
|             const params = [...Object.entries(nextOptions.query || {}), ...nextInfo.url.searchParams.entries()];
 | |
|             for (const [key, value] of params) {
 | |
|                 nextInfo.url.searchParams.set(key, value);
 | |
|             }
 | |
|             nextOptions.query = undefined;
 | |
|             nextOptions.path = nextInfo.url.toString();
 | |
|         }
 | |
|         return await __classPrivateFieldGet(this, _AbstractPage_client, "f").requestAPIList(this.constructor, nextOptions);
 | |
|     }
 | |
|     async *iterPages() {
 | |
|         // eslint-disable-next-line @typescript-eslint/no-this-alias
 | |
|         let page = this;
 | |
|         yield page;
 | |
|         while (page.hasNextPage()) {
 | |
|             page = await page.getNextPage();
 | |
|             yield page;
 | |
|         }
 | |
|     }
 | |
|     async *[(_AbstractPage_client = new WeakMap(), Symbol.asyncIterator)]() {
 | |
|         for await (const page of this.iterPages()) {
 | |
|             for (const item of page.getPaginatedItems()) {
 | |
|                 yield item;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| /**
 | |
|  * This subclass of Promise will resolve to an instantiated Page once the request completes.
 | |
|  *
 | |
|  * It also implements AsyncIterable to allow auto-paginating iteration on an unawaited list call, eg:
 | |
|  *
 | |
|  *    for await (const item of client.items.list()) {
 | |
|  *      console.log(item)
 | |
|  *    }
 | |
|  */
 | |
| export class PagePromise extends APIPromise {
 | |
|     constructor(client, request, Page) {
 | |
|         super(request, async (props) => new Page(client, props.response, await defaultParseResponse(props), props.options));
 | |
|     }
 | |
|     /**
 | |
|      * Allow auto-paginating iteration on an unawaited list call, eg:
 | |
|      *
 | |
|      *    for await (const item of client.items.list()) {
 | |
|      *      console.log(item)
 | |
|      *    }
 | |
|      */
 | |
|     async *[Symbol.asyncIterator]() {
 | |
|         const page = await this;
 | |
|         for await (const item of page) {
 | |
|             yield item;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| export const createResponseHeaders = (headers) => {
 | |
|     return new Proxy(Object.fromEntries(
 | |
|     // @ts-ignore
 | |
|     headers.entries()), {
 | |
|         get(target, name) {
 | |
|             const key = name.toString();
 | |
|             return target[key.toLowerCase()] || target[key];
 | |
|         },
 | |
|     });
 | |
| };
 | |
| // This is required so that we can determine if a given object matches the RequestOptions
 | |
| // type at runtime. While this requires duplication, it is enforced by the TypeScript
 | |
| // compiler such that any missing / extraneous keys will cause an error.
 | |
| const requestOptionsKeys = {
 | |
|     method: true,
 | |
|     path: true,
 | |
|     query: true,
 | |
|     body: true,
 | |
|     headers: true,
 | |
|     maxRetries: true,
 | |
|     stream: true,
 | |
|     timeout: true,
 | |
|     httpAgent: true,
 | |
|     signal: true,
 | |
|     idempotencyKey: true,
 | |
|     __metadata: true,
 | |
|     __binaryRequest: true,
 | |
|     __binaryResponse: true,
 | |
|     __streamClass: true,
 | |
| };
 | |
| export const isRequestOptions = (obj) => {
 | |
|     return (typeof obj === 'object' &&
 | |
|         obj !== null &&
 | |
|         !isEmptyObj(obj) &&
 | |
|         Object.keys(obj).every((k) => hasOwn(requestOptionsKeys, k)));
 | |
| };
 | |
| const getPlatformProperties = () => {
 | |
|     if (typeof Deno !== 'undefined' && Deno.build != null) {
 | |
|         return {
 | |
|             'X-Stainless-Lang': 'js',
 | |
|             'X-Stainless-Package-Version': VERSION,
 | |
|             'X-Stainless-OS': normalizePlatform(Deno.build.os),
 | |
|             'X-Stainless-Arch': normalizeArch(Deno.build.arch),
 | |
|             'X-Stainless-Runtime': 'deno',
 | |
|             'X-Stainless-Runtime-Version': typeof Deno.version === 'string' ? Deno.version : Deno.version?.deno ?? 'unknown',
 | |
|         };
 | |
|     }
 | |
|     if (typeof EdgeRuntime !== 'undefined') {
 | |
|         return {
 | |
|             'X-Stainless-Lang': 'js',
 | |
|             'X-Stainless-Package-Version': VERSION,
 | |
|             'X-Stainless-OS': 'Unknown',
 | |
|             'X-Stainless-Arch': `other:${EdgeRuntime}`,
 | |
|             'X-Stainless-Runtime': 'edge',
 | |
|             'X-Stainless-Runtime-Version': process.version,
 | |
|         };
 | |
|     }
 | |
|     // Check if Node.js
 | |
|     if (Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]') {
 | |
|         return {
 | |
|             'X-Stainless-Lang': 'js',
 | |
|             'X-Stainless-Package-Version': VERSION,
 | |
|             'X-Stainless-OS': normalizePlatform(process.platform),
 | |
|             'X-Stainless-Arch': normalizeArch(process.arch),
 | |
|             'X-Stainless-Runtime': 'node',
 | |
|             'X-Stainless-Runtime-Version': process.version,
 | |
|         };
 | |
|     }
 | |
|     const browserInfo = getBrowserInfo();
 | |
|     if (browserInfo) {
 | |
|         return {
 | |
|             'X-Stainless-Lang': 'js',
 | |
|             'X-Stainless-Package-Version': VERSION,
 | |
|             'X-Stainless-OS': 'Unknown',
 | |
|             'X-Stainless-Arch': 'unknown',
 | |
|             'X-Stainless-Runtime': `browser:${browserInfo.browser}`,
 | |
|             'X-Stainless-Runtime-Version': browserInfo.version,
 | |
|         };
 | |
|     }
 | |
|     // TODO add support for Cloudflare workers, etc.
 | |
|     return {
 | |
|         'X-Stainless-Lang': 'js',
 | |
|         'X-Stainless-Package-Version': VERSION,
 | |
|         'X-Stainless-OS': 'Unknown',
 | |
|         'X-Stainless-Arch': 'unknown',
 | |
|         'X-Stainless-Runtime': 'unknown',
 | |
|         'X-Stainless-Runtime-Version': 'unknown',
 | |
|     };
 | |
| };
 | |
| // Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
 | |
| function getBrowserInfo() {
 | |
|     if (typeof navigator === 'undefined' || !navigator) {
 | |
|         return null;
 | |
|     }
 | |
|     // NOTE: The order matters here!
 | |
|     const browserPatterns = [
 | |
|         { key: 'edge', pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
 | |
|         { key: 'ie', pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
 | |
|         { key: 'ie', pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ },
 | |
|         { key: 'chrome', pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
 | |
|         { key: 'firefox', pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
 | |
|         { key: 'safari', pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ },
 | |
|     ];
 | |
|     // Find the FIRST matching browser
 | |
|     for (const { key, pattern } of browserPatterns) {
 | |
|         const match = pattern.exec(navigator.userAgent);
 | |
|         if (match) {
 | |
|             const major = match[1] || 0;
 | |
|             const minor = match[2] || 0;
 | |
|             const patch = match[3] || 0;
 | |
|             return { browser: key, version: `${major}.${minor}.${patch}` };
 | |
|         }
 | |
|     }
 | |
|     return null;
 | |
| }
 | |
| const normalizeArch = (arch) => {
 | |
|     // Node docs:
 | |
|     // - https://nodejs.org/api/process.html#processarch
 | |
|     // Deno docs:
 | |
|     // - https://doc.deno.land/deno/stable/~/Deno.build
 | |
|     if (arch === 'x32')
 | |
|         return 'x32';
 | |
|     if (arch === 'x86_64' || arch === 'x64')
 | |
|         return 'x64';
 | |
|     if (arch === 'arm')
 | |
|         return 'arm';
 | |
|     if (arch === 'aarch64' || arch === 'arm64')
 | |
|         return 'arm64';
 | |
|     if (arch)
 | |
|         return `other:${arch}`;
 | |
|     return 'unknown';
 | |
| };
 | |
| const normalizePlatform = (platform) => {
 | |
|     // Node platforms:
 | |
|     // - https://nodejs.org/api/process.html#processplatform
 | |
|     // Deno platforms:
 | |
|     // - https://doc.deno.land/deno/stable/~/Deno.build
 | |
|     // - https://github.com/denoland/deno/issues/14799
 | |
|     platform = platform.toLowerCase();
 | |
|     // NOTE: this iOS check is untested and may not work
 | |
|     // Node does not work natively on IOS, there is a fork at
 | |
|     // https://github.com/nodejs-mobile/nodejs-mobile
 | |
|     // however it is unknown at the time of writing how to detect if it is running
 | |
|     if (platform.includes('ios'))
 | |
|         return 'iOS';
 | |
|     if (platform === 'android')
 | |
|         return 'Android';
 | |
|     if (platform === 'darwin')
 | |
|         return 'MacOS';
 | |
|     if (platform === 'win32')
 | |
|         return 'Windows';
 | |
|     if (platform === 'freebsd')
 | |
|         return 'FreeBSD';
 | |
|     if (platform === 'openbsd')
 | |
|         return 'OpenBSD';
 | |
|     if (platform === 'linux')
 | |
|         return 'Linux';
 | |
|     if (platform)
 | |
|         return `Other:${platform}`;
 | |
|     return 'Unknown';
 | |
| };
 | |
| let _platformHeaders;
 | |
| const getPlatformHeaders = () => {
 | |
|     return (_platformHeaders ?? (_platformHeaders = getPlatformProperties()));
 | |
| };
 | |
| export const safeJSON = (text) => {
 | |
|     try {
 | |
|         return JSON.parse(text);
 | |
|     }
 | |
|     catch (err) {
 | |
|         return undefined;
 | |
|     }
 | |
| };
 | |
| // https://url.spec.whatwg.org/#url-scheme-string
 | |
| const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i;
 | |
| const isAbsoluteURL = (url) => {
 | |
|     return startsWithSchemeRegexp.test(url);
 | |
| };
 | |
| export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 | |
| const validatePositiveInteger = (name, n) => {
 | |
|     if (typeof n !== 'number' || !Number.isInteger(n)) {
 | |
|         throw new OpenAIError(`${name} must be an integer`);
 | |
|     }
 | |
|     if (n < 0) {
 | |
|         throw new OpenAIError(`${name} must be a positive integer`);
 | |
|     }
 | |
|     return n;
 | |
| };
 | |
| export const castToError = (err) => {
 | |
|     if (err instanceof Error)
 | |
|         return err;
 | |
|     if (typeof err === 'object' && err !== null) {
 | |
|         try {
 | |
|             return new Error(JSON.stringify(err));
 | |
|         }
 | |
|         catch { }
 | |
|     }
 | |
|     return new Error(err);
 | |
| };
 | |
| export const ensurePresent = (value) => {
 | |
|     if (value == null)
 | |
|         throw new OpenAIError(`Expected a value to be given but received ${value} instead.`);
 | |
|     return value;
 | |
| };
 | |
| /**
 | |
|  * Read an environment variable.
 | |
|  *
 | |
|  * Trims beginning and trailing whitespace.
 | |
|  *
 | |
|  * Will return undefined if the environment variable doesn't exist or cannot be accessed.
 | |
|  */
 | |
| export const readEnv = (env) => {
 | |
|     if (typeof process !== 'undefined') {
 | |
|         return process.env?.[env]?.trim() ?? undefined;
 | |
|     }
 | |
|     if (typeof Deno !== 'undefined') {
 | |
|         return Deno.env?.get?.(env)?.trim();
 | |
|     }
 | |
|     return undefined;
 | |
| };
 | |
| export const coerceInteger = (value) => {
 | |
|     if (typeof value === 'number')
 | |
|         return Math.round(value);
 | |
|     if (typeof value === 'string')
 | |
|         return parseInt(value, 10);
 | |
|     throw new OpenAIError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
 | |
| };
 | |
| export const coerceFloat = (value) => {
 | |
|     if (typeof value === 'number')
 | |
|         return value;
 | |
|     if (typeof value === 'string')
 | |
|         return parseFloat(value);
 | |
|     throw new OpenAIError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
 | |
| };
 | |
| export const coerceBoolean = (value) => {
 | |
|     if (typeof value === 'boolean')
 | |
|         return value;
 | |
|     if (typeof value === 'string')
 | |
|         return value === 'true';
 | |
|     return Boolean(value);
 | |
| };
 | |
| export const maybeCoerceInteger = (value) => {
 | |
|     if (value === undefined) {
 | |
|         return undefined;
 | |
|     }
 | |
|     return coerceInteger(value);
 | |
| };
 | |
| export const maybeCoerceFloat = (value) => {
 | |
|     if (value === undefined) {
 | |
|         return undefined;
 | |
|     }
 | |
|     return coerceFloat(value);
 | |
| };
 | |
| export const maybeCoerceBoolean = (value) => {
 | |
|     if (value === undefined) {
 | |
|         return undefined;
 | |
|     }
 | |
|     return coerceBoolean(value);
 | |
| };
 | |
| // https://stackoverflow.com/a/34491287
 | |
| export function isEmptyObj(obj) {
 | |
|     if (!obj)
 | |
|         return true;
 | |
|     for (const _k in obj)
 | |
|         return false;
 | |
|     return true;
 | |
| }
 | |
| // https://eslint.org/docs/latest/rules/no-prototype-builtins
 | |
| export function hasOwn(obj, key) {
 | |
|     return Object.prototype.hasOwnProperty.call(obj, key);
 | |
| }
 | |
| /**
 | |
|  * Copies headers from "newHeaders" onto "targetHeaders",
 | |
|  * using lower-case for all properties,
 | |
|  * ignoring any keys with undefined values,
 | |
|  * and deleting any keys with null values.
 | |
|  */
 | |
| function applyHeadersMut(targetHeaders, newHeaders) {
 | |
|     for (const k in newHeaders) {
 | |
|         if (!hasOwn(newHeaders, k))
 | |
|             continue;
 | |
|         const lowerKey = k.toLowerCase();
 | |
|         if (!lowerKey)
 | |
|             continue;
 | |
|         const val = newHeaders[k];
 | |
|         if (val === null) {
 | |
|             delete targetHeaders[lowerKey];
 | |
|         }
 | |
|         else if (val !== undefined) {
 | |
|             targetHeaders[lowerKey] = val;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| const SENSITIVE_HEADERS = new Set(['authorization', 'api-key']);
 | |
| export function debug(action, ...args) {
 | |
|     if (typeof process !== 'undefined' && process?.env?.['DEBUG'] === 'true') {
 | |
|         const modifiedArgs = args.map((arg) => {
 | |
|             if (!arg) {
 | |
|                 return arg;
 | |
|             }
 | |
|             // Check for sensitive headers in request body 'headers' object
 | |
|             if (arg['headers']) {
 | |
|                 // clone so we don't mutate
 | |
|                 const modifiedArg = { ...arg, headers: { ...arg['headers'] } };
 | |
|                 for (const header in arg['headers']) {
 | |
|                     if (SENSITIVE_HEADERS.has(header.toLowerCase())) {
 | |
|                         modifiedArg['headers'][header] = 'REDACTED';
 | |
|                     }
 | |
|                 }
 | |
|                 return modifiedArg;
 | |
|             }
 | |
|             let modifiedArg = null;
 | |
|             // Check for sensitive headers in headers object
 | |
|             for (const header in arg) {
 | |
|                 if (SENSITIVE_HEADERS.has(header.toLowerCase())) {
 | |
|                     // avoid making a copy until we need to
 | |
|                     modifiedArg ?? (modifiedArg = { ...arg });
 | |
|                     modifiedArg[header] = 'REDACTED';
 | |
|                 }
 | |
|             }
 | |
|             return modifiedArg ?? arg;
 | |
|         });
 | |
|         console.log(`OpenAI:DEBUG:${action}`, ...modifiedArgs);
 | |
|     }
 | |
| }
 | |
| /**
 | |
|  * https://stackoverflow.com/a/2117523
 | |
|  */
 | |
| const uuid4 = () => {
 | |
|     return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
 | |
|         const r = (Math.random() * 16) | 0;
 | |
|         const v = c === 'x' ? r : (r & 0x3) | 0x8;
 | |
|         return v.toString(16);
 | |
|     });
 | |
| };
 | |
| export const isRunningInBrowser = () => {
 | |
|     return (
 | |
|     // @ts-ignore
 | |
|     typeof window !== 'undefined' &&
 | |
|         // @ts-ignore
 | |
|         typeof window.document !== 'undefined' &&
 | |
|         // @ts-ignore
 | |
|         typeof navigator !== 'undefined');
 | |
| };
 | |
| export const isHeadersProtocol = (headers) => {
 | |
|     return typeof headers?.get === 'function';
 | |
| };
 | |
| export const getRequiredHeader = (headers, header) => {
 | |
|     const foundHeader = getHeader(headers, header);
 | |
|     if (foundHeader === undefined) {
 | |
|         throw new Error(`Could not find ${header} header`);
 | |
|     }
 | |
|     return foundHeader;
 | |
| };
 | |
| export const getHeader = (headers, header) => {
 | |
|     const lowerCasedHeader = header.toLowerCase();
 | |
|     if (isHeadersProtocol(headers)) {
 | |
|         // to deal with the case where the header looks like Stainless-Event-Id
 | |
|         const intercapsHeader = header[0]?.toUpperCase() +
 | |
|             header.substring(1).replace(/([^\w])(\w)/g, (_m, g1, g2) => g1 + g2.toUpperCase());
 | |
|         for (const key of [header, lowerCasedHeader, header.toUpperCase(), intercapsHeader]) {
 | |
|             const value = headers.get(key);
 | |
|             if (value) {
 | |
|                 return value;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     for (const [key, value] of Object.entries(headers)) {
 | |
|         if (key.toLowerCase() === lowerCasedHeader) {
 | |
|             if (Array.isArray(value)) {
 | |
|                 if (value.length <= 1)
 | |
|                     return value[0];
 | |
|                 console.warn(`Received ${value.length} entries for the ${header} header, using the first entry.`);
 | |
|                 return value[0];
 | |
|             }
 | |
|             return value;
 | |
|         }
 | |
|     }
 | |
|     return undefined;
 | |
| };
 | |
| /**
 | |
|  * Encodes a string to Base64 format.
 | |
|  */
 | |
| export const toBase64 = (str) => {
 | |
|     if (!str)
 | |
|         return '';
 | |
|     if (typeof Buffer !== 'undefined') {
 | |
|         return Buffer.from(str).toString('base64');
 | |
|     }
 | |
|     if (typeof btoa !== 'undefined') {
 | |
|         return btoa(str);
 | |
|     }
 | |
|     throw new OpenAIError('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined');
 | |
| };
 | |
| /**
 | |
|  * Converts a Base64 encoded string to a Float32Array.
 | |
|  * @param base64Str - The Base64 encoded string.
 | |
|  * @returns An Array of numbers interpreted as Float32 values.
 | |
|  */
 | |
| export const toFloat32Array = (base64Str) => {
 | |
|     if (typeof Buffer !== 'undefined') {
 | |
|         // for Node.js environment
 | |
|         const buf = Buffer.from(base64Str, 'base64');
 | |
|         return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.length / Float32Array.BYTES_PER_ELEMENT));
 | |
|     }
 | |
|     else {
 | |
|         // for legacy web platform APIs
 | |
|         const binaryStr = atob(base64Str);
 | |
|         const len = binaryStr.length;
 | |
|         const bytes = new Uint8Array(len);
 | |
|         for (let i = 0; i < len; i++) {
 | |
|             bytes[i] = binaryStr.charCodeAt(i);
 | |
|         }
 | |
|         return Array.from(new Float32Array(bytes.buffer));
 | |
|     }
 | |
| };
 | |
| export function isObj(obj) {
 | |
|     return obj != null && typeof obj === 'object' && !Array.isArray(obj);
 | |
| }
 | |
| //# sourceMappingURL=core.mjs.map
 |