@backblaze-labs/b2-sdk 0.2.0 → 0.3.0
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/CHANGELOG.md +42 -1
- package/README.md +20 -2
- package/dist/_virtual/_b2-sdk-version-json.cjs +1 -1
- package/dist/_virtual/_b2-sdk-version-json.js +1 -1
- package/dist/auth/file.cjs.map +1 -1
- package/dist/backup/client.cjs +206 -0
- package/dist/backup/client.cjs.map +1 -0
- package/dist/backup/client.d.cts +238 -0
- package/dist/backup/client.d.cts.map +1 -0
- package/dist/backup/client.d.ts +238 -0
- package/dist/backup/client.d.ts.map +1 -0
- package/dist/backup/client.js +206 -0
- package/dist/backup/client.js.map +1 -0
- package/dist/backup/index.cjs +15 -0
- package/dist/backup/index.d.cts +22 -0
- package/dist/backup/index.d.cts.map +1 -0
- package/dist/backup/index.d.ts +22 -0
- package/dist/backup/index.d.ts.map +1 -0
- package/dist/backup/index.js +7 -0
- package/dist/backup/raw.cjs +146 -0
- package/dist/backup/raw.cjs.map +1 -0
- package/dist/backup/raw.d.cts +122 -0
- package/dist/backup/raw.d.cts.map +1 -0
- package/dist/backup/raw.d.ts +122 -0
- package/dist/backup/raw.d.ts.map +1 -0
- package/dist/backup/raw.js +146 -0
- package/dist/backup/raw.js.map +1 -0
- package/dist/bucket.cjs +2 -1
- package/dist/bucket.cjs.map +1 -1
- package/dist/bucket.js +2 -1
- package/dist/bucket.js.map +1 -1
- package/dist/copy/large.cjs +5 -3
- package/dist/copy/large.cjs.map +1 -1
- package/dist/copy/large.d.cts.map +1 -1
- package/dist/copy/large.d.ts.map +1 -1
- package/dist/copy/large.js +5 -3
- package/dist/copy/large.js.map +1 -1
- package/dist/download/checksum.cjs +6 -2
- package/dist/download/checksum.cjs.map +1 -1
- package/dist/download/checksum.js +6 -2
- package/dist/download/checksum.js.map +1 -1
- package/dist/download/parallel.cjs +52 -15
- package/dist/download/parallel.cjs.map +1 -1
- package/dist/download/parallel.d.cts +8 -1
- package/dist/download/parallel.d.cts.map +1 -1
- package/dist/download/parallel.d.ts +8 -1
- package/dist/download/parallel.d.ts.map +1 -1
- package/dist/download/parallel.js +52 -15
- package/dist/download/parallel.js.map +1 -1
- package/dist/download/single.cjs +141 -6
- package/dist/download/single.cjs.map +1 -1
- package/dist/download/single.d.cts.map +1 -1
- package/dist/download/single.d.ts.map +1 -1
- package/dist/download/single.js +141 -6
- package/dist/download/single.js.map +1 -1
- package/dist/errors/index.cjs +173 -3
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +119 -0
- package/dist/errors/index.d.cts.map +1 -1
- package/dist/errors/index.d.ts +119 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +161 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +1 -1
- package/dist/http/retry.js.map +1 -1
- package/dist/http/transport.cjs +61 -23
- package/dist/http/transport.cjs.map +1 -1
- package/dist/http/transport.d.cts +25 -3
- package/dist/http/transport.d.cts.map +1 -1
- package/dist/http/transport.d.ts +25 -3
- package/dist/http/transport.d.ts.map +1 -1
- package/dist/http/transport.js +59 -22
- package/dist/http/transport.js.map +1 -1
- package/dist/index.cjs +25 -2
- package/dist/index.d.cts +6 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/object.cjs +1 -1
- package/dist/object.cjs.map +1 -1
- package/dist/object.d.cts +4 -2
- package/dist/object.d.cts.map +1 -1
- package/dist/object.d.ts +4 -2
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +1 -1
- package/dist/object.js.map +1 -1
- package/dist/partner/account-info.d.cts +43 -0
- package/dist/partner/account-info.d.cts.map +1 -0
- package/dist/partner/account-info.d.ts +43 -0
- package/dist/partner/account-info.d.ts.map +1 -0
- package/dist/partner/auth-clone.cjs +64 -0
- package/dist/partner/auth-clone.cjs.map +1 -0
- package/dist/partner/auth-clone.d.cts +27 -0
- package/dist/partner/auth-clone.d.cts.map +1 -0
- package/dist/partner/auth-clone.d.ts +27 -0
- package/dist/partner/auth-clone.d.ts.map +1 -0
- package/dist/partner/auth-clone.js +63 -0
- package/dist/partner/auth-clone.js.map +1 -0
- package/dist/partner/auth-core.cjs +175 -0
- package/dist/partner/auth-core.cjs.map +1 -0
- package/dist/partner/auth-core.d.cts +111 -0
- package/dist/partner/auth-core.d.cts.map +1 -0
- package/dist/partner/auth-core.d.ts +111 -0
- package/dist/partner/auth-core.d.ts.map +1 -0
- package/dist/partner/auth-core.js +175 -0
- package/dist/partner/auth-core.js.map +1 -0
- package/dist/partner/auth-shape.cjs +36 -0
- package/dist/partner/auth-shape.cjs.map +1 -0
- package/dist/partner/auth-shape.d.cts +12 -0
- package/dist/partner/auth-shape.d.cts.map +1 -0
- package/dist/partner/auth-shape.d.ts +12 -0
- package/dist/partner/auth-shape.d.ts.map +1 -0
- package/dist/partner/auth-shape.js +36 -0
- package/dist/partner/auth-shape.js.map +1 -0
- package/dist/partner/client.cjs +255 -0
- package/dist/partner/client.cjs.map +1 -0
- package/dist/partner/client.d.cts +335 -0
- package/dist/partner/client.d.cts.map +1 -0
- package/dist/partner/client.d.ts +335 -0
- package/dist/partner/client.d.ts.map +1 -0
- package/dist/partner/client.js +255 -0
- package/dist/partner/client.js.map +1 -0
- package/dist/partner/endpoint-url.cjs +108 -0
- package/dist/partner/endpoint-url.cjs.map +1 -0
- package/dist/partner/endpoint-url.d.cts +72 -0
- package/dist/partner/endpoint-url.d.cts.map +1 -0
- package/dist/partner/endpoint-url.d.ts +72 -0
- package/dist/partner/endpoint-url.d.ts.map +1 -0
- package/dist/partner/endpoint-url.js +105 -0
- package/dist/partner/endpoint-url.js.map +1 -0
- package/dist/partner/in-memory.cjs +135 -0
- package/dist/partner/in-memory.cjs.map +1 -0
- package/dist/partner/in-memory.d.cts +99 -0
- package/dist/partner/in-memory.d.cts.map +1 -0
- package/dist/partner/in-memory.d.ts +99 -0
- package/dist/partner/in-memory.d.ts.map +1 -0
- package/dist/partner/in-memory.js +135 -0
- package/dist/partner/in-memory.js.map +1 -0
- package/dist/partner/index.cjs +18 -0
- package/dist/partner/index.d.cts +23 -0
- package/dist/partner/index.d.cts.map +1 -0
- package/dist/partner/index.d.ts +23 -0
- package/dist/partner/index.d.ts.map +1 -0
- package/dist/partner/index.js +7 -0
- package/dist/partner/raw.cjs +392 -0
- package/dist/partner/raw.cjs.map +1 -0
- package/dist/partner/raw.d.cts +223 -0
- package/dist/partner/raw.d.cts.map +1 -0
- package/dist/partner/raw.d.ts +223 -0
- package/dist/partner/raw.d.ts.map +1 -0
- package/dist/partner/raw.js +390 -0
- package/dist/partner/raw.js.map +1 -0
- package/dist/partner/redaction-placeholders.cjs +10 -0
- package/dist/partner/redaction-placeholders.cjs.map +1 -0
- package/dist/partner/redaction-placeholders.d.cts +5 -0
- package/dist/partner/redaction-placeholders.d.cts.map +1 -0
- package/dist/partner/redaction-placeholders.d.ts +5 -0
- package/dist/partner/redaction-placeholders.d.ts.map +1 -0
- package/dist/partner/redaction-placeholders.js +9 -0
- package/dist/partner/redaction-placeholders.js.map +1 -0
- package/dist/partner/redaction.cjs +267 -0
- package/dist/partner/redaction.cjs.map +1 -0
- package/dist/partner/redaction.d.cts +124 -0
- package/dist/partner/redaction.d.cts.map +1 -0
- package/dist/partner/redaction.d.ts +124 -0
- package/dist/partner/redaction.d.ts.map +1 -0
- package/dist/partner/redaction.js +257 -0
- package/dist/partner/redaction.js.map +1 -0
- package/dist/raw/index.cjs +81 -19
- package/dist/raw/index.cjs.map +1 -1
- package/dist/raw/index.d.cts +6 -2
- package/dist/raw/index.d.cts.map +1 -1
- package/dist/raw/index.d.ts +6 -2
- package/dist/raw/index.d.ts.map +1 -1
- package/dist/raw/index.js +81 -19
- package/dist/raw/index.js.map +1 -1
- package/dist/raw/url.cjs +111 -0
- package/dist/raw/url.cjs.map +1 -0
- package/dist/raw/url.d.cts +41 -0
- package/dist/raw/url.d.cts.map +1 -0
- package/dist/raw/url.d.ts +41 -0
- package/dist/raw/url.d.ts.map +1 -0
- package/dist/raw/url.js +110 -0
- package/dist/raw/url.js.map +1 -0
- package/dist/s3/sigv4.cjs +15 -10
- package/dist/s3/sigv4.cjs.map +1 -1
- package/dist/s3/sigv4.js +15 -10
- package/dist/s3/sigv4.js.map +1 -1
- package/dist/simulator/index.cjs +1197 -206
- package/dist/simulator/index.cjs.map +1 -1
- package/dist/simulator/index.d.cts +109 -11
- package/dist/simulator/index.d.cts.map +1 -1
- package/dist/simulator/index.d.ts +109 -11
- package/dist/simulator/index.d.ts.map +1 -1
- package/dist/simulator/index.js +1196 -209
- package/dist/simulator/index.js.map +1 -1
- package/dist/simulator/partner.cjs +719 -0
- package/dist/simulator/partner.cjs.map +1 -0
- package/dist/simulator/partner.d.cts +183 -0
- package/dist/simulator/partner.d.cts.map +1 -0
- package/dist/simulator/partner.d.ts +183 -0
- package/dist/simulator/partner.d.ts.map +1 -0
- package/dist/simulator/partner.js +718 -0
- package/dist/simulator/partner.js.map +1 -0
- package/dist/simulator/validation.cjs +577 -0
- package/dist/simulator/validation.cjs.map +1 -1
- package/dist/simulator/validation.d.cts +117 -0
- package/dist/simulator/validation.d.cts.map +1 -1
- package/dist/simulator/validation.d.ts +117 -0
- package/dist/simulator/validation.d.ts.map +1 -1
- package/dist/simulator/validation.js +563 -1
- package/dist/simulator/validation.js.map +1 -1
- package/dist/streams/file-source.cjs +1 -1
- package/dist/streams/file-source.cjs.map +1 -1
- package/dist/streams/file-source.js +1 -1
- package/dist/streams/file-source.js.map +1 -1
- package/dist/streams/index.cjs +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/streams/source.cjs +2 -1
- package/dist/streams/source.cjs.map +1 -1
- package/dist/streams/source.js +2 -1
- package/dist/streams/source.js.map +1 -1
- package/dist/sync/b2-sha1-reader.cjs +6 -2
- package/dist/sync/b2-sha1-reader.cjs.map +1 -1
- package/dist/sync/b2-sha1-reader.d.cts.map +1 -1
- package/dist/sync/b2-sha1-reader.d.ts.map +1 -1
- package/dist/sync/b2-sha1-reader.js +6 -2
- package/dist/sync/b2-sha1-reader.js.map +1 -1
- package/dist/sync/download-staging.cjs +11 -10
- package/dist/sync/download-staging.cjs.map +1 -1
- package/dist/sync/download-staging.d.cts +2 -1
- package/dist/sync/download-staging.d.cts.map +1 -1
- package/dist/sync/download-staging.d.ts +2 -1
- package/dist/sync/download-staging.d.ts.map +1 -1
- package/dist/sync/download-staging.js +11 -10
- package/dist/sync/download-staging.js.map +1 -1
- package/dist/sync/filters.cjs.map +1 -1
- package/dist/sync/filters.js.map +1 -1
- package/dist/sync/local-file-io.cjs +11 -6
- package/dist/sync/local-file-io.cjs.map +1 -1
- package/dist/sync/local-file-io.d.cts +1 -0
- package/dist/sync/local-file-io.d.cts.map +1 -1
- package/dist/sync/local-file-io.d.ts +1 -0
- package/dist/sync/local-file-io.d.ts.map +1 -1
- package/dist/sync/local-file-io.js +11 -6
- package/dist/sync/local-file-io.js.map +1 -1
- package/dist/sync/local-sha1.cjs +6 -15
- package/dist/sync/local-sha1.cjs.map +1 -1
- package/dist/sync/local-sha1.d.cts +0 -8
- package/dist/sync/local-sha1.d.cts.map +1 -1
- package/dist/sync/local-sha1.d.ts +0 -8
- package/dist/sync/local-sha1.d.ts.map +1 -1
- package/dist/sync/local-sha1.js +7 -15
- package/dist/sync/local-sha1.js.map +1 -1
- package/dist/sync/policies/compare.cjs +5 -3
- package/dist/sync/policies/compare.cjs.map +1 -1
- package/dist/sync/policies/compare.d.cts.map +1 -1
- package/dist/sync/policies/compare.d.ts.map +1 -1
- package/dist/sync/policies/compare.js +6 -4
- package/dist/sync/policies/compare.js.map +1 -1
- package/dist/sync/policies/index.cjs +4 -10
- package/dist/sync/policies/index.cjs.map +1 -1
- package/dist/sync/policies/index.js +4 -10
- package/dist/sync/policies/index.js.map +1 -1
- package/dist/sync/scanners/b2.cjs +2 -2
- package/dist/sync/scanners/b2.cjs.map +1 -1
- package/dist/sync/scanners/b2.js +2 -2
- package/dist/sync/scanners/b2.js.map +1 -1
- package/dist/sync/scanners/local.cjs.map +1 -1
- package/dist/sync/scanners/local.js.map +1 -1
- package/dist/sync/synchronizer.cjs +3 -2
- package/dist/sync/synchronizer.cjs.map +1 -1
- package/dist/sync/synchronizer.js +3 -2
- package/dist/sync/synchronizer.js.map +1 -1
- package/dist/types/auth.cjs +4 -0
- package/dist/types/auth.cjs.map +1 -1
- package/dist/types/auth.d.cts +4 -0
- package/dist/types/auth.d.cts.map +1 -1
- package/dist/types/auth.d.ts +4 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/auth.js +4 -0
- package/dist/types/auth.js.map +1 -1
- package/dist/types/backup.d.cts +80 -0
- package/dist/types/backup.d.cts.map +1 -0
- package/dist/types/backup.d.ts +80 -0
- package/dist/types/backup.d.ts.map +1 -0
- package/dist/types/bucket.cjs.map +1 -1
- package/dist/types/bucket.d.cts +4 -2
- package/dist/types/bucket.d.cts.map +1 -1
- package/dist/types/bucket.d.ts +4 -2
- package/dist/types/bucket.d.ts.map +1 -1
- package/dist/types/bucket.js.map +1 -1
- package/dist/types/download.cjs +62 -0
- package/dist/types/download.cjs.map +1 -0
- package/dist/types/download.d.cts +98 -0
- package/dist/types/download.d.cts.map +1 -1
- package/dist/types/download.d.ts +98 -0
- package/dist/types/download.d.ts.map +1 -1
- package/dist/types/download.js +61 -0
- package/dist/types/download.js.map +1 -0
- package/dist/types/encryption.cjs +58 -229
- package/dist/types/encryption.cjs.map +1 -1
- package/dist/types/encryption.d.cts +32 -0
- package/dist/types/encryption.d.cts.map +1 -1
- package/dist/types/encryption.d.ts +32 -0
- package/dist/types/encryption.d.ts.map +1 -1
- package/dist/types/encryption.js +56 -229
- package/dist/types/encryption.js.map +1 -1
- package/dist/types/errors.cjs +9 -0
- package/dist/types/errors.cjs.map +1 -1
- package/dist/types/errors.d.cts +1 -1
- package/dist/types/errors.d.cts.map +1 -1
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +9 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/file.cjs.map +1 -1
- package/dist/types/file.d.cts +15 -2
- package/dist/types/file.d.cts.map +1 -1
- package/dist/types/file.d.ts +15 -2
- package/dist/types/file.d.ts.map +1 -1
- package/dist/types/file.js.map +1 -1
- package/dist/types/ids.cjs +30 -0
- package/dist/types/ids.cjs.map +1 -1
- package/dist/types/ids.d.cts +32 -0
- package/dist/types/ids.d.cts.map +1 -1
- package/dist/types/ids.d.ts +32 -0
- package/dist/types/ids.d.ts.map +1 -1
- package/dist/types/ids.js +28 -1
- package/dist/types/ids.js.map +1 -1
- package/dist/types/index.d.cts +10 -6
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +10 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lock.cjs.map +1 -1
- package/dist/types/lock.d.cts +14 -0
- package/dist/types/lock.d.cts.map +1 -1
- package/dist/types/lock.d.ts +14 -0
- package/dist/types/lock.d.ts.map +1 -1
- package/dist/types/lock.js.map +1 -1
- package/dist/types/notifications.cjs +1 -1
- package/dist/types/notifications.cjs.map +1 -1
- package/dist/types/notifications.d.cts +5 -3
- package/dist/types/notifications.d.cts.map +1 -1
- package/dist/types/notifications.d.ts +5 -3
- package/dist/types/notifications.d.ts.map +1 -1
- package/dist/types/notifications.js +1 -1
- package/dist/types/notifications.js.map +1 -1
- package/dist/types/partner.cjs +32 -0
- package/dist/types/partner.cjs.map +1 -0
- package/dist/types/partner.d.cts +394 -0
- package/dist/types/partner.d.cts.map +1 -0
- package/dist/types/partner.d.ts +394 -0
- package/dist/types/partner.d.ts.map +1 -0
- package/dist/types/partner.js +29 -0
- package/dist/types/partner.js.map +1 -0
- package/dist/types/upload.d.cts +31 -18
- package/dist/types/upload.d.cts.map +1 -1
- package/dist/types/upload.d.ts +31 -18
- package/dist/types/upload.d.ts.map +1 -1
- package/dist/upload/abort-scope.cjs +0 -45
- package/dist/upload/abort-scope.cjs.map +1 -1
- package/dist/upload/abort-scope.d.cts +0 -22
- package/dist/upload/abort-scope.d.cts.map +1 -1
- package/dist/upload/abort-scope.d.ts +0 -22
- package/dist/upload/abort-scope.d.ts.map +1 -1
- package/dist/upload/abort-scope.js +1 -44
- package/dist/upload/abort-scope.js.map +1 -1
- package/dist/upload/finish.cjs +3 -8
- package/dist/upload/finish.cjs.map +1 -1
- package/dist/upload/finish.d.cts.map +1 -1
- package/dist/upload/finish.d.ts.map +1 -1
- package/dist/upload/finish.js +1 -6
- package/dist/upload/finish.js.map +1 -1
- package/dist/upload/large.cjs +5 -1
- package/dist/upload/large.cjs.map +1 -1
- package/dist/upload/large.d.cts +16 -2
- package/dist/upload/large.d.cts.map +1 -1
- package/dist/upload/large.d.ts +16 -2
- package/dist/upload/large.d.ts.map +1 -1
- package/dist/upload/large.js +5 -1
- package/dist/upload/large.js.map +1 -1
- package/dist/upload/options.cjs.map +1 -1
- package/dist/upload/options.d.cts +12 -0
- package/dist/upload/options.d.cts.map +1 -1
- package/dist/upload/options.d.ts +12 -0
- package/dist/upload/options.d.ts.map +1 -1
- package/dist/upload/options.js.map +1 -1
- package/dist/upload/resume.cjs +2 -1
- package/dist/upload/resume.cjs.map +1 -1
- package/dist/upload/resume.d.cts +3 -1
- package/dist/upload/resume.d.cts.map +1 -1
- package/dist/upload/resume.d.ts +3 -1
- package/dist/upload/resume.d.ts.map +1 -1
- package/dist/upload/resume.js +2 -1
- package/dist/upload/resume.js.map +1 -1
- package/dist/upload/retry.cjs +8 -3
- package/dist/upload/retry.cjs.map +1 -1
- package/dist/upload/retry.js +8 -3
- package/dist/upload/retry.js.map +1 -1
- package/dist/upload/single.cjs +2 -1
- package/dist/upload/single.cjs.map +1 -1
- package/dist/upload/single.d.cts +6 -0
- package/dist/upload/single.d.cts.map +1 -1
- package/dist/upload/single.d.ts +6 -0
- package/dist/upload/single.d.ts.map +1 -1
- package/dist/upload/single.js +2 -1
- package/dist/upload/single.js.map +1 -1
- package/dist/upload/stream.cjs +6 -4
- package/dist/upload/stream.cjs.map +1 -1
- package/dist/upload/stream.d.cts +6 -0
- package/dist/upload/stream.d.cts.map +1 -1
- package/dist/upload/stream.d.ts +6 -0
- package/dist/upload/stream.d.ts.map +1 -1
- package/dist/upload/stream.js +3 -1
- package/dist/upload/stream.js.map +1 -1
- package/dist/util/abort.cjs +114 -0
- package/dist/util/abort.cjs.map +1 -0
- package/dist/util/abort.d.cts +65 -0
- package/dist/util/abort.d.cts.map +1 -0
- package/dist/util/abort.d.ts +65 -0
- package/dist/util/abort.d.ts.map +1 -0
- package/dist/util/abort.js +109 -0
- package/dist/util/abort.js.map +1 -0
- package/dist/util/defaults.cjs +3 -0
- package/dist/util/defaults.cjs.map +1 -1
- package/dist/util/defaults.d.cts +2 -0
- package/dist/util/defaults.d.cts.map +1 -1
- package/dist/util/defaults.d.ts +2 -0
- package/dist/util/defaults.d.ts.map +1 -1
- package/dist/util/defaults.js +3 -1
- package/dist/util/defaults.js.map +1 -1
- package/dist/util/md5.cjs +211 -0
- package/dist/util/md5.cjs.map +1 -0
- package/dist/util/md5.d.cts +43 -0
- package/dist/util/md5.d.cts.map +1 -0
- package/dist/util/md5.d.ts +43 -0
- package/dist/util/md5.d.ts.map +1 -0
- package/dist/util/md5.js +209 -0
- package/dist/util/md5.js.map +1 -0
- package/dist/util/normalize.cjs +5 -5
- package/dist/util/normalize.cjs.map +1 -1
- package/dist/util/normalize.d.cts +7 -7
- package/dist/util/normalize.d.cts.map +1 -1
- package/dist/util/normalize.d.ts +7 -7
- package/dist/util/normalize.d.ts.map +1 -1
- package/dist/util/normalize.js +5 -5
- package/dist/util/normalize.js.map +1 -1
- package/dist/util/paginator.cjs +7 -1
- package/dist/util/paginator.cjs.map +1 -1
- package/dist/util/paginator.d.cts +3 -1
- package/dist/util/paginator.d.cts.map +1 -1
- package/dist/util/paginator.d.ts +3 -1
- package/dist/util/paginator.d.ts.map +1 -1
- package/dist/util/paginator.js +7 -1
- package/dist/util/paginator.js.map +1 -1
- package/package.json +38 -16
package/dist/simulator/index.js
CHANGED
|
@@ -2,17 +2,83 @@ import { FileAction } from "../types/file.js";
|
|
|
2
2
|
import { accountId, bucketId, fileId } from "../types/ids.js";
|
|
3
3
|
import { utf8Decoder, utf8Encoder } from "../util/text-codec.js";
|
|
4
4
|
import { encodeFileName } from "../raw/encoding.js";
|
|
5
|
-
import {
|
|
5
|
+
import { DownloadClientUnauthorizedToReadMarker, DownloadHeaderName } from "../types/download.js";
|
|
6
|
+
import { md5Base64, md5Base64Sync } from "../util/md5.js";
|
|
7
|
+
import { EncryptionAlgorithm, EncryptionMode } from "../types/encryption.js";
|
|
8
|
+
import { LegalHoldValue, RetentionMode } from "../types/lock.js";
|
|
9
|
+
import { hexEncode, hmacSha256 } from "../util/crypto.js";
|
|
6
10
|
import { sha1Hex } from "../streams/hash.js";
|
|
7
11
|
import { BucketRetentionMode } from "../types/bucket.js";
|
|
8
|
-
import { EncryptionAlgorithm, EncryptionMode } from "../types/encryption.js";
|
|
9
12
|
import { toError } from "../util/to-error.js";
|
|
13
|
+
import { b2Url, isB2ApiVersion } from "../raw/url.js";
|
|
10
14
|
import { Capability } from "../types/auth.js";
|
|
15
|
+
import { PartnerSimulator, isPartnerQueryEndpoint } from "./partner.js";
|
|
11
16
|
import { missingCapabilitiesFor } from "./capabilities.js";
|
|
12
17
|
import { BUCKET_NAME_MAX, BUCKET_NAME_MIN, FILE_NAME_MAX_BYTES } from "../internal/b2-naming.js";
|
|
13
|
-
import { BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, LIST_ENDPOINT_CAPS, validateBucketInfo, validateBucketName, validateFileInfo, validateFileName, validateMaxCount } from "./validation.js";
|
|
18
|
+
import { BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, DOWNLOAD_AUTH_DURATION_MAX_SECONDS, DOWNLOAD_AUTH_DURATION_MIN_SECONDS, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, KEY_NAME_MAX, KEY_NAME_MIN, LIST_ENDPOINT_CAPS, normalizeCreateKeyCapabilities, validateBucketInfo, validateBucketName, validateBucketTypes, validateCorsRules, validateCreateKeyCapabilities, validateCreateKeyName, validateDefaultRetention, validateDownloadAuthorizationDuration, validateDownloadAuthorizationPrefix, validateFileInfo, validateFileName, validateLifecycleRules, validateMaxCount, validateNotificationRules, validateReplicationConfiguration } from "./validation.js";
|
|
14
19
|
//#region src/simulator/index.ts
|
|
15
|
-
var UPLOAD_TOKEN_SIGNING_KEY =
|
|
20
|
+
var UPLOAD_TOKEN_SIGNING_KEY = [
|
|
21
|
+
"b2 sdk typescript",
|
|
22
|
+
"simulator upload authorization",
|
|
23
|
+
"v1"
|
|
24
|
+
].join(":");
|
|
25
|
+
var SIMULATOR_MASTER_APPLICATION_KEY_ID = "master-key-id";
|
|
26
|
+
var SIMULATOR_MASTER_APPLICATION_KEY = "master-key";
|
|
27
|
+
var SIMULATOR_TEST_APPLICATION_KEY_ID = "test-key-id";
|
|
28
|
+
var SIMULATOR_TEST_APPLICATION_KEY = "test-key";
|
|
29
|
+
var SIMULATOR_APPLICATION_KEY_ID_PREFIX = "sim_key_";
|
|
30
|
+
var SIMULATOR_MASTER_CAPABILITIES = [
|
|
31
|
+
Capability.ListBuckets,
|
|
32
|
+
Capability.ListAllBucketNames,
|
|
33
|
+
Capability.ReadBuckets,
|
|
34
|
+
Capability.WriteBuckets,
|
|
35
|
+
Capability.DeleteBuckets,
|
|
36
|
+
Capability.ReadBucketEncryption,
|
|
37
|
+
Capability.WriteBucketEncryption,
|
|
38
|
+
Capability.ReadBucketReplications,
|
|
39
|
+
Capability.WriteBucketReplications,
|
|
40
|
+
Capability.ReadBucketNotifications,
|
|
41
|
+
Capability.WriteBucketNotifications,
|
|
42
|
+
Capability.ReadBucketLogging,
|
|
43
|
+
Capability.WriteBucketLogging,
|
|
44
|
+
Capability.ReadBucketLifecycleRules,
|
|
45
|
+
Capability.WriteBucketLifecycleRules,
|
|
46
|
+
Capability.ListFiles,
|
|
47
|
+
Capability.ReadFiles,
|
|
48
|
+
Capability.WriteFiles,
|
|
49
|
+
Capability.DeleteFiles,
|
|
50
|
+
Capability.ListKeys,
|
|
51
|
+
Capability.WriteKeys,
|
|
52
|
+
Capability.DeleteKeys,
|
|
53
|
+
Capability.ShareFiles
|
|
54
|
+
];
|
|
55
|
+
function apiPathParts(path) {
|
|
56
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
57
|
+
const candidate = segments.at(-2);
|
|
58
|
+
return {
|
|
59
|
+
endpoint: segments.at(-1) ?? "",
|
|
60
|
+
version: candidate !== void 0 && isB2ApiVersion(candidate) ? candidate : "v3"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseBasicAuthorizationHeader(authzHeader) {
|
|
64
|
+
if (!authzHeader?.startsWith("Basic ")) return null;
|
|
65
|
+
const decoded = (() => {
|
|
66
|
+
try {
|
|
67
|
+
return atob(authzHeader.slice(6));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
if (decoded === null) return null;
|
|
73
|
+
const idx = decoded.indexOf(":");
|
|
74
|
+
if (idx <= 0 || idx === decoded.length - 1) return null;
|
|
75
|
+
return {
|
|
76
|
+
applicationKeyId: decoded.slice(0, idx),
|
|
77
|
+
applicationKey: decoded.slice(idx + 1)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var B2_MIN_PART_NUMBER = 1;
|
|
81
|
+
var B2_MAX_PART_NUMBER = 1e4;
|
|
16
82
|
function base64UrlEncode(bytes) {
|
|
17
83
|
let binary = "";
|
|
18
84
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
@@ -34,6 +100,11 @@ function timingSafeStringEqual(left, right) {
|
|
|
34
100
|
for (let i = 0; i < max; i++) mismatch |= (left.charCodeAt(i) || 0) ^ (right.charCodeAt(i) || 0);
|
|
35
101
|
return mismatch === 0;
|
|
36
102
|
}
|
|
103
|
+
var MS_PER_DAY = 864e5;
|
|
104
|
+
var MAX_JS_DATE_MS = 864e13;
|
|
105
|
+
function isValidRetentionTimestamp(timestamp, now = Date.now()) {
|
|
106
|
+
return Number.isFinite(timestamp) && timestamp <= MAX_JS_DATE_MS && timestamp > now;
|
|
107
|
+
}
|
|
37
108
|
/**
|
|
38
109
|
* Parse an RFC 7233 `Range` header value into inclusive start/end byte
|
|
39
110
|
* offsets clamped to the file size. Supports closed (`bytes=0-999`),
|
|
@@ -119,6 +190,51 @@ function compareB2FileNames(a, b) {
|
|
|
119
190
|
if (a > b) return 1;
|
|
120
191
|
return 0;
|
|
121
192
|
}
|
|
193
|
+
var DOWNLOAD_AUTH_TOKEN_BYTES = 32;
|
|
194
|
+
var DOWNLOAD_AUTH_TOKEN_PREFIX = "sim_dl_auth_";
|
|
195
|
+
var DOWNLOAD_AUTH_PURGE_BATCH_SIZE = 128;
|
|
196
|
+
var DOWNLOAD_RESPONSE_OVERRIDE_PARAMS = [
|
|
197
|
+
"b2ContentDisposition",
|
|
198
|
+
"b2ContentLanguage",
|
|
199
|
+
"b2ContentEncoding",
|
|
200
|
+
"b2ContentType",
|
|
201
|
+
"b2CacheControl",
|
|
202
|
+
"b2Expires"
|
|
203
|
+
];
|
|
204
|
+
var DOWNLOAD_RESPONSE_OVERRIDE_HEADERS = {
|
|
205
|
+
b2ContentDisposition: DownloadHeaderName.ContentDisposition,
|
|
206
|
+
b2ContentLanguage: DownloadHeaderName.ContentLanguage,
|
|
207
|
+
b2ContentEncoding: DownloadHeaderName.ContentEncoding,
|
|
208
|
+
b2ContentType: DownloadHeaderName.ContentType,
|
|
209
|
+
b2CacheControl: DownloadHeaderName.CacheControl,
|
|
210
|
+
b2Expires: DownloadHeaderName.Expires
|
|
211
|
+
};
|
|
212
|
+
function randomDownloadAuthorizationToken() {
|
|
213
|
+
const crypto = globalThis.crypto;
|
|
214
|
+
if (typeof crypto?.getRandomValues !== "function") throw new Error("Download authorization tokens require globalThis.crypto.getRandomValues");
|
|
215
|
+
const bytes = new Uint8Array(DOWNLOAD_AUTH_TOKEN_BYTES);
|
|
216
|
+
crypto.getRandomValues(bytes);
|
|
217
|
+
return `${DOWNLOAD_AUTH_TOKEN_PREFIX}${hexEncode(bytes)}`;
|
|
218
|
+
}
|
|
219
|
+
function pickDownloadResponseOverrides(req) {
|
|
220
|
+
const overrides = {};
|
|
221
|
+
for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
|
|
222
|
+
const value = req[param];
|
|
223
|
+
if (typeof value === "string") overrides[param] = value;
|
|
224
|
+
}
|
|
225
|
+
return overrides;
|
|
226
|
+
}
|
|
227
|
+
function validateDownloadResponseOverrides(req) {
|
|
228
|
+
for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
|
|
229
|
+
const value = req[param];
|
|
230
|
+
if (value === void 0 || typeof value === "string") continue;
|
|
231
|
+
return {
|
|
232
|
+
code: "bad_request",
|
|
233
|
+
message: `${param} must be a string`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
122
238
|
function normalizeKeyBucketIds(req) {
|
|
123
239
|
return req.bucketIds === void 0 || req.bucketIds === null ? null : Object.freeze([...req.bucketIds]);
|
|
124
240
|
}
|
|
@@ -128,6 +244,18 @@ function singleBucketId(bucketIds) {
|
|
|
128
244
|
function cloneBucketIds(bucketIds) {
|
|
129
245
|
return bucketIds === null ? null : [...bucketIds];
|
|
130
246
|
}
|
|
247
|
+
function cloneCapabilities(capabilities) {
|
|
248
|
+
return [...capabilities];
|
|
249
|
+
}
|
|
250
|
+
function isImplicitStorageMasterCredential(credentials) {
|
|
251
|
+
return credentials.applicationKeyId === SIMULATOR_MASTER_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_MASTER_APPLICATION_KEY || credentials.applicationKeyId === SIMULATOR_TEST_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_TEST_APPLICATION_KEY;
|
|
252
|
+
}
|
|
253
|
+
function isImplicitPartnerMasterCredential(credentials) {
|
|
254
|
+
return credentials.applicationKeyId === SIMULATOR_MASTER_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_MASTER_APPLICATION_KEY;
|
|
255
|
+
}
|
|
256
|
+
function isSimulatorApplicationKeyId(applicationKeyId) {
|
|
257
|
+
return applicationKeyId.startsWith(SIMULATOR_APPLICATION_KEY_ID_PREFIX);
|
|
258
|
+
}
|
|
131
259
|
function hasOwnField(body, field) {
|
|
132
260
|
return typeof body === "object" && body !== null && Object.hasOwn(body, field);
|
|
133
261
|
}
|
|
@@ -136,10 +264,148 @@ function requestStringField(body, field) {
|
|
|
136
264
|
const value = body[field];
|
|
137
265
|
return typeof value === "string" ? value : void 0;
|
|
138
266
|
}
|
|
267
|
+
function requestRecord(body) {
|
|
268
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
|
|
269
|
+
}
|
|
270
|
+
function replicationDestinationBucketIds(body) {
|
|
271
|
+
const rules = requestRecord(requestRecord(requestRecord(body)?.["replicationConfiguration"])?.["asReplicationSource"])?.["replicationRules"];
|
|
272
|
+
if (!Array.isArray(rules)) return [];
|
|
273
|
+
const bucketIds = [];
|
|
274
|
+
for (const rule of rules) {
|
|
275
|
+
const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
|
|
276
|
+
if (typeof destinationBucketId === "string") bucketIds.push(destinationBucketId);
|
|
277
|
+
}
|
|
278
|
+
return [...new Set(bucketIds)];
|
|
279
|
+
}
|
|
280
|
+
function normalizeReplicationConfiguration(config) {
|
|
281
|
+
return {
|
|
282
|
+
asReplicationSource: config.asReplicationSource ?? null,
|
|
283
|
+
asReplicationDestination: config.asReplicationDestination ?? null
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function validateBucketConfigurationFields(fields, options) {
|
|
287
|
+
if (fields.bucketInfo !== void 0) {
|
|
288
|
+
const infoError = validateBucketInfo(fields.bucketInfo);
|
|
289
|
+
if (infoError) return infoError;
|
|
290
|
+
}
|
|
291
|
+
if (fields.corsRules !== void 0) {
|
|
292
|
+
const corsError = validateCorsRules(fields.corsRules);
|
|
293
|
+
if (corsError) return corsError;
|
|
294
|
+
}
|
|
295
|
+
if (fields.lifecycleRules !== void 0) {
|
|
296
|
+
const lifecycleError = validateLifecycleRules(fields.lifecycleRules);
|
|
297
|
+
if (lifecycleError) return lifecycleError;
|
|
298
|
+
}
|
|
299
|
+
if (fields.replicationConfiguration !== void 0) {
|
|
300
|
+
const replicationError = validateReplicationConfiguration(fields.replicationConfiguration);
|
|
301
|
+
if (replicationError) return replicationError;
|
|
302
|
+
}
|
|
303
|
+
if (fields.defaultRetention !== void 0) {
|
|
304
|
+
const retentionError = validateDefaultRetention(fields.defaultRetention);
|
|
305
|
+
if (retentionError) return retentionError;
|
|
306
|
+
if (requestRecord(fields.defaultRetention)?.["mode"] !== BucketRetentionMode.None && !options.objectLockEnabled) return {
|
|
307
|
+
code: "file_lock_not_enabled",
|
|
308
|
+
message: "Bucket must have Object Lock enabled to set defaultRetention"
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
function nextObjectLockEnabled(current, requested) {
|
|
314
|
+
if (requested === void 0) return current;
|
|
315
|
+
if (typeof requested !== "boolean") return {
|
|
316
|
+
code: "bad_request",
|
|
317
|
+
message: "fileLockEnabled must be a boolean"
|
|
318
|
+
};
|
|
319
|
+
if (current && requested === false) return {
|
|
320
|
+
code: "file_lock_conflict",
|
|
321
|
+
message: "Object Lock cannot be disabled"
|
|
322
|
+
};
|
|
323
|
+
return current || requested;
|
|
324
|
+
}
|
|
325
|
+
function requestHeaderValue(headers, name) {
|
|
326
|
+
const exact = headers[name];
|
|
327
|
+
if (exact !== void 0) return exact;
|
|
328
|
+
const lowerName = name.toLowerCase();
|
|
329
|
+
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === lowerName) return value;
|
|
330
|
+
}
|
|
331
|
+
function parseContentLengthHeader(headers) {
|
|
332
|
+
const header = requestHeaderValue(headers, "content-length");
|
|
333
|
+
if (header === void 0) return { kind: "absent" };
|
|
334
|
+
if (!/^\d+$/.test(header)) return {
|
|
335
|
+
kind: "error",
|
|
336
|
+
message: `Content-Length must be a byte count: ${header}`
|
|
337
|
+
};
|
|
338
|
+
const expectedLength = Number(header);
|
|
339
|
+
if (!Number.isSafeInteger(expectedLength)) return {
|
|
340
|
+
kind: "error",
|
|
341
|
+
message: `Content-Length is too large: ${header}`
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
kind: "ok",
|
|
345
|
+
expectedLength
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function contentLengthMismatchMessage(expectedLength, actualLength) {
|
|
349
|
+
return `Content-Length ${expectedLength} does not match request body length ${actualLength}`;
|
|
350
|
+
}
|
|
139
351
|
function fileNames(...names) {
|
|
140
352
|
const present = names.filter((name) => name !== void 0);
|
|
141
353
|
return present.length > 0 ? present : void 0;
|
|
142
354
|
}
|
|
355
|
+
function queryParamsBody(params) {
|
|
356
|
+
const body = Object.create(null);
|
|
357
|
+
let hasParams = false;
|
|
358
|
+
for (const [key, value] of params) {
|
|
359
|
+
if (body[key] !== void 0) continue;
|
|
360
|
+
body[key] = value;
|
|
361
|
+
hasParams = true;
|
|
362
|
+
}
|
|
363
|
+
return hasParams ? body : null;
|
|
364
|
+
}
|
|
365
|
+
var JSON_GET_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
366
|
+
"b2_authorize_account",
|
|
367
|
+
"b2_list_groups",
|
|
368
|
+
"b2_list_group_members",
|
|
369
|
+
"bz_list_computers"
|
|
370
|
+
]);
|
|
371
|
+
var JSON_POST_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
372
|
+
"b2_create_group_member",
|
|
373
|
+
"b2_eject_group_member",
|
|
374
|
+
"b2_reserve_trial_create_account",
|
|
375
|
+
"bz_delete_computer",
|
|
376
|
+
"b2_create_bucket",
|
|
377
|
+
"b2_list_buckets",
|
|
378
|
+
"b2_delete_bucket",
|
|
379
|
+
"b2_update_bucket",
|
|
380
|
+
"b2_get_upload_url",
|
|
381
|
+
"b2_list_file_names",
|
|
382
|
+
"b2_list_file_versions",
|
|
383
|
+
"b2_get_file_info",
|
|
384
|
+
"b2_hide_file",
|
|
385
|
+
"b2_delete_file_version",
|
|
386
|
+
"b2_copy_file",
|
|
387
|
+
"b2_start_large_file",
|
|
388
|
+
"b2_get_upload_part_url",
|
|
389
|
+
"b2_finish_large_file",
|
|
390
|
+
"b2_cancel_large_file",
|
|
391
|
+
"b2_list_unfinished_large_files",
|
|
392
|
+
"b2_list_parts",
|
|
393
|
+
"b2_copy_part",
|
|
394
|
+
"b2_get_download_authorization",
|
|
395
|
+
"b2_create_key",
|
|
396
|
+
"b2_list_keys",
|
|
397
|
+
"b2_delete_key",
|
|
398
|
+
"b2_update_file_retention",
|
|
399
|
+
"b2_update_file_legal_hold",
|
|
400
|
+
"b2_get_bucket_notification_rules",
|
|
401
|
+
"b2_set_bucket_notification_rules"
|
|
402
|
+
]);
|
|
403
|
+
function jsonEndpointAllowsMethod(method, endpoint) {
|
|
404
|
+
const normalizedMethod = method.toUpperCase();
|
|
405
|
+
if (JSON_GET_ENDPOINTS.has(endpoint)) return normalizedMethod === "GET";
|
|
406
|
+
if (JSON_POST_ENDPOINTS.has(endpoint)) return normalizedMethod === "POST";
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
143
409
|
function notificationRulePrefixes(body) {
|
|
144
410
|
if (typeof body !== "object" || body === null) return void 0;
|
|
145
411
|
const rules = body["eventNotificationRules"];
|
|
@@ -156,6 +422,13 @@ function storedNotificationRulePrefixes(rules) {
|
|
|
156
422
|
function hasKeyManagementCapability(capabilities) {
|
|
157
423
|
return capabilities.some((capability) => capability === Capability.ListKeys || capability === Capability.WriteKeys || capability === Capability.DeleteKeys);
|
|
158
424
|
}
|
|
425
|
+
function missingStoredKeyCapabilities(key, required) {
|
|
426
|
+
const capabilities = new Set(key.capabilities);
|
|
427
|
+
return required.filter((capability) => !capabilities.has(capability));
|
|
428
|
+
}
|
|
429
|
+
function storedKeyAllowsBucket(key, bucketId) {
|
|
430
|
+
return key.bucketIds === null || key.bucketIds.includes(bucketId);
|
|
431
|
+
}
|
|
159
432
|
function publicServerSideEncryption(encryption) {
|
|
160
433
|
if (encryption.mode === EncryptionMode.SseC) return {
|
|
161
434
|
mode: encryption.mode,
|
|
@@ -167,26 +440,164 @@ function publicServerSideEncryption(encryption) {
|
|
|
167
440
|
};
|
|
168
441
|
return encryption;
|
|
169
442
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
443
|
+
function customerKeyDigest(customerKey) {
|
|
444
|
+
let first = 2166136261;
|
|
445
|
+
let second = 668265261;
|
|
446
|
+
for (const byte of customerKey) {
|
|
447
|
+
first = Math.imul(first ^ byte, 16777619) >>> 0;
|
|
448
|
+
second = Math.imul(second ^ byte, 2246822507) >>> 0;
|
|
449
|
+
}
|
|
450
|
+
return `${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}`;
|
|
451
|
+
}
|
|
452
|
+
var BASE64_ALPHABET = /^[A-Za-z0-9+/]+$/;
|
|
453
|
+
function normalizeSizedBase64(value, decodedByteLength) {
|
|
454
|
+
const paddedLength = Math.ceil(decodedByteLength / 3) * 4;
|
|
455
|
+
const noPaddingLength = Math.ceil(decodedByteLength * 8 / 6);
|
|
456
|
+
const padding = "=".repeat(paddedLength - noPaddingLength);
|
|
457
|
+
if (value.length === noPaddingLength && BASE64_ALPHABET.test(value)) return value.padEnd(paddedLength, "=");
|
|
458
|
+
if (value.length === paddedLength && value.endsWith(padding) && BASE64_ALPHABET.test(value.slice(0, noPaddingLength))) return value;
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
function base64ToBytes(value, decodedByteLength) {
|
|
462
|
+
const normalized = normalizeSizedBase64(value, decodedByteLength);
|
|
463
|
+
if (normalized === null) return null;
|
|
464
|
+
try {
|
|
465
|
+
const decoded = atob(normalized);
|
|
466
|
+
if (decoded.length !== decodedByteLength) return null;
|
|
467
|
+
const bytes = new Uint8Array(decoded.length);
|
|
468
|
+
for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
|
|
469
|
+
return bytes;
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function encryptionValidationError(message) {
|
|
475
|
+
return {
|
|
476
|
+
status: 400,
|
|
477
|
+
body: {
|
|
478
|
+
status: 400,
|
|
479
|
+
code: "bad_request",
|
|
480
|
+
message
|
|
481
|
+
}
|
|
180
482
|
};
|
|
181
|
-
|
|
483
|
+
}
|
|
484
|
+
function sameStoredServerSideEncryption(a, b) {
|
|
485
|
+
if (a.mode !== b.mode) return false;
|
|
486
|
+
if (a.mode === EncryptionMode.None) return true;
|
|
487
|
+
if (a.mode === EncryptionMode.SseB2 && b.mode === EncryptionMode.SseB2) return a.algorithm === b.algorithm;
|
|
488
|
+
if (a.mode === EncryptionMode.SseC && b.mode === EncryptionMode.SseC) return a.algorithm === b.algorithm && a.customerKeyDigest === b.customerKeyDigest && a.customerKeyMd5 === b.customerKeyMd5;
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
function hasCustomerEncryptionFields(encryption) {
|
|
492
|
+
return encryption["customerKey"] !== void 0 || encryption["customerKeyMd5"] !== void 0 || encryption["customerAlgorithm"] !== void 0;
|
|
493
|
+
}
|
|
494
|
+
function hasCustomerEncryptionHeaders(headers) {
|
|
495
|
+
return headers["x-bz-server-side-encryption-customer-algorithm"] !== void 0 || headers["x-bz-server-side-encryption-customer-key"] !== void 0 || headers["x-bz-server-side-encryption-customer-key-md5"] !== void 0;
|
|
496
|
+
}
|
|
497
|
+
async function storedServerSideEncryption(encryption) {
|
|
498
|
+
const runtimeEncryption = encryption;
|
|
499
|
+
if (encryption.mode === EncryptionMode.SseC) {
|
|
500
|
+
if (runtimeEncryption["algorithm"] === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
|
|
501
|
+
if (encryption.algorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
|
|
502
|
+
const customerKey = encryption.customerKey;
|
|
503
|
+
const customerKeyMd5 = encryption.customerKeyMd5;
|
|
504
|
+
if (typeof customerKey !== "string" || customerKey === "") return encryptionValidationError("SSE-C customer key is required");
|
|
505
|
+
if (typeof customerKeyMd5 !== "string" || customerKeyMd5 === "") return encryptionValidationError("SSE-C customer key MD5 is required");
|
|
506
|
+
const customerKeyBytes = base64ToBytes(customerKey, 32);
|
|
507
|
+
if (customerKeyBytes === null) return encryptionValidationError("SSE-C customer key must be a base64-encoded 256-bit key");
|
|
508
|
+
const normalizedCustomerKeyMd5 = normalizeSizedBase64(customerKeyMd5, 16);
|
|
509
|
+
if (normalizedCustomerKeyMd5 === null) return encryptionValidationError("SSE-C customer key MD5 must be a base64-encoded 128-bit digest");
|
|
510
|
+
if (await md5Base64(customerKeyBytes) !== normalizedCustomerKeyMd5) return encryptionValidationError("SSE-C customer key MD5 does not match the key");
|
|
511
|
+
return {
|
|
512
|
+
mode: encryption.mode,
|
|
513
|
+
algorithm: encryption.algorithm,
|
|
514
|
+
customerKeyDigest: customerKeyDigest(customerKeyBytes),
|
|
515
|
+
customerKeyMd5: normalizedCustomerKeyMd5
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (encryption.mode === EncryptionMode.SseB2) {
|
|
519
|
+
if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
|
|
520
|
+
if (encryption.algorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
|
|
521
|
+
return {
|
|
522
|
+
mode: encryption.mode,
|
|
523
|
+
algorithm: encryption.algorithm
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (encryption.mode === EncryptionMode.None) {
|
|
527
|
+
if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("No-encryption settings must not include SSE-C customer keys");
|
|
528
|
+
return { mode: EncryptionMode.None };
|
|
529
|
+
}
|
|
530
|
+
return encryptionValidationError(`Unsupported server-side encryption mode: ${String(runtimeEncryption["mode"])}`);
|
|
531
|
+
}
|
|
532
|
+
async function uploadServerSideEncryption(headers, fallback) {
|
|
533
|
+
const customerAlgorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
|
|
534
|
+
const customerKey = headers["x-bz-server-side-encryption-customer-key"];
|
|
535
|
+
const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
|
|
536
|
+
const managedEncryption = headers["x-bz-server-side-encryption"];
|
|
537
|
+
if (hasCustomerEncryptionHeaders(headers)) {
|
|
538
|
+
if (managedEncryption !== void 0) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
|
|
539
|
+
if (customerAlgorithm === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
|
|
540
|
+
if (customerAlgorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
|
|
541
|
+
if (!customerKey || !customerKeyMd5) return encryptionValidationError("SSE-C customer key and MD5 are required");
|
|
542
|
+
return await storedServerSideEncryption({
|
|
543
|
+
mode: EncryptionMode.SseC,
|
|
544
|
+
algorithm: EncryptionAlgorithm.Aes256,
|
|
545
|
+
customerKey,
|
|
546
|
+
customerKeyMd5
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (managedEncryption !== void 0) {
|
|
550
|
+
if (managedEncryption !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
|
|
551
|
+
return {
|
|
552
|
+
mode: EncryptionMode.SseB2,
|
|
553
|
+
algorithm: EncryptionAlgorithm.Aes256
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return await storedServerSideEncryption(fallback);
|
|
182
557
|
}
|
|
183
558
|
function defaultFileRetention(policy, uploadTimestamp) {
|
|
184
559
|
if (policy.mode === BucketRetentionMode.None || policy.period === null) return null;
|
|
185
|
-
const
|
|
560
|
+
const retainUntilTimestamp = uploadTimestamp + (policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365) * MS_PER_DAY;
|
|
561
|
+
if (!isValidRetentionTimestamp(retainUntilTimestamp, uploadTimestamp)) return null;
|
|
186
562
|
return {
|
|
187
563
|
mode: policy.mode,
|
|
188
|
-
retainUntilTimestamp
|
|
564
|
+
retainUntilTimestamp
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function isRetentionMode(value) {
|
|
568
|
+
return value === RetentionMode.Compliance || value === RetentionMode.Governance;
|
|
569
|
+
}
|
|
570
|
+
function parseFileRetentionValue(value, now) {
|
|
571
|
+
if (typeof value !== "object" || value === null) return null;
|
|
572
|
+
const candidate = value;
|
|
573
|
+
const mode = candidate["mode"];
|
|
574
|
+
const retainUntilTimestamp = candidate["retainUntilTimestamp"];
|
|
575
|
+
if (mode === null && retainUntilTimestamp === null) return {
|
|
576
|
+
mode,
|
|
577
|
+
retainUntilTimestamp
|
|
189
578
|
};
|
|
579
|
+
if (isRetentionMode(mode) && typeof retainUntilTimestamp === "number") {
|
|
580
|
+
if (!isValidRetentionTimestamp(retainUntilTimestamp, now)) return null;
|
|
581
|
+
return {
|
|
582
|
+
mode,
|
|
583
|
+
retainUntilTimestamp
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
function isRetentionWeakened(current, next) {
|
|
589
|
+
if (current === null || current === void 0) return false;
|
|
590
|
+
if (current.mode === null) return false;
|
|
591
|
+
if (next.mode === null) return true;
|
|
592
|
+
if (current.mode === RetentionMode.Compliance && next.mode !== RetentionMode.Compliance) return true;
|
|
593
|
+
if (current.retainUntilTimestamp === null) return next.mode !== current.mode;
|
|
594
|
+
if (next.retainUntilTimestamp === null) return true;
|
|
595
|
+
return next.retainUntilTimestamp < current.retainUntilTimestamp;
|
|
596
|
+
}
|
|
597
|
+
function requiresGovernanceBypass(current, next) {
|
|
598
|
+
if (current?.mode !== RetentionMode.Governance) return false;
|
|
599
|
+
if (next.mode === RetentionMode.Compliance) return true;
|
|
600
|
+
return isRetentionWeakened(current, next);
|
|
190
601
|
}
|
|
191
602
|
/**
|
|
192
603
|
* In-memory B2 simulator for testing. Implements the B2 native API at the
|
|
@@ -203,6 +614,15 @@ function defaultFileRetention(policy, uploadTimestamp) {
|
|
|
203
614
|
* })
|
|
204
615
|
* await client.authorize()
|
|
205
616
|
* ```
|
|
617
|
+
*
|
|
618
|
+
* `test-key-id:test-key` is the documented implicit full-access
|
|
619
|
+
* storage credential. `master-key-id:master-key` is also accepted for
|
|
620
|
+
* tests that share storage and Partner simulator credentials. In the
|
|
621
|
+
* default permissive mode, other well-formed Basic credentials whose
|
|
622
|
+
* applicationKeyId was not minted by `b2_create_key` receive the same
|
|
623
|
+
* implicit master grant for backward compatibility. A simulator-created
|
|
624
|
+
* key always authorizes with its own capabilities, bucket restrictions,
|
|
625
|
+
* name prefix, and expiration.
|
|
206
626
|
*/
|
|
207
627
|
var B2Simulator = class {
|
|
208
628
|
buckets = /* @__PURE__ */ new Map();
|
|
@@ -219,6 +639,8 @@ var B2Simulator = class {
|
|
|
219
639
|
onHookError;
|
|
220
640
|
strictAuth;
|
|
221
641
|
authTokenTtlMs;
|
|
642
|
+
customUploadTimestampsEnabled;
|
|
643
|
+
partner;
|
|
222
644
|
/**
|
|
223
645
|
* Issued auth tokens with their associated grant scope + expiry. In
|
|
224
646
|
* permissive mode (`strictAuth: false`) this is still populated by
|
|
@@ -234,6 +656,8 @@ var B2Simulator = class {
|
|
|
234
656
|
* state for tests.
|
|
235
657
|
*/
|
|
236
658
|
uploadTokens = /* @__PURE__ */ new Map();
|
|
659
|
+
downloadAuthorizationTokens = /* @__PURE__ */ new Map();
|
|
660
|
+
downloadAuthorizationExpiryHeap = [];
|
|
237
661
|
/**
|
|
238
662
|
* Virtual-clock offset applied to `Date.now()` for expiry checks.
|
|
239
663
|
* Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
|
|
@@ -277,7 +701,23 @@ var B2Simulator = class {
|
|
|
277
701
|
if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
|
|
278
702
|
if (options.onHookError !== void 0) this.onHookError = options.onHookError;
|
|
279
703
|
this.strictAuth = options.strictAuth ?? false;
|
|
280
|
-
this.
|
|
704
|
+
this.customUploadTimestampsEnabled = options.customUploadTimestampsEnabled ?? false;
|
|
705
|
+
this.authTokenTtlMs = options.authTokenTtlMs ?? 864e5;
|
|
706
|
+
this.partner = new PartnerSimulator({
|
|
707
|
+
accountId: this.accountId,
|
|
708
|
+
authTokenTtlMs: this.authTokenTtlMs,
|
|
709
|
+
minimumPartSize: this.minimumPartSize,
|
|
710
|
+
recommendedPartSize: this.recommendedPartSize,
|
|
711
|
+
canAuthorize: (authzHeader) => this.canAuthorizePartner(authzHeader),
|
|
712
|
+
createBucket: (req) => this.createBucket(req),
|
|
713
|
+
error: (status, code, message) => this.error(status, code, message),
|
|
714
|
+
genId: (prefix) => this.genId(prefix),
|
|
715
|
+
monotonicTimestamp: () => this.monotonicTimestamp(),
|
|
716
|
+
now: () => this.now(),
|
|
717
|
+
storeKey: (key) => {
|
|
718
|
+
this.keys.set(key.applicationKeyId, key);
|
|
719
|
+
}
|
|
720
|
+
}, options);
|
|
281
721
|
}
|
|
282
722
|
/**
|
|
283
723
|
* Advance the simulator's virtual clock by `ms` milliseconds. Used in
|
|
@@ -335,6 +775,9 @@ var B2Simulator = class {
|
|
|
335
775
|
now() {
|
|
336
776
|
return Date.now() + this.clockOffsetMs;
|
|
337
777
|
}
|
|
778
|
+
isStoredKeyExpired(key) {
|
|
779
|
+
return key.expirationTimestamp !== null && key.expirationTimestamp <= this.now();
|
|
780
|
+
}
|
|
338
781
|
pruneExpiredUploadTokens(now = this.now()) {
|
|
339
782
|
for (const [authorizationToken, token] of this.uploadTokens.entries()) if (now >= token.cleanupAt) this.uploadTokens.delete(authorizationToken);
|
|
340
783
|
}
|
|
@@ -458,15 +901,195 @@ var B2Simulator = class {
|
|
|
458
901
|
const token = this.issuedTokens.get(authToken);
|
|
459
902
|
if (!token) return this.error(401, "bad_auth_token", "unknown auth token");
|
|
460
903
|
if (this.now() > token.expiresAt) return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
|
|
904
|
+
if (token.applicationKeyId !== null) {
|
|
905
|
+
const key = this.keys.get(token.applicationKeyId);
|
|
906
|
+
if (key === void 0) return this.error(401, "bad_auth_token", "application key has been deleted; reauthorize");
|
|
907
|
+
if (this.isStoredKeyExpired(key)) return this.error(401, "expired_auth_token", "application key has expired; reauthorize");
|
|
908
|
+
}
|
|
461
909
|
const missing = missingCapabilitiesFor(endpoint, token.capabilities);
|
|
462
910
|
if (missing.length > 0) return this.error(403, "unauthorized", `application key lacks required capabilities: ${missing.join(", ")}`);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
911
|
+
return this.authorizeScopeGrant(scope, {
|
|
912
|
+
bucketIds: token.bucketIds,
|
|
913
|
+
namePrefix: token.namePrefix,
|
|
914
|
+
bucketScopeRequiredMessage: () => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; bucket scope is required`,
|
|
915
|
+
bucketMismatchMessage: (bucketId) => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; cannot access ${bucketId}`,
|
|
916
|
+
prefixMismatchMessage: (fileName) => `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
authorizeScopeGrant(scope, grant) {
|
|
920
|
+
if (grant.bucketIds !== null) {
|
|
921
|
+
if (scope?.requiresAccountLevelBucketAccess === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
|
|
922
|
+
if ((scope?.bucketIds.length ?? 0) === 0 && scope?.requiresBucketScope === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
|
|
923
|
+
for (const bucketId of scope?.bucketIds ?? []) {
|
|
924
|
+
if (grant.bucketIds.includes(bucketId)) continue;
|
|
925
|
+
return this.error(403, "unauthorized", grant.bucketMismatchMessage(bucketId));
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (grant.namePrefix !== null) for (const fileName of scope?.fileNames ?? []) {
|
|
929
|
+
if (fileName.startsWith(grant.namePrefix)) continue;
|
|
930
|
+
return this.error(403, "unauthorized", grant.prefixMismatchMessage(fileName));
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
authorizeDownloadRequest(authToken, endpoint, scope, url) {
|
|
935
|
+
if (authToken === void 0 || authToken === "") return this.error(401, "bad_auth_token", "missing authorization token");
|
|
936
|
+
const downloadAuth = this.downloadAuthorizationTokens.get(authToken);
|
|
937
|
+
if (downloadAuth === void 0) {
|
|
938
|
+
this.purgeExpiredDownloadAuthorizationTokens();
|
|
939
|
+
return this.authorizeRequest(authToken, endpoint, scope);
|
|
940
|
+
}
|
|
941
|
+
if (this.isExpiredDownloadAuthorizationToken(downloadAuth)) {
|
|
942
|
+
this.downloadAuthorizationTokens.delete(authToken);
|
|
943
|
+
this.purgeExpiredDownloadAuthorizationTokens();
|
|
944
|
+
return this.error(401, "expired_auth_token", "download authorization token has expired");
|
|
945
|
+
}
|
|
946
|
+
if (endpoint === "b2_download_file_by_id") return this.error(403, "unauthorized", "download authorization tokens cannot be used with b2_download_file_by_id");
|
|
947
|
+
const overrideError = this.authorizeDownloadResponseOverrides(downloadAuth, url);
|
|
948
|
+
if (overrideError !== null) return overrideError;
|
|
949
|
+
return this.authorizeScopeGrant(scope, {
|
|
950
|
+
bucketIds: [downloadAuth.bucketId],
|
|
951
|
+
namePrefix: downloadAuth.fileNamePrefix,
|
|
952
|
+
bucketScopeRequiredMessage: () => `download authorization is scoped to bucket ${downloadAuth.bucketId}; bucket scope is required`,
|
|
953
|
+
bucketMismatchMessage: (bucketId) => `download authorization is scoped to bucket ${downloadAuth.bucketId}; cannot access ${bucketId}`,
|
|
954
|
+
prefixMismatchMessage: (fileName) => `download authorization is scoped to prefix "${downloadAuth.fileNamePrefix}"; "${fileName}" is outside scope`
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
isExpiredDownloadAuthorizationToken(token, now = this.now()) {
|
|
958
|
+
return now >= token.expiresAt;
|
|
959
|
+
}
|
|
960
|
+
purgeExpiredDownloadAuthorizationTokens() {
|
|
961
|
+
const now = this.now();
|
|
962
|
+
for (let i = 0; i < DOWNLOAD_AUTH_PURGE_BATCH_SIZE; i++) {
|
|
963
|
+
const next = this.downloadAuthorizationExpiryHeap[0];
|
|
964
|
+
if (next === void 0 || next.expiresAt > now) return;
|
|
965
|
+
this.popDownloadAuthorizationExpiry();
|
|
966
|
+
if (this.downloadAuthorizationTokens.get(next.token)?.expiresAt === next.expiresAt) this.downloadAuthorizationTokens.delete(next.token);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
pushDownloadAuthorizationExpiry(entry) {
|
|
970
|
+
const heap = this.downloadAuthorizationExpiryHeap;
|
|
971
|
+
heap.push(entry);
|
|
972
|
+
let idx = heap.length - 1;
|
|
973
|
+
while (idx > 0) {
|
|
974
|
+
const parentIdx = Math.floor((idx - 1) / 2);
|
|
975
|
+
const parent = heap[parentIdx];
|
|
976
|
+
if (parent.expiresAt <= entry.expiresAt) break;
|
|
977
|
+
heap[idx] = parent;
|
|
978
|
+
idx = parentIdx;
|
|
979
|
+
}
|
|
980
|
+
heap[idx] = entry;
|
|
981
|
+
}
|
|
982
|
+
popDownloadAuthorizationExpiry() {
|
|
983
|
+
const heap = this.downloadAuthorizationExpiryHeap;
|
|
984
|
+
const root = heap[0];
|
|
985
|
+
const last = heap.pop();
|
|
986
|
+
if (root === void 0 || last === void 0 || heap.length === 0) return root;
|
|
987
|
+
let idx = 0;
|
|
988
|
+
while (true) {
|
|
989
|
+
const leftIdx = idx * 2 + 1;
|
|
990
|
+
const rightIdx = leftIdx + 1;
|
|
991
|
+
const left = heap[leftIdx];
|
|
992
|
+
const right = heap[rightIdx];
|
|
993
|
+
if (left === void 0) break;
|
|
994
|
+
const childIdx = right !== void 0 && right.expiresAt < left.expiresAt ? rightIdx : leftIdx;
|
|
995
|
+
const child = heap[childIdx];
|
|
996
|
+
if (child.expiresAt >= last.expiresAt) break;
|
|
997
|
+
heap[idx] = child;
|
|
998
|
+
idx = childIdx;
|
|
999
|
+
}
|
|
1000
|
+
heap[idx] = last;
|
|
1001
|
+
return root;
|
|
1002
|
+
}
|
|
1003
|
+
authorizeDownloadResponseOverrides(downloadAuth, url) {
|
|
1004
|
+
for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
|
|
1005
|
+
const expected = downloadAuth.responseHeaderOverrides[param];
|
|
1006
|
+
if (expected === void 0) continue;
|
|
1007
|
+
const actual = url.searchParams.get(param);
|
|
1008
|
+
if (actual === expected) continue;
|
|
1009
|
+
const expectedText = JSON.stringify(expected);
|
|
1010
|
+
const actualText = actual === null ? "absent" : JSON.stringify(actual);
|
|
1011
|
+
return this.error(403, "unauthorized", `download authorization requires ${param}=${expectedText}; got ${actualText}`);
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
downloadAuthorizationFromRequest(headers, url, options = {}) {
|
|
1016
|
+
const headerToken = requestHeaderValue(headers, "authorization");
|
|
1017
|
+
if (headerToken !== void 0) return headerToken;
|
|
1018
|
+
if (options.allowQueryAuthorization !== true) return void 0;
|
|
1019
|
+
const queryToken = url.searchParams.get("Authorization") ?? url.searchParams.get("authorization");
|
|
1020
|
+
return queryToken?.startsWith(DOWNLOAD_AUTH_TOKEN_PREFIX) ? queryToken : void 0;
|
|
1021
|
+
}
|
|
1022
|
+
requestHasCapability(authToken, capability) {
|
|
1023
|
+
if (!this.strictAuth) return true;
|
|
1024
|
+
if (authToken === void 0) return false;
|
|
1025
|
+
return this.issuedTokens.get(authToken)?.capabilities.includes(capability) === true;
|
|
1026
|
+
}
|
|
1027
|
+
requireFileLockEnabled(bucket) {
|
|
1028
|
+
if (bucket.info.fileLockConfiguration.value?.isFileLockEnabled === true) return null;
|
|
1029
|
+
return this.error(400, "file_lock_not_enabled", "Bucket does not have file lock enabled");
|
|
1030
|
+
}
|
|
1031
|
+
requireGovernanceBypass(req, authToken, message) {
|
|
1032
|
+
if (req.bypassGovernance !== true) return this.error(400, "file_lock_governance_protected", message);
|
|
1033
|
+
if (!this.requestHasCapability(authToken, Capability.BypassGovernance)) return this.error(403, "unauthorized", `application key lacks required capabilities: ${Capability.BypassGovernance}`);
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
validateReplicationApplicationKey(applicationKeyId, requiredCapabilities, options) {
|
|
1037
|
+
const key = this.keys.get(applicationKeyId);
|
|
1038
|
+
if (key === void 0) return this.invalidReplicationApplicationKey(options.role);
|
|
1039
|
+
if (missingStoredKeyCapabilities(key, requiredCapabilities).length > 0) return this.invalidReplicationApplicationKey(options.role);
|
|
1040
|
+
if (options.bucketId !== void 0 && !storedKeyAllowsBucket(key, options.bucketId)) return this.invalidReplicationApplicationKey(options.role);
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
invalidReplicationApplicationKey(role) {
|
|
1044
|
+
return this.error(400, "bad_request", `${role} application key is invalid or not authorized for this replication configuration`);
|
|
1045
|
+
}
|
|
1046
|
+
invalidReplicationDestinationBucket() {
|
|
1047
|
+
return this.error(400, "bad_request", "replication destination bucket is invalid or not configured for this source application key");
|
|
1048
|
+
}
|
|
1049
|
+
validateReplicationDestinationBuckets(sourceApplicationKeyId, rules) {
|
|
1050
|
+
for (const rule of rules) {
|
|
1051
|
+
const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
|
|
1052
|
+
if (typeof destinationBucketId !== "string") continue;
|
|
1053
|
+
const destinationApplicationKeyId = this.buckets.get(destinationBucketId)?.info.replicationConfiguration.asReplicationDestination?.sourceToDestinationKeyMapping[sourceApplicationKeyId];
|
|
1054
|
+
if (destinationApplicationKeyId === void 0) return this.invalidReplicationDestinationBucket();
|
|
1055
|
+
const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [Capability.WriteFiles], {
|
|
1056
|
+
bucketId: destinationBucketId,
|
|
1057
|
+
role: "replication destination"
|
|
1058
|
+
});
|
|
1059
|
+
if (destinationKeyError !== null) return destinationKeyError;
|
|
466
1060
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
validateReplicationApplicationKeys(config, bucketId) {
|
|
1064
|
+
if (!this.strictAuth || config === void 0) return null;
|
|
1065
|
+
const replicationConfiguration = requestRecord(config);
|
|
1066
|
+
const source = requestRecord(replicationConfiguration?.["asReplicationSource"]);
|
|
1067
|
+
if (source !== null) {
|
|
1068
|
+
const sourceApplicationKeyId = source["sourceApplicationKeyId"];
|
|
1069
|
+
if (typeof sourceApplicationKeyId === "string") {
|
|
1070
|
+
const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [Capability.ReadFiles, Capability.ListFiles], {
|
|
1071
|
+
bucketId,
|
|
1072
|
+
role: "replication source"
|
|
1073
|
+
});
|
|
1074
|
+
if (sourceKeyError !== null) return sourceKeyError;
|
|
1075
|
+
}
|
|
1076
|
+
const rules = source["replicationRules"];
|
|
1077
|
+
if (typeof sourceApplicationKeyId === "string" && Array.isArray(rules)) {
|
|
1078
|
+
const destinationError = this.validateReplicationDestinationBuckets(sourceApplicationKeyId, rules);
|
|
1079
|
+
if (destinationError !== null) return destinationError;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const mapping = requestRecord(requestRecord(replicationConfiguration?.["asReplicationDestination"])?.["sourceToDestinationKeyMapping"]);
|
|
1083
|
+
if (mapping !== null) for (const [sourceApplicationKeyId, destinationApplicationKeyId] of Object.entries(mapping)) {
|
|
1084
|
+
const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [Capability.ReadFiles, Capability.ListFiles], { role: "replication source" });
|
|
1085
|
+
if (sourceKeyError !== null) return sourceKeyError;
|
|
1086
|
+
if (typeof destinationApplicationKeyId === "string") {
|
|
1087
|
+
const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [Capability.WriteFiles], {
|
|
1088
|
+
bucketId,
|
|
1089
|
+
role: "replication destination"
|
|
1090
|
+
});
|
|
1091
|
+
if (destinationKeyError !== null) return destinationKeyError;
|
|
1092
|
+
}
|
|
470
1093
|
}
|
|
471
1094
|
return null;
|
|
472
1095
|
}
|
|
@@ -474,10 +1097,7 @@ var B2Simulator = class {
|
|
|
474
1097
|
const directBucketId = requestStringField(body, "bucketId");
|
|
475
1098
|
const directFileName = requestStringField(body, "fileName");
|
|
476
1099
|
switch (endpoint) {
|
|
477
|
-
case "b2_create_bucket": return
|
|
478
|
-
bucketIds: [],
|
|
479
|
-
requiresBucketScope: true
|
|
480
|
-
};
|
|
1100
|
+
case "b2_create_bucket": return this.createBucketScope(body);
|
|
481
1101
|
case "b2_list_buckets": return this.listBucketsScope(body, directBucketId);
|
|
482
1102
|
case "b2_list_file_names":
|
|
483
1103
|
case "b2_list_file_versions": return {
|
|
@@ -513,9 +1133,23 @@ var B2Simulator = class {
|
|
|
513
1133
|
};
|
|
514
1134
|
case "b2_get_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, false);
|
|
515
1135
|
case "b2_set_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, true);
|
|
1136
|
+
case "b2_update_bucket": return this.updateBucketScope(body, directBucketId);
|
|
516
1137
|
default: return this.defaultRequestScope(directBucketId, directFileName);
|
|
517
1138
|
}
|
|
518
1139
|
}
|
|
1140
|
+
createBucketScope(body) {
|
|
1141
|
+
return {
|
|
1142
|
+
bucketIds: replicationDestinationBucketIds(body),
|
|
1143
|
+
requiresBucketScope: true,
|
|
1144
|
+
requiresAccountLevelBucketAccess: true
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
updateBucketScope(body, directBucketId) {
|
|
1148
|
+
return {
|
|
1149
|
+
bucketIds: [...directBucketId === void 0 ? [] : [directBucketId], ...replicationDestinationBucketIds(body)],
|
|
1150
|
+
requiresBucketScope: true
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
519
1153
|
listBucketsScope(body, directBucketId) {
|
|
520
1154
|
if (directBucketId !== void 0) return {
|
|
521
1155
|
bucketIds: [directBucketId],
|
|
@@ -613,7 +1247,10 @@ var B2Simulator = class {
|
|
|
613
1247
|
const idParam = options.kind === "file" ? "bucketId" : "fileId";
|
|
614
1248
|
const scopedId = options.kind === "file" ? options.bucketId : options.fileId;
|
|
615
1249
|
const uploadId = this.genId(options.kind === "file" ? "upload_file" : "upload_part");
|
|
616
|
-
const uploadUrl = new URL(
|
|
1250
|
+
const uploadUrl = new URL(b2Url("http://localhost:0", {
|
|
1251
|
+
version: "v3",
|
|
1252
|
+
endpoint
|
|
1253
|
+
}));
|
|
617
1254
|
uploadUrl.searchParams.set(idParam, scopedId);
|
|
618
1255
|
uploadUrl.searchParams.set("uploadId", uploadId);
|
|
619
1256
|
const sourceToken = options.sourceAuthToken === void 0 ? void 0 : this.issuedTokens.get(options.sourceAuthToken);
|
|
@@ -686,40 +1323,61 @@ var B2Simulator = class {
|
|
|
686
1323
|
}));
|
|
687
1324
|
}
|
|
688
1325
|
/**
|
|
689
|
-
*
|
|
1326
|
+
* Resolve the application key matching the `Authorization` header on
|
|
690
1327
|
* an `authorize_account` request. The header is in the form
|
|
691
1328
|
* `Basic base64(applicationKeyId:applicationKey)`.
|
|
692
1329
|
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* `
|
|
1330
|
+
* The simulator has two implicit full-access storage credentials:
|
|
1331
|
+
* the documented `test-key-id:test-key` pair used by SDK tests and
|
|
1332
|
+
* `master-key-id:master-key`. In default permissive mode, any other
|
|
1333
|
+
* well-formed Basic credential whose applicationKeyId is not a stored
|
|
1334
|
+
* key also receives that implicit master grant for backward
|
|
1335
|
+
* compatibility. A credential whose applicationKeyId matches a stored
|
|
1336
|
+
* key must use the stored secret and must not be expired.
|
|
696
1337
|
*
|
|
697
1338
|
* @param authzHeader - Raw HTTP `Authorization` header value.
|
|
698
1339
|
*
|
|
699
|
-
* @returns The
|
|
1340
|
+
* @returns The authorizing key's grant scope or the rejection reason.
|
|
700
1341
|
*/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1342
|
+
findAuthorizationGrant(authzHeader) {
|
|
1343
|
+
const credentials = parseBasicAuthorizationHeader(authzHeader);
|
|
1344
|
+
if (credentials === null) return { kind: "invalid" };
|
|
1345
|
+
const stored = this.keys.get(credentials.applicationKeyId);
|
|
1346
|
+
if (stored !== void 0) {
|
|
1347
|
+
if (!timingSafeStringEqual(stored.applicationKey, credentials.applicationKey)) return { kind: "invalid" };
|
|
1348
|
+
if (this.isStoredKeyExpired(stored)) return { kind: "expired" };
|
|
1349
|
+
return {
|
|
1350
|
+
kind: "ok",
|
|
1351
|
+
grant: {
|
|
1352
|
+
capabilities: stored.capabilities,
|
|
1353
|
+
bucketIds: stored.bucketIds,
|
|
1354
|
+
namePrefix: stored.namePrefix,
|
|
1355
|
+
applicationKeyId: credentials.applicationKeyId,
|
|
1356
|
+
accountId: stored.accountId,
|
|
1357
|
+
expirationTimestamp: stored.expirationTimestamp
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (isSimulatorApplicationKeyId(credentials.applicationKeyId)) return { kind: "invalid" };
|
|
1362
|
+
if (isImplicitStorageMasterCredential(credentials) || !this.strictAuth) return {
|
|
1363
|
+
kind: "ok",
|
|
1364
|
+
grant: {
|
|
1365
|
+
capabilities: SIMULATOR_MASTER_CAPABILITIES,
|
|
1366
|
+
bucketIds: null,
|
|
1367
|
+
namePrefix: null,
|
|
1368
|
+
applicationKeyId: null,
|
|
1369
|
+
accountId: this.accountId,
|
|
1370
|
+
expirationTimestamp: null
|
|
708
1371
|
}
|
|
709
|
-
})();
|
|
710
|
-
if (decoded === null) return null;
|
|
711
|
-
const idx = decoded.indexOf(":");
|
|
712
|
-
if (idx === -1) return null;
|
|
713
|
-
const applicationKeyId = decoded.slice(0, idx);
|
|
714
|
-
const applicationKey = decoded.slice(idx + 1);
|
|
715
|
-
const stored = this.keys.get(applicationKeyId);
|
|
716
|
-
if (!stored || stored.applicationKey !== applicationKey) return null;
|
|
717
|
-
return {
|
|
718
|
-
capabilities: stored.capabilities,
|
|
719
|
-
bucketIds: stored.bucketIds,
|
|
720
|
-
namePrefix: stored.namePrefix,
|
|
721
|
-
applicationKeyId
|
|
722
1372
|
};
|
|
1373
|
+
return { kind: "invalid" };
|
|
1374
|
+
}
|
|
1375
|
+
canAuthorizePartner(authzHeader) {
|
|
1376
|
+
const credentials = parseBasicAuthorizationHeader(authzHeader);
|
|
1377
|
+
if (credentials === null) return false;
|
|
1378
|
+
if (isImplicitPartnerMasterCredential(credentials)) return true;
|
|
1379
|
+
const stored = this.keys.get(credentials.applicationKeyId);
|
|
1380
|
+
return stored !== void 0 && stored.accountId === this.accountId && !this.isStoredKeyExpired(stored) && timingSafeStringEqual(stored.applicationKey, credentials.applicationKey);
|
|
723
1381
|
}
|
|
724
1382
|
/**
|
|
725
1383
|
* Creates an {@link HttpTransport} that routes requests to this simulator.
|
|
@@ -811,7 +1469,7 @@ var B2Simulator = class {
|
|
|
811
1469
|
}
|
|
812
1470
|
/**
|
|
813
1471
|
* Dispatches a JSON API request to the appropriate handler.
|
|
814
|
-
* @param
|
|
1472
|
+
* @param method - The HTTP method.
|
|
815
1473
|
* @param origin - The request URL origin used for simulator-issued endpoints.
|
|
816
1474
|
* @param path - The request URL path containing the B2 endpoint name.
|
|
817
1475
|
* @param headers - The HTTP request headers; consulted by the
|
|
@@ -820,15 +1478,24 @@ var B2Simulator = class {
|
|
|
820
1478
|
*
|
|
821
1479
|
* @returns An object with HTTP status and JSON response body.
|
|
822
1480
|
*/
|
|
823
|
-
async handleRequest(
|
|
824
|
-
const endpoint = path
|
|
825
|
-
|
|
826
|
-
if (this.strictAuth) {
|
|
1481
|
+
async handleRequest(method, origin, path, headers, body) {
|
|
1482
|
+
const { endpoint, version } = apiPathParts(path);
|
|
1483
|
+
if (!jsonEndpointAllowsMethod(method, endpoint)) return this.error(405, "method_not_allowed", `${endpoint} does not support ${method.toUpperCase()}`);
|
|
1484
|
+
if (this.strictAuth && !this.partner.isEndpoint(endpoint)) {
|
|
827
1485
|
const authError = this.authorizeRequest(headers["authorization"], endpoint, this.requestScope(endpoint, body));
|
|
828
1486
|
if (authError !== null) return authError;
|
|
829
1487
|
}
|
|
830
1488
|
switch (endpoint) {
|
|
831
|
-
case "b2_authorize_account":
|
|
1489
|
+
case "b2_authorize_account":
|
|
1490
|
+
if (version === "v3" && this.partner.isAuthorizeEnabled()) return this.partner.authorize(headers["authorization"], origin);
|
|
1491
|
+
return this.authorize(headers["authorization"], origin);
|
|
1492
|
+
case "b2_create_group_member": return this.partner.createGroupMember(body, headers["authorization"]);
|
|
1493
|
+
case "b2_eject_group_member": return this.partner.ejectGroupMember(body, headers["authorization"]);
|
|
1494
|
+
case "b2_list_groups": return this.partner.listGroups(body, headers["authorization"]);
|
|
1495
|
+
case "b2_list_group_members": return this.partner.listGroupMembers(body, headers["authorization"]);
|
|
1496
|
+
case "b2_reserve_trial_create_account": return this.partner.reserveTrialCreateAccount(body, headers["authorization"]);
|
|
1497
|
+
case "bz_list_computers": return this.partner.listComputers(body, headers["authorization"]);
|
|
1498
|
+
case "bz_delete_computer": return this.partner.deleteComputer(body, headers["authorization"]);
|
|
832
1499
|
case "b2_create_bucket": return this.createBucket(body);
|
|
833
1500
|
case "b2_list_buckets": return this.listBuckets(body);
|
|
834
1501
|
case "b2_delete_bucket": return this.deleteBucket(body);
|
|
@@ -838,9 +1505,9 @@ var B2Simulator = class {
|
|
|
838
1505
|
case "b2_list_file_versions": return this.listFileVersions(body);
|
|
839
1506
|
case "b2_get_file_info": return this.getFileInfo(body);
|
|
840
1507
|
case "b2_hide_file": return this.hideFile(body);
|
|
841
|
-
case "b2_delete_file_version": return this.deleteFileVersion(body);
|
|
1508
|
+
case "b2_delete_file_version": return this.deleteFileVersion(body, headers["authorization"]);
|
|
842
1509
|
case "b2_copy_file": return await this.copyFile(body);
|
|
843
|
-
case "b2_start_large_file": return this.startLargeFile(body);
|
|
1510
|
+
case "b2_start_large_file": return await this.startLargeFile(body);
|
|
844
1511
|
case "b2_get_upload_part_url": return await this.getUploadPartUrl(body, headers["authorization"]);
|
|
845
1512
|
case "b2_finish_large_file": return this.finishLargeFile(body);
|
|
846
1513
|
case "b2_cancel_large_file": return this.cancelLargeFile(body);
|
|
@@ -848,10 +1515,10 @@ var B2Simulator = class {
|
|
|
848
1515
|
case "b2_list_parts": return this.listParts(body);
|
|
849
1516
|
case "b2_copy_part": return await this.copyPart(body);
|
|
850
1517
|
case "b2_get_download_authorization": return this.getDownloadAuthorization(body);
|
|
851
|
-
case "b2_create_key": return this.createKey(body,
|
|
1518
|
+
case "b2_create_key": return this.createKey(body, version, headers["authorization"]);
|
|
852
1519
|
case "b2_list_keys": return this.listKeys(body);
|
|
853
1520
|
case "b2_delete_key": return this.deleteKey(body);
|
|
854
|
-
case "b2_update_file_retention": return this.updateFileRetention(body);
|
|
1521
|
+
case "b2_update_file_retention": return this.updateFileRetention(body, headers["authorization"]);
|
|
855
1522
|
case "b2_update_file_legal_hold": return this.updateFileLegalHold(body);
|
|
856
1523
|
case "b2_get_bucket_notification_rules": return this.getBucketNotificationRules(body);
|
|
857
1524
|
case "b2_set_bucket_notification_rules": return this.setBucketNotificationRules(body);
|
|
@@ -881,9 +1548,25 @@ var B2Simulator = class {
|
|
|
881
1548
|
const fileName = kind === "part" || headers["x-bz-file-name"] === void 0 ? void 0 : decodeHeaderValue(headers["x-bz-file-name"]);
|
|
882
1549
|
const authError = await this.validateUploadAuthorization(kind, url, headers["authorization"], fileName);
|
|
883
1550
|
if (authError !== null) return authError;
|
|
1551
|
+
const contentLengthError = this.validateContentLength(headers, data.byteLength);
|
|
1552
|
+
if (contentLengthError !== null) return contentLengthError;
|
|
884
1553
|
if (kind === "part") return await this.handleUploadPart(url, headers, data);
|
|
885
1554
|
return await this.handleUploadFile(url, headers, data);
|
|
886
1555
|
}
|
|
1556
|
+
validateContentLength(headers, actualLength) {
|
|
1557
|
+
const parsed = parseContentLengthHeader(headers);
|
|
1558
|
+
if (parsed.kind === "absent") return null;
|
|
1559
|
+
if (parsed.kind === "error") return this.error(400, "bad_request", parsed.message);
|
|
1560
|
+
if (parsed.expectedLength !== actualLength) return this.error(400, "bad_request", contentLengthMismatchMessage(parsed.expectedLength, actualLength));
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
parseCustomUploadTimestamp(value) {
|
|
1564
|
+
if (value === void 0 || value === null) return { timestamp: null };
|
|
1565
|
+
if (!this.customUploadTimestampsEnabled) return this.error(400, "custom_timestamp_not_allowed", "Custom upload timestamps are not enabled for this account.");
|
|
1566
|
+
const timestamp = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
1567
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > this.now()) return this.error(400, "custom_timestamp_invalid", "The request has an invalid custom upload timestamp.");
|
|
1568
|
+
return { timestamp };
|
|
1569
|
+
}
|
|
887
1570
|
/**
|
|
888
1571
|
* Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
|
|
889
1572
|
* Returns the file data along with B2 response headers.
|
|
@@ -895,33 +1578,32 @@ var B2Simulator = class {
|
|
|
895
1578
|
*/
|
|
896
1579
|
handleDownload(path, headers, method = "GET") {
|
|
897
1580
|
if (path.includes("b2_download_file_by_id")) {
|
|
1581
|
+
const url = new URL(`http://localhost${path}`);
|
|
898
1582
|
if (this.strictAuth) {
|
|
899
|
-
const
|
|
900
|
-
const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
|
|
1583
|
+
const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: false }), "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
|
|
901
1584
|
bucketIds: [],
|
|
902
1585
|
requiresBucketScope: true
|
|
903
|
-
});
|
|
1586
|
+
}, url);
|
|
904
1587
|
if (authError !== null) return this.errorAsDownload(authError);
|
|
905
1588
|
}
|
|
906
|
-
const url = new URL(`http://localhost${path}`);
|
|
907
1589
|
const fileId = url.searchParams.get("fileId") ?? "";
|
|
908
|
-
return this.finalizeDownload(this.downloadById(fileId, headers
|
|
1590
|
+
return this.finalizeDownload(this.downloadById(fileId, headers, requestHeaderValue(headers, "range")), url, method);
|
|
909
1591
|
}
|
|
910
1592
|
const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
|
|
911
1593
|
if (fileMatch) {
|
|
912
1594
|
const bucketName = decodeURIComponent(fileMatch[1] ?? "");
|
|
913
1595
|
const fileName = decodeURIComponent(fileMatch[2] ?? "");
|
|
1596
|
+
const url = new URL(`http://localhost${path}`);
|
|
914
1597
|
if (this.strictAuth) {
|
|
915
1598
|
const bucket = [...this.buckets.values()].find((b) => b.info.bucketName === bucketName);
|
|
916
|
-
const authError = this.
|
|
1599
|
+
const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: true }), "b2_download_file_by_name", {
|
|
917
1600
|
bucketIds: bucket === void 0 ? [] : [bucket.info.bucketId],
|
|
918
1601
|
fileNames: [fileName],
|
|
919
1602
|
requiresBucketScope: true
|
|
920
|
-
});
|
|
1603
|
+
}, url);
|
|
921
1604
|
if (authError !== null) return this.errorAsDownload(authError);
|
|
922
1605
|
}
|
|
923
|
-
|
|
924
|
-
return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers["range"]), url, method);
|
|
1606
|
+
return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers, requestHeaderValue(headers, "range")), url, method);
|
|
925
1607
|
}
|
|
926
1608
|
return {
|
|
927
1609
|
status: 404,
|
|
@@ -958,16 +1640,8 @@ var B2Simulator = class {
|
|
|
958
1640
|
* @returns The response with overrides applied.
|
|
959
1641
|
*/
|
|
960
1642
|
finalizeDownload(response, url, method) {
|
|
961
|
-
const overrideMap = {
|
|
962
|
-
b2ContentDisposition: "Content-Disposition",
|
|
963
|
-
b2ContentLanguage: "Content-Language",
|
|
964
|
-
b2ContentEncoding: "Content-Encoding",
|
|
965
|
-
b2ContentType: "Content-Type",
|
|
966
|
-
b2CacheControl: "Cache-Control",
|
|
967
|
-
b2Expires: "Expires"
|
|
968
|
-
};
|
|
969
1643
|
const newHeaders = { ...response.headers };
|
|
970
|
-
for (const [param, header] of Object.entries(
|
|
1644
|
+
for (const [param, header] of Object.entries(DOWNLOAD_RESPONSE_OVERRIDE_HEADERS)) {
|
|
971
1645
|
const value = url.searchParams.get(param);
|
|
972
1646
|
if (value !== null) newHeaders[header] = value;
|
|
973
1647
|
}
|
|
@@ -993,6 +1667,10 @@ var B2Simulator = class {
|
|
|
993
1667
|
const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
|
|
994
1668
|
if ("status" in resolved) return resolved;
|
|
995
1669
|
const { sha1: contentSha1, data: storedData } = resolved;
|
|
1670
|
+
const serverSideEncryption = await uploadServerSideEncryption(headers, bucket.info.defaultServerSideEncryption);
|
|
1671
|
+
if ("status" in serverSideEncryption) return serverSideEncryption;
|
|
1672
|
+
const customUploadTimestamp = this.parseCustomUploadTimestamp(requestHeaderValue(headers, "x-bz-custom-upload-timestamp"));
|
|
1673
|
+
if ("status" in customUploadTimestamp) return customUploadTimestamp;
|
|
996
1674
|
const fileVersion = this.makeFileVersion({
|
|
997
1675
|
bucketId,
|
|
998
1676
|
fileName,
|
|
@@ -1001,11 +1679,14 @@ var B2Simulator = class {
|
|
|
1001
1679
|
contentSha1,
|
|
1002
1680
|
fileInfo,
|
|
1003
1681
|
action: FileAction.Upload,
|
|
1004
|
-
serverSideEncryption
|
|
1682
|
+
serverSideEncryption,
|
|
1683
|
+
...this.newReplicationStatusField(bucketId, fileName),
|
|
1684
|
+
...customUploadTimestamp.timestamp !== null ? { uploadTimestamp: customUploadTimestamp.timestamp } : {}
|
|
1005
1685
|
});
|
|
1006
1686
|
const stored = {
|
|
1007
1687
|
fileVersion,
|
|
1008
|
-
data: storedData
|
|
1688
|
+
data: storedData,
|
|
1689
|
+
serverSideEncryption
|
|
1009
1690
|
};
|
|
1010
1691
|
const existing = bucket.files.get(fileName);
|
|
1011
1692
|
if (existing) existing.push(stored);
|
|
@@ -1055,18 +1736,39 @@ var B2Simulator = class {
|
|
|
1055
1736
|
data
|
|
1056
1737
|
};
|
|
1057
1738
|
}
|
|
1739
|
+
async validateUploadPartEncryption(large, headers) {
|
|
1740
|
+
if (large.serverSideEncryption.mode !== EncryptionMode.SseC) {
|
|
1741
|
+
if (!hasCustomerEncryptionHeaders(headers)) return null;
|
|
1742
|
+
return this.error(400, "bad_request", "SSE-C upload part headers require an SSE-C large file");
|
|
1743
|
+
}
|
|
1744
|
+
const supplied = await storedServerSideEncryption({
|
|
1745
|
+
mode: EncryptionMode.SseC,
|
|
1746
|
+
algorithm: headers["x-bz-server-side-encryption-customer-algorithm"],
|
|
1747
|
+
customerKey: headers["x-bz-server-side-encryption-customer-key"],
|
|
1748
|
+
customerKeyMd5: headers["x-bz-server-side-encryption-customer-key-md5"]
|
|
1749
|
+
});
|
|
1750
|
+
if ("status" in supplied) return supplied;
|
|
1751
|
+
if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "SSE-C upload parts require matching customer encryption headers");
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1058
1754
|
async handleUploadPart(url, headers, data) {
|
|
1059
1755
|
const fileId = new URL(url).searchParams.get("fileId");
|
|
1060
1756
|
if (!fileId) return this.error(400, "bad_request", "Missing fileId");
|
|
1061
1757
|
const large = this.largeFiles.get(fileId);
|
|
1062
1758
|
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
1063
|
-
const partNumber =
|
|
1759
|
+
const partNumber = this.parsePartNumberHeader(requestHeaderValue(headers, "x-bz-part-number"));
|
|
1760
|
+
if (typeof partNumber !== "number") return partNumber;
|
|
1761
|
+
const encryptionError = await this.validateUploadPartEncryption(large, headers);
|
|
1762
|
+
if (encryptionError !== null) return encryptionError;
|
|
1064
1763
|
const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
|
|
1065
1764
|
if ("status" in resolved) return resolved;
|
|
1066
1765
|
const { sha1, data: partData } = resolved;
|
|
1766
|
+
const uploadTimestamp = this.monotonicTimestamp();
|
|
1067
1767
|
large.parts.set(partNumber, {
|
|
1068
1768
|
data: partData,
|
|
1069
|
-
sha1
|
|
1769
|
+
sha1,
|
|
1770
|
+
contentMd5: null,
|
|
1771
|
+
uploadTimestamp
|
|
1070
1772
|
});
|
|
1071
1773
|
return {
|
|
1072
1774
|
status: 200,
|
|
@@ -1075,21 +1777,32 @@ var B2Simulator = class {
|
|
|
1075
1777
|
partNumber,
|
|
1076
1778
|
contentLength: partData.byteLength,
|
|
1077
1779
|
contentSha1: sha1,
|
|
1780
|
+
contentMd5: null,
|
|
1078
1781
|
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
1079
|
-
uploadTimestamp
|
|
1782
|
+
uploadTimestamp
|
|
1080
1783
|
}
|
|
1081
1784
|
};
|
|
1082
1785
|
}
|
|
1083
|
-
|
|
1786
|
+
parsePartNumberHeader(header) {
|
|
1787
|
+
if (header === void 0) return this.error(400, "bad_request", "X-Bz-Part-Number header is required");
|
|
1788
|
+
if (!/^\d+$/.test(header)) return this.error(400, "bad_request", `X-Bz-Part-Number must be an integer between ${B2_MIN_PART_NUMBER} and ${B2_MAX_PART_NUMBER}; received ${header}`);
|
|
1789
|
+
const partNumber = Number(header);
|
|
1790
|
+
return this.validatePartNumber(partNumber) ?? partNumber;
|
|
1791
|
+
}
|
|
1792
|
+
validatePartNumber(partNumber) {
|
|
1793
|
+
if (Number.isInteger(partNumber) && partNumber >= B2_MIN_PART_NUMBER && partNumber <= B2_MAX_PART_NUMBER) return null;
|
|
1794
|
+
return this.error(400, "bad_request", `partNumber must be an integer between ${B2_MIN_PART_NUMBER} and ${B2_MAX_PART_NUMBER}; received ${String(partNumber)}`);
|
|
1795
|
+
}
|
|
1796
|
+
downloadById(fileId, headers, range) {
|
|
1084
1797
|
const found = this.findFile(fileId);
|
|
1085
1798
|
if (found === null) return {
|
|
1086
1799
|
status: 404,
|
|
1087
1800
|
headers: {},
|
|
1088
1801
|
data: null
|
|
1089
1802
|
};
|
|
1090
|
-
return this.serveFile(found.stored, range);
|
|
1803
|
+
return this.serveFile(found.stored, headers, range);
|
|
1091
1804
|
}
|
|
1092
|
-
downloadByName(bucketName, fileName, range) {
|
|
1805
|
+
downloadByName(bucketName, fileName, headers, range) {
|
|
1093
1806
|
for (const bucket of this.buckets.values()) {
|
|
1094
1807
|
if (bucket.info.bucketName !== bucketName) continue;
|
|
1095
1808
|
const versions = bucket.files.get(fileName);
|
|
@@ -1100,7 +1813,7 @@ var B2Simulator = class {
|
|
|
1100
1813
|
headers: {},
|
|
1101
1814
|
data: null
|
|
1102
1815
|
};
|
|
1103
|
-
return this.serveFile(latest, range);
|
|
1816
|
+
return this.serveFile(latest, headers, range);
|
|
1104
1817
|
}
|
|
1105
1818
|
return {
|
|
1106
1819
|
status: 404,
|
|
@@ -1108,7 +1821,41 @@ var B2Simulator = class {
|
|
|
1108
1821
|
data: null
|
|
1109
1822
|
};
|
|
1110
1823
|
}
|
|
1111
|
-
|
|
1824
|
+
validateDownloadEncryption(stored, headers) {
|
|
1825
|
+
const encryption = stored.serverSideEncryption;
|
|
1826
|
+
if (encryption.mode !== EncryptionMode.SseC) return null;
|
|
1827
|
+
const algorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
|
|
1828
|
+
const customerKey = headers["x-bz-server-side-encryption-customer-key"];
|
|
1829
|
+
const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
|
|
1830
|
+
const customerKeyBytes = customerKey === void 0 ? null : base64ToBytes(customerKey, 32);
|
|
1831
|
+
const normalizedCustomerKeyMd5 = customerKeyMd5 === void 0 ? null : normalizeSizedBase64(customerKeyMd5, 16);
|
|
1832
|
+
if (algorithm !== encryption.algorithm || customerKeyBytes === null || normalizedCustomerKeyMd5 === null || md5Base64Sync(customerKeyBytes) !== normalizedCustomerKeyMd5) return this.errorAsDownload(this.error(400, "bad_request", "SSE-C downloads require matching customer encryption headers"));
|
|
1833
|
+
if (normalizedCustomerKeyMd5 !== encryption.customerKeyMd5 || customerKeyDigest(customerKeyBytes) !== encryption.customerKeyDigest) return this.errorAsDownload(this.error(403, "access_denied", "SSE-C customer encryption key does not match the stored file"));
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
async validateSourceServerSideEncryption(stored, encryption) {
|
|
1837
|
+
const expected = stored.serverSideEncryption;
|
|
1838
|
+
if (expected.mode !== EncryptionMode.SseC) return null;
|
|
1839
|
+
if (encryption?.mode !== EncryptionMode.SseC) return this.error(400, "bad_request", "SSE-C source copies require customer encryption");
|
|
1840
|
+
const supplied = await storedServerSideEncryption(encryption);
|
|
1841
|
+
if ("status" in supplied) return supplied;
|
|
1842
|
+
if (!sameStoredServerSideEncryption(expected, supplied)) return this.error(400, "bad_request", "SSE-C source customer encryption does not match");
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
async resolveCopyDestinationEncryption(destinationBucket, requested) {
|
|
1846
|
+
if (requested !== void 0) return await storedServerSideEncryption(requested);
|
|
1847
|
+
return await storedServerSideEncryption(destinationBucket.info.defaultServerSideEncryption);
|
|
1848
|
+
}
|
|
1849
|
+
async validateCopyPartDestinationEncryption(large, encryption) {
|
|
1850
|
+
if (encryption === void 0) return large.serverSideEncryption.mode === EncryptionMode.SseC ? this.error(400, "bad_request", "SSE-C destination copies require customer encryption") : null;
|
|
1851
|
+
const supplied = await storedServerSideEncryption(encryption);
|
|
1852
|
+
if ("status" in supplied) return supplied;
|
|
1853
|
+
if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "Destination server-side encryption does not match");
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
serveFile(stored, requestHeaders, range) {
|
|
1857
|
+
const encryptionError = this.validateDownloadEncryption(stored, requestHeaders);
|
|
1858
|
+
if (encryptionError !== null) return encryptionError;
|
|
1112
1859
|
const fullData = stored.data;
|
|
1113
1860
|
let data = fullData;
|
|
1114
1861
|
let status = 200;
|
|
@@ -1139,6 +1886,10 @@ var B2Simulator = class {
|
|
|
1139
1886
|
};
|
|
1140
1887
|
for (const [key, value] of Object.entries(fv.fileInfo)) headers[`X-Bz-Info-${encodeFileName(key)}`] = encodeFileName(value);
|
|
1141
1888
|
if (!("src_last_modified_millis" in fv.fileInfo)) headers["X-Bz-Info-src_last_modified_millis"] = String(fv.uploadTimestamp);
|
|
1889
|
+
const unauthorizedToRead = [];
|
|
1890
|
+
this.addDownloadEncryptionHeaders(headers, stored.serverSideEncryption, requestHeaders, unauthorizedToRead);
|
|
1891
|
+
this.addDownloadObjectLockHeaders(headers, fv, requestHeaders, unauthorizedToRead);
|
|
1892
|
+
if (unauthorizedToRead.length > 0) headers[DownloadHeaderName.ClientUnauthorizedToRead] = unauthorizedToRead.join(",");
|
|
1142
1893
|
if (contentRange !== null) headers["Content-Range"] = contentRange;
|
|
1143
1894
|
return {
|
|
1144
1895
|
status,
|
|
@@ -1146,39 +1897,58 @@ var B2Simulator = class {
|
|
|
1146
1897
|
data
|
|
1147
1898
|
};
|
|
1148
1899
|
}
|
|
1900
|
+
addDownloadEncryptionHeaders(headers, encryption, requestHeaders, unauthorizedToRead) {
|
|
1901
|
+
if (encryption.mode === EncryptionMode.None) return;
|
|
1902
|
+
if (!this.requestHasCapability(requestHeaderValue(requestHeaders, "authorization"), Capability.ReadBucketEncryption)) {
|
|
1903
|
+
if (encryption.mode === EncryptionMode.SseB2) unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.ServerSideEncryption);
|
|
1904
|
+
else unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerAlgorithm, DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerKeyMd5);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (encryption.mode === EncryptionMode.SseB2) {
|
|
1908
|
+
headers[DownloadHeaderName.ServerSideEncryption] = encryption.algorithm;
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
headers[DownloadHeaderName.ServerSideEncryptionCustomerAlgorithm] = encryption.algorithm;
|
|
1912
|
+
headers[DownloadHeaderName.ServerSideEncryptionCustomerKeyMd5] = encryption.customerKeyMd5;
|
|
1913
|
+
}
|
|
1914
|
+
addDownloadObjectLockHeaders(headers, fileVersion, requestHeaders, unauthorizedToRead) {
|
|
1915
|
+
const authToken = requestHeaderValue(requestHeaders, "authorization");
|
|
1916
|
+
const retention = fileVersion.fileRetention.value;
|
|
1917
|
+
if (retention !== null && retention.mode !== null && retention.retainUntilTimestamp !== null) {
|
|
1918
|
+
if (this.requestHasCapability(authToken, Capability.ReadFileRetentions)) {
|
|
1919
|
+
headers[DownloadHeaderName.FileRetentionMode] = retention.mode;
|
|
1920
|
+
headers[DownloadHeaderName.FileRetentionRetainUntilTimestamp] = String(retention.retainUntilTimestamp);
|
|
1921
|
+
} else unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.FileRetentionMode, DownloadClientUnauthorizedToReadMarker.FileRetentionRetainUntilTimestamp);
|
|
1922
|
+
}
|
|
1923
|
+
const legalHold = fileVersion.legalHold.value;
|
|
1924
|
+
if (legalHold === null) return;
|
|
1925
|
+
if (this.requestHasCapability(authToken, Capability.ReadFileLegalHolds)) {
|
|
1926
|
+
headers[DownloadHeaderName.FileLegalHold] = legalHold;
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.FileLegalHold);
|
|
1930
|
+
}
|
|
1149
1931
|
authorize(authzHeader, origin = "http://localhost:0") {
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
Capability.ReadFiles,
|
|
1157
|
-
Capability.WriteFiles,
|
|
1158
|
-
Capability.DeleteFiles,
|
|
1159
|
-
Capability.ListKeys,
|
|
1160
|
-
Capability.WriteKeys,
|
|
1161
|
-
Capability.DeleteKeys,
|
|
1162
|
-
Capability.ShareFiles,
|
|
1163
|
-
Capability.ReadBucketNotifications,
|
|
1164
|
-
Capability.WriteBucketNotifications
|
|
1165
|
-
];
|
|
1166
|
-
const keyForAuth = this.findKeyForAuthHeader(authzHeader);
|
|
1167
|
-
const allowedBuckets = this.allowedBuckets(keyForAuth?.bucketIds);
|
|
1168
|
-
const legacyBucketId = singleBucketId(keyForAuth?.bucketIds);
|
|
1932
|
+
const resolution = this.findAuthorizationGrant(authzHeader);
|
|
1933
|
+
if (resolution.kind === "invalid") return this.error(401, "unauthorized", "application key is not valid");
|
|
1934
|
+
if (resolution.kind === "expired") return this.error(401, "unauthorized", "application key is expired");
|
|
1935
|
+
const { grant } = resolution;
|
|
1936
|
+
const allowedBuckets = this.allowedBuckets(grant.bucketIds);
|
|
1937
|
+
const legacyBucketId = singleBucketId(grant.bucketIds);
|
|
1169
1938
|
const legacyBucketName = legacyBucketId === null ? null : this.buckets.get(legacyBucketId)?.info.bucketName ?? null;
|
|
1170
1939
|
const tokenStr = `sim_auth_token_${this.nextId++}`;
|
|
1171
1940
|
this.issuedTokens.set(tokenStr, {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1941
|
+
accountId: grant.accountId,
|
|
1942
|
+
capabilities: grant.capabilities,
|
|
1943
|
+
bucketIds: grant.bucketIds,
|
|
1944
|
+
namePrefix: grant.namePrefix,
|
|
1175
1945
|
expiresAt: this.now() + this.authTokenTtlMs,
|
|
1176
|
-
applicationKeyId:
|
|
1946
|
+
applicationKeyId: grant.applicationKeyId
|
|
1177
1947
|
});
|
|
1178
1948
|
return {
|
|
1179
1949
|
status: 200,
|
|
1180
1950
|
body: {
|
|
1181
|
-
accountId: accountId(
|
|
1951
|
+
accountId: accountId(grant.accountId),
|
|
1182
1952
|
authorizationToken: tokenStr,
|
|
1183
1953
|
apiInfo: { storageApi: {
|
|
1184
1954
|
absoluteMinimumPartSize: this.minimumPartSize,
|
|
@@ -1187,28 +1957,30 @@ var B2Simulator = class {
|
|
|
1187
1957
|
bucketName: legacyBucketName,
|
|
1188
1958
|
downloadUrl: origin,
|
|
1189
1959
|
infoType: "storageApi",
|
|
1190
|
-
namePrefix:
|
|
1960
|
+
namePrefix: grant.namePrefix,
|
|
1191
1961
|
recommendedPartSize: this.recommendedPartSize,
|
|
1192
1962
|
s3ApiUrl: origin,
|
|
1193
1963
|
allowed: {
|
|
1194
|
-
capabilities:
|
|
1964
|
+
capabilities: cloneCapabilities(grant.capabilities),
|
|
1195
1965
|
buckets: allowedBuckets,
|
|
1196
1966
|
bucketId: legacyBucketId === null ? null : bucketId(legacyBucketId),
|
|
1197
1967
|
bucketName: legacyBucketName,
|
|
1198
|
-
namePrefix:
|
|
1968
|
+
namePrefix: grant.namePrefix
|
|
1199
1969
|
}
|
|
1200
1970
|
} },
|
|
1201
|
-
applicationKeyExpirationTimestamp:
|
|
1971
|
+
applicationKeyExpirationTimestamp: grant.expirationTimestamp
|
|
1202
1972
|
}
|
|
1203
1973
|
};
|
|
1204
1974
|
}
|
|
1205
1975
|
createBucket(req) {
|
|
1206
1976
|
const nameError = validateBucketName(req.bucketName);
|
|
1207
1977
|
if (nameError) return this.error(400, nameError.code, nameError.message);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1978
|
+
const objectLockEnabled = nextObjectLockEnabled(false, req.fileLockEnabled);
|
|
1979
|
+
if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
|
|
1980
|
+
const configError = validateBucketConfigurationFields(req, { objectLockEnabled });
|
|
1981
|
+
if (configError) return this.error(400, configError.code, configError.message);
|
|
1982
|
+
const replicationKeyError = this.validateReplicationApplicationKeys(req.replicationConfiguration, void 0);
|
|
1983
|
+
if (replicationKeyError !== null) return replicationKeyError;
|
|
1212
1984
|
for (const b of this.buckets.values()) if (b.info.bucketName === req.bucketName) return this.error(400, "duplicate_bucket_name", "Bucket name already in use");
|
|
1213
1985
|
const bid = bucketId(this.genId("b2_bucket"));
|
|
1214
1986
|
const defaultRetention = req.defaultRetention ?? {
|
|
@@ -1226,7 +1998,7 @@ var B2Simulator = class {
|
|
|
1226
1998
|
fileLockConfiguration: {
|
|
1227
1999
|
isClientAuthorizedToRead: true,
|
|
1228
2000
|
value: {
|
|
1229
|
-
isFileLockEnabled:
|
|
2001
|
+
isFileLockEnabled: objectLockEnabled,
|
|
1230
2002
|
defaultRetention
|
|
1231
2003
|
}
|
|
1232
2004
|
},
|
|
@@ -1234,10 +2006,10 @@ var B2Simulator = class {
|
|
|
1234
2006
|
options: [],
|
|
1235
2007
|
revision: 1,
|
|
1236
2008
|
defaultRetention,
|
|
1237
|
-
replicationConfiguration: req.replicationConfiguration
|
|
2009
|
+
replicationConfiguration: req.replicationConfiguration === void 0 ? {
|
|
1238
2010
|
asReplicationSource: null,
|
|
1239
2011
|
asReplicationDestination: null
|
|
1240
|
-
}
|
|
2012
|
+
} : normalizeReplicationConfiguration(req.replicationConfiguration)
|
|
1241
2013
|
};
|
|
1242
2014
|
this.buckets.set(bid, {
|
|
1243
2015
|
info,
|
|
@@ -1249,6 +2021,8 @@ var B2Simulator = class {
|
|
|
1249
2021
|
};
|
|
1250
2022
|
}
|
|
1251
2023
|
listBuckets(req) {
|
|
2024
|
+
const bucketTypesError = validateBucketTypes(req.bucketTypes);
|
|
2025
|
+
if (bucketTypesError) return this.error(400, bucketTypesError.code, bucketTypesError.message);
|
|
1252
2026
|
return {
|
|
1253
2027
|
status: 200,
|
|
1254
2028
|
body: { buckets: [...this.buckets.values()].map((b) => b.info).filter((bucket) => req.bucketId === void 0 || bucket.bucketId === req.bucketId).filter((bucket) => req.bucketName === void 0 || bucket.bucketName === req.bucketName).filter((bucket) => req.bucketTypes === void 0 || req.bucketTypes.includes(bucket.bucketType)) }
|
|
@@ -1257,36 +2031,62 @@ var B2Simulator = class {
|
|
|
1257
2031
|
deleteBucket(req) {
|
|
1258
2032
|
const bucket = this.buckets.get(req.bucketId);
|
|
1259
2033
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
2034
|
+
if (this.bucketHasContents(bucket)) return this.error(400, "cannot_delete_non_empty_bucket", "Bucket is not empty and cannot be deleted");
|
|
1260
2035
|
this.buckets.delete(req.bucketId);
|
|
1261
2036
|
return {
|
|
1262
2037
|
status: 200,
|
|
1263
2038
|
body: bucket.info
|
|
1264
2039
|
};
|
|
1265
2040
|
}
|
|
2041
|
+
bucketHasContents(bucket) {
|
|
2042
|
+
for (const versions of bucket.files.values()) if (versions.length > 0) return true;
|
|
2043
|
+
for (const large of this.largeFiles.values()) if (large.bucketId === bucket.info.bucketId) return true;
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
1266
2046
|
updateBucket(req) {
|
|
1267
2047
|
const bucket = this.buckets.get(req["bucketId"]);
|
|
1268
2048
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (
|
|
2049
|
+
const revisionGuard = req["ifRevisionIs"];
|
|
2050
|
+
if (revisionGuard !== void 0) {
|
|
2051
|
+
if (typeof revisionGuard !== "number" || !Number.isInteger(revisionGuard)) return this.error(400, "bad_request", "ifRevisionIs must be an integer");
|
|
2052
|
+
if (revisionGuard !== bucket.info.revision) return this.error(409, "conflict", "ifRevisionIs test failed");
|
|
1272
2053
|
}
|
|
2054
|
+
const objectLockEnabled = nextObjectLockEnabled(bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false, req["fileLockEnabled"]);
|
|
2055
|
+
if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
|
|
2056
|
+
const configError = validateBucketConfigurationFields({
|
|
2057
|
+
bucketInfo: req["bucketInfo"],
|
|
2058
|
+
corsRules: req["corsRules"],
|
|
2059
|
+
defaultRetention: req["defaultRetention"],
|
|
2060
|
+
lifecycleRules: req["lifecycleRules"],
|
|
2061
|
+
replicationConfiguration: req["replicationConfiguration"]
|
|
2062
|
+
}, { objectLockEnabled });
|
|
2063
|
+
if (configError) return this.error(400, configError.code, configError.message);
|
|
2064
|
+
const replicationKeyError = this.validateReplicationApplicationKeys(req["replicationConfiguration"], req["bucketId"]);
|
|
2065
|
+
if (replicationKeyError !== null) return replicationKeyError;
|
|
1273
2066
|
const updated = {
|
|
1274
2067
|
...bucket.info,
|
|
1275
2068
|
...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
|
|
1276
2069
|
...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
|
|
1277
2070
|
...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
|
|
1278
2071
|
...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
|
|
1279
|
-
...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: req["replicationConfiguration"] } : {},
|
|
2072
|
+
...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: normalizeReplicationConfiguration(req["replicationConfiguration"]) } : {},
|
|
1280
2073
|
...req["defaultRetention"] !== void 0 ? {
|
|
1281
2074
|
defaultRetention: req["defaultRetention"],
|
|
1282
2075
|
fileLockConfiguration: {
|
|
1283
2076
|
isClientAuthorizedToRead: true,
|
|
1284
2077
|
value: {
|
|
1285
|
-
isFileLockEnabled:
|
|
2078
|
+
isFileLockEnabled: objectLockEnabled,
|
|
1286
2079
|
defaultRetention: req["defaultRetention"]
|
|
1287
2080
|
}
|
|
1288
2081
|
}
|
|
1289
2082
|
} : {},
|
|
2083
|
+
...req["fileLockEnabled"] !== void 0 && req["defaultRetention"] === void 0 ? { fileLockConfiguration: {
|
|
2084
|
+
isClientAuthorizedToRead: true,
|
|
2085
|
+
value: {
|
|
2086
|
+
isFileLockEnabled: objectLockEnabled,
|
|
2087
|
+
defaultRetention: bucket.info.defaultRetention
|
|
2088
|
+
}
|
|
2089
|
+
} } : {},
|
|
1290
2090
|
...req["defaultServerSideEncryption"] !== void 0 ? { defaultServerSideEncryption: req["defaultServerSideEncryption"] } : {},
|
|
1291
2091
|
revision: bucket.info.revision + 1
|
|
1292
2092
|
};
|
|
@@ -1393,7 +2193,8 @@ var B2Simulator = class {
|
|
|
1393
2193
|
const existing = bucket.files.get(req.fileName);
|
|
1394
2194
|
const stored = {
|
|
1395
2195
|
fileVersion,
|
|
1396
|
-
data: /* @__PURE__ */ new Uint8Array(0)
|
|
2196
|
+
data: /* @__PURE__ */ new Uint8Array(0),
|
|
2197
|
+
serverSideEncryption: { mode: EncryptionMode.None }
|
|
1397
2198
|
};
|
|
1398
2199
|
if (existing) existing.push(stored);
|
|
1399
2200
|
else bucket.files.set(req.fileName, [stored]);
|
|
@@ -1402,16 +2203,19 @@ var B2Simulator = class {
|
|
|
1402
2203
|
body: fileVersion
|
|
1403
2204
|
};
|
|
1404
2205
|
}
|
|
1405
|
-
deleteFileVersion(req) {
|
|
2206
|
+
deleteFileVersion(req, authToken) {
|
|
1406
2207
|
const found = this.findFile(req.fileId);
|
|
1407
2208
|
if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(400, "file_not_present", "File version not found");
|
|
1408
2209
|
const fv = found.stored.fileVersion;
|
|
1409
2210
|
const retention = fv.fileRetention?.value;
|
|
1410
2211
|
const legalHold = fv.legalHold?.value;
|
|
1411
2212
|
const now = Date.now();
|
|
1412
|
-
if (legalHold ===
|
|
1413
|
-
if (retention?.mode ===
|
|
1414
|
-
if (retention?.mode ===
|
|
2213
|
+
if (legalHold === LegalHoldValue.On) return this.error(400, "file_lock_legal_hold_protected", "File is on legal hold and cannot be deleted");
|
|
2214
|
+
if (retention?.mode === RetentionMode.Compliance && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) return this.error(400, "file_lock_compliance_protected", `File is under compliance-mode retention and cannot be deleted until ${new Date(retention.retainUntilTimestamp).toISOString()}`);
|
|
2215
|
+
if (retention?.mode === RetentionMode.Governance && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) {
|
|
2216
|
+
const bypassError = this.requireGovernanceBypass(req, authToken, "File is under governance-mode retention; pass bypassGovernance: true to delete");
|
|
2217
|
+
if (bypassError !== null) return bypassError;
|
|
2218
|
+
}
|
|
1415
2219
|
found.versions.splice(found.index, 1);
|
|
1416
2220
|
if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
|
|
1417
2221
|
return {
|
|
@@ -1431,6 +2235,10 @@ var B2Simulator = class {
|
|
|
1431
2235
|
const destBucketId = req.destinationBucketId ?? found.bucketId;
|
|
1432
2236
|
const destBucket = this.buckets.get(destBucketId);
|
|
1433
2237
|
if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
|
|
2238
|
+
const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
|
|
2239
|
+
if (sourceEncryptionError !== null) return sourceEncryptionError;
|
|
2240
|
+
const destinationEncryption = await this.resolveCopyDestinationEncryption(destBucket, req.destinationServerSideEncryption);
|
|
2241
|
+
if ("status" in destinationEncryption) return destinationEncryption;
|
|
1434
2242
|
let data = sourceStored.data;
|
|
1435
2243
|
let contentSha1 = sourceStored.fileVersion.contentSha1 ?? "none";
|
|
1436
2244
|
if (req.range !== void 0) {
|
|
@@ -1463,11 +2271,14 @@ var B2Simulator = class {
|
|
|
1463
2271
|
contentLength: data.byteLength,
|
|
1464
2272
|
contentSha1,
|
|
1465
2273
|
fileInfo,
|
|
1466
|
-
action: FileAction.Copy
|
|
2274
|
+
action: FileAction.Copy,
|
|
2275
|
+
serverSideEncryption: destinationEncryption,
|
|
2276
|
+
...this.newReplicationStatusField(destBucketId, req.fileName)
|
|
1467
2277
|
});
|
|
1468
2278
|
const copied = {
|
|
1469
2279
|
fileVersion,
|
|
1470
|
-
data: new Uint8Array(data)
|
|
2280
|
+
data: new Uint8Array(data),
|
|
2281
|
+
serverSideEncryption: destinationEncryption
|
|
1471
2282
|
};
|
|
1472
2283
|
const existing = destBucket.files.get(req.fileName);
|
|
1473
2284
|
if (existing) existing.push(copied);
|
|
@@ -1478,7 +2289,7 @@ var B2Simulator = class {
|
|
|
1478
2289
|
body: fileVersion
|
|
1479
2290
|
};
|
|
1480
2291
|
}
|
|
1481
|
-
startLargeFile(req) {
|
|
2292
|
+
async startLargeFile(req) {
|
|
1482
2293
|
const bucket = this.buckets.get(req.bucketId);
|
|
1483
2294
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1484
2295
|
const nameError = validateFileName(req.fileName);
|
|
@@ -1487,8 +2298,13 @@ var B2Simulator = class {
|
|
|
1487
2298
|
const infoError = validateFileInfo(req.fileInfo);
|
|
1488
2299
|
if (infoError) return this.error(400, infoError.code, infoError.message);
|
|
1489
2300
|
}
|
|
2301
|
+
const customUploadTimestamp = this.parseCustomUploadTimestamp(req.customUploadTimestamp);
|
|
2302
|
+
if ("status" in customUploadTimestamp) return customUploadTimestamp;
|
|
1490
2303
|
const fid = this.genId("4_z");
|
|
1491
|
-
const uploadTimestamp = this.monotonicTimestamp();
|
|
2304
|
+
const uploadTimestamp = customUploadTimestamp.timestamp ?? this.monotonicTimestamp();
|
|
2305
|
+
const serverSideEncryption = await storedServerSideEncryption(req.serverSideEncryption ?? bucket.info.defaultServerSideEncryption);
|
|
2306
|
+
if ("status" in serverSideEncryption) return serverSideEncryption;
|
|
2307
|
+
const replicationStatus = this.replicationStatusForNewFile(req.bucketId, req.fileName);
|
|
1492
2308
|
const large = {
|
|
1493
2309
|
fileId: fid,
|
|
1494
2310
|
bucketId: req.bucketId,
|
|
@@ -1497,7 +2313,8 @@ var B2Simulator = class {
|
|
|
1497
2313
|
fileInfo: req.fileInfo ?? {},
|
|
1498
2314
|
fileRetention: req.fileRetention ?? defaultFileRetention(bucket.info.defaultRetention, uploadTimestamp),
|
|
1499
2315
|
legalHold: req.legalHold ?? null,
|
|
1500
|
-
|
|
2316
|
+
...this.replicationStatusMetadataFor(replicationStatus),
|
|
2317
|
+
serverSideEncryption,
|
|
1501
2318
|
uploadTimestamp,
|
|
1502
2319
|
parts: /* @__PURE__ */ new Map()
|
|
1503
2320
|
};
|
|
@@ -1523,6 +2340,7 @@ var B2Simulator = class {
|
|
|
1523
2340
|
isClientAuthorizedToRead: true,
|
|
1524
2341
|
value: large.legalHold
|
|
1525
2342
|
},
|
|
2343
|
+
...this.replicationStatusMetadataFor(large.replicationStatus),
|
|
1526
2344
|
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
1527
2345
|
uploadTimestamp: large.uploadTimestamp
|
|
1528
2346
|
}
|
|
@@ -1552,17 +2370,11 @@ var B2Simulator = class {
|
|
|
1552
2370
|
const bucket = this.buckets.get(large.bucketId);
|
|
1553
2371
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1554
2372
|
const sortedParts = [...large.parts.entries()].sort((a, b) => a[0] - b[0]);
|
|
1555
|
-
if (sortedParts.length >
|
|
1556
|
-
for (const [partNumber] of sortedParts) if (!Number.isInteger(partNumber) || partNumber <
|
|
2373
|
+
if (sortedParts.length > B2_MAX_PART_NUMBER) return this.error(400, "bad_request", `multipart upload has ${sortedParts.length} parts; B2 caps at ${B2_MAX_PART_NUMBER}`);
|
|
2374
|
+
for (const [partNumber] of sortedParts) if (!Number.isInteger(partNumber) || partNumber < B2_MIN_PART_NUMBER || partNumber > B2_MAX_PART_NUMBER) return this.error(400, "bad_request", `partNumber ${partNumber} is outside the [${B2_MIN_PART_NUMBER}, ${B2_MAX_PART_NUMBER}] range B2 accepts`);
|
|
1557
2375
|
if (req.partSha1Array.length !== sortedParts.length) return this.error(400, "bad_request", `partSha1Array has ${req.partSha1Array.length} entries but ${sortedParts.length} parts were uploaded`);
|
|
1558
|
-
for (
|
|
1559
|
-
|
|
1560
|
-
if (req.partSha1Array[i]?.toLowerCase() !== part.sha1) return this.error(400, "bad_request", `part ${partNumber} SHA-1 does not match the uploaded part`);
|
|
1561
|
-
}
|
|
1562
|
-
for (let i = 0; i < sortedParts.length - 1; i++) {
|
|
1563
|
-
const [partNumber, part] = sortedParts[i];
|
|
1564
|
-
if (part.data.byteLength < this.minimumPartSize) return this.error(400, "bad_request", `part ${partNumber} (${part.data.byteLength} bytes) is smaller than the minimum part size of ${this.minimumPartSize}`);
|
|
1565
|
-
}
|
|
2376
|
+
for (const [i, [partNumber, part]] of sortedParts.entries()) if (req.partSha1Array[i]?.toLowerCase() !== part.sha1) return this.error(400, "bad_request", `part ${partNumber} SHA-1 does not match the uploaded part`);
|
|
2377
|
+
for (const [partNumber, part] of sortedParts.slice(0, -1)) if (part.data.byteLength < this.minimumPartSize) return this.error(400, "bad_request", `part ${partNumber} (${part.data.byteLength} bytes) is smaller than the minimum part size of ${this.minimumPartSize}`);
|
|
1566
2378
|
let totalSize = 0;
|
|
1567
2379
|
for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
|
|
1568
2380
|
const combined = new Uint8Array(totalSize);
|
|
@@ -1581,11 +2393,14 @@ var B2Simulator = class {
|
|
|
1581
2393
|
action: FileAction.Upload,
|
|
1582
2394
|
fileRetention: large.fileRetention,
|
|
1583
2395
|
legalHold: large.legalHold,
|
|
1584
|
-
serverSideEncryption: large.serverSideEncryption
|
|
2396
|
+
serverSideEncryption: large.serverSideEncryption,
|
|
2397
|
+
uploadTimestamp: large.uploadTimestamp,
|
|
2398
|
+
...this.replicationStatusMetadataFor(large.replicationStatus)
|
|
1585
2399
|
});
|
|
1586
2400
|
const stored = {
|
|
1587
2401
|
fileVersion,
|
|
1588
|
-
data: combined
|
|
2402
|
+
data: combined,
|
|
2403
|
+
serverSideEncryption: large.serverSideEncryption
|
|
1589
2404
|
};
|
|
1590
2405
|
const existing = bucket.files.get(large.fileName);
|
|
1591
2406
|
if (existing) existing.push(stored);
|
|
@@ -1644,6 +2459,7 @@ var B2Simulator = class {
|
|
|
1644
2459
|
isClientAuthorizedToRead: true,
|
|
1645
2460
|
value: f.legalHold
|
|
1646
2461
|
},
|
|
2462
|
+
...this.replicationStatusMetadataFor(f.replicationStatus),
|
|
1647
2463
|
serverSideEncryption: publicServerSideEncryption(f.serverSideEncryption),
|
|
1648
2464
|
uploadTimestamp: f.uploadTimestamp
|
|
1649
2465
|
})),
|
|
@@ -1663,7 +2479,9 @@ var B2Simulator = class {
|
|
|
1663
2479
|
partNumber,
|
|
1664
2480
|
contentLength: part.data.byteLength,
|
|
1665
2481
|
contentSha1: part.sha1,
|
|
1666
|
-
|
|
2482
|
+
contentMd5: part.contentMd5,
|
|
2483
|
+
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
2484
|
+
uploadTimestamp: part.uploadTimestamp
|
|
1667
2485
|
}));
|
|
1668
2486
|
return {
|
|
1669
2487
|
status: 200,
|
|
@@ -1676,22 +2494,29 @@ var B2Simulator = class {
|
|
|
1676
2494
|
async copyPart(req) {
|
|
1677
2495
|
const large = this.largeFiles.get(req.largeFileId);
|
|
1678
2496
|
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
2497
|
+
const partNumberError = this.validatePartNumber(req.partNumber);
|
|
2498
|
+
if (partNumberError !== null) return partNumberError;
|
|
1679
2499
|
const found = this.findFile(req.sourceFileId);
|
|
1680
2500
|
if (found === null) return this.error(404, "file_not_present", "Source file not found");
|
|
1681
2501
|
const sourceStored = found.stored;
|
|
2502
|
+
const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
|
|
2503
|
+
if (sourceEncryptionError !== null) return sourceEncryptionError;
|
|
2504
|
+
const destinationEncryptionError = await this.validateCopyPartDestinationEncryption(large, req.destinationServerSideEncryption);
|
|
2505
|
+
if (destinationEncryptionError !== null) return destinationEncryptionError;
|
|
1682
2506
|
let partData = sourceStored.data;
|
|
1683
|
-
if (req.range) {
|
|
1684
|
-
const
|
|
1685
|
-
if (
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
partData = sourceStored.data.slice(rs, re + 1);
|
|
1689
|
-
}
|
|
2507
|
+
if (req.range !== void 0) {
|
|
2508
|
+
const parsed = parseRangeHeader(req.range, sourceStored.data.byteLength);
|
|
2509
|
+
if (parsed.kind === "malformed") return this.error(400, "bad_request", `Malformed copy range: ${req.range}`);
|
|
2510
|
+
if (parsed.kind === "unsatisfiable") return this.error(416, "range_not_satisfiable", `Unsatisfiable copy range: ${req.range}`);
|
|
2511
|
+
partData = sourceStored.data.subarray(parsed.start, parsed.end + 1);
|
|
1690
2512
|
}
|
|
1691
2513
|
const sha1 = await sha1Hex(partData);
|
|
2514
|
+
const uploadTimestamp = this.monotonicTimestamp();
|
|
1692
2515
|
large.parts.set(req.partNumber, {
|
|
1693
2516
|
data: new Uint8Array(partData),
|
|
1694
|
-
sha1
|
|
2517
|
+
sha1,
|
|
2518
|
+
contentMd5: null,
|
|
2519
|
+
uploadTimestamp
|
|
1695
2520
|
});
|
|
1696
2521
|
return {
|
|
1697
2522
|
status: 200,
|
|
@@ -1699,39 +2524,80 @@ var B2Simulator = class {
|
|
|
1699
2524
|
fileId: req.largeFileId,
|
|
1700
2525
|
partNumber: req.partNumber,
|
|
1701
2526
|
contentLength: partData.byteLength,
|
|
1702
|
-
contentSha1: sha1
|
|
2527
|
+
contentSha1: sha1,
|
|
2528
|
+
contentMd5: null,
|
|
2529
|
+
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
2530
|
+
uploadTimestamp
|
|
1703
2531
|
}
|
|
1704
2532
|
};
|
|
1705
2533
|
}
|
|
1706
2534
|
getDownloadAuthorization(req) {
|
|
2535
|
+
this.purgeExpiredDownloadAuthorizationTokens();
|
|
1707
2536
|
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
2537
|
+
const prefixError = validateDownloadAuthorizationPrefix(req.fileNamePrefix);
|
|
2538
|
+
if (prefixError) return this.error(400, prefixError.code, prefixError.message);
|
|
2539
|
+
const durationError = validateDownloadAuthorizationDuration(req.validDurationInSeconds);
|
|
2540
|
+
if (durationError) return this.error(400, durationError.code, durationError.message);
|
|
2541
|
+
const overrideError = validateDownloadResponseOverrides(req);
|
|
2542
|
+
if (overrideError) return this.error(400, overrideError.code, overrideError.message);
|
|
2543
|
+
let authorizationToken = randomDownloadAuthorizationToken();
|
|
2544
|
+
while (this.downloadAuthorizationTokens.has(authorizationToken)) authorizationToken = randomDownloadAuthorizationToken();
|
|
2545
|
+
const expiresAt = this.now() + req.validDurationInSeconds * 1e3;
|
|
2546
|
+
this.downloadAuthorizationTokens.set(authorizationToken, {
|
|
2547
|
+
bucketId: req.bucketId,
|
|
2548
|
+
fileNamePrefix: req.fileNamePrefix,
|
|
2549
|
+
expiresAt,
|
|
2550
|
+
responseHeaderOverrides: pickDownloadResponseOverrides(req)
|
|
2551
|
+
});
|
|
2552
|
+
this.pushDownloadAuthorizationExpiry({
|
|
2553
|
+
token: authorizationToken,
|
|
2554
|
+
expiresAt
|
|
2555
|
+
});
|
|
1708
2556
|
return {
|
|
1709
2557
|
status: 200,
|
|
1710
2558
|
body: {
|
|
1711
2559
|
bucketId: req.bucketId,
|
|
1712
2560
|
fileNamePrefix: req.fileNamePrefix,
|
|
1713
|
-
authorizationToken
|
|
2561
|
+
authorizationToken
|
|
1714
2562
|
}
|
|
1715
2563
|
};
|
|
1716
2564
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
if (
|
|
1720
|
-
|
|
2565
|
+
createKeyCapabilitiesOutsideGrant(authToken, requested) {
|
|
2566
|
+
const creator = authToken === void 0 ? void 0 : this.issuedTokens.get(authToken);
|
|
2567
|
+
if (creator === void 0) return this.strictAuth ? requested : [];
|
|
2568
|
+
if (creator.applicationKeyId === null) return [];
|
|
2569
|
+
const granted = new Set(creator.capabilities);
|
|
2570
|
+
return requested.filter((capability) => !granted.has(capability));
|
|
2571
|
+
}
|
|
2572
|
+
createKeyCreatorAccountId(authToken) {
|
|
2573
|
+
return (authToken === void 0 ? void 0 : this.issuedTokens.get(authToken))?.accountId ?? this.accountId;
|
|
2574
|
+
}
|
|
2575
|
+
createKey(req, version, authToken) {
|
|
2576
|
+
if (version === "v4" && hasOwnField(req, "bucketId")) return this.error(400, "bad_request", "bucketId is not accepted by v4 b2_create_key; use bucketIds");
|
|
2577
|
+
if (version !== "v4" && req.bucketId !== void 0 && req.bucketIds !== void 0) return this.error(400, "bad_request", "b2_create_key accepts either bucketIds or bucketId");
|
|
2578
|
+
const bucketIds = version !== "v4" && req.bucketId !== void 0 ? Object.freeze([req.bucketId]) : normalizeKeyBucketIds(req);
|
|
1721
2579
|
const namePrefix = req.namePrefix === void 0 || req.namePrefix === "" ? null : req.namePrefix;
|
|
1722
|
-
|
|
2580
|
+
const creatorAccountId = this.createKeyCreatorAccountId(authToken);
|
|
2581
|
+
if (req.accountId !== creatorAccountId) return this.error(400, "bad_request", "accountId must match authorized account");
|
|
2582
|
+
const keyNameError = validateCreateKeyName(req.keyName);
|
|
2583
|
+
if (keyNameError) return this.error(400, keyNameError.code, keyNameError.message);
|
|
2584
|
+
const keyName = req.keyName;
|
|
2585
|
+
const capabilitiesError = validateCreateKeyCapabilities(req.capabilities);
|
|
2586
|
+
if (capabilitiesError) return this.error(400, capabilitiesError.code, capabilitiesError.message);
|
|
2587
|
+
const capabilities = normalizeCreateKeyCapabilities(req.capabilities);
|
|
2588
|
+
const unauthorizedCapabilities = this.createKeyCapabilitiesOutsideGrant(authToken, capabilities);
|
|
2589
|
+
if (unauthorizedCapabilities.length > 0) return this.error(400, "bad_request", `requested capabilities exceed creator grant: ${unauthorizedCapabilities.join(", ")}`);
|
|
2590
|
+
if (hasKeyManagementCapability(capabilities) && (bucketIds !== null || namePrefix !== null)) return this.error(400, "bad_request", "key-management capabilities are account-level and cannot be bucket or name-prefix scoped");
|
|
1723
2591
|
const kid = this.genId("sim_key");
|
|
1724
|
-
const appKey = this.genId("sim_secret");
|
|
1725
|
-
const expiration = req.validDurationInSeconds !== void 0 ? Date.now() + req.validDurationInSeconds * 1e3 : null;
|
|
1726
2592
|
const stored = {
|
|
1727
2593
|
applicationKeyId: kid,
|
|
1728
|
-
keyName
|
|
1729
|
-
capabilities
|
|
1730
|
-
accountId:
|
|
1731
|
-
applicationKey:
|
|
2594
|
+
keyName,
|
|
2595
|
+
capabilities,
|
|
2596
|
+
accountId: creatorAccountId,
|
|
2597
|
+
applicationKey: this.genId("sim_secret"),
|
|
1732
2598
|
bucketIds,
|
|
1733
2599
|
namePrefix,
|
|
1734
|
-
expirationTimestamp:
|
|
2600
|
+
expirationTimestamp: req.validDurationInSeconds !== void 0 ? this.now() + req.validDurationInSeconds * 1e3 : null
|
|
1735
2601
|
};
|
|
1736
2602
|
this.keys.set(kid, stored);
|
|
1737
2603
|
return {
|
|
@@ -1740,7 +2606,7 @@ var B2Simulator = class {
|
|
|
1740
2606
|
keyName: stored.keyName,
|
|
1741
2607
|
applicationKeyId: stored.applicationKeyId,
|
|
1742
2608
|
applicationKey: stored.applicationKey,
|
|
1743
|
-
capabilities: stored.capabilities,
|
|
2609
|
+
capabilities: cloneCapabilities(stored.capabilities),
|
|
1744
2610
|
accountId: stored.accountId,
|
|
1745
2611
|
expirationTimestamp: stored.expirationTimestamp,
|
|
1746
2612
|
bucketIds: cloneBucketIds(stored.bucketIds),
|
|
@@ -1765,7 +2631,7 @@ var B2Simulator = class {
|
|
|
1765
2631
|
keys: allKeys.slice(0, max).map((k) => ({
|
|
1766
2632
|
keyName: k.keyName,
|
|
1767
2633
|
applicationKeyId: k.applicationKeyId,
|
|
1768
|
-
capabilities: k.capabilities,
|
|
2634
|
+
capabilities: cloneCapabilities(k.capabilities),
|
|
1769
2635
|
accountId: k.accountId,
|
|
1770
2636
|
expirationTimestamp: k.expirationTimestamp,
|
|
1771
2637
|
bucketIds: cloneBucketIds(k.bucketIds),
|
|
@@ -1791,7 +2657,7 @@ var B2Simulator = class {
|
|
|
1791
2657
|
body: {
|
|
1792
2658
|
keyName: key.keyName,
|
|
1793
2659
|
applicationKeyId: key.applicationKeyId,
|
|
1794
|
-
capabilities: key.capabilities,
|
|
2660
|
+
capabilities: cloneCapabilities(key.capabilities),
|
|
1795
2661
|
accountId: key.accountId,
|
|
1796
2662
|
expirationTimestamp: key.expirationTimestamp,
|
|
1797
2663
|
bucketIds: cloneBucketIds(key.bucketIds),
|
|
@@ -1801,31 +2667,45 @@ var B2Simulator = class {
|
|
|
1801
2667
|
}
|
|
1802
2668
|
};
|
|
1803
2669
|
}
|
|
1804
|
-
updateFileRetention(req) {
|
|
2670
|
+
updateFileRetention(req, authToken) {
|
|
1805
2671
|
const found = this.findFile(req.fileId);
|
|
1806
2672
|
if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
|
|
2673
|
+
const fileLockError = this.requireFileLockEnabled(found.bucket);
|
|
2674
|
+
if (fileLockError !== null) return fileLockError;
|
|
2675
|
+
const fileRetention = parseFileRetentionValue(req.fileRetention, Date.now());
|
|
2676
|
+
if (fileRetention === null) return this.error(400, "bad_request", "fileRetention must use a valid mode and retainUntilTimestamp pair");
|
|
2677
|
+
const current = found.stored.fileVersion.fileRetention?.value;
|
|
2678
|
+
if (current?.mode === RetentionMode.Compliance && (fileRetention.mode !== RetentionMode.Compliance || isRetentionWeakened(current, fileRetention))) return this.error(400, "file_lock_compliance_protected", "Compliance-mode retention cannot be shortened or removed");
|
|
2679
|
+
if (requiresGovernanceBypass(current, fileRetention)) {
|
|
2680
|
+
const bypassError = this.requireGovernanceBypass(req, authToken, "Governance-mode retention cannot be shortened, removed, or converted without bypassGovernance");
|
|
2681
|
+
if (bypassError !== null) return bypassError;
|
|
2682
|
+
}
|
|
1807
2683
|
found.versions[found.index] = {
|
|
1808
2684
|
fileVersion: {
|
|
1809
2685
|
...found.stored.fileVersion,
|
|
1810
2686
|
fileRetention: {
|
|
1811
2687
|
isClientAuthorizedToRead: true,
|
|
1812
|
-
value:
|
|
2688
|
+
value: fileRetention
|
|
1813
2689
|
}
|
|
1814
2690
|
},
|
|
1815
|
-
data: found.stored.data
|
|
2691
|
+
data: found.stored.data,
|
|
2692
|
+
serverSideEncryption: found.stored.serverSideEncryption
|
|
1816
2693
|
};
|
|
1817
2694
|
return {
|
|
1818
2695
|
status: 200,
|
|
1819
2696
|
body: {
|
|
1820
2697
|
fileName: req.fileName,
|
|
1821
2698
|
fileId: req.fileId,
|
|
1822
|
-
fileRetention
|
|
2699
|
+
fileRetention
|
|
1823
2700
|
}
|
|
1824
2701
|
};
|
|
1825
2702
|
}
|
|
1826
2703
|
updateFileLegalHold(req) {
|
|
1827
2704
|
const found = this.findFile(req.fileId);
|
|
1828
2705
|
if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
|
|
2706
|
+
const fileLockError = this.requireFileLockEnabled(found.bucket);
|
|
2707
|
+
if (fileLockError !== null) return fileLockError;
|
|
2708
|
+
if (req.legalHold !== LegalHoldValue.On && req.legalHold !== LegalHoldValue.Off) return this.error(400, "bad_request", "legalHold must be \"on\" or \"off\"");
|
|
1829
2709
|
found.versions[found.index] = {
|
|
1830
2710
|
fileVersion: {
|
|
1831
2711
|
...found.stored.fileVersion,
|
|
@@ -1834,7 +2714,8 @@ var B2Simulator = class {
|
|
|
1834
2714
|
value: req.legalHold
|
|
1835
2715
|
}
|
|
1836
2716
|
},
|
|
1837
|
-
data: found.stored.data
|
|
2717
|
+
data: found.stored.data,
|
|
2718
|
+
serverSideEncryption: found.stored.serverSideEncryption
|
|
1838
2719
|
};
|
|
1839
2720
|
return {
|
|
1840
2721
|
status: 200,
|
|
@@ -1858,6 +2739,8 @@ var B2Simulator = class {
|
|
|
1858
2739
|
}
|
|
1859
2740
|
setBucketNotificationRules(req) {
|
|
1860
2741
|
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
2742
|
+
const rulesError = validateNotificationRules(req.eventNotificationRules);
|
|
2743
|
+
if (rulesError) return this.error(400, rulesError.code, rulesError.message);
|
|
1861
2744
|
this.notificationRules.set(req.bucketId, req.eventNotificationRules);
|
|
1862
2745
|
return {
|
|
1863
2746
|
status: 200,
|
|
@@ -1915,11 +2798,23 @@ var B2Simulator = class {
|
|
|
1915
2798
|
isClientAuthorizedToRead: true,
|
|
1916
2799
|
value: params.legalHold ?? null
|
|
1917
2800
|
},
|
|
1918
|
-
replicationStatus
|
|
2801
|
+
...this.replicationStatusMetadataFor(params.replicationStatus),
|
|
1919
2802
|
serverSideEncryption: publicServerSideEncryption(params.serverSideEncryption ?? { mode: EncryptionMode.None }),
|
|
1920
|
-
uploadTimestamp: this.monotonicTimestamp()
|
|
2803
|
+
uploadTimestamp: params.uploadTimestamp ?? this.monotonicTimestamp()
|
|
1921
2804
|
};
|
|
1922
2805
|
}
|
|
2806
|
+
newReplicationStatusField(bucketId, fileName) {
|
|
2807
|
+
return this.replicationStatusMetadataFor(this.replicationStatusForNewFile(bucketId, fileName));
|
|
2808
|
+
}
|
|
2809
|
+
replicationStatusMetadataFor(replicationStatus) {
|
|
2810
|
+
return replicationStatus === void 0 ? {} : { replicationStatus };
|
|
2811
|
+
}
|
|
2812
|
+
replicationStatusForNewFile(bucketId, fileName) {
|
|
2813
|
+
return this.matchingEnabledReplicationRules(bucketId, fileName).length > 0 ? "PENDING" : void 0;
|
|
2814
|
+
}
|
|
2815
|
+
matchingEnabledReplicationRules(bucketId, fileName) {
|
|
2816
|
+
return (this.buckets.get(bucketId)?.info.replicationConfiguration.asReplicationSource?.replicationRules)?.filter((rule) => rule.isEnabled && fileName.startsWith(rule.fileNamePrefix)) ?? [];
|
|
2817
|
+
}
|
|
1923
2818
|
error(status, code, message) {
|
|
1924
2819
|
return {
|
|
1925
2820
|
status,
|
|
@@ -1947,8 +2842,7 @@ var B2Simulator = class {
|
|
|
1947
2842
|
* used to match against `EventNotificationRule.eventTypes` globs.
|
|
1948
2843
|
*/
|
|
1949
2844
|
firePostUploadHooks(fileVersion, bucketId, eventType) {
|
|
1950
|
-
|
|
1951
|
-
if (!bucket) return;
|
|
2845
|
+
if (!this.buckets.get(bucketId)) return;
|
|
1952
2846
|
if (this.onWebhookDeliver !== void 0) {
|
|
1953
2847
|
const rules = this.notificationRules.get(bucketId) ?? [];
|
|
1954
2848
|
for (const rule of rules) {
|
|
@@ -1962,18 +2856,13 @@ var B2Simulator = class {
|
|
|
1962
2856
|
}));
|
|
1963
2857
|
}
|
|
1964
2858
|
}
|
|
1965
|
-
if (this.onReplicate !== void 0) {
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
sourceFileVersion: fileVersion,
|
|
1973
|
-
sourceBucketId: bucketId,
|
|
1974
|
-
destinationBucketId: rule.destinationBucketId
|
|
1975
|
-
}));
|
|
1976
|
-
}
|
|
2859
|
+
if (this.onReplicate !== void 0) for (const rule of this.matchingEnabledReplicationRules(bucketId, fileVersion.fileName)) {
|
|
2860
|
+
const hook = this.onReplicate;
|
|
2861
|
+
this.dispatchHook("replication", () => hook({
|
|
2862
|
+
sourceFileVersion: fileVersion,
|
|
2863
|
+
sourceBucketId: bucketId,
|
|
2864
|
+
destinationBucketId: rule.destinationBucketId
|
|
2865
|
+
}));
|
|
1977
2866
|
}
|
|
1978
2867
|
}
|
|
1979
2868
|
/**
|
|
@@ -2057,6 +2946,83 @@ function buildFaultResponse(fault) {
|
|
|
2057
2946
|
arrayBuffer: () => Promise.resolve(utf8Encoder.encode(body).buffer)
|
|
2058
2947
|
};
|
|
2059
2948
|
}
|
|
2949
|
+
function rawUrlPathContainsLiteralBackslash(rawUrl) {
|
|
2950
|
+
const schemeEnd = rawUrl.indexOf("://");
|
|
2951
|
+
const authorityStart = schemeEnd === -1 ? 0 : schemeEnd + 3;
|
|
2952
|
+
const urlEndCandidates = [rawUrl.indexOf("?", authorityStart), rawUrl.indexOf("#", authorityStart)].filter((index) => index !== -1);
|
|
2953
|
+
const urlEnd = urlEndCandidates.length === 0 ? rawUrl.length : Math.min(...urlEndCandidates);
|
|
2954
|
+
const pathStartCandidates = [rawUrl.indexOf("/", authorityStart), rawUrl.indexOf("\\", authorityStart)].filter((index) => index !== -1 && index < urlEnd);
|
|
2955
|
+
if (pathStartCandidates.length === 0) return false;
|
|
2956
|
+
const pathStart = Math.min(...pathStartCandidates);
|
|
2957
|
+
const pathEndCandidates = [rawUrl.indexOf("?", pathStart), rawUrl.indexOf("#", pathStart)].filter((index) => index !== -1);
|
|
2958
|
+
const pathEnd = pathEndCandidates.length === 0 ? rawUrl.length : Math.min(...pathEndCandidates);
|
|
2959
|
+
return rawUrl.slice(pathStart, pathEnd).includes("\\");
|
|
2960
|
+
}
|
|
2961
|
+
function badRequestJson(message) {
|
|
2962
|
+
return {
|
|
2963
|
+
status: 400,
|
|
2964
|
+
body: {
|
|
2965
|
+
status: 400,
|
|
2966
|
+
code: "bad_request",
|
|
2967
|
+
message
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function checkedUploadBody(data, expectedLength) {
|
|
2972
|
+
if (expectedLength !== null && data.byteLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, data.byteLength)) };
|
|
2973
|
+
return { data };
|
|
2974
|
+
}
|
|
2975
|
+
function copyBytes(bytes) {
|
|
2976
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
2977
|
+
copy.set(bytes);
|
|
2978
|
+
return copy;
|
|
2979
|
+
}
|
|
2980
|
+
function concatenateChunks(chunks, totalLength) {
|
|
2981
|
+
if (chunks.length === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
2982
|
+
if (chunks.length === 1) return copyBytes(chunks[0]);
|
|
2983
|
+
const data = new Uint8Array(totalLength);
|
|
2984
|
+
let offset = 0;
|
|
2985
|
+
for (const chunk of chunks) {
|
|
2986
|
+
data.set(chunk, offset);
|
|
2987
|
+
offset += chunk.byteLength;
|
|
2988
|
+
}
|
|
2989
|
+
return data;
|
|
2990
|
+
}
|
|
2991
|
+
async function readStreamingUploadBody(stream, expectedLength) {
|
|
2992
|
+
const reader = stream.getReader();
|
|
2993
|
+
const chunks = [];
|
|
2994
|
+
let totalLength = 0;
|
|
2995
|
+
try {
|
|
2996
|
+
while (true) {
|
|
2997
|
+
const { done, value } = await reader.read();
|
|
2998
|
+
if (done) break;
|
|
2999
|
+
if (value === void 0) continue;
|
|
3000
|
+
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
3001
|
+
totalLength += chunk.byteLength;
|
|
3002
|
+
if (expectedLength !== null && totalLength > expectedLength) {
|
|
3003
|
+
await reader.cancel().catch(() => void 0);
|
|
3004
|
+
return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
|
|
3005
|
+
}
|
|
3006
|
+
chunks.push(chunk);
|
|
3007
|
+
}
|
|
3008
|
+
} finally {
|
|
3009
|
+
reader.releaseLock();
|
|
3010
|
+
}
|
|
3011
|
+
if (expectedLength !== null && totalLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
|
|
3012
|
+
return { data: concatenateChunks(chunks, totalLength) };
|
|
3013
|
+
}
|
|
3014
|
+
async function readUploadBody(body, expectedLength) {
|
|
3015
|
+
if (body instanceof ReadableStream) return readStreamingUploadBody(body, expectedLength);
|
|
3016
|
+
if (body instanceof ArrayBuffer) return checkedUploadBody(new Uint8Array(body), expectedLength);
|
|
3017
|
+
if (ArrayBuffer.isView(body)) {
|
|
3018
|
+
const view = body;
|
|
3019
|
+
return checkedUploadBody(copyBytes(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)), expectedLength);
|
|
3020
|
+
}
|
|
3021
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
3022
|
+
if (expectedLength !== null && body.size !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, body.size)) };
|
|
3023
|
+
}
|
|
3024
|
+
return checkedUploadBody(new Uint8Array(await new Response(body).arrayBuffer()), expectedLength);
|
|
3025
|
+
}
|
|
2060
3026
|
var SimulatorTransport = class {
|
|
2061
3027
|
sim;
|
|
2062
3028
|
constructor(sim) {
|
|
@@ -2066,6 +3032,12 @@ var SimulatorTransport = class {
|
|
|
2066
3032
|
const url = request.url;
|
|
2067
3033
|
const fault = this.sim.consumeMatchingFault(url);
|
|
2068
3034
|
if (fault !== null) return buildFaultResponse(fault);
|
|
3035
|
+
if (rawUrlPathContainsLiteralBackslash(url)) return buildFaultResponse({
|
|
3036
|
+
on: "\\",
|
|
3037
|
+
status: 400,
|
|
3038
|
+
code: "bad_request",
|
|
3039
|
+
message: "URL path must not contain literal backslashes"
|
|
3040
|
+
});
|
|
2069
3041
|
const headers = {};
|
|
2070
3042
|
if (request.headers) for (const [k, v] of Object.entries(request.headers)) headers[k.toLowerCase()] = v;
|
|
2071
3043
|
const parsedUrl = new URL(url);
|
|
@@ -2073,10 +3045,15 @@ var SimulatorTransport = class {
|
|
|
2073
3045
|
const isUpload = endpoint === "b2_upload_file" || endpoint === "b2_upload_part";
|
|
2074
3046
|
if (parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/")) {
|
|
2075
3047
|
const method = request.method === "HEAD" ? "HEAD" : "GET";
|
|
2076
|
-
const result = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
|
|
3048
|
+
const result = await this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
|
|
2077
3049
|
const data = result.data ?? /* @__PURE__ */ new Uint8Array(0);
|
|
2078
3050
|
const responseHeaders = new Headers(result.headers);
|
|
2079
3051
|
responseHeaders.set("Content-Type", result.headers["Content-Type"] ?? "application/octet-stream");
|
|
3052
|
+
let decodedText = null;
|
|
3053
|
+
const text = () => {
|
|
3054
|
+
decodedText ??= utf8Decoder.decode(data);
|
|
3055
|
+
return decodedText;
|
|
3056
|
+
};
|
|
2080
3057
|
const body = method === "HEAD" || result.data === null ? null : new ReadableStream({ start(controller) {
|
|
2081
3058
|
controller.enqueue(data);
|
|
2082
3059
|
controller.close();
|
|
@@ -2085,15 +3062,25 @@ var SimulatorTransport = class {
|
|
|
2085
3062
|
status: result.status,
|
|
2086
3063
|
headers: responseHeaders,
|
|
2087
3064
|
body,
|
|
2088
|
-
json: () =>
|
|
2089
|
-
|
|
3065
|
+
json: () => {
|
|
3066
|
+
try {
|
|
3067
|
+
return Promise.resolve(JSON.parse(text()));
|
|
3068
|
+
} catch {
|
|
3069
|
+
return Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON"));
|
|
3070
|
+
}
|
|
3071
|
+
},
|
|
3072
|
+
text: () => Promise.resolve(text()),
|
|
2090
3073
|
arrayBuffer: () => Promise.resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
|
|
2091
3074
|
};
|
|
2092
3075
|
}
|
|
2093
3076
|
let result;
|
|
2094
|
-
if (isUpload
|
|
2095
|
-
const
|
|
2096
|
-
result =
|
|
3077
|
+
if (isUpload) {
|
|
3078
|
+
const parsedContentLength = parseContentLengthHeader(headers);
|
|
3079
|
+
if (parsedContentLength.kind === "error") result = badRequestJson(parsedContentLength.message);
|
|
3080
|
+
else {
|
|
3081
|
+
const uploadBody = await readUploadBody(request.body ?? /* @__PURE__ */ new Uint8Array(0), parsedContentLength.kind === "ok" ? parsedContentLength.expectedLength : null);
|
|
3082
|
+
result = "error" in uploadBody ? uploadBody.error : await this.sim.handleUpload(url, headers, uploadBody.data);
|
|
3083
|
+
}
|
|
2097
3084
|
} else {
|
|
2098
3085
|
let body = null;
|
|
2099
3086
|
if (request.body) {
|
|
@@ -2103,7 +3090,7 @@ var SimulatorTransport = class {
|
|
|
2103
3090
|
} catch {
|
|
2104
3091
|
body = text;
|
|
2105
3092
|
}
|
|
2106
|
-
}
|
|
3093
|
+
} else if (request.method.toUpperCase() === "GET" && isPartnerQueryEndpoint(apiPathParts(parsedUrl.pathname).endpoint)) body = queryParamsBody(parsedUrl.searchParams);
|
|
2107
3094
|
result = await this.sim.handleRequest(request.method, parsedUrl.origin, parsedUrl.pathname, headers, body);
|
|
2108
3095
|
}
|
|
2109
3096
|
const responseBody = JSON.stringify(result.body);
|
|
@@ -2121,6 +3108,6 @@ var SimulatorTransport = class {
|
|
|
2121
3108
|
}
|
|
2122
3109
|
};
|
|
2123
3110
|
//#endregion
|
|
2124
|
-
export { B2Simulator, BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, BUCKET_NAME_MAX, BUCKET_NAME_MIN, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, FILE_NAME_MAX_BYTES, LIST_ENDPOINT_CAPS };
|
|
3111
|
+
export { B2Simulator, BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, BUCKET_NAME_MAX, BUCKET_NAME_MIN, DOWNLOAD_AUTH_DURATION_MAX_SECONDS, DOWNLOAD_AUTH_DURATION_MIN_SECONDS, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, FILE_NAME_MAX_BYTES, KEY_NAME_MAX, KEY_NAME_MIN, LIST_ENDPOINT_CAPS };
|
|
2125
3112
|
|
|
2126
3113
|
//# sourceMappingURL=index.js.map
|