@arke-institute/sdk 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{crypto-Fh8b6rTi.d.ts → crypto-96bEBoO_.d.ts} +89 -1
- package/dist/{crypto-k-x66Nei.d.cts → crypto-wBQ1LBgz.d.cts} +89 -1
- package/dist/generated/index.d.cts +102 -61
- package/dist/generated/index.d.ts +102 -61
- package/dist/index.cjs +184 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +184 -1
- package/dist/index.js.map +1 -1
- package/dist/operations/index.d.cts +2 -2
- package/dist/operations/index.d.ts +2 -2
- package/openapi/spec.json +169 -91
- package/openapi/version.json +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client/ArkeClient.ts","../src/client/config.ts","../src/client/errors.ts","../src/operations/upload/engine.ts","../src/operations/upload/cid.ts","../src/operations/folders.ts","../src/operations/batch.ts","../src/operations/crypto.ts"],"sourcesContent":["/**\n * @arke-institute/sdk\n *\n * TypeScript SDK for the Arke API - auto-generated from OpenAPI spec.\n *\n * @example\n * ```typescript\n * import { ArkeClient } from '@arke-institute/sdk';\n *\n * const arke = new ArkeClient({ authToken: 'your-jwt-token' });\n *\n * // Create an entity\n * const { data, error } = await arke.api.POST('/entities', {\n * body: {\n * collection_id: '01ABC...',\n * type: 'document',\n * properties: { title: 'My Document' }\n * }\n * });\n *\n * if (error) {\n * console.error('Failed to create entity:', error);\n * } else {\n * console.log('Created entity:', data.id);\n * }\n * ```\n */\n\n// Main client\nexport {\n ArkeClient,\n createArkeClient,\n isApiKey,\n getAuthorizationHeader,\n type ArkeApiClient,\n} from './client/ArkeClient.js';\n\n// Configuration\nexport { type ArkeClientConfig, DEFAULT_CONFIG } from './client/config.js';\n\n// Errors\nexport {\n ArkeError,\n CASConflictError,\n NotFoundError,\n ValidationError,\n AuthenticationError,\n ForbiddenError,\n parseApiError,\n} from './client/errors.js';\n\n// Generated types\nexport type { paths, components, operations } from './generated/index.js';\n\n// High-level operations (TODO: implement)\nexport {\n FolderOperations,\n BatchOperations,\n CryptoOperations,\n type UploadProgress,\n type UploadDirectoryOptions,\n type UploadDirectoryResult,\n type BatchCreateOptions,\n type BatchResult,\n type KeyPair,\n type SignedPayload,\n} from './operations/index.js';\n","/**\n * Main Arke SDK Client\n *\n * Provides type-safe access to the Arke API using openapi-fetch.\n */\n\nimport createClient, { type Client } from 'openapi-fetch';\nimport type { paths } from '../generated/types.js';\nimport { ArkeClientConfig, DEFAULT_CONFIG } from './config.js';\n\nexport type ArkeApiClient = Client<paths>;\n\n/**\n * Check if a token is an API key (starts with 'ak_' or 'uk_')\n */\nexport function isApiKey(token: string): boolean {\n return token.startsWith('ak_') || token.startsWith('uk_');\n}\n\n/**\n * Get the appropriate Authorization header value for a token\n * - API keys (ak_*, uk_*) use: ApiKey {token}\n * - JWT tokens use: Bearer {token}\n */\nexport function getAuthorizationHeader(token: string): string {\n if (isApiKey(token)) {\n return `ApiKey ${token}`;\n }\n return `Bearer ${token}`;\n}\n\n/**\n * Type-safe client for the Arke API\n *\n * @example\n * ```typescript\n * // With JWT token\n * const arke = new ArkeClient({ authToken: 'your-jwt-token' });\n *\n * // With API key (agent or user)\n * const arke = new ArkeClient({ authToken: 'ak_your-agent-api-key' });\n * const arke = new ArkeClient({ authToken: 'uk_your-user-api-key' });\n *\n * // Create an entity\n * const { data, error } = await arke.api.POST('/entities', {\n * body: {\n * collection_id: '01ABC...',\n * type: 'document',\n * properties: { title: 'My Document' }\n * }\n * });\n *\n * // Get an entity\n * const { data } = await arke.api.GET('/entities/{id}', {\n * params: { path: { id: '01XYZ...' } }\n * });\n * ```\n */\nexport class ArkeClient {\n /**\n * The underlying openapi-fetch client with full type safety\n * Use this for all API calls: arke.api.GET, arke.api.POST, etc.\n */\n public api: ArkeApiClient;\n\n private config: ArkeClientConfig;\n\n constructor(config: ArkeClientConfig = {}) {\n this.config = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n\n this.api = this.createClient();\n }\n\n private createClient(): ArkeApiClient {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.config.headers,\n };\n\n if (this.config.authToken) {\n headers['Authorization'] = getAuthorizationHeader(this.config.authToken);\n }\n\n if (this.config.network === 'test') {\n headers['X-Arke-Network'] = 'test';\n }\n\n return createClient<paths>({\n baseUrl: this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl,\n headers,\n });\n }\n\n /**\n * Update the authentication token\n * Recreates the underlying client with new headers\n */\n setAuthToken(token: string): void {\n this.config.authToken = token;\n this.api = this.createClient();\n }\n\n /**\n * Clear the authentication token\n */\n clearAuthToken(): void {\n this.config.authToken = undefined;\n this.api = this.createClient();\n }\n\n /**\n * Get the current configuration\n */\n getConfig(): Readonly<ArkeClientConfig> {\n return { ...this.config };\n }\n\n /**\n * Get the base URL\n */\n get baseUrl(): string {\n return this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl;\n }\n\n /**\n * Check if client is authenticated\n */\n get isAuthenticated(): boolean {\n return !!this.config.authToken;\n }\n\n /**\n * Get file content as a Blob\n *\n * This is a convenience method that handles the binary response parsing\n * that openapi-fetch doesn't handle automatically.\n *\n * @example\n * ```typescript\n * const { data, error } = await arke.getFileContent('01ABC...');\n * if (data) {\n * const text = await data.text();\n * // or\n * const arrayBuffer = await data.arrayBuffer();\n * }\n * ```\n */\n async getFileContent(\n fileId: string\n ): Promise<{ data: Blob | undefined; error: unknown }> {\n const { data, error } = await this.api.GET('/files/{id}/content', {\n params: { path: { id: fileId } },\n parseAs: 'blob',\n });\n return { data: data as Blob | undefined, error };\n }\n\n /**\n * Get file content as an ArrayBuffer\n *\n * This is a convenience method that handles the binary response parsing\n * that openapi-fetch doesn't handle automatically.\n *\n * @example\n * ```typescript\n * const { data, error } = await arke.getFileContentAsArrayBuffer('01ABC...');\n * if (data) {\n * const bytes = new Uint8Array(data);\n * }\n * ```\n */\n async getFileContentAsArrayBuffer(\n fileId: string\n ): Promise<{ data: ArrayBuffer | undefined; error: unknown }> {\n const { data, error } = await this.api.GET('/files/{id}/content', {\n params: { path: { id: fileId } },\n parseAs: 'arrayBuffer',\n });\n return { data: data as ArrayBuffer | undefined, error };\n }\n\n /**\n * Get file content as a ReadableStream\n *\n * This is a convenience method for streaming large files.\n *\n * @example\n * ```typescript\n * const { data, error } = await arke.getFileContentAsStream('01ABC...');\n * if (data) {\n * const reader = data.getReader();\n * // Process chunks...\n * }\n * ```\n */\n async getFileContentAsStream(\n fileId: string\n ): Promise<{ data: ReadableStream<Uint8Array> | null | undefined; error: unknown }> {\n const { data, error } = await this.api.GET('/files/{id}/content', {\n params: { path: { id: fileId } },\n parseAs: 'stream',\n });\n return { data: data as ReadableStream<Uint8Array> | null | undefined, error };\n }\n}\n\n/**\n * Create a new ArkeClient instance\n * Convenience function for those who prefer functional style\n */\nexport function createArkeClient(config?: ArkeClientConfig): ArkeClient {\n return new ArkeClient(config);\n}\n\n// Re-export types and errors\nexport type { ArkeClientConfig } from './config.js';\nexport * from './errors.js';\n","/**\n * SDK configuration types\n */\n\nexport interface ArkeClientConfig {\n /**\n * Base URL for the Arke API\n * @default 'https://arke-v1.arke.institute'\n */\n baseUrl?: string;\n\n /**\n * Authentication token - accepts either:\n * - JWT token from Supabase auth (sent as Bearer)\n * - Agent API key with 'ak_' prefix (sent as ApiKey)\n * - User API key with 'uk_' prefix (sent as ApiKey)\n *\n * The correct Authorization header format is auto-detected from the token prefix.\n */\n authToken?: string;\n\n /**\n * Network to use ('main' or 'test')\n * Test network uses 'II' prefixed IDs and isolated data\n * @default 'main'\n */\n network?: 'main' | 'test';\n\n /**\n * Callback to refresh auth token when expired\n * Called automatically on 401 responses\n */\n onTokenRefresh?: () => Promise<string>;\n\n /**\n * Custom headers to include in all requests\n */\n headers?: Record<string, string>;\n}\n\nexport const DEFAULT_CONFIG: Required<Pick<ArkeClientConfig, 'baseUrl' | 'network'>> = {\n baseUrl: 'https://arke-v1.arke.institute',\n network: 'main',\n};\n","/**\n * SDK error classes\n */\n\n/**\n * Base error class for all Arke SDK errors\n */\nexport class ArkeError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly status?: number,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'ArkeError';\n // Maintains proper stack trace for where error was thrown\n Error.captureStackTrace?.(this, this.constructor);\n }\n\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n status: this.status,\n details: this.details,\n };\n }\n}\n\n/**\n * CAS (Compare-And-Swap) conflict - entity was modified by another request\n */\nexport class CASConflictError extends ArkeError {\n constructor(\n public readonly expectedTip?: string,\n public readonly actualTip?: string\n ) {\n super(\n 'Entity was modified by another request. Refresh and retry with the current tip.',\n 'CAS_CONFLICT',\n 409,\n { expectedTip, actualTip }\n );\n this.name = 'CASConflictError';\n }\n}\n\n/**\n * Resource not found\n */\nexport class NotFoundError extends ArkeError {\n constructor(resourceType: string, id: string) {\n super(`${resourceType} not found: ${id}`, 'NOT_FOUND', 404, { resourceType, id });\n this.name = 'NotFoundError';\n }\n}\n\n/**\n * Validation error - invalid request data\n */\nexport class ValidationError extends ArkeError {\n constructor(\n message: string,\n public readonly field?: string,\n details?: unknown\n ) {\n super(message, 'VALIDATION_ERROR', 400, details ?? { field });\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Authentication required or invalid\n */\nexport class AuthenticationError extends ArkeError {\n constructor(message = 'Authentication required') {\n super(message, 'AUTH_REQUIRED', 401);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Permission denied\n */\nexport class ForbiddenError extends ArkeError {\n constructor(action?: string, resource?: string) {\n const msg = action\n ? `Permission denied: ${action}${resource ? ` on ${resource}` : ''}`\n : 'Permission denied';\n super(msg, 'FORBIDDEN', 403, { action, resource });\n this.name = 'ForbiddenError';\n }\n}\n\n/**\n * Parse API error response into appropriate error class\n */\nexport function parseApiError(status: number, body: unknown): ArkeError {\n const errorBody = body as { error?: string; message?: string; details?: unknown } | null;\n const message = errorBody?.error ?? errorBody?.message ?? 'Unknown error';\n\n switch (status) {\n case 400:\n return new ValidationError(message, undefined, errorBody?.details);\n\n case 401:\n return new AuthenticationError(message);\n\n case 403:\n return new ForbiddenError(message);\n\n case 404:\n return new NotFoundError('Resource', 'unknown');\n\n case 409: {\n // Parse CAS conflict details if available\n const details = errorBody?.details as { expected?: string; actual?: string } | undefined;\n return new CASConflictError(details?.expected, details?.actual);\n }\n\n default:\n return new ArkeError(message, 'API_ERROR', status, errorBody?.details);\n }\n}\n","/**\n * Upload Engine\n *\n * Core upload implementation optimized for fast tree visibility:\n * 1. Create folders by depth (with unidirectional 'in' → parent)\n * 2. Create file entities (metadata only, high parallelism)\n * 3. Backlink parents with 'contains' relationships\n * → Tree is now browsable! Users can explore structure immediately.\n * 4. Upload file content via POST /files/{id}/content (byte-based pool, ~200MB in flight)\n * - Direct upload to API endpoint\n * - API streams to R2, computes CID, updates entity atomically\n *\n * Byte-based pool (for uploads):\n * - Maintains ~200MB of data in flight at all times\n * - When a file completes, next file starts immediately (no gaps)\n * - Small files: Many upload in parallel\n * - Large files: Fewer upload in parallel (bandwidth-limited)\n * - Single file > 200MB: Uploads alone when pool is empty\n *\n * This minimizes time-to-browse by:\n * - Creating all entities before uploading content\n * - Using unidirectional 'in' relationship on entity creation\n * - Adding all 'contains' relationships in a single PUT per parent\n */\n\nimport type { ArkeClient } from '../../client/ArkeClient.js';\nimport type { components } from '../../generated/types.js';\nimport type {\n UploadTree,\n UploadOptions,\n UploadResult,\n UploadProgress,\n CreatedFolder,\n CreatedFile,\n CreatedEntity,\n UploadFolder,\n} from './types.js';\n\ntype CreateCollectionRequest = components['schemas']['CreateCollectionRequest'];\ntype CreateFolderRequest = components['schemas']['CreateFolderRequest'];\ntype CreateFileRequest = components['schemas']['CreateFileRequest'];\ntype UpdateFolderRequest = components['schemas']['UpdateFolderRequest'];\ntype UpdateCollectionRequest = components['schemas']['UpdateCollectionRequest'];\n\n// Phase constants\nconst PHASE_COUNT = 3; // creating, backlinking, uploading (excluding complete/error)\nconst PHASE_INDEX: Record<string, number> = {\n creating: 0,\n backlinking: 1,\n uploading: 2,\n complete: 3,\n error: -1,\n};\n\n// =============================================================================\n// Concurrency Utilities\n// =============================================================================\n\n/**\n * Simple concurrency limiter for parallel operations.\n */\nasync function parallelLimit<T, R>(\n items: T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>\n): Promise<R[]> {\n const results: R[] = [];\n let index = 0;\n\n async function worker(): Promise<void> {\n while (index < items.length) {\n const currentIndex = index++;\n const item = items[currentIndex]!;\n results[currentIndex] = await fn(item, currentIndex);\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());\n await Promise.all(workers);\n\n return results;\n}\n\n// =============================================================================\n// Byte-Based Pool\n// =============================================================================\n\n/** Target bytes in flight (~200MB) */\nconst TARGET_BYTES_IN_FLIGHT = 200 * 1024 * 1024;\n\n/**\n * Pool that maintains a target number of bytes in flight.\n *\n * When a file completes, its bytes are released and the next\n * waiting file can start immediately. This keeps the pipeline full.\n *\n * - Small files: Many run in parallel (up to ~200MB worth)\n * - Large files: Fewer run in parallel\n * - Single file > 200MB: Runs alone (allowed when pool is empty)\n */\nclass BytePool {\n private bytesInFlight = 0;\n private waitQueue: Array<() => void> = [];\n\n constructor(private targetBytes: number = TARGET_BYTES_IN_FLIGHT) {}\n\n async run<T>(size: number, fn: () => Promise<T>): Promise<T> {\n // Wait until we have room\n // Exception: if pool is empty, always allow (handles files > targetBytes)\n while (this.bytesInFlight > 0 && this.bytesInFlight + size > this.targetBytes) {\n await new Promise<void>((resolve) => this.waitQueue.push(resolve));\n }\n\n this.bytesInFlight += size;\n try {\n return await fn();\n } finally {\n this.bytesInFlight -= size;\n // Wake up next waiting task\n const next = this.waitQueue.shift();\n if (next) next();\n }\n }\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Parse folder path to get parent path.\n * e.g., \"docs/images/photos\" -> \"docs/images\"\n */\nfunction getParentPath(relativePath: string): string | null {\n const lastSlash = relativePath.lastIndexOf('/');\n if (lastSlash === -1) return null;\n return relativePath.slice(0, lastSlash);\n}\n\n/**\n * Group folders by depth level.\n */\nfunction groupFoldersByDepth(folders: UploadFolder[]): Map<number, UploadFolder[]> {\n const byDepth = new Map<number, UploadFolder[]>();\n\n for (const folder of folders) {\n const depth = folder.relativePath.split('/').length - 1;\n if (!byDepth.has(depth)) byDepth.set(depth, []);\n byDepth.get(depth)!.push(folder);\n }\n\n return byDepth;\n}\n\n// =============================================================================\n// Main Upload Function\n// =============================================================================\n\n/**\n * Main upload function.\n * Orchestrates the entire upload process with optimized relationship strategy.\n */\nexport async function uploadTree(\n client: ArkeClient,\n tree: UploadTree,\n options: UploadOptions\n): Promise<UploadResult> {\n const { target, onProgress, concurrency = 10, continueOnError = false, note } = options;\n\n const errors: Array<{ path: string; error: string }> = [];\n const createdFolders: CreatedFolder[] = [];\n const createdFiles: CreatedFile[] = [];\n\n // Maps for tracking (include label for peer_label in relationships)\n const foldersByPath = new Map<string, { id: string; cid: string; label: string }>();\n\n // Calculate totals\n const totalEntities = tree.files.length + tree.folders.length;\n const totalBytes = tree.files.reduce((sum, f) => sum + f.size, 0);\n let completedEntities = 0;\n let bytesUploaded = 0;\n\n // Helper to report progress\n const reportProgress = (progress: Partial<UploadProgress>) => {\n if (onProgress) {\n const phase = progress.phase || 'creating';\n const phaseIndex = PHASE_INDEX[phase] ?? -1;\n\n // Calculate phase percent based on current phase\n let phasePercent = 0;\n if (phase === 'creating') {\n // Creating phase: progress is based on entities created\n const done = progress.completedEntities ?? completedEntities;\n phasePercent = totalEntities > 0 ? Math.round((done / totalEntities) * 100) : 100;\n } else if (phase === 'backlinking') {\n // Backlinking phase: progress is based on parents updated\n const done = progress.completedParents ?? 0;\n const total = progress.totalParents ?? 0;\n phasePercent = total > 0 ? Math.round((done / total) * 100) : 100;\n } else if (phase === 'uploading') {\n // Uploading phase: progress is based on bytes uploaded\n const done = progress.bytesUploaded ?? bytesUploaded;\n phasePercent = totalBytes > 0 ? Math.round((done / totalBytes) * 100) : 100;\n } else if (phase === 'complete') {\n phasePercent = 100;\n }\n\n onProgress({\n phase,\n phaseIndex,\n phaseCount: PHASE_COUNT,\n phasePercent,\n totalEntities,\n completedEntities,\n totalParents: 0,\n completedParents: 0,\n totalBytes,\n bytesUploaded,\n ...progress,\n } as UploadProgress);\n }\n };\n\n try {\n // ─────────────────────────────────────────────────────────────────────────\n // SETUP: Resolve or create collection\n // ─────────────────────────────────────────────────────────────────────────\n let collectionId: string;\n let collectionCid: string;\n let collectionLabel: string;\n let collectionCreated = false;\n\n if (target.createCollection) {\n const collectionBody: CreateCollectionRequest = {\n label: target.createCollection.label,\n description: target.createCollection.description,\n roles: target.createCollection.roles,\n note,\n };\n\n const { data, error } = await client.api.POST('/collections', {\n body: collectionBody,\n });\n\n if (error || !data) {\n throw new Error(`Failed to create collection: ${JSON.stringify(error)}`);\n }\n\n collectionId = data.id;\n collectionCid = data.cid;\n collectionLabel = target.createCollection.label;\n collectionCreated = true;\n } else if (target.collectionId) {\n collectionId = target.collectionId;\n\n const { data, error } = await client.api.GET('/collections/{id}', {\n params: { path: { id: collectionId } },\n });\n\n if (error || !data) {\n throw new Error(`Failed to fetch collection: ${JSON.stringify(error)}`);\n }\n\n collectionCid = data.cid;\n collectionLabel = (data.properties?.label as string) ?? collectionId;\n } else {\n throw new Error('Must provide either collectionId or createCollection in target');\n }\n\n // Determine the parent for root-level items\n const rootParentId = target.parentId ?? collectionId;\n let rootParentLabel = collectionLabel;\n let rootParentType: 'collection' | 'folder' = 'collection';\n\n // If a specific parent folder is provided, fetch its label\n if (target.parentId && target.parentId !== collectionId) {\n const { data: parentData, error: parentError } = await client.api.GET('/folders/{id}', {\n params: { path: { id: target.parentId } },\n });\n if (parentError || !parentData) {\n throw new Error(`Failed to fetch parent folder: ${JSON.stringify(parentError)}`);\n }\n rootParentLabel = (parentData.properties?.label as string) ?? target.parentId;\n rootParentType = 'folder';\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // PHASE 1: Create entities (folders by depth, then files)\n // ─────────────────────────────────────────────────────────────────────────\n reportProgress({ phase: 'creating', completedEntities: 0 });\n\n // Group folders by depth\n const foldersByDepth = groupFoldersByDepth(tree.folders);\n const sortedDepths = [...foldersByDepth.keys()].sort((a, b) => a - b);\n\n // Create folders depth by depth (parents before children)\n for (const depth of sortedDepths) {\n const foldersAtDepth = foldersByDepth.get(depth)!;\n\n await Promise.all(\n foldersAtDepth.map(async (folder) => {\n try {\n const parentPath = getParentPath(folder.relativePath);\n const parentInfo = parentPath ? foldersByPath.get(parentPath)! : null;\n const parentId = parentInfo ? parentInfo.id : rootParentId;\n const parentType = parentInfo ? 'folder' : rootParentType;\n const parentLabel = parentInfo ? parentInfo.label : rootParentLabel;\n\n const folderBody: CreateFolderRequest = {\n label: folder.name,\n collection: collectionId,\n note,\n relationships: [{ predicate: 'in', peer: parentId, peer_type: parentType, peer_label: parentLabel }],\n };\n\n const { data, error } = await client.api.POST('/folders', {\n body: folderBody,\n });\n\n if (error || !data) {\n throw new Error(JSON.stringify(error));\n }\n\n // Track folder (include label for peer_label in child relationships)\n foldersByPath.set(folder.relativePath, { id: data.id, cid: data.cid, label: folder.name });\n createdFolders.push({\n name: folder.name,\n relativePath: folder.relativePath,\n id: data.id,\n entityCid: data.cid,\n });\n\n completedEntities++;\n reportProgress({\n phase: 'creating',\n completedEntities,\n currentItem: folder.relativePath,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: folder.relativePath, error: `Folder creation failed: ${errorMsg}` });\n completedEntities++;\n } else {\n throw new Error(`Failed to create folder ${folder.relativePath}: ${errorMsg}`);\n }\n }\n })\n );\n }\n\n // Create file entities (metadata only, no content upload yet)\n // Use simple concurrency limit for API calls\n const FILE_CREATION_CONCURRENCY = 50;\n\n await parallelLimit(tree.files, FILE_CREATION_CONCURRENCY, async (file) => {\n try {\n const parentPath = getParentPath(file.relativePath);\n const parentInfo = parentPath ? foldersByPath.get(parentPath)! : null;\n const parentId = parentInfo ? parentInfo.id : rootParentId;\n const parentType = parentInfo ? 'folder' : rootParentType;\n const parentLabel = parentInfo ? parentInfo.label : rootParentLabel;\n\n // Create file entity with 'in' relationship (include peer_label for display)\n // Server computes CID when content is uploaded\n const fileBody: CreateFileRequest = {\n key: crypto.randomUUID(), // Generate unique storage key\n filename: file.name,\n label: file.name, // Display label for the file\n content_type: file.mimeType,\n size: file.size,\n collection: collectionId,\n relationships: [{ predicate: 'in', peer: parentId, peer_type: parentType, peer_label: parentLabel }],\n };\n\n const { data, error } = await client.api.POST('/files', {\n body: fileBody,\n });\n\n if (error || !data) {\n throw new Error(`Entity creation failed: ${JSON.stringify(error)}`);\n }\n\n // Track file for later upload\n createdFiles.push({\n ...file,\n id: data.id,\n entityCid: data.cid,\n });\n\n completedEntities++;\n reportProgress({\n phase: 'creating',\n completedEntities,\n currentItem: file.relativePath,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: file.relativePath, error: errorMsg });\n completedEntities++;\n } else {\n throw new Error(`Failed to create file ${file.relativePath}: ${errorMsg}`);\n }\n }\n });\n\n // ─────────────────────────────────────────────────────────────────────────\n // PHASE 2: Backlink - Update each parent with 'contains' relationships\n // ─────────────────────────────────────────────────────────────────────────\n\n // Build parent -> children map (include label for peer_label)\n const childrenByParent = new Map<string, Array<{ id: string; type: 'file' | 'folder'; label: string }>>();\n\n // Add folders as children of their parents\n for (const folder of createdFolders) {\n const parentPath = getParentPath(folder.relativePath);\n const parentId = parentPath ? foldersByPath.get(parentPath)!.id : rootParentId;\n\n if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);\n childrenByParent.get(parentId)!.push({ id: folder.id, type: 'folder', label: folder.name });\n }\n\n // Add files as children of their parents\n for (const file of createdFiles) {\n const parentPath = getParentPath(file.relativePath);\n const parentId = parentPath ? foldersByPath.get(parentPath)!.id : rootParentId;\n\n if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);\n childrenByParent.get(parentId)!.push({ id: file.id, type: 'file', label: file.name });\n }\n\n const totalParents = childrenByParent.size;\n let completedParents = 0;\n\n reportProgress({ phase: 'backlinking', totalParents, completedParents: 0 });\n\n // Update all parents in parallel - each parent gets one PUT with all its children\n const parentEntries = [...childrenByParent.entries()];\n\n await parallelLimit(parentEntries, concurrency, async ([parentId, children]) => {\n try {\n const isCollection = parentId === collectionId;\n\n // Build relationships_add array with all children (include peer_label for display)\n const relationshipsAdd = children.map((child) => ({\n predicate: 'contains' as const,\n peer: child.id,\n peer_type: child.type,\n peer_label: child.label,\n }));\n\n if (isCollection) {\n // Get current collection CID for CAS\n const { data: collData, error: getError } = await client.api.GET('/collections/{id}', {\n params: { path: { id: parentId } },\n });\n if (getError || !collData) {\n throw new Error(`Failed to fetch collection: ${JSON.stringify(getError)}`);\n }\n\n // Update collection with relationships_add\n const updateBody: UpdateCollectionRequest = {\n expect_tip: collData.cid,\n relationships_add: relationshipsAdd,\n note: note ? `${note} (backlink)` : 'Upload backlink',\n };\n\n const { error } = await client.api.PUT('/collections/{id}', {\n params: { path: { id: parentId } },\n body: updateBody,\n });\n\n if (error) {\n throw new Error(JSON.stringify(error));\n }\n } else {\n // Get current folder CID for CAS\n const { data: folderData, error: getError } = await client.api.GET('/folders/{id}', {\n params: { path: { id: parentId } },\n });\n if (getError || !folderData) {\n throw new Error(`Failed to fetch folder: ${JSON.stringify(getError)}`);\n }\n\n // Update folder with relationships_add\n const updateBody: UpdateFolderRequest = {\n expect_tip: folderData.cid,\n relationships_add: relationshipsAdd,\n note: note ? `${note} (backlink)` : 'Upload backlink',\n };\n\n const { error } = await client.api.PUT('/folders/{id}', {\n params: { path: { id: parentId } },\n body: updateBody,\n });\n\n if (error) {\n throw new Error(JSON.stringify(error));\n }\n }\n\n completedParents++;\n reportProgress({\n phase: 'backlinking',\n totalParents,\n completedParents,\n currentItem: `parent:${parentId}`,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: `parent:${parentId}`, error: `Backlink failed: ${errorMsg}` });\n completedParents++;\n } else {\n throw new Error(`Failed to backlink parent ${parentId}: ${errorMsg}`);\n }\n }\n });\n\n // ─────────────────────────────────────────────────────────────────────────\n // PHASE 3: Upload file content directly to API\n // Tree is now browsable! Users can explore while content uploads.\n // ─────────────────────────────────────────────────────────────────────────\n reportProgress({ phase: 'uploading', bytesUploaded: 0 });\n\n // Use byte-based pool to maintain ~200MB in flight\n const pool = new BytePool();\n\n await Promise.all(\n createdFiles.map(async (file) => {\n await pool.run(file.size, async () => {\n try {\n // Get file data\n const fileData = await file.getData();\n\n let body: Blob;\n if (fileData instanceof Blob) {\n body = fileData;\n } else if (fileData instanceof Uint8Array) {\n const arrayBuffer = new ArrayBuffer(fileData.byteLength);\n new Uint8Array(arrayBuffer).set(fileData);\n body = new Blob([arrayBuffer], { type: file.mimeType });\n } else {\n body = new Blob([fileData], { type: file.mimeType });\n }\n\n // Upload content directly to API endpoint\n // The API streams to R2, computes CID, and updates the entity atomically\n const { error: uploadError } = await client.api.POST('/files/{id}/content', {\n params: { path: { id: file.id } },\n body: body as unknown as Record<string, never>,\n bodySerializer: (b: unknown) => b as BodyInit,\n headers: { 'Content-Type': file.mimeType },\n } as Parameters<typeof client.api.POST>[1]);\n\n if (uploadError) {\n throw new Error(`Upload failed: ${JSON.stringify(uploadError)}`);\n }\n\n bytesUploaded += file.size;\n reportProgress({\n phase: 'uploading',\n bytesUploaded,\n currentItem: file.relativePath,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: file.relativePath, error: `Upload failed: ${errorMsg}` });\n } else {\n throw new Error(`Failed to upload ${file.relativePath}: ${errorMsg}`);\n }\n }\n });\n })\n );\n\n // ─────────────────────────────────────────────────────────────────────────\n // Complete!\n // ─────────────────────────────────────────────────────────────────────────\n reportProgress({ phase: 'complete', totalParents, completedParents, bytesUploaded });\n\n const resultFolders: CreatedEntity[] = createdFolders.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'folder' as const,\n relativePath: f.relativePath,\n }));\n\n const resultFiles: CreatedEntity[] = createdFiles.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'file' as const,\n relativePath: f.relativePath,\n }));\n\n return {\n success: errors.length === 0,\n collection: {\n id: collectionId,\n cid: collectionCid,\n created: collectionCreated,\n },\n folders: resultFolders,\n files: resultFiles,\n errors,\n };\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n\n reportProgress({\n phase: 'error',\n error: errorMsg,\n });\n\n return {\n success: false,\n collection: {\n id: target.collectionId ?? '',\n cid: '',\n created: false,\n },\n folders: createdFolders.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'folder' as const,\n relativePath: f.relativePath,\n })),\n files: createdFiles.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'file' as const,\n relativePath: f.relativePath,\n })),\n errors: [...errors, { path: '', error: errorMsg }],\n };\n }\n}\n","/**\n * CID Computation Utility\n *\n * Computes IPFS CIDv1 (base32) for file content.\n * Uses raw codec (0x55) and SHA-256 hash.\n *\n * Note: This module is not used internally by the upload engine (the server\n * computes CIDs from uploaded content). It is exported for convenience in case\n * you want to verify entity CIDs, detect duplicates before upload, or perform\n * other content-addressed operations.\n */\n\nimport { CID } from 'multiformats/cid';\nimport { sha256 } from 'multiformats/hashes/sha2';\nimport * as raw from 'multiformats/codecs/raw';\n\n/**\n * Compute CIDv1 for binary content.\n * Returns base32 encoded string (bafk... prefix for raw codec).\n *\n * @param data - Binary content as ArrayBuffer, Uint8Array, or Blob\n * @returns CIDv1 string in base32 encoding\n *\n * @example\n * ```typescript\n * const cid = await computeCid(new TextEncoder().encode('hello world'));\n * // Returns: \"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e\"\n * ```\n */\nexport async function computeCid(data: ArrayBuffer | Uint8Array | Blob): Promise<string> {\n // Convert to Uint8Array\n let bytes: Uint8Array;\n\n if (data instanceof Blob) {\n const buffer = await data.arrayBuffer();\n bytes = new Uint8Array(buffer);\n } else if (data instanceof ArrayBuffer) {\n bytes = new Uint8Array(data);\n } else {\n bytes = data;\n }\n\n // Compute SHA-256 hash\n const hash = await sha256.digest(bytes);\n\n // Create CIDv1 with raw codec\n const cid = CID.create(1, raw.code, hash);\n\n // Return base32 encoded string\n return cid.toString();\n}\n\n/**\n * Verify a CID matches the content.\n *\n * @param data - Binary content\n * @param expectedCid - CID to verify against\n * @returns true if CID matches\n */\nexport async function verifyCid(\n data: ArrayBuffer | Uint8Array | Blob,\n expectedCid: string\n): Promise<boolean> {\n const computed = await computeCid(data);\n return computed === expectedCid;\n}\n","/**\n * Folder Operations (Legacy)\n *\n * @deprecated Use the new upload module instead:\n * ```typescript\n * import { uploadTree, buildUploadTree } from '@arke-institute/sdk/operations';\n *\n * const tree = buildUploadTree([\n * { path: 'docs/readme.md', data: readmeBuffer },\n * { path: 'images/logo.png', data: logoBlob },\n * ]);\n * const result = await uploadTree(client, tree, {\n * target: { collectionId: '...' },\n * });\n * ```\n */\n\nimport type { ArkeClient } from '../client/ArkeClient.js';\n\n/**\n * @deprecated Use UploadProgress from upload module\n */\nexport interface UploadProgress {\n phase: 'scanning' | 'creating-folders' | 'uploading-files' | 'linking' | 'complete';\n totalFiles: number;\n completedFiles: number;\n totalFolders: number;\n completedFolders: number;\n currentFile?: string;\n}\n\n/**\n * @deprecated Use UploadOptions from upload module\n */\nexport interface UploadDirectoryOptions {\n /** Collection to upload into */\n collectionId: string;\n /** Parent folder ID (optional - creates at root if not provided) */\n parentFolderId?: string;\n /** Progress callback */\n onProgress?: (progress: UploadProgress) => void;\n /** Max concurrent uploads */\n concurrency?: number;\n}\n\n/**\n * @deprecated Use UploadResult from upload module\n */\nexport interface UploadDirectoryResult {\n /** Root folder entity */\n rootFolder: unknown;\n /** All created folder entities */\n folders: unknown[];\n /** All created file entities */\n files: unknown[];\n}\n\n/**\n * Folder operations helper\n *\n * @deprecated Use uploadTree and buildUploadTree functions instead:\n * ```typescript\n * import { uploadTree, buildUploadTree } from '@arke-institute/sdk/operations';\n *\n * const tree = buildUploadTree([\n * { path: 'docs/readme.md', data: readmeBuffer },\n * ]);\n * const result = await uploadTree(client, tree, {\n * target: { collectionId: '...' },\n * });\n * ```\n */\nexport class FolderOperations {\n constructor(private client: ArkeClient) {\n void client; // Suppress unused warning\n }\n\n /**\n * Upload a local directory to Arke\n *\n * @deprecated This method has been removed. Use uploadTree and buildUploadTree instead.\n */\n async uploadDirectory(\n _localPath: string,\n _options: UploadDirectoryOptions\n ): Promise<UploadDirectoryResult> {\n throw new Error(\n 'FolderOperations.uploadDirectory has been removed. ' +\n 'Use uploadTree() with buildUploadTree() instead. ' +\n 'See: https://github.com/arke-institute/arke-sdk#upload-module'\n );\n }\n}\n","/**\n * Batch Operations\n *\n * High-level operations for bulk entity and relationship management.\n *\n * TODO: Implement batch operations\n * - createEntities: Create multiple entities in parallel\n * - updateEntities: Update multiple entities in parallel\n * - createRelationships: Create multiple relationships in parallel\n */\n\nimport type { ArkeClient } from '../client/ArkeClient.js';\n\nexport interface BatchCreateOptions {\n /** Max concurrent operations */\n concurrency?: number;\n /** Continue on individual failures */\n continueOnError?: boolean;\n /** Progress callback */\n onProgress?: (completed: number, total: number) => void;\n}\n\nexport interface BatchResult<T> {\n /** Successfully completed operations */\n succeeded: T[];\n /** Failed operations with errors */\n failed: Array<{ input: unknown; error: Error }>;\n}\n\n/**\n * Batch operations helper\n *\n * @example\n * ```typescript\n * const batch = new BatchOperations(arkeClient);\n * const result = await batch.createEntities([\n * { type: 'document', properties: { title: 'Doc 1' } },\n * { type: 'document', properties: { title: 'Doc 2' } },\n * ], { concurrency: 5 });\n * ```\n */\nexport class BatchOperations {\n constructor(private client: ArkeClient) {}\n\n /**\n * Create multiple entities in parallel\n *\n * TODO: Implement this method\n */\n async createEntities(\n _entities: Array<{\n collectionId: string;\n type: string;\n properties?: Record<string, unknown>;\n }>,\n _options?: BatchCreateOptions\n ): Promise<BatchResult<unknown>> {\n throw new Error('BatchOperations.createEntities is not yet implemented');\n }\n\n /**\n * Create multiple relationships in parallel\n *\n * TODO: Implement this method\n */\n async createRelationships(\n _relationships: Array<{\n sourceId: string;\n targetId: string;\n predicate: string;\n bidirectional?: boolean;\n properties?: Record<string, unknown>;\n }>,\n _options?: BatchCreateOptions\n ): Promise<BatchResult<unknown>> {\n throw new Error('BatchOperations.createRelationships is not yet implemented');\n }\n}\n","/**\n * Crypto Operations\n *\n * Cryptographic utilities for agents and content addressing.\n *\n * TODO: Implement crypto operations\n * - generateKeyPair: Generate Ed25519 key pair for agent authentication\n * - signPayload: Sign a payload with agent private key\n * - computeCID: Compute IPFS CID for content\n */\n\n/**\n * Ed25519 key pair for agent authentication\n */\nexport interface KeyPair {\n /** Public key in base64 */\n publicKey: string;\n /** Private key in base64 (keep secret!) */\n privateKey: string;\n}\n\n/**\n * Signed payload with signature\n */\nexport interface SignedPayload {\n /** Original payload */\n payload: string;\n /** Ed25519 signature in base64 */\n signature: string;\n /** Timestamp of signature */\n timestamp: number;\n}\n\n/**\n * Crypto operations helper\n *\n * @example\n * ```typescript\n * // Generate key pair for a new agent\n * const { publicKey, privateKey } = await CryptoOperations.generateKeyPair();\n *\n * // Sign a payload\n * const signed = await CryptoOperations.signPayload(privateKey, payload);\n * ```\n */\nexport class CryptoOperations {\n /**\n * Generate an Ed25519 key pair for agent authentication\n *\n * TODO: Implement using Node.js crypto or Web Crypto API\n */\n static async generateKeyPair(): Promise<KeyPair> {\n throw new Error('CryptoOperations.generateKeyPair is not yet implemented');\n }\n\n /**\n * Sign a payload with an Ed25519 private key\n *\n * TODO: Implement signature generation\n */\n static async signPayload(_privateKey: string, _payload: string): Promise<SignedPayload> {\n throw new Error('CryptoOperations.signPayload is not yet implemented');\n }\n\n /**\n * Verify an Ed25519 signature\n *\n * TODO: Implement signature verification\n */\n static async verifySignature(\n _publicKey: string,\n _payload: string,\n _signature: string\n ): Promise<boolean> {\n throw new Error('CryptoOperations.verifySignature is not yet implemented');\n }\n\n /**\n * Compute IPFS CID for content\n *\n * TODO: Implement using multiformats library\n */\n static async computeCID(_content: Uint8Array): Promise<string> {\n throw new Error('CryptoOperations.computeCID is not yet implemented');\n }\n}\n"],"mappings":"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","names":["code","createClient"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client/ArkeClient.ts","../src/client/config.ts","../src/client/retry.ts","../src/client/errors.ts","../src/operations/upload/engine.ts","../src/operations/upload/cid.ts","../src/operations/folders.ts","../src/operations/batch.ts","../src/operations/crypto.ts"],"sourcesContent":["/**\n * @arke-institute/sdk\n *\n * TypeScript SDK for the Arke API - auto-generated from OpenAPI spec.\n *\n * @example\n * ```typescript\n * import { ArkeClient } from '@arke-institute/sdk';\n *\n * const arke = new ArkeClient({ authToken: 'your-jwt-token' });\n *\n * // Create an entity\n * const { data, error } = await arke.api.POST('/entities', {\n * body: {\n * collection_id: '01ABC...',\n * type: 'document',\n * properties: { title: 'My Document' }\n * }\n * });\n *\n * if (error) {\n * console.error('Failed to create entity:', error);\n * } else {\n * console.log('Created entity:', data.id);\n * }\n * ```\n */\n\n// Main client\nexport {\n ArkeClient,\n createArkeClient,\n isApiKey,\n getAuthorizationHeader,\n type ArkeApiClient,\n} from './client/ArkeClient.js';\n\n// Configuration\nexport { type ArkeClientConfig, DEFAULT_CONFIG } from './client/config.js';\n\n// Errors\nexport {\n ArkeError,\n CASConflictError,\n NotFoundError,\n ValidationError,\n AuthenticationError,\n ForbiddenError,\n parseApiError,\n} from './client/errors.js';\n\n// Generated types\nexport type { paths, components, operations } from './generated/index.js';\n\n// High-level operations (TODO: implement)\nexport {\n FolderOperations,\n BatchOperations,\n CryptoOperations,\n type UploadProgress,\n type UploadDirectoryOptions,\n type UploadDirectoryResult,\n type BatchCreateOptions,\n type BatchResult,\n type KeyPair,\n type SignedPayload,\n} from './operations/index.js';\n","/**\n * Main Arke SDK Client\n *\n * Provides type-safe access to the Arke API using openapi-fetch.\n */\n\nimport createClient, { type Client } from 'openapi-fetch';\nimport type { paths, components } from '../generated/types.js';\nimport { ArkeClientConfig, DEFAULT_CONFIG } from './config.js';\nimport { createRetryFetch } from './retry.js';\n\nexport type ArkeApiClient = Client<paths>;\n\n/**\n * Check if a token is an API key (starts with 'ak_' or 'uk_')\n */\nexport function isApiKey(token: string): boolean {\n return token.startsWith('ak_') || token.startsWith('uk_');\n}\n\n/**\n * Get the appropriate Authorization header value for a token\n * - API keys (ak_*, uk_*) use: ApiKey {token}\n * - JWT tokens use: Bearer {token}\n */\nexport function getAuthorizationHeader(token: string): string {\n if (isApiKey(token)) {\n return `ApiKey ${token}`;\n }\n return `Bearer ${token}`;\n}\n\n/**\n * Type-safe client for the Arke API\n *\n * @example\n * ```typescript\n * // With JWT token\n * const arke = new ArkeClient({ authToken: 'your-jwt-token' });\n *\n * // With API key (agent or user)\n * const arke = new ArkeClient({ authToken: 'ak_your-agent-api-key' });\n * const arke = new ArkeClient({ authToken: 'uk_your-user-api-key' });\n *\n * // Create an entity\n * const { data, error } = await arke.api.POST('/entities', {\n * body: {\n * collection_id: '01ABC...',\n * type: 'document',\n * properties: { title: 'My Document' }\n * }\n * });\n *\n * // Get an entity\n * const { data } = await arke.api.GET('/entities/{id}', {\n * params: { path: { id: '01XYZ...' } }\n * });\n * ```\n */\nexport class ArkeClient {\n /**\n * The underlying openapi-fetch client with full type safety\n * Use this for all API calls: arke.api.GET, arke.api.POST, etc.\n */\n public api: ArkeApiClient;\n\n private config: ArkeClientConfig;\n\n constructor(config: ArkeClientConfig = {}) {\n this.config = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n\n this.api = this.createClient();\n }\n\n private createClient(): ArkeApiClient {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.config.headers,\n };\n\n if (this.config.authToken) {\n headers['Authorization'] = getAuthorizationHeader(this.config.authToken);\n }\n\n if (this.config.network === 'test') {\n headers['X-Arke-Network'] = 'test';\n }\n\n // Create retry-enabled fetch if retry config is not explicitly disabled\n const customFetch =\n this.config.retry === false\n ? undefined\n : createRetryFetch(this.config.retry ?? {});\n\n return createClient<paths>({\n baseUrl: this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl,\n headers,\n ...(customFetch && { fetch: customFetch }),\n });\n }\n\n /**\n * Update the authentication token\n * Recreates the underlying client with new headers\n */\n setAuthToken(token: string): void {\n this.config.authToken = token;\n this.api = this.createClient();\n }\n\n /**\n * Clear the authentication token\n */\n clearAuthToken(): void {\n this.config.authToken = undefined;\n this.api = this.createClient();\n }\n\n /**\n * Get the current configuration\n */\n getConfig(): Readonly<ArkeClientConfig> {\n return { ...this.config };\n }\n\n /**\n * Get the base URL\n */\n get baseUrl(): string {\n return this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl;\n }\n\n /**\n * Check if client is authenticated\n */\n get isAuthenticated(): boolean {\n return !!this.config.authToken;\n }\n\n /**\n * Get file content as a Blob\n *\n * This is a convenience method that handles the binary response parsing\n * that openapi-fetch doesn't handle automatically.\n *\n * @example\n * ```typescript\n * const { data, error } = await arke.getFileContent('01ABC...');\n * if (data) {\n * const text = await data.text();\n * // or\n * const arrayBuffer = await data.arrayBuffer();\n * }\n * ```\n */\n async getFileContent(\n fileId: string\n ): Promise<{ data: Blob | undefined; error: unknown }> {\n const { data, error } = await this.api.GET('/files/{id}/content', {\n params: { path: { id: fileId } },\n parseAs: 'blob',\n });\n return { data: data as Blob | undefined, error };\n }\n\n /**\n * Get file content as an ArrayBuffer\n *\n * This is a convenience method that handles the binary response parsing\n * that openapi-fetch doesn't handle automatically.\n *\n * @example\n * ```typescript\n * const { data, error } = await arke.getFileContentAsArrayBuffer('01ABC...');\n * if (data) {\n * const bytes = new Uint8Array(data);\n * }\n * ```\n */\n async getFileContentAsArrayBuffer(\n fileId: string\n ): Promise<{ data: ArrayBuffer | undefined; error: unknown }> {\n const { data, error } = await this.api.GET('/files/{id}/content', {\n params: { path: { id: fileId } },\n parseAs: 'arrayBuffer',\n });\n return { data: data as ArrayBuffer | undefined, error };\n }\n\n /**\n * Get file content as a ReadableStream\n *\n * This is a convenience method for streaming large files.\n *\n * @example\n * ```typescript\n * const { data, error } = await arke.getFileContentAsStream('01ABC...');\n * if (data) {\n * const reader = data.getReader();\n * // Process chunks...\n * }\n * ```\n */\n async getFileContentAsStream(\n fileId: string\n ): Promise<{ data: ReadableStream<Uint8Array> | null | undefined; error: unknown }> {\n const { data, error } = await this.api.GET('/files/{id}/content', {\n params: { path: { id: fileId } },\n parseAs: 'stream',\n });\n return { data: data as ReadableStream<Uint8Array> | null | undefined, error };\n }\n\n /**\n * Upload file content\n *\n * This is a convenience method that handles the binary body serialization\n * that openapi-fetch doesn't handle automatically for non-JSON bodies.\n *\n * @example\n * ```typescript\n * // Upload from a Blob\n * const blob = new Blob(['Hello, world!'], { type: 'text/plain' });\n * const { data, error } = await arke.uploadFileContent('01ABC...', blob, 'text/plain');\n *\n * // Upload from an ArrayBuffer\n * const buffer = new TextEncoder().encode('Hello, world!').buffer;\n * const { data, error } = await arke.uploadFileContent('01ABC...', buffer, 'text/plain');\n *\n * // Upload from a Uint8Array\n * const bytes = new TextEncoder().encode('Hello, world!');\n * const { data, error } = await arke.uploadFileContent('01ABC...', bytes, 'text/plain');\n * ```\n */\n async uploadFileContent(\n fileId: string,\n content: Blob | ArrayBuffer | Uint8Array,\n contentType: string\n ): Promise<{\n data: components['schemas']['UploadContentResponse'] | undefined;\n error: unknown;\n }> {\n // Convert to Blob if needed\n let body: Blob;\n if (content instanceof Blob) {\n body = content;\n } else if (content instanceof Uint8Array) {\n // Copy to a new ArrayBuffer to handle SharedArrayBuffer compatibility\n const buffer = new ArrayBuffer(content.byteLength);\n new Uint8Array(buffer).set(content);\n body = new Blob([buffer], { type: contentType });\n } else {\n // ArrayBuffer\n body = new Blob([content], { type: contentType });\n }\n\n const { data, error } = await this.api.POST('/files/{id}/content', {\n params: { path: { id: fileId } },\n body: body as unknown as Record<string, never>,\n bodySerializer: (b: unknown) => b as BodyInit,\n headers: { 'Content-Type': contentType },\n } as Parameters<typeof this.api.POST>[1]);\n\n return { data: data as components['schemas']['UploadContentResponse'] | undefined, error };\n }\n}\n\n/**\n * Create a new ArkeClient instance\n * Convenience function for those who prefer functional style\n */\nexport function createArkeClient(config?: ArkeClientConfig): ArkeClient {\n return new ArkeClient(config);\n}\n\n// Re-export types and errors\nexport type { ArkeClientConfig, RetryConfig } from './config.js';\nexport * from './errors.js';\n","/**\n * SDK configuration types\n */\n\nimport type { RetryConfig } from './retry.js';\n\nexport type { RetryConfig } from './retry.js';\n\nexport interface ArkeClientConfig {\n /**\n * Base URL for the Arke API\n * @default 'https://arke-v1.arke.institute'\n */\n baseUrl?: string;\n\n /**\n * Authentication token - accepts either:\n * - JWT token from Supabase auth (sent as Bearer)\n * - Agent API key with 'ak_' prefix (sent as ApiKey)\n * - User API key with 'uk_' prefix (sent as ApiKey)\n *\n * The correct Authorization header format is auto-detected from the token prefix.\n */\n authToken?: string;\n\n /**\n * Network to use ('main' or 'test')\n * Test network uses 'II' prefixed IDs and isolated data\n * @default 'main'\n */\n network?: 'main' | 'test';\n\n /**\n * Callback to refresh auth token when expired\n * Called automatically on 401 responses\n */\n onTokenRefresh?: () => Promise<string>;\n\n /**\n * Custom headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Retry configuration for transient errors\n *\n * Set to `false` to disable retries entirely.\n * If not specified, uses default retry behavior (3 retries with exponential backoff).\n *\n * CAS conflicts (409) are never retried - they are returned immediately.\n *\n * @example\n * ```typescript\n * const arke = new ArkeClient({\n * authToken: 'ak_...',\n * retry: {\n * maxRetries: 5,\n * initialDelay: 200,\n * onRetry: (attempt, error, delay) => {\n * console.log(`Retry ${attempt} after ${delay}ms`);\n * }\n * }\n * });\n * ```\n */\n retry?: RetryConfig | false;\n}\n\nexport const DEFAULT_CONFIG: Required<Pick<ArkeClientConfig, 'baseUrl' | 'network'>> = {\n baseUrl: 'https://arke-v1.arke.institute',\n network: 'main',\n};\n","/**\n * Retry logic for transient network and server errors\n */\n\n/**\n * Configuration for retry behavior\n */\nexport interface RetryConfig {\n /**\n * Maximum number of retry attempts\n * @default 3\n */\n maxRetries?: number;\n\n /**\n * Initial delay in milliseconds before first retry\n * @default 100\n */\n initialDelay?: number;\n\n /**\n * Maximum delay in milliseconds (caps exponential backoff)\n * @default 5000\n */\n maxDelay?: number;\n\n /**\n * Whether to retry on 5xx server errors\n * @default true\n */\n retryOn5xx?: boolean;\n\n /**\n * Whether to retry on network errors (connection refused, DNS, timeouts)\n * @default true\n */\n retryOnNetworkError?: boolean;\n\n /**\n * Optional callback invoked before each retry attempt\n * Useful for logging or monitoring\n */\n onRetry?: (attempt: number, error: Error, delayMs: number) => void;\n}\n\nexport const DEFAULT_RETRY_CONFIG: Required<Omit<RetryConfig, 'onRetry'>> = {\n maxRetries: 3,\n initialDelay: 100,\n maxDelay: 5000,\n retryOn5xx: true,\n retryOnNetworkError: true,\n};\n\n/**\n * HTTP status codes that should be retried\n */\nconst RETRYABLE_STATUS_CODES = new Set([\n 500, // Internal Server Error\n 502, // Bad Gateway\n 503, // Service Unavailable\n 504, // Gateway Timeout\n 520, // Cloudflare: Unknown Error\n 521, // Cloudflare: Web Server Is Down\n 522, // Cloudflare: Connection Timed Out\n 523, // Cloudflare: Origin Is Unreachable\n 524, // Cloudflare: A Timeout Occurred\n 525, // Cloudflare: SSL Handshake Failed\n 526, // Cloudflare: Invalid SSL Certificate\n 527, // Cloudflare: Railgun Error\n 530, // Cloudflare: Origin DNS Error\n]);\n\n/**\n * Status codes that should never be retried\n */\nconst NON_RETRYABLE_STATUS_CODES = new Set([\n 400, // Bad Request\n 401, // Unauthorized\n 403, // Forbidden\n 404, // Not Found\n 405, // Method Not Allowed\n 409, // Conflict (CAS errors)\n 410, // Gone\n 422, // Unprocessable Entity\n 429, // Too Many Requests (should be handled separately with rate limiting)\n]);\n\n/**\n * Check if a status code is retryable\n */\nexport function isRetryableStatus(status: number): boolean {\n if (NON_RETRYABLE_STATUS_CODES.has(status)) {\n return false;\n }\n return RETRYABLE_STATUS_CODES.has(status);\n}\n\n/**\n * Check if an error is a network error that should be retried\n */\nexport function isNetworkError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n\n // TypeError is thrown by fetch for network failures\n if (error instanceof TypeError) {\n const message = error.message.toLowerCase();\n return (\n message.includes('failed to fetch') ||\n message.includes('network') ||\n message.includes('fetch failed') ||\n message.includes('econnrefused') ||\n message.includes('econnreset') ||\n message.includes('etimedout') ||\n message.includes('enotfound') ||\n message.includes('dns') ||\n message.includes('socket')\n );\n }\n\n // Check for abort errors (timeouts)\n if (error.name === 'AbortError') {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if a response is a Cloudflare error page (HTML instead of JSON)\n */\nexport function isCloudflareErrorResponse(response: Response): boolean {\n const contentType = response.headers.get('content-type') || '';\n\n // If we expect JSON but got HTML, it's likely a Cloudflare error page\n if (contentType.includes('text/html') && !response.ok) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Calculate delay with exponential backoff and jitter\n */\nexport function calculateDelay(\n attempt: number,\n initialDelay: number,\n maxDelay: number\n): number {\n // Exponential backoff: initialDelay * 2^attempt\n const exponentialDelay = initialDelay * Math.pow(2, attempt);\n\n // Cap at maxDelay\n const cappedDelay = Math.min(exponentialDelay, maxDelay);\n\n // Add jitter (0-100% of the delay)\n const jitter = Math.random() * cappedDelay;\n\n return Math.floor(cappedDelay + jitter);\n}\n\n/**\n * Sleep for a specified number of milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Create a fetch function with retry logic\n *\n * @example\n * ```typescript\n * const retryFetch = createRetryFetch({\n * maxRetries: 3,\n * onRetry: (attempt, error, delay) => {\n * console.log(`Retry ${attempt} after ${delay}ms: ${error.message}`);\n * }\n * });\n *\n * // Use with openapi-fetch\n * const client = createClient<paths>({\n * baseUrl: 'https://api.example.com',\n * fetch: retryFetch,\n * });\n * ```\n */\nexport function createRetryFetch(config: RetryConfig = {}): typeof fetch {\n const {\n maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,\n initialDelay = DEFAULT_RETRY_CONFIG.initialDelay,\n maxDelay = DEFAULT_RETRY_CONFIG.maxDelay,\n retryOn5xx = DEFAULT_RETRY_CONFIG.retryOn5xx,\n retryOnNetworkError = DEFAULT_RETRY_CONFIG.retryOnNetworkError,\n onRetry,\n } = config;\n\n return async function retryFetch(\n input: RequestInfo | URL,\n init?: RequestInit\n ): Promise<Response> {\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(input, init);\n\n // Check for Cloudflare error pages\n if (isCloudflareErrorResponse(response) && attempt < maxRetries) {\n const error = new Error(\n `Cloudflare error (status ${response.status})`\n );\n lastError = error;\n\n const delay = calculateDelay(attempt, initialDelay, maxDelay);\n onRetry?.(attempt + 1, error, delay);\n await sleep(delay);\n continue;\n }\n\n // Check for retryable status codes\n if (retryOn5xx && isRetryableStatus(response.status) && attempt < maxRetries) {\n const error = new Error(\n `Server error (status ${response.status})`\n );\n lastError = error;\n\n const delay = calculateDelay(attempt, initialDelay, maxDelay);\n onRetry?.(attempt + 1, error, delay);\n await sleep(delay);\n continue;\n }\n\n // Success or non-retryable error\n return response;\n } catch (error) {\n // Network errors\n if (\n retryOnNetworkError &&\n isNetworkError(error) &&\n attempt < maxRetries\n ) {\n lastError = error as Error;\n\n const delay = calculateDelay(attempt, initialDelay, maxDelay);\n onRetry?.(attempt + 1, error as Error, delay);\n await sleep(delay);\n continue;\n }\n\n // Non-retryable error or exhausted retries\n throw error;\n }\n }\n\n // Exhausted all retries\n throw lastError ?? new Error('Request failed after retries');\n };\n}\n","/**\n * SDK error classes\n */\n\n/**\n * Base error class for all Arke SDK errors\n */\nexport class ArkeError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly status?: number,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'ArkeError';\n // Maintains proper stack trace for where error was thrown\n Error.captureStackTrace?.(this, this.constructor);\n }\n\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n status: this.status,\n details: this.details,\n };\n }\n}\n\n/**\n * CAS (Compare-And-Swap) conflict - entity was modified by another request\n */\nexport class CASConflictError extends ArkeError {\n constructor(\n public readonly expectedTip?: string,\n public readonly actualTip?: string\n ) {\n super(\n 'Entity was modified by another request. Refresh and retry with the current tip.',\n 'CAS_CONFLICT',\n 409,\n { expectedTip, actualTip }\n );\n this.name = 'CASConflictError';\n }\n}\n\n/**\n * Resource not found\n */\nexport class NotFoundError extends ArkeError {\n constructor(resourceType: string, id: string) {\n super(`${resourceType} not found: ${id}`, 'NOT_FOUND', 404, { resourceType, id });\n this.name = 'NotFoundError';\n }\n}\n\n/**\n * Validation error - invalid request data\n */\nexport class ValidationError extends ArkeError {\n constructor(\n message: string,\n public readonly field?: string,\n details?: unknown\n ) {\n super(message, 'VALIDATION_ERROR', 400, details ?? { field });\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Authentication required or invalid\n */\nexport class AuthenticationError extends ArkeError {\n constructor(message = 'Authentication required') {\n super(message, 'AUTH_REQUIRED', 401);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Permission denied\n */\nexport class ForbiddenError extends ArkeError {\n constructor(action?: string, resource?: string) {\n const msg = action\n ? `Permission denied: ${action}${resource ? ` on ${resource}` : ''}`\n : 'Permission denied';\n super(msg, 'FORBIDDEN', 403, { action, resource });\n this.name = 'ForbiddenError';\n }\n}\n\n/**\n * Parse API error response into appropriate error class\n */\nexport function parseApiError(status: number, body: unknown): ArkeError {\n const errorBody = body as { error?: string; message?: string; details?: unknown } | null;\n const message = errorBody?.error ?? errorBody?.message ?? 'Unknown error';\n\n switch (status) {\n case 400:\n return new ValidationError(message, undefined, errorBody?.details);\n\n case 401:\n return new AuthenticationError(message);\n\n case 403:\n return new ForbiddenError(message);\n\n case 404:\n return new NotFoundError('Resource', 'unknown');\n\n case 409: {\n // Parse CAS conflict details if available\n const details = errorBody?.details as { expected?: string; actual?: string } | undefined;\n return new CASConflictError(details?.expected, details?.actual);\n }\n\n default:\n return new ArkeError(message, 'API_ERROR', status, errorBody?.details);\n }\n}\n","/**\n * Upload Engine\n *\n * Core upload implementation optimized for fast tree visibility:\n * 1. Create folders by depth (with unidirectional 'in' → parent)\n * 2. Create file entities (metadata only, high parallelism)\n * 3. Backlink parents with 'contains' relationships\n * → Tree is now browsable! Users can explore structure immediately.\n * 4. Upload file content via POST /files/{id}/content (byte-based pool, ~200MB in flight)\n * - Direct upload to API endpoint\n * - API streams to R2, computes CID, updates entity atomically\n *\n * Byte-based pool (for uploads):\n * - Maintains ~200MB of data in flight at all times\n * - When a file completes, next file starts immediately (no gaps)\n * - Small files: Many upload in parallel\n * - Large files: Fewer upload in parallel (bandwidth-limited)\n * - Single file > 200MB: Uploads alone when pool is empty\n *\n * This minimizes time-to-browse by:\n * - Creating all entities before uploading content\n * - Using unidirectional 'in' relationship on entity creation\n * - Adding all 'contains' relationships in a single PUT per parent\n */\n\nimport type { ArkeClient } from '../../client/ArkeClient.js';\nimport type { components } from '../../generated/types.js';\nimport type {\n UploadTree,\n UploadOptions,\n UploadResult,\n UploadProgress,\n CreatedFolder,\n CreatedFile,\n CreatedEntity,\n UploadFolder,\n} from './types.js';\n\ntype CreateCollectionRequest = components['schemas']['CreateCollectionRequest'];\ntype CreateFolderRequest = components['schemas']['CreateFolderRequest'];\ntype CreateFileRequest = components['schemas']['CreateFileRequest'];\ntype UpdateFolderRequest = components['schemas']['UpdateFolderRequest'];\ntype UpdateCollectionRequest = components['schemas']['UpdateCollectionRequest'];\n\n// Phase constants\nconst PHASE_COUNT = 3; // creating, backlinking, uploading (excluding complete/error)\nconst PHASE_INDEX: Record<string, number> = {\n creating: 0,\n backlinking: 1,\n uploading: 2,\n complete: 3,\n error: -1,\n};\n\n// =============================================================================\n// Concurrency Utilities\n// =============================================================================\n\n/**\n * Simple concurrency limiter for parallel operations.\n */\nasync function parallelLimit<T, R>(\n items: T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>\n): Promise<R[]> {\n const results: R[] = [];\n let index = 0;\n\n async function worker(): Promise<void> {\n while (index < items.length) {\n const currentIndex = index++;\n const item = items[currentIndex]!;\n results[currentIndex] = await fn(item, currentIndex);\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());\n await Promise.all(workers);\n\n return results;\n}\n\n// =============================================================================\n// Byte-Based Pool\n// =============================================================================\n\n/** Target bytes in flight (~200MB) */\nconst TARGET_BYTES_IN_FLIGHT = 200 * 1024 * 1024;\n\n/**\n * Pool that maintains a target number of bytes in flight.\n *\n * When a file completes, its bytes are released and the next\n * waiting file can start immediately. This keeps the pipeline full.\n *\n * - Small files: Many run in parallel (up to ~200MB worth)\n * - Large files: Fewer run in parallel\n * - Single file > 200MB: Runs alone (allowed when pool is empty)\n */\nclass BytePool {\n private bytesInFlight = 0;\n private waitQueue: Array<() => void> = [];\n\n constructor(private targetBytes: number = TARGET_BYTES_IN_FLIGHT) {}\n\n async run<T>(size: number, fn: () => Promise<T>): Promise<T> {\n // Wait until we have room\n // Exception: if pool is empty, always allow (handles files > targetBytes)\n while (this.bytesInFlight > 0 && this.bytesInFlight + size > this.targetBytes) {\n await new Promise<void>((resolve) => this.waitQueue.push(resolve));\n }\n\n this.bytesInFlight += size;\n try {\n return await fn();\n } finally {\n this.bytesInFlight -= size;\n // Wake up next waiting task\n const next = this.waitQueue.shift();\n if (next) next();\n }\n }\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Parse folder path to get parent path.\n * e.g., \"docs/images/photos\" -> \"docs/images\"\n */\nfunction getParentPath(relativePath: string): string | null {\n const lastSlash = relativePath.lastIndexOf('/');\n if (lastSlash === -1) return null;\n return relativePath.slice(0, lastSlash);\n}\n\n/**\n * Group folders by depth level.\n */\nfunction groupFoldersByDepth(folders: UploadFolder[]): Map<number, UploadFolder[]> {\n const byDepth = new Map<number, UploadFolder[]>();\n\n for (const folder of folders) {\n const depth = folder.relativePath.split('/').length - 1;\n if (!byDepth.has(depth)) byDepth.set(depth, []);\n byDepth.get(depth)!.push(folder);\n }\n\n return byDepth;\n}\n\n// =============================================================================\n// Main Upload Function\n// =============================================================================\n\n/**\n * Main upload function.\n * Orchestrates the entire upload process with optimized relationship strategy.\n */\nexport async function uploadTree(\n client: ArkeClient,\n tree: UploadTree,\n options: UploadOptions\n): Promise<UploadResult> {\n const { target, onProgress, concurrency = 10, continueOnError = false, note } = options;\n\n const errors: Array<{ path: string; error: string }> = [];\n const createdFolders: CreatedFolder[] = [];\n const createdFiles: CreatedFile[] = [];\n\n // Maps for tracking (include label for peer_label in relationships)\n const foldersByPath = new Map<string, { id: string; cid: string; label: string }>();\n\n // Calculate totals\n const totalEntities = tree.files.length + tree.folders.length;\n const totalBytes = tree.files.reduce((sum, f) => sum + f.size, 0);\n let completedEntities = 0;\n let bytesUploaded = 0;\n\n // Helper to report progress\n const reportProgress = (progress: Partial<UploadProgress>) => {\n if (onProgress) {\n const phase = progress.phase || 'creating';\n const phaseIndex = PHASE_INDEX[phase] ?? -1;\n\n // Calculate phase percent based on current phase\n let phasePercent = 0;\n if (phase === 'creating') {\n // Creating phase: progress is based on entities created\n const done = progress.completedEntities ?? completedEntities;\n phasePercent = totalEntities > 0 ? Math.round((done / totalEntities) * 100) : 100;\n } else if (phase === 'backlinking') {\n // Backlinking phase: progress is based on parents updated\n const done = progress.completedParents ?? 0;\n const total = progress.totalParents ?? 0;\n phasePercent = total > 0 ? Math.round((done / total) * 100) : 100;\n } else if (phase === 'uploading') {\n // Uploading phase: progress is based on bytes uploaded\n const done = progress.bytesUploaded ?? bytesUploaded;\n phasePercent = totalBytes > 0 ? Math.round((done / totalBytes) * 100) : 100;\n } else if (phase === 'complete') {\n phasePercent = 100;\n }\n\n onProgress({\n phase,\n phaseIndex,\n phaseCount: PHASE_COUNT,\n phasePercent,\n totalEntities,\n completedEntities,\n totalParents: 0,\n completedParents: 0,\n totalBytes,\n bytesUploaded,\n ...progress,\n } as UploadProgress);\n }\n };\n\n try {\n // ─────────────────────────────────────────────────────────────────────────\n // SETUP: Resolve or create collection\n // ─────────────────────────────────────────────────────────────────────────\n let collectionId: string;\n let collectionCid: string;\n let collectionLabel: string;\n let collectionCreated = false;\n\n if (target.createCollection) {\n const collectionBody: CreateCollectionRequest = {\n label: target.createCollection.label,\n description: target.createCollection.description,\n roles: target.createCollection.roles,\n note,\n };\n\n const { data, error } = await client.api.POST('/collections', {\n body: collectionBody,\n });\n\n if (error || !data) {\n throw new Error(`Failed to create collection: ${JSON.stringify(error)}`);\n }\n\n collectionId = data.id;\n collectionCid = data.cid;\n collectionLabel = target.createCollection.label;\n collectionCreated = true;\n } else if (target.collectionId) {\n collectionId = target.collectionId;\n\n const { data, error } = await client.api.GET('/collections/{id}', {\n params: { path: { id: collectionId } },\n });\n\n if (error || !data) {\n throw new Error(`Failed to fetch collection: ${JSON.stringify(error)}`);\n }\n\n collectionCid = data.cid;\n collectionLabel = (data.properties?.label as string) ?? collectionId;\n } else {\n throw new Error('Must provide either collectionId or createCollection in target');\n }\n\n // Determine the parent for root-level items\n const rootParentId = target.parentId ?? collectionId;\n let rootParentLabel = collectionLabel;\n let rootParentType: 'collection' | 'folder' = 'collection';\n\n // If a specific parent folder is provided, fetch its label\n if (target.parentId && target.parentId !== collectionId) {\n const { data: parentData, error: parentError } = await client.api.GET('/folders/{id}', {\n params: { path: { id: target.parentId } },\n });\n if (parentError || !parentData) {\n throw new Error(`Failed to fetch parent folder: ${JSON.stringify(parentError)}`);\n }\n rootParentLabel = (parentData.properties?.label as string) ?? target.parentId;\n rootParentType = 'folder';\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // PHASE 1: Create entities (folders by depth, then files)\n // ─────────────────────────────────────────────────────────────────────────\n reportProgress({ phase: 'creating', completedEntities: 0 });\n\n // Group folders by depth\n const foldersByDepth = groupFoldersByDepth(tree.folders);\n const sortedDepths = [...foldersByDepth.keys()].sort((a, b) => a - b);\n\n // Create folders depth by depth (parents before children)\n for (const depth of sortedDepths) {\n const foldersAtDepth = foldersByDepth.get(depth)!;\n\n await Promise.all(\n foldersAtDepth.map(async (folder) => {\n try {\n const parentPath = getParentPath(folder.relativePath);\n const parentInfo = parentPath ? foldersByPath.get(parentPath)! : null;\n const parentId = parentInfo ? parentInfo.id : rootParentId;\n const parentType = parentInfo ? 'folder' : rootParentType;\n const parentLabel = parentInfo ? parentInfo.label : rootParentLabel;\n\n const folderBody: CreateFolderRequest = {\n label: folder.name,\n collection: collectionId,\n note,\n relationships: [{ predicate: 'in', peer: parentId, peer_type: parentType, peer_label: parentLabel }],\n };\n\n const { data, error } = await client.api.POST('/folders', {\n body: folderBody,\n });\n\n if (error || !data) {\n throw new Error(JSON.stringify(error));\n }\n\n // Track folder (include label for peer_label in child relationships)\n foldersByPath.set(folder.relativePath, { id: data.id, cid: data.cid, label: folder.name });\n createdFolders.push({\n name: folder.name,\n relativePath: folder.relativePath,\n id: data.id,\n entityCid: data.cid,\n });\n\n completedEntities++;\n reportProgress({\n phase: 'creating',\n completedEntities,\n currentItem: folder.relativePath,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: folder.relativePath, error: `Folder creation failed: ${errorMsg}` });\n completedEntities++;\n } else {\n throw new Error(`Failed to create folder ${folder.relativePath}: ${errorMsg}`);\n }\n }\n })\n );\n }\n\n // Create file entities (metadata only, no content upload yet)\n // Use simple concurrency limit for API calls\n const FILE_CREATION_CONCURRENCY = 50;\n\n await parallelLimit(tree.files, FILE_CREATION_CONCURRENCY, async (file) => {\n try {\n const parentPath = getParentPath(file.relativePath);\n const parentInfo = parentPath ? foldersByPath.get(parentPath)! : null;\n const parentId = parentInfo ? parentInfo.id : rootParentId;\n const parentType = parentInfo ? 'folder' : rootParentType;\n const parentLabel = parentInfo ? parentInfo.label : rootParentLabel;\n\n // Create file entity with 'in' relationship (include peer_label for display)\n // Server computes CID when content is uploaded\n const fileBody: CreateFileRequest = {\n key: crypto.randomUUID(), // Generate unique storage key\n filename: file.name,\n label: file.name, // Display label for the file\n content_type: file.mimeType,\n size: file.size,\n collection: collectionId,\n relationships: [{ predicate: 'in', peer: parentId, peer_type: parentType, peer_label: parentLabel }],\n };\n\n const { data, error } = await client.api.POST('/files', {\n body: fileBody,\n });\n\n if (error || !data) {\n throw new Error(`Entity creation failed: ${JSON.stringify(error)}`);\n }\n\n // Track file for later upload\n createdFiles.push({\n ...file,\n id: data.id,\n entityCid: data.cid,\n });\n\n completedEntities++;\n reportProgress({\n phase: 'creating',\n completedEntities,\n currentItem: file.relativePath,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: file.relativePath, error: errorMsg });\n completedEntities++;\n } else {\n throw new Error(`Failed to create file ${file.relativePath}: ${errorMsg}`);\n }\n }\n });\n\n // ─────────────────────────────────────────────────────────────────────────\n // PHASE 2: Backlink - Update each parent with 'contains' relationships\n // ─────────────────────────────────────────────────────────────────────────\n\n // Build parent -> children map (include label for peer_label)\n const childrenByParent = new Map<string, Array<{ id: string; type: 'file' | 'folder'; label: string }>>();\n\n // Add folders as children of their parents\n for (const folder of createdFolders) {\n const parentPath = getParentPath(folder.relativePath);\n const parentId = parentPath ? foldersByPath.get(parentPath)!.id : rootParentId;\n\n if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);\n childrenByParent.get(parentId)!.push({ id: folder.id, type: 'folder', label: folder.name });\n }\n\n // Add files as children of their parents\n for (const file of createdFiles) {\n const parentPath = getParentPath(file.relativePath);\n const parentId = parentPath ? foldersByPath.get(parentPath)!.id : rootParentId;\n\n if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);\n childrenByParent.get(parentId)!.push({ id: file.id, type: 'file', label: file.name });\n }\n\n const totalParents = childrenByParent.size;\n let completedParents = 0;\n\n reportProgress({ phase: 'backlinking', totalParents, completedParents: 0 });\n\n // Update all parents in parallel - each parent gets one PUT with all its children\n const parentEntries = [...childrenByParent.entries()];\n\n await parallelLimit(parentEntries, concurrency, async ([parentId, children]) => {\n try {\n const isCollection = parentId === collectionId;\n\n // Build relationships_add array with all children (include peer_label for display)\n const relationshipsAdd = children.map((child) => ({\n predicate: 'contains' as const,\n peer: child.id,\n peer_type: child.type,\n peer_label: child.label,\n }));\n\n if (isCollection) {\n // Get current collection CID for CAS\n const { data: collData, error: getError } = await client.api.GET('/collections/{id}', {\n params: { path: { id: parentId } },\n });\n if (getError || !collData) {\n throw new Error(`Failed to fetch collection: ${JSON.stringify(getError)}`);\n }\n\n // Update collection with relationships_add\n const updateBody: UpdateCollectionRequest = {\n expect_tip: collData.cid,\n relationships_add: relationshipsAdd,\n note: note ? `${note} (backlink)` : 'Upload backlink',\n };\n\n const { error } = await client.api.PUT('/collections/{id}', {\n params: { path: { id: parentId } },\n body: updateBody,\n });\n\n if (error) {\n throw new Error(JSON.stringify(error));\n }\n } else {\n // Get current folder CID for CAS\n const { data: folderData, error: getError } = await client.api.GET('/folders/{id}', {\n params: { path: { id: parentId } },\n });\n if (getError || !folderData) {\n throw new Error(`Failed to fetch folder: ${JSON.stringify(getError)}`);\n }\n\n // Update folder with relationships_add\n const updateBody: UpdateFolderRequest = {\n expect_tip: folderData.cid,\n relationships_add: relationshipsAdd,\n note: note ? `${note} (backlink)` : 'Upload backlink',\n };\n\n const { error } = await client.api.PUT('/folders/{id}', {\n params: { path: { id: parentId } },\n body: updateBody,\n });\n\n if (error) {\n throw new Error(JSON.stringify(error));\n }\n }\n\n completedParents++;\n reportProgress({\n phase: 'backlinking',\n totalParents,\n completedParents,\n currentItem: `parent:${parentId}`,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: `parent:${parentId}`, error: `Backlink failed: ${errorMsg}` });\n completedParents++;\n } else {\n throw new Error(`Failed to backlink parent ${parentId}: ${errorMsg}`);\n }\n }\n });\n\n // ─────────────────────────────────────────────────────────────────────────\n // PHASE 3: Upload file content directly to API\n // Tree is now browsable! Users can explore while content uploads.\n // ─────────────────────────────────────────────────────────────────────────\n reportProgress({ phase: 'uploading', bytesUploaded: 0 });\n\n // Use byte-based pool to maintain ~200MB in flight\n const pool = new BytePool();\n\n await Promise.all(\n createdFiles.map(async (file) => {\n await pool.run(file.size, async () => {\n try {\n // Get file data\n const fileData = await file.getData();\n\n let body: Blob;\n if (fileData instanceof Blob) {\n body = fileData;\n } else if (fileData instanceof Uint8Array) {\n const arrayBuffer = new ArrayBuffer(fileData.byteLength);\n new Uint8Array(arrayBuffer).set(fileData);\n body = new Blob([arrayBuffer], { type: file.mimeType });\n } else {\n body = new Blob([fileData], { type: file.mimeType });\n }\n\n // Upload content directly to API endpoint\n // The API streams to R2, computes CID, and updates the entity atomically\n const { error: uploadError } = await client.api.POST('/files/{id}/content', {\n params: { path: { id: file.id } },\n body: body as unknown as Record<string, never>,\n bodySerializer: (b: unknown) => b as BodyInit,\n headers: { 'Content-Type': file.mimeType },\n } as Parameters<typeof client.api.POST>[1]);\n\n if (uploadError) {\n throw new Error(`Upload failed: ${JSON.stringify(uploadError)}`);\n }\n\n bytesUploaded += file.size;\n reportProgress({\n phase: 'uploading',\n bytesUploaded,\n currentItem: file.relativePath,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n if (continueOnError) {\n errors.push({ path: file.relativePath, error: `Upload failed: ${errorMsg}` });\n } else {\n throw new Error(`Failed to upload ${file.relativePath}: ${errorMsg}`);\n }\n }\n });\n })\n );\n\n // ─────────────────────────────────────────────────────────────────────────\n // Complete!\n // ─────────────────────────────────────────────────────────────────────────\n reportProgress({ phase: 'complete', totalParents, completedParents, bytesUploaded });\n\n const resultFolders: CreatedEntity[] = createdFolders.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'folder' as const,\n relativePath: f.relativePath,\n }));\n\n const resultFiles: CreatedEntity[] = createdFiles.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'file' as const,\n relativePath: f.relativePath,\n }));\n\n return {\n success: errors.length === 0,\n collection: {\n id: collectionId,\n cid: collectionCid,\n created: collectionCreated,\n },\n folders: resultFolders,\n files: resultFiles,\n errors,\n };\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n\n reportProgress({\n phase: 'error',\n error: errorMsg,\n });\n\n return {\n success: false,\n collection: {\n id: target.collectionId ?? '',\n cid: '',\n created: false,\n },\n folders: createdFolders.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'folder' as const,\n relativePath: f.relativePath,\n })),\n files: createdFiles.map((f) => ({\n id: f.id,\n cid: f.entityCid,\n type: 'file' as const,\n relativePath: f.relativePath,\n })),\n errors: [...errors, { path: '', error: errorMsg }],\n };\n }\n}\n","/**\n * CID Computation Utility\n *\n * Computes IPFS CIDv1 (base32) for file content.\n * Uses raw codec (0x55) and SHA-256 hash.\n *\n * Note: This module is not used internally by the upload engine (the server\n * computes CIDs from uploaded content). It is exported for convenience in case\n * you want to verify entity CIDs, detect duplicates before upload, or perform\n * other content-addressed operations.\n */\n\nimport { CID } from 'multiformats/cid';\nimport { sha256 } from 'multiformats/hashes/sha2';\nimport * as raw from 'multiformats/codecs/raw';\n\n/**\n * Compute CIDv1 for binary content.\n * Returns base32 encoded string (bafk... prefix for raw codec).\n *\n * @param data - Binary content as ArrayBuffer, Uint8Array, or Blob\n * @returns CIDv1 string in base32 encoding\n *\n * @example\n * ```typescript\n * const cid = await computeCid(new TextEncoder().encode('hello world'));\n * // Returns: \"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e\"\n * ```\n */\nexport async function computeCid(data: ArrayBuffer | Uint8Array | Blob): Promise<string> {\n // Convert to Uint8Array\n let bytes: Uint8Array;\n\n if (data instanceof Blob) {\n const buffer = await data.arrayBuffer();\n bytes = new Uint8Array(buffer);\n } else if (data instanceof ArrayBuffer) {\n bytes = new Uint8Array(data);\n } else {\n bytes = data;\n }\n\n // Compute SHA-256 hash\n const hash = await sha256.digest(bytes);\n\n // Create CIDv1 with raw codec\n const cid = CID.create(1, raw.code, hash);\n\n // Return base32 encoded string\n return cid.toString();\n}\n\n/**\n * Verify a CID matches the content.\n *\n * @param data - Binary content\n * @param expectedCid - CID to verify against\n * @returns true if CID matches\n */\nexport async function verifyCid(\n data: ArrayBuffer | Uint8Array | Blob,\n expectedCid: string\n): Promise<boolean> {\n const computed = await computeCid(data);\n return computed === expectedCid;\n}\n","/**\n * Folder Operations (Legacy)\n *\n * @deprecated Use the new upload module instead:\n * ```typescript\n * import { uploadTree, buildUploadTree } from '@arke-institute/sdk/operations';\n *\n * const tree = buildUploadTree([\n * { path: 'docs/readme.md', data: readmeBuffer },\n * { path: 'images/logo.png', data: logoBlob },\n * ]);\n * const result = await uploadTree(client, tree, {\n * target: { collectionId: '...' },\n * });\n * ```\n */\n\nimport type { ArkeClient } from '../client/ArkeClient.js';\n\n/**\n * @deprecated Use UploadProgress from upload module\n */\nexport interface UploadProgress {\n phase: 'scanning' | 'creating-folders' | 'uploading-files' | 'linking' | 'complete';\n totalFiles: number;\n completedFiles: number;\n totalFolders: number;\n completedFolders: number;\n currentFile?: string;\n}\n\n/**\n * @deprecated Use UploadOptions from upload module\n */\nexport interface UploadDirectoryOptions {\n /** Collection to upload into */\n collectionId: string;\n /** Parent folder ID (optional - creates at root if not provided) */\n parentFolderId?: string;\n /** Progress callback */\n onProgress?: (progress: UploadProgress) => void;\n /** Max concurrent uploads */\n concurrency?: number;\n}\n\n/**\n * @deprecated Use UploadResult from upload module\n */\nexport interface UploadDirectoryResult {\n /** Root folder entity */\n rootFolder: unknown;\n /** All created folder entities */\n folders: unknown[];\n /** All created file entities */\n files: unknown[];\n}\n\n/**\n * Folder operations helper\n *\n * @deprecated Use uploadTree and buildUploadTree functions instead:\n * ```typescript\n * import { uploadTree, buildUploadTree } from '@arke-institute/sdk/operations';\n *\n * const tree = buildUploadTree([\n * { path: 'docs/readme.md', data: readmeBuffer },\n * ]);\n * const result = await uploadTree(client, tree, {\n * target: { collectionId: '...' },\n * });\n * ```\n */\nexport class FolderOperations {\n constructor(private client: ArkeClient) {\n void client; // Suppress unused warning\n }\n\n /**\n * Upload a local directory to Arke\n *\n * @deprecated This method has been removed. Use uploadTree and buildUploadTree instead.\n */\n async uploadDirectory(\n _localPath: string,\n _options: UploadDirectoryOptions\n ): Promise<UploadDirectoryResult> {\n throw new Error(\n 'FolderOperations.uploadDirectory has been removed. ' +\n 'Use uploadTree() with buildUploadTree() instead. ' +\n 'See: https://github.com/arke-institute/arke-sdk#upload-module'\n );\n }\n}\n","/**\n * Batch Operations\n *\n * High-level operations for bulk entity and relationship management.\n *\n * TODO: Implement batch operations\n * - createEntities: Create multiple entities in parallel\n * - updateEntities: Update multiple entities in parallel\n * - createRelationships: Create multiple relationships in parallel\n */\n\nimport type { ArkeClient } from '../client/ArkeClient.js';\n\nexport interface BatchCreateOptions {\n /** Max concurrent operations */\n concurrency?: number;\n /** Continue on individual failures */\n continueOnError?: boolean;\n /** Progress callback */\n onProgress?: (completed: number, total: number) => void;\n}\n\nexport interface BatchResult<T> {\n /** Successfully completed operations */\n succeeded: T[];\n /** Failed operations with errors */\n failed: Array<{ input: unknown; error: Error }>;\n}\n\n/**\n * Batch operations helper\n *\n * @example\n * ```typescript\n * const batch = new BatchOperations(arkeClient);\n * const result = await batch.createEntities([\n * { type: 'document', properties: { title: 'Doc 1' } },\n * { type: 'document', properties: { title: 'Doc 2' } },\n * ], { concurrency: 5 });\n * ```\n */\nexport class BatchOperations {\n constructor(private client: ArkeClient) {}\n\n /**\n * Create multiple entities in parallel\n *\n * TODO: Implement this method\n */\n async createEntities(\n _entities: Array<{\n collectionId: string;\n type: string;\n properties?: Record<string, unknown>;\n }>,\n _options?: BatchCreateOptions\n ): Promise<BatchResult<unknown>> {\n throw new Error('BatchOperations.createEntities is not yet implemented');\n }\n\n /**\n * Create multiple relationships in parallel\n *\n * TODO: Implement this method\n */\n async createRelationships(\n _relationships: Array<{\n sourceId: string;\n targetId: string;\n predicate: string;\n bidirectional?: boolean;\n properties?: Record<string, unknown>;\n }>,\n _options?: BatchCreateOptions\n ): Promise<BatchResult<unknown>> {\n throw new Error('BatchOperations.createRelationships is not yet implemented');\n }\n}\n","/**\n * Crypto Operations\n *\n * Cryptographic utilities for agents and content addressing.\n *\n * TODO: Implement crypto operations\n * - generateKeyPair: Generate Ed25519 key pair for agent authentication\n * - signPayload: Sign a payload with agent private key\n * - computeCID: Compute IPFS CID for content\n */\n\n/**\n * Ed25519 key pair for agent authentication\n */\nexport interface KeyPair {\n /** Public key in base64 */\n publicKey: string;\n /** Private key in base64 (keep secret!) */\n privateKey: string;\n}\n\n/**\n * Signed payload with signature\n */\nexport interface SignedPayload {\n /** Original payload */\n payload: string;\n /** Ed25519 signature in base64 */\n signature: string;\n /** Timestamp of signature */\n timestamp: number;\n}\n\n/**\n * Crypto operations helper\n *\n * @example\n * ```typescript\n * // Generate key pair for a new agent\n * const { publicKey, privateKey } = await CryptoOperations.generateKeyPair();\n *\n * // Sign a payload\n * const signed = await CryptoOperations.signPayload(privateKey, payload);\n * ```\n */\nexport class CryptoOperations {\n /**\n * Generate an Ed25519 key pair for agent authentication\n *\n * TODO: Implement using Node.js crypto or Web Crypto API\n */\n static async generateKeyPair(): Promise<KeyPair> {\n throw new Error('CryptoOperations.generateKeyPair is not yet implemented');\n }\n\n /**\n * Sign a payload with an Ed25519 private key\n *\n * TODO: Implement signature generation\n */\n static async signPayload(_privateKey: string, _payload: string): Promise<SignedPayload> {\n throw new Error('CryptoOperations.signPayload is not yet implemented');\n }\n\n /**\n * Verify an Ed25519 signature\n *\n * TODO: Implement signature verification\n */\n static async verifySignature(\n _publicKey: string,\n _payload: string,\n _signature: string\n ): Promise<boolean> {\n throw new Error('CryptoOperations.verifySignature is not yet implemented');\n }\n\n /**\n * Compute IPFS CID for content\n *\n * TODO: Implement using multiformats library\n */\n static async computeCID(_content: Uint8Array): Promise<string> {\n throw new Error('CryptoOperations.computeCID is not yet implemented');\n }\n}\n"],"mappings":"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","names":["code","createClient"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { a as ArkeApiClient, A as ArkeClient, b as ArkeClientConfig, f as BatchCreateOptions, B as BatchOperations, h as BatchResult, C as CryptoOperations, D as DEFAULT_CONFIG, F as FolderOperations, K as KeyPair, S as SignedPayload, d as UploadDirectoryOptions, e as UploadDirectoryResult, U as UploadProgress, c as createArkeClient, g as getAuthorizationHeader, i as isApiKey } from './crypto-
|
|
1
|
+
export { a as ArkeApiClient, A as ArkeClient, b as ArkeClientConfig, f as BatchCreateOptions, B as BatchOperations, h as BatchResult, C as CryptoOperations, D as DEFAULT_CONFIG, F as FolderOperations, K as KeyPair, S as SignedPayload, d as UploadDirectoryOptions, e as UploadDirectoryResult, U as UploadProgress, c as createArkeClient, g as getAuthorizationHeader, i as isApiKey } from './crypto-wBQ1LBgz.cjs';
|
|
2
2
|
export { components, operations, paths } from './generated/index.cjs';
|
|
3
3
|
import 'openapi-fetch';
|
|
4
4
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { a as ArkeApiClient, A as ArkeClient, b as ArkeClientConfig, f as BatchCreateOptions, B as BatchOperations, h as BatchResult, C as CryptoOperations, D as DEFAULT_CONFIG, F as FolderOperations, K as KeyPair, S as SignedPayload, d as UploadDirectoryOptions, e as UploadDirectoryResult, U as UploadProgress, c as createArkeClient, g as getAuthorizationHeader, i as isApiKey } from './crypto-
|
|
1
|
+
export { a as ArkeApiClient, A as ArkeClient, b as ArkeClientConfig, f as BatchCreateOptions, B as BatchOperations, h as BatchResult, C as CryptoOperations, D as DEFAULT_CONFIG, F as FolderOperations, K as KeyPair, S as SignedPayload, d as UploadDirectoryOptions, e as UploadDirectoryResult, U as UploadProgress, c as createArkeClient, g as getAuthorizationHeader, i as isApiKey } from './crypto-96bEBoO_.js';
|
|
2
2
|
export { components, operations, paths } from './generated/index.js';
|
|
3
3
|
import 'openapi-fetch';
|
|
4
4
|
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,147 @@ var DEFAULT_CONFIG = {
|
|
|
7
7
|
network: "main"
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
// src/client/retry.ts
|
|
11
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
initialDelay: 100,
|
|
14
|
+
maxDelay: 5e3,
|
|
15
|
+
retryOn5xx: true,
|
|
16
|
+
retryOnNetworkError: true
|
|
17
|
+
};
|
|
18
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
19
|
+
500,
|
|
20
|
+
// Internal Server Error
|
|
21
|
+
502,
|
|
22
|
+
// Bad Gateway
|
|
23
|
+
503,
|
|
24
|
+
// Service Unavailable
|
|
25
|
+
504,
|
|
26
|
+
// Gateway Timeout
|
|
27
|
+
520,
|
|
28
|
+
// Cloudflare: Unknown Error
|
|
29
|
+
521,
|
|
30
|
+
// Cloudflare: Web Server Is Down
|
|
31
|
+
522,
|
|
32
|
+
// Cloudflare: Connection Timed Out
|
|
33
|
+
523,
|
|
34
|
+
// Cloudflare: Origin Is Unreachable
|
|
35
|
+
524,
|
|
36
|
+
// Cloudflare: A Timeout Occurred
|
|
37
|
+
525,
|
|
38
|
+
// Cloudflare: SSL Handshake Failed
|
|
39
|
+
526,
|
|
40
|
+
// Cloudflare: Invalid SSL Certificate
|
|
41
|
+
527,
|
|
42
|
+
// Cloudflare: Railgun Error
|
|
43
|
+
530
|
|
44
|
+
// Cloudflare: Origin DNS Error
|
|
45
|
+
]);
|
|
46
|
+
var NON_RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
47
|
+
400,
|
|
48
|
+
// Bad Request
|
|
49
|
+
401,
|
|
50
|
+
// Unauthorized
|
|
51
|
+
403,
|
|
52
|
+
// Forbidden
|
|
53
|
+
404,
|
|
54
|
+
// Not Found
|
|
55
|
+
405,
|
|
56
|
+
// Method Not Allowed
|
|
57
|
+
409,
|
|
58
|
+
// Conflict (CAS errors)
|
|
59
|
+
410,
|
|
60
|
+
// Gone
|
|
61
|
+
422,
|
|
62
|
+
// Unprocessable Entity
|
|
63
|
+
429
|
|
64
|
+
// Too Many Requests (should be handled separately with rate limiting)
|
|
65
|
+
]);
|
|
66
|
+
function isRetryableStatus(status) {
|
|
67
|
+
if (NON_RETRYABLE_STATUS_CODES.has(status)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return RETRYABLE_STATUS_CODES.has(status);
|
|
71
|
+
}
|
|
72
|
+
function isNetworkError(error) {
|
|
73
|
+
if (!(error instanceof Error)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (error instanceof TypeError) {
|
|
77
|
+
const message = error.message.toLowerCase();
|
|
78
|
+
return message.includes("failed to fetch") || message.includes("network") || message.includes("fetch failed") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("enotfound") || message.includes("dns") || message.includes("socket");
|
|
79
|
+
}
|
|
80
|
+
if (error.name === "AbortError") {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function isCloudflareErrorResponse(response) {
|
|
86
|
+
const contentType = response.headers.get("content-type") || "";
|
|
87
|
+
if (contentType.includes("text/html") && !response.ok) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function calculateDelay(attempt, initialDelay, maxDelay) {
|
|
93
|
+
const exponentialDelay = initialDelay * Math.pow(2, attempt);
|
|
94
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
95
|
+
const jitter = Math.random() * cappedDelay;
|
|
96
|
+
return Math.floor(cappedDelay + jitter);
|
|
97
|
+
}
|
|
98
|
+
function sleep(ms) {
|
|
99
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
100
|
+
}
|
|
101
|
+
function createRetryFetch(config = {}) {
|
|
102
|
+
const {
|
|
103
|
+
maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,
|
|
104
|
+
initialDelay = DEFAULT_RETRY_CONFIG.initialDelay,
|
|
105
|
+
maxDelay = DEFAULT_RETRY_CONFIG.maxDelay,
|
|
106
|
+
retryOn5xx = DEFAULT_RETRY_CONFIG.retryOn5xx,
|
|
107
|
+
retryOnNetworkError = DEFAULT_RETRY_CONFIG.retryOnNetworkError,
|
|
108
|
+
onRetry
|
|
109
|
+
} = config;
|
|
110
|
+
return async function retryFetch(input, init) {
|
|
111
|
+
let lastError;
|
|
112
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(input, init);
|
|
115
|
+
if (isCloudflareErrorResponse(response) && attempt < maxRetries) {
|
|
116
|
+
const error = new Error(
|
|
117
|
+
`Cloudflare error (status ${response.status})`
|
|
118
|
+
);
|
|
119
|
+
lastError = error;
|
|
120
|
+
const delay = calculateDelay(attempt, initialDelay, maxDelay);
|
|
121
|
+
onRetry?.(attempt + 1, error, delay);
|
|
122
|
+
await sleep(delay);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (retryOn5xx && isRetryableStatus(response.status) && attempt < maxRetries) {
|
|
126
|
+
const error = new Error(
|
|
127
|
+
`Server error (status ${response.status})`
|
|
128
|
+
);
|
|
129
|
+
lastError = error;
|
|
130
|
+
const delay = calculateDelay(attempt, initialDelay, maxDelay);
|
|
131
|
+
onRetry?.(attempt + 1, error, delay);
|
|
132
|
+
await sleep(delay);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
return response;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (retryOnNetworkError && isNetworkError(error) && attempt < maxRetries) {
|
|
138
|
+
lastError = error;
|
|
139
|
+
const delay = calculateDelay(attempt, initialDelay, maxDelay);
|
|
140
|
+
onRetry?.(attempt + 1, error, delay);
|
|
141
|
+
await sleep(delay);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
10
151
|
// src/client/errors.ts
|
|
11
152
|
var ArkeError = class extends Error {
|
|
12
153
|
constructor(message, code2, status, details) {
|
|
@@ -116,9 +257,11 @@ var ArkeClient = class {
|
|
|
116
257
|
if (this.config.network === "test") {
|
|
117
258
|
headers["X-Arke-Network"] = "test";
|
|
118
259
|
}
|
|
260
|
+
const customFetch = this.config.retry === false ? void 0 : createRetryFetch(this.config.retry ?? {});
|
|
119
261
|
return createClient({
|
|
120
262
|
baseUrl: this.config.baseUrl ?? DEFAULT_CONFIG.baseUrl,
|
|
121
|
-
headers
|
|
263
|
+
headers,
|
|
264
|
+
...customFetch && { fetch: customFetch }
|
|
122
265
|
});
|
|
123
266
|
}
|
|
124
267
|
/**
|
|
@@ -219,6 +362,46 @@ var ArkeClient = class {
|
|
|
219
362
|
});
|
|
220
363
|
return { data, error };
|
|
221
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Upload file content
|
|
367
|
+
*
|
|
368
|
+
* This is a convenience method that handles the binary body serialization
|
|
369
|
+
* that openapi-fetch doesn't handle automatically for non-JSON bodies.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```typescript
|
|
373
|
+
* // Upload from a Blob
|
|
374
|
+
* const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
|
|
375
|
+
* const { data, error } = await arke.uploadFileContent('01ABC...', blob, 'text/plain');
|
|
376
|
+
*
|
|
377
|
+
* // Upload from an ArrayBuffer
|
|
378
|
+
* const buffer = new TextEncoder().encode('Hello, world!').buffer;
|
|
379
|
+
* const { data, error } = await arke.uploadFileContent('01ABC...', buffer, 'text/plain');
|
|
380
|
+
*
|
|
381
|
+
* // Upload from a Uint8Array
|
|
382
|
+
* const bytes = new TextEncoder().encode('Hello, world!');
|
|
383
|
+
* const { data, error } = await arke.uploadFileContent('01ABC...', bytes, 'text/plain');
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
async uploadFileContent(fileId, content, contentType) {
|
|
387
|
+
let body;
|
|
388
|
+
if (content instanceof Blob) {
|
|
389
|
+
body = content;
|
|
390
|
+
} else if (content instanceof Uint8Array) {
|
|
391
|
+
const buffer = new ArrayBuffer(content.byteLength);
|
|
392
|
+
new Uint8Array(buffer).set(content);
|
|
393
|
+
body = new Blob([buffer], { type: contentType });
|
|
394
|
+
} else {
|
|
395
|
+
body = new Blob([content], { type: contentType });
|
|
396
|
+
}
|
|
397
|
+
const { data, error } = await this.api.POST("/files/{id}/content", {
|
|
398
|
+
params: { path: { id: fileId } },
|
|
399
|
+
body,
|
|
400
|
+
bodySerializer: (b) => b,
|
|
401
|
+
headers: { "Content-Type": contentType }
|
|
402
|
+
});
|
|
403
|
+
return { data, error };
|
|
404
|
+
}
|
|
222
405
|
};
|
|
223
406
|
function createArkeClient(config) {
|
|
224
407
|
return new ArkeClient(config);
|