@backblaze-labs/b2-sdk 0.1.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 +121 -0
- package/LICENSE +21 -0
- package/README.md +664 -0
- package/dist/auth/account-info.d.cts +51 -0
- package/dist/auth/account-info.d.cts.map +1 -0
- package/dist/auth/account-info.d.ts +51 -0
- package/dist/auth/account-info.d.ts.map +1 -0
- package/dist/auth/file.cjs +210 -0
- package/dist/auth/file.cjs.map +1 -0
- package/dist/auth/file.d.cts +157 -0
- package/dist/auth/file.d.cts.map +1 -0
- package/dist/auth/file.d.ts +157 -0
- package/dist/auth/file.d.ts.map +1 -0
- package/dist/auth/file.js +210 -0
- package/dist/auth/file.js.map +1 -0
- package/dist/auth/in-memory.cjs +184 -0
- package/dist/auth/in-memory.cjs.map +1 -0
- package/dist/auth/in-memory.d.cts +147 -0
- package/dist/auth/in-memory.d.cts.map +1 -0
- package/dist/auth/in-memory.d.ts +147 -0
- package/dist/auth/in-memory.d.ts.map +1 -0
- package/dist/auth/in-memory.js +184 -0
- package/dist/auth/in-memory.js.map +1 -0
- package/dist/auth/index.cjs +10 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +6 -0
- package/dist/auth/index.d.cts.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +10 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/realms.cjs +12 -0
- package/dist/auth/realms.cjs.map +1 -0
- package/dist/auth/realms.d.cts +12 -0
- package/dist/auth/realms.d.cts.map +1 -0
- package/dist/auth/realms.d.ts +12 -0
- package/dist/auth/realms.d.ts.map +1 -0
- package/dist/auth/realms.js +12 -0
- package/dist/auth/realms.js.map +1 -0
- package/dist/auth/upload-url-pool.cjs +52 -0
- package/dist/auth/upload-url-pool.cjs.map +1 -0
- package/dist/auth/upload-url-pool.d.cts +35 -0
- package/dist/auth/upload-url-pool.d.cts.map +1 -0
- package/dist/auth/upload-url-pool.d.ts +35 -0
- package/dist/auth/upload-url-pool.d.ts.map +1 -0
- package/dist/auth/upload-url-pool.js +52 -0
- package/dist/auth/upload-url-pool.js.map +1 -0
- package/dist/bucket.cjs +789 -0
- package/dist/bucket.cjs.map +1 -0
- package/dist/bucket.d.cts +659 -0
- package/dist/bucket.d.cts.map +1 -0
- package/dist/bucket.d.ts +659 -0
- package/dist/bucket.d.ts.map +1 -0
- package/dist/bucket.js +789 -0
- package/dist/bucket.js.map +1 -0
- package/dist/client.cjs +230 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +222 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.ts +222 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +230 -0
- package/dist/client.js.map +1 -0
- package/dist/copy/large.cjs +74 -0
- package/dist/copy/large.cjs.map +1 -0
- package/dist/copy/large.d.cts +53 -0
- package/dist/copy/large.d.cts.map +1 -0
- package/dist/copy/large.d.ts +53 -0
- package/dist/copy/large.d.ts.map +1 -0
- package/dist/copy/large.js +74 -0
- package/dist/copy/large.js.map +1 -0
- package/dist/download/parallel.cjs +122 -0
- package/dist/download/parallel.cjs.map +1 -0
- package/dist/download/parallel.d.cts +42 -0
- package/dist/download/parallel.d.cts.map +1 -0
- package/dist/download/parallel.d.ts +42 -0
- package/dist/download/parallel.d.ts.map +1 -0
- package/dist/download/parallel.js +122 -0
- package/dist/download/parallel.js.map +1 -0
- package/dist/download/single.cjs +117 -0
- package/dist/download/single.cjs.map +1 -0
- package/dist/download/single.d.cts +152 -0
- package/dist/download/single.d.cts.map +1 -0
- package/dist/download/single.d.ts +152 -0
- package/dist/download/single.d.ts.map +1 -0
- package/dist/download/single.js +117 -0
- package/dist/download/single.js.map +1 -0
- package/dist/errors/index.cjs +268 -0
- package/dist/errors/index.cjs.map +1 -0
- package/dist/errors/index.d.cts +264 -0
- package/dist/errors/index.d.cts.map +1 -0
- package/dist/errors/index.d.ts +264 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +268 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http/retry.cjs +36 -0
- package/dist/http/retry.cjs.map +1 -0
- package/dist/http/retry.d.cts +34 -0
- package/dist/http/retry.d.cts.map +1 -0
- package/dist/http/retry.d.ts +34 -0
- package/dist/http/retry.d.ts.map +1 -0
- package/dist/http/retry.js +36 -0
- package/dist/http/retry.js.map +1 -0
- package/dist/http/transport.cjs +143 -0
- package/dist/http/transport.cjs.map +1 -0
- package/dist/http/transport.d.cts +122 -0
- package/dist/http/transport.d.cts.map +1 -0
- package/dist/http/transport.d.ts +122 -0
- package/dist/http/transport.d.ts.map +1 -0
- package/dist/http/transport.js +143 -0
- package/dist/http/transport.js.map +1 -0
- package/dist/http/url-guard.cjs +97 -0
- package/dist/http/url-guard.cjs.map +1 -0
- package/dist/http/url-guard.d.cts +88 -0
- package/dist/http/url-guard.d.cts.map +1 -0
- package/dist/http/url-guard.d.ts +88 -0
- package/dist/http/url-guard.d.ts.map +1 -0
- package/dist/http/url-guard.js +97 -0
- package/dist/http/url-guard.js.map +1 -0
- package/dist/http/user-agent.cjs +51 -0
- package/dist/http/user-agent.cjs.map +1 -0
- package/dist/http/user-agent.d.cts +28 -0
- package/dist/http/user-agent.d.cts.map +1 -0
- package/dist/http/user-agent.d.ts +28 -0
- package/dist/http/user-agent.d.ts.map +1 -0
- package/dist/http/user-agent.js +51 -0
- package/dist/http/user-agent.js.map +1 -0
- package/dist/index.cjs +78 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/index.cjs +7 -0
- package/dist/notifications/index.cjs.map +1 -0
- package/dist/notifications/index.d.cts +3 -0
- package/dist/notifications/index.d.cts.map +1 -0
- package/dist/notifications/index.d.ts +3 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/webhook.cjs +106 -0
- package/dist/notifications/webhook.cjs.map +1 -0
- package/dist/notifications/webhook.d.cts +146 -0
- package/dist/notifications/webhook.d.cts.map +1 -0
- package/dist/notifications/webhook.d.ts +146 -0
- package/dist/notifications/webhook.d.ts.map +1 -0
- package/dist/notifications/webhook.js +106 -0
- package/dist/notifications/webhook.js.map +1 -0
- package/dist/object.cjs +212 -0
- package/dist/object.cjs.map +1 -0
- package/dist/object.d.cts +263 -0
- package/dist/object.d.cts.map +1 -0
- package/dist/object.d.ts +263 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +212 -0
- package/dist/object.js.map +1 -0
- package/dist/package.json.cjs +9 -0
- package/dist/package.json.cjs.map +1 -0
- package/dist/package.json.js +9 -0
- package/dist/package.json.js.map +1 -0
- package/dist/raw/encoding.cjs +47 -0
- package/dist/raw/encoding.cjs.map +1 -0
- package/dist/raw/encoding.d.cts +46 -0
- package/dist/raw/encoding.d.cts.map +1 -0
- package/dist/raw/encoding.d.ts +46 -0
- package/dist/raw/encoding.d.ts.map +1 -0
- package/dist/raw/encoding.js +47 -0
- package/dist/raw/encoding.js.map +1 -0
- package/dist/raw/index.cjs +600 -0
- package/dist/raw/index.cjs.map +1 -0
- package/dist/raw/index.d.cts +381 -0
- package/dist/raw/index.d.cts.map +1 -0
- package/dist/raw/index.d.ts +381 -0
- package/dist/raw/index.d.ts.map +1 -0
- package/dist/raw/index.js +601 -0
- package/dist/raw/index.js.map +1 -0
- package/dist/s3/index.cjs +23 -0
- package/dist/s3/index.cjs.map +1 -0
- package/dist/s3/index.d.cts +74 -0
- package/dist/s3/index.d.cts.map +1 -0
- package/dist/s3/index.d.ts +74 -0
- package/dist/s3/index.d.ts.map +1 -0
- package/dist/s3/index.js +23 -0
- package/dist/s3/index.js.map +1 -0
- package/dist/simulator/capabilities.cjs +53 -0
- package/dist/simulator/capabilities.cjs.map +1 -0
- package/dist/simulator/capabilities.d.cts +15 -0
- package/dist/simulator/capabilities.d.cts.map +1 -0
- package/dist/simulator/capabilities.d.ts +15 -0
- package/dist/simulator/capabilities.d.ts.map +1 -0
- package/dist/simulator/capabilities.js +53 -0
- package/dist/simulator/capabilities.js.map +1 -0
- package/dist/simulator/index.cjs +1559 -0
- package/dist/simulator/index.cjs.map +1 -0
- package/dist/simulator/index.d.cts +495 -0
- package/dist/simulator/index.d.cts.map +1 -0
- package/dist/simulator/index.d.ts +495 -0
- package/dist/simulator/index.d.ts.map +1 -0
- package/dist/simulator/index.js +1560 -0
- package/dist/simulator/index.js.map +1 -0
- package/dist/simulator/validation.cjs +174 -0
- package/dist/simulator/validation.cjs.map +1 -0
- package/dist/simulator/validation.d.cts +104 -0
- package/dist/simulator/validation.d.cts.map +1 -0
- package/dist/simulator/validation.d.ts +104 -0
- package/dist/simulator/validation.d.ts.map +1 -0
- package/dist/simulator/validation.js +174 -0
- package/dist/simulator/validation.js.map +1 -0
- package/dist/streams/collect.cjs +26 -0
- package/dist/streams/collect.cjs.map +1 -0
- package/dist/streams/collect.d.cts +18 -0
- package/dist/streams/collect.d.cts.map +1 -0
- package/dist/streams/collect.d.ts +18 -0
- package/dist/streams/collect.d.ts.map +1 -0
- package/dist/streams/collect.js +26 -0
- package/dist/streams/collect.js.map +1 -0
- package/dist/streams/hash.cjs +123 -0
- package/dist/streams/hash.cjs.map +1 -0
- package/dist/streams/hash.d.cts +42 -0
- package/dist/streams/hash.d.cts.map +1 -0
- package/dist/streams/hash.d.ts +42 -0
- package/dist/streams/hash.d.ts.map +1 -0
- package/dist/streams/hash.js +101 -0
- package/dist/streams/hash.js.map +1 -0
- package/dist/streams/index.cjs +13 -0
- package/dist/streams/index.cjs.map +1 -0
- package/dist/streams/index.d.cts +7 -0
- package/dist/streams/index.d.cts.map +1 -0
- package/dist/streams/index.d.ts +7 -0
- package/dist/streams/index.d.ts.map +1 -0
- package/dist/streams/index.js +13 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/progress.cjs +47 -0
- package/dist/streams/progress.cjs.map +1 -0
- package/dist/streams/progress.d.cts +53 -0
- package/dist/streams/progress.d.cts.map +1 -0
- package/dist/streams/progress.d.ts +53 -0
- package/dist/streams/progress.d.ts.map +1 -0
- package/dist/streams/progress.js +47 -0
- package/dist/streams/progress.js.map +1 -0
- package/dist/streams/source.cjs +155 -0
- package/dist/streams/source.cjs.map +1 -0
- package/dist/streams/source.d.cts +140 -0
- package/dist/streams/source.d.cts.map +1 -0
- package/dist/streams/source.d.ts +140 -0
- package/dist/streams/source.d.ts.map +1 -0
- package/dist/streams/source.js +155 -0
- package/dist/streams/source.js.map +1 -0
- package/dist/sync/actions/index.cjs +191 -0
- package/dist/sync/actions/index.cjs.map +1 -0
- package/dist/sync/actions/index.d.cts +167 -0
- package/dist/sync/actions/index.d.cts.map +1 -0
- package/dist/sync/actions/index.d.ts +167 -0
- package/dist/sync/actions/index.d.ts.map +1 -0
- package/dist/sync/actions/index.js +191 -0
- package/dist/sync/actions/index.js.map +1 -0
- package/dist/sync/index.cjs +23 -0
- package/dist/sync/index.cjs.map +1 -0
- package/dist/sync/index.d.cts +21 -0
- package/dist/sync/index.d.cts.map +1 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +23 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/pairing.cjs +31 -0
- package/dist/sync/pairing.cjs.map +1 -0
- package/dist/sync/pairing.d.cts +13 -0
- package/dist/sync/pairing.d.cts.map +1 -0
- package/dist/sync/pairing.d.ts +13 -0
- package/dist/sync/pairing.d.ts.map +1 -0
- package/dist/sync/pairing.js +31 -0
- package/dist/sync/pairing.js.map +1 -0
- package/dist/sync/policies/compare.cjs +14 -0
- package/dist/sync/policies/compare.cjs.map +1 -0
- package/dist/sync/policies/compare.d.cts +13 -0
- package/dist/sync/policies/compare.d.cts.map +1 -0
- package/dist/sync/policies/compare.d.ts +13 -0
- package/dist/sync/policies/compare.d.ts.map +1 -0
- package/dist/sync/policies/compare.js +14 -0
- package/dist/sync/policies/compare.js.map +1 -0
- package/dist/sync/policies/index.cjs +74 -0
- package/dist/sync/policies/index.cjs.map +1 -0
- package/dist/sync/policies/index.d.cts +42 -0
- package/dist/sync/policies/index.d.cts.map +1 -0
- package/dist/sync/policies/index.d.ts +42 -0
- package/dist/sync/policies/index.d.ts.map +1 -0
- package/dist/sync/policies/index.js +74 -0
- package/dist/sync/policies/index.js.map +1 -0
- package/dist/sync/scanners/b2.cjs +57 -0
- package/dist/sync/scanners/b2.cjs.map +1 -0
- package/dist/sync/scanners/b2.d.cts +20 -0
- package/dist/sync/scanners/b2.d.cts.map +1 -0
- package/dist/sync/scanners/b2.d.ts +20 -0
- package/dist/sync/scanners/b2.d.ts.map +1 -0
- package/dist/sync/scanners/b2.js +57 -0
- package/dist/sync/scanners/b2.js.map +1 -0
- package/dist/sync/scanners/local.cjs +58 -0
- package/dist/sync/scanners/local.cjs.map +1 -0
- package/dist/sync/scanners/local.d.cts +24 -0
- package/dist/sync/scanners/local.d.cts.map +1 -0
- package/dist/sync/scanners/local.d.ts +24 -0
- package/dist/sync/scanners/local.d.ts.map +1 -0
- package/dist/sync/scanners/local.js +58 -0
- package/dist/sync/scanners/local.js.map +1 -0
- package/dist/sync/synchronizer.cjs +203 -0
- package/dist/sync/synchronizer.cjs.map +1 -0
- package/dist/sync/synchronizer.d.cts +52 -0
- package/dist/sync/synchronizer.d.cts.map +1 -0
- package/dist/sync/synchronizer.d.ts +52 -0
- package/dist/sync/synchronizer.d.ts.map +1 -0
- package/dist/sync/synchronizer.js +181 -0
- package/dist/sync/synchronizer.js.map +1 -0
- package/dist/sync/types.d.cts +121 -0
- package/dist/sync/types.d.cts.map +1 -0
- package/dist/sync/types.d.ts +121 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/types/auth.cjs +58 -0
- package/dist/types/auth.cjs.map +1 -0
- package/dist/types/auth.d.cts +150 -0
- package/dist/types/auth.d.cts.map +1 -0
- package/dist/types/auth.d.ts +150 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +58 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/bucket.cjs +44 -0
- package/dist/types/bucket.cjs.map +1 -0
- package/dist/types/bucket.d.cts +245 -0
- package/dist/types/bucket.d.cts.map +1 -0
- package/dist/types/bucket.d.ts +245 -0
- package/dist/types/bucket.d.ts.map +1 -0
- package/dist/types/bucket.js +44 -0
- package/dist/types/bucket.js.map +1 -0
- package/dist/types/download.d.cts +110 -0
- package/dist/types/download.d.cts.map +1 -0
- package/dist/types/download.d.ts +110 -0
- package/dist/types/download.d.ts.map +1 -0
- package/dist/types/encryption.cjs +343 -0
- package/dist/types/encryption.cjs.map +1 -0
- package/dist/types/encryption.d.cts +132 -0
- package/dist/types/encryption.d.cts.map +1 -0
- package/dist/types/encryption.d.ts +132 -0
- package/dist/types/encryption.d.ts.map +1 -0
- package/dist/types/encryption.js +321 -0
- package/dist/types/encryption.js.map +1 -0
- package/dist/types/errors.d.cts +16 -0
- package/dist/types/errors.d.cts.map +1 -0
- package/dist/types/errors.d.ts +16 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/file.cjs +23 -0
- package/dist/types/file.cjs.map +1 -0
- package/dist/types/file.d.cts +220 -0
- package/dist/types/file.d.cts.map +1 -0
- package/dist/types/file.d.ts +220 -0
- package/dist/types/file.d.ts.map +1 -0
- package/dist/types/file.js +23 -0
- package/dist/types/file.js.map +1 -0
- package/dist/types/ids.cjs +27 -0
- package/dist/types/ids.cjs.map +1 -0
- package/dist/types/ids.d.cts +84 -0
- package/dist/types/ids.d.cts.map +1 -0
- package/dist/types/ids.d.ts +84 -0
- package/dist/types/ids.d.ts.map +1 -0
- package/dist/types/ids.js +27 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/index.d.cts +25 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/key.d.cts +84 -0
- package/dist/types/key.d.cts.map +1 -0
- package/dist/types/key.d.ts +84 -0
- package/dist/types/key.d.ts.map +1 -0
- package/dist/types/lock.cjs +17 -0
- package/dist/types/lock.cjs.map +1 -0
- package/dist/types/lock.d.cts +92 -0
- package/dist/types/lock.d.cts.map +1 -0
- package/dist/types/lock.d.ts +92 -0
- package/dist/types/lock.d.ts.map +1 -0
- package/dist/types/lock.js +17 -0
- package/dist/types/lock.js.map +1 -0
- package/dist/types/notifications.cjs +24 -0
- package/dist/types/notifications.cjs.map +1 -0
- package/dist/types/notifications.d.cts +97 -0
- package/dist/types/notifications.d.cts.map +1 -0
- package/dist/types/notifications.d.ts +97 -0
- package/dist/types/notifications.d.ts.map +1 -0
- package/dist/types/notifications.js +24 -0
- package/dist/types/notifications.js.map +1 -0
- package/dist/types/replication.d.cts +42 -0
- package/dist/types/replication.d.cts.map +1 -0
- package/dist/types/replication.d.ts +42 -0
- package/dist/types/replication.d.ts.map +1 -0
- package/dist/types/upload.d.cts +219 -0
- package/dist/types/upload.d.cts.map +1 -0
- package/dist/types/upload.d.ts +219 -0
- package/dist/types/upload.d.ts.map +1 -0
- package/dist/upload/cancel.cjs +10 -0
- package/dist/upload/cancel.cjs.map +1 -0
- package/dist/upload/cancel.d.cts +23 -0
- package/dist/upload/cancel.d.cts.map +1 -0
- package/dist/upload/cancel.d.ts +23 -0
- package/dist/upload/cancel.d.ts.map +1 -0
- package/dist/upload/cancel.js +10 -0
- package/dist/upload/cancel.js.map +1 -0
- package/dist/upload/concurrency.cjs +54 -0
- package/dist/upload/concurrency.cjs.map +1 -0
- package/dist/upload/concurrency.d.cts +46 -0
- package/dist/upload/concurrency.d.cts.map +1 -0
- package/dist/upload/concurrency.d.ts +46 -0
- package/dist/upload/concurrency.d.ts.map +1 -0
- package/dist/upload/concurrency.js +54 -0
- package/dist/upload/concurrency.js.map +1 -0
- package/dist/upload/large.cjs +222 -0
- package/dist/upload/large.cjs.map +1 -0
- package/dist/upload/large.d.cts +71 -0
- package/dist/upload/large.d.cts.map +1 -0
- package/dist/upload/large.d.ts +71 -0
- package/dist/upload/large.d.ts.map +1 -0
- package/dist/upload/large.js +222 -0
- package/dist/upload/large.js.map +1 -0
- package/dist/upload/resume.cjs +34 -0
- package/dist/upload/resume.cjs.map +1 -0
- package/dist/upload/resume.d.cts +33 -0
- package/dist/upload/resume.d.cts.map +1 -0
- package/dist/upload/resume.d.ts +33 -0
- package/dist/upload/resume.d.ts.map +1 -0
- package/dist/upload/resume.js +34 -0
- package/dist/upload/resume.js.map +1 -0
- package/dist/upload/single.cjs +47 -0
- package/dist/upload/single.cjs.map +1 -0
- package/dist/upload/single.d.cts +51 -0
- package/dist/upload/single.d.cts.map +1 -0
- package/dist/upload/single.d.ts +51 -0
- package/dist/upload/single.d.ts.map +1 -0
- package/dist/upload/single.js +47 -0
- package/dist/upload/single.js.map +1 -0
- package/dist/upload/stream.cjs +198 -0
- package/dist/upload/stream.cjs.map +1 -0
- package/dist/upload/stream.d.cts +61 -0
- package/dist/upload/stream.d.cts.map +1 -0
- package/dist/upload/stream.d.ts +61 -0
- package/dist/upload/stream.d.ts.map +1 -0
- package/dist/upload/stream.js +198 -0
- package/dist/upload/stream.js.map +1 -0
- package/dist/util/best-effort.cjs +10 -0
- package/dist/util/best-effort.cjs.map +1 -0
- package/dist/util/best-effort.d.cts +31 -0
- package/dist/util/best-effort.d.cts.map +1 -0
- package/dist/util/best-effort.d.ts +31 -0
- package/dist/util/best-effort.d.ts.map +1 -0
- package/dist/util/best-effort.js +10 -0
- package/dist/util/best-effort.js.map +1 -0
- package/dist/util/defaults.cjs +11 -0
- package/dist/util/defaults.cjs.map +1 -0
- package/dist/util/defaults.d.cts +54 -0
- package/dist/util/defaults.d.cts.map +1 -0
- package/dist/util/defaults.d.ts +54 -0
- package/dist/util/defaults.d.ts.map +1 -0
- package/dist/util/defaults.js +11 -0
- package/dist/util/defaults.js.map +1 -0
- package/dist/util/normalize.cjs +16 -0
- package/dist/util/normalize.cjs.map +1 -0
- package/dist/util/normalize.d.cts +62 -0
- package/dist/util/normalize.d.cts.map +1 -0
- package/dist/util/normalize.d.ts +62 -0
- package/dist/util/normalize.d.ts.map +1 -0
- package/dist/util/normalize.js +16 -0
- package/dist/util/normalize.js.map +1 -0
- package/dist/util/paginator.cjs +20 -0
- package/dist/util/paginator.cjs.map +1 -0
- package/dist/util/paginator.d.cts +120 -0
- package/dist/util/paginator.d.cts.map +1 -0
- package/dist/util/paginator.d.ts +120 -0
- package/dist/util/paginator.d.ts.map +1 -0
- package/dist/util/paginator.js +20 -0
- package/dist/util/paginator.js.map +1 -0
- package/dist/util/plan-ranges.cjs +28 -0
- package/dist/util/plan-ranges.cjs.map +1 -0
- package/dist/util/plan-ranges.d.cts +56 -0
- package/dist/util/plan-ranges.d.cts.map +1 -0
- package/dist/util/plan-ranges.d.ts +56 -0
- package/dist/util/plan-ranges.d.ts.map +1 -0
- package/dist/util/plan-ranges.js +28 -0
- package/dist/util/plan-ranges.js.map +1 -0
- package/dist/util/text-codec.cjs +7 -0
- package/dist/util/text-codec.cjs.map +1 -0
- package/dist/util/text-codec.d.cts +26 -0
- package/dist/util/text-codec.d.cts.map +1 -0
- package/dist/util/text-codec.d.ts +26 -0
- package/dist/util/text-codec.d.ts.map +1 -0
- package/dist/util/text-codec.js +7 -0
- package/dist/util/text-codec.js.map +1 -0
- package/dist/util/to-error.cjs +7 -0
- package/dist/util/to-error.cjs.map +1 -0
- package/dist/util/to-error.d.cts +15 -0
- package/dist/util/to-error.d.cts.map +1 -0
- package/dist/util/to-error.d.ts +15 -0
- package/dist/util/to-error.d.ts.map +1 -0
- package/dist/util/to-error.js +7 -0
- package/dist/util/to-error.js.map +1 -0
- package/dist/version.cjs +6 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.d.cts +14 -0
- package/dist/version.d.cts.map +1 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +198 -0
|
@@ -0,0 +1,1560 @@
|
|
|
1
|
+
import { sha1Hex } from "../streams/hash.js";
|
|
2
|
+
import { Capability } from "../types/auth.js";
|
|
3
|
+
import { BucketRetentionMode } from "../types/bucket.js";
|
|
4
|
+
import { EncryptionMode } from "../types/encryption.js";
|
|
5
|
+
import { FileAction } from "../types/file.js";
|
|
6
|
+
import { accountId, bucketId, fileId } from "../types/ids.js";
|
|
7
|
+
import { utf8Encoder, utf8Decoder } from "../util/text-codec.js";
|
|
8
|
+
import { toError } from "../util/to-error.js";
|
|
9
|
+
import { missingCapabilitiesFor } from "./capabilities.js";
|
|
10
|
+
import { validateFileName, validateFileInfo, validateBucketName, validateBucketInfo, validateMaxCount } from "./validation.js";
|
|
11
|
+
import { 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 } from "./validation.js";
|
|
12
|
+
function parseRangeHeader(header, totalSize) {
|
|
13
|
+
const m = header.match(/^bytes=(\d*)-(\d*)$/);
|
|
14
|
+
if (!m) return { kind: "malformed" };
|
|
15
|
+
const [, startStr, endStr] = m;
|
|
16
|
+
const hasStart = startStr !== "";
|
|
17
|
+
const hasEnd = endStr !== "";
|
|
18
|
+
if (!hasStart && !hasEnd) return { kind: "malformed" };
|
|
19
|
+
if (totalSize === 0) return { kind: "unsatisfiable" };
|
|
20
|
+
let start;
|
|
21
|
+
let end;
|
|
22
|
+
if (!hasStart) {
|
|
23
|
+
const suffixLen = Number.parseInt(endStr ?? "0", 10);
|
|
24
|
+
if (!Number.isFinite(suffixLen) || suffixLen <= 0) return { kind: "malformed" };
|
|
25
|
+
start = Math.max(0, totalSize - suffixLen);
|
|
26
|
+
end = totalSize - 1;
|
|
27
|
+
} else if (!hasEnd) {
|
|
28
|
+
start = Number.parseInt(startStr ?? "0", 10);
|
|
29
|
+
end = totalSize - 1;
|
|
30
|
+
} else {
|
|
31
|
+
start = Number.parseInt(startStr ?? "0", 10);
|
|
32
|
+
end = Number.parseInt(endStr ?? "0", 10);
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start > end) {
|
|
35
|
+
return { kind: "malformed" };
|
|
36
|
+
}
|
|
37
|
+
if (start >= totalSize) return { kind: "unsatisfiable" };
|
|
38
|
+
end = Math.min(end, totalSize - 1);
|
|
39
|
+
return { kind: "ok", start, end };
|
|
40
|
+
}
|
|
41
|
+
function parseFileInfoHeaders(headers) {
|
|
42
|
+
const info = {};
|
|
43
|
+
const prefix = "x-bz-info-";
|
|
44
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
45
|
+
if (!key.startsWith(prefix)) continue;
|
|
46
|
+
const fileInfoKey = decodeURIComponent(key.slice(prefix.length));
|
|
47
|
+
info[fileInfoKey] = decodeURIComponent(value);
|
|
48
|
+
}
|
|
49
|
+
return info;
|
|
50
|
+
}
|
|
51
|
+
class B2Simulator {
|
|
52
|
+
buckets = /* @__PURE__ */ new Map();
|
|
53
|
+
accountId = "sim_account_0001";
|
|
54
|
+
largeFiles = /* @__PURE__ */ new Map();
|
|
55
|
+
keys = /* @__PURE__ */ new Map();
|
|
56
|
+
notificationRules = /* @__PURE__ */ new Map();
|
|
57
|
+
minimumPartSize;
|
|
58
|
+
recommendedPartSize;
|
|
59
|
+
faults = [];
|
|
60
|
+
nextFaultId = 1;
|
|
61
|
+
onWebhookDeliver;
|
|
62
|
+
onReplicate;
|
|
63
|
+
onHookError;
|
|
64
|
+
strictAuth;
|
|
65
|
+
authTokenTtlMs;
|
|
66
|
+
/**
|
|
67
|
+
* Issued auth tokens with their associated grant scope + expiry. In
|
|
68
|
+
* permissive mode (`strictAuth: false`) this is still populated by
|
|
69
|
+
* `authorize` but never consulted on subsequent requests. In strict
|
|
70
|
+
* mode each request looks up its `Authorization` header here.
|
|
71
|
+
*/
|
|
72
|
+
issuedTokens = /* @__PURE__ */ new Map();
|
|
73
|
+
/**
|
|
74
|
+
* Virtual-clock offset applied to `Date.now()` for expiry checks.
|
|
75
|
+
* Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
|
|
76
|
+
* past auth-token expiry without sleeping.
|
|
77
|
+
*/
|
|
78
|
+
clockOffsetMs = 0;
|
|
79
|
+
/**
|
|
80
|
+
* Per-instance monotonic counter used to mint realistic-looking
|
|
81
|
+
* IDs (`b2_bucket_<24-hex>`, `4_z<24-hex>`, etc.) and auth tokens.
|
|
82
|
+
* Module-global previously, which leaked state across `B2Simulator`
|
|
83
|
+
* instances in the same Vitest worker — concurrent tests could see
|
|
84
|
+
* colliding tokens. Per-instance ensures determinism within a single
|
|
85
|
+
* simulator's lifetime.
|
|
86
|
+
*/
|
|
87
|
+
nextId = 1;
|
|
88
|
+
/**
|
|
89
|
+
* Last-issued upload timestamp. The simulator enforces strict
|
|
90
|
+
* monotonicity so version ordering is deterministic for tests; if
|
|
91
|
+
* two writes hit the same `Date.now()` millisecond, the second
|
|
92
|
+
* receives `lastTimestamp + 1`.
|
|
93
|
+
*/
|
|
94
|
+
lastTimestamp = 0;
|
|
95
|
+
/**
|
|
96
|
+
* Outstanding fire-and-forget hook invocations. Tracked so tests
|
|
97
|
+
* (and future {@link flushHooks} callers) can wait for every
|
|
98
|
+
* dispatched `onWebhookDeliver` / `onReplicate` callback to settle
|
|
99
|
+
* before asserting against observable side effects. The microtask
|
|
100
|
+
* dance in fidelity tests was previously brittle: positive cases
|
|
101
|
+
* flushed twice, negative cases flushed once, and any addition to
|
|
102
|
+
* the hook dispatch chain quietly broke the negative path.
|
|
103
|
+
*/
|
|
104
|
+
pendingHooks = /* @__PURE__ */ new Set();
|
|
105
|
+
/**
|
|
106
|
+
* Constructs a new in-memory B2 simulator.
|
|
107
|
+
* @param options - Optional simulator overrides. See {@link B2SimulatorOptions}.
|
|
108
|
+
*/
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
this.minimumPartSize = options.minimumPartSize ?? 5e6;
|
|
111
|
+
this.recommendedPartSize = options.recommendedPartSize ?? 1e8;
|
|
112
|
+
if (options.onWebhookDeliver !== void 0) this.onWebhookDeliver = options.onWebhookDeliver;
|
|
113
|
+
if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
|
|
114
|
+
if (options.onHookError !== void 0) this.onHookError = options.onHookError;
|
|
115
|
+
this.strictAuth = options.strictAuth ?? false;
|
|
116
|
+
this.authTokenTtlMs = options.authTokenTtlMs ?? 24 * 60 * 60 * 1e3;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Advance the simulator's virtual clock by `ms` milliseconds. Used in
|
|
120
|
+
* conjunction with `strictAuth: true` + a finite `authTokenTtlMs` to
|
|
121
|
+
* force token expiry without `setTimeout`-based delays.
|
|
122
|
+
*
|
|
123
|
+
* @param ms - Milliseconds to advance. Negative values rewind (rarely useful).
|
|
124
|
+
*/
|
|
125
|
+
advanceTime(ms) {
|
|
126
|
+
this.clockOffsetMs += ms;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Current simulator time. Equal to `Date.now() + clockOffsetMs`.
|
|
130
|
+
*
|
|
131
|
+
* @returns Unix milliseconds.
|
|
132
|
+
*/
|
|
133
|
+
now() {
|
|
134
|
+
return Date.now() + this.clockOffsetMs;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generate a deterministic-but-realistic-looking B2 identifier of the
|
|
138
|
+
* shape `<prefix>_<24-hex-counter>`, which approximates the visual
|
|
139
|
+
* width of real B2 wire IDs. The counter is monotonic per instance
|
|
140
|
+
* so test fixtures are deterministic across runs of the same
|
|
141
|
+
* simulator.
|
|
142
|
+
*
|
|
143
|
+
* @param prefix - B2-style prefix (`'b2_bucket'`, `'4_z'`, etc.).
|
|
144
|
+
*
|
|
145
|
+
* @returns A simulator-issued identifier that looks like a B2 wire ID.
|
|
146
|
+
*/
|
|
147
|
+
genId(prefix) {
|
|
148
|
+
const n = this.nextId++;
|
|
149
|
+
return `${prefix}_${n.toString(16).padStart(24, "0")}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Return a strictly-increasing upload timestamp, even when multiple
|
|
153
|
+
* writes land in the same `Date.now()` millisecond. Tests rely on
|
|
154
|
+
* `uploadTimestamp` ordering to discriminate file versions; ties from
|
|
155
|
+
* coarse system-clock resolution would otherwise make version
|
|
156
|
+
* selection nondeterministic.
|
|
157
|
+
*
|
|
158
|
+
* @returns Unix milliseconds.
|
|
159
|
+
*/
|
|
160
|
+
monotonicTimestamp() {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
if (now <= this.lastTimestamp) {
|
|
163
|
+
this.lastTimestamp += 1;
|
|
164
|
+
} else {
|
|
165
|
+
this.lastTimestamp = now;
|
|
166
|
+
}
|
|
167
|
+
return this.lastTimestamp;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wait for every fire-and-forget hook (`onWebhookDeliver`,
|
|
171
|
+
* `onReplicate`) currently in flight to settle. Use in tests after
|
|
172
|
+
* an upload/copy/finish-large-file call to deterministically observe
|
|
173
|
+
* hook side effects without microtask-flush guesswork.
|
|
174
|
+
*
|
|
175
|
+
* @returns A promise that resolves once every pending hook callback
|
|
176
|
+
* has either resolved or rejected.
|
|
177
|
+
*/
|
|
178
|
+
async flushHooks() {
|
|
179
|
+
while (this.pendingHooks.size > 0) {
|
|
180
|
+
const snapshot = [...this.pendingHooks];
|
|
181
|
+
await Promise.allSettled(snapshot);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Authorize a request against the strict-auth bookkeeping. Returns
|
|
186
|
+
* a `SimulatorJsonResponse` error on failure or `null` on success.
|
|
187
|
+
* Only consulted when `strictAuth: true`.
|
|
188
|
+
*
|
|
189
|
+
* @param authToken - The `Authorization` header value from the request.
|
|
190
|
+
* @param endpoint - B2 endpoint name being invoked.
|
|
191
|
+
* @param bucketId - Optional bucket ID the request operates on, for scope check.
|
|
192
|
+
* @param fileName - Optional file name the request operates on, for prefix check.
|
|
193
|
+
*
|
|
194
|
+
* @returns `null` when the request is authorized, otherwise a 401/403 response.
|
|
195
|
+
*/
|
|
196
|
+
authorizeRequest(authToken, endpoint, bucketId2, fileName) {
|
|
197
|
+
const required = missingCapabilitiesFor(endpoint, []);
|
|
198
|
+
const noCheckNeeded = required.length === 0 && endpoint === "b2_authorize_account";
|
|
199
|
+
if (noCheckNeeded) return null;
|
|
200
|
+
if (authToken === void 0 || authToken === "") {
|
|
201
|
+
return this.error(401, "bad_auth_token", "missing Authorization header");
|
|
202
|
+
}
|
|
203
|
+
const token = this.issuedTokens.get(authToken);
|
|
204
|
+
if (!token) {
|
|
205
|
+
return this.error(401, "bad_auth_token", "unknown auth token");
|
|
206
|
+
}
|
|
207
|
+
if (this.now() > token.expiresAt) {
|
|
208
|
+
return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
|
|
209
|
+
}
|
|
210
|
+
const missing = missingCapabilitiesFor(endpoint, token.capabilities);
|
|
211
|
+
if (missing.length > 0) {
|
|
212
|
+
return this.error(
|
|
213
|
+
403,
|
|
214
|
+
"unauthorized",
|
|
215
|
+
`application key lacks required capabilities: ${missing.join(", ")}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (token.bucketId !== null && bucketId2 !== void 0 && bucketId2 !== token.bucketId) {
|
|
219
|
+
return this.error(
|
|
220
|
+
403,
|
|
221
|
+
"unauthorized",
|
|
222
|
+
`application key is scoped to bucket ${token.bucketId}; cannot access ${bucketId2}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (token.namePrefix !== null && fileName !== void 0 && !fileName.startsWith(token.namePrefix)) {
|
|
226
|
+
return this.error(
|
|
227
|
+
403,
|
|
228
|
+
"unauthorized",
|
|
229
|
+
`application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Look up the application key matching the `Authorization` header on
|
|
236
|
+
* an `authorize_account` request. The header is in the form
|
|
237
|
+
* `Basic base64(applicationKeyId:applicationKey)`.
|
|
238
|
+
*
|
|
239
|
+
* Returns `null` for the implicit master credential (anything that
|
|
240
|
+
* does not match a key minted via `b2_create_key`); in that case
|
|
241
|
+
* `authorize` grants the full master capability set.
|
|
242
|
+
*
|
|
243
|
+
* @param authzHeader - Raw HTTP `Authorization` header value.
|
|
244
|
+
*
|
|
245
|
+
* @returns The matching key's grant scope, or `null` for the master.
|
|
246
|
+
*/
|
|
247
|
+
findKeyForAuthHeader(authzHeader) {
|
|
248
|
+
if (!authzHeader || !authzHeader.startsWith("Basic ")) return null;
|
|
249
|
+
const decoded = (() => {
|
|
250
|
+
try {
|
|
251
|
+
return atob(authzHeader.slice(6));
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
256
|
+
if (decoded === null) return null;
|
|
257
|
+
const idx = decoded.indexOf(":");
|
|
258
|
+
if (idx === -1) return null;
|
|
259
|
+
const applicationKeyId = decoded.slice(0, idx);
|
|
260
|
+
const applicationKey = decoded.slice(idx + 1);
|
|
261
|
+
const stored = this.keys.get(applicationKeyId);
|
|
262
|
+
if (!stored || stored.applicationKey !== applicationKey) return null;
|
|
263
|
+
return {
|
|
264
|
+
capabilities: stored.capabilities,
|
|
265
|
+
bucketId: stored.bucketId,
|
|
266
|
+
namePrefix: stored.namePrefix,
|
|
267
|
+
applicationKeyId
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Creates an {@link HttpTransport} that routes requests to this simulator.
|
|
272
|
+
* @returns A transport instance backed by this in-memory simulator.
|
|
273
|
+
*/
|
|
274
|
+
transport() {
|
|
275
|
+
return new SimulatorTransport(this);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Register a synthetic failure to inject on requests whose URL contains
|
|
279
|
+
* `spec.on`. Use this to exercise retry / backoff / error-handling
|
|
280
|
+
* paths in tests without hand-rolling a wrapping `HttpTransport`. The
|
|
281
|
+
* fault is consumed in registration order on each matched request;
|
|
282
|
+
* once its `count` budget is exhausted it auto-retires.
|
|
283
|
+
*
|
|
284
|
+
* Faults are checked BEFORE the simulator's real handlers run, so a
|
|
285
|
+
* matched request never touches in-memory state — failed uploads
|
|
286
|
+
* don't create partial parts, failed deletes don't remove anything.
|
|
287
|
+
*
|
|
288
|
+
* @param spec - The failure to inject. See {@link FaultSpec}.
|
|
289
|
+
*
|
|
290
|
+
* @returns A handle whose `clear()` method removes this specific
|
|
291
|
+
* fault registration (other faults remain in effect).
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* // Fail the next 2 b2_upload_part calls with 503, then succeed.
|
|
296
|
+
* sim.injectFailure({ on: 'b2_upload_part', status: 503, count: 2 })
|
|
297
|
+
*
|
|
298
|
+
* // Fail every b2_authorize_account with 401 + Retry-After: 5.
|
|
299
|
+
* const handle = sim.injectFailure({
|
|
300
|
+
* on: 'b2_authorize_account',
|
|
301
|
+
* status: 401,
|
|
302
|
+
* code: 'expired_auth_token',
|
|
303
|
+
* retryAfter: 5,
|
|
304
|
+
* })
|
|
305
|
+
* // ... later
|
|
306
|
+
* handle.clear()
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
injectFailure(spec) {
|
|
310
|
+
const id = this.nextFaultId++;
|
|
311
|
+
const fault = {
|
|
312
|
+
id,
|
|
313
|
+
spec,
|
|
314
|
+
remainingSkip: spec.skip ?? 0,
|
|
315
|
+
remainingCount: spec.count ?? Number.POSITIVE_INFINITY
|
|
316
|
+
};
|
|
317
|
+
this.faults.push(fault);
|
|
318
|
+
return {
|
|
319
|
+
clear: () => {
|
|
320
|
+
const idx = this.faults.findIndex((f) => f.id === id);
|
|
321
|
+
if (idx !== -1) this.faults.splice(idx, 1);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Remove every registered fault. Equivalent to calling `.clear()` on
|
|
327
|
+
* every handle returned by {@link injectFailure}, plus a defensive
|
|
328
|
+
* reset for tests that re-use a simulator across cases.
|
|
329
|
+
*/
|
|
330
|
+
clearFaults() {
|
|
331
|
+
this.faults.length = 0;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Internal: checks the registered faults for a match on the given URL
|
|
335
|
+
* and consumes one if it should fire. Called from
|
|
336
|
+
* {@link SimulatorTransport.send} before any real handler runs.
|
|
337
|
+
*
|
|
338
|
+
* @param url - The request URL to match against each fault's `on`
|
|
339
|
+
* substring.
|
|
340
|
+
*
|
|
341
|
+
* @returns The fault to apply, or `null` if no fault should fire.
|
|
342
|
+
*
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
consumeMatchingFault(url) {
|
|
346
|
+
for (let i = 0; i < this.faults.length; i++) {
|
|
347
|
+
const fault = this.faults[i];
|
|
348
|
+
if (!url.includes(fault.spec.on)) continue;
|
|
349
|
+
if (fault.remainingSkip > 0) {
|
|
350
|
+
fault.remainingSkip -= 1;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (fault.remainingCount <= 0) continue;
|
|
354
|
+
fault.remainingCount -= 1;
|
|
355
|
+
if (fault.remainingCount <= 0) {
|
|
356
|
+
this.faults.splice(i, 1);
|
|
357
|
+
}
|
|
358
|
+
return fault.spec;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Dispatches a JSON API request to the appropriate handler.
|
|
364
|
+
* @param _method - The HTTP method (unused).
|
|
365
|
+
* @param path - The request URL path containing the B2 endpoint name.
|
|
366
|
+
* @param headers - The HTTP request headers; consulted by the
|
|
367
|
+
* strict-auth gate to look up the issued auth token.
|
|
368
|
+
* @param body - The parsed JSON request body.
|
|
369
|
+
*
|
|
370
|
+
* @returns An object with HTTP status and JSON response body.
|
|
371
|
+
*/
|
|
372
|
+
async handleRequest(_method, path, headers, body) {
|
|
373
|
+
const endpoint = path.split("/").pop() ?? "";
|
|
374
|
+
if (this.strictAuth) {
|
|
375
|
+
const authError = this.authorizeRequest(headers["authorization"], endpoint);
|
|
376
|
+
if (authError !== null) return authError;
|
|
377
|
+
}
|
|
378
|
+
switch (endpoint) {
|
|
379
|
+
case "b2_authorize_account":
|
|
380
|
+
return this.authorize(headers["authorization"]);
|
|
381
|
+
case "b2_create_bucket":
|
|
382
|
+
return this.createBucket(
|
|
383
|
+
body
|
|
384
|
+
);
|
|
385
|
+
case "b2_list_buckets":
|
|
386
|
+
return this.listBuckets();
|
|
387
|
+
case "b2_delete_bucket":
|
|
388
|
+
return this.deleteBucket(body);
|
|
389
|
+
case "b2_update_bucket":
|
|
390
|
+
return this.updateBucket(body);
|
|
391
|
+
case "b2_get_upload_url":
|
|
392
|
+
return this.getUploadUrl(body);
|
|
393
|
+
case "b2_list_file_names":
|
|
394
|
+
return this.listFileNames(
|
|
395
|
+
body
|
|
396
|
+
);
|
|
397
|
+
case "b2_list_file_versions":
|
|
398
|
+
return this.listFileVersions(
|
|
399
|
+
body
|
|
400
|
+
);
|
|
401
|
+
case "b2_get_file_info":
|
|
402
|
+
return this.getFileInfo(body);
|
|
403
|
+
case "b2_hide_file":
|
|
404
|
+
return this.hideFile(body);
|
|
405
|
+
case "b2_delete_file_version":
|
|
406
|
+
return this.deleteFileVersion(
|
|
407
|
+
body
|
|
408
|
+
);
|
|
409
|
+
case "b2_copy_file":
|
|
410
|
+
return this.copyFile(
|
|
411
|
+
body
|
|
412
|
+
);
|
|
413
|
+
case "b2_start_large_file":
|
|
414
|
+
return this.startLargeFile(
|
|
415
|
+
body
|
|
416
|
+
);
|
|
417
|
+
case "b2_get_upload_part_url":
|
|
418
|
+
return this.getUploadPartUrl(body);
|
|
419
|
+
case "b2_finish_large_file":
|
|
420
|
+
return this.finishLargeFile(body);
|
|
421
|
+
case "b2_cancel_large_file":
|
|
422
|
+
return this.cancelLargeFile(body);
|
|
423
|
+
case "b2_list_unfinished_large_files":
|
|
424
|
+
return this.listUnfinishedLargeFiles(
|
|
425
|
+
body
|
|
426
|
+
);
|
|
427
|
+
case "b2_list_parts":
|
|
428
|
+
return this.listParts(
|
|
429
|
+
body
|
|
430
|
+
);
|
|
431
|
+
case "b2_copy_part":
|
|
432
|
+
return await this.copyPart(
|
|
433
|
+
body
|
|
434
|
+
);
|
|
435
|
+
case "b2_get_download_authorization":
|
|
436
|
+
return this.getDownloadAuthorization(
|
|
437
|
+
body
|
|
438
|
+
);
|
|
439
|
+
case "b2_create_key":
|
|
440
|
+
return this.createKey(
|
|
441
|
+
body
|
|
442
|
+
);
|
|
443
|
+
case "b2_list_keys":
|
|
444
|
+
return this.listKeys(
|
|
445
|
+
body
|
|
446
|
+
);
|
|
447
|
+
case "b2_delete_key":
|
|
448
|
+
return this.deleteKey(body);
|
|
449
|
+
case "b2_update_file_retention":
|
|
450
|
+
return this.updateFileRetention(
|
|
451
|
+
body
|
|
452
|
+
);
|
|
453
|
+
case "b2_update_file_legal_hold":
|
|
454
|
+
return this.updateFileLegalHold(
|
|
455
|
+
body
|
|
456
|
+
);
|
|
457
|
+
case "b2_get_bucket_notification_rules":
|
|
458
|
+
return this.getBucketNotificationRules(body);
|
|
459
|
+
case "b2_set_bucket_notification_rules":
|
|
460
|
+
return this.setBucketNotificationRules(
|
|
461
|
+
body
|
|
462
|
+
);
|
|
463
|
+
default:
|
|
464
|
+
return {
|
|
465
|
+
status: 400,
|
|
466
|
+
body: { status: 400, code: "bad_request", message: `Unknown endpoint: ${endpoint}` }
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Handles file and part upload requests (`b2_upload_file`, `b2_upload_part`).
|
|
472
|
+
* Dispatches to the appropriate internal handler based on the URL.
|
|
473
|
+
* @param url - The upload endpoint URL used to determine the upload type.
|
|
474
|
+
* @param headers - The HTTP headers containing file metadata and authorization.
|
|
475
|
+
* @param data - The raw file or part content as bytes.
|
|
476
|
+
*
|
|
477
|
+
* @returns An object with HTTP status and JSON response body.
|
|
478
|
+
*/
|
|
479
|
+
handleUpload(url, headers, data) {
|
|
480
|
+
const isPart = url.includes("b2_upload_part");
|
|
481
|
+
if (this.strictAuth) {
|
|
482
|
+
const endpoint = isPart ? "b2_upload_part" : "b2_upload_file";
|
|
483
|
+
const authError = this.authorizeRequest(headers["authorization"], endpoint);
|
|
484
|
+
if (authError !== null) return authError;
|
|
485
|
+
}
|
|
486
|
+
if (isPart) {
|
|
487
|
+
return this.handleUploadPart(url, headers, data);
|
|
488
|
+
}
|
|
489
|
+
return this.handleUploadFile(url, headers, data);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
|
|
493
|
+
* Returns the file data along with B2 response headers.
|
|
494
|
+
* @param path - The request URL path identifying the file to download.
|
|
495
|
+
* @param headers - The HTTP request headers for range or authorization.
|
|
496
|
+
* @param method - The HTTP method; `'HEAD'` suppresses the response body.
|
|
497
|
+
*
|
|
498
|
+
* @returns The download response containing file data and B2 headers.
|
|
499
|
+
*/
|
|
500
|
+
handleDownload(path, headers, method = "GET") {
|
|
501
|
+
if (path.includes("b2_download_file_by_id")) {
|
|
502
|
+
if (this.strictAuth) {
|
|
503
|
+
const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_id");
|
|
504
|
+
if (authError !== null) return this.errorAsDownload(authError);
|
|
505
|
+
}
|
|
506
|
+
const url = new URL(`http://localhost${path}`);
|
|
507
|
+
const fileId2 = url.searchParams.get("fileId") ?? "";
|
|
508
|
+
return this.finalizeDownload(this.downloadById(fileId2, headers["range"]), url, method);
|
|
509
|
+
}
|
|
510
|
+
const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
|
|
511
|
+
if (fileMatch) {
|
|
512
|
+
const bucketName = decodeURIComponent(fileMatch[1] ?? "");
|
|
513
|
+
const fileName = decodeURIComponent(fileMatch[2] ?? "");
|
|
514
|
+
if (this.strictAuth) {
|
|
515
|
+
const authError = this.authorizeRequest(
|
|
516
|
+
headers["authorization"],
|
|
517
|
+
"b2_download_file_by_name",
|
|
518
|
+
void 0,
|
|
519
|
+
fileName
|
|
520
|
+
);
|
|
521
|
+
if (authError !== null) return this.errorAsDownload(authError);
|
|
522
|
+
}
|
|
523
|
+
const url = new URL(`http://localhost${path}`);
|
|
524
|
+
return this.finalizeDownload(
|
|
525
|
+
this.downloadByName(bucketName, fileName, headers["range"]),
|
|
526
|
+
url,
|
|
527
|
+
method
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return { status: 404, headers: {}, data: null };
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Convert a synthetic JSON error body into a `SimulatorDownloadResponse`
|
|
534
|
+
* shape so the strict-auth gate on download paths can surface 401/403s
|
|
535
|
+
* through the same transport plumbing as a regular failed download.
|
|
536
|
+
*
|
|
537
|
+
* @param json - The JSON error from {@link authorizeRequest}.
|
|
538
|
+
*
|
|
539
|
+
* @returns A download-shaped response with the error body bytes inline.
|
|
540
|
+
*/
|
|
541
|
+
errorAsDownload(json) {
|
|
542
|
+
return {
|
|
543
|
+
status: json.status,
|
|
544
|
+
headers: { "Content-Type": "application/json" },
|
|
545
|
+
data: utf8Encoder.encode(JSON.stringify(json.body))
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Applies HEAD-method body suppression and `b2Content*` response-header
|
|
550
|
+
* overrides parsed from the download URL's query string. Mirrors what the
|
|
551
|
+
* real B2 service does: any `b2Content*` query parameter is echoed back as
|
|
552
|
+
* the corresponding response header.
|
|
553
|
+
*
|
|
554
|
+
* @param response - The download response produced by {@link downloadById} or {@link downloadByName}.
|
|
555
|
+
* @param url - The parsed download URL (used to read `b2Content*` query params).
|
|
556
|
+
* @param method - The HTTP method of the originating request.
|
|
557
|
+
*
|
|
558
|
+
* @returns The response with overrides applied.
|
|
559
|
+
*/
|
|
560
|
+
finalizeDownload(response, url, method) {
|
|
561
|
+
const overrideMap = {
|
|
562
|
+
b2ContentDisposition: "Content-Disposition",
|
|
563
|
+
b2ContentLanguage: "Content-Language",
|
|
564
|
+
b2ContentEncoding: "Content-Encoding",
|
|
565
|
+
b2ContentType: "Content-Type",
|
|
566
|
+
b2CacheControl: "Cache-Control",
|
|
567
|
+
b2Expires: "Expires"
|
|
568
|
+
};
|
|
569
|
+
const newHeaders = { ...response.headers };
|
|
570
|
+
for (const [param, header] of Object.entries(overrideMap)) {
|
|
571
|
+
const value = url.searchParams.get(param);
|
|
572
|
+
if (value !== null) newHeaders[header] = value;
|
|
573
|
+
}
|
|
574
|
+
const data = method === "HEAD" ? null : response.data;
|
|
575
|
+
return { status: response.status, headers: newHeaders, data };
|
|
576
|
+
}
|
|
577
|
+
handleUploadFile(url, headers, data) {
|
|
578
|
+
const bucketId2 = new URL(url).searchParams.get("bucketId");
|
|
579
|
+
if (!bucketId2) return this.error(400, "bad_request", "Missing bucketId");
|
|
580
|
+
const bucket = this.buckets.get(bucketId2);
|
|
581
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
582
|
+
const fileName = decodeURIComponent(headers["x-bz-file-name"] ?? "");
|
|
583
|
+
const contentType = headers["content-type"] ?? "application/octet-stream";
|
|
584
|
+
const sha1 = headers["x-bz-content-sha1"] ?? "none";
|
|
585
|
+
const fileNameError = validateFileName(fileName);
|
|
586
|
+
if (fileNameError) return this.error(400, fileNameError.code, fileNameError.message);
|
|
587
|
+
const fileInfo = parseFileInfoHeaders(headers);
|
|
588
|
+
const fileInfoError = validateFileInfo(fileInfo);
|
|
589
|
+
if (fileInfoError) return this.error(400, fileInfoError.code, fileInfoError.message);
|
|
590
|
+
const fileVersion = this.makeFileVersion({
|
|
591
|
+
bucketId: bucketId2,
|
|
592
|
+
fileName,
|
|
593
|
+
contentType,
|
|
594
|
+
contentLength: data.byteLength,
|
|
595
|
+
contentSha1: sha1,
|
|
596
|
+
action: FileAction.Upload
|
|
597
|
+
});
|
|
598
|
+
const stored = { fileVersion, data };
|
|
599
|
+
const existing = bucket.files.get(fileName);
|
|
600
|
+
if (existing) {
|
|
601
|
+
existing.push(stored);
|
|
602
|
+
} else {
|
|
603
|
+
bucket.files.set(fileName, [stored]);
|
|
604
|
+
}
|
|
605
|
+
this.firePostUploadHooks(fileVersion, bucketId2, "b2:ObjectCreated:Upload");
|
|
606
|
+
return { status: 200, body: fileVersion };
|
|
607
|
+
}
|
|
608
|
+
handleUploadPart(url, headers, data) {
|
|
609
|
+
const fileId2 = new URL(url).searchParams.get("fileId");
|
|
610
|
+
if (!fileId2) return this.error(400, "bad_request", "Missing fileId");
|
|
611
|
+
const large = this.largeFiles.get(fileId2);
|
|
612
|
+
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
613
|
+
const partNumber = Number.parseInt(headers["x-bz-part-number"] ?? "0", 10);
|
|
614
|
+
const sha1 = headers["x-bz-content-sha1"] ?? "none";
|
|
615
|
+
large.parts.set(partNumber, { data, sha1 });
|
|
616
|
+
return {
|
|
617
|
+
status: 200,
|
|
618
|
+
body: {
|
|
619
|
+
fileId: large.fileId,
|
|
620
|
+
partNumber,
|
|
621
|
+
contentLength: data.byteLength,
|
|
622
|
+
contentSha1: sha1,
|
|
623
|
+
serverSideEncryption: { mode: EncryptionMode.None },
|
|
624
|
+
uploadTimestamp: Date.now()
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
downloadById(fileId2, range) {
|
|
629
|
+
const found = this.findFile(fileId2);
|
|
630
|
+
if (found === null) return { status: 404, headers: {}, data: null };
|
|
631
|
+
return this.serveFile(found.stored, range);
|
|
632
|
+
}
|
|
633
|
+
downloadByName(bucketName, fileName, range) {
|
|
634
|
+
for (const bucket of this.buckets.values()) {
|
|
635
|
+
if (bucket.info.bucketName !== bucketName) continue;
|
|
636
|
+
const versions = bucket.files.get(fileName);
|
|
637
|
+
if (!versions || versions.length === 0) break;
|
|
638
|
+
const latest = versions[versions.length - 1];
|
|
639
|
+
if (!latest || latest.fileVersion.action === FileAction.Hide) {
|
|
640
|
+
return { status: 404, headers: {}, data: null };
|
|
641
|
+
}
|
|
642
|
+
return this.serveFile(latest, range);
|
|
643
|
+
}
|
|
644
|
+
return { status: 404, headers: {}, data: null };
|
|
645
|
+
}
|
|
646
|
+
serveFile(stored, range) {
|
|
647
|
+
const fullData = stored.data;
|
|
648
|
+
let data = fullData;
|
|
649
|
+
let status = 200;
|
|
650
|
+
let contentRange = null;
|
|
651
|
+
if (range) {
|
|
652
|
+
const parsed = parseRangeHeader(range, fullData.byteLength);
|
|
653
|
+
if (parsed.kind === "ok") {
|
|
654
|
+
data = fullData.subarray(parsed.start, parsed.end + 1);
|
|
655
|
+
status = 206;
|
|
656
|
+
contentRange = `bytes ${parsed.start}-${parsed.end}/${fullData.byteLength}`;
|
|
657
|
+
} else if (parsed.kind === "unsatisfiable") {
|
|
658
|
+
return {
|
|
659
|
+
status: 416,
|
|
660
|
+
headers: {
|
|
661
|
+
"Content-Type": stored.fileVersion.contentType,
|
|
662
|
+
"Content-Range": `bytes */${fullData.byteLength}`
|
|
663
|
+
},
|
|
664
|
+
data: new Uint8Array(0)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const fv = stored.fileVersion;
|
|
669
|
+
const headers = {
|
|
670
|
+
"Content-Type": fv.contentType,
|
|
671
|
+
"Content-Length": String(data.byteLength),
|
|
672
|
+
"X-Bz-File-Id": fv.fileId,
|
|
673
|
+
"X-Bz-File-Name": encodeURIComponent(fv.fileName),
|
|
674
|
+
"X-Bz-Content-Sha1": fv.contentSha1 ?? "none",
|
|
675
|
+
"X-Bz-Upload-Timestamp": String(fv.uploadTimestamp),
|
|
676
|
+
"X-Bz-Info-src_last_modified_millis": String(fv.uploadTimestamp)
|
|
677
|
+
};
|
|
678
|
+
if (contentRange !== null) {
|
|
679
|
+
headers["Content-Range"] = contentRange;
|
|
680
|
+
}
|
|
681
|
+
return { status, headers, data };
|
|
682
|
+
}
|
|
683
|
+
// --- API handlers ---
|
|
684
|
+
authorize(authzHeader) {
|
|
685
|
+
const capabilities = [
|
|
686
|
+
Capability.ListBuckets,
|
|
687
|
+
Capability.ReadBuckets,
|
|
688
|
+
Capability.WriteBuckets,
|
|
689
|
+
Capability.DeleteBuckets,
|
|
690
|
+
Capability.ListFiles,
|
|
691
|
+
Capability.ReadFiles,
|
|
692
|
+
Capability.WriteFiles,
|
|
693
|
+
Capability.DeleteFiles,
|
|
694
|
+
Capability.ListKeys,
|
|
695
|
+
Capability.WriteKeys,
|
|
696
|
+
Capability.DeleteKeys,
|
|
697
|
+
Capability.ShareFiles,
|
|
698
|
+
Capability.ReadBucketNotifications,
|
|
699
|
+
Capability.WriteBucketNotifications
|
|
700
|
+
];
|
|
701
|
+
const keyForAuth = this.findKeyForAuthHeader(authzHeader);
|
|
702
|
+
const tokenStr = `sim_auth_token_${this.nextId++}`;
|
|
703
|
+
this.issuedTokens.set(tokenStr, {
|
|
704
|
+
capabilities: keyForAuth?.capabilities ?? capabilities,
|
|
705
|
+
bucketId: keyForAuth?.bucketId ?? null,
|
|
706
|
+
namePrefix: keyForAuth?.namePrefix ?? null,
|
|
707
|
+
expiresAt: this.now() + this.authTokenTtlMs,
|
|
708
|
+
applicationKeyId: keyForAuth?.applicationKeyId ?? null
|
|
709
|
+
});
|
|
710
|
+
return {
|
|
711
|
+
status: 200,
|
|
712
|
+
body: {
|
|
713
|
+
accountId: accountId(this.accountId),
|
|
714
|
+
// `AuthToken` has no public factory by design — auth tokens are
|
|
715
|
+
// minted by B2, not constructed by user code. The simulator is
|
|
716
|
+
// the only legitimate place that needs to forge one.
|
|
717
|
+
authorizationToken: tokenStr,
|
|
718
|
+
apiInfo: {
|
|
719
|
+
storageApi: {
|
|
720
|
+
absoluteMinimumPartSize: this.minimumPartSize,
|
|
721
|
+
apiUrl: "http://localhost:0",
|
|
722
|
+
bucketId: keyForAuth?.bucketId ?? null,
|
|
723
|
+
bucketName: null,
|
|
724
|
+
downloadUrl: "http://localhost:0",
|
|
725
|
+
infoType: "storageApi",
|
|
726
|
+
namePrefix: keyForAuth?.namePrefix ?? null,
|
|
727
|
+
recommendedPartSize: this.recommendedPartSize,
|
|
728
|
+
s3ApiUrl: "http://localhost:0",
|
|
729
|
+
allowed: {
|
|
730
|
+
capabilities: keyForAuth?.capabilities ?? capabilities,
|
|
731
|
+
bucketId: keyForAuth?.bucketId ?? null,
|
|
732
|
+
bucketName: null,
|
|
733
|
+
namePrefix: keyForAuth?.namePrefix ?? null
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
applicationKeyExpirationTimestamp: null
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
createBucket(req) {
|
|
742
|
+
const nameError = validateBucketName(req.bucketName);
|
|
743
|
+
if (nameError) return this.error(400, nameError.code, nameError.message);
|
|
744
|
+
if (req.bucketInfo !== void 0) {
|
|
745
|
+
const infoError = validateBucketInfo(req.bucketInfo);
|
|
746
|
+
if (infoError) return this.error(400, infoError.code, infoError.message);
|
|
747
|
+
}
|
|
748
|
+
for (const b of this.buckets.values()) {
|
|
749
|
+
if (b.info.bucketName === req.bucketName) {
|
|
750
|
+
return this.error(400, "duplicate_bucket_name", "Bucket name already in use");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const bid = bucketId(this.genId("b2_bucket"));
|
|
754
|
+
const defaultRetention = req.defaultRetention ?? {
|
|
755
|
+
mode: BucketRetentionMode.None,
|
|
756
|
+
period: null
|
|
757
|
+
};
|
|
758
|
+
const info = {
|
|
759
|
+
accountId: accountId(req.accountId),
|
|
760
|
+
bucketId: bid,
|
|
761
|
+
bucketName: req.bucketName,
|
|
762
|
+
bucketType: req.bucketType,
|
|
763
|
+
bucketInfo: req.bucketInfo ?? {},
|
|
764
|
+
corsRules: req.corsRules ?? [],
|
|
765
|
+
defaultServerSideEncryption: req.defaultServerSideEncryption ?? { mode: EncryptionMode.None },
|
|
766
|
+
fileLockConfiguration: {
|
|
767
|
+
isClientAuthorizedToRead: true,
|
|
768
|
+
value: {
|
|
769
|
+
isFileLockEnabled: req.fileLockEnabled ?? false,
|
|
770
|
+
defaultRetention
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
lifecycleRules: req.lifecycleRules ?? [],
|
|
774
|
+
options: [],
|
|
775
|
+
revision: 1,
|
|
776
|
+
defaultRetention,
|
|
777
|
+
replicationConfiguration: req.replicationConfiguration ?? {
|
|
778
|
+
asReplicationSource: null,
|
|
779
|
+
asReplicationDestination: null
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
this.buckets.set(bid, { info, files: /* @__PURE__ */ new Map() });
|
|
783
|
+
return { status: 200, body: info };
|
|
784
|
+
}
|
|
785
|
+
listBuckets() {
|
|
786
|
+
const buckets = [...this.buckets.values()].map((b) => b.info);
|
|
787
|
+
return { status: 200, body: { buckets } };
|
|
788
|
+
}
|
|
789
|
+
deleteBucket(req) {
|
|
790
|
+
const bucket = this.buckets.get(req.bucketId);
|
|
791
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
792
|
+
this.buckets.delete(req.bucketId);
|
|
793
|
+
return { status: 200, body: bucket.info };
|
|
794
|
+
}
|
|
795
|
+
updateBucket(req) {
|
|
796
|
+
const bucket = this.buckets.get(req["bucketId"]);
|
|
797
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
798
|
+
if (req["bucketInfo"] !== void 0) {
|
|
799
|
+
const infoError = validateBucketInfo(req["bucketInfo"]);
|
|
800
|
+
if (infoError) return this.error(400, infoError.code, infoError.message);
|
|
801
|
+
}
|
|
802
|
+
const updated = {
|
|
803
|
+
...bucket.info,
|
|
804
|
+
...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
|
|
805
|
+
...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
|
|
806
|
+
...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
|
|
807
|
+
...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
|
|
808
|
+
...req["replicationConfiguration"] !== void 0 ? {
|
|
809
|
+
replicationConfiguration: req["replicationConfiguration"]
|
|
810
|
+
} : {},
|
|
811
|
+
...req["defaultRetention"] !== void 0 ? {
|
|
812
|
+
defaultRetention: req["defaultRetention"],
|
|
813
|
+
fileLockConfiguration: {
|
|
814
|
+
isClientAuthorizedToRead: true,
|
|
815
|
+
value: {
|
|
816
|
+
isFileLockEnabled: bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false,
|
|
817
|
+
defaultRetention: req["defaultRetention"]
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} : {},
|
|
821
|
+
...req["defaultServerSideEncryption"] !== void 0 ? {
|
|
822
|
+
defaultServerSideEncryption: req["defaultServerSideEncryption"]
|
|
823
|
+
} : {},
|
|
824
|
+
revision: bucket.info.revision + 1
|
|
825
|
+
};
|
|
826
|
+
this.buckets.set(req["bucketId"], { info: updated, files: bucket.files });
|
|
827
|
+
return { status: 200, body: updated };
|
|
828
|
+
}
|
|
829
|
+
getUploadUrl(req) {
|
|
830
|
+
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
831
|
+
return {
|
|
832
|
+
status: 200,
|
|
833
|
+
body: {
|
|
834
|
+
bucketId: req.bucketId,
|
|
835
|
+
uploadUrl: `http://localhost:0/b2api/v3/b2_upload_file?bucketId=${req.bucketId}`,
|
|
836
|
+
authorizationToken: "sim_upload_token"
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
listFileNames(req) {
|
|
841
|
+
const bucket = this.buckets.get(req.bucketId);
|
|
842
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
843
|
+
const countError = validateMaxCount(req.maxFileCount, "b2_list_file_names");
|
|
844
|
+
if (countError) return this.error(400, countError.code, countError.message);
|
|
845
|
+
const max = req.maxFileCount ?? 1e3;
|
|
846
|
+
const prefix = req.prefix ?? "";
|
|
847
|
+
let allFiles = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).map(([_, versions]) => versions[versions.length - 1]).filter((v) => v !== void 0).map((v) => v.fileVersion).sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
848
|
+
if (req.startFileName) {
|
|
849
|
+
const start = req.startFileName;
|
|
850
|
+
allFiles = allFiles.filter((f) => f.fileName >= start);
|
|
851
|
+
}
|
|
852
|
+
const files = allFiles.slice(0, max);
|
|
853
|
+
const nextFileName = allFiles.length > max ? allFiles[max]?.fileName ?? null : null;
|
|
854
|
+
return { status: 200, body: { files, nextFileName } };
|
|
855
|
+
}
|
|
856
|
+
listFileVersions(req) {
|
|
857
|
+
const bucket = this.buckets.get(req.bucketId);
|
|
858
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
859
|
+
const countError = validateMaxCount(req.maxFileCount, "b2_list_file_versions");
|
|
860
|
+
if (countError) return this.error(400, countError.code, countError.message);
|
|
861
|
+
const max = req.maxFileCount ?? 1e3;
|
|
862
|
+
const prefix = req.prefix ?? "";
|
|
863
|
+
const allVersions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).flatMap(([_, versions]) => versions.map((v) => v.fileVersion)).sort((a, b) => {
|
|
864
|
+
const nameCmp = a.fileName.localeCompare(b.fileName);
|
|
865
|
+
if (nameCmp !== 0) return nameCmp;
|
|
866
|
+
return b.uploadTimestamp - a.uploadTimestamp;
|
|
867
|
+
});
|
|
868
|
+
let startIdx = 0;
|
|
869
|
+
if (req.startFileName !== void 0) {
|
|
870
|
+
const startName = req.startFileName;
|
|
871
|
+
const startId = req.startFileId;
|
|
872
|
+
const nameIdx = allVersions.findIndex((f) => f.fileName >= startName);
|
|
873
|
+
if (nameIdx === -1) {
|
|
874
|
+
startIdx = allVersions.length;
|
|
875
|
+
} else if (startId !== void 0) {
|
|
876
|
+
const exactIdx = allVersions.findIndex((f, i) => i >= nameIdx && f.fileId === startId);
|
|
877
|
+
startIdx = exactIdx !== -1 ? exactIdx : nameIdx;
|
|
878
|
+
} else {
|
|
879
|
+
startIdx = nameIdx;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const sliced = allVersions.slice(startIdx, startIdx + max);
|
|
883
|
+
const hasMore = startIdx + max < allVersions.length;
|
|
884
|
+
const nextFileName = hasMore ? allVersions[startIdx + max]?.fileName ?? null : null;
|
|
885
|
+
const nextFileId = hasMore ? allVersions[startIdx + max]?.fileId ?? null : null;
|
|
886
|
+
return { status: 200, body: { files: sliced, nextFileName, nextFileId } };
|
|
887
|
+
}
|
|
888
|
+
getFileInfo(req) {
|
|
889
|
+
const found = this.findFile(req.fileId);
|
|
890
|
+
if (found === null) return this.error(404, "file_not_present", "File not found");
|
|
891
|
+
return { status: 200, body: found.stored.fileVersion };
|
|
892
|
+
}
|
|
893
|
+
hideFile(req) {
|
|
894
|
+
const bucket = this.buckets.get(req.bucketId);
|
|
895
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
896
|
+
const nameError = validateFileName(req.fileName);
|
|
897
|
+
if (nameError) return this.error(400, nameError.code, nameError.message);
|
|
898
|
+
const fileVersion = this.makeFileVersion({
|
|
899
|
+
bucketId: req.bucketId,
|
|
900
|
+
fileName: req.fileName,
|
|
901
|
+
contentType: "application/octet-stream",
|
|
902
|
+
contentLength: 0,
|
|
903
|
+
contentSha1: "none",
|
|
904
|
+
action: FileAction.Hide
|
|
905
|
+
});
|
|
906
|
+
const existing = bucket.files.get(req.fileName);
|
|
907
|
+
const stored = { fileVersion, data: new Uint8Array(0) };
|
|
908
|
+
if (existing) {
|
|
909
|
+
existing.push(stored);
|
|
910
|
+
} else {
|
|
911
|
+
bucket.files.set(req.fileName, [stored]);
|
|
912
|
+
}
|
|
913
|
+
return { status: 200, body: fileVersion };
|
|
914
|
+
}
|
|
915
|
+
deleteFileVersion(req) {
|
|
916
|
+
const found = this.findFile(req.fileId);
|
|
917
|
+
if (found === null || found.stored.fileVersion.fileName !== req.fileName) {
|
|
918
|
+
return this.error(400, "file_not_present", "File version not found");
|
|
919
|
+
}
|
|
920
|
+
const fv = found.stored.fileVersion;
|
|
921
|
+
const retention = fv.fileRetention?.value;
|
|
922
|
+
const legalHold = fv.legalHold?.value;
|
|
923
|
+
const now = Date.now();
|
|
924
|
+
if (legalHold === "on") {
|
|
925
|
+
return this.error(
|
|
926
|
+
400,
|
|
927
|
+
"file_lock_legal_hold_protected",
|
|
928
|
+
"File is on legal hold and cannot be deleted"
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (retention?.mode === "compliance" && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) {
|
|
932
|
+
return this.error(
|
|
933
|
+
400,
|
|
934
|
+
"file_lock_compliance_protected",
|
|
935
|
+
`File is under compliance-mode retention and cannot be deleted until ${new Date(retention.retainUntilTimestamp).toISOString()}`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (retention?.mode === "governance" && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now && req.bypassGovernance !== true) {
|
|
939
|
+
return this.error(
|
|
940
|
+
400,
|
|
941
|
+
"file_lock_governance_protected",
|
|
942
|
+
"File is under governance-mode retention; pass bypassGovernance: true to delete"
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
found.versions.splice(found.index, 1);
|
|
946
|
+
if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
|
|
947
|
+
return { status: 200, body: { fileId: req.fileId, fileName: req.fileName } };
|
|
948
|
+
}
|
|
949
|
+
copyFile(req) {
|
|
950
|
+
const nameError = validateFileName(req.fileName);
|
|
951
|
+
if (nameError) return this.error(400, nameError.code, nameError.message);
|
|
952
|
+
const found = this.findFile(req.sourceFileId);
|
|
953
|
+
if (found === null) return this.error(404, "file_not_present", "Source file not found");
|
|
954
|
+
const sourceStored = found.stored;
|
|
955
|
+
const destBucketId = req.destinationBucketId ?? found.bucketId;
|
|
956
|
+
const destBucket = this.buckets.get(destBucketId);
|
|
957
|
+
if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
|
|
958
|
+
const fileVersion = this.makeFileVersion({
|
|
959
|
+
bucketId: destBucketId,
|
|
960
|
+
fileName: req.fileName,
|
|
961
|
+
contentType: sourceStored.fileVersion.contentType,
|
|
962
|
+
contentLength: sourceStored.data.byteLength,
|
|
963
|
+
contentSha1: sourceStored.fileVersion.contentSha1 ?? "none",
|
|
964
|
+
action: FileAction.Copy
|
|
965
|
+
});
|
|
966
|
+
const copied = { fileVersion, data: new Uint8Array(sourceStored.data) };
|
|
967
|
+
const existing = destBucket.files.get(req.fileName);
|
|
968
|
+
if (existing) {
|
|
969
|
+
existing.push(copied);
|
|
970
|
+
} else {
|
|
971
|
+
destBucket.files.set(req.fileName, [copied]);
|
|
972
|
+
}
|
|
973
|
+
this.firePostUploadHooks(fileVersion, destBucketId, "b2:ObjectCreated:Copy");
|
|
974
|
+
return { status: 200, body: fileVersion };
|
|
975
|
+
}
|
|
976
|
+
startLargeFile(req) {
|
|
977
|
+
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
978
|
+
const nameError = validateFileName(req.fileName);
|
|
979
|
+
if (nameError) return this.error(400, nameError.code, nameError.message);
|
|
980
|
+
if (req.fileInfo !== void 0) {
|
|
981
|
+
const infoError = validateFileInfo(req.fileInfo);
|
|
982
|
+
if (infoError) return this.error(400, infoError.code, infoError.message);
|
|
983
|
+
}
|
|
984
|
+
const fid = this.genId("4_z");
|
|
985
|
+
const large = {
|
|
986
|
+
fileId: fid,
|
|
987
|
+
bucketId: req.bucketId,
|
|
988
|
+
fileName: req.fileName,
|
|
989
|
+
contentType: req.contentType,
|
|
990
|
+
fileInfo: req.fileInfo ?? {},
|
|
991
|
+
parts: /* @__PURE__ */ new Map()
|
|
992
|
+
};
|
|
993
|
+
this.largeFiles.set(fid, large);
|
|
994
|
+
return {
|
|
995
|
+
status: 200,
|
|
996
|
+
body: {
|
|
997
|
+
fileId: fid,
|
|
998
|
+
fileName: req.fileName,
|
|
999
|
+
accountId: this.accountId,
|
|
1000
|
+
bucketId: req.bucketId,
|
|
1001
|
+
contentType: req.contentType,
|
|
1002
|
+
fileInfo: large.fileInfo
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
getUploadPartUrl(req) {
|
|
1007
|
+
if (!this.largeFiles.has(req.fileId))
|
|
1008
|
+
return this.error(400, "bad_request", "Large file not found");
|
|
1009
|
+
return {
|
|
1010
|
+
status: 200,
|
|
1011
|
+
body: {
|
|
1012
|
+
fileId: req.fileId,
|
|
1013
|
+
uploadUrl: `http://localhost:0/b2api/v3/b2_upload_part?fileId=${req.fileId}`,
|
|
1014
|
+
authorizationToken: "sim_part_token"
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
finishLargeFile(req) {
|
|
1019
|
+
const large = this.largeFiles.get(req.fileId);
|
|
1020
|
+
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
1021
|
+
const bucket = this.buckets.get(large.bucketId);
|
|
1022
|
+
if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1023
|
+
const sortedParts = [...large.parts.entries()].sort((a, b) => a[0] - b[0]);
|
|
1024
|
+
if (sortedParts.length > 1e4) {
|
|
1025
|
+
return this.error(
|
|
1026
|
+
400,
|
|
1027
|
+
"bad_request",
|
|
1028
|
+
`multipart upload has ${sortedParts.length} parts; B2 caps at 10000`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
for (const [partNumber] of sortedParts) {
|
|
1032
|
+
if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 1e4) {
|
|
1033
|
+
return this.error(
|
|
1034
|
+
400,
|
|
1035
|
+
"bad_request",
|
|
1036
|
+
`partNumber ${partNumber} is outside the [1, 10000] range B2 accepts`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (req.partSha1Array.length !== sortedParts.length) {
|
|
1041
|
+
return this.error(
|
|
1042
|
+
400,
|
|
1043
|
+
"bad_request",
|
|
1044
|
+
`partSha1Array has ${req.partSha1Array.length} entries but ${sortedParts.length} parts were uploaded`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
for (let i = 0; i < sortedParts.length - 1; i++) {
|
|
1048
|
+
const [partNumber, part] = sortedParts[i];
|
|
1049
|
+
if (part.data.byteLength < this.minimumPartSize) {
|
|
1050
|
+
return this.error(
|
|
1051
|
+
400,
|
|
1052
|
+
"bad_request",
|
|
1053
|
+
`part ${partNumber} (${part.data.byteLength} bytes) is smaller than the minimum part size of ${this.minimumPartSize}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
let totalSize = 0;
|
|
1058
|
+
for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
|
|
1059
|
+
const combined = new Uint8Array(totalSize);
|
|
1060
|
+
let offset = 0;
|
|
1061
|
+
for (const [_, part] of sortedParts) {
|
|
1062
|
+
combined.set(part.data, offset);
|
|
1063
|
+
offset += part.data.byteLength;
|
|
1064
|
+
}
|
|
1065
|
+
const fileVersion = this.makeFileVersion({
|
|
1066
|
+
bucketId: large.bucketId,
|
|
1067
|
+
fileName: large.fileName,
|
|
1068
|
+
contentType: large.contentType,
|
|
1069
|
+
contentLength: totalSize,
|
|
1070
|
+
contentSha1: "none",
|
|
1071
|
+
action: FileAction.Upload
|
|
1072
|
+
});
|
|
1073
|
+
const stored = { fileVersion, data: combined };
|
|
1074
|
+
const existing = bucket.files.get(large.fileName);
|
|
1075
|
+
if (existing) {
|
|
1076
|
+
existing.push(stored);
|
|
1077
|
+
} else {
|
|
1078
|
+
bucket.files.set(large.fileName, [stored]);
|
|
1079
|
+
}
|
|
1080
|
+
this.largeFiles.delete(req.fileId);
|
|
1081
|
+
this.firePostUploadHooks(fileVersion, large.bucketId, "b2:ObjectCreated:Upload");
|
|
1082
|
+
return { status: 200, body: fileVersion };
|
|
1083
|
+
}
|
|
1084
|
+
cancelLargeFile(req) {
|
|
1085
|
+
const large = this.largeFiles.get(req.fileId);
|
|
1086
|
+
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
1087
|
+
this.largeFiles.delete(req.fileId);
|
|
1088
|
+
return {
|
|
1089
|
+
status: 200,
|
|
1090
|
+
body: {
|
|
1091
|
+
fileId: large.fileId,
|
|
1092
|
+
accountId: this.accountId,
|
|
1093
|
+
bucketId: large.bucketId,
|
|
1094
|
+
fileName: large.fileName
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
listUnfinishedLargeFiles(req) {
|
|
1099
|
+
const countError = validateMaxCount(req.maxFileCount, "b2_list_unfinished_large_files");
|
|
1100
|
+
if (countError) return this.error(400, countError.code, countError.message);
|
|
1101
|
+
const prefix = req.namePrefix ?? "";
|
|
1102
|
+
const max = req.maxFileCount ?? 100;
|
|
1103
|
+
const candidates = [...this.largeFiles.values()].filter((f) => f.bucketId === req.bucketId).filter((f) => f.fileName.startsWith(prefix)).sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
1104
|
+
let startIndex = 0;
|
|
1105
|
+
if (req.startFileId !== void 0) {
|
|
1106
|
+
const found = candidates.findIndex((f) => f.fileId === req.startFileId);
|
|
1107
|
+
startIndex = found >= 0 ? found : 0;
|
|
1108
|
+
}
|
|
1109
|
+
const slice = candidates.slice(startIndex, startIndex + max);
|
|
1110
|
+
const files = slice.map((f) => ({
|
|
1111
|
+
fileId: f.fileId,
|
|
1112
|
+
fileName: f.fileName,
|
|
1113
|
+
accountId: this.accountId,
|
|
1114
|
+
bucketId: f.bucketId,
|
|
1115
|
+
contentType: f.contentType,
|
|
1116
|
+
fileInfo: f.fileInfo
|
|
1117
|
+
}));
|
|
1118
|
+
const hasMore = startIndex + max < candidates.length;
|
|
1119
|
+
const nextFileId = hasMore ? candidates[startIndex + max]?.fileId ?? null : null;
|
|
1120
|
+
return { status: 200, body: { files, nextFileId } };
|
|
1121
|
+
}
|
|
1122
|
+
listParts(req) {
|
|
1123
|
+
const large = this.largeFiles.get(req.fileId);
|
|
1124
|
+
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
1125
|
+
const countError = validateMaxCount(req.maxPartCount, "b2_list_parts");
|
|
1126
|
+
if (countError) return this.error(400, countError.code, countError.message);
|
|
1127
|
+
const start = req.startPartNumber ?? 1;
|
|
1128
|
+
const max = req.maxPartCount ?? 1e3;
|
|
1129
|
+
const allParts = [...large.parts.entries()].filter(([n]) => n >= start).sort((a, b) => a[0] - b[0]).map(([partNumber, part]) => ({
|
|
1130
|
+
fileId: req.fileId,
|
|
1131
|
+
partNumber,
|
|
1132
|
+
contentLength: part.data.byteLength,
|
|
1133
|
+
contentSha1: part.sha1,
|
|
1134
|
+
uploadTimestamp: Date.now()
|
|
1135
|
+
}));
|
|
1136
|
+
const parts = allParts.slice(0, max);
|
|
1137
|
+
const nextPartNumber = allParts.length > max ? allParts[max]?.partNumber ?? null : null;
|
|
1138
|
+
return { status: 200, body: { parts, nextPartNumber } };
|
|
1139
|
+
}
|
|
1140
|
+
async copyPart(req) {
|
|
1141
|
+
const large = this.largeFiles.get(req.largeFileId);
|
|
1142
|
+
if (!large) return this.error(400, "bad_request", "Large file not found");
|
|
1143
|
+
const found = this.findFile(req.sourceFileId);
|
|
1144
|
+
if (found === null) return this.error(404, "file_not_present", "Source file not found");
|
|
1145
|
+
const sourceStored = found.stored;
|
|
1146
|
+
let partData = sourceStored.data;
|
|
1147
|
+
if (req.range) {
|
|
1148
|
+
const match = req.range.match(/bytes=(\d+)-(\d+)?/);
|
|
1149
|
+
if (match) {
|
|
1150
|
+
const rs = Number.parseInt(match[1] ?? "0", 10);
|
|
1151
|
+
const re = match[2] !== void 0 ? Number.parseInt(match[2], 10) : sourceStored.data.byteLength - 1;
|
|
1152
|
+
partData = sourceStored.data.slice(rs, re + 1);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const sha1 = await sha1Hex(partData);
|
|
1156
|
+
large.parts.set(req.partNumber, { data: new Uint8Array(partData), sha1 });
|
|
1157
|
+
return {
|
|
1158
|
+
status: 200,
|
|
1159
|
+
body: {
|
|
1160
|
+
fileId: req.largeFileId,
|
|
1161
|
+
partNumber: req.partNumber,
|
|
1162
|
+
contentLength: partData.byteLength,
|
|
1163
|
+
contentSha1: sha1
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
getDownloadAuthorization(req) {
|
|
1168
|
+
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1169
|
+
return {
|
|
1170
|
+
status: 200,
|
|
1171
|
+
body: {
|
|
1172
|
+
bucketId: req.bucketId,
|
|
1173
|
+
fileNamePrefix: req.fileNamePrefix,
|
|
1174
|
+
authorizationToken: `sim_dl_auth_${this.genId("tok")}`
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
// --- Keys ---
|
|
1179
|
+
createKey(req) {
|
|
1180
|
+
const kid = this.genId("sim_key");
|
|
1181
|
+
const appKey = this.genId("sim_secret");
|
|
1182
|
+
const expiration = req.validDurationInSeconds !== void 0 ? Date.now() + req.validDurationInSeconds * 1e3 : null;
|
|
1183
|
+
const stored = {
|
|
1184
|
+
applicationKeyId: kid,
|
|
1185
|
+
keyName: req.keyName,
|
|
1186
|
+
capabilities: req.capabilities,
|
|
1187
|
+
accountId: req.accountId,
|
|
1188
|
+
applicationKey: appKey,
|
|
1189
|
+
bucketId: req.bucketId ?? null,
|
|
1190
|
+
namePrefix: req.namePrefix ?? null,
|
|
1191
|
+
expirationTimestamp: expiration
|
|
1192
|
+
};
|
|
1193
|
+
this.keys.set(kid, stored);
|
|
1194
|
+
return {
|
|
1195
|
+
status: 200,
|
|
1196
|
+
body: {
|
|
1197
|
+
keyName: stored.keyName,
|
|
1198
|
+
applicationKeyId: stored.applicationKeyId,
|
|
1199
|
+
applicationKey: stored.applicationKey,
|
|
1200
|
+
capabilities: stored.capabilities,
|
|
1201
|
+
accountId: stored.accountId,
|
|
1202
|
+
expirationTimestamp: stored.expirationTimestamp,
|
|
1203
|
+
bucketId: stored.bucketId,
|
|
1204
|
+
namePrefix: stored.namePrefix,
|
|
1205
|
+
options: []
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
listKeys(req) {
|
|
1210
|
+
const countError = validateMaxCount(req.maxKeyCount, "b2_list_keys");
|
|
1211
|
+
if (countError) return this.error(400, countError.code, countError.message);
|
|
1212
|
+
const max = req.maxKeyCount ?? 1e3;
|
|
1213
|
+
let allKeys = [...this.keys.values()].sort(
|
|
1214
|
+
(a, b) => a.applicationKeyId.localeCompare(b.applicationKeyId)
|
|
1215
|
+
);
|
|
1216
|
+
if (req.startApplicationKeyId) {
|
|
1217
|
+
const start = req.startApplicationKeyId;
|
|
1218
|
+
allKeys = allKeys.filter((k) => k.applicationKeyId >= start);
|
|
1219
|
+
}
|
|
1220
|
+
const keys = allKeys.slice(0, max).map((k) => ({
|
|
1221
|
+
keyName: k.keyName,
|
|
1222
|
+
applicationKeyId: k.applicationKeyId,
|
|
1223
|
+
capabilities: k.capabilities,
|
|
1224
|
+
accountId: k.accountId,
|
|
1225
|
+
expirationTimestamp: k.expirationTimestamp,
|
|
1226
|
+
bucketId: k.bucketId,
|
|
1227
|
+
namePrefix: k.namePrefix,
|
|
1228
|
+
options: []
|
|
1229
|
+
}));
|
|
1230
|
+
const nextId = allKeys.length > max ? allKeys[max]?.applicationKeyId ?? null : null;
|
|
1231
|
+
return { status: 200, body: { keys, nextApplicationKeyId: nextId } };
|
|
1232
|
+
}
|
|
1233
|
+
deleteKey(req) {
|
|
1234
|
+
const key = this.keys.get(req.applicationKeyId);
|
|
1235
|
+
if (!key) return this.error(400, "bad_request", "Key not found");
|
|
1236
|
+
this.keys.delete(req.applicationKeyId);
|
|
1237
|
+
for (const [token, scope] of this.issuedTokens.entries()) {
|
|
1238
|
+
if (scope.applicationKeyId === req.applicationKeyId) {
|
|
1239
|
+
this.issuedTokens.delete(token);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
status: 200,
|
|
1244
|
+
body: {
|
|
1245
|
+
keyName: key.keyName,
|
|
1246
|
+
applicationKeyId: key.applicationKeyId,
|
|
1247
|
+
capabilities: key.capabilities,
|
|
1248
|
+
accountId: key.accountId,
|
|
1249
|
+
expirationTimestamp: key.expirationTimestamp,
|
|
1250
|
+
bucketId: key.bucketId,
|
|
1251
|
+
namePrefix: key.namePrefix,
|
|
1252
|
+
options: []
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
// --- File lock ---
|
|
1257
|
+
updateFileRetention(req) {
|
|
1258
|
+
const found = this.findFile(req.fileId);
|
|
1259
|
+
if (found === null || found.stored.fileVersion.fileName !== req.fileName) {
|
|
1260
|
+
return this.error(404, "file_not_present", "File not found");
|
|
1261
|
+
}
|
|
1262
|
+
found.versions[found.index] = {
|
|
1263
|
+
fileVersion: {
|
|
1264
|
+
...found.stored.fileVersion,
|
|
1265
|
+
fileRetention: { isClientAuthorizedToRead: true, value: req.fileRetention }
|
|
1266
|
+
},
|
|
1267
|
+
data: found.stored.data
|
|
1268
|
+
};
|
|
1269
|
+
return {
|
|
1270
|
+
status: 200,
|
|
1271
|
+
body: {
|
|
1272
|
+
fileName: req.fileName,
|
|
1273
|
+
fileId: req.fileId,
|
|
1274
|
+
fileRetention: req.fileRetention
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
updateFileLegalHold(req) {
|
|
1279
|
+
const found = this.findFile(req.fileId);
|
|
1280
|
+
if (found === null || found.stored.fileVersion.fileName !== req.fileName) {
|
|
1281
|
+
return this.error(404, "file_not_present", "File not found");
|
|
1282
|
+
}
|
|
1283
|
+
found.versions[found.index] = {
|
|
1284
|
+
fileVersion: {
|
|
1285
|
+
...found.stored.fileVersion,
|
|
1286
|
+
legalHold: {
|
|
1287
|
+
isClientAuthorizedToRead: true,
|
|
1288
|
+
value: req.legalHold
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
data: found.stored.data
|
|
1292
|
+
};
|
|
1293
|
+
return {
|
|
1294
|
+
status: 200,
|
|
1295
|
+
body: {
|
|
1296
|
+
fileName: req.fileName,
|
|
1297
|
+
fileId: req.fileId,
|
|
1298
|
+
legalHold: req.legalHold
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
// --- Notifications ---
|
|
1303
|
+
getBucketNotificationRules(req) {
|
|
1304
|
+
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1305
|
+
const rules = this.notificationRules.get(req.bucketId) ?? [];
|
|
1306
|
+
return { status: 200, body: { bucketId: req.bucketId, eventNotificationRules: rules } };
|
|
1307
|
+
}
|
|
1308
|
+
setBucketNotificationRules(req) {
|
|
1309
|
+
if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
|
|
1310
|
+
this.notificationRules.set(req.bucketId, req.eventNotificationRules);
|
|
1311
|
+
return {
|
|
1312
|
+
status: 200,
|
|
1313
|
+
body: { bucketId: req.bucketId, eventNotificationRules: req.eventNotificationRules }
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
// --- Helpers ---
|
|
1317
|
+
/**
|
|
1318
|
+
* Locates a stored file version by its `fileId`, scanning every bucket.
|
|
1319
|
+
*
|
|
1320
|
+
* Returns enough context to support read-only inspection (`stored`,
|
|
1321
|
+
* `bucketId`) AND in-place mutation (`versions`, `index`) so callers
|
|
1322
|
+
* that need to splice the version out can do so without re-scanning.
|
|
1323
|
+
*
|
|
1324
|
+
* Real B2 fileIds embed the bucketId, so production lookups are O(1);
|
|
1325
|
+
* the simulator's flat ID generator (`this.genId('4_z')`) doesn't, so this
|
|
1326
|
+
* is O(buckets × files × versions). Acceptable for tests.
|
|
1327
|
+
*
|
|
1328
|
+
* @param fileId - The file version ID to locate.
|
|
1329
|
+
*
|
|
1330
|
+
* @returns The location of the matching version, or `null` if not found.
|
|
1331
|
+
*/
|
|
1332
|
+
findFile(fileId2) {
|
|
1333
|
+
for (const [bid, bucket] of this.buckets.entries()) {
|
|
1334
|
+
for (const versions of bucket.files.values()) {
|
|
1335
|
+
const idx = versions.findIndex((v) => v.fileVersion.fileId === fileId2);
|
|
1336
|
+
if (idx !== -1) {
|
|
1337
|
+
const stored = versions[idx];
|
|
1338
|
+
return { stored, bucketId: bid, bucket, versions, index: idx };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
makeFileVersion(params) {
|
|
1345
|
+
return {
|
|
1346
|
+
accountId: accountId(this.accountId),
|
|
1347
|
+
action: params.action,
|
|
1348
|
+
bucketId: bucketId(params.bucketId),
|
|
1349
|
+
contentLength: params.contentLength,
|
|
1350
|
+
contentMd5: null,
|
|
1351
|
+
contentSha1: params.contentSha1,
|
|
1352
|
+
contentType: params.contentType,
|
|
1353
|
+
fileId: fileId(this.genId("4_z")),
|
|
1354
|
+
fileInfo: {},
|
|
1355
|
+
fileName: params.fileName,
|
|
1356
|
+
fileRetention: { isClientAuthorizedToRead: true, value: null },
|
|
1357
|
+
legalHold: { isClientAuthorizedToRead: true, value: null },
|
|
1358
|
+
replicationStatus: null,
|
|
1359
|
+
serverSideEncryption: { mode: EncryptionMode.None },
|
|
1360
|
+
uploadTimestamp: this.monotonicTimestamp()
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
error(status, code, message) {
|
|
1364
|
+
return { status, body: { status, code, message } };
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Fire the pluggable post-upload hooks for a file that just landed in
|
|
1368
|
+
* a bucket: matching event-notification rules → `onWebhookDeliver`,
|
|
1369
|
+
* configured replication source rules → `onReplicate`. Errors thrown
|
|
1370
|
+
* from user-supplied hooks are swallowed so a buggy listener never
|
|
1371
|
+
* masks an otherwise-successful API response.
|
|
1372
|
+
*
|
|
1373
|
+
* Fired-and-forgotten (no await) by the handler so the synthetic
|
|
1374
|
+
* response is returned to the caller as fast as production B2 would
|
|
1375
|
+
* acknowledge the write.
|
|
1376
|
+
*
|
|
1377
|
+
* @param fileVersion - The freshly-stored file metadata.
|
|
1378
|
+
* @param bucketId - The bucket the upload landed in.
|
|
1379
|
+
* @param eventType - The B2 event-type tag (e.g. `'b2:ObjectCreated:Upload'`)
|
|
1380
|
+
* used to match against `EventNotificationRule.eventTypes` globs.
|
|
1381
|
+
*/
|
|
1382
|
+
firePostUploadHooks(fileVersion, bucketId2, eventType) {
|
|
1383
|
+
const bucket = this.buckets.get(bucketId2);
|
|
1384
|
+
if (!bucket) return;
|
|
1385
|
+
if (this.onWebhookDeliver !== void 0) {
|
|
1386
|
+
const rules = this.notificationRules.get(bucketId2) ?? [];
|
|
1387
|
+
for (const rule of rules) {
|
|
1388
|
+
if (!rule.isEnabled) continue;
|
|
1389
|
+
const matches = rule.eventTypes.some((pattern) => eventTypeMatches(pattern, eventType));
|
|
1390
|
+
if (!matches) continue;
|
|
1391
|
+
const hook = this.onWebhookDeliver;
|
|
1392
|
+
this.dispatchHook("webhook", () => hook({ rule, fileVersion, bucketId: bucketId2 }));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (this.onReplicate !== void 0) {
|
|
1396
|
+
const replConfig = bucket.info.replicationConfiguration;
|
|
1397
|
+
const sourceRules = replConfig.asReplicationSource?.replicationRules ?? [];
|
|
1398
|
+
for (const rule of sourceRules) {
|
|
1399
|
+
if (!rule.isEnabled) continue;
|
|
1400
|
+
if (rule.fileNamePrefix && !fileVersion.fileName.startsWith(rule.fileNamePrefix)) continue;
|
|
1401
|
+
const hook = this.onReplicate;
|
|
1402
|
+
this.dispatchHook(
|
|
1403
|
+
"replication",
|
|
1404
|
+
() => hook({
|
|
1405
|
+
sourceFileVersion: fileVersion,
|
|
1406
|
+
sourceBucketId: bucketId2,
|
|
1407
|
+
destinationBucketId: rule.destinationBucketId
|
|
1408
|
+
})
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Schedule a user-supplied hook callback. Errors are routed to
|
|
1415
|
+
* `onHookError` if configured, swallowed otherwise. The returned
|
|
1416
|
+
* promise is tracked in {@link pendingHooks} so `flushHooks()` can
|
|
1417
|
+
* wait for every dispatched hook to settle.
|
|
1418
|
+
*
|
|
1419
|
+
* @param kind - Which hook this is (`'webhook'` or `'replication'`).
|
|
1420
|
+
* @param fn - Thunk that performs the hook invocation.
|
|
1421
|
+
*/
|
|
1422
|
+
dispatchHook(kind, fn) {
|
|
1423
|
+
const task = Promise.resolve().then(() => fn()).catch((err) => {
|
|
1424
|
+
if (this.onHookError !== void 0) {
|
|
1425
|
+
this.onHookError({ kind, error: toError(err) });
|
|
1426
|
+
}
|
|
1427
|
+
}).finally(() => {
|
|
1428
|
+
this.pendingHooks.delete(task);
|
|
1429
|
+
});
|
|
1430
|
+
this.pendingHooks.add(task);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const eventTypeRegexCache = /* @__PURE__ */ new Map();
|
|
1434
|
+
function eventTypeMatches(pattern, eventType) {
|
|
1435
|
+
if (!pattern.includes("*")) return pattern === eventType;
|
|
1436
|
+
let compiled = eventTypeRegexCache.get(pattern);
|
|
1437
|
+
if (compiled === void 0) {
|
|
1438
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1439
|
+
compiled = new RegExp(`^${escaped}$`);
|
|
1440
|
+
eventTypeRegexCache.set(pattern, compiled);
|
|
1441
|
+
}
|
|
1442
|
+
return compiled.test(eventType);
|
|
1443
|
+
}
|
|
1444
|
+
function buildFaultResponse(fault) {
|
|
1445
|
+
const status = fault.status ?? 503;
|
|
1446
|
+
const code = fault.code ?? "service_unavailable";
|
|
1447
|
+
const message = fault.message ?? "simulated failure";
|
|
1448
|
+
const body = JSON.stringify({ status, code, message });
|
|
1449
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1450
|
+
if (fault.retryAfter !== void 0) {
|
|
1451
|
+
headers.set("Retry-After", String(fault.retryAfter));
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
status,
|
|
1455
|
+
headers,
|
|
1456
|
+
body: new ReadableStream({
|
|
1457
|
+
start(controller) {
|
|
1458
|
+
controller.enqueue(utf8Encoder.encode(body));
|
|
1459
|
+
controller.close();
|
|
1460
|
+
}
|
|
1461
|
+
}),
|
|
1462
|
+
json: () => Promise.resolve(JSON.parse(body)),
|
|
1463
|
+
text: () => Promise.resolve(body),
|
|
1464
|
+
arrayBuffer: () => Promise.resolve(utf8Encoder.encode(body).buffer)
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
class SimulatorTransport {
|
|
1468
|
+
constructor(sim) {
|
|
1469
|
+
this.sim = sim;
|
|
1470
|
+
}
|
|
1471
|
+
async send(request) {
|
|
1472
|
+
const url = request.url;
|
|
1473
|
+
const fault = this.sim.consumeMatchingFault(url);
|
|
1474
|
+
if (fault !== null) {
|
|
1475
|
+
return buildFaultResponse(fault);
|
|
1476
|
+
}
|
|
1477
|
+
const headers = {};
|
|
1478
|
+
if (request.headers) {
|
|
1479
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
1480
|
+
headers[k.toLowerCase()] = v;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const isUpload = url.includes("b2_upload_file") || url.includes("b2_upload_part");
|
|
1484
|
+
const parsedUrl = new URL(url);
|
|
1485
|
+
const isDownload = parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/");
|
|
1486
|
+
if (isDownload) {
|
|
1487
|
+
const method = request.method === "HEAD" ? "HEAD" : "GET";
|
|
1488
|
+
const result2 = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
|
|
1489
|
+
const data = result2.data ?? new Uint8Array(0);
|
|
1490
|
+
const responseHeaders = new Headers(result2.headers);
|
|
1491
|
+
responseHeaders.set(
|
|
1492
|
+
"Content-Type",
|
|
1493
|
+
result2.headers["Content-Type"] ?? "application/octet-stream"
|
|
1494
|
+
);
|
|
1495
|
+
const body = method === "HEAD" || result2.data === null ? null : new ReadableStream({
|
|
1496
|
+
start(controller) {
|
|
1497
|
+
controller.enqueue(data);
|
|
1498
|
+
controller.close();
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
return {
|
|
1502
|
+
status: result2.status,
|
|
1503
|
+
headers: responseHeaders,
|
|
1504
|
+
body,
|
|
1505
|
+
json: () => Promise.reject(new Error("Download response is not JSON")),
|
|
1506
|
+
text: () => Promise.resolve(utf8Decoder.decode(data)),
|
|
1507
|
+
arrayBuffer: () => Promise.resolve(
|
|
1508
|
+
data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
1509
|
+
)
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
let result;
|
|
1513
|
+
if (isUpload && request.body) {
|
|
1514
|
+
const data = new Uint8Array(
|
|
1515
|
+
request.body instanceof ArrayBuffer ? request.body : request.body instanceof Uint8Array ? request.body.buffer.slice(
|
|
1516
|
+
request.body.byteOffset,
|
|
1517
|
+
request.body.byteOffset + request.body.byteLength
|
|
1518
|
+
) : await new Response(request.body).arrayBuffer()
|
|
1519
|
+
);
|
|
1520
|
+
result = this.sim.handleUpload(url, headers, data);
|
|
1521
|
+
} else {
|
|
1522
|
+
let body = null;
|
|
1523
|
+
if (request.body) {
|
|
1524
|
+
const text = typeof request.body === "string" ? request.body : await new Response(request.body).text();
|
|
1525
|
+
try {
|
|
1526
|
+
body = JSON.parse(text);
|
|
1527
|
+
} catch {
|
|
1528
|
+
body = text;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
result = await this.sim.handleRequest(request.method, parsedUrl.pathname, headers, body);
|
|
1532
|
+
}
|
|
1533
|
+
const responseBody = JSON.stringify(result.body);
|
|
1534
|
+
return {
|
|
1535
|
+
status: result.status,
|
|
1536
|
+
headers: new Headers({ "Content-Type": "application/json" }),
|
|
1537
|
+
body: new ReadableStream({
|
|
1538
|
+
start(controller) {
|
|
1539
|
+
controller.enqueue(utf8Encoder.encode(responseBody));
|
|
1540
|
+
controller.close();
|
|
1541
|
+
}
|
|
1542
|
+
}),
|
|
1543
|
+
json: () => Promise.resolve(result.body),
|
|
1544
|
+
text: () => Promise.resolve(responseBody),
|
|
1545
|
+
arrayBuffer: () => Promise.resolve(utf8Encoder.encode(responseBody).buffer)
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
export {
|
|
1550
|
+
B2Simulator,
|
|
1551
|
+
BUCKET_INFO_MAX_KEYS,
|
|
1552
|
+
BUCKET_INFO_VALUE_MAX,
|
|
1553
|
+
BUCKET_NAME_MAX,
|
|
1554
|
+
BUCKET_NAME_MIN,
|
|
1555
|
+
FILE_INFO_TOTAL_MAX,
|
|
1556
|
+
FILE_INFO_VALUE_MAX,
|
|
1557
|
+
FILE_NAME_MAX_BYTES,
|
|
1558
|
+
LIST_ENDPOINT_CAPS
|
|
1559
|
+
};
|
|
1560
|
+
//# sourceMappingURL=index.js.map
|