npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uluops/sdk-core

v0.16.0

Published

Shared infrastructure for UluOps SDKs, including the canonical content-addressing (hash parity) implementation shared with the registry API and @uluops/core

Readme

UluOps · Operating Intelligence as Infrastructure


@uluops/sdk-core

npm version License: MIT Node.js TypeScript

Shared infrastructure for UluOps SDKs. Provides HTTP client, authentication strategies, error hierarchy, configuration loaders, and utility functions used by @uluops/ops-sdk and @uluops/registry-sdk.

Current version: 0.15.0

Quick Start

Building an SDK on top of sdk-core

import { HttpClient, type HttpClientConfig } from '@uluops/sdk-core/http';

// Extend HttpClient with your SDK's defaults
class MyHttpClient extends HttpClient {
  constructor(config: Partial<HttpClientConfig> = {}) {
    super({
      baseUrl: config.baseUrl ?? 'https://api.example.com/v1',
      sdkName: '@my-org/my-sdk',
      sdkVersion: '1.0.0',
      loggerPrefix: '[my-sdk:http]',
      ...config,
    });
  }
}

const http = new MyHttpClient({ apiKey: 'ulr_your-api-key-here' });

// Type-safe API calls with automatic retry, auth, and error mapping
const data = await http.get<{ items: string[] }>('/items');
const created = await http.post<{ id: string }>('/items', { name: 'new item' });

See Error handling with typed errors below for try/catch patterns.

Using the HTTP client directly

import { HttpClient } from '@uluops/sdk-core';

const client = new HttpClient({
  baseUrl: 'https://api.example.com/v1',
  sdkName: 'my-app',
  sdkVersion: '1.0.0',
  loggerPrefix: '[my-app]',
  apiKey: 'ulr_your-api-key-here',
  timeout: 30000,
  retries: 3,
  // Optional: route security-relevant events (rejected credentials, blocked
  // redirects, failed token refresh, credential swaps) to your telemetry sink.
  // See "Security Events" below.
  onSecurityEvent: (event) => myTelemetry.record(event),
});

const result = await client.get<MyType>('/endpoint');

Error handling with typed errors

import { NotFoundError, RateLimitError, isSdkApiError } from '@uluops/sdk-core/errors';

