@backblaze-labs/b2-sdk 0.2.0 → 0.4.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 +86 -1
- package/README.md +38 -7
- 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/auth/reauth-coalescer.cjs +65 -0
- package/dist/auth/reauth-coalescer.cjs.map +1 -0
- package/dist/auth/reauth-coalescer.d.cts +34 -0
- package/dist/auth/reauth-coalescer.d.cts.map +1 -0
- package/dist/auth/reauth-coalescer.d.ts +34 -0
- package/dist/auth/reauth-coalescer.d.ts.map +1 -0
- package/dist/auth/reauth-coalescer.js +65 -0
- package/dist/auth/reauth-coalescer.js.map +1 -0
- 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 +168 -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 +168 -0
- package/dist/backup/raw.js.map +1 -0
- package/dist/bucket-validation.cjs +111 -0
- package/dist/bucket-validation.cjs.map +1 -0
- package/dist/bucket-validation.d.cts +27 -0
- package/dist/bucket-validation.d.cts.map +1 -0
- package/dist/bucket-validation.d.ts +27 -0
- package/dist/bucket-validation.d.ts.map +1 -0
- package/dist/bucket-validation.js +109 -0
- package/dist/bucket-validation.js.map +1 -0
- package/dist/bucket.cjs +125 -74
- package/dist/bucket.cjs.map +1 -1
- package/dist/bucket.d.cts +156 -61
- package/dist/bucket.d.cts.map +1 -1
- package/dist/bucket.d.ts +156 -61
- package/dist/bucket.d.ts.map +1 -1
- package/dist/bucket.js +125 -74
- package/dist/bucket.js.map +1 -1
- package/dist/client.cjs +29 -8
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +30 -9
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.ts +30 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +29 -8
- package/dist/client.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 +179 -19
- package/dist/download/single.cjs.map +1 -1
- package/dist/download/single.d.cts +10 -4
- package/dist/download/single.d.cts.map +1 -1
- package/dist/download/single.d.ts +10 -4
- package/dist/download/single.d.ts.map +1 -1
- package/dist/download/single.js +179 -19
- package/dist/download/single.js.map +1 -1
- package/dist/errors/index.cjs +196 -3
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +134 -0
- package/dist/errors/index.d.cts.map +1 -1
- package/dist/errors/index.d.ts +134 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +183 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/http/non-idempotent-mutations.cjs +38 -0
- package/dist/http/non-idempotent-mutations.cjs.map +1 -0
- package/dist/http/non-idempotent-mutations.d.cts +25 -0
- package/dist/http/non-idempotent-mutations.d.cts.map +1 -0
- package/dist/http/non-idempotent-mutations.d.ts +25 -0
- package/dist/http/non-idempotent-mutations.d.ts.map +1 -0
- package/dist/http/non-idempotent-mutations.js +35 -0
- package/dist/http/non-idempotent-mutations.js.map +1 -0
- 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 +62 -23
- package/dist/http/transport.cjs.map +1 -1
- package/dist/http/transport.d.cts +30 -3
- package/dist/http/transport.d.cts.map +1 -1
- package/dist/http/transport.d.ts +30 -3
- package/dist/http/transport.d.ts.map +1 -1
- package/dist/http/transport.js +60 -22
- package/dist/http/transport.js.map +1 -1
- package/dist/http/user-agent.cjs +12 -1
- package/dist/http/user-agent.cjs.map +1 -1
- package/dist/http/user-agent.d.cts +8 -0
- package/dist/http/user-agent.d.cts.map +1 -1
- package/dist/http/user-agent.d.ts +8 -0
- package/dist/http/user-agent.d.ts.map +1 -1
- package/dist/http/user-agent.js +13 -3
- package/dist/http/user-agent.js.map +1 -1
- package/dist/index.cjs +50 -2
- package/dist/index.d.cts +15 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +15 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/notifications/index.cjs +3 -0
- package/dist/notifications/index.d.cts +2 -0
- package/dist/notifications/index.d.cts.map +1 -1
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +2 -1
- package/dist/object.cjs +13 -2
- package/dist/object.cjs.map +1 -1
- package/dist/object.d.cts +11 -3
- package/dist/object.d.cts.map +1 -1
- package/dist/object.d.ts +11 -3
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +13 -2
- 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 +156 -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 +156 -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 +270 -0
- package/dist/partner/client.cjs.map +1 -0
- package/dist/partner/client.d.cts +357 -0
- package/dist/partner/client.d.cts.map +1 -0
- package/dist/partner/client.d.ts +357 -0
- package/dist/partner/client.d.ts.map +1 -0
- package/dist/partner/client.js +270 -0
- package/dist/partner/client.js.map +1 -0
- package/dist/partner/endpoint-url.cjs +109 -0
- package/dist/partner/endpoint-url.cjs.map +1 -0
- package/dist/partner/endpoint-url.d.cts +74 -0
- package/dist/partner/endpoint-url.d.cts.map +1 -0
- package/dist/partner/endpoint-url.d.ts +74 -0
- package/dist/partner/endpoint-url.d.ts.map +1 -0
- package/dist/partner/endpoint-url.js +106 -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 +27 -0
- package/dist/partner/index.d.cts +32 -0
- package/dist/partner/index.d.cts.map +1 -0
- package/dist/partner/index.d.ts +32 -0
- package/dist/partner/index.d.ts.map +1 -0
- package/dist/partner/index.js +9 -0
- package/dist/partner/raw.cjs +400 -0
- package/dist/partner/raw.cjs.map +1 -0
- package/dist/partner/raw.d.cts +220 -0
- package/dist/partner/raw.d.cts.map +1 -0
- package/dist/partner/raw.d.ts +220 -0
- package/dist/partner/raw.d.ts.map +1 -0
- package/dist/partner/raw.js +398 -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 +292 -0
- package/dist/partner/redaction.cjs.map +1 -0
- package/dist/partner/redaction.d.cts +158 -0
- package/dist/partner/redaction.d.cts.map +1 -0
- package/dist/partner/redaction.d.ts +158 -0
- package/dist/partner/redaction.d.ts.map +1 -0
- package/dist/partner/redaction.js +281 -0
- package/dist/partner/redaction.js.map +1 -0
- package/dist/raw/encoding.cjs +29 -10
- package/dist/raw/encoding.cjs.map +1 -1
- package/dist/raw/encoding.d.cts +3 -2
- package/dist/raw/encoding.d.cts.map +1 -1
- package/dist/raw/encoding.d.ts +3 -2
- package/dist/raw/encoding.d.ts.map +1 -1
- package/dist/raw/encoding.js +29 -10
- package/dist/raw/encoding.js.map +1 -1
- package/dist/raw/index.cjs +131 -44
- package/dist/raw/index.cjs.map +1 -1
- package/dist/raw/index.d.cts +21 -15
- package/dist/raw/index.d.cts.map +1 -1
- package/dist/raw/index.d.ts +21 -15
- package/dist/raw/index.d.ts.map +1 -1
- package/dist/raw/index.js +114 -45
- 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 +1579 -257
- package/dist/simulator/index.cjs.map +1 -1
- package/dist/simulator/index.d.cts +124 -11
- package/dist/simulator/index.d.cts.map +1 -1
- package/dist/simulator/index.d.ts +124 -11
- package/dist/simulator/index.d.ts.map +1 -1
- package/dist/simulator/index.js +1568 -262
- package/dist/simulator/index.js.map +1 -1
- package/dist/simulator/partner-capabilities.cjs +77 -0
- package/dist/simulator/partner-capabilities.cjs.map +1 -0
- package/dist/simulator/partner-capabilities.d.cts +53 -0
- package/dist/simulator/partner-capabilities.d.cts.map +1 -0
- package/dist/simulator/partner-capabilities.d.ts +53 -0
- package/dist/simulator/partner-capabilities.d.ts.map +1 -0
- package/dist/simulator/partner-capabilities.js +73 -0
- package/dist/simulator/partner-capabilities.js.map +1 -0
- package/dist/simulator/partner.cjs +713 -0
- package/dist/simulator/partner.cjs.map +1 -0
- package/dist/simulator/partner.d.cts +186 -0
- package/dist/simulator/partner.d.cts.map +1 -0
- package/dist/simulator/partner.d.ts +186 -0
- package/dist/simulator/partner.d.ts.map +1 -0
- package/dist/simulator/partner.js +713 -0
- package/dist/simulator/partner.js.map +1 -0
- package/dist/simulator/validation.cjs +732 -25
- package/dist/simulator/validation.cjs.map +1 -1
- package/dist/simulator/validation.d.cts +145 -6
- package/dist/simulator/validation.d.cts.map +1 -1
- package/dist/simulator/validation.d.ts +145 -6
- package/dist/simulator/validation.d.ts.map +1 -1
- package/dist/simulator/validation.js +705 -26
- package/dist/simulator/validation.js.map +1 -1
- package/dist/streams/file-source.cjs +2 -2
- package/dist/streams/file-source.cjs.map +1 -1
- package/dist/streams/file-source.js +2 -2
- package/dist/streams/file-source.js.map +1 -1
- package/dist/streams/hash.cjs +246 -13
- package/dist/streams/hash.cjs.map +1 -1
- package/dist/streams/hash.d.cts +37 -12
- package/dist/streams/hash.d.cts.map +1 -1
- package/dist/streams/hash.d.ts +37 -12
- package/dist/streams/hash.d.ts.map +1 -1
- package/dist/streams/hash.js +246 -15
- package/dist/streams/hash.js.map +1 -1
- package/dist/streams/index.cjs +3 -1
- package/dist/streams/index.d.cts +1 -1
- package/dist/streams/index.d.cts.map +1 -1
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.d.ts.map +1 -1
- package/dist/streams/index.js +3 -3
- package/dist/streams/progress.cjs +63 -4
- package/dist/streams/progress.cjs.map +1 -1
- package/dist/streams/progress.d.cts +28 -2
- package/dist/streams/progress.d.cts.map +1 -1
- package/dist/streams/progress.d.ts +28 -2
- package/dist/streams/progress.d.ts.map +1 -1
- package/dist/streams/progress.js +63 -4
- package/dist/streams/progress.js.map +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 +156 -43
- package/dist/sync/download-staging.cjs.map +1 -1
- package/dist/sync/download-staging.d.cts +26 -2
- package/dist/sync/download-staging.d.cts.map +1 -1
- package/dist/sync/download-staging.d.ts +26 -2
- package/dist/sync/download-staging.d.ts.map +1 -1
- package/dist/sync/download-staging.js +155 -44
- 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/index.d.cts +1 -1
- package/dist/sync/index.d.cts.map +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/local-file-io.cjs +15 -6
- package/dist/sync/local-file-io.cjs.map +1 -1
- package/dist/sync/local-file-io.d.cts +3 -0
- package/dist/sync/local-file-io.d.cts.map +1 -1
- package/dist/sync/local-file-io.d.ts +3 -0
- package/dist/sync/local-file-io.d.ts.map +1 -1
- package/dist/sync/local-file-io.js +15 -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/pairing.cjs +1 -0
- package/dist/sync/pairing.cjs.map +1 -1
- package/dist/sync/pairing.js +1 -0
- package/dist/sync/pairing.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 +4 -2
- package/dist/sync/scanners/b2.cjs.map +1 -1
- package/dist/sync/scanners/b2.d.cts.map +1 -1
- package/dist/sync/scanners/b2.d.ts.map +1 -1
- package/dist/sync/scanners/b2.js +4 -2
- package/dist/sync/scanners/b2.js.map +1 -1
- package/dist/sync/scanners/local.cjs +150 -33
- package/dist/sync/scanners/local.cjs.map +1 -1
- package/dist/sync/scanners/local.d.cts +6 -0
- package/dist/sync/scanners/local.d.cts.map +1 -1
- package/dist/sync/scanners/local.d.ts +6 -0
- package/dist/sync/scanners/local.d.ts.map +1 -1
- package/dist/sync/scanners/local.js +150 -33
- package/dist/sync/scanners/local.js.map +1 -1
- package/dist/sync/synchronizer.cjs +79 -31
- package/dist/sync/synchronizer.cjs.map +1 -1
- package/dist/sync/synchronizer.d.cts +1 -0
- package/dist/sync/synchronizer.d.cts.map +1 -1
- package/dist/sync/synchronizer.d.ts +1 -0
- package/dist/sync/synchronizer.d.ts.map +1 -1
- package/dist/sync/synchronizer.js +79 -31
- package/dist/sync/synchronizer.js.map +1 -1
- package/dist/sync/types.d.cts +19 -3
- package/dist/sync/types.d.cts.map +1 -1
- package/dist/sync/types.d.ts +19 -3
- package/dist/sync/types.d.ts.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 +85 -0
- package/dist/types/backup.d.cts.map +1 -0
- package/dist/types/backup.d.ts +85 -0
- package/dist/types/backup.d.ts.map +1 -0
- package/dist/types/bucket.cjs +81 -11
- package/dist/types/bucket.cjs.map +1 -1
- package/dist/types/bucket.d.cts +194 -41
- package/dist/types/bucket.d.cts.map +1 -1
- package/dist/types/bucket.d.ts +194 -41
- package/dist/types/bucket.d.ts.map +1 -1
- package/dist/types/bucket.js +67 -12
- 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 +100 -16
- package/dist/types/download.d.cts.map +1 -1
- package/dist/types/download.d.ts +100 -16
- 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 +76 -229
- package/dist/types/encryption.cjs.map +1 -1
- package/dist/types/encryption.d.cts +48 -6
- package/dist/types/encryption.d.cts.map +1 -1
- package/dist/types/encryption.d.ts +48 -6
- package/dist/types/encryption.d.ts.map +1 -1
- package/dist/types/encryption.js +74 -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 +3 -0
- package/dist/types/file.cjs.map +1 -1
- package/dist/types/file.d.cts +140 -18
- package/dist/types/file.d.cts.map +1 -1
- package/dist/types/file.d.ts +140 -18
- package/dist/types/file.d.ts.map +1 -1
- package/dist/types/file.js +3 -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 +16 -12
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +16 -12
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/key.d.cts +4 -3
- package/dist/types/key.d.cts.map +1 -1
- package/dist/types/key.d.ts +4 -3
- package/dist/types/key.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 +31 -1
- package/dist/types/notifications.cjs.map +1 -1
- package/dist/types/notifications.d.cts +31 -4
- package/dist/types/notifications.d.cts.map +1 -1
- package/dist/types/notifications.d.ts +31 -4
- package/dist/types/notifications.d.ts.map +1 -1
- package/dist/types/notifications.js +30 -2
- 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 +386 -0
- package/dist/types/partner.d.cts.map +1 -0
- package/dist/types/partner.d.ts +386 -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 +35 -18
- package/dist/types/upload.d.cts.map +1 -1
- package/dist/types/upload.d.ts +35 -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 +15 -8
- package/dist/upload/large.cjs.map +1 -1
- package/dist/upload/large.d.cts +31 -8
- package/dist/upload/large.d.cts.map +1 -1
- package/dist/upload/large.d.ts +31 -8
- package/dist/upload/large.d.ts.map +1 -1
- package/dist/upload/large.js +15 -8
- package/dist/upload/large.js.map +1 -1
- package/dist/upload/options.cjs.map +1 -1
- package/dist/upload/options.d.cts +24 -0
- package/dist/upload/options.d.cts.map +1 -1
- package/dist/upload/options.d.ts +24 -0
- package/dist/upload/options.d.ts.map +1 -1
- package/dist/upload/options.js.map +1 -1
- package/dist/upload/resume.cjs +6 -4
- package/dist/upload/resume.cjs.map +1 -1
- package/dist/upload/resume.d.cts +9 -5
- package/dist/upload/resume.d.cts.map +1 -1
- package/dist/upload/resume.d.ts +9 -5
- package/dist/upload/resume.d.ts.map +1 -1
- package/dist/upload/resume.js +6 -4
- package/dist/upload/resume.js.map +1 -1
- package/dist/upload/retry.cjs +25 -13
- package/dist/upload/retry.cjs.map +1 -1
- package/dist/upload/retry.d.cts.map +1 -1
- package/dist/upload/retry.d.ts.map +1 -1
- package/dist/upload/retry.js +25 -13
- package/dist/upload/retry.js.map +1 -1
- package/dist/upload/single.cjs +35 -30
- package/dist/upload/single.cjs.map +1 -1
- package/dist/upload/single.d.cts +12 -0
- package/dist/upload/single.d.cts.map +1 -1
- package/dist/upload/single.d.ts +12 -0
- package/dist/upload/single.d.ts.map +1 -1
- package/dist/upload/single.js +35 -30
- package/dist/upload/single.js.map +1 -1
- package/dist/upload/stream.cjs +18 -5
- package/dist/upload/stream.cjs.map +1 -1
- package/dist/upload/stream.d.cts +12 -0
- package/dist/upload/stream.d.cts.map +1 -1
- package/dist/upload/stream.d.ts +12 -0
- package/dist/upload/stream.d.ts.map +1 -1
- package/dist/upload/stream.js +15 -2
- 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/dist/version.cjs +56 -4
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +53 -4
- package/dist/version.d.cts.map +1 -1
- package/dist/version.d.ts +53 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +52 -5
- package/dist/version.js.map +1 -1
- package/package.json +47 -18
package/dist/simulator/index.cjs
CHANGED
|
@@ -3,17 +3,97 @@ const require_file = require("../types/file.cjs");
|
|
|
3
3
|
const require_ids = require("../types/ids.cjs");
|
|
4
4
|
const require_text_codec = require("../util/text-codec.cjs");
|
|
5
5
|
const require_encoding = require("../raw/encoding.cjs");
|
|
6
|
+
const require_download = require("../types/download.cjs");
|
|
7
|
+
const require_md5 = require("../util/md5.cjs");
|
|
8
|
+
const require_encryption = require("../types/encryption.cjs");
|
|
9
|
+
const require_lock = require("../types/lock.cjs");
|
|
6
10
|
const require_crypto = require("../util/crypto.cjs");
|
|
7
11
|
const require_hash = require("../streams/hash.cjs");
|
|
8
12
|
const require_bucket = require("../types/bucket.cjs");
|
|
9
|
-
const require_encryption = require("../types/encryption.cjs");
|
|
10
13
|
const require_to_error = require("../util/to-error.cjs");
|
|
14
|
+
const require_url = require("../raw/url.cjs");
|
|
11
15
|
const require_auth = require("../types/auth.cjs");
|
|
16
|
+
const require_partner_capabilities = require("./partner-capabilities.cjs");
|
|
17
|
+
const require_partner = require("./partner.cjs");
|
|
12
18
|
const require_capabilities = require("./capabilities.cjs");
|
|
13
19
|
const require_b2_naming = require("../internal/b2-naming.cjs");
|
|
14
20
|
const require_validation = require("./validation.cjs");
|
|
15
21
|
//#region src/simulator/index.ts
|
|
16
|
-
var UPLOAD_TOKEN_SIGNING_KEY =
|
|
22
|
+
var UPLOAD_TOKEN_SIGNING_KEY = [
|
|
23
|
+
"b2 sdk typescript",
|
|
24
|
+
"simulator upload authorization",
|
|
25
|
+
"v1"
|
|
26
|
+
].join(":");
|
|
27
|
+
var SIMULATOR_MASTER_APPLICATION_KEY_ID = "master-key-id";
|
|
28
|
+
var SIMULATOR_MASTER_APPLICATION_KEY = "master-key";
|
|
29
|
+
var SIMULATOR_TEST_APPLICATION_KEY_ID = "test-key-id";
|
|
30
|
+
var SIMULATOR_TEST_APPLICATION_KEY = "test-key";
|
|
31
|
+
var SIMULATOR_APPLICATION_KEY_ID_PREFIX = "sim_key_";
|
|
32
|
+
var SIMULATOR_MASTER_CAPABILITIES = [
|
|
33
|
+
require_auth.Capability.ListBuckets,
|
|
34
|
+
require_auth.Capability.ListAllBucketNames,
|
|
35
|
+
require_auth.Capability.ReadBuckets,
|
|
36
|
+
require_auth.Capability.WriteBuckets,
|
|
37
|
+
require_auth.Capability.DeleteBuckets,
|
|
38
|
+
require_auth.Capability.ReadBucketEncryption,
|
|
39
|
+
require_auth.Capability.WriteBucketEncryption,
|
|
40
|
+
require_auth.Capability.ReadBucketReplications,
|
|
41
|
+
require_auth.Capability.WriteBucketReplications,
|
|
42
|
+
require_auth.Capability.ReadBucketNotifications,
|
|
43
|
+
require_auth.Capability.WriteBucketNotifications,
|
|
44
|
+
require_auth.Capability.ReadBucketLogging,
|
|
45
|
+
require_auth.Capability.WriteBucketLogging,
|
|
46
|
+
require_auth.Capability.ReadBucketLifecycleRules,
|
|
47
|
+
require_auth.Capability.WriteBucketLifecycleRules,
|
|
48
|
+
require_auth.Capability.ListFiles,
|
|
49
|
+
require_auth.Capability.ReadFiles,
|
|
50
|
+
require_auth.Capability.WriteFiles,
|
|
51
|
+
require_auth.Capability.DeleteFiles,
|
|
52
|
+
require_auth.Capability.ListKeys,
|
|
53
|
+
require_auth.Capability.WriteKeys,
|
|
54
|
+
require_auth.Capability.DeleteKeys,
|
|
55
|
+
require_auth.Capability.ShareFiles
|
|
56
|
+
];
|
|
57
|
+
function apiPathParts(path) {
|
|
58
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
59
|
+
const candidate = segments.at(-2);
|
|
60
|
+
const endpoint = segments.at(-1) ?? "";
|
|
61
|
+
const isB2ApiPath = segments.at(-3) === "b2api";
|
|
62
|
+
if (candidate !== void 0 && require_url.isB2ApiVersion(candidate)) {
|
|
63
|
+
if (candidate === "v3" || candidate === "v4") return {
|
|
64
|
+
endpoint,
|
|
65
|
+
version: candidate
|
|
66
|
+
};
|
|
67
|
+
if (isB2ApiPath) return {
|
|
68
|
+
endpoint,
|
|
69
|
+
version: "v3",
|
|
70
|
+
unsupportedVersion: candidate
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
endpoint,
|
|
75
|
+
version: "v3"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function parseBasicAuthorizationHeader(authzHeader) {
|
|
79
|
+
if (!authzHeader?.startsWith("Basic ")) return null;
|
|
80
|
+
const decoded = (() => {
|
|
81
|
+
try {
|
|
82
|
+
return atob(authzHeader.slice(6));
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
if (decoded === null) return null;
|
|
88
|
+
const idx = decoded.indexOf(":");
|
|
89
|
+
if (idx <= 0 || idx === decoded.length - 1) return null;
|
|
90
|
+
return {
|
|
91
|
+
applicationKeyId: decoded.slice(0, idx),
|
|
92
|
+
applicationKey: decoded.slice(idx + 1)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
var B2_MIN_PART_NUMBER = 1;
|
|
96
|
+
var B2_MAX_PART_NUMBER = 1e4;
|
|
17
97
|
function base64UrlEncode(bytes) {
|
|
18
98
|
let binary = "";
|
|
19
99
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
@@ -35,6 +115,11 @@ function timingSafeStringEqual(left, right) {
|
|
|
35
115
|
for (let i = 0; i < max; i++) mismatch |= (left.charCodeAt(i) || 0) ^ (right.charCodeAt(i) || 0);
|
|
36
116
|
return mismatch === 0;
|
|
37
117
|
}
|
|
118
|
+
var MS_PER_DAY = 864e5;
|
|
119
|
+
var MAX_JS_DATE_MS = 864e13;
|
|
120
|
+
function isValidRetentionTimestamp(timestamp, now = Date.now()) {
|
|
121
|
+
return Number.isFinite(timestamp) && timestamp <= MAX_JS_DATE_MS && timestamp > now;
|
|
122
|
+
}
|
|
38
123
|
/**
|
|
39
124
|
* Parse an RFC 7233 `Range` header value into inclusive start/end byte
|
|
40
125
|
* offsets clamped to the file size. Supports closed (`bytes=0-999`),
|
|
@@ -120,6 +205,56 @@ function compareB2FileNames(a, b) {
|
|
|
120
205
|
if (a > b) return 1;
|
|
121
206
|
return 0;
|
|
122
207
|
}
|
|
208
|
+
function compareFileVersionListEntries(a, b) {
|
|
209
|
+
const nameCmp = compareB2FileNames(a.fileName, b.fileName);
|
|
210
|
+
if (nameCmp !== 0) return nameCmp;
|
|
211
|
+
return b.uploadTimestamp - a.uploadTimestamp;
|
|
212
|
+
}
|
|
213
|
+
var DOWNLOAD_AUTH_TOKEN_BYTES = 32;
|
|
214
|
+
var DOWNLOAD_AUTH_TOKEN_PREFIX = "sim_dl_auth_";
|
|
215
|
+
var DOWNLOAD_AUTH_PURGE_BATCH_SIZE = 128;
|
|
216
|
+
var DOWNLOAD_RESPONSE_OVERRIDE_PARAMS = [
|
|
217
|
+
"b2ContentDisposition",
|
|
218
|
+
"b2ContentLanguage",
|
|
219
|
+
"b2ContentEncoding",
|
|
220
|
+
"b2ContentType",
|
|
221
|
+
"b2CacheControl",
|
|
222
|
+
"b2Expires"
|
|
223
|
+
];
|
|
224
|
+
var DOWNLOAD_RESPONSE_OVERRIDE_HEADERS = {
|
|
225
|
+
b2ContentDisposition: require_download.DownloadHeaderName.ContentDisposition,
|
|
226
|
+
b2ContentLanguage: require_download.DownloadHeaderName.ContentLanguage,
|
|
227
|
+
b2ContentEncoding: require_download.DownloadHeaderName.ContentEncoding,
|
|
228
|
+
b2ContentType: require_download.DownloadHeaderName.ContentType,
|
|
229
|
+
b2CacheControl: require_download.DownloadHeaderName.CacheControl,
|
|
230
|
+
b2Expires: require_download.DownloadHeaderName.Expires
|
|
231
|
+
};
|
|
232
|
+
function randomDownloadAuthorizationToken() {
|
|
233
|
+
const crypto = globalThis.crypto;
|
|
234
|
+
if (typeof crypto?.getRandomValues !== "function") throw new Error("Download authorization tokens require globalThis.crypto.getRandomValues");
|
|
235
|
+
const bytes = new Uint8Array(DOWNLOAD_AUTH_TOKEN_BYTES);
|
|
236
|
+
crypto.getRandomValues(bytes);
|
|
237
|
+
return `${DOWNLOAD_AUTH_TOKEN_PREFIX}${require_crypto.hexEncode(bytes)}`;
|
|
238
|
+
}
|
|
239
|
+
function pickDownloadResponseOverrides(req) {
|
|
240
|
+
const overrides = {};
|
|
241
|
+
for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
|
|
242
|
+
const value = req[param];
|
|
243
|
+
if (typeof value === "string") overrides[param] = value;
|
|
244
|
+
}
|
|
245
|
+
return overrides;
|
|
246
|
+
}
|
|
247
|
+
function validateDownloadResponseOverrides(req) {
|
|
248
|
+
for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
|
|
249
|
+
const value = req[param];
|
|
250
|
+
if (value === void 0 || typeof value === "string") continue;
|
|
251
|
+
return {
|
|
252
|
+
code: "bad_request",
|
|
253
|
+
message: `${param} must be a string`
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
123
258
|
function normalizeKeyBucketIds(req) {
|
|
124
259
|
return req.bucketIds === void 0 || req.bucketIds === null ? null : Object.freeze([...req.bucketIds]);
|
|
125
260
|
}
|
|
@@ -129,6 +264,18 @@ function singleBucketId(bucketIds) {
|
|
|
129
264
|
function cloneBucketIds(bucketIds) {
|
|
130
265
|
return bucketIds === null ? null : [...bucketIds];
|
|
131
266
|
}
|
|
267
|
+
function cloneCapabilities(capabilities) {
|
|
268
|
+
return [...capabilities];
|
|
269
|
+
}
|
|
270
|
+
function isImplicitStorageMasterCredential(credentials) {
|
|
271
|
+
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;
|
|
272
|
+
}
|
|
273
|
+
function isImplicitPartnerMasterCredential(credentials) {
|
|
274
|
+
return credentials.applicationKeyId === SIMULATOR_MASTER_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_MASTER_APPLICATION_KEY;
|
|
275
|
+
}
|
|
276
|
+
function isSimulatorApplicationKeyId(applicationKeyId) {
|
|
277
|
+
return applicationKeyId.startsWith(SIMULATOR_APPLICATION_KEY_ID_PREFIX);
|
|
278
|
+
}
|
|
132
279
|
function hasOwnField(body, field) {
|
|
133
280
|
return typeof body === "object" && body !== null && Object.hasOwn(body, field);
|
|
134
281
|
}
|
|
@@ -137,10 +284,347 @@ function requestStringField(body, field) {
|
|
|
137
284
|
const value = body[field];
|
|
138
285
|
return typeof value === "string" ? value : void 0;
|
|
139
286
|
}
|
|
287
|
+
function requestRecord(body) {
|
|
288
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
|
|
289
|
+
}
|
|
290
|
+
function replicationDestinationBucketIds(body) {
|
|
291
|
+
const rules = requestRecord(requestRecord(requestRecord(body)?.["replicationConfiguration"])?.["asReplicationSource"])?.["replicationRules"];
|
|
292
|
+
if (!Array.isArray(rules)) return [];
|
|
293
|
+
const bucketIds = [];
|
|
294
|
+
for (const rule of rules) {
|
|
295
|
+
const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
|
|
296
|
+
if (typeof destinationBucketId === "string") bucketIds.push(destinationBucketId);
|
|
297
|
+
}
|
|
298
|
+
return [...new Set(bucketIds)];
|
|
299
|
+
}
|
|
300
|
+
function normalizeReplicationConfiguration(config) {
|
|
301
|
+
return {
|
|
302
|
+
asReplicationSource: config.asReplicationSource ?? null,
|
|
303
|
+
asReplicationDestination: config.asReplicationDestination ?? null
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function wrapReplicationConfiguration(config) {
|
|
307
|
+
if (config === void 0) return {
|
|
308
|
+
isClientAuthorizedToRead: true,
|
|
309
|
+
value: null
|
|
310
|
+
};
|
|
311
|
+
const normalized = normalizeReplicationConfiguration(config);
|
|
312
|
+
return {
|
|
313
|
+
isClientAuthorizedToRead: true,
|
|
314
|
+
value: normalized.asReplicationSource === null && normalized.asReplicationDestination === null ? null : normalized
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function filterReadableReplicationConfiguration(config, canRead) {
|
|
318
|
+
return canRead ? {
|
|
319
|
+
isClientAuthorizedToRead: true,
|
|
320
|
+
value: config.value
|
|
321
|
+
} : {
|
|
322
|
+
isClientAuthorizedToRead: false,
|
|
323
|
+
value: null
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function readableFileRetention(value, canRead) {
|
|
327
|
+
return canRead ? {
|
|
328
|
+
isClientAuthorizedToRead: true,
|
|
329
|
+
value
|
|
330
|
+
} : {
|
|
331
|
+
isClientAuthorizedToRead: false,
|
|
332
|
+
value: null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function readableLegalHold(value, canRead) {
|
|
336
|
+
return canRead ? {
|
|
337
|
+
isClientAuthorizedToRead: true,
|
|
338
|
+
value
|
|
339
|
+
} : {
|
|
340
|
+
isClientAuthorizedToRead: false,
|
|
341
|
+
value: null
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function validateBucketConfigurationFields(fields, options) {
|
|
345
|
+
if (fields.bucketInfo !== void 0) {
|
|
346
|
+
const infoError = require_validation.validateBucketInfo(fields.bucketInfo);
|
|
347
|
+
if (infoError) return infoError;
|
|
348
|
+
}
|
|
349
|
+
if (fields.corsRules !== void 0) {
|
|
350
|
+
const corsError = require_validation.validateCorsRules(fields.corsRules);
|
|
351
|
+
if (corsError) return corsError;
|
|
352
|
+
}
|
|
353
|
+
if (fields.lifecycleRules !== void 0) {
|
|
354
|
+
const lifecycleError = require_validation.validateLifecycleRules(fields.lifecycleRules);
|
|
355
|
+
if (lifecycleError) return lifecycleError;
|
|
356
|
+
}
|
|
357
|
+
if (fields.defaultServerSideEncryption !== void 0) {
|
|
358
|
+
const encryptionError = validateBucketDefaultServerSideEncryption(fields.defaultServerSideEncryption);
|
|
359
|
+
if (encryptionError) return encryptionError;
|
|
360
|
+
}
|
|
361
|
+
if (fields.replicationConfiguration !== void 0) {
|
|
362
|
+
const replicationError = require_validation.validateReplicationConfiguration(fields.replicationConfiguration);
|
|
363
|
+
if (replicationError) return replicationError;
|
|
364
|
+
}
|
|
365
|
+
if (fields.defaultRetention !== void 0) {
|
|
366
|
+
const retentionError = require_validation.validateDefaultRetention(fields.defaultRetention);
|
|
367
|
+
if (retentionError) return retentionError;
|
|
368
|
+
if (requestRecord(fields.defaultRetention)?.["mode"] !== require_bucket.BucketRetentionMode.None && !options.objectLockEnabled) return {
|
|
369
|
+
code: "file_lock_not_enabled",
|
|
370
|
+
message: "Bucket must have Object Lock enabled to set defaultRetention"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
function validateBucketDefaultServerSideEncryption(encryption) {
|
|
376
|
+
const runtimeEncryption = requestRecord(encryption);
|
|
377
|
+
if (runtimeEncryption === null) return {
|
|
378
|
+
code: "bad_request",
|
|
379
|
+
message: "defaultServerSideEncryption must be an object"
|
|
380
|
+
};
|
|
381
|
+
if (hasCustomerEncryptionFields(runtimeEncryption)) return {
|
|
382
|
+
code: "bad_request",
|
|
383
|
+
message: "Bucket default server-side encryption cannot use SSE-C"
|
|
384
|
+
};
|
|
385
|
+
const mode = runtimeEncryption["mode"];
|
|
386
|
+
if (mode === require_encryption.EncryptionMode.None) return null;
|
|
387
|
+
if (mode === require_encryption.EncryptionMode.SseB2) {
|
|
388
|
+
if (runtimeEncryption["algorithm"] !== require_encryption.EncryptionAlgorithm.Aes256) return {
|
|
389
|
+
code: "bad_request",
|
|
390
|
+
message: "SSE-B2 algorithm must be AES256"
|
|
391
|
+
};
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
if (mode === require_encryption.EncryptionMode.SseC) return {
|
|
395
|
+
code: "bad_request",
|
|
396
|
+
message: "Bucket default server-side encryption cannot use SSE-C"
|
|
397
|
+
};
|
|
398
|
+
return {
|
|
399
|
+
code: "bad_request",
|
|
400
|
+
message: `Unsupported bucket default server-side encryption mode: ${String(mode)}`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function nextObjectLockEnabled(current, requested) {
|
|
404
|
+
if (requested === void 0) return current;
|
|
405
|
+
if (typeof requested !== "boolean") return {
|
|
406
|
+
code: "bad_request",
|
|
407
|
+
message: "fileLockEnabled must be a boolean"
|
|
408
|
+
};
|
|
409
|
+
if (current && requested === false) return {
|
|
410
|
+
code: "file_lock_conflict",
|
|
411
|
+
message: "Object Lock cannot be disabled"
|
|
412
|
+
};
|
|
413
|
+
return current || requested;
|
|
414
|
+
}
|
|
415
|
+
function requestHeaderValue(headers, name) {
|
|
416
|
+
const exact = headers[name];
|
|
417
|
+
if (exact !== void 0) return exact;
|
|
418
|
+
const lowerName = name.toLowerCase();
|
|
419
|
+
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === lowerName) return value;
|
|
420
|
+
}
|
|
421
|
+
function parseContentLengthHeader(headers) {
|
|
422
|
+
const header = requestHeaderValue(headers, "content-length");
|
|
423
|
+
if (header === void 0) return { kind: "absent" };
|
|
424
|
+
if (!/^\d+$/.test(header)) return {
|
|
425
|
+
kind: "error",
|
|
426
|
+
message: `Content-Length must be a byte count: ${header}`
|
|
427
|
+
};
|
|
428
|
+
const expectedLength = Number(header);
|
|
429
|
+
if (!Number.isSafeInteger(expectedLength)) return {
|
|
430
|
+
kind: "error",
|
|
431
|
+
message: `Content-Length is too large: ${header}`
|
|
432
|
+
};
|
|
433
|
+
return {
|
|
434
|
+
kind: "ok",
|
|
435
|
+
expectedLength
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function contentLengthMismatchMessage(expectedLength, actualLength) {
|
|
439
|
+
return `Content-Length ${expectedLength} does not match request body length ${actualLength}`;
|
|
440
|
+
}
|
|
140
441
|
function fileNames(...names) {
|
|
141
442
|
const present = names.filter((name) => name !== void 0);
|
|
142
443
|
return present.length > 0 ? present : void 0;
|
|
143
444
|
}
|
|
445
|
+
function queryParamsBody(params, endpoint) {
|
|
446
|
+
const metadata = jsonEndpointMetadata(endpoint);
|
|
447
|
+
const queryFields = new Set(metadata?.queryFields ?? []);
|
|
448
|
+
const numericFields = new Set(metadata?.numericQueryFields ?? []);
|
|
449
|
+
const body = Object.create(null);
|
|
450
|
+
for (const [key, value] of params) {
|
|
451
|
+
if (!queryFields.has(key)) continue;
|
|
452
|
+
if (body[key] !== void 0) continue;
|
|
453
|
+
body[key] = queryParamValue(value, numericFields.has(key));
|
|
454
|
+
}
|
|
455
|
+
return body;
|
|
456
|
+
}
|
|
457
|
+
function queryParamValue(value, numeric) {
|
|
458
|
+
if (!numeric || !/^-?\d+$/.test(value)) return value;
|
|
459
|
+
const numericValue = Number(value);
|
|
460
|
+
return Number.isSafeInteger(numericValue) ? numericValue : value;
|
|
461
|
+
}
|
|
462
|
+
var JSON_ENDPOINTS = {
|
|
463
|
+
b2_authorize_account: { methods: ["GET"] },
|
|
464
|
+
[require_partner_capabilities.PartnerEndpoint.ListGroups]: {
|
|
465
|
+
methods: ["GET"],
|
|
466
|
+
queryFields: [
|
|
467
|
+
"adminAccountId",
|
|
468
|
+
"groupName",
|
|
469
|
+
"startGroupId",
|
|
470
|
+
"maxGroupCount"
|
|
471
|
+
],
|
|
472
|
+
numericQueryFields: ["maxGroupCount"]
|
|
473
|
+
},
|
|
474
|
+
[require_partner_capabilities.PartnerEndpoint.ListGroupMembers]: {
|
|
475
|
+
methods: ["GET"],
|
|
476
|
+
queryFields: [
|
|
477
|
+
"adminAccountId",
|
|
478
|
+
"groupId",
|
|
479
|
+
"startEmail",
|
|
480
|
+
"maxMemberCount"
|
|
481
|
+
],
|
|
482
|
+
numericQueryFields: ["maxMemberCount"]
|
|
483
|
+
},
|
|
484
|
+
[require_partner_capabilities.PartnerEndpoint.ListComputers]: {
|
|
485
|
+
methods: ["GET"],
|
|
486
|
+
queryFields: [
|
|
487
|
+
"accountId",
|
|
488
|
+
"startComputerId",
|
|
489
|
+
"maxComputerCount"
|
|
490
|
+
],
|
|
491
|
+
numericQueryFields: ["maxComputerCount"]
|
|
492
|
+
},
|
|
493
|
+
b2_get_upload_url: {
|
|
494
|
+
methods: ["GET", "POST"],
|
|
495
|
+
queryFields: ["bucketId"]
|
|
496
|
+
},
|
|
497
|
+
b2_list_file_names: {
|
|
498
|
+
methods: ["GET", "POST"],
|
|
499
|
+
queryFields: [
|
|
500
|
+
"bucketId",
|
|
501
|
+
"delimiter",
|
|
502
|
+
"maxFileCount",
|
|
503
|
+
"prefix",
|
|
504
|
+
"startFileName"
|
|
505
|
+
],
|
|
506
|
+
numericQueryFields: ["maxFileCount"]
|
|
507
|
+
},
|
|
508
|
+
b2_list_file_versions: {
|
|
509
|
+
methods: ["GET", "POST"],
|
|
510
|
+
queryFields: [
|
|
511
|
+
"bucketId",
|
|
512
|
+
"delimiter",
|
|
513
|
+
"maxFileCount",
|
|
514
|
+
"prefix",
|
|
515
|
+
"startFileName",
|
|
516
|
+
"startFileId"
|
|
517
|
+
],
|
|
518
|
+
numericQueryFields: ["maxFileCount"]
|
|
519
|
+
},
|
|
520
|
+
b2_get_file_info: {
|
|
521
|
+
methods: ["GET", "POST"],
|
|
522
|
+
queryFields: ["fileId"]
|
|
523
|
+
},
|
|
524
|
+
b2_copy_file: {
|
|
525
|
+
methods: ["GET", "POST"],
|
|
526
|
+
queryFields: [
|
|
527
|
+
"sourceFileId",
|
|
528
|
+
"fileName",
|
|
529
|
+
"destinationBucketId",
|
|
530
|
+
"range",
|
|
531
|
+
"metadataDirective",
|
|
532
|
+
"contentType"
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
b2_get_upload_part_url: {
|
|
536
|
+
methods: ["GET", "POST"],
|
|
537
|
+
queryFields: ["fileId"]
|
|
538
|
+
},
|
|
539
|
+
b2_list_unfinished_large_files: {
|
|
540
|
+
methods: ["GET", "POST"],
|
|
541
|
+
queryFields: [
|
|
542
|
+
"bucketId",
|
|
543
|
+
"namePrefix",
|
|
544
|
+
"startFileId",
|
|
545
|
+
"maxFileCount"
|
|
546
|
+
],
|
|
547
|
+
numericQueryFields: ["maxFileCount"]
|
|
548
|
+
},
|
|
549
|
+
b2_list_parts: {
|
|
550
|
+
methods: ["GET", "POST"],
|
|
551
|
+
queryFields: [
|
|
552
|
+
"fileId",
|
|
553
|
+
"startPartNumber",
|
|
554
|
+
"maxPartCount"
|
|
555
|
+
],
|
|
556
|
+
numericQueryFields: ["startPartNumber", "maxPartCount"]
|
|
557
|
+
},
|
|
558
|
+
b2_copy_part: {
|
|
559
|
+
methods: ["GET", "POST"],
|
|
560
|
+
queryFields: [
|
|
561
|
+
"sourceFileId",
|
|
562
|
+
"largeFileId",
|
|
563
|
+
"partNumber",
|
|
564
|
+
"range"
|
|
565
|
+
],
|
|
566
|
+
numericQueryFields: ["partNumber"]
|
|
567
|
+
},
|
|
568
|
+
b2_get_download_authorization: {
|
|
569
|
+
methods: ["GET", "POST"],
|
|
570
|
+
queryFields: [
|
|
571
|
+
"bucketId",
|
|
572
|
+
"fileNamePrefix",
|
|
573
|
+
"validDurationInSeconds",
|
|
574
|
+
"b2ContentDisposition",
|
|
575
|
+
"b2ContentLanguage",
|
|
576
|
+
"b2ContentEncoding",
|
|
577
|
+
"b2ContentType",
|
|
578
|
+
"b2CacheControl",
|
|
579
|
+
"b2Expires"
|
|
580
|
+
],
|
|
581
|
+
numericQueryFields: ["validDurationInSeconds"]
|
|
582
|
+
},
|
|
583
|
+
b2_list_keys: {
|
|
584
|
+
methods: ["GET", "POST"],
|
|
585
|
+
queryFields: [
|
|
586
|
+
"accountId",
|
|
587
|
+
"maxKeyCount",
|
|
588
|
+
"startApplicationKeyId"
|
|
589
|
+
],
|
|
590
|
+
numericQueryFields: ["maxKeyCount"]
|
|
591
|
+
},
|
|
592
|
+
b2_get_bucket_notification_rules: {
|
|
593
|
+
methods: ["GET", "POST"],
|
|
594
|
+
queryFields: ["bucketId"]
|
|
595
|
+
},
|
|
596
|
+
[require_partner_capabilities.PartnerEndpoint.CreateGroupMember]: { methods: ["POST"] },
|
|
597
|
+
[require_partner_capabilities.PartnerEndpoint.EjectGroupMember]: { methods: ["POST"] },
|
|
598
|
+
[require_partner_capabilities.PartnerEndpoint.ReserveTrialCreateAccount]: { methods: ["POST"] },
|
|
599
|
+
[require_partner_capabilities.PartnerEndpoint.DeleteComputer]: { methods: ["POST"] },
|
|
600
|
+
b2_create_bucket: { methods: ["POST"] },
|
|
601
|
+
b2_list_buckets: { methods: ["POST"] },
|
|
602
|
+
b2_delete_bucket: { methods: ["POST"] },
|
|
603
|
+
b2_update_bucket: { methods: ["POST"] },
|
|
604
|
+
b2_hide_file: { methods: ["POST"] },
|
|
605
|
+
b2_delete_file_version: { methods: ["POST"] },
|
|
606
|
+
b2_start_large_file: { methods: ["POST"] },
|
|
607
|
+
b2_finish_large_file: { methods: ["POST"] },
|
|
608
|
+
b2_cancel_large_file: { methods: ["POST"] },
|
|
609
|
+
b2_create_key: { methods: ["POST"] },
|
|
610
|
+
b2_delete_key: { methods: ["POST"] },
|
|
611
|
+
b2_update_file_retention: { methods: ["POST"] },
|
|
612
|
+
b2_update_file_legal_hold: { methods: ["POST"] },
|
|
613
|
+
b2_set_bucket_notification_rules: { methods: ["POST"] }
|
|
614
|
+
};
|
|
615
|
+
function jsonEndpointMetadata(endpoint) {
|
|
616
|
+
return JSON_ENDPOINTS[endpoint];
|
|
617
|
+
}
|
|
618
|
+
function jsonEndpointAllowsMethod(method, endpoint) {
|
|
619
|
+
const metadata = jsonEndpointMetadata(endpoint);
|
|
620
|
+
if (metadata === void 0) return true;
|
|
621
|
+
const normalizedMethod = method.toUpperCase();
|
|
622
|
+
if (normalizedMethod !== "GET" && normalizedMethod !== "POST") return false;
|
|
623
|
+
return metadata.methods.includes(normalizedMethod);
|
|
624
|
+
}
|
|
625
|
+
function isJsonQueryEndpoint(endpoint) {
|
|
626
|
+
return jsonEndpointMetadata(endpoint)?.queryFields !== void 0;
|
|
627
|
+
}
|
|
144
628
|
function notificationRulePrefixes(body) {
|
|
145
629
|
if (typeof body !== "object" || body === null) return void 0;
|
|
146
630
|
const rules = body["eventNotificationRules"];
|
|
@@ -157,6 +641,13 @@ function storedNotificationRulePrefixes(rules) {
|
|
|
157
641
|
function hasKeyManagementCapability(capabilities) {
|
|
158
642
|
return capabilities.some((capability) => capability === require_auth.Capability.ListKeys || capability === require_auth.Capability.WriteKeys || capability === require_auth.Capability.DeleteKeys);
|
|
159
643
|
}
|
|
644
|
+
function missingStoredKeyCapabilities(key, required) {
|
|
645
|
+
const capabilities = new Set(key.capabilities);
|
|
646
|
+
return required.filter((capability) => !capabilities.has(capability));
|
|
647
|
+
}
|
|
648
|
+
function storedKeyAllowsBucket(key, bucketId) {
|
|
649
|
+
return key.bucketIds === null || key.bucketIds.includes(bucketId);
|
|
650
|
+
}
|
|
160
651
|
function publicServerSideEncryption(encryption) {
|
|
161
652
|
if (encryption.mode === require_encryption.EncryptionMode.SseC) return {
|
|
162
653
|
mode: encryption.mode,
|
|
@@ -168,27 +659,212 @@ function publicServerSideEncryption(encryption) {
|
|
|
168
659
|
};
|
|
169
660
|
return encryption;
|
|
170
661
|
}
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
662
|
+
function publicBucketDefaultServerSideEncryption(encryption) {
|
|
663
|
+
return {
|
|
664
|
+
isClientAuthorizedToRead: true,
|
|
665
|
+
value: publicBucketDefaultServerSideEncryptionValue(encryption)
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function publicBucketDefaultServerSideEncryptionValue(encryption) {
|
|
669
|
+
if (encryption?.mode === require_encryption.EncryptionMode.SseB2) return encryption;
|
|
670
|
+
return {
|
|
671
|
+
mode: null,
|
|
672
|
+
algorithm: null
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function bucketDefaultServerSideEncryptionForStorage(info) {
|
|
676
|
+
const encryption = info.defaultServerSideEncryption;
|
|
677
|
+
if (!encryption.isClientAuthorizedToRead) return {
|
|
678
|
+
mode: null,
|
|
679
|
+
algorithm: null
|
|
680
|
+
};
|
|
681
|
+
return encryption.value ?? {
|
|
682
|
+
mode: null,
|
|
683
|
+
algorithm: null
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function customerKeyDigest(customerKey) {
|
|
687
|
+
let first = 2166136261;
|
|
688
|
+
let second = 668265261;
|
|
689
|
+
for (const byte of customerKey) {
|
|
690
|
+
first = Math.imul(first ^ byte, 16777619) >>> 0;
|
|
691
|
+
second = Math.imul(second ^ byte, 2246822507) >>> 0;
|
|
692
|
+
}
|
|
693
|
+
return `${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}`;
|
|
694
|
+
}
|
|
695
|
+
var BASE64_ALPHABET = /^[A-Za-z0-9+/]+$/;
|
|
696
|
+
function normalizeSizedBase64(value, decodedByteLength) {
|
|
697
|
+
const paddedLength = Math.ceil(decodedByteLength / 3) * 4;
|
|
698
|
+
const noPaddingLength = Math.ceil(decodedByteLength * 8 / 6);
|
|
699
|
+
const padding = "=".repeat(paddedLength - noPaddingLength);
|
|
700
|
+
if (value.length === noPaddingLength && BASE64_ALPHABET.test(value)) return value.padEnd(paddedLength, "=");
|
|
701
|
+
if (value.length === paddedLength && value.endsWith(padding) && BASE64_ALPHABET.test(value.slice(0, noPaddingLength))) return value;
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
function base64ToBytes(value, decodedByteLength) {
|
|
705
|
+
const normalized = normalizeSizedBase64(value, decodedByteLength);
|
|
706
|
+
if (normalized === null) return null;
|
|
707
|
+
try {
|
|
708
|
+
const decoded = atob(normalized);
|
|
709
|
+
if (decoded.length !== decodedByteLength) return null;
|
|
710
|
+
const bytes = new Uint8Array(decoded.length);
|
|
711
|
+
for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
|
|
712
|
+
return bytes;
|
|
713
|
+
} catch {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function encryptionValidationError(message) {
|
|
718
|
+
return {
|
|
719
|
+
status: 400,
|
|
720
|
+
body: {
|
|
721
|
+
status: 400,
|
|
722
|
+
code: "bad_request",
|
|
723
|
+
message
|
|
724
|
+
}
|
|
177
725
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
726
|
+
}
|
|
727
|
+
function sameStoredServerSideEncryption(a, b) {
|
|
728
|
+
if (a.mode !== b.mode) return false;
|
|
729
|
+
if (a.mode === require_encryption.EncryptionMode.None) return true;
|
|
730
|
+
if (a.mode === require_encryption.EncryptionMode.SseB2 && b.mode === require_encryption.EncryptionMode.SseB2) return a.algorithm === b.algorithm;
|
|
731
|
+
if (a.mode === require_encryption.EncryptionMode.SseC && b.mode === require_encryption.EncryptionMode.SseC) return a.algorithm === b.algorithm && a.customerKeyDigest === b.customerKeyDigest && a.customerKeyMd5 === b.customerKeyMd5;
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
function hasCustomerEncryptionFields(encryption) {
|
|
735
|
+
return encryption["customerKey"] !== void 0 || encryption["customerKeyMd5"] !== void 0 || encryption["customerAlgorithm"] !== void 0;
|
|
736
|
+
}
|
|
737
|
+
function hasCustomerEncryptionHeaders(headers) {
|
|
738
|
+
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;
|
|
739
|
+
}
|
|
740
|
+
function isExactNoEncryptionWireSetting(encryption) {
|
|
741
|
+
return encryption["mode"] === null && encryption["algorithm"] === null && Object.keys(encryption).length === 2;
|
|
742
|
+
}
|
|
743
|
+
async function storedServerSideEncryption(encryption) {
|
|
744
|
+
const runtimeEncryption = encryption;
|
|
745
|
+
if (encryption.mode === null) {
|
|
746
|
+
if (isExactNoEncryptionWireSetting(runtimeEncryption)) return { mode: require_encryption.EncryptionMode.None };
|
|
747
|
+
if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("No-encryption settings must not include SSE-C customer keys");
|
|
748
|
+
return encryptionValidationError("No-encryption wire settings must be exactly { mode: null, algorithm: null }");
|
|
749
|
+
}
|
|
750
|
+
if (encryption.mode === require_encryption.EncryptionMode.SseC) {
|
|
751
|
+
if (runtimeEncryption["algorithm"] === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
|
|
752
|
+
if (encryption.algorithm !== require_encryption.EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
|
|
753
|
+
const customerKey = "customerKey" in encryption ? encryption.customerKey : void 0;
|
|
754
|
+
const customerKeyMd5 = "customerKeyMd5" in encryption ? encryption.customerKeyMd5 : void 0;
|
|
755
|
+
if (typeof customerKey !== "string" || customerKey === "") return encryptionValidationError("SSE-C customer key is required");
|
|
756
|
+
if (typeof customerKeyMd5 !== "string" || customerKeyMd5 === "") return encryptionValidationError("SSE-C customer key MD5 is required");
|
|
757
|
+
const customerKeyBytes = base64ToBytes(customerKey, 32);
|
|
758
|
+
if (customerKeyBytes === null) return encryptionValidationError("SSE-C customer key must be a base64-encoded 256-bit key");
|
|
759
|
+
const normalizedCustomerKeyMd5 = normalizeSizedBase64(customerKeyMd5, 16);
|
|
760
|
+
if (normalizedCustomerKeyMd5 === null) return encryptionValidationError("SSE-C customer key MD5 must be a base64-encoded 128-bit digest");
|
|
761
|
+
if (await require_md5.md5Base64(customerKeyBytes) !== normalizedCustomerKeyMd5) return encryptionValidationError("SSE-C customer key MD5 does not match the key");
|
|
762
|
+
return {
|
|
763
|
+
mode: encryption.mode,
|
|
764
|
+
algorithm: encryption.algorithm,
|
|
765
|
+
customerKeyDigest: customerKeyDigest(customerKeyBytes),
|
|
766
|
+
customerKeyMd5: normalizedCustomerKeyMd5
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (encryption.mode === require_encryption.EncryptionMode.SseB2) {
|
|
770
|
+
if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
|
|
771
|
+
if (encryption.algorithm !== require_encryption.EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
|
|
772
|
+
return {
|
|
773
|
+
mode: encryption.mode,
|
|
774
|
+
algorithm: encryption.algorithm
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (encryption.mode === require_encryption.EncryptionMode.None) {
|
|
778
|
+
if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("No-encryption settings must not include SSE-C customer keys");
|
|
779
|
+
return { mode: require_encryption.EncryptionMode.None };
|
|
780
|
+
}
|
|
781
|
+
return encryptionValidationError(`Unsupported server-side encryption mode: ${String(runtimeEncryption["mode"])}`);
|
|
782
|
+
}
|
|
783
|
+
async function uploadServerSideEncryption(headers, fallback) {
|
|
784
|
+
const customerAlgorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
|
|
785
|
+
const customerKey = headers["x-bz-server-side-encryption-customer-key"];
|
|
786
|
+
const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
|
|
787
|
+
const managedEncryption = headers["x-bz-server-side-encryption"];
|
|
788
|
+
if (hasCustomerEncryptionHeaders(headers)) {
|
|
789
|
+
if (managedEncryption !== void 0) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
|
|
790
|
+
if (customerAlgorithm === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
|
|
791
|
+
if (customerAlgorithm !== require_encryption.EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
|
|
792
|
+
if (!customerKey || !customerKeyMd5) return encryptionValidationError("SSE-C customer key and MD5 are required");
|
|
793
|
+
return await storedServerSideEncryption({
|
|
794
|
+
mode: require_encryption.EncryptionMode.SseC,
|
|
795
|
+
algorithm: require_encryption.EncryptionAlgorithm.Aes256,
|
|
796
|
+
customerKey,
|
|
797
|
+
customerKeyMd5
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (managedEncryption !== void 0) {
|
|
801
|
+
if (managedEncryption !== require_encryption.EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
|
|
802
|
+
return {
|
|
803
|
+
mode: require_encryption.EncryptionMode.SseB2,
|
|
804
|
+
algorithm: require_encryption.EncryptionAlgorithm.Aes256
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return await storedServerSideEncryption(fallback);
|
|
808
|
+
}
|
|
809
|
+
function noBucketDefaultRetention() {
|
|
810
|
+
return {
|
|
811
|
+
mode: null,
|
|
812
|
+
period: null
|
|
181
813
|
};
|
|
182
|
-
|
|
814
|
+
}
|
|
815
|
+
function bucketDefaultRetentionForResponse(policy) {
|
|
816
|
+
if (policy === void 0 || policy.mode === require_bucket.BucketRetentionMode.None) return noBucketDefaultRetention();
|
|
817
|
+
return policy;
|
|
818
|
+
}
|
|
819
|
+
function bucketDefaultRetention(info) {
|
|
820
|
+
return info.fileLockConfiguration.value?.defaultRetention;
|
|
183
821
|
}
|
|
184
822
|
function defaultFileRetention(policy, uploadTimestamp) {
|
|
823
|
+
if (policy === void 0) return null;
|
|
824
|
+
if (policy.mode === null) return null;
|
|
185
825
|
if (policy.mode === require_bucket.BucketRetentionMode.None || policy.period === null) return null;
|
|
186
|
-
const
|
|
826
|
+
const retainUntilTimestamp = uploadTimestamp + (policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365) * MS_PER_DAY;
|
|
827
|
+
if (!isValidRetentionTimestamp(retainUntilTimestamp, uploadTimestamp)) return null;
|
|
187
828
|
return {
|
|
188
829
|
mode: policy.mode,
|
|
189
|
-
retainUntilTimestamp
|
|
830
|
+
retainUntilTimestamp
|
|
190
831
|
};
|
|
191
832
|
}
|
|
833
|
+
function isRetentionMode(value) {
|
|
834
|
+
return value === require_lock.RetentionMode.Compliance || value === require_lock.RetentionMode.Governance;
|
|
835
|
+
}
|
|
836
|
+
function parseFileRetentionValue(value, now) {
|
|
837
|
+
if (typeof value !== "object" || value === null) return null;
|
|
838
|
+
const candidate = value;
|
|
839
|
+
const mode = candidate["mode"];
|
|
840
|
+
const retainUntilTimestamp = candidate["retainUntilTimestamp"];
|
|
841
|
+
if (mode === null && retainUntilTimestamp === null) return {
|
|
842
|
+
mode,
|
|
843
|
+
retainUntilTimestamp
|
|
844
|
+
};
|
|
845
|
+
if (isRetentionMode(mode) && typeof retainUntilTimestamp === "number") {
|
|
846
|
+
if (!isValidRetentionTimestamp(retainUntilTimestamp, now)) return null;
|
|
847
|
+
return {
|
|
848
|
+
mode,
|
|
849
|
+
retainUntilTimestamp
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
function isRetentionWeakened(current, next) {
|
|
855
|
+
if (current === null || current === void 0) return false;
|
|
856
|
+
if (current.mode === null) return false;
|
|
857
|
+
if (next.mode === null) return true;
|
|
858
|
+
if (current.mode === require_lock.RetentionMode.Compliance && next.mode !== require_lock.RetentionMode.Compliance) return true;
|
|
859
|
+
if (current.retainUntilTimestamp === null) return next.mode !== current.mode;
|
|
860
|
+
if (next.retainUntilTimestamp === null) return true;
|
|
861
|
+
return next.retainUntilTimestamp < current.retainUntilTimestamp;
|
|
862
|
+
}
|
|
863
|
+
function requiresGovernanceBypass(current, next) {
|
|
864
|
+
if (current?.mode !== require_lock.RetentionMode.Governance) return false;
|
|
865
|
+
if (next.mode === require_lock.RetentionMode.Compliance) return true;
|
|
866
|
+
return isRetentionWeakened(current, next);
|
|
867
|
+
}
|
|
192
868
|
/**
|
|
193
869
|
* In-memory B2 simulator for testing. Implements the B2 native API at the
|
|
194
870
|
* request/response level without any network I/O. Supports 25+ operations
|
|
@@ -204,6 +880,15 @@ function defaultFileRetention(policy, uploadTimestamp) {
|
|
|
204
880
|
* })
|
|
205
881
|
* await client.authorize()
|
|
206
882
|
* ```
|
|
883
|
+
*
|
|
884
|
+
* `test-key-id:test-key` is the documented implicit full-access
|
|
885
|
+
* storage credential. `master-key-id:master-key` is also accepted for
|
|
886
|
+
* tests that share storage and Partner simulator credentials. In the
|
|
887
|
+
* default permissive mode, other well-formed Basic credentials whose
|
|
888
|
+
* applicationKeyId was not minted by `b2_create_key` receive the same
|
|
889
|
+
* implicit master grant for backward compatibility. A simulator-created
|
|
890
|
+
* key always authorizes with its own capabilities, bucket restrictions,
|
|
891
|
+
* name prefix, and expiration.
|
|
207
892
|
*/
|
|
208
893
|
var B2Simulator = class {
|
|
209
894
|
buckets = /* @__PURE__ */ new Map();
|
|
@@ -220,6 +905,8 @@ var B2Simulator = class {
|
|
|
220
905
|
onHookError;
|
|
221
906
|
strictAuth;
|
|
222
907
|
authTokenTtlMs;
|
|
908
|
+
customUploadTimestampsEnabled;
|
|
909
|
+
partner;
|
|
223
910
|
/**
|
|
224
911
|
* Issued auth tokens with their associated grant scope + expiry. In
|
|
225
912
|
* permissive mode (`strictAuth: false`) this is still populated by
|
|
@@ -235,6 +922,8 @@ var B2Simulator = class {
|
|
|
235
922
|
* state for tests.
|
|
236
923
|
*/
|
|
237
924
|
uploadTokens = /* @__PURE__ */ new Map();
|
|
925
|
+
downloadAuthorizationTokens = /* @__PURE__ */ new Map();
|
|
926
|
+
downloadAuthorizationExpiryHeap = [];
|
|
238
927
|
/**
|
|
239
928
|
* Virtual-clock offset applied to `Date.now()` for expiry checks.
|
|
240
929
|
* Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
|
|
@@ -278,7 +967,23 @@ var B2Simulator = class {
|
|
|
278
967
|
if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
|
|
279
968
|
if (options.onHookError !== void 0) this.onHookError = options.onHookError;
|
|
280
969
|
this.strictAuth = options.strictAuth ?? false;
|
|
281
|
-
this.
|
|
970
|
+
this.customUploadTimestampsEnabled = options.customUploadTimestampsEnabled ?? false;
|
|
971
|
+
this.authTokenTtlMs = options.authTokenTtlMs ?? 864e5;
|
|
972
|
+
this.partner = new require_partner.PartnerSimulator({
|
|
973
|
+
accountId: this.accountId,
|
|
974
|
+
authTokenTtlMs: this.authTokenTtlMs,
|
|
975
|
+
minimumPartSize: this.minimumPartSize,
|
|
976
|
+
recommendedPartSize: this.recommendedPartSize,
|
|
977
|
+
canAuthorize: (authzHeader) => this.canAuthorizePartner(authzHeader),
|
|
978
|
+
createBucket: (req) => this.createBucket(req),
|
|
979
|
+
error: (status, code, message) => this.error(status, code, message),
|
|
980
|
+
genId: (prefix) => this.genId(prefix),
|
|
981
|
+
monotonicTimestamp: () => this.monotonicTimestamp(),
|
|
982
|
+
now: () => this.now(),
|
|
983
|
+
storeKey: (key) => {
|
|
984
|
+
this.keys.set(key.applicationKeyId, key);
|
|
985
|
+
}
|
|
986
|
+
}, options);
|
|
282
987
|
}
|
|
283
988
|
/**
|
|
284
989
|
* Advance the simulator's virtual clock by `ms` milliseconds. Used in
|
|
@@ -336,6 +1041,9 @@ var B2Simulator = class {
|
|
|
336
1041
|
now() {
|
|
337
1042
|
return Date.now() + this.clockOffsetMs;
|
|
338
1043
|
}
|
|
1044
|
+
isStoredKeyExpired(key) {
|
|
1045
|
+
return key.expirationTimestamp !== null && key.expirationTimestamp <= this.now();
|
|
1046
|
+
}
|
|
339
1047
|
pruneExpiredUploadTokens(now = this.now()) {
|
|
340
1048
|
for (const [authorizationToken, token] of this.uploadTokens.entries()) if (now >= token.cleanupAt) this.uploadTokens.delete(authorizationToken);
|
|
341
1049
|
}
|
|
@@ -459,15 +1167,205 @@ var B2Simulator = class {
|
|
|
459
1167
|
const token = this.issuedTokens.get(authToken);
|
|
460
1168
|
if (!token) return this.error(401, "bad_auth_token", "unknown auth token");
|
|
461
1169
|
if (this.now() > token.expiresAt) return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
|
|
1170
|
+
if (token.applicationKeyId !== null) {
|
|
1171
|
+
const key = this.keys.get(token.applicationKeyId);
|
|
1172
|
+
if (key === void 0) return this.error(401, "bad_auth_token", "application key has been deleted; reauthorize");
|
|
1173
|
+
if (this.isStoredKeyExpired(key)) return this.error(401, "expired_auth_token", "application key has expired; reauthorize");
|
|
1174
|
+
}
|
|
462
1175
|
const missing = require_capabilities.missingCapabilitiesFor(endpoint, token.capabilities);
|
|
463
1176
|
if (missing.length > 0) return this.error(403, "unauthorized", `application key lacks required capabilities: ${missing.join(", ")}`);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1177
|
+
return this.authorizeScopeGrant(scope, {
|
|
1178
|
+
bucketIds: token.bucketIds,
|
|
1179
|
+
namePrefix: token.namePrefix,
|
|
1180
|
+
bucketScopeRequiredMessage: () => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; bucket scope is required`,
|
|
1181
|
+
bucketMismatchMessage: (bucketId) => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; cannot access ${bucketId}`,
|
|
1182
|
+
prefixMismatchMessage: (fileName) => `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
authorizeScopeGrant(scope, grant) {
|
|
1186
|
+
if (grant.bucketIds !== null) {
|
|
1187
|
+
if (scope?.requiresAccountLevelBucketAccess === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
|
|
1188
|
+
if ((scope?.bucketIds.length ?? 0) === 0 && scope?.requiresBucketScope === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
|
|
1189
|
+
for (const bucketId of scope?.bucketIds ?? []) {
|
|
1190
|
+
if (grant.bucketIds.includes(bucketId)) continue;
|
|
1191
|
+
return this.error(403, "unauthorized", grant.bucketMismatchMessage(bucketId));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (grant.namePrefix !== null) for (const fileName of scope?.fileNames ?? []) {
|
|
1195
|
+
if (fileName.startsWith(grant.namePrefix)) continue;
|
|
1196
|
+
return this.error(403, "unauthorized", grant.prefixMismatchMessage(fileName));
|
|
1197
|
+
}
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
authorizeDownloadRequest(authToken, endpoint, scope, url) {
|
|
1201
|
+
if (authToken === void 0 || authToken === "") return this.error(401, "bad_auth_token", "missing authorization token");
|
|
1202
|
+
const downloadAuth = this.downloadAuthorizationTokens.get(authToken);
|
|
1203
|
+
if (downloadAuth === void 0) {
|
|
1204
|
+
this.purgeExpiredDownloadAuthorizationTokens();
|
|
1205
|
+
return this.authorizeRequest(authToken, endpoint, scope);
|
|
1206
|
+
}
|
|
1207
|
+
if (this.isExpiredDownloadAuthorizationToken(downloadAuth)) {
|
|
1208
|
+
this.downloadAuthorizationTokens.delete(authToken);
|
|
1209
|
+
this.purgeExpiredDownloadAuthorizationTokens();
|
|
1210
|
+
return this.error(401, "expired_auth_token", "download authorization token has expired");
|
|
1211
|
+
}
|
|
1212
|
+
if (endpoint === "b2_download_file_by_id") return this.error(403, "unauthorized", "download authorization tokens cannot be used with b2_download_file_by_id");
|
|
1213
|
+
const overrideError = this.authorizeDownloadResponseOverrides(downloadAuth, url);
|
|
1214
|
+
if (overrideError !== null) return overrideError;
|
|
1215
|
+
return this.authorizeScopeGrant(scope, {
|
|
1216
|
+
bucketIds: [downloadAuth.bucketId],
|
|
1217
|
+
namePrefix: downloadAuth.fileNamePrefix,
|
|
1218
|
+
bucketScopeRequiredMessage: () => `download authorization is scoped to bucket ${downloadAuth.bucketId}; bucket scope is required`,
|
|
1219
|
+
bucketMismatchMessage: (bucketId) => `download authorization is scoped to bucket ${downloadAuth.bucketId}; cannot access ${bucketId}`,
|
|
1220
|
+
prefixMismatchMessage: (fileName) => `download authorization is scoped to prefix "${downloadAuth.fileNamePrefix}"; "${fileName}" is outside scope`
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
isExpiredDownloadAuthorizationToken(token, now = this.now()) {
|
|
1224
|
+
return now >= token.expiresAt;
|
|
1225
|
+
}
|
|
1226
|
+
purgeExpiredDownloadAuthorizationTokens() {
|
|
1227
|
+
const now = this.now();
|
|
1228
|
+
for (let i = 0; i < DOWNLOAD_AUTH_PURGE_BATCH_SIZE; i++) {
|
|
1229
|
+
const next = this.downloadAuthorizationExpiryHeap[0];
|
|
1230
|
+
if (next === void 0 || next.expiresAt > now) return;
|
|
1231
|
+
this.popDownloadAuthorizationExpiry();
|
|
1232
|
+
if (this.downloadAuthorizationTokens.get(next.token)?.expiresAt === next.expiresAt) this.downloadAuthorizationTokens.delete(next.token);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
pushDownloadAuthorizationExpiry(entry) {
|
|
1236
|
+
const heap = this.downloadAuthorizationExpiryHeap;
|
|
1237
|
+
heap.push(entry);
|
|
1238
|
+
let idx = heap.length - 1;
|
|
1239
|
+
while (idx > 0) {
|
|
1240
|
+
const parentIdx = Math.floor((idx - 1) / 2);
|
|
1241
|
+
const parent = heap[parentIdx];
|
|
1242
|
+
if (parent.expiresAt <= entry.expiresAt) break;
|
|
1243
|
+
heap[idx] = parent;
|
|
1244
|
+
idx = parentIdx;
|
|
1245
|
+
}
|
|
1246
|
+
heap[idx] = entry;
|
|
1247
|
+
}
|
|
1248
|
+
popDownloadAuthorizationExpiry() {
|
|
1249
|
+
const heap = this.downloadAuthorizationExpiryHeap;
|
|
1250
|
+
const root = heap[0];
|
|
1251
|
+
const last = heap.pop();
|
|
1252
|
+
if (root === void 0 || last === void 0 || heap.length === 0) return root;
|
|
1253
|
+
let idx = 0;
|
|
1254
|
+
while (true) {
|
|
1255
|
+
const leftIdx = idx * 2 + 1;
|
|
1256
|
+
const rightIdx = leftIdx + 1;
|
|
1257
|
+
const left = heap[leftIdx];
|
|
1258
|
+
const right = heap[rightIdx];
|
|
1259
|
+
if (left === void 0) break;
|
|
1260
|
+
const childIdx = right !== void 0 && right.expiresAt < left.expiresAt ? rightIdx : leftIdx;
|
|
1261
|
+
const child = heap[childIdx];
|
|
1262
|
+
if (child.expiresAt >= last.expiresAt) break;
|
|
1263
|
+
heap[idx] = child;
|
|
1264
|
+
idx = childIdx;
|
|
467
1265
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1266
|
+
heap[idx] = last;
|
|
1267
|
+
return root;
|
|
1268
|
+
}
|
|
1269
|
+
authorizeDownloadResponseOverrides(downloadAuth, url) {
|
|
1270
|
+
for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
|
|
1271
|
+
const expected = downloadAuth.responseHeaderOverrides[param];
|
|
1272
|
+
if (expected === void 0) continue;
|
|
1273
|
+
const actual = url.searchParams.get(param);
|
|
1274
|
+
if (actual === expected) continue;
|
|
1275
|
+
const expectedText = JSON.stringify(expected);
|
|
1276
|
+
const actualText = actual === null ? "absent" : JSON.stringify(actual);
|
|
1277
|
+
return this.error(403, "unauthorized", `download authorization requires ${param}=${expectedText}; got ${actualText}`);
|
|
1278
|
+
}
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
downloadAuthorizationFromRequest(headers, url, options = {}) {
|
|
1282
|
+
const headerToken = requestHeaderValue(headers, "authorization");
|
|
1283
|
+
if (headerToken !== void 0) return headerToken;
|
|
1284
|
+
if (options.allowQueryAuthorization !== true) return void 0;
|
|
1285
|
+
const queryToken = url.searchParams.get("Authorization") ?? url.searchParams.get("authorization");
|
|
1286
|
+
return queryToken?.startsWith(DOWNLOAD_AUTH_TOKEN_PREFIX) ? queryToken : void 0;
|
|
1287
|
+
}
|
|
1288
|
+
requestHasCapability(authToken, capability) {
|
|
1289
|
+
if (!this.strictAuth) return true;
|
|
1290
|
+
if (authToken === void 0) return false;
|
|
1291
|
+
return this.issuedTokens.get(authToken)?.capabilities.includes(capability) === true;
|
|
1292
|
+
}
|
|
1293
|
+
bucketInfoForResponse(info, authToken) {
|
|
1294
|
+
return {
|
|
1295
|
+
...info,
|
|
1296
|
+
defaultServerSideEncryption: this.requestHasCapability(authToken, require_auth.Capability.ReadBucketEncryption) ? info.defaultServerSideEncryption : {
|
|
1297
|
+
isClientAuthorizedToRead: false,
|
|
1298
|
+
value: null
|
|
1299
|
+
},
|
|
1300
|
+
replicationConfiguration: filterReadableReplicationConfiguration(info.replicationConfiguration, this.requestHasCapability(authToken, require_auth.Capability.ReadBucketReplications))
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
requireFileLockEnabled(bucket) {
|
|
1304
|
+
if (bucket.info.fileLockConfiguration.value?.isFileLockEnabled === true) return null;
|
|
1305
|
+
return this.error(400, "file_lock_not_enabled", "Bucket does not have file lock enabled");
|
|
1306
|
+
}
|
|
1307
|
+
requireGovernanceBypass(req, authToken, message) {
|
|
1308
|
+
if (req.bypassGovernance !== true) return this.error(400, "file_lock_governance_protected", message);
|
|
1309
|
+
if (!this.requestHasCapability(authToken, require_auth.Capability.BypassGovernance)) return this.error(403, "unauthorized", `application key lacks required capabilities: ${require_auth.Capability.BypassGovernance}`);
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
validateReplicationApplicationKey(applicationKeyId, requiredCapabilities, options) {
|
|
1313
|
+
const key = this.keys.get(applicationKeyId);
|
|
1314
|
+
if (key === void 0) return this.invalidReplicationApplicationKey(options.role);
|
|
1315
|
+
if (missingStoredKeyCapabilities(key, requiredCapabilities).length > 0) return this.invalidReplicationApplicationKey(options.role);
|
|
1316
|
+
if (options.bucketId !== void 0 && !storedKeyAllowsBucket(key, options.bucketId)) return this.invalidReplicationApplicationKey(options.role);
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
invalidReplicationApplicationKey(role) {
|
|
1320
|
+
return this.error(400, "bad_request", `${role} application key is invalid or not authorized for this replication configuration`);
|
|
1321
|
+
}
|
|
1322
|
+
invalidReplicationDestinationBucket() {
|
|
1323
|
+
return this.error(400, "bad_request", "replication destination bucket is invalid or not configured for this source application key");
|
|
1324
|
+
}
|
|
1325
|
+
validateReplicationDestinationBuckets(sourceApplicationKeyId, rules) {
|
|
1326
|
+
for (const rule of rules) {
|
|
1327
|
+
const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
|
|
1328
|
+
if (typeof destinationBucketId !== "string") continue;
|
|
1329
|
+
const destinationApplicationKeyId = this.buckets.get(destinationBucketId)?.info.replicationConfiguration.value?.asReplicationDestination?.sourceToDestinationKeyMapping[sourceApplicationKeyId];
|
|
1330
|
+
if (destinationApplicationKeyId === void 0) return this.invalidReplicationDestinationBucket();
|
|
1331
|
+
const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [require_auth.Capability.WriteFiles], {
|
|
1332
|
+
bucketId: destinationBucketId,
|
|
1333
|
+
role: "replication destination"
|
|
1334
|
+
});
|
|
1335
|
+
if (destinationKeyError !== null) return destinationKeyError;
|
|
1336
|
+
}
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
validateReplicationApplicationKeys(config, bucketId) {
|
|
1340
|
+
if (!this.strictAuth || config === void 0) return null;
|
|
1341
|
+
const replicationConfiguration = requestRecord(config);
|
|
1342
|
+
const source = requestRecord(replicationConfiguration?.["asReplicationSource"]);
|
|
1343
|
+
if (source !== null) {
|
|
1344
|
+
const sourceApplicationKeyId = source["sourceApplicationKeyId"];
|
|
1345
|
+
if (typeof sourceApplicationKeyId === "string") {
|
|
1346
|
+
const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [require_auth.Capability.ReadFiles, require_auth.Capability.ListFiles], {
|
|
1347
|
+
bucketId,
|
|
1348
|
+
role: "replication source"
|
|
1349
|
+
});
|
|
1350
|
+
if (sourceKeyError !== null) return sourceKeyError;
|
|
1351
|
+
}
|
|
1352
|
+
const rules = source["replicationRules"];
|
|
1353
|
+
if (typeof sourceApplicationKeyId === "string" && Array.isArray(rules)) {
|
|
1354
|
+
const destinationError = this.validateReplicationDestinationBuckets(sourceApplicationKeyId, rules);
|
|
1355
|
+
if (destinationError !== null) return destinationError;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const mapping = requestRecord(requestRecord(replicationConfiguration?.["asReplicationDestination"])?.["sourceToDestinationKeyMapping"]);
|
|
1359
|
+
if (mapping !== null) for (const [sourceApplicationKeyId, destinationApplicationKeyId] of Object.entries(mapping)) {
|
|
1360
|
+
const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [require_auth.Capability.ReadFiles, require_auth.Capability.ListFiles], { role: "replication source" });
|
|
1361
|
+
if (sourceKeyError !== null) return sourceKeyError;
|
|
1362
|
+
if (typeof destinationApplicationKeyId === "string") {
|
|
1363
|
+
const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [require_auth.Capability.WriteFiles], {
|
|
1364
|
+
bucketId,
|
|
1365
|
+
role: "replication destination"
|
|
1366
|
+
});
|
|
1367
|
+
if (destinationKeyError !== null) return destinationKeyError;
|
|
1368
|
+
}
|
|
471
1369
|
}
|
|
472
1370
|
return null;
|
|
473
1371
|
}
|
|
@@ -475,10 +1373,7 @@ var B2Simulator = class {
|
|
|
475
1373
|
const directBucketId = requestStringField(body, "bucketId");
|
|
476
1374
|
const directFileName = requestStringField(body, "fileName");
|
|
477
1375
|
switch (endpoint) {
|
|
478
|
-
case "b2_create_bucket": return
|
|
479
|
-
bucketIds: [],
|
|
480
|
-
requiresBucketScope: true
|
|
481
|
-
};
|
|
1376
|
+
case "b2_create_bucket": return this.createBucketScope(body);
|
|
482
1377
|
case "b2_list_buckets": return this.listBucketsScope(body, directBucketId);
|
|
483
1378
|
case "b2_list_file_names":
|
|
484
1379
|
case "b2_list_file_versions": return {
|
|
@@ -514,9 +1409,23 @@ var B2Simulator = class {
|
|
|
514
1409
|
};
|
|
515
1410
|
case "b2_get_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, false);
|
|
516
1411
|
case "b2_set_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, true);
|
|
1412
|
+
case "b2_update_bucket": return this.updateBucketScope(body, directBucketId);
|
|
517
1413
|
default: return this.defaultRequestScope(directBucketId, directFileName);
|
|
518
1414
|
}
|
|
519
1415
|
}
|
|
1416
|
+
createBucketScope(body) {
|
|
1417
|
+
return {
|
|
1418
|
+
bucketIds: replicationDestinationBucketIds(body),
|
|
1419
|
+
requiresBucketScope: true,
|
|
1420
|
+
requiresAccountLevelBucketAccess: true
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
updateBucketScope(body, directBucketId) {
|
|
1424
|
+
return {
|
|
1425
|
+
bucketIds: [...directBucketId === void 0 ? [] : [directBucketId], ...replicationDestinationBucketIds(body)],
|
|
1426
|
+
requiresBucketScope: true
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
520
1429
|
listBucketsScope(body, directBucketId) {
|
|
521
1430
|
if (directBucketId !== void 0) return {
|
|
522
1431
|
bucketIds: [directBucketId],
|
|
@@ -614,7 +1523,10 @@ var B2Simulator = class {
|
|
|
614
1523
|
const idParam = options.kind === "file" ? "bucketId" : "fileId";
|
|
615
1524
|
const scopedId = options.kind === "file" ? options.bucketId : options.fileId;
|
|
616
1525
|
const uploadId = this.genId(options.kind === "file" ? "upload_file" : "upload_part");
|
|
617
|
-
const uploadUrl = new URL(
|
|
1526
|
+
const uploadUrl = new URL(require_url.b2Url("http://localhost:0", {
|
|
1527
|
+
version: "v3",
|
|
1528
|
+
endpoint
|
|
1529
|
+
}));
|
|
618
1530
|
uploadUrl.searchParams.set(idParam, scopedId);
|
|
619
1531
|
uploadUrl.searchParams.set("uploadId", uploadId);
|
|
620
1532
|
const sourceToken = options.sourceAuthToken === void 0 ? void 0 : this.issuedTokens.get(options.sourceAuthToken);
|
|
@@ -687,40 +1599,61 @@ var B2Simulator = class {
|
|
|
687
1599
|
}));
|
|
688
1600
|
}
|
|
689
1601
|
/**
|
|
690
|
-
*
|
|
1602
|
+
* Resolve the application key matching the `Authorization` header on
|
|
691
1603
|
* an `authorize_account` request. The header is in the form
|
|
692
1604
|
* `Basic base64(applicationKeyId:applicationKey)`.
|
|
693
1605
|
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
* `
|
|
1606
|
+
* The simulator has two implicit full-access storage credentials:
|
|
1607
|
+
* the documented `test-key-id:test-key` pair used by SDK tests and
|
|
1608
|
+
* `master-key-id:master-key`. In default permissive mode, any other
|
|
1609
|
+
* well-formed Basic credential whose applicationKeyId is not a stored
|
|
1610
|
+
* key also receives that implicit master grant for backward
|
|
1611
|
+
* compatibility. A credential whose applicationKeyId matches a stored
|
|
1612
|
+
* key must use the stored secret and must not be expired.
|
|
697
1613
|
*
|
|
698
1614
|
* @param authzHeader - Raw HTTP `Authorization` header value.
|
|
699
1615
|
*
|
|
700
|
-
* @returns The
|
|
1616
|
+
* @returns The authorizing key's grant scope or the rejection reason.
|
|
701
1617
|
*/
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1618
|
+
findAuthorizationGrant(authzHeader) {
|
|
1619
|
+
const credentials = parseBasicAuthorizationHeader(authzHeader);
|
|
1620
|
+
if (credentials === null) return { kind: "invalid" };
|
|
1621
|
+
const stored = this.keys.get(credentials.applicationKeyId);
|
|
1622
|
+
if (stored !== void 0) {
|
|
1623
|
+
if (!timingSafeStringEqual(stored.applicationKey, credentials.applicationKey)) return { kind: "invalid" };
|
|
1624
|
+
if (this.isStoredKeyExpired(stored)) return { kind: "expired" };
|
|
1625
|
+
return {
|
|
1626
|
+
kind: "ok",
|
|
1627
|
+
grant: {
|
|
1628
|
+
capabilities: stored.capabilities,
|
|
1629
|
+
bucketIds: stored.bucketIds,
|
|
1630
|
+
namePrefix: stored.namePrefix,
|
|
1631
|
+
applicationKeyId: credentials.applicationKeyId,
|
|
1632
|
+
accountId: stored.accountId,
|
|
1633
|
+
expirationTimestamp: stored.expirationTimestamp
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
if (isSimulatorApplicationKeyId(credentials.applicationKeyId)) return { kind: "invalid" };
|
|
1638
|
+
if (isImplicitStorageMasterCredential(credentials) || !this.strictAuth) return {
|
|
1639
|
+
kind: "ok",
|
|
1640
|
+
grant: {
|
|
1641
|
+
capabilities: SIMULATOR_MASTER_CAPABILITIES,
|
|
1642
|
+
bucketIds: null,
|
|
1643
|
+
namePrefix: null,
|
|
1644
|
+
applicationKeyId: null,
|
|
1645
|
+
accountId: this.accountId,
|
|
1646
|
+
expirationTimestamp: null
|
|
709
1647
|
}
|
|
710
|
-
})();
|
|
711
|
-
if (decoded === null) return null;
|
|
712
|
-
const idx = decoded.indexOf(":");
|
|
713
|
-
if (idx === -1) return null;
|
|
714
|
-
const applicationKeyId = decoded.slice(0, idx);
|
|
715
|
-
const applicationKey = decoded.slice(idx + 1);
|
|
716
|
-
const stored = this.keys.get(applicationKeyId);
|
|
717
|
-
if (!stored || stored.applicationKey !== applicationKey) return null;
|
|
718
|
-
return {
|
|
719
|
-
capabilities: stored.capabilities,
|
|
720
|
-
bucketIds: stored.bucketIds,
|
|
721
|
-
namePrefix: stored.namePrefix,
|
|
722
|
-
applicationKeyId
|
|
723
1648
|
};
|
|
1649
|
+
return { kind: "invalid" };
|
|
1650
|
+
}
|
|
1651
|
+
canAuthorizePartner(authzHeader) {
|
|
1652
|
+
const credentials = parseBasicAuthorizationHeader(authzHeader);
|
|
1653
|
+
if (credentials === null) return false;
|
|
1654
|
+
if (isImplicitPartnerMasterCredential(credentials)) return true;
|
|
1655
|
+
const stored = this.keys.get(credentials.applicationKeyId);
|
|
1656
|
+
return stored !== void 0 && stored.accountId === this.accountId && !this.isStoredKeyExpired(stored) && timingSafeStringEqual(stored.applicationKey, credentials.applicationKey);
|
|
724
1657
|
}
|
|
725
1658
|
/**
|
|
726
1659
|
* Creates an {@link HttpTransport} that routes requests to this simulator.
|
|
@@ -812,7 +1745,7 @@ var B2Simulator = class {
|
|
|
812
1745
|
}
|
|
813
1746
|
/**
|
|
814
1747
|
* Dispatches a JSON API request to the appropriate handler.
|
|
815
|
-
* @param
|
|
1748
|
+
* @param method - The HTTP method.
|
|
816
1749
|
* @param origin - The request URL origin used for simulator-issued endpoints.
|
|
817
1750
|
* @param path - The request URL path containing the B2 endpoint name.
|
|
818
1751
|
* @param headers - The HTTP request headers; consulted by the
|
|
@@ -821,38 +1754,48 @@ var B2Simulator = class {
|
|
|
821
1754
|
*
|
|
822
1755
|
* @returns An object with HTTP status and JSON response body.
|
|
823
1756
|
*/
|
|
824
|
-
async handleRequest(
|
|
825
|
-
const endpoint = path
|
|
826
|
-
|
|
827
|
-
if (this.
|
|
1757
|
+
async handleRequest(method, origin, path, headers, body) {
|
|
1758
|
+
const { endpoint, version, unsupportedVersion } = apiPathParts(path);
|
|
1759
|
+
if (unsupportedVersion !== void 0) return this.error(400, "unsupported_api_version", `B2 API version ${unsupportedVersion} is not supported by the simulator; use v4 or v3`);
|
|
1760
|
+
if (!jsonEndpointAllowsMethod(method, endpoint)) return this.error(405, "method_not_allowed", `${endpoint} does not support ${method.toUpperCase()}`);
|
|
1761
|
+
if (this.strictAuth && !this.partner.isEndpoint(endpoint)) {
|
|
828
1762
|
const authError = this.authorizeRequest(headers["authorization"], endpoint, this.requestScope(endpoint, body));
|
|
829
1763
|
if (authError !== null) return authError;
|
|
830
1764
|
}
|
|
831
1765
|
switch (endpoint) {
|
|
832
|
-
case "b2_authorize_account":
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
case
|
|
836
|
-
case
|
|
1766
|
+
case "b2_authorize_account":
|
|
1767
|
+
if (version === "v3" && this.partner.isAuthorizeEnabled()) return this.partner.authorize(headers["authorization"], origin);
|
|
1768
|
+
return this.authorize(headers["authorization"], origin);
|
|
1769
|
+
case require_partner_capabilities.PartnerEndpoint.CreateGroupMember: return this.partner.createGroupMember(body, headers["authorization"]);
|
|
1770
|
+
case require_partner_capabilities.PartnerEndpoint.EjectGroupMember: return this.partner.ejectGroupMember(body, headers["authorization"]);
|
|
1771
|
+
case require_partner_capabilities.PartnerEndpoint.ListGroups: return this.partner.listGroups(body, headers["authorization"]);
|
|
1772
|
+
case require_partner_capabilities.PartnerEndpoint.ListGroupMembers: return this.partner.listGroupMembers(body, headers["authorization"]);
|
|
1773
|
+
case require_partner_capabilities.PartnerEndpoint.ReserveTrialCreateAccount: return this.partner.reserveTrialCreateAccount(body, headers["authorization"]);
|
|
1774
|
+
case require_partner_capabilities.PartnerEndpoint.ListComputers: return this.partner.listComputers(body, headers["authorization"]);
|
|
1775
|
+
case require_partner_capabilities.PartnerEndpoint.DeleteComputer: return this.partner.deleteComputer(body, headers["authorization"]);
|
|
1776
|
+
case "b2_create_bucket": return this.createBucket(body, headers["authorization"]);
|
|
1777
|
+
case "b2_list_buckets": return this.listBuckets(body, headers["authorization"]);
|
|
1778
|
+
case "b2_delete_bucket": return this.deleteBucket(body, headers["authorization"]);
|
|
1779
|
+
case "b2_update_bucket": return this.updateBucket(body, headers["authorization"]);
|
|
837
1780
|
case "b2_get_upload_url": return await this.getUploadUrl(body, headers["authorization"]);
|
|
838
1781
|
case "b2_list_file_names": return this.listFileNames(body);
|
|
839
1782
|
case "b2_list_file_versions": return this.listFileVersions(body);
|
|
840
1783
|
case "b2_get_file_info": return this.getFileInfo(body);
|
|
841
1784
|
case "b2_hide_file": return this.hideFile(body);
|
|
842
|
-
case "b2_delete_file_version": return this.deleteFileVersion(body);
|
|
1785
|
+
case "b2_delete_file_version": return this.deleteFileVersion(body, headers["authorization"]);
|
|
843
1786
|
case "b2_copy_file": return await this.copyFile(body);
|
|
844
|
-
case "b2_start_large_file": return this.startLargeFile(body);
|
|
1787
|
+
case "b2_start_large_file": return await this.startLargeFile(body, headers["authorization"]);
|
|
845
1788
|
case "b2_get_upload_part_url": return await this.getUploadPartUrl(body, headers["authorization"]);
|
|
846
1789
|
case "b2_finish_large_file": return this.finishLargeFile(body);
|
|
847
1790
|
case "b2_cancel_large_file": return this.cancelLargeFile(body);
|
|
848
|
-
case "b2_list_unfinished_large_files": return this.listUnfinishedLargeFiles(body);
|
|
1791
|
+
case "b2_list_unfinished_large_files": return this.listUnfinishedLargeFiles(body, headers["authorization"]);
|
|
849
1792
|
case "b2_list_parts": return this.listParts(body);
|
|
850
1793
|
case "b2_copy_part": return await this.copyPart(body);
|
|
851
1794
|
case "b2_get_download_authorization": return this.getDownloadAuthorization(body);
|
|
852
|
-
case "b2_create_key": return this.createKey(body,
|
|
1795
|
+
case "b2_create_key": return this.createKey(body, version, headers["authorization"]);
|
|
853
1796
|
case "b2_list_keys": return this.listKeys(body);
|
|
854
1797
|
case "b2_delete_key": return this.deleteKey(body);
|
|
855
|
-
case "b2_update_file_retention": return this.updateFileRetention(body);
|
|
1798
|
+
case "b2_update_file_retention": return this.updateFileRetention(body, headers["authorization"]);
|
|
856
1799
|
case "b2_update_file_legal_hold": return this.updateFileLegalHold(body);
|
|
857
1800
|
case "b2_get_bucket_notification_rules": return this.getBucketNotificationRules(body);
|
|
858
1801
|
case "b2_set_bucket_notification_rules": return this.setBucketNotificationRules(body);
|
|
@@ -882,9 +1825,25 @@ var B2Simulator = class {
|
|
|
882
1825
|
const fileName = kind === "part" || headers["x-bz-file-name"] === void 0 ? void 0 : decodeHeaderValue(headers["x-bz-file-name"]);
|
|
883
1826
|
const authError = await this.validateUploadAuthorization(kind, url, headers["authorization"], fileName);
|
|
884
1827
|
if (authError !== null) return authError;
|
|
1828
|
+
const contentLengthError = this.validateContentLength(headers, data.byteLength);
|
|
1829
|
+
if (contentLengthError !== null) return contentLengthError;
|
|
885
1830
|
if (kind === "part") return await this.handleUploadPart(url, headers, data);
|
|
886
1831
|
return await this.handleUploadFile(url, headers, data);
|
|
887
1832
|
}
|
|
1833
|
+
validateContentLength(headers, actualLength) {
|
|
1834
|
+
const parsed = parseContentLengthHeader(headers);
|
|
1835
|
+
if (parsed.kind === "absent") return null;
|
|
1836
|
+
if (parsed.kind === "error") return this.error(400, "bad_request", parsed.message);
|
|
1837
|
+
if (parsed.expectedLength !== actualLength) return this.error(400, "bad_request", contentLengthMismatchMessage(parsed.expectedLength, actualLength));
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
parseCustomUploadTimestamp(value) {
|
|
1841
|
+
if (value === void 0 || value === null) return { timestamp: null };
|
|
1842
|
+
if (!this.customUploadTimestampsEnabled) return this.error(400, "custom_timestamp_not_allowed", "Custom upload timestamps are not enabled for this account.");
|
|
1843
|
+
const timestamp = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
1844
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > this.now()) return this.error(400, "custom_timestamp_invalid", "The request has an invalid custom upload timestamp.");
|
|
1845
|
+
return { timestamp };
|
|
1846
|
+
}
|
|
888
1847
|
/**
|
|
889
1848
|
* Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
|
|
890
1849
|
* Returns the file data along with B2 response headers.
|
|
@@ -896,33 +1855,32 @@ var B2Simulator = class {
|
|
|
896
1855
|
*/
|
|
897
1856
|
handleDownload(path, headers, method = "GET") {
|
|
898
1857
|
if (path.includes("b2_download_file_by_id")) {
|
|
1858
|
+
const url = new URL(`http://localhost${path}`);
|
|
899
1859
|
if (this.strictAuth) {
|
|
900
|
-
const
|
|
901
|
-
const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
|
|
1860
|
+
const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: false }), "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
|
|
902
1861
|
bucketIds: [],
|
|
903
1862
|
requiresBucketScope: true
|
|
904
|
-
});
|
|
1863
|
+
}, url);
|
|
905
1864
|
if (authError !== null) return this.errorAsDownload(authError);
|
|
906
1865
|
}
|
|
907
|
-
const url = new URL(`http://localhost${path}`);
|
|
908
1866
|
const fileId = url.searchParams.get("fileId") ?? "";
|
|
909
|
-
return this.finalizeDownload(this.downloadById(fileId, headers
|
|
1867
|
+
return this.finalizeDownload(this.downloadById(fileId, headers, requestHeaderValue(headers, "range")), url, method);
|
|
910
1868
|
}
|
|
911
1869
|
const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
|
|
912
1870
|
if (fileMatch) {
|
|
913
1871
|
const bucketName = decodeURIComponent(fileMatch[1] ?? "");
|
|
914
1872
|
const fileName = decodeURIComponent(fileMatch[2] ?? "");
|
|
1873
|
+
const url = new URL(`http://localhost${path}`);
|
|
915
1874
|
if (this.strictAuth) {
|
|
916
1875
|
const bucket = [...this.buckets.values()].find((b) => b.info.bucketName === bucketName);
|
|
917
|
-
const authError = this.
|
|
1876
|
+
const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: true }), "b2_download_file_by_name", {
|
|
918
1877
|
bucketIds: bucket === void 0 ? [] : [bucket.info.bucketId],
|
|
919
1878
|
fileNames: [fileName],
|
|
920
1879
|
requiresBucketScope: true
|
|
921
|
-
});
|
|
1880
|
+
}, url);
|
|
922
1881
|
if (authError !== null) return this.errorAsDownload(authError);
|
|
923
1882
|
}
|
|
924
|
-
|
|
925
|
-
return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers["range"]), url, method);
|
|
1883
|
+
return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers, requestHeaderValue(headers, "range")), url, method);
|
|
926
1884
|
}
|
|
927
1885
|
return {
|
|
928
1886
|
status: 404,
|
|
@@ -959,16 +1917,8 @@ var B2Simulator = class {
|
|
|
959
1917
|
* @returns The response with overrides applied.
|
|
960
1918
|
*/
|
|
961
1919
|
finalizeDownload(response, url, method) {
|
|
962
|
-
const overrideMap = {
|
|
963
|
-
b2ContentDisposition: "Content-Disposition",
|
|
964
|
-
b2ContentLanguage: "Content-Language",
|
|
965
|
-
b2ContentEncoding: "Content-Encoding",
|
|
966
|
-
b2ContentType: "Content-Type",
|
|
967
|
-
b2CacheControl: "Cache-Control",
|
|
968
|
-
b2Expires: "Expires"
|
|
969
|
-
};
|
|
970
1920
|
const newHeaders = { ...response.headers };
|
|
971
|
-
for (const [param, header] of Object.entries(
|
|
1921
|
+
for (const [param, header] of Object.entries(DOWNLOAD_RESPONSE_OVERRIDE_HEADERS)) {
|
|
972
1922
|
const value = url.searchParams.get(param);
|
|
973
1923
|
if (value !== null) newHeaders[header] = value;
|
|
974
1924
|
}
|
|
@@ -994,6 +1944,10 @@ var B2Simulator = class {
|
|
|
994
1944
|
const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
|
|
995
1945
|
if ("status" in resolved) return resolved;
|
|
996
1946
|
const { sha1: contentSha1, data: storedData } = resolved;
|
|
1947
|
+
const serverSideEncryption = await uploadServerSideEncryption(headers, bucketDefaultServerSideEncryptionForStorage(bucket.info));
|
|
1948
|
+
if ("status" in serverSideEncryption) return serverSideEncryption;
|
|
1949
|
+
const customUploadTimestamp = this.parseCustomUploadTimestamp(requestHeaderValue(headers, "x-bz-custom-upload-timestamp"));
|
|
1950
|
+
if ("status" in customUploadTimestamp) return customUploadTimestamp;
|
|
997
1951
|
const fileVersion = this.makeFileVersion({
|
|
998
1952
|
bucketId,
|
|
999
1953
|
fileName,
|
|
@@ -1002,11 +1956,14 @@ var B2Simulator = class {
|
|
|
1002
1956
|
contentSha1,
|
|
1003
1957
|
fileInfo,
|
|
1004
1958
|
action: require_file.FileAction.Upload,
|
|
1005
|
-
serverSideEncryption
|
|
1959
|
+
serverSideEncryption,
|
|
1960
|
+
...this.newReplicationStatusField(bucketId, fileName),
|
|
1961
|
+
...customUploadTimestamp.timestamp !== null ? { uploadTimestamp: customUploadTimestamp.timestamp } : {}
|
|
1006
1962
|
});
|
|
1007
1963
|
const stored = {
|
|
1008
1964
|
fileVersion,
|
|
1009
|
-
data: storedData
|
|
1965
|
+
data: storedData,
|
|
1966
|
+
serverSideEncryption
|
|
1010
1967
|
};
|
|
1011
1968
|
const existing = bucket.files.get(fileName);
|
|
1012
1969
|
if (existing) existing.push(stored);
|
|
@@ -1056,18 +2013,39 @@ var B2Simulator = class {
|
|
|
1056
2013
|
data
|
|
1057
2014
|
};
|
|
1058
2015
|
}
|
|
2016
|
+
async validateUploadPartEncryption(large, headers) {
|
|
2017
|
+
if (large.serverSideEncryption.mode !== require_encryption.EncryptionMode.SseC) {
|
|
2018
|
+
if (!hasCustomerEncryptionHeaders(headers)) return null;
|
|
2019
|
+
return this.error(400, "bad_request", "SSE-C upload part headers require an SSE-C large file");
|
|
2020
|
+
}
|
|
2021
|
+
const supplied = await storedServerSideEncryption({
|
|
2022
|
+
mode: require_encryption.EncryptionMode.SseC,
|
|
2023
|
+
algorithm: headers["x-bz-server-side-encryption-customer-algorithm"],
|
|
2024
|
+
customerKey: headers["x-bz-server-side-encryption-customer-key"],
|
|
2025
|
+
customerKeyMd5: headers["x-bz-server-side-encryption-customer-key-md5"]
|
|
2026
|
+
});
|
|
2027
|
+
if ("status" in supplied) return supplied;
|
|
2028
|
+
if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "SSE-C upload parts require matching customer encryption headers");
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
1059
2031
|
async handleUploadPart(url, headers, data) {
|
|
1060
2032
|
const fileId = new URL(url).searchParams.get("fileId");
|
|
1061
2033
|
if (!fileId) return this.error(400, "bad_request", "Missing fileId");
|
|
1062
2034
|
const large = this.largeFiles.get(fileId);
|
|
1063
2035
|
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
1064
|
-
const partNumber =
|
|
2036
|
+
const partNumber = this.parsePartNumberHeader(requestHeaderValue(headers, "x-bz-part-number"));
|
|
2037
|
+
if (typeof partNumber !== "number") return partNumber;
|
|
2038
|
+
const encryptionError = await this.validateUploadPartEncryption(large, headers);
|
|
2039
|
+
if (encryptionError !== null) return encryptionError;
|
|
1065
2040
|
const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
|
|
1066
2041
|
if ("status" in resolved) return resolved;
|
|
1067
2042
|
const { sha1, data: partData } = resolved;
|
|
2043
|
+
const uploadTimestamp = this.monotonicTimestamp();
|
|
1068
2044
|
large.parts.set(partNumber, {
|
|
1069
2045
|
data: partData,
|
|
1070
|
-
sha1
|
|
2046
|
+
sha1,
|
|
2047
|
+
contentMd5: null,
|
|
2048
|
+
uploadTimestamp
|
|
1071
2049
|
});
|
|
1072
2050
|
return {
|
|
1073
2051
|
status: 200,
|
|
@@ -1076,21 +2054,32 @@ var B2Simulator = class {
|
|
|
1076
2054
|
partNumber,
|
|
1077
2055
|
contentLength: partData.byteLength,
|
|
1078
2056
|
contentSha1: sha1,
|
|
2057
|
+
contentMd5: null,
|
|
1079
2058
|
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
1080
|
-
uploadTimestamp
|
|
2059
|
+
uploadTimestamp
|
|
1081
2060
|
}
|
|
1082
2061
|
};
|
|
1083
2062
|
}
|
|
1084
|
-
|
|
2063
|
+
parsePartNumberHeader(header) {
|
|
2064
|
+
if (header === void 0) return this.error(400, "bad_request", "X-Bz-Part-Number header is required");
|
|
2065
|
+
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}`);
|
|
2066
|
+
const partNumber = Number(header);
|
|
2067
|
+
return this.validatePartNumber(partNumber) ?? partNumber;
|
|
2068
|
+
}
|
|
2069
|
+
validatePartNumber(partNumber) {
|
|
2070
|
+
if (Number.isInteger(partNumber) && partNumber >= B2_MIN_PART_NUMBER && partNumber <= B2_MAX_PART_NUMBER) return null;
|
|
2071
|
+
return this.error(400, "bad_request", `partNumber must be an integer between ${B2_MIN_PART_NUMBER} and ${B2_MAX_PART_NUMBER}; received ${String(partNumber)}`);
|
|
2072
|
+
}
|
|
2073
|
+
downloadById(fileId, headers, range) {
|
|
1085
2074
|
const found = this.findFile(fileId);
|
|
1086
2075
|
if (found === null) return {
|
|
1087
2076
|
status: 404,
|
|
1088
2077
|
headers: {},
|
|
1089
2078
|
data: null
|
|
1090
2079
|
};
|
|
1091
|
-
return this.serveFile(found.stored, range);
|
|
2080
|
+
return this.serveFile(found.stored, headers, range);
|
|
1092
2081
|
}
|
|
1093
|
-
downloadByName(bucketName, fileName, range) {
|
|
2082
|
+
downloadByName(bucketName, fileName, headers, range) {
|
|
1094
2083
|
for (const bucket of this.buckets.values()) {
|
|
1095
2084
|
if (bucket.info.bucketName !== bucketName) continue;
|
|
1096
2085
|
const versions = bucket.files.get(fileName);
|
|
@@ -1101,7 +2090,7 @@ var B2Simulator = class {
|
|
|
1101
2090
|
headers: {},
|
|
1102
2091
|
data: null
|
|
1103
2092
|
};
|
|
1104
|
-
return this.serveFile(latest, range);
|
|
2093
|
+
return this.serveFile(latest, headers, range);
|
|
1105
2094
|
}
|
|
1106
2095
|
return {
|
|
1107
2096
|
status: 404,
|
|
@@ -1109,7 +2098,41 @@ var B2Simulator = class {
|
|
|
1109
2098
|
data: null
|
|
1110
2099
|
};
|
|
1111
2100
|
}
|
|
1112
|
-
|
|
2101
|
+
validateDownloadEncryption(stored, headers) {
|
|
2102
|
+
const encryption = stored.serverSideEncryption;
|
|
2103
|
+
if (encryption.mode !== require_encryption.EncryptionMode.SseC) return null;
|
|
2104
|
+
const algorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
|
|
2105
|
+
const customerKey = headers["x-bz-server-side-encryption-customer-key"];
|
|
2106
|
+
const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
|
|
2107
|
+
const customerKeyBytes = customerKey === void 0 ? null : base64ToBytes(customerKey, 32);
|
|
2108
|
+
const normalizedCustomerKeyMd5 = customerKeyMd5 === void 0 ? null : normalizeSizedBase64(customerKeyMd5, 16);
|
|
2109
|
+
if (algorithm !== encryption.algorithm || customerKeyBytes === null || normalizedCustomerKeyMd5 === null || require_md5.md5Base64Sync(customerKeyBytes) !== normalizedCustomerKeyMd5) return this.errorAsDownload(this.error(400, "bad_request", "SSE-C downloads require matching customer encryption headers"));
|
|
2110
|
+
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"));
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
async validateSourceServerSideEncryption(stored, encryption) {
|
|
2114
|
+
const expected = stored.serverSideEncryption;
|
|
2115
|
+
if (expected.mode !== require_encryption.EncryptionMode.SseC) return null;
|
|
2116
|
+
if (encryption?.mode !== require_encryption.EncryptionMode.SseC) return this.error(400, "bad_request", "SSE-C source copies require customer encryption");
|
|
2117
|
+
const supplied = await storedServerSideEncryption(encryption);
|
|
2118
|
+
if ("status" in supplied) return supplied;
|
|
2119
|
+
if (!sameStoredServerSideEncryption(expected, supplied)) return this.error(400, "bad_request", "SSE-C source customer encryption does not match");
|
|
2120
|
+
return null;
|
|
2121
|
+
}
|
|
2122
|
+
async resolveCopyDestinationEncryption(destinationBucket, requested) {
|
|
2123
|
+
if (requested !== void 0) return await storedServerSideEncryption(requested);
|
|
2124
|
+
return await storedServerSideEncryption(bucketDefaultServerSideEncryptionForStorage(destinationBucket.info));
|
|
2125
|
+
}
|
|
2126
|
+
async validateCopyPartDestinationEncryption(large, encryption) {
|
|
2127
|
+
if (encryption === void 0) return large.serverSideEncryption.mode === require_encryption.EncryptionMode.SseC ? this.error(400, "bad_request", "SSE-C destination copies require customer encryption") : null;
|
|
2128
|
+
const supplied = await storedServerSideEncryption(encryption);
|
|
2129
|
+
if ("status" in supplied) return supplied;
|
|
2130
|
+
if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "Destination server-side encryption does not match");
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
serveFile(stored, requestHeaders, range) {
|
|
2134
|
+
const encryptionError = this.validateDownloadEncryption(stored, requestHeaders);
|
|
2135
|
+
if (encryptionError !== null) return encryptionError;
|
|
1113
2136
|
const fullData = stored.data;
|
|
1114
2137
|
let data = fullData;
|
|
1115
2138
|
let status = 200;
|
|
@@ -1140,6 +2163,10 @@ var B2Simulator = class {
|
|
|
1140
2163
|
};
|
|
1141
2164
|
for (const [key, value] of Object.entries(fv.fileInfo)) headers[`X-Bz-Info-${require_encoding.encodeFileName(key)}`] = require_encoding.encodeFileName(value);
|
|
1142
2165
|
if (!("src_last_modified_millis" in fv.fileInfo)) headers["X-Bz-Info-src_last_modified_millis"] = String(fv.uploadTimestamp);
|
|
2166
|
+
const unauthorizedToRead = [];
|
|
2167
|
+
this.addDownloadEncryptionHeaders(headers, stored.serverSideEncryption, requestHeaders, unauthorizedToRead);
|
|
2168
|
+
this.addDownloadObjectLockHeaders(headers, fv, requestHeaders, unauthorizedToRead);
|
|
2169
|
+
if (unauthorizedToRead.length > 0) headers[require_download.DownloadHeaderName.ClientUnauthorizedToRead] = unauthorizedToRead.join(",");
|
|
1143
2170
|
if (contentRange !== null) headers["Content-Range"] = contentRange;
|
|
1144
2171
|
return {
|
|
1145
2172
|
status,
|
|
@@ -1147,39 +2174,58 @@ var B2Simulator = class {
|
|
|
1147
2174
|
data
|
|
1148
2175
|
};
|
|
1149
2176
|
}
|
|
2177
|
+
addDownloadEncryptionHeaders(headers, encryption, requestHeaders, unauthorizedToRead) {
|
|
2178
|
+
if (encryption.mode === require_encryption.EncryptionMode.None) return;
|
|
2179
|
+
if (!this.requestHasCapability(requestHeaderValue(requestHeaders, "authorization"), require_auth.Capability.ReadBucketEncryption)) {
|
|
2180
|
+
if (encryption.mode === require_encryption.EncryptionMode.SseB2) unauthorizedToRead.push(require_download.DownloadClientUnauthorizedToReadMarker.ServerSideEncryption);
|
|
2181
|
+
else unauthorizedToRead.push(require_download.DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerAlgorithm, require_download.DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerKeyMd5);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (encryption.mode === require_encryption.EncryptionMode.SseB2) {
|
|
2185
|
+
headers[require_download.DownloadHeaderName.ServerSideEncryption] = encryption.algorithm;
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
headers[require_download.DownloadHeaderName.ServerSideEncryptionCustomerAlgorithm] = encryption.algorithm;
|
|
2189
|
+
headers[require_download.DownloadHeaderName.ServerSideEncryptionCustomerKeyMd5] = encryption.customerKeyMd5;
|
|
2190
|
+
}
|
|
2191
|
+
addDownloadObjectLockHeaders(headers, fileVersion, requestHeaders, unauthorizedToRead) {
|
|
2192
|
+
const authToken = requestHeaderValue(requestHeaders, "authorization");
|
|
2193
|
+
const retention = fileVersion.fileRetention.value;
|
|
2194
|
+
if (retention !== null && retention.mode !== null && retention.retainUntilTimestamp !== null) {
|
|
2195
|
+
if (this.requestHasCapability(authToken, require_auth.Capability.ReadFileRetentions)) {
|
|
2196
|
+
headers[require_download.DownloadHeaderName.FileRetentionMode] = retention.mode;
|
|
2197
|
+
headers[require_download.DownloadHeaderName.FileRetentionRetainUntilTimestamp] = String(retention.retainUntilTimestamp);
|
|
2198
|
+
} else unauthorizedToRead.push(require_download.DownloadClientUnauthorizedToReadMarker.FileRetentionMode, require_download.DownloadClientUnauthorizedToReadMarker.FileRetentionRetainUntilTimestamp);
|
|
2199
|
+
}
|
|
2200
|
+
const legalHold = fileVersion.legalHold.value;
|
|
2201
|
+
if (legalHold === null) return;
|
|
2202
|
+
if (this.requestHasCapability(authToken, require_auth.Capability.ReadFileLegalHolds)) {
|
|
2203
|
+
headers[require_download.DownloadHeaderName.FileLegalHold] = legalHold;
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
unauthorizedToRead.push(require_download.DownloadClientUnauthorizedToReadMarker.FileLegalHold);
|
|
2207
|
+
}
|
|
1150
2208
|
authorize(authzHeader, origin = "http://localhost:0") {
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
require_auth.Capability.ReadFiles,
|
|
1158
|
-
require_auth.Capability.WriteFiles,
|
|
1159
|
-
require_auth.Capability.DeleteFiles,
|
|
1160
|
-
require_auth.Capability.ListKeys,
|
|
1161
|
-
require_auth.Capability.WriteKeys,
|
|
1162
|
-
require_auth.Capability.DeleteKeys,
|
|
1163
|
-
require_auth.Capability.ShareFiles,
|
|
1164
|
-
require_auth.Capability.ReadBucketNotifications,
|
|
1165
|
-
require_auth.Capability.WriteBucketNotifications
|
|
1166
|
-
];
|
|
1167
|
-
const keyForAuth = this.findKeyForAuthHeader(authzHeader);
|
|
1168
|
-
const allowedBuckets = this.allowedBuckets(keyForAuth?.bucketIds);
|
|
1169
|
-
const legacyBucketId = singleBucketId(keyForAuth?.bucketIds);
|
|
2209
|
+
const resolution = this.findAuthorizationGrant(authzHeader);
|
|
2210
|
+
if (resolution.kind === "invalid") return this.error(401, "unauthorized", "application key is not valid");
|
|
2211
|
+
if (resolution.kind === "expired") return this.error(401, "unauthorized", "application key is expired");
|
|
2212
|
+
const { grant } = resolution;
|
|
2213
|
+
const allowedBuckets = this.allowedBuckets(grant.bucketIds);
|
|
2214
|
+
const legacyBucketId = singleBucketId(grant.bucketIds);
|
|
1170
2215
|
const legacyBucketName = legacyBucketId === null ? null : this.buckets.get(legacyBucketId)?.info.bucketName ?? null;
|
|
1171
2216
|
const tokenStr = `sim_auth_token_${this.nextId++}`;
|
|
1172
2217
|
this.issuedTokens.set(tokenStr, {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
2218
|
+
accountId: grant.accountId,
|
|
2219
|
+
capabilities: grant.capabilities,
|
|
2220
|
+
bucketIds: grant.bucketIds,
|
|
2221
|
+
namePrefix: grant.namePrefix,
|
|
1176
2222
|
expiresAt: this.now() + this.authTokenTtlMs,
|
|
1177
|
-
applicationKeyId:
|
|
2223
|
+
applicationKeyId: grant.applicationKeyId
|
|
1178
2224
|
});
|
|
1179
2225
|
return {
|
|
1180
2226
|
status: 200,
|
|
1181
2227
|
body: {
|
|
1182
|
-
accountId: require_ids.accountId(
|
|
2228
|
+
accountId: require_ids.accountId(grant.accountId),
|
|
1183
2229
|
authorizationToken: tokenStr,
|
|
1184
2230
|
apiInfo: { storageApi: {
|
|
1185
2231
|
absoluteMinimumPartSize: this.minimumPartSize,
|
|
@@ -1188,34 +2234,35 @@ var B2Simulator = class {
|
|
|
1188
2234
|
bucketName: legacyBucketName,
|
|
1189
2235
|
downloadUrl: origin,
|
|
1190
2236
|
infoType: "storageApi",
|
|
1191
|
-
namePrefix:
|
|
2237
|
+
namePrefix: grant.namePrefix,
|
|
1192
2238
|
recommendedPartSize: this.recommendedPartSize,
|
|
1193
2239
|
s3ApiUrl: origin,
|
|
1194
2240
|
allowed: {
|
|
1195
|
-
capabilities:
|
|
2241
|
+
capabilities: cloneCapabilities(grant.capabilities),
|
|
1196
2242
|
buckets: allowedBuckets,
|
|
1197
2243
|
bucketId: legacyBucketId === null ? null : require_ids.bucketId(legacyBucketId),
|
|
1198
2244
|
bucketName: legacyBucketName,
|
|
1199
|
-
namePrefix:
|
|
2245
|
+
namePrefix: grant.namePrefix
|
|
1200
2246
|
}
|
|
1201
2247
|
} },
|
|
1202
|
-
applicationKeyExpirationTimestamp:
|
|
2248
|
+
applicationKeyExpirationTimestamp: grant.expirationTimestamp
|
|
1203
2249
|
}
|
|
1204
2250
|
};
|
|
1205
2251
|
}
|
|
1206
|
-
createBucket(req) {
|
|
2252
|
+
createBucket(req, authToken) {
|
|
1207
2253
|
const nameError = require_validation.validateBucketName(req.bucketName);
|
|
1208
2254
|
if (nameError) return this.error(400, nameError.code, nameError.message);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
2255
|
+
const bucketTypeError = require_validation.validateBucketType(req.bucketType);
|
|
2256
|
+
if (bucketTypeError) return this.error(400, bucketTypeError.code, bucketTypeError.message);
|
|
2257
|
+
const objectLockEnabled = nextObjectLockEnabled(false, req.fileLockEnabled);
|
|
2258
|
+
if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
|
|
2259
|
+
const configError = validateBucketConfigurationFields(req, { objectLockEnabled });
|
|
2260
|
+
if (configError) return this.error(400, configError.code, configError.message);
|
|
2261
|
+
const replicationKeyError = this.validateReplicationApplicationKeys(req.replicationConfiguration, void 0);
|
|
2262
|
+
if (replicationKeyError !== null) return replicationKeyError;
|
|
1213
2263
|
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");
|
|
1214
2264
|
const bid = require_ids.bucketId(this.genId("b2_bucket"));
|
|
1215
|
-
const defaultRetention = req.defaultRetention
|
|
1216
|
-
mode: require_bucket.BucketRetentionMode.None,
|
|
1217
|
-
period: null
|
|
1218
|
-
};
|
|
2265
|
+
const defaultRetention = bucketDefaultRetentionForResponse(req.defaultRetention);
|
|
1219
2266
|
const info = {
|
|
1220
2267
|
accountId: require_ids.accountId(req.accountId),
|
|
1221
2268
|
bucketId: bid,
|
|
@@ -1223,22 +2270,18 @@ var B2Simulator = class {
|
|
|
1223
2270
|
bucketType: req.bucketType,
|
|
1224
2271
|
bucketInfo: req.bucketInfo ?? {},
|
|
1225
2272
|
corsRules: req.corsRules ?? [],
|
|
1226
|
-
defaultServerSideEncryption: req.defaultServerSideEncryption
|
|
2273
|
+
defaultServerSideEncryption: publicBucketDefaultServerSideEncryption(req.defaultServerSideEncryption),
|
|
1227
2274
|
fileLockConfiguration: {
|
|
1228
2275
|
isClientAuthorizedToRead: true,
|
|
1229
2276
|
value: {
|
|
1230
|
-
isFileLockEnabled:
|
|
2277
|
+
isFileLockEnabled: objectLockEnabled,
|
|
1231
2278
|
defaultRetention
|
|
1232
2279
|
}
|
|
1233
2280
|
},
|
|
1234
2281
|
lifecycleRules: req.lifecycleRules ?? [],
|
|
1235
2282
|
options: [],
|
|
1236
2283
|
revision: 1,
|
|
1237
|
-
|
|
1238
|
-
replicationConfiguration: req.replicationConfiguration ?? {
|
|
1239
|
-
asReplicationSource: null,
|
|
1240
|
-
asReplicationDestination: null
|
|
1241
|
-
}
|
|
2284
|
+
replicationConfiguration: wrapReplicationConfiguration(req.replicationConfiguration)
|
|
1242
2285
|
};
|
|
1243
2286
|
this.buckets.set(bid, {
|
|
1244
2287
|
info,
|
|
@@ -1246,49 +2289,79 @@ var B2Simulator = class {
|
|
|
1246
2289
|
});
|
|
1247
2290
|
return {
|
|
1248
2291
|
status: 200,
|
|
1249
|
-
body: info
|
|
2292
|
+
body: this.bucketInfoForResponse(info, authToken)
|
|
1250
2293
|
};
|
|
1251
2294
|
}
|
|
1252
|
-
listBuckets(req) {
|
|
2295
|
+
listBuckets(req, authToken) {
|
|
2296
|
+
const bucketTypesError = require_validation.validateBucketTypes(req.bucketTypes);
|
|
2297
|
+
if (bucketTypesError) return this.error(400, bucketTypesError.code, bucketTypesError.message);
|
|
1253
2298
|
return {
|
|
1254
2299
|
status: 200,
|
|
1255
|
-
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.
|
|
2300
|
+
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[0] === "all" || req.bucketTypes.some((bucketType) => bucketType === bucket.bucketType)).map((bucket) => this.bucketInfoForResponse(bucket, authToken)) }
|
|
1256
2301
|
};
|
|
1257
2302
|
}
|
|
1258
|
-
deleteBucket(req) {
|
|
2303
|
+
deleteBucket(req, authToken) {
|
|
1259
2304
|
const bucket = this.buckets.get(req.bucketId);
|
|
1260
2305
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
2306
|
+
if (this.bucketHasContents(bucket)) return this.error(400, "cannot_delete_non_empty_bucket", "Bucket is not empty and cannot be deleted");
|
|
1261
2307
|
this.buckets.delete(req.bucketId);
|
|
1262
2308
|
return {
|
|
1263
2309
|
status: 200,
|
|
1264
|
-
body: bucket.info
|
|
2310
|
+
body: this.bucketInfoForResponse(bucket.info, authToken)
|
|
1265
2311
|
};
|
|
1266
2312
|
}
|
|
1267
|
-
|
|
2313
|
+
bucketHasContents(bucket) {
|
|
2314
|
+
for (const versions of bucket.files.values()) if (versions.length > 0) return true;
|
|
2315
|
+
for (const large of this.largeFiles.values()) if (large.bucketId === bucket.info.bucketId) return true;
|
|
2316
|
+
return false;
|
|
2317
|
+
}
|
|
2318
|
+
updateBucket(req, authToken) {
|
|
1268
2319
|
const bucket = this.buckets.get(req["bucketId"]);
|
|
1269
2320
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
2321
|
+
const revisionGuard = req["ifRevisionIs"];
|
|
2322
|
+
if (revisionGuard !== void 0) {
|
|
2323
|
+
if (typeof revisionGuard !== "number" || !Number.isInteger(revisionGuard)) return this.error(400, "bad_request", "ifRevisionIs must be an integer");
|
|
2324
|
+
if (revisionGuard !== bucket.info.revision) return this.error(409, "conflict", "ifRevisionIs test failed");
|
|
2325
|
+
}
|
|
2326
|
+
const objectLockEnabled = nextObjectLockEnabled(bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false, req["fileLockEnabled"]);
|
|
2327
|
+
if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
|
|
2328
|
+
if (req["bucketType"] !== void 0) {
|
|
2329
|
+
const bucketTypeError = require_validation.validateBucketType(req["bucketType"]);
|
|
2330
|
+
if (bucketTypeError) return this.error(400, bucketTypeError.code, bucketTypeError.message);
|
|
1273
2331
|
}
|
|
2332
|
+
const configError = validateBucketConfigurationFields({
|
|
2333
|
+
bucketInfo: req["bucketInfo"],
|
|
2334
|
+
corsRules: req["corsRules"],
|
|
2335
|
+
defaultServerSideEncryption: req["defaultServerSideEncryption"],
|
|
2336
|
+
defaultRetention: req["defaultRetention"],
|
|
2337
|
+
lifecycleRules: req["lifecycleRules"],
|
|
2338
|
+
replicationConfiguration: req["replicationConfiguration"]
|
|
2339
|
+
}, { objectLockEnabled });
|
|
2340
|
+
if (configError) return this.error(400, configError.code, configError.message);
|
|
2341
|
+
const replicationKeyError = this.validateReplicationApplicationKeys(req["replicationConfiguration"], req["bucketId"]);
|
|
2342
|
+
if (replicationKeyError !== null) return replicationKeyError;
|
|
1274
2343
|
const updated = {
|
|
1275
2344
|
...bucket.info,
|
|
1276
2345
|
...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
|
|
1277
2346
|
...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
|
|
1278
2347
|
...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
|
|
1279
2348
|
...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
|
|
1280
|
-
...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: req["replicationConfiguration"] } : {},
|
|
1281
|
-
...req["defaultRetention"] !== void 0 ? {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
isFileLockEnabled: bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false,
|
|
1287
|
-
defaultRetention: req["defaultRetention"]
|
|
1288
|
-
}
|
|
2349
|
+
...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: wrapReplicationConfiguration(req["replicationConfiguration"]) } : {},
|
|
2350
|
+
...req["defaultRetention"] !== void 0 ? { fileLockConfiguration: {
|
|
2351
|
+
isClientAuthorizedToRead: true,
|
|
2352
|
+
value: {
|
|
2353
|
+
isFileLockEnabled: objectLockEnabled,
|
|
2354
|
+
defaultRetention: bucketDefaultRetentionForResponse(req["defaultRetention"])
|
|
1289
2355
|
}
|
|
1290
|
-
} : {},
|
|
1291
|
-
...req["
|
|
2356
|
+
} } : {},
|
|
2357
|
+
...req["fileLockEnabled"] !== void 0 && req["defaultRetention"] === void 0 ? { fileLockConfiguration: {
|
|
2358
|
+
isClientAuthorizedToRead: true,
|
|
2359
|
+
value: {
|
|
2360
|
+
isFileLockEnabled: objectLockEnabled,
|
|
2361
|
+
defaultRetention: bucketDefaultRetention(bucket.info) ?? noBucketDefaultRetention()
|
|
2362
|
+
}
|
|
2363
|
+
} } : {},
|
|
2364
|
+
...req["defaultServerSideEncryption"] !== void 0 ? { defaultServerSideEncryption: publicBucketDefaultServerSideEncryption(req["defaultServerSideEncryption"]) } : {},
|
|
1292
2365
|
revision: bucket.info.revision + 1
|
|
1293
2366
|
};
|
|
1294
2367
|
this.buckets.set(req["bucketId"], {
|
|
@@ -1297,7 +2370,7 @@ var B2Simulator = class {
|
|
|
1297
2370
|
});
|
|
1298
2371
|
return {
|
|
1299
2372
|
status: 200,
|
|
1300
|
-
body: updated
|
|
2373
|
+
body: this.bucketInfoForResponse(updated, authToken)
|
|
1301
2374
|
};
|
|
1302
2375
|
}
|
|
1303
2376
|
async getUploadUrl(req, authToken) {
|
|
@@ -1323,7 +2396,13 @@ var B2Simulator = class {
|
|
|
1323
2396
|
if (countError) return this.error(400, countError.code, countError.message);
|
|
1324
2397
|
const max = req.maxFileCount ?? 1e3;
|
|
1325
2398
|
const prefix = req.prefix ?? "";
|
|
1326
|
-
|
|
2399
|
+
const currentVersions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).map(([_, versions]) => versions[versions.length - 1]).filter((v) => v !== void 0).map((v) => v.fileVersion).filter((v) => v.action !== require_file.FileAction.Hide).sort((a, b) => compareB2FileNames(a.fileName, b.fileName));
|
|
2400
|
+
let allFiles = this.applyListDelimiter({
|
|
2401
|
+
bucketId: req.bucketId,
|
|
2402
|
+
delimiter: req.delimiter,
|
|
2403
|
+
entries: currentVersions,
|
|
2404
|
+
prefix
|
|
2405
|
+
});
|
|
1327
2406
|
if (req.startFileName) {
|
|
1328
2407
|
const start = req.startFileName;
|
|
1329
2408
|
allFiles = allFiles.filter((f) => f.fileName >= start);
|
|
@@ -1343,11 +2422,17 @@ var B2Simulator = class {
|
|
|
1343
2422
|
if (countError) return this.error(400, countError.code, countError.message);
|
|
1344
2423
|
const max = req.maxFileCount ?? 1e3;
|
|
1345
2424
|
const prefix = req.prefix ?? "";
|
|
1346
|
-
const
|
|
2425
|
+
const versions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).flatMap(([_, versions]) => versions.map((v) => v.fileVersion)).sort((a, b) => {
|
|
1347
2426
|
const nameCmp = compareB2FileNames(a.fileName, b.fileName);
|
|
1348
2427
|
if (nameCmp !== 0) return nameCmp;
|
|
1349
2428
|
return b.uploadTimestamp - a.uploadTimestamp;
|
|
1350
2429
|
});
|
|
2430
|
+
const allVersions = this.applyListDelimiter({
|
|
2431
|
+
bucketId: req.bucketId,
|
|
2432
|
+
delimiter: req.delimiter,
|
|
2433
|
+
entries: versions,
|
|
2434
|
+
prefix
|
|
2435
|
+
});
|
|
1351
2436
|
let startIdx = 0;
|
|
1352
2437
|
if (req.startFileName !== void 0) {
|
|
1353
2438
|
const startName = req.startFileName;
|
|
@@ -1370,6 +2455,48 @@ var B2Simulator = class {
|
|
|
1370
2455
|
}
|
|
1371
2456
|
};
|
|
1372
2457
|
}
|
|
2458
|
+
applyListDelimiter(params) {
|
|
2459
|
+
if (params.delimiter === void 0 || params.delimiter === "") return params.entries.map((entry) => this.toListEntry(entry));
|
|
2460
|
+
const grouped = [];
|
|
2461
|
+
const seenFolders = /* @__PURE__ */ new Set();
|
|
2462
|
+
for (const entry of params.entries) {
|
|
2463
|
+
const delimiterIndex = entry.fileName.indexOf(params.delimiter, params.prefix.length);
|
|
2464
|
+
if (delimiterIndex === -1) {
|
|
2465
|
+
grouped.push(this.toListEntry(entry));
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
const folderName = entry.fileName.slice(0, delimiterIndex + params.delimiter.length);
|
|
2469
|
+
if (!seenFolders.has(folderName)) {
|
|
2470
|
+
seenFolders.add(folderName);
|
|
2471
|
+
grouped.push(this.makeFolderListEntry(params.bucketId, folderName));
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return grouped.sort(compareFileVersionListEntries);
|
|
2475
|
+
}
|
|
2476
|
+
toListEntry(entry) {
|
|
2477
|
+
if (entry.action !== require_file.FileAction.Hide) return entry;
|
|
2478
|
+
const { contentType: _contentType, fileRetention, legalHold, serverSideEncryption, ...listed } = entry;
|
|
2479
|
+
return {
|
|
2480
|
+
...listed,
|
|
2481
|
+
action: require_file.FileAction.Hide,
|
|
2482
|
+
contentType: require_file.HIDE_MARKER_CONTENT_TYPE
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
makeFolderListEntry(bucketId$1, fileName) {
|
|
2486
|
+
return {
|
|
2487
|
+
accountId: require_ids.accountId(this.accountId),
|
|
2488
|
+
action: require_file.FileAction.Folder,
|
|
2489
|
+
bucketId: require_ids.bucketId(bucketId$1),
|
|
2490
|
+
contentLength: 0,
|
|
2491
|
+
contentMd5: null,
|
|
2492
|
+
contentSha1: null,
|
|
2493
|
+
contentType: null,
|
|
2494
|
+
fileId: null,
|
|
2495
|
+
fileInfo: {},
|
|
2496
|
+
fileName,
|
|
2497
|
+
uploadTimestamp: 0
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
1373
2500
|
getFileInfo(req) {
|
|
1374
2501
|
const found = this.findFile(req.fileId);
|
|
1375
2502
|
if (found === null) return this.error(404, "file_not_present", "File not found");
|
|
@@ -1386,7 +2513,7 @@ var B2Simulator = class {
|
|
|
1386
2513
|
const fileVersion = this.makeFileVersion({
|
|
1387
2514
|
bucketId: req.bucketId,
|
|
1388
2515
|
fileName: req.fileName,
|
|
1389
|
-
contentType:
|
|
2516
|
+
contentType: require_file.HIDE_MARKER_CONTENT_TYPE,
|
|
1390
2517
|
contentLength: 0,
|
|
1391
2518
|
contentSha1: "none",
|
|
1392
2519
|
action: require_file.FileAction.Hide
|
|
@@ -1394,7 +2521,8 @@ var B2Simulator = class {
|
|
|
1394
2521
|
const existing = bucket.files.get(req.fileName);
|
|
1395
2522
|
const stored = {
|
|
1396
2523
|
fileVersion,
|
|
1397
|
-
data: /* @__PURE__ */ new Uint8Array(0)
|
|
2524
|
+
data: /* @__PURE__ */ new Uint8Array(0),
|
|
2525
|
+
serverSideEncryption: { mode: require_encryption.EncryptionMode.None }
|
|
1398
2526
|
};
|
|
1399
2527
|
if (existing) existing.push(stored);
|
|
1400
2528
|
else bucket.files.set(req.fileName, [stored]);
|
|
@@ -1403,16 +2531,19 @@ var B2Simulator = class {
|
|
|
1403
2531
|
body: fileVersion
|
|
1404
2532
|
};
|
|
1405
2533
|
}
|
|
1406
|
-
deleteFileVersion(req) {
|
|
2534
|
+
deleteFileVersion(req, authToken) {
|
|
1407
2535
|
const found = this.findFile(req.fileId);
|
|
1408
2536
|
if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(400, "file_not_present", "File version not found");
|
|
1409
2537
|
const fv = found.stored.fileVersion;
|
|
1410
2538
|
const retention = fv.fileRetention?.value;
|
|
1411
2539
|
const legalHold = fv.legalHold?.value;
|
|
1412
2540
|
const now = Date.now();
|
|
1413
|
-
if (legalHold ===
|
|
1414
|
-
if (retention?.mode ===
|
|
1415
|
-
if (retention?.mode ===
|
|
2541
|
+
if (legalHold === require_lock.LegalHoldValue.On) return this.error(400, "file_lock_legal_hold_protected", "File is on legal hold and cannot be deleted");
|
|
2542
|
+
if (retention?.mode === require_lock.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()}`);
|
|
2543
|
+
if (retention?.mode === require_lock.RetentionMode.Governance && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) {
|
|
2544
|
+
const bypassError = this.requireGovernanceBypass(req, authToken, "File is under governance-mode retention; pass bypassGovernance: true to delete");
|
|
2545
|
+
if (bypassError !== null) return bypassError;
|
|
2546
|
+
}
|
|
1416
2547
|
found.versions.splice(found.index, 1);
|
|
1417
2548
|
if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
|
|
1418
2549
|
return {
|
|
@@ -1432,6 +2563,10 @@ var B2Simulator = class {
|
|
|
1432
2563
|
const destBucketId = req.destinationBucketId ?? found.bucketId;
|
|
1433
2564
|
const destBucket = this.buckets.get(destBucketId);
|
|
1434
2565
|
if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
|
|
2566
|
+
const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
|
|
2567
|
+
if (sourceEncryptionError !== null) return sourceEncryptionError;
|
|
2568
|
+
const destinationEncryption = await this.resolveCopyDestinationEncryption(destBucket, req.destinationServerSideEncryption);
|
|
2569
|
+
if ("status" in destinationEncryption) return destinationEncryption;
|
|
1435
2570
|
let data = sourceStored.data;
|
|
1436
2571
|
let contentSha1 = sourceStored.fileVersion.contentSha1 ?? "none";
|
|
1437
2572
|
if (req.range !== void 0) {
|
|
@@ -1464,11 +2599,14 @@ var B2Simulator = class {
|
|
|
1464
2599
|
contentLength: data.byteLength,
|
|
1465
2600
|
contentSha1,
|
|
1466
2601
|
fileInfo,
|
|
1467
|
-
action: require_file.FileAction.Copy
|
|
2602
|
+
action: require_file.FileAction.Copy,
|
|
2603
|
+
serverSideEncryption: destinationEncryption,
|
|
2604
|
+
...this.newReplicationStatusField(destBucketId, req.fileName)
|
|
1468
2605
|
});
|
|
1469
2606
|
const copied = {
|
|
1470
2607
|
fileVersion,
|
|
1471
|
-
data: new Uint8Array(data)
|
|
2608
|
+
data: new Uint8Array(data),
|
|
2609
|
+
serverSideEncryption: destinationEncryption
|
|
1472
2610
|
};
|
|
1473
2611
|
const existing = destBucket.files.get(req.fileName);
|
|
1474
2612
|
if (existing) existing.push(copied);
|
|
@@ -1479,7 +2617,7 @@ var B2Simulator = class {
|
|
|
1479
2617
|
body: fileVersion
|
|
1480
2618
|
};
|
|
1481
2619
|
}
|
|
1482
|
-
startLargeFile(req) {
|
|
2620
|
+
async startLargeFile(req, authToken) {
|
|
1483
2621
|
const bucket = this.buckets.get(req.bucketId);
|
|
1484
2622
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1485
2623
|
const nameError = require_validation.validateFileName(req.fileName);
|
|
@@ -1488,17 +2626,23 @@ var B2Simulator = class {
|
|
|
1488
2626
|
const infoError = require_validation.validateFileInfo(req.fileInfo);
|
|
1489
2627
|
if (infoError) return this.error(400, infoError.code, infoError.message);
|
|
1490
2628
|
}
|
|
2629
|
+
const customUploadTimestamp = this.parseCustomUploadTimestamp(req.customUploadTimestamp);
|
|
2630
|
+
if ("status" in customUploadTimestamp) return customUploadTimestamp;
|
|
1491
2631
|
const fid = this.genId("4_z");
|
|
1492
|
-
const uploadTimestamp = this.monotonicTimestamp();
|
|
2632
|
+
const uploadTimestamp = customUploadTimestamp.timestamp ?? this.monotonicTimestamp();
|
|
2633
|
+
const serverSideEncryption = await storedServerSideEncryption(req.serverSideEncryption ?? bucketDefaultServerSideEncryptionForStorage(bucket.info));
|
|
2634
|
+
if ("status" in serverSideEncryption) return serverSideEncryption;
|
|
2635
|
+
const replicationStatus = this.replicationStatusForNewFile(req.bucketId, req.fileName);
|
|
1493
2636
|
const large = {
|
|
1494
2637
|
fileId: fid,
|
|
1495
2638
|
bucketId: req.bucketId,
|
|
1496
2639
|
fileName: req.fileName,
|
|
1497
2640
|
contentType: req.contentType,
|
|
1498
2641
|
fileInfo: req.fileInfo ?? {},
|
|
1499
|
-
fileRetention: req.fileRetention ?? defaultFileRetention(bucket.info
|
|
2642
|
+
fileRetention: req.fileRetention ?? defaultFileRetention(bucketDefaultRetention(bucket.info), uploadTimestamp),
|
|
1500
2643
|
legalHold: req.legalHold ?? null,
|
|
1501
|
-
|
|
2644
|
+
...this.replicationStatusMetadataFor(replicationStatus),
|
|
2645
|
+
serverSideEncryption,
|
|
1502
2646
|
uploadTimestamp,
|
|
1503
2647
|
parts: /* @__PURE__ */ new Map()
|
|
1504
2648
|
};
|
|
@@ -1516,14 +2660,9 @@ var B2Simulator = class {
|
|
|
1516
2660
|
contentLength: 0,
|
|
1517
2661
|
contentSha1: "none",
|
|
1518
2662
|
contentMd5: null,
|
|
1519
|
-
fileRetention:
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
},
|
|
1523
|
-
legalHold: {
|
|
1524
|
-
isClientAuthorizedToRead: true,
|
|
1525
|
-
value: large.legalHold
|
|
1526
|
-
},
|
|
2663
|
+
fileRetention: readableFileRetention(large.fileRetention, this.requestHasCapability(authToken, require_auth.Capability.ReadFileRetentions)),
|
|
2664
|
+
legalHold: readableLegalHold(large.legalHold, this.requestHasCapability(authToken, require_auth.Capability.ReadFileLegalHolds)),
|
|
2665
|
+
...this.replicationStatusMetadataFor(large.replicationStatus),
|
|
1527
2666
|
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
1528
2667
|
uploadTimestamp: large.uploadTimestamp
|
|
1529
2668
|
}
|
|
@@ -1553,17 +2692,11 @@ var B2Simulator = class {
|
|
|
1553
2692
|
const bucket = this.buckets.get(large.bucketId);
|
|
1554
2693
|
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1555
2694
|
const sortedParts = [...large.parts.entries()].sort((a, b) => a[0] - b[0]);
|
|
1556
|
-
if (sortedParts.length >
|
|
1557
|
-
for (const [partNumber] of sortedParts) if (!Number.isInteger(partNumber) || partNumber <
|
|
2695
|
+
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}`);
|
|
2696
|
+
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`);
|
|
1558
2697
|
if (req.partSha1Array.length !== sortedParts.length) return this.error(400, "bad_request", `partSha1Array has ${req.partSha1Array.length} entries but ${sortedParts.length} parts were uploaded`);
|
|
1559
|
-
for (
|
|
1560
|
-
|
|
1561
|
-
if (req.partSha1Array[i]?.toLowerCase() !== part.sha1) return this.error(400, "bad_request", `part ${partNumber} SHA-1 does not match the uploaded part`);
|
|
1562
|
-
}
|
|
1563
|
-
for (let i = 0; i < sortedParts.length - 1; i++) {
|
|
1564
|
-
const [partNumber, part] = sortedParts[i];
|
|
1565
|
-
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
|
-
}
|
|
2698
|
+
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`);
|
|
2699
|
+
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}`);
|
|
1567
2700
|
let totalSize = 0;
|
|
1568
2701
|
for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
|
|
1569
2702
|
const combined = new Uint8Array(totalSize);
|
|
@@ -1582,11 +2715,14 @@ var B2Simulator = class {
|
|
|
1582
2715
|
action: require_file.FileAction.Upload,
|
|
1583
2716
|
fileRetention: large.fileRetention,
|
|
1584
2717
|
legalHold: large.legalHold,
|
|
1585
|
-
serverSideEncryption: large.serverSideEncryption
|
|
2718
|
+
serverSideEncryption: large.serverSideEncryption,
|
|
2719
|
+
uploadTimestamp: large.uploadTimestamp,
|
|
2720
|
+
...this.replicationStatusMetadataFor(large.replicationStatus)
|
|
1586
2721
|
});
|
|
1587
2722
|
const stored = {
|
|
1588
2723
|
fileVersion,
|
|
1589
|
-
data: combined
|
|
2724
|
+
data: combined,
|
|
2725
|
+
serverSideEncryption: large.serverSideEncryption
|
|
1590
2726
|
};
|
|
1591
2727
|
const existing = bucket.files.get(large.fileName);
|
|
1592
2728
|
if (existing) existing.push(stored);
|
|
@@ -1612,7 +2748,7 @@ var B2Simulator = class {
|
|
|
1612
2748
|
}
|
|
1613
2749
|
};
|
|
1614
2750
|
}
|
|
1615
|
-
listUnfinishedLargeFiles(req) {
|
|
2751
|
+
listUnfinishedLargeFiles(req, authToken) {
|
|
1616
2752
|
const countError = require_validation.validateMaxCount(req.maxFileCount, "b2_list_unfinished_large_files");
|
|
1617
2753
|
if (countError) return this.error(400, countError.code, countError.message);
|
|
1618
2754
|
const prefix = req.namePrefix ?? "";
|
|
@@ -1623,10 +2759,13 @@ var B2Simulator = class {
|
|
|
1623
2759
|
const found = candidates.findIndex((f) => f.fileId === req.startFileId);
|
|
1624
2760
|
startIndex = found >= 0 ? found : 0;
|
|
1625
2761
|
}
|
|
2762
|
+
const slice = candidates.slice(startIndex, startIndex + max);
|
|
2763
|
+
const canReadFileRetentions = this.requestHasCapability(authToken, require_auth.Capability.ReadFileRetentions);
|
|
2764
|
+
const canReadFileLegalHolds = this.requestHasCapability(authToken, require_auth.Capability.ReadFileLegalHolds);
|
|
1626
2765
|
return {
|
|
1627
2766
|
status: 200,
|
|
1628
2767
|
body: {
|
|
1629
|
-
files:
|
|
2768
|
+
files: slice.map((f) => ({
|
|
1630
2769
|
fileId: f.fileId,
|
|
1631
2770
|
fileName: f.fileName,
|
|
1632
2771
|
accountId: this.accountId,
|
|
@@ -1637,14 +2776,9 @@ var B2Simulator = class {
|
|
|
1637
2776
|
contentSha1: "none",
|
|
1638
2777
|
contentMd5: null,
|
|
1639
2778
|
fileInfo: f.fileInfo,
|
|
1640
|
-
fileRetention:
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
},
|
|
1644
|
-
legalHold: {
|
|
1645
|
-
isClientAuthorizedToRead: true,
|
|
1646
|
-
value: f.legalHold
|
|
1647
|
-
},
|
|
2779
|
+
fileRetention: readableFileRetention(f.fileRetention, canReadFileRetentions),
|
|
2780
|
+
legalHold: readableLegalHold(f.legalHold, canReadFileLegalHolds),
|
|
2781
|
+
...this.replicationStatusMetadataFor(f.replicationStatus),
|
|
1648
2782
|
serverSideEncryption: publicServerSideEncryption(f.serverSideEncryption),
|
|
1649
2783
|
uploadTimestamp: f.uploadTimestamp
|
|
1650
2784
|
})),
|
|
@@ -1664,7 +2798,9 @@ var B2Simulator = class {
|
|
|
1664
2798
|
partNumber,
|
|
1665
2799
|
contentLength: part.data.byteLength,
|
|
1666
2800
|
contentSha1: part.sha1,
|
|
1667
|
-
|
|
2801
|
+
contentMd5: part.contentMd5,
|
|
2802
|
+
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
2803
|
+
uploadTimestamp: part.uploadTimestamp
|
|
1668
2804
|
}));
|
|
1669
2805
|
return {
|
|
1670
2806
|
status: 200,
|
|
@@ -1677,22 +2813,29 @@ var B2Simulator = class {
|
|
|
1677
2813
|
async copyPart(req) {
|
|
1678
2814
|
const large = this.largeFiles.get(req.largeFileId);
|
|
1679
2815
|
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
2816
|
+
const partNumberError = this.validatePartNumber(req.partNumber);
|
|
2817
|
+
if (partNumberError !== null) return partNumberError;
|
|
1680
2818
|
const found = this.findFile(req.sourceFileId);
|
|
1681
2819
|
if (found === null) return this.error(404, "file_not_present", "Source file not found");
|
|
1682
2820
|
const sourceStored = found.stored;
|
|
2821
|
+
const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
|
|
2822
|
+
if (sourceEncryptionError !== null) return sourceEncryptionError;
|
|
2823
|
+
const destinationEncryptionError = await this.validateCopyPartDestinationEncryption(large, req.destinationServerSideEncryption);
|
|
2824
|
+
if (destinationEncryptionError !== null) return destinationEncryptionError;
|
|
1683
2825
|
let partData = sourceStored.data;
|
|
1684
|
-
if (req.range) {
|
|
1685
|
-
const
|
|
1686
|
-
if (
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
partData = sourceStored.data.slice(rs, re + 1);
|
|
1690
|
-
}
|
|
2826
|
+
if (req.range !== void 0) {
|
|
2827
|
+
const parsed = parseRangeHeader(req.range, sourceStored.data.byteLength);
|
|
2828
|
+
if (parsed.kind === "malformed") return this.error(400, "bad_request", `Malformed copy range: ${req.range}`);
|
|
2829
|
+
if (parsed.kind === "unsatisfiable") return this.error(416, "range_not_satisfiable", `Unsatisfiable copy range: ${req.range}`);
|
|
2830
|
+
partData = sourceStored.data.subarray(parsed.start, parsed.end + 1);
|
|
1691
2831
|
}
|
|
1692
2832
|
const sha1 = await require_hash.sha1Hex(partData);
|
|
2833
|
+
const uploadTimestamp = this.monotonicTimestamp();
|
|
1693
2834
|
large.parts.set(req.partNumber, {
|
|
1694
2835
|
data: new Uint8Array(partData),
|
|
1695
|
-
sha1
|
|
2836
|
+
sha1,
|
|
2837
|
+
contentMd5: null,
|
|
2838
|
+
uploadTimestamp
|
|
1696
2839
|
});
|
|
1697
2840
|
return {
|
|
1698
2841
|
status: 200,
|
|
@@ -1700,39 +2843,80 @@ var B2Simulator = class {
|
|
|
1700
2843
|
fileId: req.largeFileId,
|
|
1701
2844
|
partNumber: req.partNumber,
|
|
1702
2845
|
contentLength: partData.byteLength,
|
|
1703
|
-
contentSha1: sha1
|
|
2846
|
+
contentSha1: sha1,
|
|
2847
|
+
contentMd5: null,
|
|
2848
|
+
serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
|
|
2849
|
+
uploadTimestamp
|
|
1704
2850
|
}
|
|
1705
2851
|
};
|
|
1706
2852
|
}
|
|
1707
2853
|
getDownloadAuthorization(req) {
|
|
2854
|
+
this.purgeExpiredDownloadAuthorizationTokens();
|
|
1708
2855
|
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
2856
|
+
const prefixError = require_validation.validateDownloadAuthorizationPrefix(req.fileNamePrefix);
|
|
2857
|
+
if (prefixError) return this.error(400, prefixError.code, prefixError.message);
|
|
2858
|
+
const durationError = require_validation.validateDownloadAuthorizationDuration(req.validDurationInSeconds);
|
|
2859
|
+
if (durationError) return this.error(400, durationError.code, durationError.message);
|
|
2860
|
+
const overrideError = validateDownloadResponseOverrides(req);
|
|
2861
|
+
if (overrideError) return this.error(400, overrideError.code, overrideError.message);
|
|
2862
|
+
let authorizationToken = randomDownloadAuthorizationToken();
|
|
2863
|
+
while (this.downloadAuthorizationTokens.has(authorizationToken)) authorizationToken = randomDownloadAuthorizationToken();
|
|
2864
|
+
const expiresAt = this.now() + req.validDurationInSeconds * 1e3;
|
|
2865
|
+
this.downloadAuthorizationTokens.set(authorizationToken, {
|
|
2866
|
+
bucketId: req.bucketId,
|
|
2867
|
+
fileNamePrefix: req.fileNamePrefix,
|
|
2868
|
+
expiresAt,
|
|
2869
|
+
responseHeaderOverrides: pickDownloadResponseOverrides(req)
|
|
2870
|
+
});
|
|
2871
|
+
this.pushDownloadAuthorizationExpiry({
|
|
2872
|
+
token: authorizationToken,
|
|
2873
|
+
expiresAt
|
|
2874
|
+
});
|
|
1709
2875
|
return {
|
|
1710
2876
|
status: 200,
|
|
1711
2877
|
body: {
|
|
1712
2878
|
bucketId: req.bucketId,
|
|
1713
2879
|
fileNamePrefix: req.fileNamePrefix,
|
|
1714
|
-
authorizationToken
|
|
2880
|
+
authorizationToken
|
|
1715
2881
|
}
|
|
1716
2882
|
};
|
|
1717
2883
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
2884
|
+
createKeyCapabilitiesOutsideGrant(authToken, requested) {
|
|
2885
|
+
const creator = authToken === void 0 ? void 0 : this.issuedTokens.get(authToken);
|
|
2886
|
+
if (creator === void 0) return this.strictAuth ? requested : [];
|
|
2887
|
+
if (creator.applicationKeyId === null) return [];
|
|
2888
|
+
const granted = new Set(creator.capabilities);
|
|
2889
|
+
return requested.filter((capability) => !granted.has(capability));
|
|
2890
|
+
}
|
|
2891
|
+
createKeyCreatorAccountId(authToken) {
|
|
2892
|
+
return (authToken === void 0 ? void 0 : this.issuedTokens.get(authToken))?.accountId ?? this.accountId;
|
|
2893
|
+
}
|
|
2894
|
+
createKey(req, version, authToken) {
|
|
2895
|
+
if (version === "v4" && hasOwnField(req, "bucketId")) return this.error(400, "bad_request", "bucketId is not accepted by v4 b2_create_key; use bucketIds");
|
|
2896
|
+
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");
|
|
2897
|
+
const bucketIds = version !== "v4" && req.bucketId !== void 0 ? Object.freeze([req.bucketId]) : normalizeKeyBucketIds(req);
|
|
1722
2898
|
const namePrefix = req.namePrefix === void 0 || req.namePrefix === "" ? null : req.namePrefix;
|
|
1723
|
-
|
|
2899
|
+
const creatorAccountId = this.createKeyCreatorAccountId(authToken);
|
|
2900
|
+
if (req.accountId !== creatorAccountId) return this.error(400, "bad_request", "accountId must match authorized account");
|
|
2901
|
+
const keyNameError = require_validation.validateCreateKeyName(req.keyName);
|
|
2902
|
+
if (keyNameError) return this.error(400, keyNameError.code, keyNameError.message);
|
|
2903
|
+
const keyName = req.keyName;
|
|
2904
|
+
const capabilitiesError = require_validation.validateCreateKeyCapabilities(req.capabilities);
|
|
2905
|
+
if (capabilitiesError) return this.error(400, capabilitiesError.code, capabilitiesError.message);
|
|
2906
|
+
const capabilities = require_validation.normalizeCreateKeyCapabilities(req.capabilities);
|
|
2907
|
+
const unauthorizedCapabilities = this.createKeyCapabilitiesOutsideGrant(authToken, capabilities);
|
|
2908
|
+
if (unauthorizedCapabilities.length > 0) return this.error(400, "bad_request", `requested capabilities exceed creator grant: ${unauthorizedCapabilities.join(", ")}`);
|
|
2909
|
+
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");
|
|
1724
2910
|
const kid = this.genId("sim_key");
|
|
1725
|
-
const appKey = this.genId("sim_secret");
|
|
1726
|
-
const expiration = req.validDurationInSeconds !== void 0 ? Date.now() + req.validDurationInSeconds * 1e3 : null;
|
|
1727
2911
|
const stored = {
|
|
1728
2912
|
applicationKeyId: kid,
|
|
1729
|
-
keyName
|
|
1730
|
-
capabilities
|
|
1731
|
-
accountId:
|
|
1732
|
-
applicationKey:
|
|
2913
|
+
keyName,
|
|
2914
|
+
capabilities,
|
|
2915
|
+
accountId: creatorAccountId,
|
|
2916
|
+
applicationKey: this.genId("sim_secret"),
|
|
1733
2917
|
bucketIds,
|
|
1734
2918
|
namePrefix,
|
|
1735
|
-
expirationTimestamp:
|
|
2919
|
+
expirationTimestamp: req.validDurationInSeconds !== void 0 ? this.now() + req.validDurationInSeconds * 1e3 : null
|
|
1736
2920
|
};
|
|
1737
2921
|
this.keys.set(kid, stored);
|
|
1738
2922
|
return {
|
|
@@ -1741,7 +2925,7 @@ var B2Simulator = class {
|
|
|
1741
2925
|
keyName: stored.keyName,
|
|
1742
2926
|
applicationKeyId: stored.applicationKeyId,
|
|
1743
2927
|
applicationKey: stored.applicationKey,
|
|
1744
|
-
capabilities: stored.capabilities,
|
|
2928
|
+
capabilities: cloneCapabilities(stored.capabilities),
|
|
1745
2929
|
accountId: stored.accountId,
|
|
1746
2930
|
expirationTimestamp: stored.expirationTimestamp,
|
|
1747
2931
|
bucketIds: cloneBucketIds(stored.bucketIds),
|
|
@@ -1766,7 +2950,7 @@ var B2Simulator = class {
|
|
|
1766
2950
|
keys: allKeys.slice(0, max).map((k) => ({
|
|
1767
2951
|
keyName: k.keyName,
|
|
1768
2952
|
applicationKeyId: k.applicationKeyId,
|
|
1769
|
-
capabilities: k.capabilities,
|
|
2953
|
+
capabilities: cloneCapabilities(k.capabilities),
|
|
1770
2954
|
accountId: k.accountId,
|
|
1771
2955
|
expirationTimestamp: k.expirationTimestamp,
|
|
1772
2956
|
bucketIds: cloneBucketIds(k.bucketIds),
|
|
@@ -1792,7 +2976,7 @@ var B2Simulator = class {
|
|
|
1792
2976
|
body: {
|
|
1793
2977
|
keyName: key.keyName,
|
|
1794
2978
|
applicationKeyId: key.applicationKeyId,
|
|
1795
|
-
capabilities: key.capabilities,
|
|
2979
|
+
capabilities: cloneCapabilities(key.capabilities),
|
|
1796
2980
|
accountId: key.accountId,
|
|
1797
2981
|
expirationTimestamp: key.expirationTimestamp,
|
|
1798
2982
|
bucketIds: cloneBucketIds(key.bucketIds),
|
|
@@ -1802,31 +2986,45 @@ var B2Simulator = class {
|
|
|
1802
2986
|
}
|
|
1803
2987
|
};
|
|
1804
2988
|
}
|
|
1805
|
-
updateFileRetention(req) {
|
|
2989
|
+
updateFileRetention(req, authToken) {
|
|
1806
2990
|
const found = this.findFile(req.fileId);
|
|
1807
2991
|
if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
|
|
2992
|
+
const fileLockError = this.requireFileLockEnabled(found.bucket);
|
|
2993
|
+
if (fileLockError !== null) return fileLockError;
|
|
2994
|
+
const fileRetention = parseFileRetentionValue(req.fileRetention, Date.now());
|
|
2995
|
+
if (fileRetention === null) return this.error(400, "bad_request", "fileRetention must use a valid mode and retainUntilTimestamp pair");
|
|
2996
|
+
const current = found.stored.fileVersion.fileRetention?.value;
|
|
2997
|
+
if (current?.mode === require_lock.RetentionMode.Compliance && (fileRetention.mode !== require_lock.RetentionMode.Compliance || isRetentionWeakened(current, fileRetention))) return this.error(400, "file_lock_compliance_protected", "Compliance-mode retention cannot be shortened or removed");
|
|
2998
|
+
if (requiresGovernanceBypass(current, fileRetention)) {
|
|
2999
|
+
const bypassError = this.requireGovernanceBypass(req, authToken, "Governance-mode retention cannot be shortened, removed, or converted without bypassGovernance");
|
|
3000
|
+
if (bypassError !== null) return bypassError;
|
|
3001
|
+
}
|
|
1808
3002
|
found.versions[found.index] = {
|
|
1809
3003
|
fileVersion: {
|
|
1810
3004
|
...found.stored.fileVersion,
|
|
1811
3005
|
fileRetention: {
|
|
1812
3006
|
isClientAuthorizedToRead: true,
|
|
1813
|
-
value:
|
|
3007
|
+
value: fileRetention
|
|
1814
3008
|
}
|
|
1815
3009
|
},
|
|
1816
|
-
data: found.stored.data
|
|
3010
|
+
data: found.stored.data,
|
|
3011
|
+
serverSideEncryption: found.stored.serverSideEncryption
|
|
1817
3012
|
};
|
|
1818
3013
|
return {
|
|
1819
3014
|
status: 200,
|
|
1820
3015
|
body: {
|
|
1821
3016
|
fileName: req.fileName,
|
|
1822
3017
|
fileId: req.fileId,
|
|
1823
|
-
fileRetention
|
|
3018
|
+
fileRetention
|
|
1824
3019
|
}
|
|
1825
3020
|
};
|
|
1826
3021
|
}
|
|
1827
3022
|
updateFileLegalHold(req) {
|
|
1828
3023
|
const found = this.findFile(req.fileId);
|
|
1829
3024
|
if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
|
|
3025
|
+
const fileLockError = this.requireFileLockEnabled(found.bucket);
|
|
3026
|
+
if (fileLockError !== null) return fileLockError;
|
|
3027
|
+
if (req.legalHold !== require_lock.LegalHoldValue.On && req.legalHold !== require_lock.LegalHoldValue.Off) return this.error(400, "bad_request", "legalHold must be \"on\" or \"off\"");
|
|
1830
3028
|
found.versions[found.index] = {
|
|
1831
3029
|
fileVersion: {
|
|
1832
3030
|
...found.stored.fileVersion,
|
|
@@ -1835,7 +3033,8 @@ var B2Simulator = class {
|
|
|
1835
3033
|
value: req.legalHold
|
|
1836
3034
|
}
|
|
1837
3035
|
},
|
|
1838
|
-
data: found.stored.data
|
|
3036
|
+
data: found.stored.data,
|
|
3037
|
+
serverSideEncryption: found.stored.serverSideEncryption
|
|
1839
3038
|
};
|
|
1840
3039
|
return {
|
|
1841
3040
|
status: 200,
|
|
@@ -1859,6 +3058,8 @@ var B2Simulator = class {
|
|
|
1859
3058
|
}
|
|
1860
3059
|
setBucketNotificationRules(req) {
|
|
1861
3060
|
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
3061
|
+
const rulesError = require_validation.validateNotificationRules(req.eventNotificationRules);
|
|
3062
|
+
if (rulesError) return this.error(400, rulesError.code, rulesError.message);
|
|
1862
3063
|
this.notificationRules.set(req.bucketId, req.eventNotificationRules);
|
|
1863
3064
|
return {
|
|
1864
3065
|
status: 200,
|
|
@@ -1916,11 +3117,23 @@ var B2Simulator = class {
|
|
|
1916
3117
|
isClientAuthorizedToRead: true,
|
|
1917
3118
|
value: params.legalHold ?? null
|
|
1918
3119
|
},
|
|
1919
|
-
replicationStatus
|
|
3120
|
+
...this.replicationStatusMetadataFor(params.replicationStatus),
|
|
1920
3121
|
serverSideEncryption: publicServerSideEncryption(params.serverSideEncryption ?? { mode: require_encryption.EncryptionMode.None }),
|
|
1921
|
-
uploadTimestamp: this.monotonicTimestamp()
|
|
3122
|
+
uploadTimestamp: params.uploadTimestamp ?? this.monotonicTimestamp()
|
|
1922
3123
|
};
|
|
1923
3124
|
}
|
|
3125
|
+
newReplicationStatusField(bucketId, fileName) {
|
|
3126
|
+
return this.replicationStatusMetadataFor(this.replicationStatusForNewFile(bucketId, fileName));
|
|
3127
|
+
}
|
|
3128
|
+
replicationStatusMetadataFor(replicationStatus) {
|
|
3129
|
+
return replicationStatus === void 0 ? {} : { replicationStatus };
|
|
3130
|
+
}
|
|
3131
|
+
replicationStatusForNewFile(bucketId, fileName) {
|
|
3132
|
+
return this.matchingEnabledReplicationRules(bucketId, fileName).length > 0 ? "PENDING" : void 0;
|
|
3133
|
+
}
|
|
3134
|
+
matchingEnabledReplicationRules(bucketId, fileName) {
|
|
3135
|
+
return (this.buckets.get(bucketId)?.info.replicationConfiguration.value?.asReplicationSource?.replicationRules)?.filter((rule) => rule.isEnabled && fileName.startsWith(rule.fileNamePrefix)) ?? [];
|
|
3136
|
+
}
|
|
1924
3137
|
error(status, code, message) {
|
|
1925
3138
|
return {
|
|
1926
3139
|
status,
|
|
@@ -1948,8 +3161,7 @@ var B2Simulator = class {
|
|
|
1948
3161
|
* used to match against `EventNotificationRule.eventTypes` globs.
|
|
1949
3162
|
*/
|
|
1950
3163
|
firePostUploadHooks(fileVersion, bucketId, eventType) {
|
|
1951
|
-
|
|
1952
|
-
if (!bucket) return;
|
|
3164
|
+
if (!this.buckets.get(bucketId)) return;
|
|
1953
3165
|
if (this.onWebhookDeliver !== void 0) {
|
|
1954
3166
|
const rules = this.notificationRules.get(bucketId) ?? [];
|
|
1955
3167
|
for (const rule of rules) {
|
|
@@ -1963,18 +3175,13 @@ var B2Simulator = class {
|
|
|
1963
3175
|
}));
|
|
1964
3176
|
}
|
|
1965
3177
|
}
|
|
1966
|
-
if (this.onReplicate !== void 0) {
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
sourceFileVersion: fileVersion,
|
|
1974
|
-
sourceBucketId: bucketId,
|
|
1975
|
-
destinationBucketId: rule.destinationBucketId
|
|
1976
|
-
}));
|
|
1977
|
-
}
|
|
3178
|
+
if (this.onReplicate !== void 0) for (const rule of this.matchingEnabledReplicationRules(bucketId, fileVersion.fileName)) {
|
|
3179
|
+
const hook = this.onReplicate;
|
|
3180
|
+
this.dispatchHook("replication", () => hook({
|
|
3181
|
+
sourceFileVersion: fileVersion,
|
|
3182
|
+
sourceBucketId: bucketId,
|
|
3183
|
+
destinationBucketId: rule.destinationBucketId
|
|
3184
|
+
}));
|
|
1978
3185
|
}
|
|
1979
3186
|
}
|
|
1980
3187
|
/**
|
|
@@ -2058,6 +3265,83 @@ function buildFaultResponse(fault) {
|
|
|
2058
3265
|
arrayBuffer: () => Promise.resolve(require_text_codec.utf8Encoder.encode(body).buffer)
|
|
2059
3266
|
};
|
|
2060
3267
|
}
|
|
3268
|
+
function rawUrlPathContainsLiteralBackslash(rawUrl) {
|
|
3269
|
+
const schemeEnd = rawUrl.indexOf("://");
|
|
3270
|
+
const authorityStart = schemeEnd === -1 ? 0 : schemeEnd + 3;
|
|
3271
|
+
const urlEndCandidates = [rawUrl.indexOf("?", authorityStart), rawUrl.indexOf("#", authorityStart)].filter((index) => index !== -1);
|
|
3272
|
+
const urlEnd = urlEndCandidates.length === 0 ? rawUrl.length : Math.min(...urlEndCandidates);
|
|
3273
|
+
const pathStartCandidates = [rawUrl.indexOf("/", authorityStart), rawUrl.indexOf("\\", authorityStart)].filter((index) => index !== -1 && index < urlEnd);
|
|
3274
|
+
if (pathStartCandidates.length === 0) return false;
|
|
3275
|
+
const pathStart = Math.min(...pathStartCandidates);
|
|
3276
|
+
const pathEndCandidates = [rawUrl.indexOf("?", pathStart), rawUrl.indexOf("#", pathStart)].filter((index) => index !== -1);
|
|
3277
|
+
const pathEnd = pathEndCandidates.length === 0 ? rawUrl.length : Math.min(...pathEndCandidates);
|
|
3278
|
+
return rawUrl.slice(pathStart, pathEnd).includes("\\");
|
|
3279
|
+
}
|
|
3280
|
+
function badRequestJson(message) {
|
|
3281
|
+
return {
|
|
3282
|
+
status: 400,
|
|
3283
|
+
body: {
|
|
3284
|
+
status: 400,
|
|
3285
|
+
code: "bad_request",
|
|
3286
|
+
message
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function checkedUploadBody(data, expectedLength) {
|
|
3291
|
+
if (expectedLength !== null && data.byteLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, data.byteLength)) };
|
|
3292
|
+
return { data };
|
|
3293
|
+
}
|
|
3294
|
+
function copyBytes(bytes) {
|
|
3295
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
3296
|
+
copy.set(bytes);
|
|
3297
|
+
return copy;
|
|
3298
|
+
}
|
|
3299
|
+
function concatenateChunks(chunks, totalLength) {
|
|
3300
|
+
if (chunks.length === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
3301
|
+
if (chunks.length === 1) return copyBytes(chunks[0]);
|
|
3302
|
+
const data = new Uint8Array(totalLength);
|
|
3303
|
+
let offset = 0;
|
|
3304
|
+
for (const chunk of chunks) {
|
|
3305
|
+
data.set(chunk, offset);
|
|
3306
|
+
offset += chunk.byteLength;
|
|
3307
|
+
}
|
|
3308
|
+
return data;
|
|
3309
|
+
}
|
|
3310
|
+
async function readStreamingUploadBody(stream, expectedLength) {
|
|
3311
|
+
const reader = stream.getReader();
|
|
3312
|
+
const chunks = [];
|
|
3313
|
+
let totalLength = 0;
|
|
3314
|
+
try {
|
|
3315
|
+
while (true) {
|
|
3316
|
+
const { done, value } = await reader.read();
|
|
3317
|
+
if (done) break;
|
|
3318
|
+
if (value === void 0) continue;
|
|
3319
|
+
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
3320
|
+
totalLength += chunk.byteLength;
|
|
3321
|
+
if (expectedLength !== null && totalLength > expectedLength) {
|
|
3322
|
+
await reader.cancel().catch(() => void 0);
|
|
3323
|
+
return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
|
|
3324
|
+
}
|
|
3325
|
+
chunks.push(chunk);
|
|
3326
|
+
}
|
|
3327
|
+
} finally {
|
|
3328
|
+
reader.releaseLock();
|
|
3329
|
+
}
|
|
3330
|
+
if (expectedLength !== null && totalLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
|
|
3331
|
+
return { data: concatenateChunks(chunks, totalLength) };
|
|
3332
|
+
}
|
|
3333
|
+
async function readUploadBody(body, expectedLength) {
|
|
3334
|
+
if (body instanceof ReadableStream) return readStreamingUploadBody(body, expectedLength);
|
|
3335
|
+
if (body instanceof ArrayBuffer) return checkedUploadBody(new Uint8Array(body), expectedLength);
|
|
3336
|
+
if (ArrayBuffer.isView(body)) {
|
|
3337
|
+
const view = body;
|
|
3338
|
+
return checkedUploadBody(copyBytes(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)), expectedLength);
|
|
3339
|
+
}
|
|
3340
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
3341
|
+
if (expectedLength !== null && body.size !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, body.size)) };
|
|
3342
|
+
}
|
|
3343
|
+
return checkedUploadBody(new Uint8Array(await new Response(body).arrayBuffer()), expectedLength);
|
|
3344
|
+
}
|
|
2061
3345
|
var SimulatorTransport = class {
|
|
2062
3346
|
sim;
|
|
2063
3347
|
constructor(sim) {
|
|
@@ -2067,6 +3351,12 @@ var SimulatorTransport = class {
|
|
|
2067
3351
|
const url = request.url;
|
|
2068
3352
|
const fault = this.sim.consumeMatchingFault(url);
|
|
2069
3353
|
if (fault !== null) return buildFaultResponse(fault);
|
|
3354
|
+
if (rawUrlPathContainsLiteralBackslash(url)) return buildFaultResponse({
|
|
3355
|
+
on: "\\",
|
|
3356
|
+
status: 400,
|
|
3357
|
+
code: "bad_request",
|
|
3358
|
+
message: "URL path must not contain literal backslashes"
|
|
3359
|
+
});
|
|
2070
3360
|
const headers = {};
|
|
2071
3361
|
if (request.headers) for (const [k, v] of Object.entries(request.headers)) headers[k.toLowerCase()] = v;
|
|
2072
3362
|
const parsedUrl = new URL(url);
|
|
@@ -2074,10 +3364,15 @@ var SimulatorTransport = class {
|
|
|
2074
3364
|
const isUpload = endpoint === "b2_upload_file" || endpoint === "b2_upload_part";
|
|
2075
3365
|
if (parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/")) {
|
|
2076
3366
|
const method = request.method === "HEAD" ? "HEAD" : "GET";
|
|
2077
|
-
const result = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
|
|
3367
|
+
const result = await this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
|
|
2078
3368
|
const data = result.data ?? /* @__PURE__ */ new Uint8Array(0);
|
|
2079
3369
|
const responseHeaders = new Headers(result.headers);
|
|
2080
3370
|
responseHeaders.set("Content-Type", result.headers["Content-Type"] ?? "application/octet-stream");
|
|
3371
|
+
let decodedText = null;
|
|
3372
|
+
const text = () => {
|
|
3373
|
+
decodedText ??= require_text_codec.utf8Decoder.decode(data);
|
|
3374
|
+
return decodedText;
|
|
3375
|
+
};
|
|
2081
3376
|
const body = method === "HEAD" || result.data === null ? null : new ReadableStream({ start(controller) {
|
|
2082
3377
|
controller.enqueue(data);
|
|
2083
3378
|
controller.close();
|
|
@@ -2086,16 +3381,27 @@ var SimulatorTransport = class {
|
|
|
2086
3381
|
status: result.status,
|
|
2087
3382
|
headers: responseHeaders,
|
|
2088
3383
|
body,
|
|
2089
|
-
json: () =>
|
|
2090
|
-
|
|
3384
|
+
json: () => {
|
|
3385
|
+
try {
|
|
3386
|
+
return Promise.resolve(JSON.parse(text()));
|
|
3387
|
+
} catch {
|
|
3388
|
+
return Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON"));
|
|
3389
|
+
}
|
|
3390
|
+
},
|
|
3391
|
+
text: () => Promise.resolve(text()),
|
|
2091
3392
|
arrayBuffer: () => Promise.resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
|
|
2092
3393
|
};
|
|
2093
3394
|
}
|
|
2094
3395
|
let result;
|
|
2095
|
-
if (isUpload
|
|
2096
|
-
const
|
|
2097
|
-
result =
|
|
3396
|
+
if (isUpload) {
|
|
3397
|
+
const parsedContentLength = parseContentLengthHeader(headers);
|
|
3398
|
+
if (parsedContentLength.kind === "error") result = badRequestJson(parsedContentLength.message);
|
|
3399
|
+
else {
|
|
3400
|
+
const uploadBody = await readUploadBody(request.body ?? /* @__PURE__ */ new Uint8Array(0), parsedContentLength.kind === "ok" ? parsedContentLength.expectedLength : null);
|
|
3401
|
+
result = "error" in uploadBody ? uploadBody.error : await this.sim.handleUpload(url, headers, uploadBody.data);
|
|
3402
|
+
}
|
|
2098
3403
|
} else {
|
|
3404
|
+
const requestPath = apiPathParts(parsedUrl.pathname);
|
|
2099
3405
|
let body = null;
|
|
2100
3406
|
if (request.body) {
|
|
2101
3407
|
const text = typeof request.body === "string" ? request.body : await new Response(request.body).text();
|
|
@@ -2104,7 +3410,7 @@ var SimulatorTransport = class {
|
|
|
2104
3410
|
} catch {
|
|
2105
3411
|
body = text;
|
|
2106
3412
|
}
|
|
2107
|
-
}
|
|
3413
|
+
} else if (request.method.toUpperCase() === "GET" && isJsonQueryEndpoint(requestPath.endpoint)) body = queryParamsBody(parsedUrl.searchParams, requestPath.endpoint);
|
|
2108
3414
|
result = await this.sim.handleRequest(request.method, parsedUrl.origin, parsedUrl.pathname, headers, body);
|
|
2109
3415
|
}
|
|
2110
3416
|
const responseBody = JSON.stringify(result.body);
|
|
@@ -2123,13 +3429,29 @@ var SimulatorTransport = class {
|
|
|
2123
3429
|
};
|
|
2124
3430
|
//#endregion
|
|
2125
3431
|
exports.B2Simulator = B2Simulator;
|
|
3432
|
+
exports.BUCKET_INFO_KEY_MAX_BYTES = require_bucket.BUCKET_INFO_KEY_MAX_BYTES;
|
|
3433
|
+
exports.BUCKET_INFO_KEY_MIN_BYTES = require_bucket.BUCKET_INFO_KEY_MIN_BYTES;
|
|
3434
|
+
exports.BUCKET_INFO_KEY_PATTERN = require_bucket.BUCKET_INFO_KEY_PATTERN;
|
|
2126
3435
|
exports.BUCKET_INFO_MAX_KEYS = require_validation.BUCKET_INFO_MAX_KEYS;
|
|
3436
|
+
exports.BUCKET_INFO_RESERVED_PREFIX = require_bucket.BUCKET_INFO_RESERVED_PREFIX;
|
|
3437
|
+
exports.BUCKET_INFO_VALUES_MAX_BYTES = require_bucket.BUCKET_INFO_VALUES_MAX_BYTES;
|
|
2127
3438
|
exports.BUCKET_INFO_VALUE_MAX = require_validation.BUCKET_INFO_VALUE_MAX;
|
|
2128
3439
|
exports.BUCKET_NAME_MAX = require_b2_naming.BUCKET_NAME_MAX;
|
|
2129
3440
|
exports.BUCKET_NAME_MIN = require_b2_naming.BUCKET_NAME_MIN;
|
|
3441
|
+
exports.CORS_MAX_AGE_SECONDS_MAX = require_bucket.CORS_MAX_AGE_SECONDS_MAX;
|
|
3442
|
+
exports.CORS_RULES_MAX_COUNT = require_bucket.CORS_RULES_MAX_COUNT;
|
|
3443
|
+
exports.CORS_RULE_MAX_BYTES = require_bucket.CORS_RULE_MAX_BYTES;
|
|
3444
|
+
exports.CORS_RULE_NAME_MAX_LENGTH = require_bucket.CORS_RULE_NAME_MAX_LENGTH;
|
|
3445
|
+
exports.CORS_RULE_NAME_MIN_LENGTH = require_bucket.CORS_RULE_NAME_MIN_LENGTH;
|
|
3446
|
+
exports.CORS_RULE_NAME_PATTERN = require_bucket.CORS_RULE_NAME_PATTERN;
|
|
3447
|
+
exports.CORS_RULE_NAME_RESERVED_PREFIX = require_bucket.CORS_RULE_NAME_RESERVED_PREFIX;
|
|
3448
|
+
exports.DOWNLOAD_AUTH_DURATION_MAX_SECONDS = require_validation.DOWNLOAD_AUTH_DURATION_MAX_SECONDS;
|
|
3449
|
+
exports.DOWNLOAD_AUTH_DURATION_MIN_SECONDS = require_validation.DOWNLOAD_AUTH_DURATION_MIN_SECONDS;
|
|
2130
3450
|
exports.FILE_INFO_TOTAL_MAX = require_validation.FILE_INFO_TOTAL_MAX;
|
|
2131
3451
|
exports.FILE_INFO_VALUE_MAX = require_validation.FILE_INFO_VALUE_MAX;
|
|
2132
3452
|
exports.FILE_NAME_MAX_BYTES = require_b2_naming.FILE_NAME_MAX_BYTES;
|
|
3453
|
+
exports.KEY_NAME_MAX = require_validation.KEY_NAME_MAX;
|
|
3454
|
+
exports.KEY_NAME_MIN = require_validation.KEY_NAME_MIN;
|
|
2133
3455
|
exports.LIST_ENDPOINT_CAPS = require_validation.LIST_ENDPOINT_CAPS;
|
|
2134
3456
|
|
|
2135
3457
|
//# sourceMappingURL=index.cjs.map
|