@objectstack/service-storage 4.0.3 → 4.0.5

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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/local-storage-adapter.ts","../src/storage-service-plugin.ts","../src/s3-storage-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { promises as fs } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport type { IStorageService, StorageUploadOptions, StorageFileInfo } from '@objectstack/spec/contracts';\n\n/**\n * Configuration options for LocalStorageAdapter.\n */\nexport interface LocalStorageAdapterOptions {\n /** Root directory for file storage */\n rootDir: string;\n}\n\n/**\n * Local filesystem storage adapter implementing IStorageService.\n *\n * Stores files on the local disk under a configurable root directory.\n * Suitable for development, testing, and single-server deployments.\n */\nexport class LocalStorageAdapter implements IStorageService {\n private readonly rootDir: string;\n\n constructor(options: LocalStorageAdapterOptions) {\n this.rootDir = options.rootDir;\n }\n\n private resolvePath(key: string): string {\n return join(this.rootDir, key);\n }\n\n async upload(key: string, data: Buffer | ReadableStream, _options?: StorageUploadOptions): Promise<void> {\n const filePath = this.resolvePath(key);\n await fs.mkdir(dirname(filePath), { recursive: true });\n\n if (data instanceof Buffer) {\n await fs.writeFile(filePath, data);\n } else {\n // Convert ReadableStream to Buffer\n const chunks: Uint8Array[] = [];\n const reader = (data as ReadableStream).getReader();\n let done = false;\n while (!done) {\n const result = await reader.read();\n done = result.done;\n if (result.value) chunks.push(result.value);\n }\n await fs.writeFile(filePath, Buffer.concat(chunks));\n }\n }\n\n async download(key: string): Promise<Buffer> {\n const filePath = this.resolvePath(key);\n return fs.readFile(filePath);\n }\n\n async delete(key: string): Promise<void> {\n const filePath = this.resolvePath(key);\n await fs.unlink(filePath);\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await fs.access(this.resolvePath(key));\n return true;\n } catch {\n return false;\n }\n }\n\n async getInfo(key: string): Promise<StorageFileInfo> {\n const filePath = this.resolvePath(key);\n const stat = await fs.stat(filePath);\n return {\n key,\n size: stat.size,\n lastModified: stat.mtime,\n };\n }\n\n async list(prefix: string): Promise<StorageFileInfo[]> {\n const dirPath = this.resolvePath(prefix);\n try {\n const entries = await fs.readdir(dirPath);\n const results: StorageFileInfo[] = [];\n for (const entry of entries) {\n const fullKey = prefix ? `${prefix}/${entry}` : entry;\n try {\n const info = await this.getInfo(fullKey);\n results.push(info);\n } catch {\n // Skip entries that can't be stat'd\n }\n }\n return results;\n } catch {\n return [];\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { LocalStorageAdapter } from './local-storage-adapter.js';\nimport type { LocalStorageAdapterOptions } from './local-storage-adapter.js';\n\n/**\n * Configuration options for the StorageServicePlugin.\n */\nexport interface StorageServicePluginOptions {\n /** Storage adapter type (default: 'local') */\n adapter?: 'local' | 's3';\n /** Options for the local storage adapter */\n local?: LocalStorageAdapterOptions;\n /** S3 configuration (used when adapter is 's3') */\n s3?: { bucket: string; region: string; endpoint?: string };\n}\n\n/**\n * StorageServicePlugin — Production IStorageService implementation.\n *\n * Registers a file storage service with the kernel during the init phase.\n * Supports local filesystem and S3 adapters.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { StorageServicePlugin } from '@objectstack/service-storage';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new StorageServicePlugin({\n * adapter: 'local',\n * local: { rootDir: './uploads' },\n * }));\n * await kernel.bootstrap();\n *\n * const storage = kernel.getService('file-storage');\n * await storage.upload('file.txt', Buffer.from('hello'));\n * ```\n */\nexport class StorageServicePlugin implements Plugin {\n name = 'com.objectstack.service.storage';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: StorageServicePluginOptions;\n\n constructor(options: StorageServicePluginOptions = {}) {\n this.options = { adapter: 'local', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 's3') {\n throw new Error(\n 'S3 storage adapter is not yet implemented. ' +\n 'Use adapter: \"local\" or provide a custom IStorageService via ctx.registerService(\"file-storage\", impl).'\n );\n }\n\n const rootDir = this.options.local?.rootDir ?? './storage';\n const storage = new LocalStorageAdapter({ rootDir });\n ctx.registerService('file-storage', storage);\n ctx.logger.info(`StorageServicePlugin: registered local storage adapter (root: ${rootDir})`);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IStorageService, StorageUploadOptions, StorageFileInfo } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the S3 storage adapter.\n */\nexport interface S3StorageAdapterOptions {\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g. 'us-east-1') */\n region: string;\n /** Optional endpoint URL for S3-compatible services (MinIO, etc.) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId?: string;\n /** AWS secret access key */\n secretAccessKey?: string;\n}\n\n/**\n * S3 storage adapter skeleton implementing IStorageService.\n *\n * This is a placeholder for future AWS S3 integration.\n * Concrete implementation will use the `@aws-sdk/client-s3` package.\n *\n * @example\n * ```ts\n * const storage = new S3StorageAdapter({\n * bucket: 'my-bucket',\n * region: 'us-east-1',\n * });\n * await storage.upload('path/to/file.txt', buffer);\n * ```\n */\nexport class S3StorageAdapter implements IStorageService {\n private readonly bucket: string;\n private readonly region: string;\n\n constructor(options: S3StorageAdapterOptions) {\n this.bucket = options.bucket;\n this.region = options.region;\n }\n\n async upload(_key: string, _data: Buffer | ReadableStream, _options?: StorageUploadOptions): Promise<void> {\n throw new Error(`S3StorageAdapter not yet implemented (bucket: ${this.bucket}, region: ${this.region})`);\n }\n\n async download(_key: string): Promise<Buffer> {\n throw new Error('S3StorageAdapter not yet implemented');\n }\n\n async delete(_key: string): Promise<void> {\n throw new Error('S3StorageAdapter not yet implemented');\n }\n\n async exists(_key: string): Promise<boolean> {\n throw new Error('S3StorageAdapter not yet implemented');\n }\n\n async getInfo(_key: string): Promise<StorageFileInfo> {\n throw new Error('S3StorageAdapter not yet implemented');\n }\n\n async list(_prefix: string): Promise<StorageFileInfo[]> {\n throw new Error('S3StorageAdapter not yet implemented');\n }\n\n async getSignedUrl(_key: string, _expiresIn: number): Promise<string> {\n throw new Error('S3StorageAdapter not yet implemented');\n }\n\n async initiateChunkedUpload(_key: string, _options?: StorageUploadOptions): Promise<string> {\n throw new Error('S3StorageAdapter.initiateChunkedUpload not yet implemented');\n }\n\n async uploadChunk(_uploadId: string, _partNumber: number, _data: Buffer): Promise<string> {\n throw new Error('S3StorageAdapter.uploadChunk not yet implemented');\n }\n\n async completeChunkedUpload(_uploadId: string, _parts: Array<{ partNumber: number; eTag: string }>): Promise<string> {\n throw new Error('S3StorageAdapter.completeChunkedUpload not yet implemented');\n }\n\n async abortChunkedUpload(_uploadId: string): Promise<void> {\n throw new Error('S3StorageAdapter.abortChunkedUpload not yet implemented');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/s3-storage-adapter.ts","../src/local-storage-adapter.ts","../src/metadata-store.ts","../src/storage-routes.ts","../src/objects/system-file.object.ts","../src/objects/system-upload-session.object.ts","../src/storage-service-plugin.ts","../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IStorageService,\n StorageUploadOptions,\n StorageFileInfo,\n PresignedUploadDescriptor,\n PresignedDownloadDescriptor,\n} from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the S3 storage adapter.\n */\nexport interface S3StorageAdapterOptions {\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g. 'us-east-1') */\n region: string;\n /** Optional endpoint URL for S3-compatible services (MinIO, R2, etc.) */\n endpoint?: string;\n /** AWS access key ID (falls back to env/SDK chain) */\n accessKeyId?: string;\n /** AWS secret access key (falls back to env/SDK chain) */\n secretAccessKey?: string;\n /** Force path-style URLs (needed for MinIO / self-hosted) */\n forcePathStyle?: boolean;\n}\n\n/**\n * S3 storage adapter implementing IStorageService.\n *\n * Uses `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` as\n * peer dependencies. These must be installed separately when using the S3\n * adapter in production.\n *\n * @example\n * ```ts\n * const storage = new S3StorageAdapter({\n * bucket: 'my-bucket',\n * region: 'us-east-1',\n * });\n * await storage.upload('path/to/file.txt', buffer);\n * ```\n */\nexport class S3StorageAdapter implements IStorageService {\n private readonly bucket: string;\n private readonly region: string;\n private readonly endpoint?: string;\n private readonly forcePathStyle: boolean;\n private clientPromise: Promise<any> | null = null;\n\n constructor(private readonly options: S3StorageAdapterOptions) {\n this.bucket = options.bucket;\n this.region = options.region;\n this.endpoint = options.endpoint;\n this.forcePathStyle = options.forcePathStyle ?? false;\n }\n\n /**\n * Lazily resolve the AWS S3 client to avoid crashing at import time when\n * `@aws-sdk/client-s3` isn't installed.\n */\n private async getClient(): Promise<any> {\n if (!this.clientPromise) {\n this.clientPromise = (async () => {\n let s3Mod: any;\n try {\n s3Mod = await import('@aws-sdk/client-s3');\n } catch {\n throw new Error(\n 'S3StorageAdapter requires @aws-sdk/client-s3. Install it with: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner',\n );\n }\n const { S3Client } = s3Mod;\n const clientOpts: any = { region: this.region };\n if (this.endpoint) clientOpts.endpoint = this.endpoint;\n if (this.forcePathStyle) clientOpts.forcePathStyle = true;\n if (this.options.accessKeyId && this.options.secretAccessKey) {\n clientOpts.credentials = {\n accessKeyId: this.options.accessKeyId,\n secretAccessKey: this.options.secretAccessKey,\n };\n }\n return new S3Client(clientOpts);\n })();\n }\n return this.clientPromise;\n }\n\n private async s3Mod(): Promise<any> {\n try {\n return await import('@aws-sdk/client-s3');\n } catch {\n throw new Error('S3StorageAdapter requires @aws-sdk/client-s3');\n }\n }\n\n private async presignerMod(): Promise<any> {\n try {\n return await import('@aws-sdk/s3-request-presigner');\n } catch {\n throw new Error('S3StorageAdapter requires @aws-sdk/s3-request-presigner');\n }\n }\n\n // ---------------------------------------------------------------------------\n // Basic operations\n // ---------------------------------------------------------------------------\n\n async upload(key: string, data: Buffer | ReadableStream, options?: StorageUploadOptions): Promise<void> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const body = data instanceof Buffer ? data : await streamToBuffer(data);\n const cmd = new s3.PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: body,\n ContentType: options?.contentType,\n Metadata: options?.metadata,\n ACL: options?.acl === 'public-read' ? 'public-read' : undefined,\n });\n await client.send(cmd);\n }\n\n async download(key: string): Promise<Buffer> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });\n const res = await client.send(cmd);\n return streamToBuffer(res.Body);\n }\n\n async delete(key: string): Promise<void> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });\n await client.send(cmd);\n }\n\n async exists(key: string): Promise<boolean> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n try {\n const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });\n await client.send(cmd);\n return true;\n } catch (err: any) {\n if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) return false;\n throw err;\n }\n }\n\n async getInfo(key: string): Promise<StorageFileInfo> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });\n const res = await client.send(cmd);\n return {\n key,\n size: res.ContentLength ?? 0,\n contentType: res.ContentType,\n lastModified: res.LastModified ?? new Date(),\n metadata: res.Metadata,\n };\n }\n\n async list(prefix: string): Promise<StorageFileInfo[]> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });\n const res = await client.send(cmd);\n return (res.Contents ?? []).map((item: any) => ({\n key: item.Key,\n size: item.Size ?? 0,\n lastModified: item.LastModified ?? new Date(),\n }));\n }\n\n // ---------------------------------------------------------------------------\n // Presigned URLs\n // ---------------------------------------------------------------------------\n\n async getSignedUrl(key: string, expiresIn: number): Promise<string> {\n const desc = await this.getPresignedDownload(key, expiresIn);\n return desc.downloadUrl;\n }\n\n async getPresignedUpload(\n key: string,\n expiresIn: number,\n options?: StorageUploadOptions,\n ): Promise<PresignedUploadDescriptor> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const { getSignedUrl } = await this.presignerMod();\n const cmd = new s3.PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n ContentType: options?.contentType,\n Metadata: options?.metadata,\n ACL: options?.acl === 'public-read' ? 'public-read' : undefined,\n });\n const url = await getSignedUrl(client, cmd, { expiresIn });\n return {\n uploadUrl: url,\n method: 'PUT',\n headers: options?.contentType ? { 'content-type': options.contentType } : undefined,\n expiresIn,\n };\n }\n\n async getPresignedDownload(key: string, expiresIn: number): Promise<PresignedDownloadDescriptor> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const { getSignedUrl } = await this.presignerMod();\n const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });\n const url = await getSignedUrl(client, cmd, { expiresIn });\n return { downloadUrl: url, expiresIn };\n }\n\n // ---------------------------------------------------------------------------\n // Chunked / multipart upload\n // ---------------------------------------------------------------------------\n\n async initiateChunkedUpload(key: string, options?: StorageUploadOptions): Promise<string> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const cmd = new s3.CreateMultipartUploadCommand({\n Bucket: this.bucket,\n Key: key,\n ContentType: options?.contentType,\n Metadata: options?.metadata,\n });\n const res = await client.send(cmd);\n return res.UploadId!;\n }\n\n async uploadChunk(uploadId: string, partNumber: number, data: Buffer): Promise<string> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n // We need the key — store the relationship elsewhere or pass via metadata.\n // For the S3 adapter, `uploadId` is the S3-native UploadId. The key is\n // tracked in the StorageMetadataStore (system_upload_session.key).\n // Here we retrieve it from session state; the plugin ensures the correct\n // key is passed. However, the IStorageService contract doesn't include key\n // in uploadChunk — so we work around by storing the mapping in a WeakMap\n // keyed by uploadId. For a robust implementation we'll add a lookup:\n const key = this._uploadKeys?.get(uploadId);\n if (!key) {\n throw new Error('S3StorageAdapter: key not found for uploadId. Call setUploadKey() before uploadChunk().');\n }\n const cmd = new s3.UploadPartCommand({\n Bucket: this.bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n Body: data,\n });\n const res = await client.send(cmd);\n return res.ETag!;\n }\n\n async completeChunkedUpload(\n uploadId: string,\n parts: Array<{ partNumber: number; eTag: string }>,\n ): Promise<string> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const key = this._uploadKeys?.get(uploadId);\n if (!key) {\n throw new Error('S3StorageAdapter: key not found for uploadId.');\n }\n const cmd = new s3.CompleteMultipartUploadCommand({\n Bucket: this.bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: {\n Parts: parts.map(p => ({ PartNumber: p.partNumber, ETag: p.eTag })),\n },\n });\n await client.send(cmd);\n this._uploadKeys?.delete(uploadId);\n return key;\n }\n\n async abortChunkedUpload(uploadId: string): Promise<void> {\n const client = await this.getClient();\n const s3 = await this.s3Mod();\n const key = this._uploadKeys?.get(uploadId);\n if (!key) return;\n const cmd = new s3.AbortMultipartUploadCommand({\n Bucket: this.bucket,\n Key: key,\n UploadId: uploadId,\n });\n await client.send(cmd);\n this._uploadKeys?.delete(uploadId);\n }\n\n // ---------------------------------------------------------------------------\n // Internal upload key tracking\n // ---------------------------------------------------------------------------\n private _uploadKeys: Map<string, string> = new Map();\n\n /**\n * Register the storage key for a multipart upload session. Must be called\n * by the StorageServicePlugin after `initiateChunkedUpload()` returns so\n * that subsequent `uploadChunk` / `completeChunkedUpload` calls can resolve\n * the S3 key without it being part of the IStorageService contract signature.\n */\n setUploadKey(uploadId: string, key: string): void {\n this._uploadKeys.set(uploadId, key);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nasync function streamToBuffer(stream: any): Promise<Buffer> {\n if (Buffer.isBuffer(stream)) return stream;\n if (stream instanceof Uint8Array) return Buffer.from(stream);\n const chunks: Uint8Array[] = [];\n if (typeof stream[Symbol.asyncIterator] === 'function') {\n for await (const chunk of stream) {\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n }\n } else if (stream.getReader) {\n const reader = stream.getReader();\n let done = false;\n while (!done) {\n const result = await reader.read();\n done = result.done;\n if (result.value) chunks.push(result.value);\n }\n } else {\n throw new Error('Cannot convert stream to buffer');\n }\n return Buffer.concat(chunks);\n}\n\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { promises as fs, createReadStream, createWriteStream } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { createHmac, randomUUID } from 'node:crypto';\nimport type {\n IStorageService,\n StorageUploadOptions,\n StorageFileInfo,\n PresignedUploadDescriptor,\n PresignedDownloadDescriptor,\n} from '@objectstack/spec/contracts';\n\n/**\n * Configuration options for LocalStorageAdapter.\n */\nexport interface LocalStorageAdapterOptions {\n /** Root directory for committed files */\n rootDir: string;\n /**\n * Public base URL the adapter prepends to presigned upload / download URLs.\n * Defaults to a relative path so the same-origin REST server handles the\n * request. Override (e.g. `https://api.example.com`) when the storage\n * routes are exposed on a different host.\n * @default ''\n */\n baseUrl?: string;\n /**\n * Base path of the local storage REST routes mounted by\n * `StorageServicePlugin`. Used to construct presigned URLs.\n * @default '/api/v1/storage'\n */\n basePath?: string;\n /**\n * HMAC secret used to sign presigned-upload tokens.\n * Auto-generated if omitted (suitable for single-process dev usage).\n */\n signingSecret?: string;\n}\n\ninterface PresignTokenPayload {\n k: string; // storage key\n ct?: string; // content-type\n exp: number; // expiry epoch seconds\n op: 'put' | 'get';\n}\n\n/**\n * Local filesystem storage adapter implementing IStorageService.\n *\n * Stores committed files under `rootDir/`, in-flight multipart parts under\n * `rootDir/.parts/<uploadId>/<chunkIndex>`. Presigned URLs are HMAC-signed\n * tokens redeemed against the local REST routes mounted by\n * `StorageServicePlugin` — letting the browser PUT bytes directly without\n * proxying through the application logic.\n *\n * Suitable for development, testing, and single-server deployments.\n */\nexport class LocalStorageAdapter implements IStorageService {\n private readonly rootDir: string;\n private readonly partsDir: string;\n private readonly baseUrl: string;\n private readonly basePath: string;\n private readonly signingSecret: string;\n\n constructor(options: LocalStorageAdapterOptions) {\n this.rootDir = options.rootDir;\n this.partsDir = join(this.rootDir, '.parts');\n this.baseUrl = options.baseUrl ?? '';\n this.basePath = options.basePath ?? '/api/v1/storage';\n this.signingSecret = options.signingSecret ?? randomUUID();\n }\n\n // ---------------------------------------------------------------------------\n // Path helpers\n // ---------------------------------------------------------------------------\n\n private resolvePath(key: string): string {\n if (key.includes('..')) {\n throw new Error(`LocalStorageAdapter: path traversal not allowed (key=\"${key}\")`);\n }\n return join(this.rootDir, key);\n }\n\n private resolvePartPath(uploadId: string, partNumber: number): string {\n if (!/^[A-Za-z0-9_-]+$/.test(uploadId)) {\n throw new Error(`LocalStorageAdapter: invalid uploadId \"${uploadId}\"`);\n }\n return join(this.partsDir, uploadId, String(partNumber).padStart(8, '0'));\n }\n\n // ---------------------------------------------------------------------------\n // Basic file operations\n // ---------------------------------------------------------------------------\n\n async upload(\n key: string,\n data: Buffer | ReadableStream,\n _options?: StorageUploadOptions,\n ): Promise<void> {\n const filePath = this.resolvePath(key);\n await fs.mkdir(dirname(filePath), { recursive: true });\n\n if (data instanceof Buffer) {\n await fs.writeFile(filePath, data);\n return;\n }\n\n // Convert ReadableStream to Buffer\n const chunks: Uint8Array[] = [];\n const reader = (data as ReadableStream).getReader();\n let done = false;\n while (!done) {\n const result = await reader.read();\n done = result.done;\n if (result.value) chunks.push(result.value);\n }\n await fs.writeFile(filePath, Buffer.concat(chunks));\n }\n\n async download(key: string): Promise<Buffer> {\n return fs.readFile(this.resolvePath(key));\n }\n\n async delete(key: string): Promise<void> {\n await fs.unlink(this.resolvePath(key)).catch((err) => {\n if (err && err.code === 'ENOENT') return;\n throw err;\n });\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await fs.access(this.resolvePath(key));\n return true;\n } catch {\n return false;\n }\n }\n\n async getInfo(key: string): Promise<StorageFileInfo> {\n const filePath = this.resolvePath(key);\n const stat = await fs.stat(filePath);\n return { key, size: stat.size, lastModified: stat.mtime };\n }\n\n async list(prefix: string): Promise<StorageFileInfo[]> {\n const dirPath = this.resolvePath(prefix);\n try {\n const entries = await fs.readdir(dirPath);\n const results: StorageFileInfo[] = [];\n for (const entry of entries) {\n if (entry.startsWith('.')) continue;\n const fullKey = prefix ? `${prefix}/${entry}` : entry;\n try {\n results.push(await this.getInfo(fullKey));\n } catch {\n /* skip */\n }\n }\n return results;\n } catch {\n return [];\n }\n }\n\n // ---------------------------------------------------------------------------\n // Presigned URL helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Sign an opaque token for the given payload.\n * Format: base64url(JSON.stringify(payload)) + '.' + base64url(HMAC)\n */\n private signToken(payload: PresignTokenPayload): string {\n const b64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');\n const sig = createHmac('sha256', this.signingSecret).update(b64).digest('base64url');\n return `${b64}.${sig}`;\n }\n\n /**\n * Verify and decode a presigned token. Throws on invalid signature or\n * expiration.\n */\n verifyToken(token: string, expectedOp: 'put' | 'get'): PresignTokenPayload {\n const [b64, sig] = token.split('.');\n if (!b64 || !sig) throw new Error('Invalid storage token format');\n\n const expected = createHmac('sha256', this.signingSecret).update(b64).digest('base64url');\n if (expected !== sig) throw new Error('Invalid storage token signature');\n\n let payload: PresignTokenPayload;\n try {\n payload = JSON.parse(Buffer.from(b64, 'base64url').toString('utf8'));\n } catch {\n throw new Error('Malformed storage token payload');\n }\n\n if (payload.op !== expectedOp) {\n throw new Error(`Storage token op mismatch (expected=\"${expectedOp}\", actual=\"${payload.op}\")`);\n }\n if (Date.now() / 1000 > payload.exp) {\n throw new Error('Storage token expired');\n }\n return payload;\n }\n\n async getPresignedUpload(\n key: string,\n expiresIn: number,\n options?: StorageUploadOptions,\n ): Promise<PresignedUploadDescriptor> {\n const exp = Math.floor(Date.now() / 1000) + Math.max(1, expiresIn);\n const token = this.signToken({ k: key, ct: options?.contentType, exp, op: 'put' });\n\n return {\n uploadUrl: `${this.baseUrl}${this.basePath}/_local/raw/${token}`,\n method: 'PUT',\n headers: options?.contentType ? { 'content-type': options.contentType } : { 'content-type': 'application/octet-stream' },\n expiresIn,\n downloadUrl: `${this.baseUrl}${this.basePath}/_local/file/${encodeURIComponent(key)}`,\n };\n }\n\n async getPresignedDownload(key: string, expiresIn: number): Promise<PresignedDownloadDescriptor> {\n const exp = Math.floor(Date.now() / 1000) + Math.max(1, expiresIn);\n const token = this.signToken({ k: key, exp, op: 'get' });\n return {\n downloadUrl: `${this.baseUrl}${this.basePath}/_local/raw/${token}`,\n expiresIn,\n };\n }\n\n async getSignedUrl(key: string, expiresIn: number): Promise<string> {\n const desc = await this.getPresignedDownload(key, expiresIn);\n return desc.downloadUrl;\n }\n\n // ---------------------------------------------------------------------------\n // Chunked / multipart upload\n // ---------------------------------------------------------------------------\n\n async initiateChunkedUpload(key: string, options?: StorageUploadOptions): Promise<string> {\n const uploadId = randomUUID().replace(/-/g, '');\n const dir = join(this.partsDir, uploadId);\n await fs.mkdir(dir, { recursive: true });\n const meta = {\n key,\n contentType: options?.contentType,\n metadata: options?.metadata,\n createdAt: new Date().toISOString(),\n };\n await fs.writeFile(join(dir, '_meta.json'), JSON.stringify(meta), 'utf8');\n return uploadId;\n }\n\n async uploadChunk(uploadId: string, partNumber: number, data: Buffer): Promise<string> {\n if (!Number.isInteger(partNumber) || partNumber < 1) {\n throw new Error(`uploadChunk: partNumber must be a positive integer (got ${partNumber})`);\n }\n const partPath = this.resolvePartPath(uploadId, partNumber);\n await fs.mkdir(dirname(partPath), { recursive: true });\n await fs.writeFile(partPath, data);\n // ETag for local mode = hex md5 of part bytes (matches S3 single-part ETag format)\n const { createHash } = await import('node:crypto');\n return createHash('md5').update(data).digest('hex');\n }\n\n async completeChunkedUpload(\n uploadId: string,\n parts: Array<{ partNumber: number; eTag: string }>,\n ): Promise<string> {\n const dir = join(this.partsDir, uploadId);\n let meta: { key?: string } = {};\n try {\n meta = JSON.parse(await fs.readFile(join(dir, '_meta.json'), 'utf8'));\n } catch {\n throw new Error(`Upload session \"${uploadId}\" not found`);\n }\n const targetKey = meta.key;\n if (!targetKey) {\n throw new Error(`Upload session \"${uploadId}\" missing target key`);\n }\n\n const sortedParts = [...parts].sort((a, b) => a.partNumber - b.partNumber);\n const finalPath = this.resolvePath(targetKey);\n await fs.mkdir(dirname(finalPath), { recursive: true });\n\n // Stream-concat parts into the final file\n const out = createWriteStream(finalPath);\n try {\n for (const p of sortedParts) {\n const partPath = this.resolvePartPath(uploadId, p.partNumber);\n await new Promise<void>((resolve, reject) => {\n const inp = createReadStream(partPath);\n inp.on('error', reject);\n inp.on('end', () => resolve());\n inp.pipe(out, { end: false });\n });\n }\n } finally {\n await new Promise<void>((resolve) => out.end(() => resolve()));\n }\n\n // Cleanup part directory\n await fs.rm(dir, { recursive: true, force: true });\n return targetKey;\n }\n\n async abortChunkedUpload(uploadId: string): Promise<void> {\n await fs.rm(join(this.partsDir, uploadId), { recursive: true, force: true });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine } from '@objectstack/spec/contracts';\n\n/**\n * Persisted file metadata record (matches `system_file` object schema).\n */\nexport interface FileRecord {\n id: string;\n key: string;\n name: string;\n mime_type?: string;\n size?: number;\n scope?: string;\n bucket?: string;\n acl?: string;\n status: 'pending' | 'committed' | 'deleted';\n etag?: string;\n owner_id?: string;\n metadata?: string;\n created_at?: string;\n updated_at?: string;\n}\n\n/**\n * Persisted upload-session record (matches `system_upload_session` object schema).\n */\nexport interface UploadSessionRecord {\n id: string;\n file_id: string;\n key: string;\n filename: string;\n mime_type?: string;\n total_size: number;\n chunk_size: number;\n total_chunks: number;\n uploaded_chunks?: number;\n uploaded_size?: number;\n parts?: string;\n resume_token?: string;\n backend_upload_id?: string;\n scope?: string;\n bucket?: string;\n metadata?: string;\n status: 'in_progress' | 'completing' | 'completed' | 'failed' | 'expired';\n started_at?: string;\n expires_at?: string;\n updated_at?: string;\n}\n\n/**\n * Storage metadata persistence.\n *\n * Backed by `IDataEngine` (objectql) when available — otherwise falls back to\n * a process-local Map (suitable for tests and dev environments where the\n * data engine isn't wired up).\n */\nexport class StorageMetadataStore {\n private readonly files = new Map<string, FileRecord>();\n private readonly sessions = new Map<string, UploadSessionRecord>();\n\n constructor(private readonly engine: IDataEngine | null) {}\n\n // ---------------------------------------------------------------------------\n // Files\n // ---------------------------------------------------------------------------\n\n async createFile(rec: FileRecord): Promise<FileRecord> {\n const now = new Date().toISOString();\n const full: FileRecord = { created_at: now, updated_at: now, ...rec };\n this.files.set(full.id, full);\n if (this.engine) {\n try {\n await this.engine.insert('system_file', full);\n } catch {\n /* engine not available or schema not migrated — keep in-memory only */\n }\n }\n return full;\n }\n\n async getFile(id: string): Promise<FileRecord | null> {\n if (this.engine) {\n try {\n const found = await this.engine.findOne('system_file', { where: { id } });\n if (found) return found as FileRecord;\n } catch {\n /* fall through to memory */\n }\n }\n return this.files.get(id) ?? null;\n }\n\n async updateFile(id: string, patch: Partial<FileRecord>): Promise<FileRecord | null> {\n const existing = await this.getFile(id);\n if (!existing) return null;\n const merged: FileRecord = { ...existing, ...patch, id, updated_at: new Date().toISOString() };\n this.files.set(id, merged);\n if (this.engine) {\n try {\n await this.engine.update('system_file', merged as any, { where: { id } } as any);\n } catch {\n /* ignore */\n }\n }\n return merged;\n }\n\n async deleteFile(id: string): Promise<void> {\n this.files.delete(id);\n if (this.engine) {\n try {\n await this.engine.delete('system_file', { where: { id } } as any);\n } catch {\n /* ignore */\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Upload sessions\n // ---------------------------------------------------------------------------\n\n async createSession(rec: UploadSessionRecord): Promise<UploadSessionRecord> {\n const now = new Date().toISOString();\n const full: UploadSessionRecord = {\n uploaded_chunks: 0,\n uploaded_size: 0,\n parts: '[]',\n started_at: now,\n updated_at: now,\n ...rec,\n };\n this.sessions.set(full.id, full);\n if (this.engine) {\n try {\n await this.engine.insert('system_upload_session', full);\n } catch {\n /* ignore */\n }\n }\n return full;\n }\n\n async getSession(id: string): Promise<UploadSessionRecord | null> {\n if (this.engine) {\n try {\n const found = await this.engine.findOne('system_upload_session', { where: { id } });\n if (found) return found as UploadSessionRecord;\n } catch {\n /* ignore */\n }\n }\n return this.sessions.get(id) ?? null;\n }\n\n async updateSession(id: string, patch: Partial<UploadSessionRecord>): Promise<UploadSessionRecord | null> {\n const existing = await this.getSession(id);\n if (!existing) return null;\n const merged: UploadSessionRecord = {\n ...existing,\n ...patch,\n id,\n updated_at: new Date().toISOString(),\n };\n this.sessions.set(id, merged);\n if (this.engine) {\n try {\n await this.engine.update('system_upload_session', merged as any, { where: { id } } as any);\n } catch {\n /* ignore */\n }\n }\n return merged;\n }\n\n async deleteSession(id: string): Promise<void> {\n this.sessions.delete(id);\n if (this.engine) {\n try {\n await this.engine.delete('system_upload_session', { where: { id } } as any);\n } catch {\n /* ignore */\n }\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { randomUUID } from 'node:crypto';\nimport type { IHttpServer, IHttpRequest, IHttpResponse, IStorageService } from '@objectstack/spec/contracts';\nimport type { StorageMetadataStore } from './metadata-store.js';\nimport type { LocalStorageAdapter } from './local-storage-adapter.js';\n\n/**\n * Options for the storage route registration helper.\n */\nexport interface StorageRoutesOptions {\n basePath?: string;\n /** Default presigned URL TTL in seconds */\n presignedTtl?: number;\n /** Default chunked upload session TTL in seconds */\n sessionTtl?: number;\n}\n\n/**\n * Register `/api/v1/storage/*` REST routes with the HTTP server.\n *\n * Implements the contract defined in `packages/spec/src/api/storage.zod.ts`\n * (`StorageApiContracts`). This function follows the \"autonomous plugin route\n * registration\" pattern used by `I18nServicePlugin`, `AuthPlugin`, etc.\n *\n * Routes:\n * - POST /storage/upload/presigned → get presigned upload URL\n * - POST /storage/upload/complete → mark upload as committed\n * - POST /storage/upload/chunked → initiate chunked upload\n * - PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex → upload a chunk\n * - POST /storage/upload/chunked/:uploadId/complete → complete chunked\n * - GET /storage/upload/chunked/:uploadId/progress → get upload progress\n * - GET /storage/files/:fileId/url → get download URL\n * - PUT /storage/_local/raw/:token → local adapter raw upload\n * - GET /storage/_local/raw/:token → local adapter raw download\n */\nexport function registerStorageRoutes(\n httpServer: IHttpServer,\n storage: IStorageService,\n store: StorageMetadataStore,\n opts: StorageRoutesOptions = {},\n): void {\n const basePath = opts.basePath ?? '/api/v1/storage';\n const presignedTtl = opts.presignedTtl ?? 3600;\n const sessionTtl = opts.sessionTtl ?? 86400;\n\n // ---------------------------------------------------------------------------\n // POST /storage/upload/presigned\n // ---------------------------------------------------------------------------\n httpServer.post(`${basePath}/upload/presigned`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { filename, mimeType, size, scope, bucket } = req.body ?? {};\n if (!filename || !mimeType || size == null) {\n res.status(400).json({ error: 'filename, mimeType, and size are required' });\n return;\n }\n\n const fileId = randomUUID();\n const key = buildKey(scope ?? 'user', fileId, filename);\n\n // Persist pending file record\n await store.createFile({\n id: fileId,\n key,\n name: filename,\n mime_type: mimeType,\n size,\n scope: scope ?? 'user',\n bucket,\n acl: 'private',\n status: 'pending',\n });\n\n // If adapter supports presigned upload, use it; otherwise build a local stub URL\n let uploadUrl: string;\n let method: 'PUT' | 'POST' = 'PUT';\n let headers: Record<string, string> = { 'content-type': mimeType };\n let expiresIn = presignedTtl;\n\n if (storage.getPresignedUpload) {\n const desc = await storage.getPresignedUpload(key, presignedTtl, { contentType: mimeType });\n uploadUrl = desc.uploadUrl;\n method = desc.method;\n if (desc.headers) headers = desc.headers;\n expiresIn = desc.expiresIn;\n } else {\n // Fallback — caller should PUT to the standard raw endpoint\n uploadUrl = `${basePath}/_local/raw/${fileId}`;\n }\n\n res.json({\n data: {\n uploadUrl,\n method,\n headers,\n fileId,\n expiresIn,\n downloadUrl: `${basePath}/files/${fileId}/url`,\n },\n });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // POST /storage/upload/complete\n // ---------------------------------------------------------------------------\n httpServer.post(`${basePath}/upload/complete`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { fileId, eTag } = req.body ?? {};\n if (!fileId) {\n res.status(400).json({ error: 'fileId is required' });\n return;\n }\n\n const file = await store.getFile(fileId);\n if (!file) {\n res.status(404).json({ error: 'File not found' });\n return;\n }\n\n const updated = await store.updateFile(fileId, {\n status: 'committed',\n etag: eTag ?? undefined,\n });\n\n res.json({\n data: {\n path: updated!.key,\n name: updated!.name,\n size: updated!.size ?? 0,\n mimeType: updated!.mime_type ?? 'application/octet-stream',\n lastModified: updated!.updated_at ?? new Date().toISOString(),\n created: updated!.created_at ?? new Date().toISOString(),\n etag: updated!.etag,\n },\n });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // POST /storage/upload/chunked\n // ---------------------------------------------------------------------------\n httpServer.post(`${basePath}/upload/chunked`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { filename, mimeType, totalSize, chunkSize: reqChunkSize, scope, bucket, metadata } = req.body ?? {};\n if (!filename || !mimeType || !totalSize) {\n res.status(400).json({ error: 'filename, mimeType, and totalSize are required' });\n return;\n }\n\n const chunkSize = Math.max(reqChunkSize ?? 5242880, 5242880);\n const totalChunks = Math.ceil(totalSize / chunkSize);\n\n const fileId = randomUUID();\n const key = buildKey(scope ?? 'user', fileId, filename);\n\n // Create pending file\n await store.createFile({\n id: fileId,\n key,\n name: filename,\n mime_type: mimeType,\n size: totalSize,\n scope: scope ?? 'user',\n bucket,\n acl: 'private',\n status: 'pending',\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n });\n\n // Initiate chunked upload in backend\n let backendUploadId: string | undefined;\n if (storage.initiateChunkedUpload) {\n backendUploadId = await storage.initiateChunkedUpload(key, { contentType: mimeType, metadata });\n // S3 adapter needs to know the key for subsequent chunk/complete calls\n if ('setUploadKey' in storage && typeof (storage as any).setUploadKey === 'function') {\n (storage as any).setUploadKey(backendUploadId, key);\n }\n }\n\n const uploadId = backendUploadId ?? randomUUID().replace(/-/g, '');\n const resumeToken = randomUUID();\n const expiresAt = new Date(Date.now() + sessionTtl * 1000).toISOString();\n\n await store.createSession({\n id: uploadId,\n file_id: fileId,\n key,\n filename,\n mime_type: mimeType,\n total_size: totalSize,\n chunk_size: chunkSize,\n total_chunks: totalChunks,\n resume_token: resumeToken,\n backend_upload_id: backendUploadId,\n scope: scope ?? 'user',\n bucket,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n status: 'in_progress',\n expires_at: expiresAt,\n });\n\n res.json({\n data: {\n uploadId,\n resumeToken,\n fileId,\n totalChunks,\n chunkSize,\n expiresAt,\n },\n });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex\n // ---------------------------------------------------------------------------\n httpServer.put(`${basePath}/upload/chunked/:uploadId/chunk/:chunkIndex`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { uploadId, chunkIndex: chunkIndexStr } = req.params;\n const chunkIndex = parseInt(chunkIndexStr, 10);\n if (!uploadId || isNaN(chunkIndex)) {\n res.status(400).json({ error: 'uploadId and chunkIndex are required' });\n return;\n }\n\n const session = await store.getSession(uploadId);\n if (!session) {\n res.status(404).json({ error: 'Upload session not found' });\n return;\n }\n\n // Verify resume token\n const token = (req.headers['x-resume-token'] ?? '') as string;\n if (session.resume_token && token !== session.resume_token) {\n res.status(403).json({ error: 'Invalid resume token' });\n return;\n }\n\n // Get raw body (binary data)\n let data: Buffer;\n if (req.rawBody) {\n data = await req.rawBody();\n } else if (Buffer.isBuffer(req.body)) {\n data = req.body;\n } else if (req.body instanceof ArrayBuffer) {\n data = Buffer.from(req.body);\n } else {\n res.status(400).json({ error: 'Binary body required' });\n return;\n }\n\n // Upload the chunk (S3 uses 1-based part numbers)\n let eTag = '';\n if (storage.uploadChunk) {\n eTag = await storage.uploadChunk(uploadId, chunkIndex + 1, data);\n }\n\n // Update session progress\n const currentParts: Array<{ chunkIndex: number; eTag: string }> = JSON.parse(session.parts ?? '[]');\n currentParts.push({ chunkIndex, eTag });\n const uploadedChunks = (session.uploaded_chunks ?? 0) + 1;\n const uploadedSize = (session.uploaded_size ?? 0) + data.byteLength;\n await store.updateSession(uploadId, {\n uploaded_chunks: uploadedChunks,\n uploaded_size: uploadedSize,\n parts: JSON.stringify(currentParts),\n });\n\n res.json({\n data: {\n chunkIndex,\n eTag,\n bytesReceived: data.byteLength,\n },\n });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // POST /storage/upload/chunked/:uploadId/complete\n // ---------------------------------------------------------------------------\n httpServer.post(`${basePath}/upload/chunked/:uploadId/complete`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { uploadId } = req.params;\n const session = await store.getSession(uploadId);\n if (!session) {\n res.status(404).json({ error: 'Upload session not found' });\n return;\n }\n\n await store.updateSession(uploadId, { status: 'completing' });\n\n const partsFromBody = (req.body?.parts ?? []) as Array<{ chunkIndex: number; eTag: string }>;\n const partsForBackend = partsFromBody.map(p => ({\n partNumber: p.chunkIndex + 1,\n eTag: p.eTag,\n }));\n\n let finalKey = session.key;\n if (storage.completeChunkedUpload) {\n finalKey = await storage.completeChunkedUpload(uploadId, partsForBackend);\n }\n\n // Update file + session\n await store.updateFile(session.file_id, { status: 'committed', key: finalKey });\n await store.updateSession(uploadId, { status: 'completed' });\n\n res.json({\n data: {\n fileId: session.file_id,\n key: finalKey,\n size: session.total_size,\n mimeType: session.mime_type ?? 'application/octet-stream',\n url: `${basePath}/files/${session.file_id}/url`,\n },\n });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // GET /storage/upload/chunked/:uploadId/progress\n // ---------------------------------------------------------------------------\n httpServer.get(`${basePath}/upload/chunked/:uploadId/progress`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { uploadId } = req.params;\n const session = await store.getSession(uploadId);\n if (!session) {\n res.status(404).json({ error: 'Upload session not found' });\n return;\n }\n\n const uploadedChunks = session.uploaded_chunks ?? 0;\n const uploadedSize = session.uploaded_size ?? 0;\n const percentComplete = session.total_size > 0\n ? Math.min(100, Math.round((uploadedSize / session.total_size) * 100))\n : 0;\n\n res.json({\n data: {\n uploadId: session.id,\n fileId: session.file_id,\n filename: session.filename,\n totalSize: session.total_size,\n uploadedSize,\n totalChunks: session.total_chunks,\n uploadedChunks,\n percentComplete,\n status: session.status,\n startedAt: session.started_at,\n expiresAt: session.expires_at,\n },\n });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // GET /storage/files/:fileId/url\n // ---------------------------------------------------------------------------\n httpServer.get(`${basePath}/files/:fileId/url`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { fileId } = req.params;\n const file = await store.getFile(fileId);\n if (!file || file.status !== 'committed') {\n res.status(404).json({ error: 'File not found or not committed' });\n return;\n }\n\n let url: string;\n if (storage.getPresignedDownload) {\n const desc = await storage.getPresignedDownload(file.key, presignedTtl);\n url = desc.downloadUrl;\n } else if (storage.getSignedUrl) {\n url = await storage.getSignedUrl(file.key, presignedTtl);\n } else {\n url = `${basePath}/_local/file/${encodeURIComponent(file.key)}`;\n }\n\n res.json({ url });\n } catch (err: any) {\n res.status(500).json({ error: err.message ?? 'Internal error' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // PUT /storage/_local/raw/:token — presigned raw upload (LocalStorageAdapter)\n // ---------------------------------------------------------------------------\n httpServer.put(`${basePath}/_local/raw/:token`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { token } = req.params;\n const localAdapter = storage as LocalStorageAdapter;\n if (!localAdapter.verifyToken) {\n res.status(501).json({ error: 'Presigned raw upload not supported by this adapter' });\n return;\n }\n\n const payload = localAdapter.verifyToken(token, 'put');\n let data: Buffer;\n if (req.rawBody) {\n data = await req.rawBody();\n } else if (Buffer.isBuffer(req.body)) {\n data = req.body;\n } else {\n res.status(400).json({ error: 'Binary body required' });\n return;\n }\n\n await storage.upload(payload.k, data, { contentType: payload.ct });\n res.json({ ok: true, key: payload.k });\n } catch (err: any) {\n const statusCode = err.message?.includes('expired') || err.message?.includes('signature') ? 403 : 500;\n res.status(statusCode).json({ error: err.message ?? 'Upload failed' });\n }\n });\n\n // ---------------------------------------------------------------------------\n // GET /storage/_local/raw/:token — presigned raw download (LocalStorageAdapter)\n // ---------------------------------------------------------------------------\n httpServer.get(`${basePath}/_local/raw/:token`, async (req: IHttpRequest, res: IHttpResponse) => {\n try {\n const { token } = req.params;\n const localAdapter = storage as LocalStorageAdapter;\n if (!localAdapter.verifyToken) {\n res.status(501).json({ error: 'Presigned download not supported by this adapter' });\n return;\n }\n\n const payload = localAdapter.verifyToken(token, 'get');\n const data = await storage.download(payload.k);\n\n res.header('content-type', payload.ct ?? 'application/octet-stream');\n res.header('content-length', String(data.byteLength));\n // IHttpResponse only has json/send — use send with binary encoding\n // This works for Hono adapter since send passes through\n (res as any).send(data);\n } catch (err: any) {\n const statusCode = err.message?.includes('expired') || err.message?.includes('signature') ? 403 : 500;\n res.status(statusCode).json({ error: err.message ?? 'Download failed' });\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction buildKey(scope: string, fileId: string, filename: string): string {\n const ext = filename.includes('.') ? '.' + filename.split('.').pop() : '';\n return `${scope}/${fileId}${ext}`;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * System File Object\n *\n * Persisted metadata for files stored via the Storage Service.\n *\n * The Storage Service contract addresses files by `key` (path inside the\n * configured backend). The REST protocol (see `packages/spec/src/api/storage.zod.ts`)\n * exposes an opaque `fileId` so that:\n *\n * 1. Client code never needs to know — or be able to spoof — backend keys.\n * 2. Files can be moved between buckets / storage tiers without breaking links.\n * 3. Lifecycle status (uploading → committed → deleted) can be tracked.\n *\n * Belongs to `@objectstack/service-storage` per the\n * \"protocol + service ownership\" pattern used by `service-feed`.\n */\nexport const SystemFile = ObjectSchema.create({\n name: 'system_file',\n label: 'System File',\n pluralLabel: 'System Files',\n icon: 'file',\n description: 'Storage service file metadata (fileId ↔ key mapping)',\n titleFormat: '{name}',\n compactLayout: ['name', 'mime_type', 'size', 'status', 'created_at'],\n\n fields: {\n id: Field.text({\n label: 'File ID',\n required: true,\n readonly: true,\n }),\n\n key: Field.text({\n label: 'Storage Key',\n required: true,\n searchable: true,\n }),\n\n name: Field.text({\n label: 'File Name',\n required: true,\n searchable: true,\n }),\n\n mime_type: Field.text({\n label: 'MIME Type',\n }),\n\n size: Field.number({\n label: 'Size (bytes)',\n }),\n\n scope: Field.select({\n label: 'Scope',\n options: [\n { label: 'User', value: 'user' },\n { label: 'Tenant', value: 'tenant' },\n { label: 'Public', value: 'public' },\n { label: 'Private', value: 'private' },\n { label: 'Temp', value: 'temp' },\n ],\n }),\n\n bucket: Field.text({\n label: 'Bucket',\n }),\n\n acl: Field.select({\n label: 'ACL',\n options: [\n { label: 'Private', value: 'private' },\n { label: 'Public Read', value: 'public-read' },\n ],\n }),\n\n status: Field.select({\n label: 'Status',\n required: true,\n options: [\n { label: 'Pending Upload', value: 'pending' },\n { label: 'Committed', value: 'committed' },\n { label: 'Deleted', value: 'deleted' },\n ],\n }),\n\n etag: Field.text({\n label: 'ETag',\n }),\n\n owner_id: Field.text({\n label: 'Owner ID',\n }),\n\n metadata: Field.text({\n label: 'Metadata (JSON)',\n }),\n\n created_at: Field.datetime({\n label: 'Created At',\n }),\n\n updated_at: Field.datetime({\n label: 'Updated At',\n }),\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * System Upload Session Object\n *\n * Persisted state for in-flight chunked / multipart uploads.\n *\n * Tracks upload progress so that interrupted uploads can be resumed via\n * `POST /api/v1/storage/upload/chunked/:uploadId/progress`. Sessions are\n * cleaned up by the storage service on `complete` / `abort` / TTL expiry.\n */\nexport const SystemUploadSession = ObjectSchema.create({\n name: 'system_upload_session',\n label: 'System Upload Session',\n pluralLabel: 'System Upload Sessions',\n icon: 'upload-cloud',\n description: 'Resumable multipart upload sessions tracked by service-storage',\n titleFormat: '{filename}',\n compactLayout: ['filename', 'status', 'uploaded_chunks', 'total_chunks', 'expires_at'],\n\n fields: {\n id: Field.text({\n label: 'Upload Session ID',\n required: true,\n readonly: true,\n }),\n\n file_id: Field.text({\n label: 'File ID',\n required: true,\n }),\n\n key: Field.text({\n label: 'Storage Key',\n required: true,\n }),\n\n filename: Field.text({\n label: 'Filename',\n required: true,\n }),\n\n mime_type: Field.text({\n label: 'MIME Type',\n }),\n\n total_size: Field.number({\n label: 'Total Size (bytes)',\n required: true,\n }),\n\n chunk_size: Field.number({\n label: 'Chunk Size (bytes)',\n required: true,\n }),\n\n total_chunks: Field.number({\n label: 'Total Chunks',\n required: true,\n }),\n\n uploaded_chunks: Field.number({\n label: 'Uploaded Chunks',\n }),\n\n uploaded_size: Field.number({\n label: 'Uploaded Size (bytes)',\n }),\n\n parts: Field.text({\n label: 'Uploaded Parts (JSON)',\n }),\n\n resume_token: Field.text({\n label: 'Resume Token',\n }),\n\n backend_upload_id: Field.text({\n label: 'Backend Upload ID',\n }),\n\n scope: Field.text({\n label: 'Scope',\n }),\n\n bucket: Field.text({\n label: 'Bucket',\n }),\n\n metadata: Field.text({\n label: 'Metadata (JSON)',\n }),\n\n status: Field.select({\n label: 'Status',\n required: true,\n options: [\n { label: 'In Progress', value: 'in_progress' },\n { label: 'Completing', value: 'completing' },\n { label: 'Completed', value: 'completed' },\n { label: 'Failed', value: 'failed' },\n { label: 'Expired', value: 'expired' },\n ],\n }),\n\n started_at: Field.datetime({\n label: 'Started At',\n }),\n\n expires_at: Field.datetime({\n label: 'Expires At',\n }),\n\n updated_at: Field.datetime({\n label: 'Updated At',\n }),\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IHttpServer, IDataEngine, IStorageService } from '@objectstack/spec/contracts';\nimport { LocalStorageAdapter } from './local-storage-adapter.js';\nimport type { LocalStorageAdapterOptions } from './local-storage-adapter.js';\nimport { StorageMetadataStore } from './metadata-store.js';\nimport { registerStorageRoutes } from './storage-routes.js';\nimport { SystemFile, SystemUploadSession } from './objects/index.js';\n\n/**\n * Configuration options for the StorageServicePlugin.\n */\nexport interface StorageServicePluginOptions {\n /** Storage adapter type (default: 'local') */\n adapter?: 'local' | 's3';\n /** Options for the local storage adapter */\n local?: LocalStorageAdapterOptions;\n /** S3 configuration (used when adapter is 's3') */\n s3?: { bucket: string; region: string; endpoint?: string };\n /**\n * Whether to register REST routes with the HTTP server.\n * @default true\n */\n registerRoutes?: boolean;\n /**\n * Base path for storage REST routes.\n * @default '/api/v1/storage'\n */\n basePath?: string;\n /**\n * Default presigned URL TTL in seconds.\n * @default 3600\n */\n presignedTtl?: number;\n /**\n * Default chunked upload session TTL in seconds.\n * @default 86400\n */\n sessionTtl?: number;\n}\n\n/**\n * StorageServicePlugin — Production IStorageService implementation.\n *\n * Registers a file storage service with the kernel during the init phase.\n * Supports local filesystem (development/testing/single-server) and\n * S3-compatible storage (production). Automatically mounts\n * `/api/v1/storage/*` REST routes via the `kernel:ready` hook when an\n * HTTP server is available.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { StorageServicePlugin } from '@objectstack/service-storage';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new StorageServicePlugin({\n * adapter: 'local',\n * local: { rootDir: './uploads' },\n * }));\n * await kernel.bootstrap();\n *\n * const storage = kernel.getService('file-storage');\n * await storage.upload('file.txt', Buffer.from('hello'));\n * ```\n */\nexport class StorageServicePlugin implements Plugin {\n name = 'com.objectstack.service.storage';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: StorageServicePluginOptions;\n private storage: IStorageService | null = null;\n private store: StorageMetadataStore | null = null;\n\n constructor(options: StorageServicePluginOptions = {}) {\n this.options = { adapter: 'local', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapter = this.options.adapter;\n if (adapter === 's3') {\n // Dynamically import the S3 adapter (to avoid top-level import of optional peer dep)\n const { S3StorageAdapter } = await import('./s3-storage-adapter.js');\n const s3Opts = this.options.s3;\n if (!s3Opts) {\n throw new Error('StorageServicePlugin: s3 options are required when adapter is \"s3\"');\n }\n this.storage = new S3StorageAdapter(s3Opts);\n } else {\n const rootDir = this.options.local?.rootDir ?? './storage';\n const basePath = this.options.basePath ?? '/api/v1/storage';\n this.storage = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local });\n }\n\n ctx.registerService('file-storage', this.storage);\n ctx.logger.info(`StorageServicePlugin: registered ${adapter} storage adapter`);\n\n // Register system objects via manifest service (if available)\n try {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.storage',\n name: 'Storage Service',\n version: '1.0.0',\n type: 'plugin',\n scope: 'project',\n objects: [SystemFile, SystemUploadSession],\n });\n } catch {\n // manifest service may not be available in all environments\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (this.options.registerRoutes === false) return;\n\n ctx.hook('kernel:ready', async () => {\n let httpServer: IHttpServer | null = null;\n try {\n httpServer = ctx.getService<IHttpServer>('http-server');\n } catch {\n // not available\n }\n\n if (!httpServer || !this.storage) {\n ctx.logger.warn(\n 'StorageServicePlugin: no HTTP server available — REST routes not registered. ' +\n 'File storage is still accessible programmatically via kernel.getService(\"file-storage\").',\n );\n return;\n }\n\n // Create metadata store backed by data engine (if available)\n let engine: IDataEngine | null = null;\n try {\n engine = ctx.getService<IDataEngine>('objectql');\n } catch {\n // data engine not wired — use in-memory fallback\n }\n this.store = new StorageMetadataStore(engine);\n\n registerStorageRoutes(httpServer, this.storage, this.store, {\n basePath: this.options.basePath ?? '/api/v1/storage',\n presignedTtl: this.options.presignedTtl,\n sessionTtl: this.options.sessionTtl,\n });\n\n ctx.logger.info('StorageServicePlugin: REST routes registered at ' + (this.options.basePath ?? '/api/v1/storage'));\n });\n }\n}\n\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { StorageServicePlugin } from './storage-service-plugin.js';\nexport type { StorageServicePluginOptions } from './storage-service-plugin.js';\nexport { LocalStorageAdapter } from './local-storage-adapter.js';\nexport type { LocalStorageAdapterOptions } from './local-storage-adapter.js';\nexport { S3StorageAdapter } from './s3-storage-adapter.js';\nexport type { S3StorageAdapterOptions } from './s3-storage-adapter.js';\nexport { StorageMetadataStore } from './metadata-store.js';\nexport type { FileRecord, UploadSessionRecord } from './metadata-store.js';\nexport { registerStorageRoutes } from './storage-routes.js';\nexport type { StorageRoutesOptions } from './storage-routes.js';\nexport { SystemFile, SystemUploadSession } from './objects/index.js';\n"],"mappings":";;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AA+TA,eAAe,eAAe,QAA8B;AAC1D,MAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,MAAI,kBAAkB,WAAY,QAAO,OAAO,KAAK,MAAM;AAC3D,QAAM,SAAuB,CAAC;AAC9B,MAAI,OAAO,OAAO,OAAO,aAAa,MAAM,YAAY;AACtD,qBAAiB,SAAS,QAAQ;AAChC,aAAO,KAAK,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,KAAK;AAAA,IACpE;AAAA,EACF,WAAW,OAAO,WAAW;AAC3B,UAAM,SAAS,OAAO,UAAU;AAChC,QAAI,OAAO;AACX,WAAO,CAAC,MAAM;AACZ,YAAM,SAAS,MAAM,OAAO,KAAK;AACjC,aAAO,OAAO;AACd,UAAI,OAAO,MAAO,QAAO,KAAK,OAAO,KAAK;AAAA,IAC5C;AAAA,EACF,OAAO;AACL,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AACA,SAAO,OAAO,OAAO,MAAM;AAC7B;AAnVA,IA4Ca;AA5Cb;AAAA;AAAA;AA4CO,IAAM,mBAAN,MAAkD;AAAA,MAOvD,YAA6B,SAAkC;AAAlC;AAF7B,aAAQ,gBAAqC;AA6P7C;AAAA;AAAA;AAAA,aAAQ,cAAmC,oBAAI,IAAI;AA1PjD,aAAK,SAAS,QAAQ;AACtB,aAAK,SAAS,QAAQ;AACtB,aAAK,WAAW,QAAQ;AACxB,aAAK,iBAAiB,QAAQ,kBAAkB;AAAA,MAClD;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,MAAc,YAA0B;AACtC,YAAI,CAAC,KAAK,eAAe;AACvB,eAAK,iBAAiB,YAAY;AAChC,gBAAI;AACJ,gBAAI;AACF,sBAAQ,MAAM,OAAO,oBAAoB;AAAA,YAC3C,QAAQ;AACN,oBAAM,IAAI;AAAA,gBACR;AAAA,cACF;AAAA,YACF;AACA,kBAAM,EAAE,SAAS,IAAI;AACrB,kBAAM,aAAkB,EAAE,QAAQ,KAAK,OAAO;AAC9C,gBAAI,KAAK,SAAU,YAAW,WAAW,KAAK;AAC9C,gBAAI,KAAK,eAAgB,YAAW,iBAAiB;AACrD,gBAAI,KAAK,QAAQ,eAAe,KAAK,QAAQ,iBAAiB;AAC5D,yBAAW,cAAc;AAAA,gBACvB,aAAa,KAAK,QAAQ;AAAA,gBAC1B,iBAAiB,KAAK,QAAQ;AAAA,cAChC;AAAA,YACF;AACA,mBAAO,IAAI,SAAS,UAAU;AAAA,UAChC,GAAG;AAAA,QACL;AACA,eAAO,KAAK;AAAA,MACd;AAAA,MAEA,MAAc,QAAsB;AAClC,YAAI;AACF,iBAAO,MAAM,OAAO,oBAAoB;AAAA,QAC1C,QAAQ;AACN,gBAAM,IAAI,MAAM,8CAA8C;AAAA,QAChE;AAAA,MACF;AAAA,MAEA,MAAc,eAA6B;AACzC,YAAI;AACF,iBAAO,MAAM,OAAO,+BAA+B;AAAA,QACrD,QAAQ;AACN,gBAAM,IAAI,MAAM,yDAAyD;AAAA,QAC3E;AAAA,MACF;AAAA;AAAA;AAAA;AAAA,MAMA,MAAM,OAAO,KAAa,MAA+B,SAA+C;AACtG,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,OAAO,gBAAgB,SAAS,OAAO,MAAM,eAAe,IAAI;AACtE,cAAM,MAAM,IAAI,GAAG,iBAAiB;AAAA,UAClC,QAAQ,KAAK;AAAA,UACb,KAAK;AAAA,UACL,MAAM;AAAA,UACN,aAAa,SAAS;AAAA,UACtB,UAAU,SAAS;AAAA,UACnB,KAAK,SAAS,QAAQ,gBAAgB,gBAAgB;AAAA,QACxD,CAAC;AACD,cAAM,OAAO,KAAK,GAAG;AAAA,MACvB;AAAA,MAEA,MAAM,SAAS,KAA8B;AAC3C,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,IAAI,GAAG,iBAAiB,EAAE,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AACrE,cAAM,MAAM,MAAM,OAAO,KAAK,GAAG;AACjC,eAAO,eAAe,IAAI,IAAI;AAAA,MAChC;AAAA,MAEA,MAAM,OAAO,KAA4B;AACvC,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,IAAI,GAAG,oBAAoB,EAAE,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AACxE,cAAM,OAAO,KAAK,GAAG;AAAA,MACvB;AAAA,MAEA,MAAM,OAAO,KAA+B;AAC1C,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,YAAI;AACF,gBAAM,MAAM,IAAI,GAAG,kBAAkB,EAAE,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AACtE,gBAAM,OAAO,KAAK,GAAG;AACrB,iBAAO;AAAA,QACT,SAAS,KAAU;AACjB,cAAI,IAAI,SAAS,cAAc,IAAI,WAAW,mBAAmB,IAAK,QAAO;AAC7E,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,MAEA,MAAM,QAAQ,KAAuC;AACnD,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,IAAI,GAAG,kBAAkB,EAAE,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AACtE,cAAM,MAAM,MAAM,OAAO,KAAK,GAAG;AACjC,eAAO;AAAA,UACL;AAAA,UACA,MAAM,IAAI,iBAAiB;AAAA,UAC3B,aAAa,IAAI;AAAA,UACjB,cAAc,IAAI,gBAAgB,oBAAI,KAAK;AAAA,UAC3C,UAAU,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,MAEA,MAAM,KAAK,QAA4C;AACrD,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,IAAI,GAAG,qBAAqB,EAAE,QAAQ,KAAK,QAAQ,QAAQ,OAAO,CAAC;AAC/E,cAAM,MAAM,MAAM,OAAO,KAAK,GAAG;AACjC,gBAAQ,IAAI,YAAY,CAAC,GAAG,IAAI,CAAC,UAAe;AAAA,UAC9C,KAAK,KAAK;AAAA,UACV,MAAM,KAAK,QAAQ;AAAA,UACnB,cAAc,KAAK,gBAAgB,oBAAI,KAAK;AAAA,QAC9C,EAAE;AAAA,MACJ;AAAA;AAAA;AAAA;AAAA,MAMA,MAAM,aAAa,KAAa,WAAoC;AAClE,cAAM,OAAO,MAAM,KAAK,qBAAqB,KAAK,SAAS;AAC3D,eAAO,KAAK;AAAA,MACd;AAAA,MAEA,MAAM,mBACJ,KACA,WACA,SACoC;AACpC,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,EAAE,aAAa,IAAI,MAAM,KAAK,aAAa;AACjD,cAAM,MAAM,IAAI,GAAG,iBAAiB;AAAA,UAClC,QAAQ,KAAK;AAAA,UACb,KAAK;AAAA,UACL,aAAa,SAAS;AAAA,UACtB,UAAU,SAAS;AAAA,UACnB,KAAK,SAAS,QAAQ,gBAAgB,gBAAgB;AAAA,QACxD,CAAC;AACD,cAAM,MAAM,MAAM,aAAa,QAAQ,KAAK,EAAE,UAAU,CAAC;AACzD,eAAO;AAAA,UACL,WAAW;AAAA,UACX,QAAQ;AAAA,UACR,SAAS,SAAS,cAAc,EAAE,gBAAgB,QAAQ,YAAY,IAAI;AAAA,UAC1E;AAAA,QACF;AAAA,MACF;AAAA,MAEA,MAAM,qBAAqB,KAAa,WAAyD;AAC/F,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,EAAE,aAAa,IAAI,MAAM,KAAK,aAAa;AACjD,cAAM,MAAM,IAAI,GAAG,iBAAiB,EAAE,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AACrE,cAAM,MAAM,MAAM,aAAa,QAAQ,KAAK,EAAE,UAAU,CAAC;AACzD,eAAO,EAAE,aAAa,KAAK,UAAU;AAAA,MACvC;AAAA;AAAA;AAAA;AAAA,MAMA,MAAM,sBAAsB,KAAa,SAAiD;AACxF,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,IAAI,GAAG,6BAA6B;AAAA,UAC9C,QAAQ,KAAK;AAAA,UACb,KAAK;AAAA,UACL,aAAa,SAAS;AAAA,UACtB,UAAU,SAAS;AAAA,QACrB,CAAC;AACD,cAAM,MAAM,MAAM,OAAO,KAAK,GAAG;AACjC,eAAO,IAAI;AAAA,MACb;AAAA,MAEA,MAAM,YAAY,UAAkB,YAAoB,MAA+B;AACrF,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAQ5B,cAAM,MAAM,KAAK,aAAa,IAAI,QAAQ;AAC1C,YAAI,CAAC,KAAK;AACR,gBAAM,IAAI,MAAM,yFAAyF;AAAA,QAC3G;AACA,cAAM,MAAM,IAAI,GAAG,kBAAkB;AAAA,UACnC,QAAQ,KAAK;AAAA,UACb,KAAK;AAAA,UACL,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,MAAM;AAAA,QACR,CAAC;AACD,cAAM,MAAM,MAAM,OAAO,KAAK,GAAG;AACjC,eAAO,IAAI;AAAA,MACb;AAAA,MAEA,MAAM,sBACJ,UACA,OACiB;AACjB,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,KAAK,aAAa,IAAI,QAAQ;AAC1C,YAAI,CAAC,KAAK;AACR,gBAAM,IAAI,MAAM,+CAA+C;AAAA,QACjE;AACA,cAAM,MAAM,IAAI,GAAG,+BAA+B;AAAA,UAChD,QAAQ,KAAK;AAAA,UACb,KAAK;AAAA,UACL,UAAU;AAAA,UACV,iBAAiB;AAAA,YACf,OAAO,MAAM,IAAI,QAAM,EAAE,YAAY,EAAE,YAAY,MAAM,EAAE,KAAK,EAAE;AAAA,UACpE;AAAA,QACF,CAAC;AACD,cAAM,OAAO,KAAK,GAAG;AACrB,aAAK,aAAa,OAAO,QAAQ;AACjC,eAAO;AAAA,MACT;AAAA,MAEA,MAAM,mBAAmB,UAAiC;AACxD,cAAM,SAAS,MAAM,KAAK,UAAU;AACpC,cAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,cAAM,MAAM,KAAK,aAAa,IAAI,QAAQ;AAC1C,YAAI,CAAC,IAAK;AACV,cAAM,MAAM,IAAI,GAAG,4BAA4B;AAAA,UAC7C,QAAQ,KAAK;AAAA,UACb,KAAK;AAAA,UACL,UAAU;AAAA,QACZ,CAAC;AACD,cAAM,OAAO,KAAK,GAAG;AACrB,aAAK,aAAa,OAAO,QAAQ;AAAA,MACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAaA,aAAa,UAAkB,KAAmB;AAChD,aAAK,YAAY,IAAI,UAAU,GAAG;AAAA,MACpC;AAAA,IACF;AAAA;AAAA;;;ACvTA,SAAS,YAAY,IAAI,kBAAkB,yBAAyB;AACpE,SAAS,MAAM,eAAe;AAC9B,SAAS,YAAY,kBAAkB;AAsDhC,IAAM,sBAAN,MAAqD;AAAA,EAO1D,YAAY,SAAqC;AAC/C,SAAK,UAAU,QAAQ;AACvB,SAAK,WAAW,KAAK,KAAK,SAAS,QAAQ;AAC3C,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,WAAW,QAAQ,YAAY;AACpC,SAAK,gBAAgB,QAAQ,iBAAiB,WAAW;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAY,KAAqB;AACvC,QAAI,IAAI,SAAS,IAAI,GAAG;AACtB,YAAM,IAAI,MAAM,yDAAyD,GAAG,IAAI;AAAA,IAClF;AACA,WAAO,KAAK,KAAK,SAAS,GAAG;AAAA,EAC/B;AAAA,EAEQ,gBAAgB,UAAkB,YAA4B;AACpE,QAAI,CAAC,mBAAmB,KAAK,QAAQ,GAAG;AACtC,YAAM,IAAI,MAAM,0CAA0C,QAAQ,GAAG;AAAA,IACvE;AACA,WAAO,KAAK,KAAK,UAAU,UAAU,OAAO,UAAU,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OACJ,KACA,MACA,UACe;AACf,UAAM,WAAW,KAAK,YAAY,GAAG;AACrC,UAAM,GAAG,MAAM,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAErD,QAAI,gBAAgB,QAAQ;AAC1B,YAAM,GAAG,UAAU,UAAU,IAAI;AACjC;AAAA,IACF;AAGA,UAAM,SAAuB,CAAC;AAC9B,UAAM,SAAU,KAAwB,UAAU;AAClD,QAAI,OAAO;AACX,WAAO,CAAC,MAAM;AACZ,YAAM,SAAS,MAAM,OAAO,KAAK;AACjC,aAAO,OAAO;AACd,UAAI,OAAO,MAAO,QAAO,KAAK,OAAO,KAAK;AAAA,IAC5C;AACA,UAAM,GAAG,UAAU,UAAU,OAAO,OAAO,MAAM,CAAC;AAAA,EACpD;AAAA,EAEA,MAAM,SAAS,KAA8B;AAC3C,WAAO,GAAG,SAAS,KAAK,YAAY,GAAG,CAAC;AAAA,EAC1C;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,UAAM,GAAG,OAAO,KAAK,YAAY,GAAG,CAAC,EAAE,MAAM,CAAC,QAAQ;AACpD,UAAI,OAAO,IAAI,SAAS,SAAU;AAClC,YAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,KAA+B;AAC1C,QAAI;AACF,YAAM,GAAG,OAAO,KAAK,YAAY,GAAG,CAAC;AACrC,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,KAAuC;AACnD,UAAM,WAAW,KAAK,YAAY,GAAG;AACrC,UAAM,OAAO,MAAM,GAAG,KAAK,QAAQ;AACnC,WAAO,EAAE,KAAK,MAAM,KAAK,MAAM,cAAc,KAAK,MAAM;AAAA,EAC1D;AAAA,EAEA,MAAM,KAAK,QAA4C;AACrD,UAAM,UAAU,KAAK,YAAY,MAAM;AACvC,QAAI;AACF,YAAM,UAAU,MAAM,GAAG,QAAQ,OAAO;AACxC,YAAM,UAA6B,CAAC;AACpC,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,WAAW,GAAG,EAAG;AAC3B,cAAM,UAAU,SAAS,GAAG,MAAM,IAAI,KAAK,KAAK;AAChD,YAAI;AACF,kBAAQ,KAAK,MAAM,KAAK,QAAQ,OAAO,CAAC;AAAA,QAC1C,QAAQ;AAAA,QAER;AAAA,MACF;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,UAAU,SAAsC;AACtD,UAAM,MAAM,OAAO,KAAK,KAAK,UAAU,OAAO,GAAG,MAAM,EAAE,SAAS,WAAW;AAC7E,UAAM,MAAM,WAAW,UAAU,KAAK,aAAa,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW;AACnF,WAAO,GAAG,GAAG,IAAI,GAAG;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,OAAe,YAAgD;AACzE,UAAM,CAAC,KAAK,GAAG,IAAI,MAAM,MAAM,GAAG;AAClC,QAAI,CAAC,OAAO,CAAC,IAAK,OAAM,IAAI,MAAM,8BAA8B;AAEhE,UAAM,WAAW,WAAW,UAAU,KAAK,aAAa,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW;AACxF,QAAI,aAAa,IAAK,OAAM,IAAI,MAAM,iCAAiC;AAEvE,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,OAAO,KAAK,KAAK,WAAW,EAAE,SAAS,MAAM,CAAC;AAAA,IACrE,QAAQ;AACN,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AAEA,QAAI,QAAQ,OAAO,YAAY;AAC7B,YAAM,IAAI,MAAM,wCAAwC,UAAU,cAAc,QAAQ,EAAE,IAAI;AAAA,IAChG;AACA,QAAI,KAAK,IAAI,IAAI,MAAO,QAAQ,KAAK;AACnC,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,mBACJ,KACA,WACA,SACoC;AACpC,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AACjE,UAAM,QAAQ,KAAK,UAAU,EAAE,GAAG,KAAK,IAAI,SAAS,aAAa,KAAK,IAAI,MAAM,CAAC;AAEjF,WAAO;AAAA,MACL,WAAW,GAAG,KAAK,OAAO,GAAG,KAAK,QAAQ,eAAe,KAAK;AAAA,MAC9D,QAAQ;AAAA,MACR,SAAS,SAAS,cAAc,EAAE,gBAAgB,QAAQ,YAAY,IAAI,EAAE,gBAAgB,2BAA2B;AAAA,MACvH;AAAA,MACA,aAAa,GAAG,KAAK,OAAO,GAAG,KAAK,QAAQ,gBAAgB,mBAAmB,GAAG,CAAC;AAAA,IACrF;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,KAAa,WAAyD;AAC/F,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AACjE,UAAM,QAAQ,KAAK,UAAU,EAAE,GAAG,KAAK,KAAK,IAAI,MAAM,CAAC;AACvD,WAAO;AAAA,MACL,aAAa,GAAG,KAAK,OAAO,GAAG,KAAK,QAAQ,eAAe,KAAK;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,KAAa,WAAoC;AAClE,UAAM,OAAO,MAAM,KAAK,qBAAqB,KAAK,SAAS;AAC3D,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,sBAAsB,KAAa,SAAiD;AACxF,UAAM,WAAW,WAAW,EAAE,QAAQ,MAAM,EAAE;AAC9C,UAAM,MAAM,KAAK,KAAK,UAAU,QAAQ;AACxC,UAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACvC,UAAM,OAAO;AAAA,MACX;AAAA,MACA,aAAa,SAAS;AAAA,MACtB,UAAU,SAAS;AAAA,MACnB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AACA,UAAM,GAAG,UAAU,KAAK,KAAK,YAAY,GAAG,KAAK,UAAU,IAAI,GAAG,MAAM;AACxE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,UAAkB,YAAoB,MAA+B;AACrF,QAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GAAG;AACnD,YAAM,IAAI,MAAM,2DAA2D,UAAU,GAAG;AAAA,IAC1F;AACA,UAAM,WAAW,KAAK,gBAAgB,UAAU,UAAU;AAC1D,UAAM,GAAG,MAAM,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACrD,UAAM,GAAG,UAAU,UAAU,IAAI;AAEjC,UAAM,EAAE,WAAW,IAAI,MAAM,OAAO,QAAa;AACjD,WAAO,WAAW,KAAK,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AAAA,EACpD;AAAA,EAEA,MAAM,sBACJ,UACA,OACiB;AACjB,UAAM,MAAM,KAAK,KAAK,UAAU,QAAQ;AACxC,QAAI,OAAyB,CAAC;AAC9B,QAAI;AACF,aAAO,KAAK,MAAM,MAAM,GAAG,SAAS,KAAK,KAAK,YAAY,GAAG,MAAM,CAAC;AAAA,IACtE,QAAQ;AACN,YAAM,IAAI,MAAM,mBAAmB,QAAQ,aAAa;AAAA,IAC1D;AACA,UAAM,YAAY,KAAK;AACvB,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,mBAAmB,QAAQ,sBAAsB;AAAA,IACnE;AAEA,UAAM,cAAc,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AACzE,UAAM,YAAY,KAAK,YAAY,SAAS;AAC5C,UAAM,GAAG,MAAM,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AAGtD,UAAM,MAAM,kBAAkB,SAAS;AACvC,QAAI;AACF,iBAAW,KAAK,aAAa;AAC3B,cAAM,WAAW,KAAK,gBAAgB,UAAU,EAAE,UAAU;AAC5D,cAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,gBAAM,MAAM,iBAAiB,QAAQ;AACrC,cAAI,GAAG,SAAS,MAAM;AACtB,cAAI,GAAG,OAAO,MAAM,QAAQ,CAAC;AAC7B,cAAI,KAAK,KAAK,EAAE,KAAK,MAAM,CAAC;AAAA,QAC9B,CAAC;AAAA,MACH;AAAA,IACF,UAAE;AACA,YAAM,IAAI,QAAc,CAAC,YAAY,IAAI,IAAI,MAAM,QAAQ,CAAC,CAAC;AAAA,IAC/D;AAGA,UAAM,GAAG,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACjD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,mBAAmB,UAAiC;AACxD,UAAM,GAAG,GAAG,KAAK,KAAK,UAAU,QAAQ,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC7E;AACF;;;AC/PO,IAAM,uBAAN,MAA2B;AAAA,EAIhC,YAA6B,QAA4B;AAA5B;AAH7B,SAAiB,QAAQ,oBAAI,IAAwB;AACrD,SAAiB,WAAW,oBAAI,IAAiC;AAAA,EAEP;AAAA;AAAA;AAAA;AAAA,EAM1D,MAAM,WAAW,KAAsC;AACrD,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,OAAmB,EAAE,YAAY,KAAK,YAAY,KAAK,GAAG,IAAI;AACpE,SAAK,MAAM,IAAI,KAAK,IAAI,IAAI;AAC5B,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,eAAe,IAAI;AAAA,MAC9C,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QAAQ,IAAwC;AACpD,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,eAAe,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AACxE,YAAI,MAAO,QAAO;AAAA,MACpB,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,KAAK,MAAM,IAAI,EAAE,KAAK;AAAA,EAC/B;AAAA,EAEA,MAAM,WAAW,IAAY,OAAwD;AACnF,UAAM,WAAW,MAAM,KAAK,QAAQ,EAAE;AACtC,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,SAAqB,EAAE,GAAG,UAAU,GAAG,OAAO,IAAI,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE;AAC7F,SAAK,MAAM,IAAI,IAAI,MAAM;AACzB,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,eAAe,QAAe,EAAE,OAAO,EAAE,GAAG,EAAE,CAAQ;AAAA,MACjF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,IAA2B;AAC1C,SAAK,MAAM,OAAO,EAAE;AACpB,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,eAAe,EAAE,OAAO,EAAE,GAAG,EAAE,CAAQ;AAAA,MAClE,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,KAAwD;AAC1E,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,OAA4B;AAAA,MAChC,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,GAAG;AAAA,IACL;AACA,SAAK,SAAS,IAAI,KAAK,IAAI,IAAI;AAC/B,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,yBAAyB,IAAI;AAAA,MACxD,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,IAAiD;AAChE,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,yBAAyB,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAClF,YAAI,MAAO,QAAO;AAAA,MACpB,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,KAAK,SAAS,IAAI,EAAE,KAAK;AAAA,EAClC;AAAA,EAEA,MAAM,cAAc,IAAY,OAA0E;AACxG,UAAM,WAAW,MAAM,KAAK,WAAW,EAAE;AACzC,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,SAA8B;AAAA,MAClC,GAAG;AAAA,MACH,GAAG;AAAA,MACH;AAAA,MACA,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACrC;AACA,SAAK,SAAS,IAAI,IAAI,MAAM;AAC5B,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,yBAAyB,QAAe,EAAE,OAAO,EAAE,GAAG,EAAE,CAAQ;AAAA,MAC3F,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,cAAc,IAA2B;AAC7C,SAAK,SAAS,OAAO,EAAE;AACvB,QAAI,KAAK,QAAQ;AACf,UAAI;AACF,cAAM,KAAK,OAAO,OAAO,yBAAyB,EAAE,OAAO,EAAE,GAAG,EAAE,CAAQ;AAAA,MAC5E,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;ACxLA,SAAS,cAAAA,mBAAkB;AAkCpB,SAAS,sBACd,YACA,SACA,OACA,OAA6B,CAAC,GACxB;AACN,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,aAAa,KAAK,cAAc;AAKtC,aAAW,KAAK,GAAG,QAAQ,qBAAqB,OAAO,KAAmB,QAAuB;AAC/F,QAAI;AACF,YAAM,EAAE,UAAU,UAAU,MAAM,OAAO,OAAO,IAAI,IAAI,QAAQ,CAAC;AACjE,UAAI,CAAC,YAAY,CAAC,YAAY,QAAQ,MAAM;AAC1C,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,4CAA4C,CAAC;AAC3E;AAAA,MACF;AAEA,YAAM,SAASA,YAAW;AAC1B,YAAM,MAAM,SAAS,SAAS,QAAQ,QAAQ,QAAQ;AAGtD,YAAM,MAAM,WAAW;AAAA,QACrB,IAAI;AAAA,QACJ;AAAA,QACA,MAAM;AAAA,QACN,WAAW;AAAA,QACX;AAAA,QACA,OAAO,SAAS;AAAA,QAChB;AAAA,QACA,KAAK;AAAA,QACL,QAAQ;AAAA,MACV,CAAC;AAGD,UAAI;AACJ,UAAI,SAAyB;AAC7B,UAAI,UAAkC,EAAE,gBAAgB,SAAS;AACjE,UAAI,YAAY;AAEhB,UAAI,QAAQ,oBAAoB;AAC9B,cAAM,OAAO,MAAM,QAAQ,mBAAmB,KAAK,cAAc,EAAE,aAAa,SAAS,CAAC;AAC1F,oBAAY,KAAK;AACjB,iBAAS,KAAK;AACd,YAAI,KAAK,QAAS,WAAU,KAAK;AACjC,oBAAY,KAAK;AAAA,MACnB,OAAO;AAEL,oBAAY,GAAG,QAAQ,eAAe,MAAM;AAAA,MAC9C;AAEA,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa,GAAG,QAAQ,UAAU,MAAM;AAAA,QAC1C;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,KAAK,GAAG,QAAQ,oBAAoB,OAAO,KAAmB,QAAuB;AAC9F,QAAI;AACF,YAAM,EAAE,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC;AACtC,UAAI,CAAC,QAAQ;AACX,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,qBAAqB,CAAC;AACpD;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,MAAM,QAAQ,MAAM;AACvC,UAAI,CAAC,MAAM;AACT,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,iBAAiB,CAAC;AAChD;AAAA,MACF;AAEA,YAAM,UAAU,MAAM,MAAM,WAAW,QAAQ;AAAA,QAC7C,QAAQ;AAAA,QACR,MAAM,QAAQ;AAAA,MAChB,CAAC;AAED,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,UACJ,MAAM,QAAS;AAAA,UACf,MAAM,QAAS;AAAA,UACf,MAAM,QAAS,QAAQ;AAAA,UACvB,UAAU,QAAS,aAAa;AAAA,UAChC,cAAc,QAAS,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,UAC5D,SAAS,QAAS,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,UACvD,MAAM,QAAS;AAAA,QACjB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,KAAK,GAAG,QAAQ,mBAAmB,OAAO,KAAmB,QAAuB;AAC7F,QAAI;AACF,YAAM,EAAE,UAAU,UAAU,WAAW,WAAW,cAAc,OAAO,QAAQ,SAAS,IAAI,IAAI,QAAQ,CAAC;AACzG,UAAI,CAAC,YAAY,CAAC,YAAY,CAAC,WAAW;AACxC,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,iDAAiD,CAAC;AAChF;AAAA,MACF;AAEA,YAAM,YAAY,KAAK,IAAI,gBAAgB,SAAS,OAAO;AAC3D,YAAM,cAAc,KAAK,KAAK,YAAY,SAAS;AAEnD,YAAM,SAASA,YAAW;AAC1B,YAAM,MAAM,SAAS,SAAS,QAAQ,QAAQ,QAAQ;AAGtD,YAAM,MAAM,WAAW;AAAA,QACrB,IAAI;AAAA,QACJ;AAAA,QACA,MAAM;AAAA,QACN,WAAW;AAAA,QACX,MAAM;AAAA,QACN,OAAO,SAAS;AAAA,QAChB;AAAA,QACA,KAAK;AAAA,QACL,QAAQ;AAAA,QACR,UAAU,WAAW,KAAK,UAAU,QAAQ,IAAI;AAAA,MAClD,CAAC;AAGD,UAAI;AACJ,UAAI,QAAQ,uBAAuB;AACjC,0BAAkB,MAAM,QAAQ,sBAAsB,KAAK,EAAE,aAAa,UAAU,SAAS,CAAC;AAE9F,YAAI,kBAAkB,WAAW,OAAQ,QAAgB,iBAAiB,YAAY;AACpF,UAAC,QAAgB,aAAa,iBAAiB,GAAG;AAAA,QACpD;AAAA,MACF;AAEA,YAAM,WAAW,mBAAmBA,YAAW,EAAE,QAAQ,MAAM,EAAE;AACjE,YAAM,cAAcA,YAAW;AAC/B,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,aAAa,GAAI,EAAE,YAAY;AAEvE,YAAM,MAAM,cAAc;AAAA,QACxB,IAAI;AAAA,QACJ,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,cAAc;AAAA,QACd,cAAc;AAAA,QACd,mBAAmB;AAAA,QACnB,OAAO,SAAS;AAAA,QAChB;AAAA,QACA,UAAU,WAAW,KAAK,UAAU,QAAQ,IAAI;AAAA,QAChD,QAAQ;AAAA,QACR,YAAY;AAAA,MACd,CAAC;AAED,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,IAAI,GAAG,QAAQ,+CAA+C,OAAO,KAAmB,QAAuB;AACxH,QAAI;AACF,YAAM,EAAE,UAAU,YAAY,cAAc,IAAI,IAAI;AACpD,YAAM,aAAa,SAAS,eAAe,EAAE;AAC7C,UAAI,CAAC,YAAY,MAAM,UAAU,GAAG;AAClC,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uCAAuC,CAAC;AACtE;AAAA,MACF;AAEA,YAAM,UAAU,MAAM,MAAM,WAAW,QAAQ;AAC/C,UAAI,CAAC,SAAS;AACZ,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,2BAA2B,CAAC;AAC1D;AAAA,MACF;AAGA,YAAM,QAAS,IAAI,QAAQ,gBAAgB,KAAK;AAChD,UAAI,QAAQ,gBAAgB,UAAU,QAAQ,cAAc;AAC1D,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uBAAuB,CAAC;AACtD;AAAA,MACF;AAGA,UAAI;AACJ,UAAI,IAAI,SAAS;AACf,eAAO,MAAM,IAAI,QAAQ;AAAA,MAC3B,WAAW,OAAO,SAAS,IAAI,IAAI,GAAG;AACpC,eAAO,IAAI;AAAA,MACb,WAAW,IAAI,gBAAgB,aAAa;AAC1C,eAAO,OAAO,KAAK,IAAI,IAAI;AAAA,MAC7B,OAAO;AACL,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uBAAuB,CAAC;AACtD;AAAA,MACF;AAGA,UAAI,OAAO;AACX,UAAI,QAAQ,aAAa;AACvB,eAAO,MAAM,QAAQ,YAAY,UAAU,aAAa,GAAG,IAAI;AAAA,MACjE;AAGA,YAAM,eAA4D,KAAK,MAAM,QAAQ,SAAS,IAAI;AAClG,mBAAa,KAAK,EAAE,YAAY,KAAK,CAAC;AACtC,YAAM,kBAAkB,QAAQ,mBAAmB,KAAK;AACxD,YAAM,gBAAgB,QAAQ,iBAAiB,KAAK,KAAK;AACzD,YAAM,MAAM,cAAc,UAAU;AAAA,QAClC,iBAAiB;AAAA,QACjB,eAAe;AAAA,QACf,OAAO,KAAK,UAAU,YAAY;AAAA,MACpC,CAAC;AAED,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA,eAAe,KAAK;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,KAAK,GAAG,QAAQ,sCAAsC,OAAO,KAAmB,QAAuB;AAChH,QAAI;AACF,YAAM,EAAE,SAAS,IAAI,IAAI;AACzB,YAAM,UAAU,MAAM,MAAM,WAAW,QAAQ;AAC/C,UAAI,CAAC,SAAS;AACZ,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,2BAA2B,CAAC;AAC1D;AAAA,MACF;AAEA,YAAM,MAAM,cAAc,UAAU,EAAE,QAAQ,aAAa,CAAC;AAE5D,YAAM,gBAAiB,IAAI,MAAM,SAAS,CAAC;AAC3C,YAAM,kBAAkB,cAAc,IAAI,QAAM;AAAA,QAC9C,YAAY,EAAE,aAAa;AAAA,QAC3B,MAAM,EAAE;AAAA,MACV,EAAE;AAEF,UAAI,WAAW,QAAQ;AACvB,UAAI,QAAQ,uBAAuB;AACjC,mBAAW,MAAM,QAAQ,sBAAsB,UAAU,eAAe;AAAA,MAC1E;AAGA,YAAM,MAAM,WAAW,QAAQ,SAAS,EAAE,QAAQ,aAAa,KAAK,SAAS,CAAC;AAC9E,YAAM,MAAM,cAAc,UAAU,EAAE,QAAQ,YAAY,CAAC;AAE3D,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,UACJ,QAAQ,QAAQ;AAAA,UAChB,KAAK;AAAA,UACL,MAAM,QAAQ;AAAA,UACd,UAAU,QAAQ,aAAa;AAAA,UAC/B,KAAK,GAAG,QAAQ,UAAU,QAAQ,OAAO;AAAA,QAC3C;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,IAAI,GAAG,QAAQ,sCAAsC,OAAO,KAAmB,QAAuB;AAC/G,QAAI;AACF,YAAM,EAAE,SAAS,IAAI,IAAI;AACzB,YAAM,UAAU,MAAM,MAAM,WAAW,QAAQ;AAC/C,UAAI,CAAC,SAAS;AACZ,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,2BAA2B,CAAC;AAC1D;AAAA,MACF;AAEA,YAAM,iBAAiB,QAAQ,mBAAmB;AAClD,YAAM,eAAe,QAAQ,iBAAiB;AAC9C,YAAM,kBAAkB,QAAQ,aAAa,IACzC,KAAK,IAAI,KAAK,KAAK,MAAO,eAAe,QAAQ,aAAc,GAAG,CAAC,IACnE;AAEJ,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,UACJ,UAAU,QAAQ;AAAA,UAClB,QAAQ,QAAQ;AAAA,UAChB,UAAU,QAAQ;AAAA,UAClB,WAAW,QAAQ;AAAA,UACnB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB;AAAA,UACA;AAAA,UACA,QAAQ,QAAQ;AAAA,UAChB,WAAW,QAAQ;AAAA,UACnB,WAAW,QAAQ;AAAA,QACrB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,IAAI,GAAG,QAAQ,sBAAsB,OAAO,KAAmB,QAAuB;AAC/F,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,IAAI;AACvB,YAAM,OAAO,MAAM,MAAM,QAAQ,MAAM;AACvC,UAAI,CAAC,QAAQ,KAAK,WAAW,aAAa;AACxC,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,kCAAkC,CAAC;AACjE;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,QAAQ,sBAAsB;AAChC,cAAM,OAAO,MAAM,QAAQ,qBAAqB,KAAK,KAAK,YAAY;AACtE,cAAM,KAAK;AAAA,MACb,WAAW,QAAQ,cAAc;AAC/B,cAAM,MAAM,QAAQ,aAAa,KAAK,KAAK,YAAY;AAAA,MACzD,OAAO;AACL,cAAM,GAAG,QAAQ,gBAAgB,mBAAmB,KAAK,GAAG,CAAC;AAAA,MAC/D;AAEA,UAAI,KAAK,EAAE,IAAI,CAAC;AAAA,IAClB,SAAS,KAAU;AACjB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,iBAAiB,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAKD,aAAW,IAAI,GAAG,QAAQ,sBAAsB,OAAO,KAAmB,QAAuB;AAC/F,QAAI;AACF,YAAM,EAAE,MAAM,IAAI,IAAI;AACtB,YAAM,eAAe;AACrB,UAAI,CAAC,aAAa,aAAa;AAC7B,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,qDAAqD,CAAC;AACpF;AAAA,MACF;AAEA,YAAM,UAAU,aAAa,YAAY,OAAO,KAAK;AACrD,UAAI;AACJ,UAAI,IAAI,SAAS;AACf,eAAO,MAAM,IAAI,QAAQ;AAAA,MAC3B,WAAW,OAAO,SAAS,IAAI,IAAI,GAAG;AACpC,eAAO,IAAI;AAAA,MACb,OAAO;AACL,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,uBAAuB,CAAC;AACtD;AAAA,MACF;AAEA,YAAM,QAAQ,OAAO,QAAQ,GAAG,MAAM,EAAE,aAAa,QAAQ,GAAG,CAAC;AACjE,UAAI,KAAK,EAAE,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;AAAA,IACvC,SAAS,KAAU;AACjB,YAAM,aAAa,IAAI,SAAS,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,WAAW,IAAI,MAAM;AAClG,UAAI,OAAO,UAAU,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,gBAAgB,CAAC;AAAA,IACvE;AAAA,EACF,CAAC;AAKD,aAAW,IAAI,GAAG,QAAQ,sBAAsB,OAAO,KAAmB,QAAuB;AAC/F,QAAI;AACF,YAAM,EAAE,MAAM,IAAI,IAAI;AACtB,YAAM,eAAe;AACrB,UAAI,CAAC,aAAa,aAAa;AAC7B,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,mDAAmD,CAAC;AAClF;AAAA,MACF;AAEA,YAAM,UAAU,aAAa,YAAY,OAAO,KAAK;AACrD,YAAM,OAAO,MAAM,QAAQ,SAAS,QAAQ,CAAC;AAE7C,UAAI,OAAO,gBAAgB,QAAQ,MAAM,0BAA0B;AACnE,UAAI,OAAO,kBAAkB,OAAO,KAAK,UAAU,CAAC;AAGpD,MAAC,IAAY,KAAK,IAAI;AAAA,IACxB,SAAS,KAAU;AACjB,YAAM,aAAa,IAAI,SAAS,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,WAAW,IAAI,MAAM;AAClG,UAAI,OAAO,UAAU,EAAE,KAAK,EAAE,OAAO,IAAI,WAAW,kBAAkB,CAAC;AAAA,IACzE;AAAA,EACF,CAAC;AACH;AAMA,SAAS,SAAS,OAAe,QAAgB,UAA0B;AACzE,QAAM,MAAM,SAAS,SAAS,GAAG,IAAI,MAAM,SAAS,MAAM,GAAG,EAAE,IAAI,IAAI;AACvE,SAAO,GAAG,KAAK,IAAI,MAAM,GAAG,GAAG;AACjC;;;AC5cA,SAAS,cAAc,aAAa;AAkB7B,IAAM,aAAa,aAAa,OAAO;AAAA,EAC5C,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe,CAAC,QAAQ,aAAa,QAAQ,UAAU,YAAY;AAAA,EAEnE,QAAQ;AAAA,IACN,IAAI,MAAM,KAAK;AAAA,MACb,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,KAAK,MAAM,KAAK;AAAA,MACd,OAAO;AAAA,MACP,UAAU;AAAA,MACV,YAAY;AAAA,IACd,CAAC;AAAA,IAED,MAAM,MAAM,KAAK;AAAA,MACf,OAAO;AAAA,MACP,UAAU;AAAA,MACV,YAAY;AAAA,IACd,CAAC;AAAA,IAED,WAAW,MAAM,KAAK;AAAA,MACpB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,MAAM,MAAM,OAAO;AAAA,MACjB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,OAAO,MAAM,OAAO;AAAA,MAClB,OAAO;AAAA,MACP,SAAS;AAAA,QACP,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,QAC/B,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,QACnC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,QACnC,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,QACrC,EAAE,OAAO,QAAQ,OAAO,OAAO;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,IAED,QAAQ,MAAM,KAAK;AAAA,MACjB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,KAAK,MAAM,OAAO;AAAA,MAChB,OAAO;AAAA,MACP,SAAS;AAAA,QACP,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,QACrC,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,MAC/C;AAAA,IACF,CAAC;AAAA,IAED,QAAQ,MAAM,OAAO;AAAA,MACnB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,SAAS;AAAA,QACP,EAAE,OAAO,kBAAkB,OAAO,UAAU;AAAA,QAC5C,EAAE,OAAO,aAAa,OAAO,YAAY;AAAA,QACzC,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,MACvC;AAAA,IACF,CAAC;AAAA,IAED,MAAM,MAAM,KAAK;AAAA,MACf,OAAO;AAAA,IACT,CAAC;AAAA,IAED,UAAU,MAAM,KAAK;AAAA,MACnB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,UAAU,MAAM,KAAK;AAAA,MACnB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,YAAY,MAAM,SAAS;AAAA,MACzB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,YAAY,MAAM,SAAS;AAAA,MACzB,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF,CAAC;;;AC3GD,SAAS,gBAAAC,eAAc,SAAAC,cAAa;AAW7B,IAAM,sBAAsBD,cAAa,OAAO;AAAA,EACrD,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe,CAAC,YAAY,UAAU,mBAAmB,gBAAgB,YAAY;AAAA,EAErF,QAAQ;AAAA,IACN,IAAIC,OAAM,KAAK;AAAA,MACb,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,SAASA,OAAM,KAAK;AAAA,MAClB,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,KAAKA,OAAM,KAAK;AAAA,MACd,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,UAAUA,OAAM,KAAK;AAAA,MACnB,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,WAAWA,OAAM,KAAK;AAAA,MACpB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,YAAYA,OAAM,OAAO;AAAA,MACvB,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,YAAYA,OAAM,OAAO;AAAA,MACvB,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,cAAcA,OAAM,OAAO;AAAA,MACzB,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,iBAAiBA,OAAM,OAAO;AAAA,MAC5B,OAAO;AAAA,IACT,CAAC;AAAA,IAED,eAAeA,OAAM,OAAO;AAAA,MAC1B,OAAO;AAAA,IACT,CAAC;AAAA,IAED,OAAOA,OAAM,KAAK;AAAA,MAChB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,cAAcA,OAAM,KAAK;AAAA,MACvB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,mBAAmBA,OAAM,KAAK;AAAA,MAC5B,OAAO;AAAA,IACT,CAAC;AAAA,IAED,OAAOA,OAAM,KAAK;AAAA,MAChB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,QAAQA,OAAM,KAAK;AAAA,MACjB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,UAAUA,OAAM,KAAK;AAAA,MACnB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,QAAQA,OAAM,OAAO;AAAA,MACnB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,SAAS;AAAA,QACP,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,QAC7C,EAAE,OAAO,cAAc,OAAO,aAAa;AAAA,QAC3C,EAAE,OAAO,aAAa,OAAO,YAAY;AAAA,QACzC,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,QACnC,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,MACvC;AAAA,IACF,CAAC;AAAA,IAED,YAAYA,OAAM,SAAS;AAAA,MACzB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,YAAYA,OAAM,SAAS;AAAA,MACzB,OAAO;AAAA,IACT,CAAC;AAAA,IAED,YAAYA,OAAM,SAAS;AAAA,MACzB,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF,CAAC;;;ACpDM,IAAM,uBAAN,MAA6C;AAAA,EASlD,YAAY,UAAuC,CAAC,GAAG;AARvD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAGP,SAAQ,UAAkC;AAC1C,SAAQ,QAAqC;AAG3C,SAAK,UAAU,EAAE,SAAS,SAAS,GAAG,QAAQ;AAAA,EAChD;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,UAAM,UAAU,KAAK,QAAQ;AAC7B,QAAI,YAAY,MAAM;AAEpB,YAAM,EAAE,kBAAAC,kBAAiB,IAAI,MAAM;AACnC,YAAM,SAAS,KAAK,QAAQ;AAC5B,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,oEAAoE;AAAA,MACtF;AACA,WAAK,UAAU,IAAIA,kBAAiB,MAAM;AAAA,IAC5C,OAAO;AACL,YAAM,UAAU,KAAK,QAAQ,OAAO,WAAW;AAC/C,YAAM,WAAW,KAAK,QAAQ,YAAY;AAC1C,WAAK,UAAU,IAAI,oBAAoB,EAAE,SAAS,UAAU,GAAG,KAAK,QAAQ,MAAM,CAAC;AAAA,IACrF;AAEA,QAAI,gBAAgB,gBAAgB,KAAK,OAAO;AAChD,QAAI,OAAO,KAAK,oCAAoC,OAAO,kBAAkB;AAG7E,QAAI;AACF,UAAI,WAAuC,UAAU,EAAE,SAAS;AAAA,QAC9D,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,SAAS;AAAA,QACT,MAAM;AAAA,QACN,OAAO;AAAA,QACP,SAAS,CAAC,YAAY,mBAAmB;AAAA,MAC3C,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC7C,QAAI,KAAK,QAAQ,mBAAmB,MAAO;AAE3C,QAAI,KAAK,gBAAgB,YAAY;AACnC,UAAI,aAAiC;AACrC,UAAI;AACF,qBAAa,IAAI,WAAwB,aAAa;AAAA,MACxD,QAAQ;AAAA,MAER;AAEA,UAAI,CAAC,cAAc,CAAC,KAAK,SAAS;AAChC,YAAI,OAAO;AAAA,UACT;AAAA,QAEF;AACA;AAAA,MACF;AAGA,UAAI,SAA6B;AACjC,UAAI;AACF,iBAAS,IAAI,WAAwB,UAAU;AAAA,MACjD,QAAQ;AAAA,MAER;AACA,WAAK,QAAQ,IAAI,qBAAqB,MAAM;AAE5C,4BAAsB,YAAY,KAAK,SAAS,KAAK,OAAO;AAAA,QAC1D,UAAU,KAAK,QAAQ,YAAY;AAAA,QACnC,cAAc,KAAK,QAAQ;AAAA,QAC3B,YAAY,KAAK,QAAQ;AAAA,MAC3B,CAAC;AAED,UAAI,OAAO,KAAK,sDAAsD,KAAK,QAAQ,YAAY,kBAAkB;AAAA,IACnH,CAAC;AAAA,EACH;AACF;;;ACjJA;","names":["randomUUID","ObjectSchema","Field","S3StorageAdapter"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-storage",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton",
6
6
  "type": "module",
@@ -14,13 +14,50 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@objectstack/core": "4.0.3",
18
- "@objectstack/spec": "4.0.3"
17
+ "@objectstack/core": "4.0.5",
18
+ "@objectstack/spec": "4.0.5"
19
+ },
20
+ "peerDependencies": {
21
+ "@aws-sdk/client-s3": "^3.0.0",
22
+ "@aws-sdk/s3-request-presigner": "^3.0.0"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "@aws-sdk/client-s3": {
26
+ "optional": true
27
+ },
28
+ "@aws-sdk/s3-request-presigner": {
29
+ "optional": true
30
+ }
19
31
  },
20
32
  "devDependencies": {
21
- "@types/node": "^25.6.0",
22
- "typescript": "^6.0.2",
23
- "vitest": "^4.1.4"
33
+ "@types/node": "^25.6.2",
34
+ "typescript": "^6.0.3",
35
+ "vitest": "^4.1.5"
36
+ },
37
+ "keywords": [
38
+ "objectstack",
39
+ "service",
40
+ "storage",
41
+ "s3",
42
+ "files"
43
+ ],
44
+ "author": "ObjectStack",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/objectstack-ai/framework.git",
48
+ "directory": "packages/services/service-storage"
49
+ },
50
+ "homepage": "https://objectstack.ai/docs",
51
+ "bugs": "https://github.com/objectstack-ai/framework/issues",
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "README.md"
58
+ ],
59
+ "engines": {
60
+ "node": ">=18.0.0"
24
61
  },