try {
  await client.get('/missing-resource');
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log('Not found:', error.message);
  } else if (error instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${error.retryAfter}s`);
  } else if (isSdkApiError(error)) {
    console.log(`API error [${error.code}]: ${error.message}`);
  }
}

Table of Contents

Overview

This package extracts the shared infrastructure that was duplicated across @uluops/ops-sdk and @uluops/registry-sdk. It provides:

  • HTTP Client: Native fetch-based client with timeout, retry, auth, rate limiting, { data: T } envelope parsing, and a structured security-event channel
  • Authentication: API key and JWT session strategies with automatic token refresh
  • Error Hierarchy: 11 typed error classes mapped to HTTP status codes, plus type guard functions
  • Configuration: Credential chain loader (constructor > env vars > .env files > stored credentials)
  • Utilities: Logger with sensitive data redaction (object-level and string-level), retry with exponential backoff, rate limit header parsing

Prerequisites

  • Node.js 20.3.0 or higher (uses native fetch; streaming uses AbortSignal.any, added in 20.3.0)
  • TypeScript 5.0+ (for TypeScript users)
  • ESM project ("type": "module" in package.json) — this package is ESM-only

Installation

# npm
npm install @uluops/sdk-core

# yarn
yarn add @uluops/sdk-core

# pnpm
pnpm add @uluops/sdk-core

API Reference

HttpClient

The core HTTP client using native fetch with automatic retry, auth, and error mapping.

Configuration

import { HttpClient, type HttpClientConfig } from '@uluops/sdk-core/http';

const client = new HttpClient({
  // Required
  baseUrl: 'https://api.example.com/v1',   // API base URL
  sdkName: '@my-org/my-sdk',               // For User-Agent header
  sdkVersion: '1.0.0',                     // For User-Agent header
  loggerPrefix: '[my-sdk:http]',           // Log message prefix

  // Authentication (choose one)
  apiKey: 'ulr_...',                        // API key (preferred)
  sessionToken: 'jwt-token',               // Existing session token
  email: '[email protected]',               // Email for login
  password: 'password',                    // Password for login

  // Optional
  authBaseUrl: 'https://auth.example.com', // Separate auth endpoint URL
  timeout: 30000,                          // Request timeout in ms (default: 30000)
  retries: 3,                              // Max retry attempts (default: 3)
  debug: false,                            // Enable debug logging
  defaultHeaders: { 'X-Custom': 'value' }, // Extra default headers
  onTokenRefresh: (token) => { /* ... */ },// Token refresh callback
  onRateLimitApproaching: (info) => {      // Fires when remaining < threshold
    console.warn(`${info.remaining}/${info.limit} remaining`);
  },
  rateLimitThreshold: 0.1,                 // Threshold ratio (default: 0.1)
  onRetry: ({ attempt, maxAttempts, error, delayMs }) => {
    console.warn(`Retry ${attempt}/${maxAttempts} in ${delayMs}ms`);
  },
  onSecurityEvent: (event) => {             // Structured security telemetry
    myTelemetry.record(event);              // See "Security Events" below
  },
});

Request Methods

// GET — always retried on transient errors
const data = await client.get<MyType>('/endpoint', { page: 1 });

// POST
const created = await client.post<MyType>('/endpoint', { name: 'value' });

// PUT
const updated = await client.put<MyType>('/endpoint/123', { name: 'updated' });

// PATCH (supports options: params, skipAuth)
const patched = await client.patch<MyType>('/endpoint/123', { name: 'patched' }, { skipAuth: true });

// DELETE
await client.delete('/endpoint/123');

Advanced Request Options

// Retry mutations (POST/PUT/DELETE are NOT retried by default)
const result = await client.request<MyType>('POST', '/idempotent-endpoint', {
  body: { key: 'value' },
  retryMutations: true,
});

// Runtime validation: the generic T is asserted at the JSON boundary, NOT
// validated at runtime (same convention as axios/ky/got). For runtime
// guarantees, parse the result yourself with a Zod schema. The SDK deliberately
// does NOT accept a caller-supplied schema — an earlier `options.schema` was
// removed in 0.11.0 because any object with a `.parse` method was accepted,
// creating a code-execution primitive from untrusted input. See CHANGELOG 0.11.0.
import { z } from 'zod';
const schema = z.object({ id: z.string(), name: z.string() });
const validated = schema.parse(await client.get<unknown>('/endpoint'));

// ⚠️ Raw response — bypasses retry, token refresh, and rate limit tracking
const raw = await client.requestRaw<MyRawType>('GET', '/endpoint');
console.log(raw); // parsed JSON without envelope unwrapping

// ⚠️ Binary response — same bypass as requestRaw
const binary = await client.requestBinary('GET', '/files/report.pdf');
console.log(binary.data, binary.contentType);

Streaming Responses (0.15.0+)

Unlike requestRaw/requestBinary, requestStream/getStream keep full resilience through headers — transient-error retry with backoff/Retry-After, deduplicated 401 token refresh, redirect rejection, rate-limit tracking, and security-event emission — then hand off the redirect-checked, 2xx-guaranteed Response with its body unread. Non-2xx responses buffer the JSON error body and throw the same typed SdkApiError hierarchy as request().

After handoff the transport steps back:

  • No retry ever. A stream that dies after 2xx headers is a 200-then-die the transport cannot heal — integrity verification (row counts, checksums) is the consumer's job.
  • Timeout covers time-to-headers only. The internal timer is released at handoff; a slow or infinite body is not bounded by the transport.
  • Body cancellation belongs to the caller via options.signal, which participates in the fetch for its full lifecycle — thread an AbortSignal and abort it from an idle watchdog if the stream stalls.
// Stream a dataset export (NDJSON) through a BFF passthrough
const watchdog = new AbortController();
const res = await client.getStream(
  '/export/projects/p1/issues',
  { format: 'ndjson' },
  { signal: watchdog.signal }
);
console.log(res.headers.get('x-export-total-rows')); // headers readable at handoff
return new Response(res.body, { headers: filtered(res.headers) }); // caller owns the body

Rate Limit Info

// After any request, check rate limit headers
const info = client.getRateLimitInfo();
if (info) {
  console.log(`${info.remaining}/${info.limit} requests remaining`);
  console.log(`Resets at: ${info.reset}`);
}

Automatic Retries

The client automatically retries on transient errors (502, 503, 504, 429) and network failures (DNS, connection reset, ECONNREFUSED) with exponential backoff and jitter:

  • GET requests: Always retried (up to retries attempts)
  • Mutations (POST/PUT/DELETE): Only retried when retryMutations: true
  • Network errors: Always retried (transient by nature)
  • Backoff: Exponential with jitter (base: 1s, max: 30s)
  • retries: 0: Makes exactly one attempt (no retries) and surfaces the real typed error (e.g. NetworkError) — it does not skip the request
  • 401 handling: Automatic token refresh with deduplication (one refresh at a time). A 401 with no configured credentials throws a UnauthorizedError explaining how to set them; a 401 with credentials present preserves the server's reason and appends guidance that the credential may be expired, revoked, or invalid
  • Redirects are NOT retried: an upstream 3xx from the configured origin throws a non-retryable RedirectError (see Security Events) — it is rejected before the request body is replayed, not treated as a transient fault
  • Visibility: Use onRetry callback to observe retry attempts in real time

Security Events

Beyond the operational onRetry / onRateLimitApproaching callbacks, the client exposes a single structured security-event channel. It delivers the security-relevant events the client already observes as a discriminated union, so you can route them to your telemetry sink instead of scraping free-text logs or classifying thrown errors.

import { HttpClient, type SecurityEvent } from '@uluops/sdk-core';

const client = new HttpClient({
  baseUrl: 'https://api.example.com/v1',
  sdkName: 'my-app',
  sdkVersion: '1.0.0',
  loggerPrefix: '[my-app]',
  apiKey: 'ulr_your-api-key-here',
  onSecurityEvent: (event: SecurityEvent) => {
    switch (event.type) {
      case 'auth_failure':           // a sent credential was rejected (401)
        siem.alert('auth_rejected', { authType: event.authType, requestId: event.requestId });
        break;
      case 'redirect_rejected':      // upstream 3xx blocked before the body was replayed
        siem.alert('redirect_blocked', { origin: event.baseUrl });
        break;
      case 'token_refresh_failed':   // session re-login was rejected
        siem.alert('session_refresh_failed', {});
        break;
      case 'auth_strategy_replaced': // live credential swap via setAuthStrategy
        siem.alert('credential_swapped', { from: event.previousType, to: event.newType });
        break;
    }
  },
});

| Event type | Fires when | Payload fields (beyond type, timestamp, message) | |--------------|-----------|--------------------------------------------------------| | auth_failure | A sent credential is rejected with 401 | authType, statusCode, requestId? | | redirect_rejected | The configured origin returns a 3xx the SDK refuses to follow | baseUrl | | token_refresh_failed | A session token refresh (re-login) is rejected | authType ('session') | | auth_strategy_replaced | The live credential is swapped via setAuthStrategy | previousType, newType |

The channel is reporting, not enforcement — the SDK sets no policy and takes no action; you decide what constitutes an incident. Delivery is best-effort and fire-and-forget: a handler that throws is caught and logged, never propagated into request flow. Every event field is credential-safe by construction, so events may be logged verbatim. All event types (SecurityEvent, SecurityEventHandler, the four event interfaces, AuthType) are exported from the root and /http.


Authentication

Three authentication strategies, resolved via priority chain.

API Key (Recommended)

Keys must start with ulr_, be at least 20 characters, and contain only alphanumeric characters, underscores, and hyphens.

import { ApiKeyAuth } from '@uluops/sdk-core/http';

const auth = new ApiKeyAuth('ulr_your-api-key-here');
auth.getAuthorizationHeader(); // 'Bearer ulr_your-api-key-here'
auth.getType();                // 'api_key'
auth.isAuthenticated();        // true

JWT Session

import { JwtSessionAuth } from '@uluops/sdk-core/http';
import type { FetchClient } from '@uluops/sdk-core/http';

const auth = new JwtSessionAuth({
  email: '[email protected]',
  password: 'password',
  httpClient: myFetchClient,          // For login/refresh requests
  onTokenRefresh: (token) => { /* save token */ },
  // clearCredentialsAfterLogin: true (default) — zeroes password after first
  // login for CWE-316 mitigation, but disables automatic token refresh.
  // Set to false for long-lived sessions (MCP servers, daemons).
});

// Login happens automatically on first request
await auth.refresh();
auth.getSessionToken();  // 'jwt-token-here'
auth.getExpiresAt();     // Date

Strategy Factory

import { createAuthStrategy } from '@uluops/sdk-core/http';

// Priority: apiKey > sessionToken > email/password
const strategy = createAuthStrategy({
  apiKey: 'ulr_...',
});
strategy.getType(); // 'api_key'

Errors

All API errors extend SdkApiError and include statusCode, code, message, details, and requestId.

Error Classes

| Error | Status | When It Happens | |-------|--------|-----------------| | ValidationError | 400 | Invalid request data | | UnauthorizedError | 401 | Missing or invalid credentials | | ForbiddenError | 403 | Valid credentials but insufficient permissions | | NotFoundError | 404 | Resource doesn't exist | | ConflictError | 409 | Name collision or state conflict | | PayloadTooLargeError | 413 | Request body exceeds size limit | | UnprocessableError | 422 | Valid syntax but invalid semantics | | RateLimitError | 429 | Too many requests (retryAfter property) | | ServiceUnavailableError | 503 | Server temporarily down (retryAfter property) | | NetworkError | 0 | DNS failure, connection refused (auto-retried) | | RedirectError | 0 | Configured origin returned a 3xx the SDK refuses to follow (not retried) | | TimeoutError | 0 | Request exceeded timeout |

Error Factory

import { createErrorFromStatus, NotFoundError } from '@uluops/sdk-core/errors';

// Signature: createErrorFromStatus(statusCode, message, code?, details?, requestId?)
const error = createErrorFromStatus(404, 'Project not found', 'NOT_FOUND', { id: '123' });
error instanceof NotFoundError; // true
error.isRetryable();            // false

Type Guards

import {
  isSdkApiError,
  isValidationError,
  isNotFoundError,
  isConflictError,
  isUnprocessableError,
  isRateLimitError,
  isNetworkError,
  isRedirectError,
} from '@uluops/sdk-core/errors';

if (isSdkApiError(error)) {
  console.log(error.code, error.statusCode);
}

Error Serialization

Errors safely serialize to JSON with sensitive details redacted:

const error = new NotFoundError('Item not found', { id: '123', apiKey: 'ulr_secret' });
console.log(JSON.stringify(error));
// { "name": "SdkApiError", "code": "NOT_FOUND", "statusCode": 404,
//   "message": "Item not found", "details": { "id": "123", "apiKey": "[REDACTED]" } }
// Note: keys matching sensitive patterns (apiKey, token, secret, jwt, passwd, etc.) are automatically redacted

Configuration

Credential Loading

Loads credentials from multiple sources in priority order:

  1. Explicit arguments: apiKey, sessionToken, email/password
  2. Environment variables: via SDK-specific EnvVarConfig
  3. Local .env file: via dotenv in the current working directory
  4. Global credentials: ~/.uluops/credentials.json
import {
  loadCredentials,
  loadConfig,
  type EnvVarConfig,
  type Credentials,
  type SdkConfig,
} from '@uluops/sdk-core/config';

// Each SDK passes its own env var names
const MY_ENV_VARS: EnvVarConfig = {
  apiKey: 'MY_API_KEY',
  email: 'MY_EMAIL',
  password: 'MY_PASSWORD',
  baseUrl: 'MY_BASE_URL',
  debug: 'MY_DEBUG',
};

// Load credentials with priority chain
const creds: Credentials = loadCredentials({ envVars: MY_ENV_VARS });

// Load full config (credentials + baseUrl + debug)
const config: SdkConfig = loadConfig({
  envVars: MY_ENV_VARS,
  defaults: { baseUrl: 'https://api.example.com/v1' },
});

Credential Validation

import { validateCredentials, isApiKey, API_KEY_PREFIX } from '@uluops/sdk-core/config';

// Throws ValidationError if no credentials found
validateCredentials({ apiKey: process.env.MY_API_KEY });

// Check API key format. Enforces the ulr_ prefix AND the minimum length
// (MIN_API_KEY_LENGTH, 20 chars) so a value that passes isApiKey also passes
// the ApiKeyAuth constructor.
isApiKey('ulr_abc123def456ghij'); // true  (20 chars)
isApiKey('invalid');              // false (no prefix)
isApiKey('ulr_short');            // false (prefix but too short)

Stored Credentials

import { loadStoredCredentials, getCredentialsPath } from '@uluops/sdk-core/config';

// Path: ~/.uluops/credentials.json
console.log(getCredentialsPath());

// Load from stored file (returns null if not found or invalid)
// Warns if file permissions are world-readable on Unix systems
// Validates field formats (apiKey prefix, non-empty sessionToken/email)
const stored = loadStoredCredentials();

Environment & Paths

import { loadEnvFiles, getGlobalConfigDir } from '@uluops/sdk-core/config';

// Load .env from cwd and ~/.uluops/.env
loadEnvFiles();

// Get the global config directory path (~/.uluops)
const configDir = getGlobalConfigDir();

Constants

import {
  SDK_CORE_VERSION,      // package version string
  DEFAULT_TIMEOUT,       // 30000 ms
  DEFAULT_RETRY_COUNT,   // 3
  BACKOFF_BASE_MS,       // 1000 ms
  MAX_BACKOFF_MS,        // 30000 ms
  JITTER_MIN,            // 0.1 (10% of delay)
  JITTER_MAX,            // 0.2 (20% of delay)
  API_KEY_PREFIX,        // 'ulr_'
  HTTP_STATUS,           // { OK: 200, CREATED: 201, ... }
  ERROR_CODES,           // { NOT_FOUND: 'NOT_FOUND', ... }
  RETRYABLE_STATUS_CODES,// Set(502, 503, 504, 429)
  CONFIG_PATHS,          // { configDir, envFile, credentialsFile }
} from '@uluops/sdk-core/config';

Utilities

Logger

import { createLogger, type Logger } from '@uluops/sdk-core/utils';

const logger: Logger = createLogger('[my-sdk]', true);
logger.debug('Fetching', '/api/items');
logger.warn('Retry attempt', 2);
logger.error('Request failed', error);

// Disabled logger (no-ops)
const silent = createLogger('[my-sdk]', false);

Sensitive Data Redaction

import { redactSensitive, sanitizeForLog, sanitizeForDisplay, sanitizeString } from '@uluops/sdk-core/utils';

// Redact showing last 4 chars
redactSensitive('ulr_secret_key_12345678', 4); // '***5678'

// Sanitize an object for logging (redacts sensitive keys in structured data)
sanitizeForLog({ apiKey: 'ulr_...', name: 'safe' });
// { apiKey: '[REDACTED]', name: 'safe' }

// Sanitize an object for display (deep, strips sensitive keys)
sanitizeForDisplay({ apiKey: 'ulr_...', name: 'safe' });
// { apiKey: '[REDACTED]', name: 'safe' }

// Sanitize a string (redacts credential values in free-form text)
sanitizeString('Login failed with apiKey=ulr_abc123def456');
// 'Login failed with [REDACTED]'

Retry with Backoff

import { retry, sleep } from '@uluops/sdk-core/utils';

// Retry an async operation with exponential backoff
const result = await retry(
  () => fetchData(),
  { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000 },
);

// Simple delay
await sleep(1000);

Helpers

import { isPlainObject, isUuid, truncate, toQuery } from '@uluops/sdk-core/utils';

isPlainObject({});           // true
isPlainObject([]);           // false
isUuid('550e8400-...');      // true
truncate('long string', 5); // 'long ...'

// Build query params from an object (accepts string, number, boolean, null)
const params = toQuery({ page: 1, active: true, empty: undefined });
// { page: 1, active: true } — undefined values stripped

Rate Limit Parsing

import { parseRateLimitHeaders, type RateLimitInfo } from '@uluops/sdk-core/utils';

const headers = new Headers({
  'x-ratelimit-limit': '100',
  'x-ratelimit-remaining': '42',
  'x-ratelimit-reset': '1700000000',
});

const info: RateLimitInfo | undefined = parseRateLimitHeaders(headers);
// { limit: 100, remaining: 42, reset: Date }

Content Hashing

The canonical content-addressing implementation shared by the registry API (which computes and stores hashes at publish time) and @uluops/core (which verifies caller-pinned hashes at resolve time). Both sides import from here so they hash identically by construction.

Two artifacts are hashed with two different rules and are not interchangeable:

  • YAML sourcecomputeHash — normalizes first (yaml.parseyaml.stringify with sorted keys), so semantically identical documents hash identically. Falls back to raw bytes for non-object / unparseable content.
  • Rendered promptcomputePromptHash — hashes the markdown byte-for-byte with no normalization.
import {
  computeHash,
  computePromptHash,
  verifyHash,
  verifyPromptHash,
} from '@uluops/sdk-core/utils';

const yamlHash = computeHash(definitionYaml);        // 'sha256:...' (normalized)
const promptHash = computePromptHash(renderedPrompt); // 'sha256:...' (raw bytes)

// Verification is timing-safe and never throws on a malformed/wrong-length pin —
// it returns false (a clean refusal), so caller-supplied pins can't crash you.
verifyHash(definitionYaml, expectedHash);        // boolean
verifyPromptHash(renderedPrompt, expectedPromptHash); // boolean

The yaml dependency is pinned exact (2.9.0) to match the version that produced the registry's stored hashes. Bumping it can change normalization and is guarded by golden fixtures captured from real published definitions.

Package Exports

| Export Path | Contents | |------------|----------| | @uluops/sdk-core | Everything (HttpClient, errors, config, utils) | | @uluops/sdk-core/http | HttpClient (incl. requestStream/getStream), RequestStreamOptions, ApiKeyAuth, JwtSessionAuth, createAuthStrategy, SecurityEvent types | | @uluops/sdk-core/errors | SdkApiError + all error subclasses, createErrorFromStatus, type guards | | @uluops/sdk-core/config | loadCredentials, loadConfig, constants, EnvVarConfig | | @uluops/sdk-core/utils | createLogger, redactSensitive, sanitizeString, sleep, retry, toQuery, computeHash, computePromptHash, verifyHash, verifyPromptHash |

Extending for Your SDK

This package is designed to be extended, not used directly by end users. Here's the pattern used by @uluops/ops-sdk and @uluops/registry-sdk:

1. Extend HttpClient with SDK defaults

// my-sdk/src/http/http-client.ts
import { HttpClient, type HttpClientConfig } from '@uluops/sdk-core/http';
import { SDK_VERSION, DEFAULT_BASE_URL } from '../config/constants.js';

export class MyHttpClient extends HttpClient {
  constructor(config: Partial<HttpClientConfig> = {}) {
    super({
      ...config,
      baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
      sdkName: '@my-org/my-sdk',
      sdkVersion: SDK_VERSION,
      loggerPrefix: '[my-sdk:http]',
    });
  }
}

2. Alias the base error class

// my-sdk/src/errors/errors.ts
export {
  SdkApiError as MyApiError,
  isSdkApiError as isMyApiError,
  ValidationError,
  NotFoundError,
  // ... all other errors
} from '@uluops/sdk-core/errors';

3. Wrap config loaders with SDK-specific env vars

// my-sdk/src/config/loaders.ts
import { loadConfig as coreLoadConfig, type EnvVarConfig } from '@uluops/sdk-core/config';

const MY_ENV_VARS: EnvVarConfig = {
  apiKey: 'MY_SDK_API_KEY',
  email: 'MY_SDK_EMAIL',
  password: 'MY_SDK_PASSWORD',
  baseUrl: 'MY_SDK_BASE_URL',
  debug: 'MY_SDK_DEBUG',
};

export function loadConfig(options = {}) {
  return coreLoadConfig({
    ...options,
    envVars: MY_ENV_VARS,
    defaults: { baseUrl: 'https://api.my-service.com/v1' },
  });
}

4. Re-export shared utilities

// my-sdk/src/utils/helpers.ts
export { sleep, retry, truncate, isPlainObject, isUuid } from '@uluops/sdk-core/utils';

// Add SDK-specific helpers
export function myCustomHelper() { /* ... */ }

5. Auth strategy access (for login flows)

HttpClient exposes its auth strategy so an SDK subclass can implement a login() method that swaps the credential in place after authenticating:

import { JwtSessionAuth } from '@uluops/sdk-core/http';

class MyHttpClient extends HttpClient {
  async login(email: string, password: string): Promise<void> {
    const auth = new JwtSessionAuth(this.createFetchClient(), { email, password });
    await auth.refresh();          // performs the login POST against authBaseUrl
    this.setAuthStrategy(auth);    // subsequent requests use the session token
  }
}

client.getAuthStrategy();  // AuthStrategy | null — current credential strategy
client.getAuthBaseUrl();   // string — the auth endpoint origin (for temp clients)

setAuthStrategy performs no authorization check of its own — replacing the credential is an intentional trusted-caller capability (the caller already holds the client reference). Because the swap changes which credential every subsequent request carries, it emits an auth_strategy_replaced security event so embedders can observe and correlate credential changes.

License

MIT License - see LICENSE for details.