@rebasepro/server 0.12.1-canary.g389e9b2 → 0.12.1-canary.g4e7bcbf
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/{S3StorageController-CK7r5yZb.js → S3StorageController-B2EDXXMH.js} +8 -2
- package/dist/{S3StorageController-CK7r5yZb.js.map → S3StorageController-B2EDXXMH.js.map} +1 -1
- package/dist/api/rest/write-validation.d.ts +22 -0
- package/dist/auth/bearer-token.d.ts +26 -0
- package/dist/auth/middleware.d.ts +2 -0
- package/dist/auth/require-auth.d.ts +39 -0
- package/dist/{auth-DQv2_Z3S.js → auth-B9AJsH3u.js} +93 -65
- package/dist/auth-B9AJsH3u.js.map +1 -0
- package/dist/backend-CIxN4FVm.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +140 -26
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +1 -5
- package/dist/{openapi-generator-5re-77gM.js → openapi-generator-Bul4BS5n.js} +2 -2
- package/dist/{openapi-generator-5re-77gM.js.map → openapi-generator-Bul4BS5n.js.map} +1 -1
- package/dist/{src-By2xYed6.js → src-1LrhRnw9.js} +114 -17
- package/dist/{src-By2xYed6.js.map → src-1LrhRnw9.js.map} +1 -1
- package/package.json +5 -5
- package/dist/auth-DQv2_Z3S.js.map +0 -1
|
@@ -202,7 +202,13 @@ var S3StorageController = class {
|
|
|
202
202
|
Bucket: resolvedBucket,
|
|
203
203
|
Key: resolvedPath
|
|
204
204
|
});
|
|
205
|
-
|
|
205
|
+
try {
|
|
206
|
+
await client.send(command);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const s3Error = error;
|
|
209
|
+
if (s3Error.name === "NoSuchKey" || s3Error.$metadata?.httpStatusCode === 404) return;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
206
212
|
}
|
|
207
213
|
async listObjects(prefix, options) {
|
|
208
214
|
const resolvedBucket = this.getBucket(options?.bucket);
|
|
@@ -240,4 +246,4 @@ var S3StorageController = class {
|
|
|
240
246
|
//#endregion
|
|
241
247
|
export { S3StorageController_exports as n, S3StorageController as t };
|
|
242
248
|
|
|
243
|
-
//# sourceMappingURL=S3StorageController-
|
|
249
|
+
//# sourceMappingURL=S3StorageController-B2EDXXMH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"S3StorageController-CK7r5yZb.js","names":[],"sources":["../src/storage/S3StorageController.ts"],"sourcesContent":["/**\n * S3-compatible storage controller (works with AWS S3 and MinIO)\n */\n\nimport type { S3Client as S3ClientType, } from \"@aws-sdk/client-s3\";\nimport { DEFAULT_MAX_FILE_SIZE, S3StorageConfig, StorageController } from \"./types\";\nimport {\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference,\n UploadFileProps,\n UploadFileResult\n} from \"@rebasepro/types\";\n\nlet _s3Module: typeof import(\"@aws-sdk/client-s3\") | undefined;\nlet _presignerModule: typeof import(\"@aws-sdk/s3-request-presigner\") | undefined;\n\nasync function loadS3() {\n if (!_s3Module) {\n try {\n _s3Module = await import(\"@aws-sdk/client-s3\");\n } catch {\n throw new Error(\n \"@aws-sdk/client-s3 is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _s3Module;\n}\n\nasync function loadPresigner() {\n if (!_presignerModule) {\n try {\n _presignerModule = await import(\"@aws-sdk/s3-request-presigner\");\n } catch {\n throw new Error(\n \"@aws-sdk/s3-request-presigner is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _presignerModule;\n}\n\n/**\n * S3-compatible storage implementation\n * Works with AWS S3 and MinIO (with forcePathStyle option)\n */\nexport class S3StorageController implements StorageController {\n private config: S3StorageConfig;\n private _client: S3ClientType | undefined;\n\n constructor(config: S3StorageConfig) {\n this.config = config;\n }\n\n /**\n * Lazily create and cache the S3 client on first use\n */\n private async getClient(): Promise<S3ClientType> {\n if (!this._client) {\n const s3 = await loadS3();\n this._client = new s3.S3Client({\n region: this.config.region || \"us-east-1\",\n endpoint: this.config.endpoint,\n forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint, // Auto-enable for custom endpoints (MinIO)\n credentials: {\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey\n }\n });\n }\n return this._client;\n }\n\n getType(): \"s3\" {\n return \"s3\";\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n if (file.size > maxSize) {\n throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);\n }\n\n if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {\n if (!this.config.allowedMimeTypes.includes(file.type)) {\n throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(\", \")}`);\n }\n }\n }\n\n /**\n * Get the bucket name - either from parameter or config\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for S3 it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps): Promise<UploadFileResult> {\n this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n\n // Convert File to Buffer\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.PutObjectCommand({\n Bucket: usedBucket,\n Key: key,\n Body: buffer,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n });\n\n await client.send(command);\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\n };\n }\n\n /**\n * Flatten nested metadata to string values (S3 requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle s3:// and gs:// URLs for backward compatibility\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const presigner = await loadPresigner();\n const client = await this.getClient();\n\n // First check if the object exists and get metadata\n const headCommand = new s3.HeadObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const headResult = await client.send(headCommand);\n\n // Generate a signed URL\n const getCommand = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: headResult.ContentLength || 0,\n contentType: headResult.ContentType || \"application/octet-stream\",\n customMetadata: headResult.Metadata || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NotFound\" || s3Error.$metadata?.httpStatusCode === 404) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw error;\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const response = await client.send(command);\n\n if (!response.Body) {\n return null;\n }\n\n // Convert stream to buffer\n const chunks: Uint8Array[] = [];\n for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n const contentType = response.ContentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([buffer], { type: contentType });\n return new File([blob], fileName, { type: contentType });\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return null;\n }\n throw error;\n }\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.DeleteObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n await client.send(command);\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: prefix || undefined,\n MaxKeys: options?.maxResults ?? 1000,\n ContinuationToken: options?.pageToken,\n Delimiter: \"/\" // This gives us folder-like behavior\n });\n\n const response = await client.send(command);\n\n const items: StorageReference[] = (response.Contents || []).map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${obj.Key}`\n }));\n\n const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({\n bucket: resolvedBucket,\n fullPath: prefix.Prefix || \"\",\n name: (prefix.Prefix || \"\").replace(/\\/$/, \"\").split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`\n }));\n\n return {\n items,\n prefixes,\n nextPageToken: response.NextContinuationToken\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"S3StorageController-B2EDXXMH.js","names":[],"sources":["../src/storage/S3StorageController.ts"],"sourcesContent":["/**\n * S3-compatible storage controller (works with AWS S3 and MinIO)\n */\n\nimport type { S3Client as S3ClientType, } from \"@aws-sdk/client-s3\";\nimport { DEFAULT_MAX_FILE_SIZE, S3StorageConfig, StorageController } from \"./types\";\nimport {\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference,\n UploadFileProps,\n UploadFileResult\n} from \"@rebasepro/types\";\n\nlet _s3Module: typeof import(\"@aws-sdk/client-s3\") | undefined;\nlet _presignerModule: typeof import(\"@aws-sdk/s3-request-presigner\") | undefined;\n\nasync function loadS3() {\n if (!_s3Module) {\n try {\n _s3Module = await import(\"@aws-sdk/client-s3\");\n } catch {\n throw new Error(\n \"@aws-sdk/client-s3 is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _s3Module;\n}\n\nasync function loadPresigner() {\n if (!_presignerModule) {\n try {\n _presignerModule = await import(\"@aws-sdk/s3-request-presigner\");\n } catch {\n throw new Error(\n \"@aws-sdk/s3-request-presigner is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _presignerModule;\n}\n\n/**\n * S3-compatible storage implementation\n * Works with AWS S3 and MinIO (with forcePathStyle option)\n */\nexport class S3StorageController implements StorageController {\n private config: S3StorageConfig;\n private _client: S3ClientType | undefined;\n\n constructor(config: S3StorageConfig) {\n this.config = config;\n }\n\n /**\n * Lazily create and cache the S3 client on first use\n */\n private async getClient(): Promise<S3ClientType> {\n if (!this._client) {\n const s3 = await loadS3();\n this._client = new s3.S3Client({\n region: this.config.region || \"us-east-1\",\n endpoint: this.config.endpoint,\n forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint, // Auto-enable for custom endpoints (MinIO)\n credentials: {\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey\n }\n });\n }\n return this._client;\n }\n\n getType(): \"s3\" {\n return \"s3\";\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n if (file.size > maxSize) {\n throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);\n }\n\n if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {\n if (!this.config.allowedMimeTypes.includes(file.type)) {\n throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(\", \")}`);\n }\n }\n }\n\n /**\n * Get the bucket name - either from parameter or config\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for S3 it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps): Promise<UploadFileResult> {\n this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n\n // Convert File to Buffer\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.PutObjectCommand({\n Bucket: usedBucket,\n Key: key,\n Body: buffer,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n });\n\n await client.send(command);\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\n };\n }\n\n /**\n * Flatten nested metadata to string values (S3 requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle s3:// and gs:// URLs for backward compatibility\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const presigner = await loadPresigner();\n const client = await this.getClient();\n\n // First check if the object exists and get metadata\n const headCommand = new s3.HeadObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const headResult = await client.send(headCommand);\n\n // Generate a signed URL\n const getCommand = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: headResult.ContentLength || 0,\n contentType: headResult.ContentType || \"application/octet-stream\",\n customMetadata: headResult.Metadata || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NotFound\" || s3Error.$metadata?.httpStatusCode === 404) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw error;\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const response = await client.send(command);\n\n if (!response.Body) {\n return null;\n }\n\n // Convert stream to buffer\n const chunks: Uint8Array[] = [];\n for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n const contentType = response.ContentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([buffer], { type: contentType });\n return new File([blob], fileName, { type: contentType });\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return null;\n }\n throw error;\n }\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.DeleteObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Deleting something that is already gone is the outcome the caller\n // wanted, so it is not an error. AWS itself answers DeleteObject with\n // 204 for a key that never existed, but S3-compatible backends (MinIO,\n // Ceph, R2 in some configurations) do raise NoSuchKey — and the local\n // controller has always returned quietly in this case. Without this,\n // which of those a deployment happens to run decides whether\n // `DELETE /api/storage/:path` on a stale reference is a 200 or a 500.\n try {\n await client.send(command);\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return;\n }\n throw error;\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: prefix || undefined,\n MaxKeys: options?.maxResults ?? 1000,\n ContinuationToken: options?.pageToken,\n Delimiter: \"/\" // This gives us folder-like behavior\n });\n\n const response = await client.send(command);\n\n const items: StorageReference[] = (response.Contents || []).map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${obj.Key}`\n }));\n\n const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(prefix => ({\n bucket: resolvedBucket,\n fullPath: prefix.Prefix || \"\",\n name: (prefix.Prefix || \"\").replace(/\\/$/, \"\").split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`\n }));\n\n return {\n items,\n prefixes,\n nextPageToken: response.NextContinuationToken\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -20,3 +20,25 @@ export declare function assertKnownWriteFields(values: Record<string, unknown>,
|
|
|
20
20
|
rowIndex?: number;
|
|
21
21
|
extraKnownFields?: readonly string[];
|
|
22
22
|
}): void;
|
|
23
|
+
/**
|
|
24
|
+
* Narrow response rows to the fields the caller asked for.
|
|
25
|
+
*
|
|
26
|
+
* `?fields=id,title` is documented in the generated OpenAPI — "Comma-separated
|
|
27
|
+
* list of fields to return (field selection)" — and it is the first thing shown
|
|
28
|
+
* on every endpoint in the API Explorer. It was parsed into `options.fields`
|
|
29
|
+
* and then read by nothing at all: no driver referenced it, and every request
|
|
30
|
+
* came back with every column. A caller asking for two fields of a `posts` row
|
|
31
|
+
* still received its whole `content`.
|
|
32
|
+
*
|
|
33
|
+
* This shapes the *response*, which is what the parameter says it does; it is
|
|
34
|
+
* not a column pushdown, so it saves bandwidth rather than database work.
|
|
35
|
+
*
|
|
36
|
+
* `id` always survives. Rows are addressed by it everywhere above this layer —
|
|
37
|
+
* the admin table, realtime reconciliation, the offline cache — and a row that
|
|
38
|
+
* arrives without one is not a smaller row, it is an unusable one. Asking for
|
|
39
|
+
* `fields=title` and being unable to open the record you clicked is a worse
|
|
40
|
+
* answer than one extra key.
|
|
41
|
+
*/
|
|
42
|
+
export declare function projectResponseFields<T extends Record<string, unknown>>(rows: T[], fields: readonly string[] | undefined, collection: CollectionConfig, options?: {
|
|
43
|
+
include?: readonly string[];
|
|
44
|
+
}): T[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing of the `Authorization: Bearer <token>` header.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than a helper inside `middleware.ts`, because the
|
|
5
|
+
* API-key middleware needs it too and `middleware.ts` already imports from
|
|
6
|
+
* there — putting it in either one would close an import cycle.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Pull the token out of an `Authorization: Bearer <token>` header.
|
|
12
|
+
*
|
|
13
|
+
* RFC 7235 §2.1 makes the auth-scheme name case-insensitive, and plenty of
|
|
14
|
+
* clients send `bearer` — HTTP libraries that lower-case what they emit,
|
|
15
|
+
* hand-written curl, proxies that normalise headers. Matching `"Bearer "`
|
|
16
|
+
* exactly turned those into a 401 that, from the caller's side, is
|
|
17
|
+
* indistinguishable from a rejected token; people spend an afternoon
|
|
18
|
+
* regenerating credentials that were fine. Only the scheme is folded — the
|
|
19
|
+
* token itself is returned byte-for-byte.
|
|
20
|
+
*
|
|
21
|
+
* Returns `undefined` when there is no header or it names another scheme.
|
|
22
|
+
* That is deliberately distinct from an empty token (`Bearer ` with nothing
|
|
23
|
+
* after it), which callers still route into verification so it fails as a bad
|
|
24
|
+
* token rather than as a missing header.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractBearerToken(authHeader: string | undefined | null): string | undefined;
|
|
@@ -3,6 +3,8 @@ import { DataDriver } from "@rebasepro/types";
|
|
|
3
3
|
import { AccessTokenPayload } from "./jwt";
|
|
4
4
|
import type { HonoEnv } from "../api/types";
|
|
5
5
|
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
6
|
+
import { extractBearerToken } from "./bearer-token";
|
|
7
|
+
export { extractBearerToken };
|
|
6
8
|
/**
|
|
7
9
|
* Result from a custom auth validator.
|
|
8
10
|
* - `false`/`null`/`undefined` = not authenticated
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AuthAdapter } from "@rebasepro/types";
|
|
2
|
+
import type { RebaseAuthConfig } from "../init";
|
|
3
|
+
/**
|
|
4
|
+
* Whether the `auth` config is an `AuthAdapter` (it can verify a request) or a
|
|
5
|
+
* plain `RebaseAuthConfig`. Lives here rather than in `init.ts` so this module
|
|
6
|
+
* stays free of it; `init.ts` re-exports it under its original name.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isAuthAdapter(auth: RebaseAuthConfig | AuthAdapter): auth is AuthAdapter;
|
|
9
|
+
/**
|
|
10
|
+
* Does this server require an authenticated caller?
|
|
11
|
+
*
|
|
12
|
+
* One predicate, because there are two enforcement points — the HTTP data
|
|
13
|
+
* routes and the realtime socket — and they used to compute it separately. The
|
|
14
|
+
* socket's copy read
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* authConfig?.requireAuth !== false && !!authConfig?.jwtSecret
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* which differs from this in two ways, both of them open. With no auth config
|
|
21
|
+
* at all it returned `false` while the HTTP side returned `true`, so a server
|
|
22
|
+
* that answered 401 to every `/api/data` read served the same rows over the
|
|
23
|
+
* socket. And an explicit `requireAuth: true` was ANDed away whenever the
|
|
24
|
+
* credential came from an adapter rather than a local `jwtSecret` — asking for
|
|
25
|
+
* authentication was what switched it off.
|
|
26
|
+
*
|
|
27
|
+
* It is worth stating why that is worse than an ordinary missing check: the
|
|
28
|
+
* socket seeds each session with `authenticated: !requireAuth`, so a `false`
|
|
29
|
+
* here does not skip a gate, it marks every client that connects as already
|
|
30
|
+
* past it.
|
|
31
|
+
*
|
|
32
|
+
* Its own module rather than a helper in `init.ts` so the drivers can import it
|
|
33
|
+
* without pulling the backend entry point in behind it.
|
|
34
|
+
*
|
|
35
|
+
* - an `AuthAdapter` always implies auth is required (secure by default)
|
|
36
|
+
* - a `RebaseAuthConfig` is honoured, and only an explicit `false` opens it
|
|
37
|
+
* - no auth configuration at all defaults to required
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveRequireAuth(auth?: RebaseAuthConfig | AuthAdapter): boolean;
|
|
@@ -192,6 +192,40 @@ function safeCompare(a, b) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
//#endregion
|
|
195
|
+
//#region src/auth/bearer-token.ts
|
|
196
|
+
/**
|
|
197
|
+
* Parsing of the `Authorization: Bearer <token>` header.
|
|
198
|
+
*
|
|
199
|
+
* Its own module rather than a helper inside `middleware.ts`, because the
|
|
200
|
+
* API-key middleware needs it too and `middleware.ts` already imports from
|
|
201
|
+
* there — putting it in either one would close an import cycle.
|
|
202
|
+
*
|
|
203
|
+
* @module
|
|
204
|
+
*/
|
|
205
|
+
/**
|
|
206
|
+
* Pull the token out of an `Authorization: Bearer <token>` header.
|
|
207
|
+
*
|
|
208
|
+
* RFC 7235 §2.1 makes the auth-scheme name case-insensitive, and plenty of
|
|
209
|
+
* clients send `bearer` — HTTP libraries that lower-case what they emit,
|
|
210
|
+
* hand-written curl, proxies that normalise headers. Matching `"Bearer "`
|
|
211
|
+
* exactly turned those into a 401 that, from the caller's side, is
|
|
212
|
+
* indistinguishable from a rejected token; people spend an afternoon
|
|
213
|
+
* regenerating credentials that were fine. Only the scheme is folded — the
|
|
214
|
+
* token itself is returned byte-for-byte.
|
|
215
|
+
*
|
|
216
|
+
* Returns `undefined` when there is no header or it names another scheme.
|
|
217
|
+
* That is deliberately distinct from an empty token (`Bearer ` with nothing
|
|
218
|
+
* after it), which callers still route into verification so it fails as a bad
|
|
219
|
+
* token rather than as a missing header.
|
|
220
|
+
*/
|
|
221
|
+
function extractBearerToken(authHeader) {
|
|
222
|
+
if (!authHeader) return void 0;
|
|
223
|
+
const separator = authHeader.indexOf(" ");
|
|
224
|
+
if (separator < 0) return void 0;
|
|
225
|
+
if (authHeader.slice(0, separator).toLowerCase() !== "bearer") return void 0;
|
|
226
|
+
return authHeader.slice(separator + 1);
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
195
229
|
//#region src/auth/api-keys/api-key-middleware.ts
|
|
196
230
|
/**
|
|
197
231
|
* Hono middleware for authenticating requests via Service API Keys.
|
|
@@ -386,8 +420,7 @@ function createFunctionApiKeyGuard(mountPrefix) {
|
|
|
386
420
|
function createApiKeyPreAuth(options) {
|
|
387
421
|
return async (c, next) => {
|
|
388
422
|
if (c.get("apiKey")) return next();
|
|
389
|
-
const
|
|
390
|
-
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
423
|
+
const token = extractBearerToken(c.req.header("authorization")) ?? "";
|
|
391
424
|
if (!token || !isApiKeyToken(token)) return next();
|
|
392
425
|
const result = await validateApiKey(c, token, options);
|
|
393
426
|
if (result !== true) return result;
|
|
@@ -408,12 +441,12 @@ function createApiKeyPreAuth(options) {
|
|
|
408
441
|
*/
|
|
409
442
|
var requireAuth = async (c, next) => {
|
|
410
443
|
if (c.get("user")) return next();
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
444
|
+
const token = extractBearerToken(c.req.header("authorization"));
|
|
445
|
+
if (token === void 0) return c.json({ error: {
|
|
413
446
|
message: "Authorization header missing or invalid",
|
|
414
447
|
code: "UNAUTHORIZED"
|
|
415
448
|
} }, 401);
|
|
416
|
-
const payload = verifyAccessToken(
|
|
449
|
+
const payload = verifyAccessToken(token);
|
|
417
450
|
if (!payload) return c.json({ error: {
|
|
418
451
|
message: "Invalid or expired token",
|
|
419
452
|
code: "UNAUTHORIZED"
|
|
@@ -436,12 +469,11 @@ function createRequireAuth(options) {
|
|
|
436
469
|
const key = options.serviceKey;
|
|
437
470
|
return async (c, next) => {
|
|
438
471
|
if (c.get("user")) return next();
|
|
439
|
-
const
|
|
440
|
-
if (
|
|
472
|
+
const token = extractBearerToken(c.req.header("authorization"));
|
|
473
|
+
if (token === void 0) return c.json({ error: {
|
|
441
474
|
message: "Authorization header missing or invalid",
|
|
442
475
|
code: "UNAUTHORIZED"
|
|
443
476
|
} }, 401);
|
|
444
|
-
const token = authHeader.substring(7);
|
|
445
477
|
if (safeCompare(token, key)) {
|
|
446
478
|
c.set("user", {
|
|
447
479
|
uid: "service",
|
|
@@ -485,9 +517,9 @@ var requireAdmin = async (c, next) => {
|
|
|
485
517
|
*/
|
|
486
518
|
var optionalAuth = async (c, next) => {
|
|
487
519
|
if (c.get("user")) return next();
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
490
|
-
const payload = verifyAccessToken(
|
|
520
|
+
const token = extractBearerToken(c.req.header("authorization"));
|
|
521
|
+
if (token !== void 0) {
|
|
522
|
+
const payload = verifyAccessToken(token);
|
|
491
523
|
if (payload) c.set("user", payload);
|
|
492
524
|
}
|
|
493
525
|
return next();
|
|
@@ -560,55 +592,53 @@ function createAuthMiddleware(options) {
|
|
|
560
592
|
} }, 401);
|
|
561
593
|
}
|
|
562
594
|
else {
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
595
|
+
const token = extractBearerToken(c.req.header("authorization"));
|
|
596
|
+
if (token !== void 0) if (serviceKey && safeCompare(token, serviceKey)) {
|
|
597
|
+
c.set("user", {
|
|
598
|
+
uid: "service",
|
|
599
|
+
roles: ["admin"]
|
|
600
|
+
});
|
|
601
|
+
try {
|
|
602
|
+
c.set("driver", await scopeDataDriver(driver, {
|
|
568
603
|
uid: "service",
|
|
569
604
|
roles: ["admin"]
|
|
570
|
-
});
|
|
605
|
+
}));
|
|
606
|
+
} catch (error) {
|
|
607
|
+
logger.error("[AUTH] RLS scoping failed for service key", { error });
|
|
608
|
+
return c.json({ error: {
|
|
609
|
+
message: "Internal authentication error",
|
|
610
|
+
code: "INTERNAL_ERROR"
|
|
611
|
+
} }, 500);
|
|
612
|
+
}
|
|
613
|
+
} else if (apiKeyStore && isApiKeyToken(token)) {
|
|
614
|
+
const result = await validateApiKey(c, token, {
|
|
615
|
+
store: apiKeyStore,
|
|
616
|
+
driver
|
|
617
|
+
});
|
|
618
|
+
if (result !== true) return result;
|
|
619
|
+
} else {
|
|
620
|
+
const payload = extractUserFromToken(token);
|
|
621
|
+
if (payload) {
|
|
622
|
+
c.set("user", payload);
|
|
571
623
|
try {
|
|
572
|
-
|
|
573
|
-
uid:
|
|
574
|
-
roles:
|
|
575
|
-
}
|
|
624
|
+
const user = {
|
|
625
|
+
uid: payload.uid,
|
|
626
|
+
roles: payload.roles
|
|
627
|
+
};
|
|
628
|
+
c.set("driver", await scopeDataDriver(driver, user));
|
|
576
629
|
} catch (error) {
|
|
577
|
-
logger.error("[AUTH] RLS scoping failed for
|
|
630
|
+
logger.error("[AUTH] RLS scoping failed for authenticated user", { error });
|
|
578
631
|
return c.json({ error: {
|
|
579
632
|
message: "Internal authentication error",
|
|
580
633
|
code: "INTERNAL_ERROR"
|
|
581
634
|
} }, 500);
|
|
582
635
|
}
|
|
583
|
-
} else
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
} else {
|
|
590
|
-
const payload = extractUserFromToken(token);
|
|
591
|
-
if (payload) {
|
|
592
|
-
c.set("user", payload);
|
|
593
|
-
try {
|
|
594
|
-
const user = {
|
|
595
|
-
uid: payload.uid,
|
|
596
|
-
roles: payload.roles
|
|
597
|
-
};
|
|
598
|
-
c.set("driver", await scopeDataDriver(driver, user));
|
|
599
|
-
} catch (error) {
|
|
600
|
-
logger.error("[AUTH] RLS scoping failed for authenticated user", { error });
|
|
601
|
-
return c.json({ error: {
|
|
602
|
-
message: "Internal authentication error",
|
|
603
|
-
code: "INTERNAL_ERROR"
|
|
604
|
-
} }, 500);
|
|
605
|
-
}
|
|
606
|
-
} else return c.json({ error: {
|
|
607
|
-
message: "Invalid or expired token",
|
|
608
|
-
code: "UNAUTHORIZED"
|
|
609
|
-
} }, 401);
|
|
610
|
-
}
|
|
611
|
-
} else try {
|
|
636
|
+
} else return c.json({ error: {
|
|
637
|
+
message: "Invalid or expired token",
|
|
638
|
+
code: "UNAUTHORIZED"
|
|
639
|
+
} }, 401);
|
|
640
|
+
}
|
|
641
|
+
else try {
|
|
612
642
|
c.set("driver", await scopeDataDriver(driver, {
|
|
613
643
|
uid: ANONYMOUS_USER_ID,
|
|
614
644
|
roles: ["anon"]
|
|
@@ -646,8 +676,7 @@ function createAuthMiddleware(options) {
|
|
|
646
676
|
* ```
|
|
647
677
|
*/
|
|
648
678
|
var queryTokenAuth = async (c, next) => {
|
|
649
|
-
|
|
650
|
-
if (authHeader && authHeader.startsWith("Bearer ")) return next();
|
|
679
|
+
if (extractBearerToken(c.req.header("authorization")) !== void 0) return next();
|
|
651
680
|
const queryToken = c.req.query("token");
|
|
652
681
|
if (queryToken) {
|
|
653
682
|
const payload = verifyAccessToken(queryToken);
|
|
@@ -731,8 +760,9 @@ var fileTokenAuth = async (c, next) => {
|
|
|
731
760
|
resolvedPath: filePath
|
|
732
761
|
};
|
|
733
762
|
};
|
|
734
|
-
|
|
735
|
-
|
|
763
|
+
const bearerToken = extractBearerToken(authHeader);
|
|
764
|
+
if (bearerToken !== void 0) {
|
|
765
|
+
const payload = verifyDownloadToken(bearerToken);
|
|
736
766
|
if (payload) {
|
|
737
767
|
const rawPath = extractWildcard(c);
|
|
738
768
|
if (rawPath) {
|
|
@@ -790,8 +820,7 @@ function createAdapterAuthMiddleware(options) {
|
|
|
790
820
|
return async (c, next) => {
|
|
791
821
|
const driver = resolveDriver ? resolveDriver(c) : baseDriver;
|
|
792
822
|
if (apiKeyStore) {
|
|
793
|
-
const
|
|
794
|
-
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
823
|
+
const token = extractBearerToken(c.req.header("authorization")) ?? "";
|
|
795
824
|
if (token.startsWith("rk_")) {
|
|
796
825
|
const result = await validateApiKey(c, token, {
|
|
797
826
|
store: apiKeyStore,
|
|
@@ -6126,10 +6155,10 @@ function mountSessionRoutes(opts) {
|
|
|
6126
6155
|
else await authRepo.deleteRefreshToken(tokenHash);
|
|
6127
6156
|
}
|
|
6128
6157
|
clearRefreshCookie(c, config.cookieAuth);
|
|
6129
|
-
const
|
|
6130
|
-
if (ops.afterLogout &&
|
|
6158
|
+
const accessToken = extractBearerToken(c.req.header("authorization"));
|
|
6159
|
+
if (ops.afterLogout && accessToken !== void 0) {
|
|
6131
6160
|
const { verifyAccessToken } = await import("./jwt-DD6EtpGj.js").then((n) => n.u);
|
|
6132
|
-
const payload = verifyAccessToken(
|
|
6161
|
+
const payload = verifyAccessToken(accessToken);
|
|
6133
6162
|
if (payload) ops.afterLogout(payload.uid).catch((err) => {
|
|
6134
6163
|
logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
|
|
6135
6164
|
});
|
|
@@ -7371,9 +7400,8 @@ function createBuiltinAuthAdapter(config) {
|
|
|
7371
7400
|
id: "rebase-builtin",
|
|
7372
7401
|
serviceKey,
|
|
7373
7402
|
async verifyRequest(request) {
|
|
7374
|
-
const
|
|
7375
|
-
if (
|
|
7376
|
-
const token = authHeader.substring(7);
|
|
7403
|
+
const token = extractBearerToken(request.headers.get("authorization"));
|
|
7404
|
+
if (token === void 0) return null;
|
|
7377
7405
|
if (serviceKey && safeCompare(token, serviceKey)) return {
|
|
7378
7406
|
uid: "service",
|
|
7379
7407
|
email: "service@rebase.internal",
|
|
@@ -8918,6 +8946,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
8918
8946
|
verifyPassword: () => verifyPassword
|
|
8919
8947
|
});
|
|
8920
8948
|
//#endregion
|
|
8921
|
-
export {
|
|
8949
|
+
export { httpMethodToOperation as $, getEmailVerificationTemplate as A, optionalAuth as B, createDataRateLimiter as C, validatePasswordStrength as D, hashPassword as E, createAdapterAuthMiddleware as F, createApiKeyPreAuth as G, queryTokenAuth as H, createAuthMiddleware as I, isApiKeyToken as J, createFunctionApiKeyGuard as K, createRequireAuth as L, getPasswordResetTemplate as M, getUserInvitationTemplate as N, verifyPassword as O, getWelcomeEmailTemplate as P, scopeDataDriver as Q, extractUserFromToken as R, _coercedNumber as S, resolveAuthHooks as T, requireAdmin as U, publicObjectAuth as V, requireAuth as W, extractBearerToken as X, validateApiKey as Y, safeCompare as Z, createBuiltinAuthAdapter as _, createSpotifyProvider as a, object as b, createGitLabProvider as c, createFacebookProvider as d, isOperationAllowed as et, createAppleProvider as f, createGoogleProvider as g, createLinkedinProvider as h, createApiKeyStore as i, resolveClientListLimit as it, getMagicLinkTemplate as j, generateSecurePassword as k, createDiscordProvider as l, createGitHubProvider as m, createCustomAuthAdapter as n, isPublicStoragePath as nt, createSlackProvider as o, createMicrosoftProvider as p, createStorageApiKeyGuard as q, createApiKeyRoutes as r, MAX_LIST_LIMIT as rt, createBitbucketProvider as s, auth_exports as t, PUBLIC_STORAGE_PREFIX as tt, createTwitterProvider as u, ZodNumber as v, MemoryRateLimitStore as w, string as x, _enum as y, fileTokenAuth as z };
|
|
8922
8950
|
|
|
8923
|
-
//# sourceMappingURL=auth-
|
|
8951
|
+
//# sourceMappingURL=auth-B9AJsH3u.js.map
|