25
62
  "scripts": {
26
63
  "build": "tsup --config ../../../tsup.config.ts",
@@ -1,22 +0,0 @@
1
-
2
- > @objectstack/service-storage@4.0.3 build /home/runner/work/framework/framework/packages/services/service-storage
3
- > tsup --config ../../../tsup.config.ts
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- ESM Build start
12
- CJS Build start
13
- CJS dist/index.cjs 5.36 KB
14
- CJS dist/index.cjs.map 11.56 KB
15
- CJS ⚡️ Build success in 112ms
16
- ESM dist/index.js 4.04 KB
17
- ESM dist/index.js.map 10.95 KB
18
- ESM ⚡️ Build success in 119ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 11080ms
21
- DTS dist/index.d.ts 4.20 KB
22
- DTS dist/index.d.cts 4.20 KB
package/CHANGELOG.md DELETED
@@ -1,169 +0,0 @@
1
- # @objectstack/service-storage
2
-
3
- ## 4.0.3
4
-
5
- ### Patch Changes
6
-
7
- - @objectstack/spec@4.0.3
8
- - @objectstack/core@4.0.3
9
-
10
- ## 4.0.2
11
-
12
- ### Patch Changes
13
-
14
- - Updated dependencies [5f659e9]
15
- - @objectstack/spec@4.0.2
16
- - @objectstack/core@4.0.2
17
-
18
- ## 4.0.0
19
-
20
- ### Patch Changes
21
-
22
- - Updated dependencies [f08ffc3]
23
- - Updated dependencies [e0b0a78]
24
- - @objectstack/spec@4.0.0
25
- - @objectstack/core@4.0.0
26
-
27
- ## 3.3.1
28
-
29
- ### Patch Changes
30
-
31
- - @objectstack/spec@3.3.1
32
- - @objectstack/core@3.3.1
33
-
34
- ## 3.3.0
35
-
36
- ### Patch Changes
37
-
38
- - @objectstack/spec@3.3.0
39
- - @objectstack/core@3.3.0
40
-
41
- ## 3.2.9
42
-
43
- ### Patch Changes
44
-
45
- - @objectstack/spec@3.2.9
46
- - @objectstack/core@3.2.9
47
-
48
- ## 3.2.8
49
-
50
- ### Patch Changes
51
-
52
- - @objectstack/spec@3.2.8
53
- - @objectstack/core@3.2.8
54
-
55
- ## 3.2.7
56
-
57
- ### Patch Changes
58
-
59
- - @objectstack/spec@3.2.7
60
- - @objectstack/core@3.2.7
61
-
62
- ## 3.2.6
63
-
64
- ### Patch Changes
65
-
66
- - @objectstack/spec@3.2.6
67
- - @objectstack/core@3.2.6
68
-
69
- ## 3.2.5
70
-
71
- ### Patch Changes
72
-
73
- - @objectstack/spec@3.2.5
74
- - @objectstack/core@3.2.5
75
-
76
- ## 3.2.4
77
-
78
- ### Patch Changes
79
-
80
- - @objectstack/spec@3.2.4
81
- - @objectstack/core@3.2.4
82
-
83
- ## 3.2.3
84
-
85
- ### Patch Changes
86
-
87
- - @objectstack/spec@3.2.3
88
- - @objectstack/core@3.2.3
89
-
90
- ## 3.2.2
91
-
92
- ### Patch Changes
93
-
94
- - Updated dependencies [46defbb]
95
- - @objectstack/spec@3.2.2
96
- - @objectstack/core@3.2.2
97
-
98
- ## 3.2.1
99
-
100
- ### Patch Changes
101
-
102
- - Updated dependencies [850b546]
103
- - @objectstack/spec@3.2.1
104
- - @objectstack/core@3.2.1
105
-
106
- ## 3.2.0
107
-
108
- ### Patch Changes
109
-
110
- - Updated dependencies [5901c29]
111
- - @objectstack/spec@3.2.0
112
- - @objectstack/core@3.2.0
113
-
114
- ## 3.1.1
115
-
116
- ### Patch Changes
117
-
118
- - Updated dependencies [953d667]
119
- - @objectstack/spec@3.1.1
120
- - @objectstack/core@3.1.1
121
-
122
- ## 3.1.0
123
-
124
- ### Patch Changes
125
-
126
- - Updated dependencies [0088830]
127
- - @objectstack/spec@3.1.0
128
- - @objectstack/core@3.1.0
129
-
130
- ## 3.0.11
131
-
132
- ### Patch Changes
133
-
134
- - Updated dependencies [92d9d99]
135
- - @objectstack/spec@3.0.11
136
- - @objectstack/core@3.0.11
137
-
138
- ## 3.0.10
139
-
140
- ### Patch Changes
141
-
142
- - Updated dependencies [d1e5d31]
143
- - @objectstack/spec@3.0.10
144
- - @objectstack/core@3.0.10
145
-
146
- ## 3.0.9
147
-
148
- ### Patch Changes
149
-
150
- - Updated dependencies [15e0df6]
151
- - @objectstack/spec@3.0.9
152
- - @objectstack/core@3.0.9
153
-
154
- ## 3.0.8
155
-
156
- ### Patch Changes
157
-
158
- - Updated dependencies [5a968a2]
159
- - @objectstack/spec@3.0.8
160
- - @objectstack/core@3.0.8
161
-
162
- ## 3.0.7
163
-
164
- ### Patch Changes
165
-
166
- - Updated dependencies [0119bd7]
167
- - Updated dependencies [5426bdf]
168
- - @objectstack/spec@3.0.7
169
- - @objectstack/core@3.0.7
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- export { StorageServicePlugin } from './storage-service-plugin.js';
4
- export type { StorageServicePluginOptions } from './storage-service-plugin.js';
5
- export { LocalStorageAdapter } from './local-storage-adapter.js';
6
- export type { LocalStorageAdapterOptions } from './local-storage-adapter.js';
7
- export { S3StorageAdapter } from './s3-storage-adapter.js';
8
- export type { S3StorageAdapterOptions } from './s3-storage-adapter.js';
@@ -1,91 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, afterEach } from 'vitest';
4
- import { promises as fs } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { tmpdir } from 'node:os';
7
- import { LocalStorageAdapter } from './local-storage-adapter';
8
- import type { IStorageService } from '@objectstack/spec/contracts';
9
-
10
- describe('LocalStorageAdapter', () => {
11
- let rootDir: string;
12
- let adapter: LocalStorageAdapter;
13
-
14
- const createTempDir = async () => {
15
- rootDir = join(tmpdir(), `os-test-storage-${Date.now()}-${Math.random().toString(36).slice(2)}`);
16
- await fs.mkdir(rootDir, { recursive: true });
17
- adapter = new LocalStorageAdapter({ rootDir });
18
- };
19
-
20
- afterEach(async () => {
21
- if (rootDir) {
22
- await fs.rm(rootDir, { recursive: true, force: true });
23
- }
24
- });
25
-
26
- it('should implement IStorageService contract', async () => {
27
- await createTempDir();
28
- const storage: IStorageService = adapter;
29
- expect(typeof storage.upload).toBe('function');
30
- expect(typeof storage.download).toBe('function');
31
- expect(typeof storage.delete).toBe('function');
32
- expect(typeof storage.exists).toBe('function');
33
- expect(typeof storage.getInfo).toBe('function');
34
- expect(typeof storage.list).toBe('function');
35
- });
36
-
37
- it('should upload and download a file', async () => {
38
- await createTempDir();
39
- const content = Buffer.from('hello world');
40
- await adapter.upload('test.txt', content);
41
-
42
- const downloaded = await adapter.download('test.txt');
43
- expect(downloaded.toString()).toBe('hello world');
44
- });
45
-
46
- it('should create nested directories automatically', async () => {
47
- await createTempDir();
48
- await adapter.upload('deep/nested/file.txt', Buffer.from('nested'));
49
- const downloaded = await adapter.download('deep/nested/file.txt');
50
- expect(downloaded.toString()).toBe('nested');
51
- });
52
-
53
- it('should check file existence', async () => {
54
- await createTempDir();
55
- expect(await adapter.exists('missing.txt')).toBe(false);
56
- await adapter.upload('exists.txt', Buffer.from('yes'));
57
- expect(await adapter.exists('exists.txt')).toBe(true);
58
- });
59
-
60
- it('should delete a file', async () => {
61
- await createTempDir();
62
- await adapter.upload('deleteme.txt', Buffer.from('bye'));
63
- await adapter.delete('deleteme.txt');
64
- expect(await adapter.exists('deleteme.txt')).toBe(false);
65
- });
66
-
67
- it('should get file info', async () => {
68
- await createTempDir();
69
- await adapter.upload('info.txt', Buffer.from('metadata'));
70
- const info = await adapter.getInfo('info.txt');
71
- expect(info.key).toBe('info.txt');
72
- expect(info.size).toBe(8); // 'metadata'.length
73
- expect(info.lastModified).toBeInstanceOf(Date);
74
- });
75
-
76
- it('should list files in a directory', async () => {
77
- await createTempDir();
78
- await adapter.upload('docs/a.txt', Buffer.from('a'));
79
- await adapter.upload('docs/b.txt', Buffer.from('bb'));
80
- const files = await adapter.list('docs');
81
- expect(files).toHaveLength(2);
82
- const keys = files.map(f => f.key).sort();
83
- expect(keys).toEqual(['docs/a.txt', 'docs/b.txt']);
84
- });
85
-
86
- it('should return empty array when listing non-existent directory', async () => {
87
- await createTempDir();
88
- const files = await adapter.list('nonexistent');
89
- expect(files).toEqual([]);
90
- });
91
- });