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