@geekmidas/storage 10.0.0-alpha.67 → 10.0.0-alpha.68

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/README.md CHANGED
@@ -85,6 +85,49 @@ const storage = AmazonStorageClient.create({
85
85
  });
86
86
  ```
87
87
 
88
+ ## Storage URLs and credentials
89
+
90
+ A declared bucket reaches an app as one URL, `<ID>_URL`, and
91
+ `createStorageClient(url)` picks the driver by its scheme. An `s3://` URL
92
+ names the bucket as its host and the rest as query parameters:
93
+
94
+ ```
95
+ s3://uploads?region=eu-west-1
96
+ s3://uploads?region=auto&endpoint=https://<account>.r2.cloudflarestorage.com
97
+ ```
98
+
99
+ Credentials are optional, and come from one of two places:
100
+
101
+ - **The URL's userinfo** — `s3://KEY:SECRET@uploads?region=eu-west-1`. A key
102
+ here signs for this bucket only and wins over anything in the environment.
103
+ Percent-encode both halves: AWS secrets often contain `/` (`%2F`) and `+`
104
+ (`%2B`). `s3Url.build` does this for you, and `s3Url.parse` decodes them.
105
+ - **The SDK's default chain** — for a URL with no userinfo: the shared
106
+ `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, a profile, or an execution role.
107
+
108
+ ```typescript
109
+ import { s3Url } from '@geekmidas/storage/aws';
110
+
111
+ s3Url.build({
112
+ bucket: 'uploads',
113
+ region: 'eu-west-1',
114
+ accessKeyId: 'AKIA…',
115
+ secretAccessKey: 'wJal/rXUt+nFEMI…',
116
+ });
117
+ // 's3://AKIA…:wJal%2FrXUt%2BnFEMI…@uploads?region=eu-west-1'
118
+ ```
119
+
120
+ Prefer a key per bucket, scoped to that bucket, over one shared pair: an app
121
+ that reads two buckets then holds two narrow keys rather than one that opens
122
+ both. A URL with only one half of a pair (`s3://KEY@uploads`) is refused with
123
+ `IncompleteStorageCredentials` — it is never completed from the environment.
124
+
125
+ Every storage error that carries a URL (`MalformedStorageUrl`,
126
+ `UnexpectedStorageScheme`, `MissingStorageBucket`,
127
+ `UnregisteredStorageScheme`, `IncompleteStorageCredentials`) holds it with the
128
+ userinfo replaced by `REDACTED`, and its message never includes the URL.
129
+ `redactStorageUrl(url)` does the same for your own log lines.
130
+
88
131
  ## API Reference
89
132
 
90
133
  ### StorageClient Interface
@@ -400,20 +443,13 @@ npm run test:once
400
443
 
401
444
  ### Local Development with MinIO
402
445
 
403
- 1. Start MinIO using Docker Compose:
404
- ```bash
405
- docker-compose up -d minio
406
- ```
407
-
408
- 2. MinIO will be available at:
409
- - API: http://localhost:9000
410
- - Console: http://localhost:9001
411
- - Credentials: minioadmin/minioadmin
446
+ The suite starts the repo compose stack's MinIO itself (`geekmidas` /
447
+ `geekmidas`, with a `geekmidas` bucket). It publishes on port 9000; when
448
+ another project holds that port, move it:
412
449
 
413
- 3. Run integration tests:
414
- ```bash
415
- npm run test:integration
416
- ```
450
+ ```bash
451
+ MINIO_API_HOST_PORT=29000 MINIO_CONSOLE_HOST_PORT=29001 pnpm vitest run --project storage
452
+ ```
417
453
 
418
454
  ### Project Structure
419
455
 
package/dist/aws.cjs CHANGED
@@ -132,12 +132,14 @@ let AmazonCannedAccessControlList = /* @__PURE__ */ function(AmazonCannedAccessC
132
132
  const s3Driver = {
133
133
  scheme: "s3:",
134
134
  create(url) {
135
- const { bucket, region, endpoint, forcePathStyle } = require_s3Url.parse(url);
135
+ const { bucket, region, endpoint, forcePathStyle, accessKeyId, secretAccessKey } = require_s3Url.parse(url);
136
136
  return AmazonStorageClient.create({
137
137
  bucket,
138
138
  region,
139
139
  endpoint,
140
- forcePathStyle
140
+ forcePathStyle,
141
+ accessKeyId,
142
+ secretAccessKey
141
143
  });
142
144
  }
143
145
  };
package/dist/aws.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"aws.cjs","names":["S3Client","GetObjectCommand","getSignedUrl","ListObjectVersionsCommand","PutObjectCommand","createPresignedPost","DeleteObjectCommand","parse"],"sources":["../src/AmazonStorageClient.ts","../src/s3Driver.ts"],"sourcesContent":["import {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tListObjectVersionsCommand,\n\tPutObjectCommand,\n\tS3Client,\n} from '@aws-sdk/client-s3';\nimport { createPresignedPost } from '@aws-sdk/s3-presigned-post';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\n\nimport type { Cache } from '@geekmidas/cache';\nimport {\n\ttype DocumentVersion,\n\ttype File,\n\ttype GetUploadParams,\n\ttype GetUploadResponse,\n\ttype StorageClient,\n\tStorageProvider,\n} from './StorageClient';\n\nexport class AmazonStorageClient implements StorageClient {\n\treadonly provider = StorageProvider.AWSS3;\n\n\tstatic create(\n\t\toptions: AmazonStorageClientCreateOptions,\n\t): AmazonStorageClient {\n\t\tconst {\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\taccessKeyId,\n\t\t\tacl,\n\t\t\tendpoint,\n\t\t\tsecretAccessKey,\n\t\t\tforcePathStyle = false,\n\t\t\tcache,\n\t\t} = options;\n\t\tconst hasCredentials = accessKeyId && secretAccessKey;\n\t\tconst credentials = hasCredentials\n\t\t\t? { accessKeyId, secretAccessKey }\n\t\t\t: undefined;\n\n\t\tconst client = new S3Client({\n\t\t\tregion,\n\t\t\tcredentials,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t});\n\n\t\treturn new AmazonStorageClient(client, bucket, acl, cache);\n\t}\n\tconstructor(\n\t\tprivate readonly client: S3Client,\n\t\tprivate readonly bucket: string,\n\t\tprivate readonly acl = AmazonCannedAccessControlList.AuthenticatedRead,\n\t\treadonly cache?: Cache,\n\t) {}\n\n\tprivate createGetObjectCommand(\n\t\tfile: File,\n\t\toverrides?: { VersionId?: string },\n\t): GetObjectCommand {\n\t\tconst disposition =\n\t\t\tfile.disposition ?? (file.name ? 'attachment' : undefined);\n\t\tconst ResponseContentDisposition = disposition\n\t\t\t? file.name\n\t\t\t\t? `${disposition}; filename=${encodeURIComponent(file.name)}`\n\t\t\t\t: disposition\n\t\t\t: undefined;\n\n\t\treturn new GetObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: file.path,\n\t\t\tResponseContentDisposition,\n\t\t\tResponseContentType: file.responseContentType,\n\t\t\t...overrides,\n\t\t});\n\t}\n\n\tgetVersionDownloadURL(file: File, versionId: string): Promise<string> {\n\t\tconst command = this.createGetObjectCommand(file, { VersionId: versionId });\n\t\treturn getSignedUrl(this.client, command, { expiresIn: 60 * 60 * 24 });\n\t}\n\n\tasync getVersions(key: string): Promise<DocumentVersion[]> {\n\t\tconst command = new ListObjectVersionsCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tPrefix: key,\n\t\t});\n\n\t\tconst { Versions = [] } = await this.client.send(command);\n\n\t\treturn Versions.map((version) => ({\n\t\t\tid: version.VersionId || '',\n\t\t\tcreatedAt: version.LastModified || new Date(),\n\t\t}));\n\t}\n\n\tasync getDownloadURL(file: File, expiresIn = 60 * 60): Promise<string> {\n\t\tconst cacheKey = `download-url:${file.path}`;\n\t\tconst cachedURL = await this.cache?.get<string>(cacheKey);\n\n\t\tif (cachedURL) {\n\t\t\treturn cachedURL;\n\t\t}\n\n\t\tconst command = this.createGetObjectCommand(file);\n\t\tconst url = await getSignedUrl(this.client, command, { expiresIn });\n\t\tconst cacheExpiration = Math.max(expiresIn - 60, 0);\n\n\t\tif (cacheExpiration) {\n\t\t\tawait this.cache?.set(cacheKey, url, cacheExpiration);\n\t\t}\n\n\t\treturn url;\n\t}\n\n\tasync getUploadURL(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 60 * 60,\n\t): Promise<string> {\n\t\tconst command = new PutObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: params.path,\n\t\t\tContentType: params.contentType,\n\t\t\tContentLength: params.contentLength,\n\t\t});\n\n\t\treturn getSignedUrl(this.client, command, { expiresIn });\n\t}\n\n\tasync getUpload(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 5,\n\t): Promise<GetUploadResponse> {\n\t\tconst { path } = params;\n\t\tconst { fields: values, url } = await createPresignedPost(this.client, {\n\t\t\tExpires: expiresIn * 60,\n\t\t\tBucket: this.bucket,\n\t\t\tFields: {\n\t\t\t\tacl: this.acl,\n\t\t\t},\n\t\t\tConditions: [\n\t\t\t\t// content length restrictions: 0-1MB]\n\t\t\t\t// ['content-length-range', 0, contentLength],\n\t\t\t\t// specify content-type to be more generic- images only\n\t\t\t\t// ['starts-with', '$Content-Type', 'image/'],\n\t\t\t\t// ['starts-with', '$Content-Type', contentType],\n\t\t\t],\n\t\t\tKey: path,\n\t\t});\n\n\t\tconst keys = Object.keys(values);\n\t\tconst fields = keys.map((key) => ({ key, value: values[key] || '' }));\n\n\t\treturn { url, fields };\n\t}\n\n\tasync upload(\n\t\tkey: string,\n\t\tdata: string | Buffer,\n\t\tcontentType: string,\n\t): Promise<void> {\n\t\tconst Body = typeof data === 'string' ? Buffer.from(data, 'base64') : data;\n\n\t\tconst params = {\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t\tBody,\n\t\t\tContentType: contentType,\n\t\t};\n\n\t\tconst command = new PutObjectCommand(params);\n\n\t\tawait this.client.send(command);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tconst command = new DeleteObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t});\n\n\t\tawait this.client.send(command);\n\t}\n}\n\nexport enum AmazonCannedAccessControlList {\n\tAuthenticatedRead = 'authenticated-read',\n\tPrivate = 'private',\n\tPublicRead = 'public-read',\n\tPublicReadWrite = 'public-read-write',\n\tAwsExecRead = 'aws-exec-read',\n\tBucketOwnerRead = 'bucket-owner-read',\n\tBucketOwnerFullControl = 'bucket-owner-full-control',\n\tLogDeliveryWrite = 'log-delivery-write',\n}\n\ninterface AmazonStorageClientCreateOptions {\n\tbucket: string;\n\tregion?: string;\n\tacl?: AmazonCannedAccessControlList;\n\taccessKeyId?: string;\n\tsecretAccessKey?: string;\n\tendpoint?: string;\n\tforcePathStyle?: boolean;\n\tcache?: Cache;\n}\n","/**\n * The S3 driver — `s3://` URLs to an {@link AmazonStorageClient}.\n *\n * Registered by whoever assembles the application, so neutral code can build a\n * client from a URL without importing this file or the AWS SDK behind it.\n */\n\nimport { AmazonStorageClient } from './AmazonStorageClient';\nimport type { StorageDriver } from './registry';\nimport { parse } from './s3Url';\n\nexport const s3Driver: StorageDriver = {\n\tscheme: 's3:',\n\tcreate(url) {\n\t\tconst { bucket, region, endpoint, forcePathStyle } = parse(url);\n\t\treturn AmazonStorageClient.create({\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t});\n\t},\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"aws.cjs","names":["S3Client","GetObjectCommand","getSignedUrl","ListObjectVersionsCommand","PutObjectCommand","createPresignedPost","DeleteObjectCommand","parse"],"sources":["../src/AmazonStorageClient.ts","../src/s3Driver.ts"],"sourcesContent":["import {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tListObjectVersionsCommand,\n\tPutObjectCommand,\n\tS3Client,\n} from '@aws-sdk/client-s3';\nimport { createPresignedPost } from '@aws-sdk/s3-presigned-post';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\n\nimport type { Cache } from '@geekmidas/cache';\nimport {\n\ttype DocumentVersion,\n\ttype File,\n\ttype GetUploadParams,\n\ttype GetUploadResponse,\n\ttype StorageClient,\n\tStorageProvider,\n} from './StorageClient';\n\nexport class AmazonStorageClient implements StorageClient {\n\treadonly provider = StorageProvider.AWSS3;\n\n\tstatic create(\n\t\toptions: AmazonStorageClientCreateOptions,\n\t): AmazonStorageClient {\n\t\tconst {\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\taccessKeyId,\n\t\t\tacl,\n\t\t\tendpoint,\n\t\t\tsecretAccessKey,\n\t\t\tforcePathStyle = false,\n\t\t\tcache,\n\t\t} = options;\n\t\tconst hasCredentials = accessKeyId && secretAccessKey;\n\t\tconst credentials = hasCredentials\n\t\t\t? { accessKeyId, secretAccessKey }\n\t\t\t: undefined;\n\n\t\tconst client = new S3Client({\n\t\t\tregion,\n\t\t\tcredentials,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t});\n\n\t\treturn new AmazonStorageClient(client, bucket, acl, cache);\n\t}\n\tconstructor(\n\t\tprivate readonly client: S3Client,\n\t\tprivate readonly bucket: string,\n\t\tprivate readonly acl = AmazonCannedAccessControlList.AuthenticatedRead,\n\t\treadonly cache?: Cache,\n\t) {}\n\n\tprivate createGetObjectCommand(\n\t\tfile: File,\n\t\toverrides?: { VersionId?: string },\n\t): GetObjectCommand {\n\t\tconst disposition =\n\t\t\tfile.disposition ?? (file.name ? 'attachment' : undefined);\n\t\tconst ResponseContentDisposition = disposition\n\t\t\t? file.name\n\t\t\t\t? `${disposition}; filename=${encodeURIComponent(file.name)}`\n\t\t\t\t: disposition\n\t\t\t: undefined;\n\n\t\treturn new GetObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: file.path,\n\t\t\tResponseContentDisposition,\n\t\t\tResponseContentType: file.responseContentType,\n\t\t\t...overrides,\n\t\t});\n\t}\n\n\tgetVersionDownloadURL(file: File, versionId: string): Promise<string> {\n\t\tconst command = this.createGetObjectCommand(file, { VersionId: versionId });\n\t\treturn getSignedUrl(this.client, command, { expiresIn: 60 * 60 * 24 });\n\t}\n\n\tasync getVersions(key: string): Promise<DocumentVersion[]> {\n\t\tconst command = new ListObjectVersionsCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tPrefix: key,\n\t\t});\n\n\t\tconst { Versions = [] } = await this.client.send(command);\n\n\t\treturn Versions.map((version) => ({\n\t\t\tid: version.VersionId || '',\n\t\t\tcreatedAt: version.LastModified || new Date(),\n\t\t}));\n\t}\n\n\tasync getDownloadURL(file: File, expiresIn = 60 * 60): Promise<string> {\n\t\tconst cacheKey = `download-url:${file.path}`;\n\t\tconst cachedURL = await this.cache?.get<string>(cacheKey);\n\n\t\tif (cachedURL) {\n\t\t\treturn cachedURL;\n\t\t}\n\n\t\tconst command = this.createGetObjectCommand(file);\n\t\tconst url = await getSignedUrl(this.client, command, { expiresIn });\n\t\tconst cacheExpiration = Math.max(expiresIn - 60, 0);\n\n\t\tif (cacheExpiration) {\n\t\t\tawait this.cache?.set(cacheKey, url, cacheExpiration);\n\t\t}\n\n\t\treturn url;\n\t}\n\n\tasync getUploadURL(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 60 * 60,\n\t): Promise<string> {\n\t\tconst command = new PutObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: params.path,\n\t\t\tContentType: params.contentType,\n\t\t\tContentLength: params.contentLength,\n\t\t});\n\n\t\treturn getSignedUrl(this.client, command, { expiresIn });\n\t}\n\n\tasync getUpload(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 5,\n\t): Promise<GetUploadResponse> {\n\t\tconst { path } = params;\n\t\tconst { fields: values, url } = await createPresignedPost(this.client, {\n\t\t\tExpires: expiresIn * 60,\n\t\t\tBucket: this.bucket,\n\t\t\tFields: {\n\t\t\t\tacl: this.acl,\n\t\t\t},\n\t\t\tConditions: [\n\t\t\t\t// content length restrictions: 0-1MB]\n\t\t\t\t// ['content-length-range', 0, contentLength],\n\t\t\t\t// specify content-type to be more generic- images only\n\t\t\t\t// ['starts-with', '$Content-Type', 'image/'],\n\t\t\t\t// ['starts-with', '$Content-Type', contentType],\n\t\t\t],\n\t\t\tKey: path,\n\t\t});\n\n\t\tconst keys = Object.keys(values);\n\t\tconst fields = keys.map((key) => ({ key, value: values[key] || '' }));\n\n\t\treturn { url, fields };\n\t}\n\n\tasync upload(\n\t\tkey: string,\n\t\tdata: string | Buffer,\n\t\tcontentType: string,\n\t): Promise<void> {\n\t\tconst Body = typeof data === 'string' ? Buffer.from(data, 'base64') : data;\n\n\t\tconst params = {\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t\tBody,\n\t\t\tContentType: contentType,\n\t\t};\n\n\t\tconst command = new PutObjectCommand(params);\n\n\t\tawait this.client.send(command);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tconst command = new DeleteObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t});\n\n\t\tawait this.client.send(command);\n\t}\n}\n\nexport enum AmazonCannedAccessControlList {\n\tAuthenticatedRead = 'authenticated-read',\n\tPrivate = 'private',\n\tPublicRead = 'public-read',\n\tPublicReadWrite = 'public-read-write',\n\tAwsExecRead = 'aws-exec-read',\n\tBucketOwnerRead = 'bucket-owner-read',\n\tBucketOwnerFullControl = 'bucket-owner-full-control',\n\tLogDeliveryWrite = 'log-delivery-write',\n}\n\ninterface AmazonStorageClientCreateOptions {\n\tbucket: string;\n\tregion?: string;\n\tacl?: AmazonCannedAccessControlList;\n\taccessKeyId?: string;\n\tsecretAccessKey?: string;\n\tendpoint?: string;\n\tforcePathStyle?: boolean;\n\tcache?: Cache;\n}\n","/**\n * The S3 driver — `s3://` URLs to an {@link AmazonStorageClient}.\n *\n * Registered by whoever assembles the application, so neutral code can build a\n * client from a URL without importing this file or the AWS SDK behind it.\n */\n\nimport { AmazonStorageClient } from './AmazonStorageClient';\nimport type { StorageDriver } from './registry';\nimport { parse } from './s3Url';\n\nexport const s3Driver: StorageDriver = {\n\tscheme: 's3:',\n\tcreate(url) {\n\t\t// The URL's own key pair, when it has one, wins over the SDK's default\n\t\t// chain; without one the client resolves credentials as it always has.\n\t\tconst {\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t\taccessKeyId,\n\t\t\tsecretAccessKey,\n\t\t} = parse(url);\n\t\treturn AmazonStorageClient.create({\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t\taccessKeyId,\n\t\t\tsecretAccessKey,\n\t\t});\n\t},\n};\n"],"mappings":"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"}
package/dist/aws.mjs CHANGED
@@ -131,12 +131,14 @@ let AmazonCannedAccessControlList = /* @__PURE__ */ function(AmazonCannedAccessC
131
131
  const s3Driver = {
132
132
  scheme: "s3:",
133
133
  create(url) {
134
- const { bucket, region, endpoint, forcePathStyle } = parse(url);
134
+ const { bucket, region, endpoint, forcePathStyle, accessKeyId, secretAccessKey } = parse(url);
135
135
  return AmazonStorageClient.create({
136
136
  bucket,
137
137
  region,
138
138
  endpoint,
139
- forcePathStyle
139
+ forcePathStyle,
140
+ accessKeyId,
141
+ secretAccessKey
140
142
  });
141
143
  }
142
144
  };
package/dist/aws.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"aws.mjs","names":[],"sources":["../src/AmazonStorageClient.ts","../src/s3Driver.ts"],"sourcesContent":["import {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tListObjectVersionsCommand,\n\tPutObjectCommand,\n\tS3Client,\n} from '@aws-sdk/client-s3';\nimport { createPresignedPost } from '@aws-sdk/s3-presigned-post';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\n\nimport type { Cache } from '@geekmidas/cache';\nimport {\n\ttype DocumentVersion,\n\ttype File,\n\ttype GetUploadParams,\n\ttype GetUploadResponse,\n\ttype StorageClient,\n\tStorageProvider,\n} from './StorageClient';\n\nexport class AmazonStorageClient implements StorageClient {\n\treadonly provider = StorageProvider.AWSS3;\n\n\tstatic create(\n\t\toptions: AmazonStorageClientCreateOptions,\n\t): AmazonStorageClient {\n\t\tconst {\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\taccessKeyId,\n\t\t\tacl,\n\t\t\tendpoint,\n\t\t\tsecretAccessKey,\n\t\t\tforcePathStyle = false,\n\t\t\tcache,\n\t\t} = options;\n\t\tconst hasCredentials = accessKeyId && secretAccessKey;\n\t\tconst credentials = hasCredentials\n\t\t\t? { accessKeyId, secretAccessKey }\n\t\t\t: undefined;\n\n\t\tconst client = new S3Client({\n\t\t\tregion,\n\t\t\tcredentials,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t});\n\n\t\treturn new AmazonStorageClient(client, bucket, acl, cache);\n\t}\n\tconstructor(\n\t\tprivate readonly client: S3Client,\n\t\tprivate readonly bucket: string,\n\t\tprivate readonly acl = AmazonCannedAccessControlList.AuthenticatedRead,\n\t\treadonly cache?: Cache,\n\t) {}\n\n\tprivate createGetObjectCommand(\n\t\tfile: File,\n\t\toverrides?: { VersionId?: string },\n\t): GetObjectCommand {\n\t\tconst disposition =\n\t\t\tfile.disposition ?? (file.name ? 'attachment' : undefined);\n\t\tconst ResponseContentDisposition = disposition\n\t\t\t? file.name\n\t\t\t\t? `${disposition}; filename=${encodeURIComponent(file.name)}`\n\t\t\t\t: disposition\n\t\t\t: undefined;\n\n\t\treturn new GetObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: file.path,\n\t\t\tResponseContentDisposition,\n\t\t\tResponseContentType: file.responseContentType,\n\t\t\t...overrides,\n\t\t});\n\t}\n\n\tgetVersionDownloadURL(file: File, versionId: string): Promise<string> {\n\t\tconst command = this.createGetObjectCommand(file, { VersionId: versionId });\n\t\treturn getSignedUrl(this.client, command, { expiresIn: 60 * 60 * 24 });\n\t}\n\n\tasync getVersions(key: string): Promise<DocumentVersion[]> {\n\t\tconst command = new ListObjectVersionsCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tPrefix: key,\n\t\t});\n\n\t\tconst { Versions = [] } = await this.client.send(command);\n\n\t\treturn Versions.map((version) => ({\n\t\t\tid: version.VersionId || '',\n\t\t\tcreatedAt: version.LastModified || new Date(),\n\t\t}));\n\t}\n\n\tasync getDownloadURL(file: File, expiresIn = 60 * 60): Promise<string> {\n\t\tconst cacheKey = `download-url:${file.path}`;\n\t\tconst cachedURL = await this.cache?.get<string>(cacheKey);\n\n\t\tif (cachedURL) {\n\t\t\treturn cachedURL;\n\t\t}\n\n\t\tconst command = this.createGetObjectCommand(file);\n\t\tconst url = await getSignedUrl(this.client, command, { expiresIn });\n\t\tconst cacheExpiration = Math.max(expiresIn - 60, 0);\n\n\t\tif (cacheExpiration) {\n\t\t\tawait this.cache?.set(cacheKey, url, cacheExpiration);\n\t\t}\n\n\t\treturn url;\n\t}\n\n\tasync getUploadURL(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 60 * 60,\n\t): Promise<string> {\n\t\tconst command = new PutObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: params.path,\n\t\t\tContentType: params.contentType,\n\t\t\tContentLength: params.contentLength,\n\t\t});\n\n\t\treturn getSignedUrl(this.client, command, { expiresIn });\n\t}\n\n\tasync getUpload(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 5,\n\t): Promise<GetUploadResponse> {\n\t\tconst { path } = params;\n\t\tconst { fields: values, url } = await createPresignedPost(this.client, {\n\t\t\tExpires: expiresIn * 60,\n\t\t\tBucket: this.bucket,\n\t\t\tFields: {\n\t\t\t\tacl: this.acl,\n\t\t\t},\n\t\t\tConditions: [\n\t\t\t\t// content length restrictions: 0-1MB]\n\t\t\t\t// ['content-length-range', 0, contentLength],\n\t\t\t\t// specify content-type to be more generic- images only\n\t\t\t\t// ['starts-with', '$Content-Type', 'image/'],\n\t\t\t\t// ['starts-with', '$Content-Type', contentType],\n\t\t\t],\n\t\t\tKey: path,\n\t\t});\n\n\t\tconst keys = Object.keys(values);\n\t\tconst fields = keys.map((key) => ({ key, value: values[key] || '' }));\n\n\t\treturn { url, fields };\n\t}\n\n\tasync upload(\n\t\tkey: string,\n\t\tdata: string | Buffer,\n\t\tcontentType: string,\n\t): Promise<void> {\n\t\tconst Body = typeof data === 'string' ? Buffer.from(data, 'base64') : data;\n\n\t\tconst params = {\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t\tBody,\n\t\t\tContentType: contentType,\n\t\t};\n\n\t\tconst command = new PutObjectCommand(params);\n\n\t\tawait this.client.send(command);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tconst command = new DeleteObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t});\n\n\t\tawait this.client.send(command);\n\t}\n}\n\nexport enum AmazonCannedAccessControlList {\n\tAuthenticatedRead = 'authenticated-read',\n\tPrivate = 'private',\n\tPublicRead = 'public-read',\n\tPublicReadWrite = 'public-read-write',\n\tAwsExecRead = 'aws-exec-read',\n\tBucketOwnerRead = 'bucket-owner-read',\n\tBucketOwnerFullControl = 'bucket-owner-full-control',\n\tLogDeliveryWrite = 'log-delivery-write',\n}\n\ninterface AmazonStorageClientCreateOptions {\n\tbucket: string;\n\tregion?: string;\n\tacl?: AmazonCannedAccessControlList;\n\taccessKeyId?: string;\n\tsecretAccessKey?: string;\n\tendpoint?: string;\n\tforcePathStyle?: boolean;\n\tcache?: Cache;\n}\n","/**\n * The S3 driver — `s3://` URLs to an {@link AmazonStorageClient}.\n *\n * Registered by whoever assembles the application, so neutral code can build a\n * client from a URL without importing this file or the AWS SDK behind it.\n */\n\nimport { AmazonStorageClient } from './AmazonStorageClient';\nimport type { StorageDriver } from './registry';\nimport { parse } from './s3Url';\n\nexport const s3Driver: StorageDriver = {\n\tscheme: 's3:',\n\tcreate(url) {\n\t\tconst { bucket, region, endpoint, forcePathStyle } = parse(url);\n\t\treturn AmazonStorageClient.create({\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t});\n\t},\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"aws.mjs","names":[],"sources":["../src/AmazonStorageClient.ts","../src/s3Driver.ts"],"sourcesContent":["import {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tListObjectVersionsCommand,\n\tPutObjectCommand,\n\tS3Client,\n} from '@aws-sdk/client-s3';\nimport { createPresignedPost } from '@aws-sdk/s3-presigned-post';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\n\nimport type { Cache } from '@geekmidas/cache';\nimport {\n\ttype DocumentVersion,\n\ttype File,\n\ttype GetUploadParams,\n\ttype GetUploadResponse,\n\ttype StorageClient,\n\tStorageProvider,\n} from './StorageClient';\n\nexport class AmazonStorageClient implements StorageClient {\n\treadonly provider = StorageProvider.AWSS3;\n\n\tstatic create(\n\t\toptions: AmazonStorageClientCreateOptions,\n\t): AmazonStorageClient {\n\t\tconst {\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\taccessKeyId,\n\t\t\tacl,\n\t\t\tendpoint,\n\t\t\tsecretAccessKey,\n\t\t\tforcePathStyle = false,\n\t\t\tcache,\n\t\t} = options;\n\t\tconst hasCredentials = accessKeyId && secretAccessKey;\n\t\tconst credentials = hasCredentials\n\t\t\t? { accessKeyId, secretAccessKey }\n\t\t\t: undefined;\n\n\t\tconst client = new S3Client({\n\t\t\tregion,\n\t\t\tcredentials,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t});\n\n\t\treturn new AmazonStorageClient(client, bucket, acl, cache);\n\t}\n\tconstructor(\n\t\tprivate readonly client: S3Client,\n\t\tprivate readonly bucket: string,\n\t\tprivate readonly acl = AmazonCannedAccessControlList.AuthenticatedRead,\n\t\treadonly cache?: Cache,\n\t) {}\n\n\tprivate createGetObjectCommand(\n\t\tfile: File,\n\t\toverrides?: { VersionId?: string },\n\t): GetObjectCommand {\n\t\tconst disposition =\n\t\t\tfile.disposition ?? (file.name ? 'attachment' : undefined);\n\t\tconst ResponseContentDisposition = disposition\n\t\t\t? file.name\n\t\t\t\t? `${disposition}; filename=${encodeURIComponent(file.name)}`\n\t\t\t\t: disposition\n\t\t\t: undefined;\n\n\t\treturn new GetObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: file.path,\n\t\t\tResponseContentDisposition,\n\t\t\tResponseContentType: file.responseContentType,\n\t\t\t...overrides,\n\t\t});\n\t}\n\n\tgetVersionDownloadURL(file: File, versionId: string): Promise<string> {\n\t\tconst command = this.createGetObjectCommand(file, { VersionId: versionId });\n\t\treturn getSignedUrl(this.client, command, { expiresIn: 60 * 60 * 24 });\n\t}\n\n\tasync getVersions(key: string): Promise<DocumentVersion[]> {\n\t\tconst command = new ListObjectVersionsCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tPrefix: key,\n\t\t});\n\n\t\tconst { Versions = [] } = await this.client.send(command);\n\n\t\treturn Versions.map((version) => ({\n\t\t\tid: version.VersionId || '',\n\t\t\tcreatedAt: version.LastModified || new Date(),\n\t\t}));\n\t}\n\n\tasync getDownloadURL(file: File, expiresIn = 60 * 60): Promise<string> {\n\t\tconst cacheKey = `download-url:${file.path}`;\n\t\tconst cachedURL = await this.cache?.get<string>(cacheKey);\n\n\t\tif (cachedURL) {\n\t\t\treturn cachedURL;\n\t\t}\n\n\t\tconst command = this.createGetObjectCommand(file);\n\t\tconst url = await getSignedUrl(this.client, command, { expiresIn });\n\t\tconst cacheExpiration = Math.max(expiresIn - 60, 0);\n\n\t\tif (cacheExpiration) {\n\t\t\tawait this.cache?.set(cacheKey, url, cacheExpiration);\n\t\t}\n\n\t\treturn url;\n\t}\n\n\tasync getUploadURL(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 60 * 60,\n\t): Promise<string> {\n\t\tconst command = new PutObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: params.path,\n\t\t\tContentType: params.contentType,\n\t\t\tContentLength: params.contentLength,\n\t\t});\n\n\t\treturn getSignedUrl(this.client, command, { expiresIn });\n\t}\n\n\tasync getUpload(\n\t\tparams: GetUploadParams,\n\t\texpiresIn = 5,\n\t): Promise<GetUploadResponse> {\n\t\tconst { path } = params;\n\t\tconst { fields: values, url } = await createPresignedPost(this.client, {\n\t\t\tExpires: expiresIn * 60,\n\t\t\tBucket: this.bucket,\n\t\t\tFields: {\n\t\t\t\tacl: this.acl,\n\t\t\t},\n\t\t\tConditions: [\n\t\t\t\t// content length restrictions: 0-1MB]\n\t\t\t\t// ['content-length-range', 0, contentLength],\n\t\t\t\t// specify content-type to be more generic- images only\n\t\t\t\t// ['starts-with', '$Content-Type', 'image/'],\n\t\t\t\t// ['starts-with', '$Content-Type', contentType],\n\t\t\t],\n\t\t\tKey: path,\n\t\t});\n\n\t\tconst keys = Object.keys(values);\n\t\tconst fields = keys.map((key) => ({ key, value: values[key] || '' }));\n\n\t\treturn { url, fields };\n\t}\n\n\tasync upload(\n\t\tkey: string,\n\t\tdata: string | Buffer,\n\t\tcontentType: string,\n\t): Promise<void> {\n\t\tconst Body = typeof data === 'string' ? Buffer.from(data, 'base64') : data;\n\n\t\tconst params = {\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t\tBody,\n\t\t\tContentType: contentType,\n\t\t};\n\n\t\tconst command = new PutObjectCommand(params);\n\n\t\tawait this.client.send(command);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tconst command = new DeleteObjectCommand({\n\t\t\tBucket: this.bucket,\n\t\t\tKey: key,\n\t\t});\n\n\t\tawait this.client.send(command);\n\t}\n}\n\nexport enum AmazonCannedAccessControlList {\n\tAuthenticatedRead = 'authenticated-read',\n\tPrivate = 'private',\n\tPublicRead = 'public-read',\n\tPublicReadWrite = 'public-read-write',\n\tAwsExecRead = 'aws-exec-read',\n\tBucketOwnerRead = 'bucket-owner-read',\n\tBucketOwnerFullControl = 'bucket-owner-full-control',\n\tLogDeliveryWrite = 'log-delivery-write',\n}\n\ninterface AmazonStorageClientCreateOptions {\n\tbucket: string;\n\tregion?: string;\n\tacl?: AmazonCannedAccessControlList;\n\taccessKeyId?: string;\n\tsecretAccessKey?: string;\n\tendpoint?: string;\n\tforcePathStyle?: boolean;\n\tcache?: Cache;\n}\n","/**\n * The S3 driver — `s3://` URLs to an {@link AmazonStorageClient}.\n *\n * Registered by whoever assembles the application, so neutral code can build a\n * client from a URL without importing this file or the AWS SDK behind it.\n */\n\nimport { AmazonStorageClient } from './AmazonStorageClient';\nimport type { StorageDriver } from './registry';\nimport { parse } from './s3Url';\n\nexport const s3Driver: StorageDriver = {\n\tscheme: 's3:',\n\tcreate(url) {\n\t\t// The URL's own key pair, when it has one, wins over the SDK's default\n\t\t// chain; without one the client resolves credentials as it always has.\n\t\tconst {\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t\taccessKeyId,\n\t\t\tsecretAccessKey,\n\t\t} = parse(url);\n\t\treturn AmazonStorageClient.create({\n\t\t\tbucket,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\tforcePathStyle,\n\t\t\taccessKeyId,\n\t\t\tsecretAccessKey,\n\t\t});\n\t},\n};\n"],"mappings":"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"}
@@ -13,14 +13,37 @@
13
13
  */
14
14
  /** Base for anything wrong with a storage URL, so callers may catch broadly. */
15
15
  var StorageUrlError = class extends Error {
16
- /** The URL that could not be used. */
16
+ /**
17
+ * The URL that could not be used, with any credentials it carried replaced
18
+ * by `REDACTED` — an error is logged, and a key in its userinfo would be
19
+ * logged with it.
20
+ */
17
21
  url;
18
22
  constructor(url, message) {
19
23
  super(message);
20
- this.url = url;
24
+ this.url = redactStorageUrl(url);
21
25
  this.name = new.target.name;
22
26
  }
23
27
  };
28
+ /**
29
+ * Replace a URL's userinfo with `REDACTED`, so `s3://KEY:SECRET@uploads` can be
30
+ * logged as `s3://REDACTED@uploads`. A URL without userinfo comes back as is.
31
+ *
32
+ * A string the URL parser rejects is still redacted, up to its last `@`: an
33
+ * unencoded `/` in a secret is the commonest way to write a malformed one, and
34
+ * that string is exactly the one that ends up in an error.
35
+ */
36
+ function redactStorageUrl(url) {
37
+ try {
38
+ const parsed = new URL(url);
39
+ if (!parsed.username && !parsed.password) return url;
40
+ parsed.username = "REDACTED";
41
+ parsed.password = "";
42
+ return parsed.toString();
43
+ } catch {
44
+ return url.replace(/^([a-z][a-z0-9+.-]*:\/\/).*@/i, "$1REDACTED@");
45
+ }
46
+ }
24
47
  /** The string is not a URL at all. */
25
48
  var MalformedStorageUrl = class extends StorageUrlError {
26
49
  constructor(url) {
@@ -56,6 +79,20 @@ var MissingStorageBucket = class extends StorageUrlError {
56
79
  }
57
80
  };
58
81
  /**
82
+ * The URL's userinfo carries one half of a key pair: an access key with no
83
+ * secret, or a secret with no access key. Either both are in the URL, or
84
+ * neither is and the provider's default credential chain applies — half a pair
85
+ * is never silently completed from the environment.
86
+ */
87
+ var IncompleteStorageCredentials = class extends StorageUrlError {
88
+ /** Which half is missing: `'accessKeyId'` or `'secretAccessKey'`. */
89
+ missing;
90
+ constructor(url, missing) {
91
+ super(url, "Storage URL credentials need both halves: write them as KEY:SECRET@bucket, or leave them out to use the default credential chain");
92
+ this.missing = missing;
93
+ }
94
+ };
95
+ /**
59
96
  * No driver is registered for the URL's scheme.
60
97
  *
61
98
  * Usually means the build pinned a driver for a different target, or an entry
@@ -74,6 +111,12 @@ var UnregisteredStorageScheme = class extends StorageUrlError {
74
111
  }
75
112
  };
76
113
  //#endregion
114
+ Object.defineProperty(exports, "IncompleteStorageCredentials", {
115
+ enumerable: true,
116
+ get: function() {
117
+ return IncompleteStorageCredentials;
118
+ }
119
+ });
77
120
  Object.defineProperty(exports, "MalformedStorageUrl", {
78
121
  enumerable: true,
79
122
  get: function() {
@@ -104,5 +147,11 @@ Object.defineProperty(exports, "UnregisteredStorageScheme", {
104
147
  return UnregisteredStorageScheme;
105
148
  }
106
149
  });
150
+ Object.defineProperty(exports, "redactStorageUrl", {
151
+ enumerable: true,
152
+ get: function() {
153
+ return redactStorageUrl;
154
+ }
155
+ });
107
156
 
108
- //# sourceMappingURL=errors-Bexjg-j-.cjs.map
157
+ //# sourceMappingURL=errors-BB8jdHif.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-BB8jdHif.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Storage errors — shared by every implementation, not just S3.\n *\n * A `gs://` or Azure client hits the same three problems as the S3 one: the\n * string is not a URL, it is a URL for a different provider, or it addresses no\n * bucket. Each carries the provider-specific detail as a field, so the type is\n * common while the specifics stay accurate.\n *\n * Messages state the rule, which is constant, and never interpolate the value.\n * An interpolated message cannot be matched on, reads differently every time it\n * is thrown, and carries user input into every log line that touches it.\n */\n\n/** Base for anything wrong with a storage URL, so callers may catch broadly. */\nexport abstract class StorageUrlError extends Error {\n\t/**\n\t * The URL that could not be used, with any credentials it carried replaced\n\t * by `REDACTED` — an error is logged, and a key in its userinfo would be\n\t * logged with it.\n\t */\n\treadonly url: string;\n\n\tconstructor(url: string, message: string) {\n\t\tsuper(message);\n\t\tthis.url = redactStorageUrl(url);\n\t\tthis.name = new.target.name;\n\t}\n}\n\n/**\n * Replace a URL's userinfo with `REDACTED`, so `s3://KEY:SECRET@uploads` can be\n * logged as `s3://REDACTED@uploads`. A URL without userinfo comes back as is.\n *\n * A string the URL parser rejects is still redacted, up to its last `@`: an\n * unencoded `/` in a secret is the commonest way to write a malformed one, and\n * that string is exactly the one that ends up in an error.\n */\nexport function redactStorageUrl(url: string): string {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (!parsed.username && !parsed.password) return url;\n\t\tparsed.username = 'REDACTED';\n\t\tparsed.password = '';\n\t\treturn parsed.toString();\n\t} catch {\n\t\treturn url.replace(/^([a-z][a-z0-9+.-]*:\\/\\/).*@/i, '$1REDACTED@');\n\t}\n}\n\n/** The string is not a URL at all. */\nexport class MalformedStorageUrl extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Not a valid URL');\n\t}\n}\n\n/**\n * A URL, but for a different provider — a `gs://` URL reaching the S3 client,\n * or the reverse. The usual cause is a construct resolving against the wrong\n * target, which is worth distinguishing from a malformed string.\n */\nexport class UnexpectedStorageScheme extends StorageUrlError {\n\t/** The scheme this client handles, e.g. `'s3:'`. */\n\treadonly expected: string;\n\t/** The scheme the URL actually carried, e.g. `'gs:'`. */\n\treadonly actual: string;\n\n\tconstructor(url: string, expected: string, actual: string) {\n\t\tsuper(url, 'Storage URL is for a different provider');\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\n/**\n * A well-formed URL that addresses no bucket.\n *\n * \"Bucket\" is the term S3 and GCS share; Azure calls the same thing a\n * container. One error covers all three — the word in the message is the\n * common one rather than a new abstraction nobody uses.\n */\nexport class MissingStorageBucket extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Storage URL must address a bucket');\n\t}\n}\n\n/**\n * The URL's userinfo carries one half of a key pair: an access key with no\n * secret, or a secret with no access key. Either both are in the URL, or\n * neither is and the provider's default credential chain applies — half a pair\n * is never silently completed from the environment.\n */\nexport class IncompleteStorageCredentials extends StorageUrlError {\n\t/** Which half is missing: `'accessKeyId'` or `'secretAccessKey'`. */\n\treadonly missing: 'accessKeyId' | 'secretAccessKey';\n\n\tconstructor(url: string, missing: 'accessKeyId' | 'secretAccessKey') {\n\t\tsuper(\n\t\t\turl,\n\t\t\t'Storage URL credentials need both halves: write them as KEY:SECRET@bucket, or leave them out to use the default credential chain',\n\t\t);\n\t\tthis.missing = missing;\n\t}\n}\n\n/**\n * No driver is registered for the URL's scheme.\n *\n * Usually means the build pinned a driver for a different target, or an entry\n * point registered nothing at all — so the registered schemes are carried\n * alongside, since \"what *is* available\" is the actionable half.\n */\nexport class UnregisteredStorageScheme extends StorageUrlError {\n\t/** The scheme that had no driver, e.g. `'gs:'`. */\n\treadonly scheme: string;\n\t/** What is registered, so the caller can see what was pinned instead. */\n\treadonly registered: readonly string[];\n\n\tconstructor(url: string, scheme: string, registered: readonly string[]) {\n\t\tsuper(url, 'No storage driver is registered for this URL scheme');\n\t\tthis.scheme = scheme;\n\t\tthis.registered = registered;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAsB,kBAAtB,cAA8C,MAAM;;;;;;CAMnD;CAEA,YAAY,KAAa,SAAiB;EACzC,MAAM,OAAO;EACb,KAAK,MAAM,iBAAiB,GAAG;EAC/B,KAAK,OAAO,WAAW;CACxB;AACD;;;;;;;;;AAUA,SAAgB,iBAAiB,KAAqB;CACrD,IAAI;EACH,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,IAAI,CAAC,OAAO,YAAY,CAAC,OAAO,UAAU,OAAO;EACjD,OAAO,WAAW;EAClB,OAAO,WAAW;EAClB,OAAO,OAAO,SAAS;CACxB,QAAQ;EACP,OAAO,IAAI,QAAQ,iCAAiC,aAAa;CAClE;AACD;;AAGA,IAAa,sBAAb,cAAyC,gBAAgB;CACxD,YAAY,KAAa;EACxB,MAAM,KAAK,iBAAiB;CAC7B;AACD;;;;;;AAOA,IAAa,0BAAb,cAA6C,gBAAgB;;CAE5D;;CAEA;CAEA,YAAY,KAAa,UAAkB,QAAgB;EAC1D,MAAM,KAAK,yCAAyC;EACpD,KAAK,WAAW;EAChB,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,uBAAb,cAA0C,gBAAgB;CACzD,YAAY,KAAa;EACxB,MAAM,KAAK,mCAAmC;CAC/C;AACD;;;;;;;AAQA,IAAa,+BAAb,cAAkD,gBAAgB;;CAEjE;CAEA,YAAY,KAAa,SAA4C;EACpE,MACC,KACA,kIACD;EACA,KAAK,UAAU;CAChB;AACD;;;;;;;;AASA,IAAa,4BAAb,cAA+C,gBAAgB;;CAE9D;;CAEA;CAEA,YAAY,KAAa,QAAgB,YAA+B;EACvE,MAAM,KAAK,qDAAqD;EAChE,KAAK,SAAS;EACd,KAAK,aAAa;CACnB;AACD"}
@@ -13,14 +13,37 @@
13
13
  */
14
14
  /** Base for anything wrong with a storage URL, so callers may catch broadly. */
15
15
  var StorageUrlError = class extends Error {
16
- /** The URL that could not be used. */
16
+ /**
17
+ * The URL that could not be used, with any credentials it carried replaced
18
+ * by `REDACTED` — an error is logged, and a key in its userinfo would be
19
+ * logged with it.
20
+ */
17
21
  url;
18
22
  constructor(url, message) {
19
23
  super(message);
20
- this.url = url;
24
+ this.url = redactStorageUrl(url);
21
25
  this.name = new.target.name;
22
26
  }
23
27
  };
28
+ /**
29
+ * Replace a URL's userinfo with `REDACTED`, so `s3://KEY:SECRET@uploads` can be
30
+ * logged as `s3://REDACTED@uploads`. A URL without userinfo comes back as is.
31
+ *
32
+ * A string the URL parser rejects is still redacted, up to its last `@`: an
33
+ * unencoded `/` in a secret is the commonest way to write a malformed one, and
34
+ * that string is exactly the one that ends up in an error.
35
+ */
36
+ function redactStorageUrl(url) {
37
+ try {
38
+ const parsed = new URL(url);
39
+ if (!parsed.username && !parsed.password) return url;
40
+ parsed.username = "REDACTED";
41
+ parsed.password = "";
42
+ return parsed.toString();
43
+ } catch {
44
+ return url.replace(/^([a-z][a-z0-9+.-]*:\/\/).*@/i, "$1REDACTED@");
45
+ }
46
+ }
24
47
  /** The string is not a URL at all. */
25
48
  var MalformedStorageUrl = class extends StorageUrlError {
26
49
  constructor(url) {
@@ -56,6 +79,20 @@ var MissingStorageBucket = class extends StorageUrlError {
56
79
  }
57
80
  };
58
81
  /**
82
+ * The URL's userinfo carries one half of a key pair: an access key with no
83
+ * secret, or a secret with no access key. Either both are in the URL, or
84
+ * neither is and the provider's default credential chain applies — half a pair
85
+ * is never silently completed from the environment.
86
+ */
87
+ var IncompleteStorageCredentials = class extends StorageUrlError {
88
+ /** Which half is missing: `'accessKeyId'` or `'secretAccessKey'`. */
89
+ missing;
90
+ constructor(url, missing) {
91
+ super(url, "Storage URL credentials need both halves: write them as KEY:SECRET@bucket, or leave them out to use the default credential chain");
92
+ this.missing = missing;
93
+ }
94
+ };
95
+ /**
59
96
  * No driver is registered for the URL's scheme.
60
97
  *
61
98
  * Usually means the build pinned a driver for a different target, or an entry
@@ -74,6 +111,6 @@ var UnregisteredStorageScheme = class extends StorageUrlError {
74
111
  }
75
112
  };
76
113
  //#endregion
77
- export { UnregisteredStorageScheme as a, UnexpectedStorageScheme as i, MissingStorageBucket as n, StorageUrlError as r, MalformedStorageUrl as t };
114
+ export { UnexpectedStorageScheme as a, StorageUrlError as i, MalformedStorageUrl as n, UnregisteredStorageScheme as o, MissingStorageBucket as r, redactStorageUrl as s, IncompleteStorageCredentials as t };
78
115
 
79
- //# sourceMappingURL=errors-CpYcpJCY.mjs.map
116
+ //# sourceMappingURL=errors-COXs1iNg.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-COXs1iNg.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Storage errors — shared by every implementation, not just S3.\n *\n * A `gs://` or Azure client hits the same three problems as the S3 one: the\n * string is not a URL, it is a URL for a different provider, or it addresses no\n * bucket. Each carries the provider-specific detail as a field, so the type is\n * common while the specifics stay accurate.\n *\n * Messages state the rule, which is constant, and never interpolate the value.\n * An interpolated message cannot be matched on, reads differently every time it\n * is thrown, and carries user input into every log line that touches it.\n */\n\n/** Base for anything wrong with a storage URL, so callers may catch broadly. */\nexport abstract class StorageUrlError extends Error {\n\t/**\n\t * The URL that could not be used, with any credentials it carried replaced\n\t * by `REDACTED` — an error is logged, and a key in its userinfo would be\n\t * logged with it.\n\t */\n\treadonly url: string;\n\n\tconstructor(url: string, message: string) {\n\t\tsuper(message);\n\t\tthis.url = redactStorageUrl(url);\n\t\tthis.name = new.target.name;\n\t}\n}\n\n/**\n * Replace a URL's userinfo with `REDACTED`, so `s3://KEY:SECRET@uploads` can be\n * logged as `s3://REDACTED@uploads`. A URL without userinfo comes back as is.\n *\n * A string the URL parser rejects is still redacted, up to its last `@`: an\n * unencoded `/` in a secret is the commonest way to write a malformed one, and\n * that string is exactly the one that ends up in an error.\n */\nexport function redactStorageUrl(url: string): string {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tif (!parsed.username && !parsed.password) return url;\n\t\tparsed.username = 'REDACTED';\n\t\tparsed.password = '';\n\t\treturn parsed.toString();\n\t} catch {\n\t\treturn url.replace(/^([a-z][a-z0-9+.-]*:\\/\\/).*@/i, '$1REDACTED@');\n\t}\n}\n\n/** The string is not a URL at all. */\nexport class MalformedStorageUrl extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Not a valid URL');\n\t}\n}\n\n/**\n * A URL, but for a different provider — a `gs://` URL reaching the S3 client,\n * or the reverse. The usual cause is a construct resolving against the wrong\n * target, which is worth distinguishing from a malformed string.\n */\nexport class UnexpectedStorageScheme extends StorageUrlError {\n\t/** The scheme this client handles, e.g. `'s3:'`. */\n\treadonly expected: string;\n\t/** The scheme the URL actually carried, e.g. `'gs:'`. */\n\treadonly actual: string;\n\n\tconstructor(url: string, expected: string, actual: string) {\n\t\tsuper(url, 'Storage URL is for a different provider');\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\n/**\n * A well-formed URL that addresses no bucket.\n *\n * \"Bucket\" is the term S3 and GCS share; Azure calls the same thing a\n * container. One error covers all three — the word in the message is the\n * common one rather than a new abstraction nobody uses.\n */\nexport class MissingStorageBucket extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Storage URL must address a bucket');\n\t}\n}\n\n/**\n * The URL's userinfo carries one half of a key pair: an access key with no\n * secret, or a secret with no access key. Either both are in the URL, or\n * neither is and the provider's default credential chain applies — half a pair\n * is never silently completed from the environment.\n */\nexport class IncompleteStorageCredentials extends StorageUrlError {\n\t/** Which half is missing: `'accessKeyId'` or `'secretAccessKey'`. */\n\treadonly missing: 'accessKeyId' | 'secretAccessKey';\n\n\tconstructor(url: string, missing: 'accessKeyId' | 'secretAccessKey') {\n\t\tsuper(\n\t\t\turl,\n\t\t\t'Storage URL credentials need both halves: write them as KEY:SECRET@bucket, or leave them out to use the default credential chain',\n\t\t);\n\t\tthis.missing = missing;\n\t}\n}\n\n/**\n * No driver is registered for the URL's scheme.\n *\n * Usually means the build pinned a driver for a different target, or an entry\n * point registered nothing at all — so the registered schemes are carried\n * alongside, since \"what *is* available\" is the actionable half.\n */\nexport class UnregisteredStorageScheme extends StorageUrlError {\n\t/** The scheme that had no driver, e.g. `'gs:'`. */\n\treadonly scheme: string;\n\t/** What is registered, so the caller can see what was pinned instead. */\n\treadonly registered: readonly string[];\n\n\tconstructor(url: string, scheme: string, registered: readonly string[]) {\n\t\tsuper(url, 'No storage driver is registered for this URL scheme');\n\t\tthis.scheme = scheme;\n\t\tthis.registered = registered;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAsB,kBAAtB,cAA8C,MAAM;;;;;;CAMnD;CAEA,YAAY,KAAa,SAAiB;EACzC,MAAM,OAAO;EACb,KAAK,MAAM,iBAAiB,GAAG;EAC/B,KAAK,OAAO,WAAW;CACxB;AACD;;;;;;;;;AAUA,SAAgB,iBAAiB,KAAqB;CACrD,IAAI;EACH,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,IAAI,CAAC,OAAO,YAAY,CAAC,OAAO,UAAU,OAAO;EACjD,OAAO,WAAW;EAClB,OAAO,WAAW;EAClB,OAAO,OAAO,SAAS;CACxB,QAAQ;EACP,OAAO,IAAI,QAAQ,iCAAiC,aAAa;CAClE;AACD;;AAGA,IAAa,sBAAb,cAAyC,gBAAgB;CACxD,YAAY,KAAa;EACxB,MAAM,KAAK,iBAAiB;CAC7B;AACD;;;;;;AAOA,IAAa,0BAAb,cAA6C,gBAAgB;;CAE5D;;CAEA;CAEA,YAAY,KAAa,UAAkB,QAAgB;EAC1D,MAAM,KAAK,yCAAyC;EACpD,KAAK,WAAW;EAChB,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,uBAAb,cAA0C,gBAAgB;CACzD,YAAY,KAAa;EACxB,MAAM,KAAK,mCAAmC;CAC/C;AACD;;;;;;;AAQA,IAAa,+BAAb,cAAkD,gBAAgB;;CAEjE;CAEA,YAAY,KAAa,SAA4C;EACpE,MACC,KACA,kIACD;EACA,KAAK,UAAU;CAChB;AACD;;;;;;;;AASA,IAAa,4BAAb,cAA+C,gBAAgB;;CAE9D;;CAEA;CAEA,YAAY,KAAa,QAAgB,YAA+B;EACvE,MAAM,KAAK,qDAAqD;EAChE,KAAK,SAAS;EACd,KAAK,aAAa;CACnB;AACD"}
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_errors = require("./errors-Bexjg-j-.cjs");
2
+ const require_errors = require("./errors-BB8jdHif.cjs");
3
3
  //#region src/registry.ts
4
4
  /**
5
5
  * Driver registry — how a storage URL becomes a client without the caller
@@ -41,12 +41,14 @@ function schemeOf(url) {
41
41
  return separator === -1 ? "" : url.slice(0, separator + 1);
42
42
  }
43
43
  //#endregion
44
+ exports.IncompleteStorageCredentials = require_errors.IncompleteStorageCredentials;
44
45
  exports.MalformedStorageUrl = require_errors.MalformedStorageUrl;
45
46
  exports.MissingStorageBucket = require_errors.MissingStorageBucket;
46
47
  exports.StorageUrlError = require_errors.StorageUrlError;
47
48
  exports.UnexpectedStorageScheme = require_errors.UnexpectedStorageScheme;
48
49
  exports.UnregisteredStorageScheme = require_errors.UnregisteredStorageScheme;
49
50
  exports.createStorageClient = createStorageClient;
51
+ exports.redactStorageUrl = require_errors.redactStorageUrl;
50
52
  exports.registerStorageDriver = registerStorageDriver;
51
53
  exports.registeredStorageSchemes = registeredStorageSchemes;
52
54
 
package/dist/index.d.cts CHANGED
@@ -14,10 +14,23 @@ import { i as registeredStorageSchemes, l as StorageClient, n as createStorageCl
14
14
  */
15
15
  /** Base for anything wrong with a storage URL, so callers may catch broadly. */
16
16
  export declare abstract class StorageUrlError extends Error {
17
- /** The URL that could not be used. */
17
+ /**
18
+ * The URL that could not be used, with any credentials it carried replaced
19
+ * by `REDACTED` — an error is logged, and a key in its userinfo would be
20
+ * logged with it.
21
+ */
18
22
  readonly url: string;
19
23
  constructor(url: string, message: string);
20
24
  }
25
+ /**
26
+ * Replace a URL's userinfo with `REDACTED`, so `s3://KEY:SECRET@uploads` can be
27
+ * logged as `s3://REDACTED@uploads`. A URL without userinfo comes back as is.
28
+ *
29
+ * A string the URL parser rejects is still redacted, up to its last `@`: an
30
+ * unencoded `/` in a secret is the commonest way to write a malformed one, and
31
+ * that string is exactly the one that ends up in an error.
32
+ */
33
+ export declare function redactStorageUrl(url: string): string;
21
34
  /** The string is not a URL at all. */
22
35
  export declare class MalformedStorageUrl extends StorageUrlError {
23
36
  constructor(url: string);
@@ -44,6 +57,17 @@ export declare class UnexpectedStorageScheme extends StorageUrlError {
44
57
  export declare class MissingStorageBucket extends StorageUrlError {
45
58
  constructor(url: string);
46
59
  }
60
+ /**
61
+ * The URL's userinfo carries one half of a key pair: an access key with no
62
+ * secret, or a secret with no access key. Either both are in the URL, or
63
+ * neither is and the provider's default credential chain applies — half a pair
64
+ * is never silently completed from the environment.
65
+ */
66
+ export declare class IncompleteStorageCredentials extends StorageUrlError {
67
+ /** Which half is missing: `'accessKeyId'` or `'secretAccessKey'`. */
68
+ readonly missing: 'accessKeyId' | 'secretAccessKey';
69
+ constructor(url: string, missing: 'accessKeyId' | 'secretAccessKey');
70
+ }
47
71
  /**
48
72
  * No driver is registered for the URL's scheme.
49
73
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;8BAcsB,wBAAwB;;WAEpC;EAET,YAAY,aAAa;;;qBAQb,4BAA4B;EACxC,YAAY;;;;;;;qBAUA,gCAAgC;;WAEnC;;WAEA;EAET,YAAY,aAAa,kBAAkB;;;;;;;;;qBAc/B,6BAA6B;EACzC,YAAY;;;;;;;;;qBAYA,kCAAkC;;WAErC;;WAEA;EAET,YAAY,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;8BAcsB,wBAAwB;;;;;;WAMpC;EAET,YAAY,aAAa;;;;;;;;;;wBAeV,iBAAiB;;qBAapB,4BAA4B;EACxC,YAAY;;;;;;;qBAUA,gCAAgC;;WAEnC;;WAEA;EAET,YAAY,aAAa,kBAAkB;;;;;;;;;qBAc/B,6BAA6B;EACzC,YAAY;;;;;;;;qBAWA,qCAAqC;;WAExC;EAET,YAAY,aAAa;;;;;;;;;qBAgBb,kCAAkC;;WAErC;;WAEA;EAET,YAAY,aAAa,gBAAgB"}
package/dist/index.d.mts CHANGED
@@ -14,10 +14,23 @@ import { i as registeredStorageSchemes, l as StorageClient, n as createStorageCl
14
14
  */
15
15
  /** Base for anything wrong with a storage URL, so callers may catch broadly. */
16
16
  export declare abstract class StorageUrlError extends Error {
17
- /** The URL that could not be used. */
17
+ /**
18
+ * The URL that could not be used, with any credentials it carried replaced
19
+ * by `REDACTED` — an error is logged, and a key in its userinfo would be
20
+ * logged with it.
21
+ */
18
22
  readonly url: string;
19
23
  constructor(url: string, message: string);
20
24
  }
25
+ /**
26
+ * Replace a URL's userinfo with `REDACTED`, so `s3://KEY:SECRET@uploads` can be
27
+ * logged as `s3://REDACTED@uploads`. A URL without userinfo comes back as is.
28
+ *
29
+ * A string the URL parser rejects is still redacted, up to its last `@`: an
30
+ * unencoded `/` in a secret is the commonest way to write a malformed one, and
31
+ * that string is exactly the one that ends up in an error.
32
+ */
33
+ export declare function redactStorageUrl(url: string): string;
21
34
  /** The string is not a URL at all. */
22
35
  export declare class MalformedStorageUrl extends StorageUrlError {
23
36
  constructor(url: string);
@@ -44,6 +57,17 @@ export declare class UnexpectedStorageScheme extends StorageUrlError {
44
57
  export declare class MissingStorageBucket extends StorageUrlError {
45
58
  constructor(url: string);
46
59
  }
60
+ /**
61
+ * The URL's userinfo carries one half of a key pair: an access key with no
62
+ * secret, or a secret with no access key. Either both are in the URL, or
63
+ * neither is and the provider's default credential chain applies — half a pair
64
+ * is never silently completed from the environment.
65
+ */
66
+ export declare class IncompleteStorageCredentials extends StorageUrlError {
67
+ /** Which half is missing: `'accessKeyId'` or `'secretAccessKey'`. */
68
+ readonly missing: 'accessKeyId' | 'secretAccessKey';
69
+ constructor(url: string, missing: 'accessKeyId' | 'secretAccessKey');
70
+ }
47
71
  /**
48
72
  * No driver is registered for the URL's scheme.
49
73
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;8BAcsB,wBAAwB;;WAEpC;EAET,YAAY,aAAa;;;qBAQb,4BAA4B;EACxC,YAAY;;;;;;;qBAUA,gCAAgC;;WAEnC;;WAEA;EAET,YAAY,aAAa,kBAAkB;;;;;;;;;qBAc/B,6BAA6B;EACzC,YAAY;;;;;;;;;qBAYA,kCAAkC;;WAErC;;WAEA;EAET,YAAY,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;8BAcsB,wBAAwB;;;;;;WAMpC;EAET,YAAY,aAAa;;;;;;;;;;wBAeV,iBAAiB;;qBAapB,4BAA4B;EACxC,YAAY;;;;;;;qBAUA,gCAAgC;;WAEnC;;WAEA;EAET,YAAY,aAAa,kBAAkB;;;;;;;;;qBAc/B,6BAA6B;EACzC,YAAY;;;;;;;;qBAWA,qCAAqC;;WAExC;EAET,YAAY,aAAa;;;;;;;;;qBAgBb,kCAAkC;;WAErC;;WAEA;EAET,YAAY,aAAa,gBAAgB"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as UnregisteredStorageScheme, i as UnexpectedStorageScheme, n as MissingStorageBucket, r as StorageUrlError, t as MalformedStorageUrl } from "./errors-CpYcpJCY.mjs";
1
+ import { a as UnexpectedStorageScheme, i as StorageUrlError, n as MalformedStorageUrl, o as UnregisteredStorageScheme, r as MissingStorageBucket, s as redactStorageUrl, t as IncompleteStorageCredentials } from "./errors-COXs1iNg.mjs";
2
2
  //#region src/registry.ts
3
3
  /**
4
4
  * Driver registry — how a storage URL becomes a client without the caller
@@ -40,6 +40,6 @@ function schemeOf(url) {
40
40
  return separator === -1 ? "" : url.slice(0, separator + 1);
41
41
  }
42
42
  //#endregion
43
- export { MalformedStorageUrl, MissingStorageBucket, StorageUrlError, UnexpectedStorageScheme, UnregisteredStorageScheme, createStorageClient, registerStorageDriver, registeredStorageSchemes };
43
+ export { IncompleteStorageCredentials, MalformedStorageUrl, MissingStorageBucket, StorageUrlError, UnexpectedStorageScheme, UnregisteredStorageScheme, createStorageClient, redactStorageUrl, registerStorageDriver, registeredStorageSchemes };
44
44
 
45
45
  //# sourceMappingURL=index.mjs.map
package/dist/s3Url.cjs CHANGED
@@ -11,7 +11,7 @@ var __exportAll = (all, no_symbols) => {
11
11
  return target;
12
12
  };
13
13
  //#endregion
14
- const require_errors = require("./errors-Bexjg-j-.cjs");
14
+ const require_errors = require("./errors-BB8jdHif.cjs");
15
15
  //#region src/s3Url.ts
16
16
  /**
17
17
  * The `s3://` URL codec — how an S3 bucket is addressed as a single string.
@@ -32,21 +32,29 @@ const SCHEME = "s3:";
32
32
  /**
33
33
  * Compose an address into a URL.
34
34
  *
35
- * Credentials never appear. The AWS SDK resolves them from its own chain — an
36
- * execution role when deployed, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`
37
- * locally — so the URL stays safe to log and identical across environments that
38
- * differ only in who is calling.
35
+ * Credentials appear only when the address carries them, percent-encoded —
36
+ * AWS secrets routinely contain `/` and `+`. An address without them builds a
37
+ * URL with no userinfo, which leaves the caller to the SDK's own chain.
39
38
  */
40
39
  function build(address) {
41
- const { bucket, region, endpoint, forcePathStyle } = address;
40
+ const { bucket, region, endpoint, forcePathStyle, accessKeyId, secretAccessKey } = address;
42
41
  if (!bucket) throw new require_errors.MissingStorageBucket("");
43
42
  const url = new URL(`${SCHEME}//${bucket}`);
43
+ if (accessKeyId || secretAccessKey) {
44
+ if (!accessKeyId) throw new require_errors.IncompleteStorageCredentials(url.toString(), "accessKeyId");
45
+ if (!secretAccessKey) throw new require_errors.IncompleteStorageCredentials(url.toString(), "secretAccessKey");
46
+ url.username = encodeURIComponent(accessKeyId);
47
+ url.password = encodeURIComponent(secretAccessKey);
48
+ }
44
49
  if (region) url.searchParams.set("region", region);
45
50
  if (endpoint) url.searchParams.set("endpoint", endpoint);
46
51
  if (forcePathStyle) url.searchParams.set("forcePathStyle", "true");
47
52
  return url.toString();
48
53
  }
49
- /** Parse a URL back into an address. Throws if it is not an `s3://` URL. */
54
+ /**
55
+ * Parse a URL back into an address. Throws if it is not an `s3://` URL, or if
56
+ * its userinfo carries only one half of a key pair.
57
+ */
50
58
  function parse(url) {
51
59
  let parsed;
52
60
  try {
@@ -60,13 +68,34 @@ function parse(url) {
60
68
  const region = parsed.searchParams.get("region") ?? void 0;
61
69
  const endpoint = parsed.searchParams.get("endpoint") ?? void 0;
62
70
  const forcePathStyle = parsed.searchParams.get("forcePathStyle") === "true" ? true : void 0;
71
+ const credentials = credentialsOf(url, parsed);
63
72
  return {
64
73
  bucket,
65
74
  ...region ? { region } : {},
66
75
  ...endpoint ? { endpoint } : {},
67
- ...forcePathStyle ? { forcePathStyle } : {}
76
+ ...forcePathStyle ? { forcePathStyle } : {},
77
+ ...credentials
78
+ };
79
+ }
80
+ /** The URL's userinfo as a key pair: both halves, or nothing. */
81
+ function credentialsOf(url, parsed) {
82
+ const accessKeyId = decode(url, parsed.username);
83
+ const secretAccessKey = decode(url, parsed.password);
84
+ if (!accessKeyId && !secretAccessKey) return {};
85
+ if (!accessKeyId) throw new require_errors.IncompleteStorageCredentials(url, "accessKeyId");
86
+ if (!secretAccessKey) throw new require_errors.IncompleteStorageCredentials(url, "secretAccessKey");
87
+ return {
88
+ accessKeyId,
89
+ secretAccessKey
68
90
  };
69
91
  }
92
+ function decode(url, value) {
93
+ try {
94
+ return decodeURIComponent(value);
95
+ } catch {
96
+ throw new require_errors.MalformedStorageUrl(url);
97
+ }
98
+ }
70
99
  //#endregion
71
100
  exports.build = build;
72
101
  exports.parse = parse;
@@ -1 +1 @@
1
- {"version":3,"file":"s3Url.cjs","names":["MissingStorageBucket","MalformedStorageUrl","UnexpectedStorageScheme"],"sources":["../src/s3Url.ts"],"sourcesContent":["/**\n * The `s3://` URL codec — how an S3 bucket is addressed as a single string.\n *\n * It lives beside the client rather than in a neutral package because `bucket`,\n * `region`, and `forcePathStyle` are S3's vocabulary. The neutral layer only\n * knows that a construct provides *one URL*; what that URL says is between the\n * component that composes it and the client that parses it. A `gs://` codec\n * would sit beside its own client and share nothing with this one.\n *\n * Both directions live here so they cannot drift: `parse(build(x))` is `x`.\n */\n\nimport {\n\tMalformedStorageUrl,\n\tMissingStorageBucket,\n\tUnexpectedStorageScheme,\n} from './errors';\n\n/** What an S3 URL addresses. Credentials are deliberately absent — see below. */\nexport interface S3Address {\n\tbucket: string;\n\t/** Read off the bucket, never inherited: a bucket may live in another region. */\n\tregion?: string;\n\t/** Set for S3-compatible backends such as MinIO. */\n\tendpoint?: string;\n\t/** MinIO and most S3-compatible servers need path-style addressing. */\n\tforcePathStyle?: boolean;\n}\n\nconst SCHEME = 's3:';\n\n/**\n * Compose an address into a URL.\n *\n * Credentials never appear. The AWS SDK resolves them from its own chain — an\n * execution role when deployed, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`\n * locally — so the URL stays safe to log and identical across environments that\n * differ only in who is calling.\n */\nexport function build(address: S3Address): string {\n\tconst { bucket, region, endpoint, forcePathStyle } = address;\n\tif (!bucket) throw new MissingStorageBucket('');\n\n\tconst url = new URL(`${SCHEME}//${bucket}`);\n\tif (region) url.searchParams.set('region', region);\n\tif (endpoint) url.searchParams.set('endpoint', endpoint);\n\tif (forcePathStyle) url.searchParams.set('forcePathStyle', 'true');\n\treturn url.toString();\n}\n\n/** Parse a URL back into an address. Throws if it is not an `s3://` URL. */\nexport function parse(url: string): S3Address {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new MalformedStorageUrl(url);\n\t}\n\n\tif (parsed.protocol !== SCHEME) {\n\t\tthrow new UnexpectedStorageScheme(url, SCHEME, parsed.protocol);\n\t}\n\n\tconst bucket = parsed.hostname;\n\tif (!bucket) throw new MissingStorageBucket(url);\n\n\tconst region = parsed.searchParams.get('region') ?? undefined;\n\tconst endpoint = parsed.searchParams.get('endpoint') ?? undefined;\n\tconst forcePathStyle =\n\t\tparsed.searchParams.get('forcePathStyle') === 'true' ? true : undefined;\n\n\treturn {\n\t\tbucket,\n\t\t...(region ? { region } : {}),\n\t\t...(endpoint ? { endpoint } : {}),\n\t\t...(forcePathStyle ? { forcePathStyle } : {}),\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,SAAS;;;;;;;;;AAUf,SAAgB,MAAM,SAA4B;CACjD,MAAM,EAAE,QAAQ,QAAQ,UAAU,mBAAmB;CACrD,IAAI,CAAC,QAAQ,MAAM,IAAIA,eAAAA,qBAAqB,EAAE;CAE9C,MAAM,MAAM,IAAI,IAAI,GAAG,OAAO,IAAI,QAAQ;CAC1C,IAAI,QAAQ,IAAI,aAAa,IAAI,UAAU,MAAM;CACjD,IAAI,UAAU,IAAI,aAAa,IAAI,YAAY,QAAQ;CACvD,IAAI,gBAAgB,IAAI,aAAa,IAAI,kBAAkB,MAAM;CACjE,OAAO,IAAI,SAAS;AACrB;;AAGA,SAAgB,MAAM,KAAwB;CAC7C,IAAI;CACJ,IAAI;EACH,SAAS,IAAI,IAAI,GAAG;CACrB,QAAQ;EACP,MAAM,IAAIC,eAAAA,oBAAoB,GAAG;CAClC;CAEA,IAAI,OAAO,aAAa,QACvB,MAAM,IAAIC,eAAAA,wBAAwB,KAAK,QAAQ,OAAO,QAAQ;CAG/D,MAAM,SAAS,OAAO;CACtB,IAAI,CAAC,QAAQ,MAAM,IAAIF,eAAAA,qBAAqB,GAAG;CAE/C,MAAM,SAAS,OAAO,aAAa,IAAI,QAAQ,KAAK,KAAA;CACpD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU,KAAK,KAAA;CACxD,MAAM,iBACL,OAAO,aAAa,IAAI,gBAAgB,MAAM,SAAS,OAAO,KAAA;CAE/D,OAAO;EACN;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;CAC5C;AACD"}
1
+ {"version":3,"file":"s3Url.cjs","names":["MissingStorageBucket","IncompleteStorageCredentials","MalformedStorageUrl","UnexpectedStorageScheme"],"sources":["../src/s3Url.ts"],"sourcesContent":["/**\n * The `s3://` URL codec — how an S3 bucket is addressed as a single string.\n *\n * It lives beside the client rather than in a neutral package because `bucket`,\n * `region`, and `forcePathStyle` are S3's vocabulary. The neutral layer only\n * knows that a construct provides *one URL*; what that URL says is between the\n * component that composes it and the client that parses it. A `gs://` codec\n * would sit beside its own client and share nothing with this one.\n *\n * Both directions live here so they cannot drift: `parse(build(x))` is `x`.\n */\n\nimport {\n\tIncompleteStorageCredentials,\n\tMalformedStorageUrl,\n\tMissingStorageBucket,\n\tUnexpectedStorageScheme,\n} from './errors';\n\n/**\n * What an S3 URL addresses, and optionally who it calls as.\n *\n * Credentials are optional and come as a pair. With them, the URL is a key\n * scoped to this one bucket (`s3://KEY:SECRET@uploads`); without them the AWS\n * SDK resolves its own chain — an execution role when deployed,\n * `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` otherwise. The URL's pair wins\n * over that chain.\n */\nexport interface S3Address {\n\tbucket: string;\n\t/** Read off the bucket, never inherited: a bucket may live in another region. */\n\tregion?: string;\n\t/** Set for S3-compatible backends such as MinIO. */\n\tendpoint?: string;\n\t/** MinIO and most S3-compatible servers need path-style addressing. */\n\tforcePathStyle?: boolean;\n\t/** The URL's own access key; always paired with `secretAccessKey`. */\n\taccessKeyId?: string;\n\t/** The URL's own secret; always paired with `accessKeyId`. */\n\tsecretAccessKey?: string;\n}\n\nconst SCHEME = 's3:';\n\n/**\n * Compose an address into a URL.\n *\n * Credentials appear only when the address carries them, percent-encoded —\n * AWS secrets routinely contain `/` and `+`. An address without them builds a\n * URL with no userinfo, which leaves the caller to the SDK's own chain.\n */\nexport function build(address: S3Address): string {\n\tconst {\n\t\tbucket,\n\t\tregion,\n\t\tendpoint,\n\t\tforcePathStyle,\n\t\taccessKeyId,\n\t\tsecretAccessKey,\n\t} = address;\n\tif (!bucket) throw new MissingStorageBucket('');\n\n\tconst url = new URL(`${SCHEME}//${bucket}`);\n\tif (accessKeyId || secretAccessKey) {\n\t\tif (!accessKeyId) {\n\t\t\tthrow new IncompleteStorageCredentials(url.toString(), 'accessKeyId');\n\t\t}\n\t\tif (!secretAccessKey) {\n\t\t\tthrow new IncompleteStorageCredentials(url.toString(), 'secretAccessKey');\n\t\t}\n\t\turl.username = encodeURIComponent(accessKeyId);\n\t\turl.password = encodeURIComponent(secretAccessKey);\n\t}\n\tif (region) url.searchParams.set('region', region);\n\tif (endpoint) url.searchParams.set('endpoint', endpoint);\n\tif (forcePathStyle) url.searchParams.set('forcePathStyle', 'true');\n\treturn url.toString();\n}\n\n/**\n * Parse a URL back into an address. Throws if it is not an `s3://` URL, or if\n * its userinfo carries only one half of a key pair.\n */\nexport function parse(url: string): S3Address {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new MalformedStorageUrl(url);\n\t}\n\n\tif (parsed.protocol !== SCHEME) {\n\t\tthrow new UnexpectedStorageScheme(url, SCHEME, parsed.protocol);\n\t}\n\n\tconst bucket = parsed.hostname;\n\tif (!bucket) throw new MissingStorageBucket(url);\n\n\tconst region = parsed.searchParams.get('region') ?? undefined;\n\tconst endpoint = parsed.searchParams.get('endpoint') ?? undefined;\n\tconst forcePathStyle =\n\t\tparsed.searchParams.get('forcePathStyle') === 'true' ? true : undefined;\n\tconst credentials = credentialsOf(url, parsed);\n\n\treturn {\n\t\tbucket,\n\t\t...(region ? { region } : {}),\n\t\t...(endpoint ? { endpoint } : {}),\n\t\t...(forcePathStyle ? { forcePathStyle } : {}),\n\t\t...credentials,\n\t};\n}\n\n/** The URL's userinfo as a key pair: both halves, or nothing. */\nfunction credentialsOf(\n\turl: string,\n\tparsed: URL,\n): Pick<S3Address, 'accessKeyId' | 'secretAccessKey'> {\n\tconst accessKeyId = decode(url, parsed.username);\n\tconst secretAccessKey = decode(url, parsed.password);\n\tif (!accessKeyId && !secretAccessKey) return {};\n\tif (!accessKeyId) {\n\t\tthrow new IncompleteStorageCredentials(url, 'accessKeyId');\n\t}\n\tif (!secretAccessKey) {\n\t\tthrow new IncompleteStorageCredentials(url, 'secretAccessKey');\n\t}\n\treturn { accessKeyId, secretAccessKey };\n}\n\nfunction decode(url: string, value: string): string {\n\ttry {\n\t\treturn decodeURIComponent(value);\n\t} catch {\n\t\tthrow new MalformedStorageUrl(url);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,SAAS;;;;;;;;AASf,SAAgB,MAAM,SAA4B;CACjD,MAAM,EACL,QACA,QACA,UACA,gBACA,aACA,oBACG;CACJ,IAAI,CAAC,QAAQ,MAAM,IAAIA,eAAAA,qBAAqB,EAAE;CAE9C,MAAM,MAAM,IAAI,IAAI,GAAG,OAAO,IAAI,QAAQ;CAC1C,IAAI,eAAe,iBAAiB;EACnC,IAAI,CAAC,aACJ,MAAM,IAAIC,eAAAA,6BAA6B,IAAI,SAAS,GAAG,aAAa;EAErE,IAAI,CAAC,iBACJ,MAAM,IAAIA,eAAAA,6BAA6B,IAAI,SAAS,GAAG,iBAAiB;EAEzE,IAAI,WAAW,mBAAmB,WAAW;EAC7C,IAAI,WAAW,mBAAmB,eAAe;CAClD;CACA,IAAI,QAAQ,IAAI,aAAa,IAAI,UAAU,MAAM;CACjD,IAAI,UAAU,IAAI,aAAa,IAAI,YAAY,QAAQ;CACvD,IAAI,gBAAgB,IAAI,aAAa,IAAI,kBAAkB,MAAM;CACjE,OAAO,IAAI,SAAS;AACrB;;;;;AAMA,SAAgB,MAAM,KAAwB;CAC7C,IAAI;CACJ,IAAI;EACH,SAAS,IAAI,IAAI,GAAG;CACrB,QAAQ;EACP,MAAM,IAAIC,eAAAA,oBAAoB,GAAG;CAClC;CAEA,IAAI,OAAO,aAAa,QACvB,MAAM,IAAIC,eAAAA,wBAAwB,KAAK,QAAQ,OAAO,QAAQ;CAG/D,MAAM,SAAS,OAAO;CACtB,IAAI,CAAC,QAAQ,MAAM,IAAIH,eAAAA,qBAAqB,GAAG;CAE/C,MAAM,SAAS,OAAO,aAAa,IAAI,QAAQ,KAAK,KAAA;CACpD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU,KAAK,KAAA;CACxD,MAAM,iBACL,OAAO,aAAa,IAAI,gBAAgB,MAAM,SAAS,OAAO,KAAA;CAC/D,MAAM,cAAc,cAAc,KAAK,MAAM;CAE7C,OAAO;EACN;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;EAC3C,GAAG;CACJ;AACD;;AAGA,SAAS,cACR,KACA,QACqD;CACrD,MAAM,cAAc,OAAO,KAAK,OAAO,QAAQ;CAC/C,MAAM,kBAAkB,OAAO,KAAK,OAAO,QAAQ;CACnD,IAAI,CAAC,eAAe,CAAC,iBAAiB,OAAO,CAAC;CAC9C,IAAI,CAAC,aACJ,MAAM,IAAIC,eAAAA,6BAA6B,KAAK,aAAa;CAE1D,IAAI,CAAC,iBACJ,MAAM,IAAIA,eAAAA,6BAA6B,KAAK,iBAAiB;CAE9D,OAAO;EAAE;EAAa;CAAgB;AACvC;AAEA,SAAS,OAAO,KAAa,OAAuB;CACnD,IAAI;EACH,OAAO,mBAAmB,KAAK;CAChC,QAAQ;EACP,MAAM,IAAIC,eAAAA,oBAAoB,GAAG;CAClC;AACD"}
package/dist/s3Url.d.cts CHANGED
@@ -12,7 +12,15 @@ declare namespace s3Url_d_exports {
12
12
  *
13
13
  * Both directions live here so they cannot drift: `parse(build(x))` is `x`.
14
14
  */
15
- /** What an S3 URL addresses. Credentials are deliberately absent — see below. */
15
+ /**
16
+ * What an S3 URL addresses, and optionally who it calls as.
17
+ *
18
+ * Credentials are optional and come as a pair. With them, the URL is a key
19
+ * scoped to this one bucket (`s3://KEY:SECRET@uploads`); without them the AWS
20
+ * SDK resolves its own chain — an execution role when deployed,
21
+ * `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` otherwise. The URL's pair wins
22
+ * over that chain.
23
+ */
16
24
  export interface S3Address {
17
25
  bucket: string;
18
26
  /** Read off the bucket, never inherited: a bucket may live in another region. */
@@ -21,17 +29,23 @@ export interface S3Address {
21
29
  endpoint?: string;
22
30
  /** MinIO and most S3-compatible servers need path-style addressing. */
23
31
  forcePathStyle?: boolean;
32
+ /** The URL's own access key; always paired with `secretAccessKey`. */
33
+ accessKeyId?: string;
34
+ /** The URL's own secret; always paired with `accessKeyId`. */
35
+ secretAccessKey?: string;
24
36
  }
25
37
  /**
26
38
  * Compose an address into a URL.
27
39
  *
28
- * Credentials never appear. The AWS SDK resolves them from its own chain — an
29
- * execution role when deployed, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`
30
- * locally — so the URL stays safe to log and identical across environments that
31
- * differ only in who is calling.
40
+ * Credentials appear only when the address carries them, percent-encoded —
41
+ * AWS secrets routinely contain `/` and `+`. An address without them builds a
42
+ * URL with no userinfo, which leaves the caller to the SDK's own chain.
32
43
  */
33
44
  export declare function build(address: S3Address): string;
34
- /** Parse a URL back into an address. Throws if it is not an `s3://` URL. */
45
+ /**
46
+ * Parse a URL back into an address. Throws if it is not an `s3://` URL, or if
47
+ * its userinfo carries only one half of a key pair.
48
+ */
35
49
  export declare function parse(url: string): S3Address;
36
50
  //#endregion
37
51
  export { s3Url_d_exports as t };
@@ -1 +1 @@
1
- {"version":3,"file":"s3Url.d.cts","names":[],"sources":["../src/s3Url.ts"],"mappings":";;;;;;;;;;;;;;;iBAmBiB;EAChB;;EAEA;;EAEA;;EAEA;;;;;;;;;;wBAae,MAAM,SAAS;;wBAYf,MAAM,cAAc"}
1
+ {"version":3,"file":"s3Url.d.cts","names":[],"sources":["../src/s3Url.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;iBA4BiB;EAChB;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;wBAYe,MAAM,SAAS;;;;;wBAgCf,MAAM,cAAc"}
package/dist/s3Url.d.mts CHANGED
@@ -12,7 +12,15 @@ declare namespace s3Url_d_exports {
12
12
  *
13
13
  * Both directions live here so they cannot drift: `parse(build(x))` is `x`.
14
14
  */
15
- /** What an S3 URL addresses. Credentials are deliberately absent — see below. */
15
+ /**
16
+ * What an S3 URL addresses, and optionally who it calls as.
17
+ *
18
+ * Credentials are optional and come as a pair. With them, the URL is a key
19
+ * scoped to this one bucket (`s3://KEY:SECRET@uploads`); without them the AWS
20
+ * SDK resolves its own chain — an execution role when deployed,
21
+ * `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` otherwise. The URL's pair wins
22
+ * over that chain.
23
+ */
16
24
  export interface S3Address {
17
25
  bucket: string;
18
26
  /** Read off the bucket, never inherited: a bucket may live in another region. */
@@ -21,17 +29,23 @@ export interface S3Address {
21
29
  endpoint?: string;
22
30
  /** MinIO and most S3-compatible servers need path-style addressing. */
23
31
  forcePathStyle?: boolean;
32
+ /** The URL's own access key; always paired with `secretAccessKey`. */
33
+ accessKeyId?: string;
34
+ /** The URL's own secret; always paired with `accessKeyId`. */
35
+ secretAccessKey?: string;
24
36
  }
25
37
  /**
26
38
  * Compose an address into a URL.
27
39
  *
28
- * Credentials never appear. The AWS SDK resolves them from its own chain — an
29
- * execution role when deployed, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`
30
- * locally — so the URL stays safe to log and identical across environments that
31
- * differ only in who is calling.
40
+ * Credentials appear only when the address carries them, percent-encoded —
41
+ * AWS secrets routinely contain `/` and `+`. An address without them builds a
42
+ * URL with no userinfo, which leaves the caller to the SDK's own chain.
32
43
  */
33
44
  export declare function build(address: S3Address): string;
34
- /** Parse a URL back into an address. Throws if it is not an `s3://` URL. */
45
+ /**
46
+ * Parse a URL back into an address. Throws if it is not an `s3://` URL, or if
47
+ * its userinfo carries only one half of a key pair.
48
+ */
35
49
  export declare function parse(url: string): S3Address;
36
50
  //#endregion
37
51
  export { s3Url_d_exports as t };
@@ -1 +1 @@
1
- {"version":3,"file":"s3Url.d.mts","names":[],"sources":["../src/s3Url.ts"],"mappings":";;;;;;;;;;;;;;;iBAmBiB;EAChB;;EAEA;;EAEA;;EAEA;;;;;;;;;;wBAae,MAAM,SAAS;;wBAYf,MAAM,cAAc"}
1
+ {"version":3,"file":"s3Url.d.mts","names":[],"sources":["../src/s3Url.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;iBA4BiB;EAChB;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;wBAYe,MAAM,SAAS;;;;;wBAgCf,MAAM,cAAc"}
package/dist/s3Url.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { i as UnexpectedStorageScheme, n as MissingStorageBucket, t as MalformedStorageUrl } from "./errors-CpYcpJCY.mjs";
2
+ import { a as UnexpectedStorageScheme, n as MalformedStorageUrl, r as MissingStorageBucket, t as IncompleteStorageCredentials } from "./errors-COXs1iNg.mjs";
3
3
  //#region src/s3Url.ts
4
4
  /**
5
5
  * The `s3://` URL codec — how an S3 bucket is addressed as a single string.
@@ -20,21 +20,29 @@ const SCHEME = "s3:";
20
20
  /**
21
21
  * Compose an address into a URL.
22
22
  *
23
- * Credentials never appear. The AWS SDK resolves them from its own chain — an
24
- * execution role when deployed, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`
25
- * locally — so the URL stays safe to log and identical across environments that
26
- * differ only in who is calling.
23
+ * Credentials appear only when the address carries them, percent-encoded —
24
+ * AWS secrets routinely contain `/` and `+`. An address without them builds a
25
+ * URL with no userinfo, which leaves the caller to the SDK's own chain.
27
26
  */
28
27
  function build(address) {
29
- const { bucket, region, endpoint, forcePathStyle } = address;
28
+ const { bucket, region, endpoint, forcePathStyle, accessKeyId, secretAccessKey } = address;
30
29
  if (!bucket) throw new MissingStorageBucket("");
31
30
  const url = new URL(`${SCHEME}//${bucket}`);
31
+ if (accessKeyId || secretAccessKey) {
32
+ if (!accessKeyId) throw new IncompleteStorageCredentials(url.toString(), "accessKeyId");
33
+ if (!secretAccessKey) throw new IncompleteStorageCredentials(url.toString(), "secretAccessKey");
34
+ url.username = encodeURIComponent(accessKeyId);
35
+ url.password = encodeURIComponent(secretAccessKey);
36
+ }
32
37
  if (region) url.searchParams.set("region", region);
33
38
  if (endpoint) url.searchParams.set("endpoint", endpoint);
34
39
  if (forcePathStyle) url.searchParams.set("forcePathStyle", "true");
35
40
  return url.toString();
36
41
  }
37
- /** Parse a URL back into an address. Throws if it is not an `s3://` URL. */
42
+ /**
43
+ * Parse a URL back into an address. Throws if it is not an `s3://` URL, or if
44
+ * its userinfo carries only one half of a key pair.
45
+ */
38
46
  function parse(url) {
39
47
  let parsed;
40
48
  try {
@@ -48,13 +56,34 @@ function parse(url) {
48
56
  const region = parsed.searchParams.get("region") ?? void 0;
49
57
  const endpoint = parsed.searchParams.get("endpoint") ?? void 0;
50
58
  const forcePathStyle = parsed.searchParams.get("forcePathStyle") === "true" ? true : void 0;
59
+ const credentials = credentialsOf(url, parsed);
51
60
  return {
52
61
  bucket,
53
62
  ...region ? { region } : {},
54
63
  ...endpoint ? { endpoint } : {},
55
- ...forcePathStyle ? { forcePathStyle } : {}
64
+ ...forcePathStyle ? { forcePathStyle } : {},
65
+ ...credentials
66
+ };
67
+ }
68
+ /** The URL's userinfo as a key pair: both halves, or nothing. */
69
+ function credentialsOf(url, parsed) {
70
+ const accessKeyId = decode(url, parsed.username);
71
+ const secretAccessKey = decode(url, parsed.password);
72
+ if (!accessKeyId && !secretAccessKey) return {};
73
+ if (!accessKeyId) throw new IncompleteStorageCredentials(url, "accessKeyId");
74
+ if (!secretAccessKey) throw new IncompleteStorageCredentials(url, "secretAccessKey");
75
+ return {
76
+ accessKeyId,
77
+ secretAccessKey
56
78
  };
57
79
  }
80
+ function decode(url, value) {
81
+ try {
82
+ return decodeURIComponent(value);
83
+ } catch {
84
+ throw new MalformedStorageUrl(url);
85
+ }
86
+ }
58
87
  //#endregion
59
88
  export { build, parse, s3Url_exports as t };
60
89
 
@@ -1 +1 @@
1
- {"version":3,"file":"s3Url.mjs","names":[],"sources":["../src/s3Url.ts"],"sourcesContent":["/**\n * The `s3://` URL codec — how an S3 bucket is addressed as a single string.\n *\n * It lives beside the client rather than in a neutral package because `bucket`,\n * `region`, and `forcePathStyle` are S3's vocabulary. The neutral layer only\n * knows that a construct provides *one URL*; what that URL says is between the\n * component that composes it and the client that parses it. A `gs://` codec\n * would sit beside its own client and share nothing with this one.\n *\n * Both directions live here so they cannot drift: `parse(build(x))` is `x`.\n */\n\nimport {\n\tMalformedStorageUrl,\n\tMissingStorageBucket,\n\tUnexpectedStorageScheme,\n} from './errors';\n\n/** What an S3 URL addresses. Credentials are deliberately absent — see below. */\nexport interface S3Address {\n\tbucket: string;\n\t/** Read off the bucket, never inherited: a bucket may live in another region. */\n\tregion?: string;\n\t/** Set for S3-compatible backends such as MinIO. */\n\tendpoint?: string;\n\t/** MinIO and most S3-compatible servers need path-style addressing. */\n\tforcePathStyle?: boolean;\n}\n\nconst SCHEME = 's3:';\n\n/**\n * Compose an address into a URL.\n *\n * Credentials never appear. The AWS SDK resolves them from its own chain — an\n * execution role when deployed, `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`\n * locally — so the URL stays safe to log and identical across environments that\n * differ only in who is calling.\n */\nexport function build(address: S3Address): string {\n\tconst { bucket, region, endpoint, forcePathStyle } = address;\n\tif (!bucket) throw new MissingStorageBucket('');\n\n\tconst url = new URL(`${SCHEME}//${bucket}`);\n\tif (region) url.searchParams.set('region', region);\n\tif (endpoint) url.searchParams.set('endpoint', endpoint);\n\tif (forcePathStyle) url.searchParams.set('forcePathStyle', 'true');\n\treturn url.toString();\n}\n\n/** Parse a URL back into an address. Throws if it is not an `s3://` URL. */\nexport function parse(url: string): S3Address {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new MalformedStorageUrl(url);\n\t}\n\n\tif (parsed.protocol !== SCHEME) {\n\t\tthrow new UnexpectedStorageScheme(url, SCHEME, parsed.protocol);\n\t}\n\n\tconst bucket = parsed.hostname;\n\tif (!bucket) throw new MissingStorageBucket(url);\n\n\tconst region = parsed.searchParams.get('region') ?? undefined;\n\tconst endpoint = parsed.searchParams.get('endpoint') ?? undefined;\n\tconst forcePathStyle =\n\t\tparsed.searchParams.get('forcePathStyle') === 'true' ? true : undefined;\n\n\treturn {\n\t\tbucket,\n\t\t...(region ? { region } : {}),\n\t\t...(endpoint ? { endpoint } : {}),\n\t\t...(forcePathStyle ? { forcePathStyle } : {}),\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,MAAM,SAAS;;;;;;;;;AAUf,SAAgB,MAAM,SAA4B;CACjD,MAAM,EAAE,QAAQ,QAAQ,UAAU,mBAAmB;CACrD,IAAI,CAAC,QAAQ,MAAM,IAAI,qBAAqB,EAAE;CAE9C,MAAM,MAAM,IAAI,IAAI,GAAG,OAAO,IAAI,QAAQ;CAC1C,IAAI,QAAQ,IAAI,aAAa,IAAI,UAAU,MAAM;CACjD,IAAI,UAAU,IAAI,aAAa,IAAI,YAAY,QAAQ;CACvD,IAAI,gBAAgB,IAAI,aAAa,IAAI,kBAAkB,MAAM;CACjE,OAAO,IAAI,SAAS;AACrB;;AAGA,SAAgB,MAAM,KAAwB;CAC7C,IAAI;CACJ,IAAI;EACH,SAAS,IAAI,IAAI,GAAG;CACrB,QAAQ;EACP,MAAM,IAAI,oBAAoB,GAAG;CAClC;CAEA,IAAI,OAAO,aAAa,QACvB,MAAM,IAAI,wBAAwB,KAAK,QAAQ,OAAO,QAAQ;CAG/D,MAAM,SAAS,OAAO;CACtB,IAAI,CAAC,QAAQ,MAAM,IAAI,qBAAqB,GAAG;CAE/C,MAAM,SAAS,OAAO,aAAa,IAAI,QAAQ,KAAK,KAAA;CACpD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU,KAAK,KAAA;CACxD,MAAM,iBACL,OAAO,aAAa,IAAI,gBAAgB,MAAM,SAAS,OAAO,KAAA;CAE/D,OAAO;EACN;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;CAC5C;AACD"}
1
+ {"version":3,"file":"s3Url.mjs","names":[],"sources":["../src/s3Url.ts"],"sourcesContent":["/**\n * The `s3://` URL codec — how an S3 bucket is addressed as a single string.\n *\n * It lives beside the client rather than in a neutral package because `bucket`,\n * `region`, and `forcePathStyle` are S3's vocabulary. The neutral layer only\n * knows that a construct provides *one URL*; what that URL says is between the\n * component that composes it and the client that parses it. A `gs://` codec\n * would sit beside its own client and share nothing with this one.\n *\n * Both directions live here so they cannot drift: `parse(build(x))` is `x`.\n */\n\nimport {\n\tIncompleteStorageCredentials,\n\tMalformedStorageUrl,\n\tMissingStorageBucket,\n\tUnexpectedStorageScheme,\n} from './errors';\n\n/**\n * What an S3 URL addresses, and optionally who it calls as.\n *\n * Credentials are optional and come as a pair. With them, the URL is a key\n * scoped to this one bucket (`s3://KEY:SECRET@uploads`); without them the AWS\n * SDK resolves its own chain — an execution role when deployed,\n * `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` otherwise. The URL's pair wins\n * over that chain.\n */\nexport interface S3Address {\n\tbucket: string;\n\t/** Read off the bucket, never inherited: a bucket may live in another region. */\n\tregion?: string;\n\t/** Set for S3-compatible backends such as MinIO. */\n\tendpoint?: string;\n\t/** MinIO and most S3-compatible servers need path-style addressing. */\n\tforcePathStyle?: boolean;\n\t/** The URL's own access key; always paired with `secretAccessKey`. */\n\taccessKeyId?: string;\n\t/** The URL's own secret; always paired with `accessKeyId`. */\n\tsecretAccessKey?: string;\n}\n\nconst SCHEME = 's3:';\n\n/**\n * Compose an address into a URL.\n *\n * Credentials appear only when the address carries them, percent-encoded —\n * AWS secrets routinely contain `/` and `+`. An address without them builds a\n * URL with no userinfo, which leaves the caller to the SDK's own chain.\n */\nexport function build(address: S3Address): string {\n\tconst {\n\t\tbucket,\n\t\tregion,\n\t\tendpoint,\n\t\tforcePathStyle,\n\t\taccessKeyId,\n\t\tsecretAccessKey,\n\t} = address;\n\tif (!bucket) throw new MissingStorageBucket('');\n\n\tconst url = new URL(`${SCHEME}//${bucket}`);\n\tif (accessKeyId || secretAccessKey) {\n\t\tif (!accessKeyId) {\n\t\t\tthrow new IncompleteStorageCredentials(url.toString(), 'accessKeyId');\n\t\t}\n\t\tif (!secretAccessKey) {\n\t\t\tthrow new IncompleteStorageCredentials(url.toString(), 'secretAccessKey');\n\t\t}\n\t\turl.username = encodeURIComponent(accessKeyId);\n\t\turl.password = encodeURIComponent(secretAccessKey);\n\t}\n\tif (region) url.searchParams.set('region', region);\n\tif (endpoint) url.searchParams.set('endpoint', endpoint);\n\tif (forcePathStyle) url.searchParams.set('forcePathStyle', 'true');\n\treturn url.toString();\n}\n\n/**\n * Parse a URL back into an address. Throws if it is not an `s3://` URL, or if\n * its userinfo carries only one half of a key pair.\n */\nexport function parse(url: string): S3Address {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new MalformedStorageUrl(url);\n\t}\n\n\tif (parsed.protocol !== SCHEME) {\n\t\tthrow new UnexpectedStorageScheme(url, SCHEME, parsed.protocol);\n\t}\n\n\tconst bucket = parsed.hostname;\n\tif (!bucket) throw new MissingStorageBucket(url);\n\n\tconst region = parsed.searchParams.get('region') ?? undefined;\n\tconst endpoint = parsed.searchParams.get('endpoint') ?? undefined;\n\tconst forcePathStyle =\n\t\tparsed.searchParams.get('forcePathStyle') === 'true' ? true : undefined;\n\tconst credentials = credentialsOf(url, parsed);\n\n\treturn {\n\t\tbucket,\n\t\t...(region ? { region } : {}),\n\t\t...(endpoint ? { endpoint } : {}),\n\t\t...(forcePathStyle ? { forcePathStyle } : {}),\n\t\t...credentials,\n\t};\n}\n\n/** The URL's userinfo as a key pair: both halves, or nothing. */\nfunction credentialsOf(\n\turl: string,\n\tparsed: URL,\n): Pick<S3Address, 'accessKeyId' | 'secretAccessKey'> {\n\tconst accessKeyId = decode(url, parsed.username);\n\tconst secretAccessKey = decode(url, parsed.password);\n\tif (!accessKeyId && !secretAccessKey) return {};\n\tif (!accessKeyId) {\n\t\tthrow new IncompleteStorageCredentials(url, 'accessKeyId');\n\t}\n\tif (!secretAccessKey) {\n\t\tthrow new IncompleteStorageCredentials(url, 'secretAccessKey');\n\t}\n\treturn { accessKeyId, secretAccessKey };\n}\n\nfunction decode(url: string, value: string): string {\n\ttry {\n\t\treturn decodeURIComponent(value);\n\t} catch {\n\t\tthrow new MalformedStorageUrl(url);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,MAAM,SAAS;;;;;;;;AASf,SAAgB,MAAM,SAA4B;CACjD,MAAM,EACL,QACA,QACA,UACA,gBACA,aACA,oBACG;CACJ,IAAI,CAAC,QAAQ,MAAM,IAAI,qBAAqB,EAAE;CAE9C,MAAM,MAAM,IAAI,IAAI,GAAG,OAAO,IAAI,QAAQ;CAC1C,IAAI,eAAe,iBAAiB;EACnC,IAAI,CAAC,aACJ,MAAM,IAAI,6BAA6B,IAAI,SAAS,GAAG,aAAa;EAErE,IAAI,CAAC,iBACJ,MAAM,IAAI,6BAA6B,IAAI,SAAS,GAAG,iBAAiB;EAEzE,IAAI,WAAW,mBAAmB,WAAW;EAC7C,IAAI,WAAW,mBAAmB,eAAe;CAClD;CACA,IAAI,QAAQ,IAAI,aAAa,IAAI,UAAU,MAAM;CACjD,IAAI,UAAU,IAAI,aAAa,IAAI,YAAY,QAAQ;CACvD,IAAI,gBAAgB,IAAI,aAAa,IAAI,kBAAkB,MAAM;CACjE,OAAO,IAAI,SAAS;AACrB;;;;;AAMA,SAAgB,MAAM,KAAwB;CAC7C,IAAI;CACJ,IAAI;EACH,SAAS,IAAI,IAAI,GAAG;CACrB,QAAQ;EACP,MAAM,IAAI,oBAAoB,GAAG;CAClC;CAEA,IAAI,OAAO,aAAa,QACvB,MAAM,IAAI,wBAAwB,KAAK,QAAQ,OAAO,QAAQ;CAG/D,MAAM,SAAS,OAAO;CACtB,IAAI,CAAC,QAAQ,MAAM,IAAI,qBAAqB,GAAG;CAE/C,MAAM,SAAS,OAAO,aAAa,IAAI,QAAQ,KAAK,KAAA;CACpD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU,KAAK,KAAA;CACxD,MAAM,iBACL,OAAO,aAAa,IAAI,gBAAgB,MAAM,SAAS,OAAO,KAAA;CAC/D,MAAM,cAAc,cAAc,KAAK,MAAM;CAE7C,OAAO;EACN;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;EAC3C,GAAG;CACJ;AACD;;AAGA,SAAS,cACR,KACA,QACqD;CACrD,MAAM,cAAc,OAAO,KAAK,OAAO,QAAQ;CAC/C,MAAM,kBAAkB,OAAO,KAAK,OAAO,QAAQ;CACnD,IAAI,CAAC,eAAe,CAAC,iBAAiB,OAAO,CAAC;CAC9C,IAAI,CAAC,aACJ,MAAM,IAAI,6BAA6B,KAAK,aAAa;CAE1D,IAAI,CAAC,iBACJ,MAAM,IAAI,6BAA6B,KAAK,iBAAiB;CAE9D,OAAO;EAAE;EAAa;CAAgB;AACvC;AAEA,SAAS,OAAO,KAAa,OAAuB;CACnD,IAAI;EACH,OAAO,mBAAmB,KAAK;CAChC,QAAQ;EACP,MAAM,IAAI,oBAAoB,GAAG;CAClC;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/storage",
3
- "version": "10.0.0-alpha.67",
3
+ "version": "10.0.0-alpha.68",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,7 +52,7 @@
52
52
  "@aws-sdk/client-s3": ">=3.1136.0",
53
53
  "@aws-sdk/s3-presigned-post": ">=3.1136.0",
54
54
  "@aws-sdk/s3-request-presigner": ">=3.1136.0",
55
- "@geekmidas/cache": "^10.0.0-alpha.67"
55
+ "@geekmidas/cache": "^10.0.0-alpha.68"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@geekmidas/cache": {
@@ -72,7 +72,7 @@
72
72
  }
73
73
  },
74
74
  "devDependencies": {
75
- "@geekmidas/cache": "^10.0.0-alpha.67"
75
+ "@geekmidas/cache": "^10.0.0-alpha.68"
76
76
  },
77
77
  "scripts": {}
78
78
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-Bexjg-j-.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Storage errors — shared by every implementation, not just S3.\n *\n * A `gs://` or Azure client hits the same three problems as the S3 one: the\n * string is not a URL, it is a URL for a different provider, or it addresses no\n * bucket. Each carries the provider-specific detail as a field, so the type is\n * common while the specifics stay accurate.\n *\n * Messages state the rule, which is constant, and never interpolate the value.\n * An interpolated message cannot be matched on, reads differently every time it\n * is thrown, and carries user input into every log line that touches it.\n */\n\n/** Base for anything wrong with a storage URL, so callers may catch broadly. */\nexport abstract class StorageUrlError extends Error {\n\t/** The URL that could not be used. */\n\treadonly url: string;\n\n\tconstructor(url: string, message: string) {\n\t\tsuper(message);\n\t\tthis.url = url;\n\t\tthis.name = new.target.name;\n\t}\n}\n\n/** The string is not a URL at all. */\nexport class MalformedStorageUrl extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Not a valid URL');\n\t}\n}\n\n/**\n * A URL, but for a different provider — a `gs://` URL reaching the S3 client,\n * or the reverse. The usual cause is a construct resolving against the wrong\n * target, which is worth distinguishing from a malformed string.\n */\nexport class UnexpectedStorageScheme extends StorageUrlError {\n\t/** The scheme this client handles, e.g. `'s3:'`. */\n\treadonly expected: string;\n\t/** The scheme the URL actually carried, e.g. `'gs:'`. */\n\treadonly actual: string;\n\n\tconstructor(url: string, expected: string, actual: string) {\n\t\tsuper(url, 'Storage URL is for a different provider');\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\n/**\n * A well-formed URL that addresses no bucket.\n *\n * \"Bucket\" is the term S3 and GCS share; Azure calls the same thing a\n * container. One error covers all three — the word in the message is the\n * common one rather than a new abstraction nobody uses.\n */\nexport class MissingStorageBucket extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Storage URL must address a bucket');\n\t}\n}\n\n/**\n * No driver is registered for the URL's scheme.\n *\n * Usually means the build pinned a driver for a different target, or an entry\n * point registered nothing at all — so the registered schemes are carried\n * alongside, since \"what *is* available\" is the actionable half.\n */\nexport class UnregisteredStorageScheme extends StorageUrlError {\n\t/** The scheme that had no driver, e.g. `'gs:'`. */\n\treadonly scheme: string;\n\t/** What is registered, so the caller can see what was pinned instead. */\n\treadonly registered: readonly string[];\n\n\tconstructor(url: string, scheme: string, registered: readonly string[]) {\n\t\tsuper(url, 'No storage driver is registered for this URL scheme');\n\t\tthis.scheme = scheme;\n\t\tthis.registered = registered;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAsB,kBAAtB,cAA8C,MAAM;;CAEnD;CAEA,YAAY,KAAa,SAAiB;EACzC,MAAM,OAAO;EACb,KAAK,MAAM;EACX,KAAK,OAAO,WAAW;CACxB;AACD;;AAGA,IAAa,sBAAb,cAAyC,gBAAgB;CACxD,YAAY,KAAa;EACxB,MAAM,KAAK,iBAAiB;CAC7B;AACD;;;;;;AAOA,IAAa,0BAAb,cAA6C,gBAAgB;;CAE5D;;CAEA;CAEA,YAAY,KAAa,UAAkB,QAAgB;EAC1D,MAAM,KAAK,yCAAyC;EACpD,KAAK,WAAW;EAChB,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,uBAAb,cAA0C,gBAAgB;CACzD,YAAY,KAAa;EACxB,MAAM,KAAK,mCAAmC;CAC/C;AACD;;;;;;;;AASA,IAAa,4BAAb,cAA+C,gBAAgB;;CAE9D;;CAEA;CAEA,YAAY,KAAa,QAAgB,YAA+B;EACvE,MAAM,KAAK,qDAAqD;EAChE,KAAK,SAAS;EACd,KAAK,aAAa;CACnB;AACD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-CpYcpJCY.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Storage errors — shared by every implementation, not just S3.\n *\n * A `gs://` or Azure client hits the same three problems as the S3 one: the\n * string is not a URL, it is a URL for a different provider, or it addresses no\n * bucket. Each carries the provider-specific detail as a field, so the type is\n * common while the specifics stay accurate.\n *\n * Messages state the rule, which is constant, and never interpolate the value.\n * An interpolated message cannot be matched on, reads differently every time it\n * is thrown, and carries user input into every log line that touches it.\n */\n\n/** Base for anything wrong with a storage URL, so callers may catch broadly. */\nexport abstract class StorageUrlError extends Error {\n\t/** The URL that could not be used. */\n\treadonly url: string;\n\n\tconstructor(url: string, message: string) {\n\t\tsuper(message);\n\t\tthis.url = url;\n\t\tthis.name = new.target.name;\n\t}\n}\n\n/** The string is not a URL at all. */\nexport class MalformedStorageUrl extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Not a valid URL');\n\t}\n}\n\n/**\n * A URL, but for a different provider — a `gs://` URL reaching the S3 client,\n * or the reverse. The usual cause is a construct resolving against the wrong\n * target, which is worth distinguishing from a malformed string.\n */\nexport class UnexpectedStorageScheme extends StorageUrlError {\n\t/** The scheme this client handles, e.g. `'s3:'`. */\n\treadonly expected: string;\n\t/** The scheme the URL actually carried, e.g. `'gs:'`. */\n\treadonly actual: string;\n\n\tconstructor(url: string, expected: string, actual: string) {\n\t\tsuper(url, 'Storage URL is for a different provider');\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\n/**\n * A well-formed URL that addresses no bucket.\n *\n * \"Bucket\" is the term S3 and GCS share; Azure calls the same thing a\n * container. One error covers all three — the word in the message is the\n * common one rather than a new abstraction nobody uses.\n */\nexport class MissingStorageBucket extends StorageUrlError {\n\tconstructor(url: string) {\n\t\tsuper(url, 'Storage URL must address a bucket');\n\t}\n}\n\n/**\n * No driver is registered for the URL's scheme.\n *\n * Usually means the build pinned a driver for a different target, or an entry\n * point registered nothing at all — so the registered schemes are carried\n * alongside, since \"what *is* available\" is the actionable half.\n */\nexport class UnregisteredStorageScheme extends StorageUrlError {\n\t/** The scheme that had no driver, e.g. `'gs:'`. */\n\treadonly scheme: string;\n\t/** What is registered, so the caller can see what was pinned instead. */\n\treadonly registered: readonly string[];\n\n\tconstructor(url: string, scheme: string, registered: readonly string[]) {\n\t\tsuper(url, 'No storage driver is registered for this URL scheme');\n\t\tthis.scheme = scheme;\n\t\tthis.registered = registered;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAsB,kBAAtB,cAA8C,MAAM;;CAEnD;CAEA,YAAY,KAAa,SAAiB;EACzC,MAAM,OAAO;EACb,KAAK,MAAM;EACX,KAAK,OAAO,WAAW;CACxB;AACD;;AAGA,IAAa,sBAAb,cAAyC,gBAAgB;CACxD,YAAY,KAAa;EACxB,MAAM,KAAK,iBAAiB;CAC7B;AACD;;;;;;AAOA,IAAa,0BAAb,cAA6C,gBAAgB;;CAE5D;;CAEA;CAEA,YAAY,KAAa,UAAkB,QAAgB;EAC1D,MAAM,KAAK,yCAAyC;EACpD,KAAK,WAAW;EAChB,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,uBAAb,cAA0C,gBAAgB;CACzD,YAAY,KAAa;EACxB,MAAM,KAAK,mCAAmC;CAC/C;AACD;;;;;;;;AASA,IAAa,4BAAb,cAA+C,gBAAgB;;CAE9D;;CAEA;CAEA,YAAY,KAAa,QAAgB,YAA+B;EACvE,MAAM,KAAK,qDAAqD;EAChE,KAAK,SAAS;EACd,KAAK,aAAa;CACnB;AACD"}