@indigoai-us/hq-cloud 6.18.44-alpha.8 → 6.18.44
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/dist/bin/sync-runner-planning.d.ts +12 -0
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +18 -0
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +18 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +75 -6
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +22 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +33 -4
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +2 -1
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +6 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +10 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-windows-test-harness.d.ts +19 -0
- package/dist/cli/rescue-windows-test-harness.d.ts.map +1 -0
- package/dist/cli/rescue-windows-test-harness.js +443 -0
- package/dist/cli/rescue-windows-test-harness.js.map +1 -0
- package/dist/company-resolver.d.ts +22 -0
- package/dist/company-resolver.d.ts.map +1 -1
- package/dist/company-resolver.js +1 -1
- package/dist/company-resolver.js.map +1 -1
- package/dist/ignore.d.ts +4 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +10 -12
- package/dist/ignore.js.map +1 -1
- package/dist/journal-snapshot-decoder.js +15 -2
- package/dist/journal-snapshot-decoder.js.map +1 -1
- package/dist/sync/durable-apply.d.ts +2 -0
- package/dist/sync/durable-apply.d.ts.map +1 -1
- package/dist/sync/durable-apply.js +9 -12
- package/dist/sync/durable-apply.js.map +1 -1
- package/dist/sync/fsync-compat.d.ts +13 -0
- package/dist/sync/fsync-compat.d.ts.map +1 -0
- package/dist/sync/fsync-compat.js +33 -0
- package/dist/sync/fsync-compat.js.map +1 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -1
- package/dist/sync/multipart-uploader.js +2 -1
- package/dist/sync/multipart-uploader.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts.map +1 -1
- package/dist/sync/scope-inventory-client.js +7 -2
- package/dist/sync/scope-inventory-client.js.map +1 -1
- package/dist/telemetry.d.ts +31 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +177 -17
- package/dist/telemetry.js.map +1 -1
- package/dist/usage-company-fallback.d.ts +104 -0
- package/dist/usage-company-fallback.d.ts.map +1 -0
- package/dist/usage-company-fallback.js +599 -0
- package/dist/usage-company-fallback.js.map +1 -0
- package/dist/watch-owner-lease.d.ts +3 -0
- package/dist/watch-owner-lease.d.ts.map +1 -1
- package/dist/watch-owner-lease.js +14 -3
- package/dist/watch-owner-lease.js.map +1 -1
- package/package.json +2 -2
- package/dist/active-company.test.d.ts +0 -2
- package/dist/active-company.test.d.ts.map +0 -1
- package/dist/active-company.test.js +0 -149
- package/dist/active-company.test.js.map +0 -1
- package/dist/agent-codex-instructions.test.d.ts +0 -2
- package/dist/agent-codex-instructions.test.d.ts.map +0 -1
- package/dist/agent-codex-instructions.test.js +0 -271
- package/dist/agent-codex-instructions.test.js.map +0 -1
- package/dist/backup-prune.test.d.ts +0 -2
- package/dist/backup-prune.test.d.ts.map +0 -1
- package/dist/backup-prune.test.js +0 -154
- package/dist/backup-prune.test.js.map +0 -1
- package/dist/bandwidth.test.d.ts +0 -2
- package/dist/bandwidth.test.d.ts.map +0 -1
- package/dist/bandwidth.test.js +0 -342
- package/dist/bandwidth.test.js.map +0 -1
- package/dist/best-effort-error.test.d.ts +0 -2
- package/dist/best-effort-error.test.d.ts.map +0 -1
- package/dist/best-effort-error.test.js +0 -54
- package/dist/best-effort-error.test.js.map +0 -1
- package/dist/bin/area-ledger-migration.test.d.ts +0 -2
- package/dist/bin/area-ledger-migration.test.d.ts.map +0 -1
- package/dist/bin/area-ledger-migration.test.js +0 -410
- package/dist/bin/area-ledger-migration.test.js.map +0 -1
- package/dist/bin/backup-prune-runner.test.d.ts +0 -2
- package/dist/bin/backup-prune-runner.test.d.ts.map +0 -1
- package/dist/bin/backup-prune-runner.test.js +0 -22
- package/dist/bin/backup-prune-runner.test.js.map +0 -1
- package/dist/bin/journal-repair.test.d.ts +0 -2
- package/dist/bin/journal-repair.test.d.ts.map +0 -1
- package/dist/bin/journal-repair.test.js +0 -53
- package/dist/bin/journal-repair.test.js.map +0 -1
- package/dist/bin/layout-compatibility.test.d.ts +0 -2
- package/dist/bin/layout-compatibility.test.d.ts.map +0 -1
- package/dist/bin/layout-compatibility.test.js +0 -87
- package/dist/bin/layout-compatibility.test.js.map +0 -1
- package/dist/bin/outpost-heartbeat.test.d.ts +0 -7
- package/dist/bin/outpost-heartbeat.test.d.ts.map +0 -1
- package/dist/bin/outpost-heartbeat.test.js +0 -84
- package/dist/bin/outpost-heartbeat.test.js.map +0 -1
- package/dist/bin/outpost-session-host.test.d.ts +0 -7
- package/dist/bin/outpost-session-host.test.d.ts.map +0 -1
- package/dist/bin/outpost-session-host.test.js +0 -52
- package/dist/bin/outpost-session-host.test.js.map +0 -1
- package/dist/bin/sync-mutation.test.d.ts +0 -2
- package/dist/bin/sync-mutation.test.d.ts.map +0 -1
- package/dist/bin/sync-mutation.test.js +0 -64
- package/dist/bin/sync-mutation.test.js.map +0 -1
- package/dist/bin/sync-runner-args.test.d.ts +0 -2
- package/dist/bin/sync-runner-args.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-args.test.js +0 -82
- package/dist/bin/sync-runner-args.test.js.map +0 -1
- package/dist/bin/sync-runner-auth-wait.test.d.ts +0 -2
- package/dist/bin/sync-runner-auth-wait.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-auth-wait.test.js +0 -406
- package/dist/bin/sync-runner-auth-wait.test.js.map +0 -1
- package/dist/bin/sync-runner-backoff.regression.test.d.ts +0 -29
- package/dist/bin/sync-runner-backoff.regression.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-backoff.regression.test.js +0 -224
- package/dist/bin/sync-runner-backoff.regression.test.js.map +0 -1
- package/dist/bin/sync-runner-company.test.d.ts +0 -2
- package/dist/bin/sync-runner-company.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-company.test.js +0 -2439
- package/dist/bin/sync-runner-company.test.js.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.d.ts +0 -2
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js +0 -648
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot.bench.test.d.ts +0 -2
- package/dist/bin/sync-runner-delete-snapshot.bench.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot.bench.test.js +0 -329
- package/dist/bin/sync-runner-delete-snapshot.bench.test.js.map +0 -1
- package/dist/bin/sync-runner-events.test.d.ts +0 -2
- package/dist/bin/sync-runner-events.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-events.test.js +0 -364
- package/dist/bin/sync-runner-events.test.js.map +0 -1
- package/dist/bin/sync-runner-heap-telemetry.test.d.ts +0 -2
- package/dist/bin/sync-runner-heap-telemetry.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-heap-telemetry.test.js +0 -1966
- package/dist/bin/sync-runner-heap-telemetry.test.js.map +0 -1
- package/dist/bin/sync-runner-manifest.test.d.ts +0 -12
- package/dist/bin/sync-runner-manifest.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-manifest.test.js +0 -921
- package/dist/bin/sync-runner-manifest.test.js.map +0 -1
- package/dist/bin/sync-runner-planning.test.d.ts +0 -2
- package/dist/bin/sync-runner-planning.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-planning.test.js +0 -448
- package/dist/bin/sync-runner-planning.test.js.map +0 -1
- package/dist/bin/sync-runner-post-sync-flags.test.d.ts +0 -2
- package/dist/bin/sync-runner-post-sync-flags.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-post-sync-flags.test.js +0 -64
- package/dist/bin/sync-runner-post-sync-flags.test.js.map +0 -1
- package/dist/bin/sync-runner-publish-expansion.test.d.ts +0 -2
- package/dist/bin/sync-runner-publish-expansion.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-publish-expansion.test.js +0 -54
- package/dist/bin/sync-runner-publish-expansion.test.js.map +0 -1
- package/dist/bin/sync-runner-receiver-unresolved-company.test.d.ts +0 -2
- package/dist/bin/sync-runner-receiver-unresolved-company.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-receiver-unresolved-company.test.js +0 -379
- package/dist/bin/sync-runner-receiver-unresolved-company.test.js.map +0 -1
- package/dist/bin/sync-runner-rollup.test.d.ts +0 -2
- package/dist/bin/sync-runner-rollup.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-rollup.test.js +0 -24
- package/dist/bin/sync-runner-rollup.test.js.map +0 -1
- package/dist/bin/sync-runner-watch-loop.test.d.ts +0 -2
- package/dist/bin/sync-runner-watch-loop.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-watch-loop.test.js +0 -734
- package/dist/bin/sync-runner-watch-loop.test.js.map +0 -1
- package/dist/bin/sync-runner-watch-routes.test.d.ts +0 -2
- package/dist/bin/sync-runner-watch-routes.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-watch-routes.test.js +0 -181
- package/dist/bin/sync-runner-watch-routes.test.js.map +0 -1
- package/dist/bin/sync-runner.test.d.ts +0 -10
- package/dist/bin/sync-runner.test.d.ts.map +0 -1
- package/dist/bin/sync-runner.test.js +0 -18984
- package/dist/bin/sync-runner.test.js.map +0 -1
- package/dist/cli/case-alias.test.d.ts +0 -2
- package/dist/cli/case-alias.test.d.ts.map +0 -1
- package/dist/cli/case-alias.test.js +0 -96
- package/dist/cli/case-alias.test.js.map +0 -1
- package/dist/cli/conflict-recovery.test.d.ts +0 -2
- package/dist/cli/conflict-recovery.test.d.ts.map +0 -1
- package/dist/cli/conflict-recovery.test.js +0 -201
- package/dist/cli/conflict-recovery.test.js.map +0 -1
- package/dist/cli/conflict-winner.test.d.ts +0 -7
- package/dist/cli/conflict-winner.test.d.ts.map +0 -1
- package/dist/cli/conflict-winner.test.js +0 -98
- package/dist/cli/conflict-winner.test.js.map +0 -1
- package/dist/cli/doctor.test.d.ts +0 -13
- package/dist/cli/doctor.test.d.ts.map +0 -1
- package/dist/cli/doctor.test.js +0 -607
- package/dist/cli/doctor.test.js.map +0 -1
- package/dist/cli/invite.test.d.ts +0 -5
- package/dist/cli/invite.test.d.ts.map +0 -1
- package/dist/cli/invite.test.js +0 -178
- package/dist/cli/invite.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.js +0 -300
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner.test.js +0 -266
- package/dist/cli/onepass-pull-planner.test.js.map +0 -1
- package/dist/cli/push-scan-exclusions.test.d.ts +0 -2
- package/dist/cli/push-scan-exclusions.test.d.ts.map +0 -1
- package/dist/cli/push-scan-exclusions.test.js +0 -99
- package/dist/cli/push-scan-exclusions.test.js.map +0 -1
- package/dist/cli/reindex-knowledge.test.d.ts +0 -12
- package/dist/cli/reindex-knowledge.test.d.ts.map +0 -1
- package/dist/cli/reindex-knowledge.test.js +0 -260
- package/dist/cli/reindex-knowledge.test.js.map +0 -1
- package/dist/cli/reindex.test.d.ts +0 -11
- package/dist/cli/reindex.test.d.ts.map +0 -1
- package/dist/cli/reindex.test.js +0 -811
- package/dist/cli/reindex.test.js.map +0 -1
- package/dist/cli/rescue-breaker.test.d.ts +0 -2
- package/dist/cli/rescue-breaker.test.d.ts.map +0 -1
- package/dist/cli/rescue-breaker.test.js +0 -812
- package/dist/cli/rescue-breaker.test.js.map +0 -1
- package/dist/cli/rescue-classify-ordering.test.d.ts +0 -2
- package/dist/cli/rescue-classify-ordering.test.d.ts.map +0 -1
- package/dist/cli/rescue-classify-ordering.test.js +0 -500
- package/dist/cli/rescue-classify-ordering.test.js.map +0 -1
- package/dist/cli/rescue-clone-diagnostics.test.d.ts +0 -2
- package/dist/cli/rescue-clone-diagnostics.test.d.ts.map +0 -1
- package/dist/cli/rescue-clone-diagnostics.test.js +0 -101
- package/dist/cli/rescue-clone-diagnostics.test.js.map +0 -1
- package/dist/cli/rescue-clone-network-retry.test.d.ts +0 -2
- package/dist/cli/rescue-clone-network-retry.test.d.ts.map +0 -1
- package/dist/cli/rescue-clone-network-retry.test.js +0 -415
- package/dist/cli/rescue-clone-network-retry.test.js.map +0 -1
- package/dist/cli/rescue-core-stamp.test.d.ts +0 -2
- package/dist/cli/rescue-core-stamp.test.d.ts.map +0 -1
- package/dist/cli/rescue-core-stamp.test.js +0 -364
- package/dist/cli/rescue-core-stamp.test.js.map +0 -1
- package/dist/cli/rescue-drift-reconcile.test.d.ts +0 -2
- package/dist/cli/rescue-drift-reconcile.test.d.ts.map +0 -1
- package/dist/cli/rescue-drift-reconcile.test.js +0 -162
- package/dist/cli/rescue-drift-reconcile.test.js.map +0 -1
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts +0 -2
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +0 -1
- package/dist/cli/rescue-drop-dir-symlink.test.js +0 -255
- package/dist/cli/rescue-drop-dir-symlink.test.js.map +0 -1
- package/dist/cli/rescue-exec-bit-preserve.test.d.ts +0 -2
- package/dist/cli/rescue-exec-bit-preserve.test.d.ts.map +0 -1
- package/dist/cli/rescue-exec-bit-preserve.test.js +0 -177
- package/dist/cli/rescue-exec-bit-preserve.test.js.map +0 -1
- package/dist/cli/rescue-hq-root-guard.test.d.ts +0 -2
- package/dist/cli/rescue-hq-root-guard.test.d.ts.map +0 -1
- package/dist/cli/rescue-hq-root-guard.test.js +0 -210
- package/dist/cli/rescue-hq-root-guard.test.js.map +0 -1
- package/dist/cli/rescue-journal-reconcile.test.d.ts +0 -2
- package/dist/cli/rescue-journal-reconcile.test.d.ts.map +0 -1
- package/dist/cli/rescue-journal-reconcile.test.js +0 -186
- package/dist/cli/rescue-journal-reconcile.test.js.map +0 -1
- package/dist/cli/rescue-keep-local-ignore.test.d.ts +0 -2
- package/dist/cli/rescue-keep-local-ignore.test.d.ts.map +0 -1
- package/dist/cli/rescue-keep-local-ignore.test.js +0 -315
- package/dist/cli/rescue-keep-local-ignore.test.js.map +0 -1
- package/dist/cli/rescue-move.test.d.ts +0 -2
- package/dist/cli/rescue-move.test.d.ts.map +0 -1
- package/dist/cli/rescue-move.test.js +0 -166
- package/dist/cli/rescue-move.test.js.map +0 -1
- package/dist/cli/rescue-mtime-preserve.test.d.ts +0 -2
- package/dist/cli/rescue-mtime-preserve.test.d.ts.map +0 -1
- package/dist/cli/rescue-mtime-preserve.test.js +0 -186
- package/dist/cli/rescue-mtime-preserve.test.js.map +0 -1
- package/dist/cli/rescue-preserve-copy-symlink.test.d.ts +0 -2
- package/dist/cli/rescue-preserve-copy-symlink.test.d.ts.map +0 -1
- package/dist/cli/rescue-preserve-copy-symlink.test.js +0 -483
- package/dist/cli/rescue-preserve-copy-symlink.test.js.map +0 -1
- package/dist/cli/rescue-realpath.test.d.ts +0 -2
- package/dist/cli/rescue-realpath.test.d.ts.map +0 -1
- package/dist/cli/rescue-realpath.test.js +0 -109
- package/dist/cli/rescue-realpath.test.js.map +0 -1
- package/dist/cli/rescue-restore-flag.test.d.ts +0 -2
- package/dist/cli/rescue-restore-flag.test.d.ts.map +0 -1
- package/dist/cli/rescue-restore-flag.test.js +0 -82
- package/dist/cli/rescue-restore-flag.test.js.map +0 -1
- package/dist/cli/rescue-rsync-preflight.test.d.ts +0 -2
- package/dist/cli/rescue-rsync-preflight.test.d.ts.map +0 -1
- package/dist/cli/rescue-rsync-preflight.test.js +0 -579
- package/dist/cli/rescue-rsync-preflight.test.js.map +0 -1
- package/dist/cli/rescue-rsync-status.test.d.ts +0 -2
- package/dist/cli/rescue-rsync-status.test.d.ts.map +0 -1
- package/dist/cli/rescue-rsync-status.test.js +0 -406
- package/dist/cli/rescue-rsync-status.test.js.map +0 -1
- package/dist/cli/rescue-settings-reconcile.test.d.ts +0 -2
- package/dist/cli/rescue-settings-reconcile.test.d.ts.map +0 -1
- package/dist/cli/rescue-settings-reconcile.test.js +0 -610
- package/dist/cli/rescue-settings-reconcile.test.js.map +0 -1
- package/dist/cli/rescue-snapshot-copy.test.d.ts +0 -2
- package/dist/cli/rescue-snapshot-copy.test.d.ts.map +0 -1
- package/dist/cli/rescue-snapshot-copy.test.js +0 -790
- package/dist/cli/rescue-snapshot-copy.test.js.map +0 -1
- package/dist/cli/rescue-snapshot-recovery.test.d.ts +0 -2
- package/dist/cli/rescue-snapshot-recovery.test.d.ts.map +0 -1
- package/dist/cli/rescue-snapshot-recovery.test.js +0 -265
- package/dist/cli/rescue-snapshot-recovery.test.js.map +0 -1
- package/dist/cli/rescue-snapshot.test.d.ts +0 -2
- package/dist/cli/rescue-snapshot.test.d.ts.map +0 -1
- package/dist/cli/rescue-snapshot.test.js +0 -46
- package/dist/cli/rescue-snapshot.test.js.map +0 -1
- package/dist/cli/rescue-source-switch-floor.test.d.ts +0 -2
- package/dist/cli/rescue-source-switch-floor.test.d.ts.map +0 -1
- package/dist/cli/rescue-source-switch-floor.test.js +0 -247
- package/dist/cli/rescue-source-switch-floor.test.js.map +0 -1
- package/dist/cli/rescue-unchanged-delete.test.d.ts +0 -2
- package/dist/cli/rescue-unchanged-delete.test.d.ts.map +0 -1
- package/dist/cli/rescue-unchanged-delete.test.js +0 -305
- package/dist/cli/rescue-unchanged-delete.test.js.map +0 -1
- package/dist/cli/rescue.reindex.test.d.ts +0 -2
- package/dist/cli/rescue.reindex.test.d.ts.map +0 -1
- package/dist/cli/rescue.reindex.test.js +0 -72
- package/dist/cli/rescue.reindex.test.js.map +0 -1
- package/dist/cli/rescue.test.d.ts +0 -2
- package/dist/cli/rescue.test.d.ts.map +0 -1
- package/dist/cli/rescue.test.js +0 -127
- package/dist/cli/rescue.test.js.map +0 -1
- package/dist/cli/share-plan-limit-presign.test.d.ts +0 -2
- package/dist/cli/share-plan-limit-presign.test.d.ts.map +0 -1
- package/dist/cli/share-plan-limit-presign.test.js +0 -271
- package/dist/cli/share-plan-limit-presign.test.js.map +0 -1
- package/dist/cli/share-presign-transport.test.d.ts +0 -22
- package/dist/cli/share-presign-transport.test.d.ts.map +0 -1
- package/dist/cli/share-presign-transport.test.js +0 -388
- package/dist/cli/share-presign-transport.test.js.map +0 -1
- package/dist/cli/share.test.d.ts +0 -5
- package/dist/cli/share.test.d.ts.map +0 -1
- package/dist/cli/share.test.js +0 -12335
- package/dist/cli/share.test.js.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +0 -15
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.js +0 -273
- package/dist/cli/streaming-pull-planner.bench.test.js.map +0 -1
- package/dist/cli/sync-scope.test.d.ts +0 -22
- package/dist/cli/sync-scope.test.d.ts.map +0 -1
- package/dist/cli/sync-scope.test.js +0 -903
- package/dist/cli/sync-scope.test.js.map +0 -1
- package/dist/cli/sync.test.d.ts +0 -5
- package/dist/cli/sync.test.d.ts.map +0 -1
- package/dist/cli/sync.test.js +0 -9028
- package/dist/cli/sync.test.js.map +0 -1
- package/dist/cli/tombstones.test.d.ts +0 -2
- package/dist/cli/tombstones.test.d.ts.map +0 -1
- package/dist/cli/tombstones.test.js +0 -120
- package/dist/cli/tombstones.test.js.map +0 -1
- package/dist/cli/watch-event-push-conflict.test.d.ts +0 -25
- package/dist/cli/watch-event-push-conflict.test.d.ts.map +0 -1
- package/dist/cli/watch-event-push-conflict.test.js +0 -230
- package/dist/cli/watch-event-push-conflict.test.js.map +0 -1
- package/dist/client-info.test.d.ts +0 -11
- package/dist/client-info.test.d.ts.map +0 -1
- package/dist/client-info.test.js +0 -168
- package/dist/client-info.test.js.map +0 -1
- package/dist/cognito-auth-lock-no-hard-links.test.d.ts +0 -11
- package/dist/cognito-auth-lock-no-hard-links.test.d.ts.map +0 -1
- package/dist/cognito-auth-lock-no-hard-links.test.js +0 -76
- package/dist/cognito-auth-lock-no-hard-links.test.js.map +0 -1
- package/dist/cognito-auth.test.d.ts +0 -9
- package/dist/cognito-auth.test.d.ts.map +0 -1
- package/dist/cognito-auth.test.js +0 -745
- package/dist/cognito-auth.test.js.map +0 -1
- package/dist/company-resolver.test.d.ts +0 -7
- package/dist/company-resolver.test.d.ts.map +0 -1
- package/dist/company-resolver.test.js +0 -533
- package/dist/company-resolver.test.js.map +0 -1
- package/dist/company-slug.test.d.ts +0 -7
- package/dist/company-slug.test.d.ts.map +0 -1
- package/dist/company-slug.test.js +0 -43
- package/dist/company-slug.test.js.map +0 -1
- package/dist/company-vault-transport.test.d.ts +0 -12
- package/dist/company-vault-transport.test.d.ts.map +0 -1
- package/dist/company-vault-transport.test.js +0 -97
- package/dist/company-vault-transport.test.js.map +0 -1
- package/dist/context.test.d.ts +0 -7
- package/dist/context.test.d.ts.map +0 -1
- package/dist/context.test.js +0 -646
- package/dist/context.test.js.map +0 -1
- package/dist/entity-resolver.test.d.ts +0 -10
- package/dist/entity-resolver.test.d.ts.map +0 -1
- package/dist/entity-resolver.test.js +0 -244
- package/dist/entity-resolver.test.js.map +0 -1
- package/dist/getindigo-dead-refs-contract.test.d.ts +0 -2
- package/dist/getindigo-dead-refs-contract.test.d.ts.map +0 -1
- package/dist/getindigo-dead-refs-contract.test.js +0 -58
- package/dist/getindigo-dead-refs-contract.test.js.map +0 -1
- package/dist/hermetic-test-env.test.d.ts +0 -2
- package/dist/hermetic-test-env.test.d.ts.map +0 -1
- package/dist/hermetic-test-env.test.js +0 -61
- package/dist/hermetic-test-env.test.js.map +0 -1
- package/dist/hq-root-recovery.test.d.ts +0 -2
- package/dist/hq-root-recovery.test.d.ts.map +0 -1
- package/dist/hq-root-recovery.test.js +0 -65
- package/dist/hq-root-recovery.test.js.map +0 -1
- package/dist/ignore.test.d.ts +0 -10
- package/dist/ignore.test.d.ts.map +0 -1
- package/dist/ignore.test.js +0 -926
- package/dist/ignore.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -26
- package/dist/index.test.js.map +0 -1
- package/dist/journal-area-ledger-read-boundary.test.d.ts +0 -2
- package/dist/journal-area-ledger-read-boundary.test.d.ts.map +0 -1
- package/dist/journal-area-ledger-read-boundary.test.js +0 -420
- package/dist/journal-area-ledger-read-boundary.test.js.map +0 -1
- package/dist/journal-baseline.test.d.ts +0 -2
- package/dist/journal-baseline.test.d.ts.map +0 -1
- package/dist/journal-baseline.test.js +0 -218
- package/dist/journal-baseline.test.js.map +0 -1
- package/dist/journal-decode-window.test.d.ts +0 -2
- package/dist/journal-decode-window.test.d.ts.map +0 -1
- package/dist/journal-decode-window.test.js +0 -99
- package/dist/journal-decode-window.test.js.map +0 -1
- package/dist/journal-delta-writes.test.d.ts +0 -2
- package/dist/journal-delta-writes.test.d.ts.map +0 -1
- package/dist/journal-delta-writes.test.js +0 -159
- package/dist/journal-delta-writes.test.js.map +0 -1
- package/dist/journal-ledger-index.test.d.ts +0 -15
- package/dist/journal-ledger-index.test.d.ts.map +0 -1
- package/dist/journal-ledger-index.test.js +0 -269
- package/dist/journal-ledger-index.test.js.map +0 -1
- package/dist/journal-public-boundary.test.d.ts +0 -2
- package/dist/journal-public-boundary.test.d.ts.map +0 -1
- package/dist/journal-public-boundary.test.js +0 -216
- package/dist/journal-public-boundary.test.js.map +0 -1
- package/dist/journal-row-store.test.d.ts +0 -2
- package/dist/journal-row-store.test.d.ts.map +0 -1
- package/dist/journal-row-store.test.js +0 -729
- package/dist/journal-row-store.test.js.map +0 -1
- package/dist/journal-snapshot-memory-helpers.d.ts +0 -33
- package/dist/journal-snapshot-memory-helpers.d.ts.map +0 -1
- package/dist/journal-snapshot-memory-helpers.js +0 -212
- package/dist/journal-snapshot-memory-helpers.js.map +0 -1
- package/dist/journal-snapshot-memory.test.d.ts +0 -2
- package/dist/journal-snapshot-memory.test.d.ts.map +0 -1
- package/dist/journal-snapshot-memory.test.js +0 -47
- package/dist/journal-snapshot-memory.test.js.map +0 -1
- package/dist/journal-snapshot-v4-memory.test.d.ts +0 -2
- package/dist/journal-snapshot-v4-memory.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4-memory.test.js +0 -24
- package/dist/journal-snapshot-v4-memory.test.js.map +0 -1
- package/dist/journal-snapshot-v4-writer-memory.test.d.ts +0 -2
- package/dist/journal-snapshot-v4-writer-memory.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4-writer-memory.test.js +0 -76
- package/dist/journal-snapshot-v4-writer-memory.test.js.map +0 -1
- package/dist/journal-snapshot-v4-writer.test.d.ts +0 -2
- package/dist/journal-snapshot-v4-writer.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4-writer.test.js +0 -282
- package/dist/journal-snapshot-v4-writer.test.js.map +0 -1
- package/dist/journal-snapshot-v4.test.d.ts +0 -2
- package/dist/journal-snapshot-v4.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4.test.js +0 -281
- package/dist/journal-snapshot-v4.test.js.map +0 -1
- package/dist/journal-store.test.d.ts +0 -2
- package/dist/journal-store.test.d.ts.map +0 -1
- package/dist/journal-store.test.js +0 -1227
- package/dist/journal-store.test.js.map +0 -1
- package/dist/journal-streaming.heap.test.d.ts +0 -2
- package/dist/journal-streaming.heap.test.d.ts.map +0 -1
- package/dist/journal-streaming.heap.test.js +0 -190
- package/dist/journal-streaming.heap.test.js.map +0 -1
- package/dist/journal.test.d.ts +0 -9
- package/dist/journal.test.d.ts.map +0 -1
- package/dist/journal.test.js +0 -1074
- package/dist/journal.test.js.map +0 -1
- package/dist/kms10-etag-safe-pull.test.d.ts +0 -2
- package/dist/kms10-etag-safe-pull.test.d.ts.map +0 -1
- package/dist/kms10-etag-safe-pull.test.js +0 -53
- package/dist/kms10-etag-safe-pull.test.js.map +0 -1
- package/dist/lib/cloud-authoritative.test.d.ts +0 -2
- package/dist/lib/cloud-authoritative.test.d.ts.map +0 -1
- package/dist/lib/cloud-authoritative.test.js +0 -42
- package/dist/lib/cloud-authoritative.test.js.map +0 -1
- package/dist/lib/conflict-reconcile.test.d.ts +0 -7
- package/dist/lib/conflict-reconcile.test.d.ts.map +0 -1
- package/dist/lib/conflict-reconcile.test.js +0 -246
- package/dist/lib/conflict-reconcile.test.js.map +0 -1
- package/dist/lib/conflict.test.d.ts +0 -9
- package/dist/lib/conflict.test.d.ts.map +0 -1
- package/dist/lib/conflict.test.js +0 -340
- package/dist/lib/conflict.test.js.map +0 -1
- package/dist/lib/describe-error.test.d.ts +0 -2
- package/dist/lib/describe-error.test.d.ts.map +0 -1
- package/dist/lib/describe-error.test.js +0 -148
- package/dist/lib/describe-error.test.js.map +0 -1
- package/dist/lib/machine-id.test.d.ts +0 -8
- package/dist/lib/machine-id.test.d.ts.map +0 -1
- package/dist/lib/machine-id.test.js +0 -228
- package/dist/lib/machine-id.test.js.map +0 -1
- package/dist/lib/net-errors.test.d.ts +0 -2
- package/dist/lib/net-errors.test.d.ts.map +0 -1
- package/dist/lib/net-errors.test.js +0 -111
- package/dist/lib/net-errors.test.js.map +0 -1
- package/dist/lib/ontology-entity-meta.test.d.ts +0 -2
- package/dist/lib/ontology-entity-meta.test.d.ts.map +0 -1
- package/dist/lib/ontology-entity-meta.test.js +0 -79
- package/dist/lib/ontology-entity-meta.test.js.map +0 -1
- package/dist/lib/ontology-entity-push.test.d.ts +0 -2
- package/dist/lib/ontology-entity-push.test.d.ts.map +0 -1
- package/dist/lib/ontology-entity-push.test.js +0 -156
- package/dist/lib/ontology-entity-push.test.js.map +0 -1
- package/dist/lib/personal-connector-settings-sync.test.d.ts +0 -2
- package/dist/lib/personal-connector-settings-sync.test.d.ts.map +0 -1
- package/dist/lib/personal-connector-settings-sync.test.js +0 -34
- package/dist/lib/personal-connector-settings-sync.test.js.map +0 -1
- package/dist/lib/readlink-safe.test.d.ts +0 -2
- package/dist/lib/readlink-safe.test.d.ts.map +0 -1
- package/dist/lib/readlink-safe.test.js +0 -34
- package/dist/lib/readlink-safe.test.js.map +0 -1
- package/dist/lib/s3-content-checksum.test.d.ts +0 -2
- package/dist/lib/s3-content-checksum.test.d.ts.map +0 -1
- package/dist/lib/s3-content-checksum.test.js +0 -26
- package/dist/lib/s3-content-checksum.test.js.map +0 -1
- package/dist/lib/skill-register-landed-bytes.test.d.ts +0 -2
- package/dist/lib/skill-register-landed-bytes.test.d.ts.map +0 -1
- package/dist/lib/skill-register-landed-bytes.test.js +0 -83
- package/dist/lib/skill-register-landed-bytes.test.js.map +0 -1
- package/dist/local-path-codec.test.d.ts +0 -14
- package/dist/local-path-codec.test.d.ts.map +0 -1
- package/dist/local-path-codec.test.js +0 -122
- package/dist/local-path-codec.test.js.map +0 -1
- package/dist/machine-auth.test.d.ts +0 -14
- package/dist/machine-auth.test.d.ts.map +0 -1
- package/dist/machine-auth.test.js +0 -1482
- package/dist/machine-auth.test.js.map +0 -1
- package/dist/machine-token-cache-isolation.test.d.ts +0 -24
- package/dist/machine-token-cache-isolation.test.d.ts.map +0 -1
- package/dist/machine-token-cache-isolation.test.js +0 -284
- package/dist/machine-token-cache-isolation.test.js.map +0 -1
- package/dist/manifest/US-002.acceptance.test.d.ts +0 -11
- package/dist/manifest/US-002.acceptance.test.d.ts.map +0 -1
- package/dist/manifest/US-002.acceptance.test.js +0 -160
- package/dist/manifest/US-002.acceptance.test.js.map +0 -1
- package/dist/manifest/US-004.acceptance.test.d.ts +0 -12
- package/dist/manifest/US-004.acceptance.test.d.ts.map +0 -1
- package/dist/manifest/US-004.acceptance.test.js +0 -173
- package/dist/manifest/US-004.acceptance.test.js.map +0 -1
- package/dist/manifest/build-manifest-resident-parity.test.d.ts +0 -2
- package/dist/manifest/build-manifest-resident-parity.test.d.ts.map +0 -1
- package/dist/manifest/build-manifest-resident-parity.test.js +0 -69
- package/dist/manifest/build-manifest-resident-parity.test.js.map +0 -1
- package/dist/manifest/build-manifest.bench.test.d.ts +0 -22
- package/dist/manifest/build-manifest.bench.test.d.ts.map +0 -1
- package/dist/manifest/build-manifest.bench.test.js +0 -930
- package/dist/manifest/build-manifest.bench.test.js.map +0 -1
- package/dist/manifest/build-manifest.test.d.ts +0 -10
- package/dist/manifest/build-manifest.test.d.ts.map +0 -1
- package/dist/manifest/build-manifest.test.js +0 -1217
- package/dist/manifest/build-manifest.test.js.map +0 -1
- package/dist/manifest/contract.test.d.ts +0 -17
- package/dist/manifest/contract.test.d.ts.map +0 -1
- package/dist/manifest/contract.test.js +0 -346
- package/dist/manifest/contract.test.js.map +0 -1
- package/dist/manifest/import-graph.test.d.ts +0 -25
- package/dist/manifest/import-graph.test.d.ts.map +0 -1
- package/dist/manifest/import-graph.test.js +0 -120
- package/dist/manifest/import-graph.test.js.map +0 -1
- package/dist/manifest/ledger-shard-isolation.test.d.ts +0 -18
- package/dist/manifest/ledger-shard-isolation.test.d.ts.map +0 -1
- package/dist/manifest/ledger-shard-isolation.test.js +0 -153
- package/dist/manifest/ledger-shard-isolation.test.js.map +0 -1
- package/dist/manifest/snapshot-store.test.d.ts +0 -11
- package/dist/manifest/snapshot-store.test.d.ts.map +0 -1
- package/dist/manifest/snapshot-store.test.js +0 -206
- package/dist/manifest/snapshot-store.test.js.map +0 -1
- package/dist/manifest/upload-manifest.test.d.ts +0 -10
- package/dist/manifest/upload-manifest.test.d.ts.map +0 -1
- package/dist/manifest/upload-manifest.test.js +0 -1335
- package/dist/manifest/upload-manifest.test.js.map +0 -1
- package/dist/manifest-reconcile.test.d.ts +0 -2
- package/dist/manifest-reconcile.test.d.ts.map +0 -1
- package/dist/manifest-reconcile.test.js +0 -1029
- package/dist/manifest-reconcile.test.js.map +0 -1
- package/dist/object-io.test.d.ts +0 -11
- package/dist/object-io.test.d.ts.map +0 -1
- package/dist/object-io.test.js +0 -2376
- package/dist/object-io.test.js.map +0 -1
- package/dist/operation-lock.test.d.ts +0 -5
- package/dist/operation-lock.test.d.ts.map +0 -1
- package/dist/operation-lock.test.js +0 -796
- package/dist/operation-lock.test.js.map +0 -1
- package/dist/outcome-telemetry.test.d.ts +0 -9
- package/dist/outcome-telemetry.test.d.ts.map +0 -1
- package/dist/outcome-telemetry.test.js +0 -663
- package/dist/outcome-telemetry.test.js.map +0 -1
- package/dist/outposts/client.test.d.ts +0 -9
- package/dist/outposts/client.test.d.ts.map +0 -1
- package/dist/outposts/client.test.js +0 -415
- package/dist/outposts/client.test.js.map +0 -1
- package/dist/outposts/command.test.d.ts +0 -18
- package/dist/outposts/command.test.d.ts.map +0 -1
- package/dist/outposts/command.test.js +0 -1230
- package/dist/outposts/command.test.js.map +0 -1
- package/dist/outposts/replica-sync.test.d.ts +0 -2
- package/dist/outposts/replica-sync.test.d.ts.map +0 -1
- package/dist/outposts/replica-sync.test.js +0 -224
- package/dist/outposts/replica-sync.test.js.map +0 -1
- package/dist/outposts/self-deploy.test.d.ts +0 -2
- package/dist/outposts/self-deploy.test.d.ts.map +0 -1
- package/dist/outposts/self-deploy.test.js +0 -218
- package/dist/outposts/self-deploy.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-bounds.test.d.ts +0 -25
- package/dist/outposts/session-heartbeat-bounds.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-bounds.test.js +0 -156
- package/dist/outposts/session-heartbeat-bounds.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-guard.test.d.ts +0 -16
- package/dist/outposts/session-heartbeat-guard.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-guard.test.js +0 -88
- package/dist/outposts/session-heartbeat-guard.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-publisher.test.d.ts +0 -10
- package/dist/outposts/session-heartbeat-publisher.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-publisher.test.js +0 -153
- package/dist/outposts/session-heartbeat-publisher.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-retain-guard.test.d.ts +0 -18
- package/dist/outposts/session-heartbeat-retain-guard.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-retain-guard.test.js +0 -106
- package/dist/outposts/session-heartbeat-retain-guard.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-scan.test.d.ts +0 -20
- package/dist/outposts/session-heartbeat-scan.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-scan.test.js +0 -241
- package/dist/outposts/session-heartbeat-scan.test.js.map +0 -1
- package/dist/outposts/session-heartbeat.test.d.ts +0 -11
- package/dist/outposts/session-heartbeat.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat.test.js +0 -370
- package/dist/outposts/session-heartbeat.test.js.map +0 -1
- package/dist/outposts/session-host-auth.test.d.ts +0 -16
- package/dist/outposts/session-host-auth.test.d.ts.map +0 -1
- package/dist/outposts/session-host-auth.test.js +0 -284
- package/dist/outposts/session-host-auth.test.js.map +0 -1
- package/dist/outposts/session-host-command.test.d.ts +0 -10
- package/dist/outposts/session-host-command.test.d.ts.map +0 -1
- package/dist/outposts/session-host-command.test.js +0 -180
- package/dist/outposts/session-host-command.test.js.map +0 -1
- package/dist/outposts/session-host-connection.test.d.ts +0 -18
- package/dist/outposts/session-host-connection.test.d.ts.map +0 -1
- package/dist/outposts/session-host-connection.test.js +0 -392
- package/dist/outposts/session-host-connection.test.js.map +0 -1
- package/dist/outposts/session-host-fakes.d.ts +0 -61
- package/dist/outposts/session-host-fakes.d.ts.map +0 -1
- package/dist/outposts/session-host-fakes.js +0 -115
- package/dist/outposts/session-host-fakes.js.map +0 -1
- package/dist/outposts/session-host-ndjson.test.d.ts +0 -11
- package/dist/outposts/session-host-ndjson.test.d.ts.map +0 -1
- package/dist/outposts/session-host-ndjson.test.js +0 -73
- package/dist/outposts/session-host-ndjson.test.js.map +0 -1
- package/dist/outposts/session-host-protocol.test.d.ts +0 -15
- package/dist/outposts/session-host-protocol.test.d.ts.map +0 -1
- package/dist/outposts/session-host-protocol.test.js +0 -0
- package/dist/outposts/session-host-protocol.test.js.map +0 -1
- package/dist/outposts/session-host-server.test.d.ts +0 -10
- package/dist/outposts/session-host-server.test.d.ts.map +0 -1
- package/dist/outposts/session-host-server.test.js +0 -118
- package/dist/outposts/session-host-server.test.js.map +0 -1
- package/dist/outposts/session-host-sessions.test.d.ts +0 -18
- package/dist/outposts/session-host-sessions.test.d.ts.map +0 -1
- package/dist/outposts/session-host-sessions.test.js +0 -439
- package/dist/outposts/session-host-sessions.test.js.map +0 -1
- package/dist/outposts/terminal.test.d.ts +0 -2
- package/dist/outposts/terminal.test.d.ts.map +0 -1
- package/dist/outposts/terminal.test.js +0 -146
- package/dist/outposts/terminal.test.js.map +0 -1
- package/dist/outposts/transport.test.d.ts +0 -10
- package/dist/outposts/transport.test.d.ts.map +0 -1
- package/dist/outposts/transport.test.js +0 -205
- package/dist/outposts/transport.test.js.map +0 -1
- package/dist/packed-journal-phase2.test.d.ts +0 -2
- package/dist/packed-journal-phase2.test.d.ts.map +0 -1
- package/dist/packed-journal-phase2.test.js +0 -273
- package/dist/packed-journal-phase2.test.js.map +0 -1
- package/dist/path-ancestors.test.d.ts +0 -2
- package/dist/path-ancestors.test.d.ts.map +0 -1
- package/dist/path-ancestors.test.js +0 -21
- package/dist/path-ancestors.test.js.map +0 -1
- package/dist/personal-vault-exclusions.test.d.ts +0 -22
- package/dist/personal-vault-exclusions.test.d.ts.map +0 -1
- package/dist/personal-vault-exclusions.test.js +0 -294
- package/dist/personal-vault-exclusions.test.js.map +0 -1
- package/dist/personal-vault.test.d.ts +0 -18
- package/dist/personal-vault.test.d.ts.map +0 -1
- package/dist/personal-vault.test.js +0 -1099
- package/dist/personal-vault.test.js.map +0 -1
- package/dist/plan-limit-pause.test.d.ts +0 -2
- package/dist/plan-limit-pause.test.d.ts.map +0 -1
- package/dist/plan-limit-pause.test.js +0 -150
- package/dist/plan-limit-pause.test.js.map +0 -1
- package/dist/plan-limit.fixtures.d.ts +0 -34
- package/dist/plan-limit.fixtures.d.ts.map +0 -1
- package/dist/plan-limit.fixtures.js +0 -96
- package/dist/plan-limit.fixtures.js.map +0 -1
- package/dist/plan-limit.test.d.ts +0 -2
- package/dist/plan-limit.test.d.ts.map +0 -1
- package/dist/plan-limit.test.js +0 -132
- package/dist/plan-limit.test.js.map +0 -1
- package/dist/prefix-coalesce.test.d.ts +0 -2
- package/dist/prefix-coalesce.test.d.ts.map +0 -1
- package/dist/prefix-coalesce.test.js +0 -260
- package/dist/prefix-coalesce.test.js.map +0 -1
- package/dist/private-folder-transport.test.d.ts +0 -2
- package/dist/private-folder-transport.test.d.ts.map +0 -1
- package/dist/private-folder-transport.test.js +0 -33
- package/dist/private-folder-transport.test.js.map +0 -1
- package/dist/public-surface.test.d.ts +0 -15
- package/dist/public-surface.test.d.ts.map +0 -1
- package/dist/public-surface.test.js +0 -110
- package/dist/public-surface.test.js.map +0 -1
- package/dist/qmd-reindex.test.d.ts +0 -14
- package/dist/qmd-reindex.test.d.ts.map +0 -1
- package/dist/qmd-reindex.test.js +0 -899
- package/dist/qmd-reindex.test.js.map +0 -1
- package/dist/read-only-state-dir.test.d.ts +0 -27
- package/dist/read-only-state-dir.test.d.ts.map +0 -1
- package/dist/read-only-state-dir.test.js +0 -167
- package/dist/read-only-state-dir.test.js.map +0 -1
- package/dist/remote-pull.test.d.ts +0 -2
- package/dist/remote-pull.test.d.ts.map +0 -1
- package/dist/remote-pull.test.js +0 -1147
- package/dist/remote-pull.test.js.map +0 -1
- package/dist/s3-download-stream-hash.test.d.ts +0 -2
- package/dist/s3-download-stream-hash.test.d.ts.map +0 -1
- package/dist/s3-download-stream-hash.test.js +0 -70
- package/dist/s3-download-stream-hash.test.js.map +0 -1
- package/dist/s3.list-cost.test.d.ts +0 -2
- package/dist/s3.list-cost.test.d.ts.map +0 -1
- package/dist/s3.list-cost.test.js +0 -51
- package/dist/s3.list-cost.test.js.map +0 -1
- package/dist/s3.rename-retry.test.d.ts +0 -2
- package/dist/s3.rename-retry.test.d.ts.map +0 -1
- package/dist/s3.rename-retry.test.js +0 -217
- package/dist/s3.rename-retry.test.js.map +0 -1
- package/dist/s3.symlink-materialize.test.d.ts +0 -18
- package/dist/s3.symlink-materialize.test.d.ts.map +0 -1
- package/dist/s3.symlink-materialize.test.js +0 -579
- package/dist/s3.symlink-materialize.test.js.map +0 -1
- package/dist/s3.test.d.ts +0 -9
- package/dist/s3.test.d.ts.map +0 -1
- package/dist/s3.test.js +0 -2047
- package/dist/s3.test.js.map +0 -1
- package/dist/s3.windows-portability.test.d.ts +0 -12
- package/dist/s3.windows-portability.test.d.ts.map +0 -1
- package/dist/s3.windows-portability.test.js +0 -223
- package/dist/s3.windows-portability.test.js.map +0 -1
- package/dist/schemas/signal-types.test.d.ts +0 -2
- package/dist/schemas/signal-types.test.d.ts.map +0 -1
- package/dist/schemas/signal-types.test.js +0 -65
- package/dist/schemas/signal-types.test.js.map +0 -1
- package/dist/schemas/source-channels.test.d.ts +0 -2
- package/dist/schemas/source-channels.test.d.ts.map +0 -1
- package/dist/schemas/source-channels.test.js +0 -65
- package/dist/schemas/source-channels.test.js.map +0 -1
- package/dist/scope-shrink.test.d.ts +0 -13
- package/dist/scope-shrink.test.d.ts.map +0 -1
- package/dist/scope-shrink.test.js +0 -558
- package/dist/scope-shrink.test.js.map +0 -1
- package/dist/session-log-capture.test.d.ts +0 -2
- package/dist/session-log-capture.test.d.ts.map +0 -1
- package/dist/session-log-capture.test.js +0 -533
- package/dist/session-log-capture.test.js.map +0 -1
- package/dist/session-log-retention.test.d.ts +0 -2
- package/dist/session-log-retention.test.d.ts.map +0 -1
- package/dist/session-log-retention.test.js +0 -383
- package/dist/session-log-retention.test.js.map +0 -1
- package/dist/signals/get.test.d.ts +0 -5
- package/dist/signals/get.test.d.ts.map +0 -1
- package/dist/signals/get.test.js +0 -254
- package/dist/signals/get.test.js.map +0 -1
- package/dist/signals/list.test.d.ts +0 -9
- package/dist/signals/list.test.d.ts.map +0 -1
- package/dist/signals/list.test.js +0 -330
- package/dist/signals/list.test.js.map +0 -1
- package/dist/skill-telemetry.test.d.ts +0 -2
- package/dist/skill-telemetry.test.d.ts.map +0 -1
- package/dist/skill-telemetry.test.js +0 -1484
- package/dist/skill-telemetry.test.js.map +0 -1
- package/dist/sources/get.test.d.ts +0 -5
- package/dist/sources/get.test.d.ts.map +0 -1
- package/dist/sources/get.test.js +0 -236
- package/dist/sources/get.test.js.map +0 -1
- package/dist/sources/list.test.d.ts +0 -8
- package/dist/sources/list.test.d.ts.map +0 -1
- package/dist/sources/list.test.js +0 -318
- package/dist/sources/list.test.js.map +0 -1
- package/dist/sts-credential-refresh.test.d.ts +0 -2
- package/dist/sts-credential-refresh.test.d.ts.map +0 -1
- package/dist/sts-credential-refresh.test.js +0 -181
- package/dist/sts-credential-refresh.test.js.map +0 -1
- package/dist/swallowed-error-sites.test.d.ts +0 -2
- package/dist/swallowed-error-sites.test.d.ts.map +0 -1
- package/dist/swallowed-error-sites.test.js +0 -199
- package/dist/swallowed-error-sites.test.js.map +0 -1
- package/dist/swallowed-error-w32.test.d.ts +0 -2
- package/dist/swallowed-error-w32.test.d.ts.map +0 -1
- package/dist/swallowed-error-w32.test.js +0 -171
- package/dist/swallowed-error-w32.test.js.map +0 -1
- package/dist/swallowed-error-w33.test.d.ts +0 -2
- package/dist/swallowed-error-w33.test.d.ts.map +0 -1
- package/dist/swallowed-error-w33.test.js +0 -125
- package/dist/swallowed-error-w33.test.js.map +0 -1
- package/dist/swallowed-error-w34.test.d.ts +0 -2
- package/dist/swallowed-error-w34.test.d.ts.map +0 -1
- package/dist/swallowed-error-w34.test.js +0 -110
- package/dist/swallowed-error-w34.test.js.map +0 -1
- package/dist/swallowed-error-w35.test.d.ts +0 -2
- package/dist/swallowed-error-w35.test.d.ts.map +0 -1
- package/dist/swallowed-error-w35.test.js +0 -223
- package/dist/swallowed-error-w35.test.js.map +0 -1
- package/dist/sync/area-ledger-auto-migration.test.d.ts +0 -2
- package/dist/sync/area-ledger-auto-migration.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-auto-migration.test.js +0 -291
- package/dist/sync/area-ledger-auto-migration.test.js.map +0 -1
- package/dist/sync/area-ledger-migration.test.d.ts +0 -2
- package/dist/sync/area-ledger-migration.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-migration.test.js +0 -554
- package/dist/sync/area-ledger-migration.test.js.map +0 -1
- package/dist/sync/area-ledger-recovery.bench.test.d.ts +0 -2
- package/dist/sync/area-ledger-recovery.bench.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-recovery.bench.test.js +0 -60
- package/dist/sync/area-ledger-recovery.bench.test.js.map +0 -1
- package/dist/sync/area-ledger-writer-lock.bench.test.d.ts +0 -2
- package/dist/sync/area-ledger-writer-lock.bench.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-writer-lock.bench.test.js +0 -57
- package/dist/sync/area-ledger-writer-lock.bench.test.js.map +0 -1
- package/dist/sync/area-ledger.test.d.ts +0 -2
- package/dist/sync/area-ledger.test.d.ts.map +0 -1
- package/dist/sync/area-ledger.test.js +0 -1401
- package/dist/sync/area-ledger.test.js.map +0 -1
- package/dist/sync/area-resolver.test.d.ts +0 -2
- package/dist/sync/area-resolver.test.d.ts.map +0 -1
- package/dist/sync/area-resolver.test.js +0 -234
- package/dist/sync/area-resolver.test.js.map +0 -1
- package/dist/sync/base-version-resolver.test.d.ts +0 -2
- package/dist/sync/base-version-resolver.test.d.ts.map +0 -1
- package/dist/sync/base-version-resolver.test.js +0 -24
- package/dist/sync/base-version-resolver.test.js.map +0 -1
- package/dist/sync/candidate-uploader.test.d.ts +0 -2
- package/dist/sync/candidate-uploader.test.d.ts.map +0 -1
- package/dist/sync/candidate-uploader.test.js +0 -132
- package/dist/sync/candidate-uploader.test.js.map +0 -1
- package/dist/sync/child-process-sync-worker-env.test.d.ts +0 -2
- package/dist/sync/child-process-sync-worker-env.test.d.ts.map +0 -1
- package/dist/sync/child-process-sync-worker-env.test.js +0 -38
- package/dist/sync/child-process-sync-worker-env.test.js.map +0 -1
- package/dist/sync/child-process-sync-worker-errors.test.d.ts +0 -2
- package/dist/sync/child-process-sync-worker-errors.test.d.ts.map +0 -1
- package/dist/sync/child-process-sync-worker-errors.test.js +0 -121
- package/dist/sync/child-process-sync-worker-errors.test.js.map +0 -1
- package/dist/sync/child-process-sync-worker.test.d.ts +0 -2
- package/dist/sync/child-process-sync-worker.test.d.ts.map +0 -1
- package/dist/sync/child-process-sync-worker.test.js +0 -262
- package/dist/sync/child-process-sync-worker.test.js.map +0 -1
- package/dist/sync/content-hash.test.d.ts +0 -2
- package/dist/sync/content-hash.test.d.ts.map +0 -1
- package/dist/sync/content-hash.test.js +0 -16
- package/dist/sync/content-hash.test.js.map +0 -1
- package/dist/sync/delta-client.test.d.ts +0 -2
- package/dist/sync/delta-client.test.d.ts.map +0 -1
- package/dist/sync/delta-client.test.js +0 -97
- package/dist/sync/delta-client.test.js.map +0 -1
- package/dist/sync/durable-apply.test.d.ts +0 -2
- package/dist/sync/durable-apply.test.d.ts.map +0 -1
- package/dist/sync/durable-apply.test.js +0 -180
- package/dist/sync/durable-apply.test.js.map +0 -1
- package/dist/sync/event-sync.test.d.ts +0 -14
- package/dist/sync/event-sync.test.d.ts.map +0 -1
- package/dist/sync/event-sync.test.js +0 -1714
- package/dist/sync/event-sync.test.js.map +0 -1
- package/dist/sync/feature-flags.test.d.ts +0 -24
- package/dist/sync/feature-flags.test.d.ts.map +0 -1
- package/dist/sync/feature-flags.test.js +0 -594
- package/dist/sync/feature-flags.test.js.map +0 -1
- package/dist/sync/full-pass-cap-governor.bench.test.d.ts +0 -2
- package/dist/sync/full-pass-cap-governor.bench.test.d.ts.map +0 -1
- package/dist/sync/full-pass-cap-governor.bench.test.js +0 -179
- package/dist/sync/full-pass-cap-governor.bench.test.js.map +0 -1
- package/dist/sync/full-pass-caps.test.d.ts +0 -2
- package/dist/sync/full-pass-caps.test.d.ts.map +0 -1
- package/dist/sync/full-pass-caps.test.js +0 -550
- package/dist/sync/full-pass-caps.test.js.map +0 -1
- package/dist/sync/idle-full-reconcile.test.d.ts +0 -2
- package/dist/sync/idle-full-reconcile.test.d.ts.map +0 -1
- package/dist/sync/idle-full-reconcile.test.js +0 -62
- package/dist/sync/idle-full-reconcile.test.js.map +0 -1
- package/dist/sync/inbound-event-admission.test.d.ts +0 -2
- package/dist/sync/inbound-event-admission.test.d.ts.map +0 -1
- package/dist/sync/inbound-event-admission.test.js +0 -125
- package/dist/sync/inbound-event-admission.test.js.map +0 -1
- package/dist/sync/journal-ledger-stream.test.d.ts +0 -12
- package/dist/sync/journal-ledger-stream.test.d.ts.map +0 -1
- package/dist/sync/journal-ledger-stream.test.js +0 -134
- package/dist/sync/journal-ledger-stream.test.js.map +0 -1
- package/dist/sync/lease-client.test.d.ts +0 -2
- package/dist/sync/lease-client.test.d.ts.map +0 -1
- package/dist/sync/lease-client.test.js +0 -119
- package/dist/sync/lease-client.test.js.map +0 -1
- package/dist/sync/logger.test.d.ts +0 -19
- package/dist/sync/logger.test.d.ts.map +0 -1
- package/dist/sync/logger.test.js +0 -204
- package/dist/sync/logger.test.js.map +0 -1
- package/dist/sync/metrics.test.d.ts +0 -19
- package/dist/sync/metrics.test.d.ts.map +0 -1
- package/dist/sync/metrics.test.js +0 -357
- package/dist/sync/metrics.test.js.map +0 -1
- package/dist/sync/multipart-uploader.test.d.ts +0 -2
- package/dist/sync/multipart-uploader.test.d.ts.map +0 -1
- package/dist/sync/multipart-uploader.test.js +0 -135
- package/dist/sync/multipart-uploader.test.js.map +0 -1
- package/dist/sync/mutation-client.test.d.ts +0 -2
- package/dist/sync/mutation-client.test.d.ts.map +0 -1
- package/dist/sync/mutation-client.test.js +0 -100
- package/dist/sync/mutation-client.test.js.map +0 -1
- package/dist/sync/mutation-session.test.d.ts +0 -2
- package/dist/sync/mutation-session.test.d.ts.map +0 -1
- package/dist/sync/mutation-session.test.js +0 -190
- package/dist/sync/mutation-session.test.js.map +0 -1
- package/dist/sync/parallel-sync-coordinator.test.d.ts +0 -2
- package/dist/sync/parallel-sync-coordinator.test.d.ts.map +0 -1
- package/dist/sync/parallel-sync-coordinator.test.js +0 -564
- package/dist/sync/parallel-sync-coordinator.test.js.map +0 -1
- package/dist/sync/path-leases.test.d.ts +0 -2
- package/dist/sync/path-leases.test.d.ts.map +0 -1
- package/dist/sync/path-leases.test.js +0 -51
- package/dist/sync/path-leases.test.js.map +0 -1
- package/dist/sync/per-area-maintenance.test.d.ts +0 -2
- package/dist/sync/per-area-maintenance.test.d.ts.map +0 -1
- package/dist/sync/per-area-maintenance.test.js +0 -100
- package/dist/sync/per-area-maintenance.test.js.map +0 -1
- package/dist/sync/push-event.test.d.ts +0 -15
- package/dist/sync/push-event.test.d.ts.map +0 -1
- package/dist/sync/push-event.test.js +0 -282
- package/dist/sync/push-event.test.js.map +0 -1
- package/dist/sync/push-receiver.test.d.ts +0 -25
- package/dist/sync/push-receiver.test.d.ts.map +0 -1
- package/dist/sync/push-receiver.test.js +0 -2851
- package/dist/sync/push-receiver.test.js.map +0 -1
- package/dist/sync/push-transport.test.d.ts +0 -2
- package/dist/sync/push-transport.test.d.ts.map +0 -1
- package/dist/sync/push-transport.test.js +0 -70
- package/dist/sync/push-transport.test.js.map +0 -1
- package/dist/sync/push-walk-matchers.bench.test.d.ts +0 -2
- package/dist/sync/push-walk-matchers.bench.test.d.ts.map +0 -1
- package/dist/sync/push-walk-matchers.bench.test.js +0 -230
- package/dist/sync/push-walk-matchers.bench.test.js.map +0 -1
- package/dist/sync/realtime-network-integration.test.d.ts +0 -2
- package/dist/sync/realtime-network-integration.test.d.ts.map +0 -1
- package/dist/sync/realtime-network-integration.test.js +0 -83
- package/dist/sync/realtime-network-integration.test.js.map +0 -1
- package/dist/sync/realtime-rollout.test.d.ts +0 -2
- package/dist/sync/realtime-rollout.test.d.ts.map +0 -1
- package/dist/sync/realtime-rollout.test.js +0 -94
- package/dist/sync/realtime-rollout.test.js.map +0 -1
- package/dist/sync/realtime-telemetry.test.d.ts +0 -2
- package/dist/sync/realtime-telemetry.test.d.ts.map +0 -1
- package/dist/sync/realtime-telemetry.test.js +0 -300
- package/dist/sync/realtime-telemetry.test.js.map +0 -1
- package/dist/sync/receiver-origin-config.test.d.ts +0 -2
- package/dist/sync/receiver-origin-config.test.d.ts.map +0 -1
- package/dist/sync/receiver-origin-config.test.js +0 -39
- package/dist/sync/receiver-origin-config.test.js.map +0 -1
- package/dist/sync/reconcile-cursor.test.d.ts +0 -2
- package/dist/sync/reconcile-cursor.test.d.ts.map +0 -1
- package/dist/sync/reconcile-cursor.test.js +0 -139
- package/dist/sync/reconcile-cursor.test.js.map +0 -1
- package/dist/sync/remote-file-spool.test.d.ts +0 -2
- package/dist/sync/remote-file-spool.test.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.test.js +0 -77
- package/dist/sync/remote-file-spool.test.js.map +0 -1
- package/dist/sync/scope-inventory-client.test.d.ts +0 -2
- package/dist/sync/scope-inventory-client.test.d.ts.map +0 -1
- package/dist/sync/scope-inventory-client.test.js +0 -103
- package/dist/sync/scope-inventory-client.test.js.map +0 -1
- package/dist/sync/secret-path-policy.test.d.ts +0 -2
- package/dist/sync/secret-path-policy.test.d.ts.map +0 -1
- package/dist/sync/secret-path-policy.test.js +0 -183
- package/dist/sync/secret-path-policy.test.js.map +0 -1
- package/dist/sync/snapshot-client.test.d.ts +0 -2
- package/dist/sync/snapshot-client.test.d.ts.map +0 -1
- package/dist/sync/snapshot-client.test.js +0 -169
- package/dist/sync/snapshot-client.test.js.map +0 -1
- package/dist/sync/state-store.test.d.ts +0 -2
- package/dist/sync/state-store.test.d.ts.map +0 -1
- package/dist/sync/state-store.test.js +0 -1556
- package/dist/sync/state-store.test.js.map +0 -1
- package/dist/sync/sync-contract.test.d.ts +0 -2
- package/dist/sync/sync-contract.test.d.ts.map +0 -1
- package/dist/sync/sync-contract.test.js +0 -141
- package/dist/sync/sync-contract.test.js.map +0 -1
- package/dist/sync/targeted-pull-prefixes.test.d.ts +0 -2
- package/dist/sync/targeted-pull-prefixes.test.d.ts.map +0 -1
- package/dist/sync/targeted-pull-prefixes.test.js +0 -33
- package/dist/sync/targeted-pull-prefixes.test.js.map +0 -1
- package/dist/sync/targeted-upsert-exact-list.test.d.ts +0 -2
- package/dist/sync/targeted-upsert-exact-list.test.d.ts.map +0 -1
- package/dist/sync/targeted-upsert-exact-list.test.js +0 -247
- package/dist/sync/targeted-upsert-exact-list.test.js.map +0 -1
- package/dist/sync/unchanged-push-events.test.d.ts +0 -2
- package/dist/sync/unchanged-push-events.test.d.ts.map +0 -1
- package/dist/sync/unchanged-push-events.test.js +0 -76
- package/dist/sync/unchanged-push-events.test.js.map +0 -1
- package/dist/sync/uploader-finalization.test.d.ts +0 -2
- package/dist/sync/uploader-finalization.test.d.ts.map +0 -1
- package/dist/sync/uploader-finalization.test.js +0 -107
- package/dist/sync/uploader-finalization.test.js.map +0 -1
- package/dist/sync-core.test.d.ts +0 -2
- package/dist/sync-core.test.d.ts.map +0 -1
- package/dist/sync-core.test.js +0 -189
- package/dist/sync-core.test.js.map +0 -1
- package/dist/sync-progress.test.d.ts +0 -2
- package/dist/sync-progress.test.d.ts.map +0 -1
- package/dist/sync-progress.test.js +0 -277
- package/dist/sync-progress.test.js.map +0 -1
- package/dist/telemetry-events.test.d.ts +0 -2
- package/dist/telemetry-events.test.d.ts.map +0 -1
- package/dist/telemetry-events.test.js +0 -113
- package/dist/telemetry-events.test.js.map +0 -1
- package/dist/telemetry.test.d.ts +0 -11
- package/dist/telemetry.test.d.ts.map +0 -1
- package/dist/telemetry.test.js +0 -1603
- package/dist/telemetry.test.js.map +0 -1
- package/dist/vault-client.test.d.ts +0 -7
- package/dist/vault-client.test.d.ts.map +0 -1
- package/dist/vault-client.test.js +0 -1334
- package/dist/vault-client.test.js.map +0 -1
- package/dist/watch-coverage.test.d.ts +0 -2
- package/dist/watch-coverage.test.d.ts.map +0 -1
- package/dist/watch-coverage.test.js +0 -56
- package/dist/watch-coverage.test.js.map +0 -1
- package/dist/watch-owner-lease.test.d.ts +0 -2
- package/dist/watch-owner-lease.test.d.ts.map +0 -1
- package/dist/watch-owner-lease.test.js +0 -353
- package/dist/watch-owner-lease.test.js.map +0 -1
- package/dist/watch-parent.test.d.ts +0 -2
- package/dist/watch-parent.test.d.ts.map +0 -1
- package/dist/watch-parent.test.js +0 -131
- package/dist/watch-parent.test.js.map +0 -1
- package/dist/watch-roots.test.d.ts +0 -18
- package/dist/watch-roots.test.d.ts.map +0 -1
- package/dist/watch-roots.test.js +0 -395
- package/dist/watch-roots.test.js.map +0 -1
- package/dist/watcher-continuity-hot-path.test.d.ts +0 -2
- package/dist/watcher-continuity-hot-path.test.d.ts.map +0 -1
- package/dist/watcher-continuity-hot-path.test.js +0 -40
- package/dist/watcher-continuity-hot-path.test.js.map +0 -1
- package/dist/watcher-event-gate.test.d.ts +0 -16
- package/dist/watcher-event-gate.test.d.ts.map +0 -1
- package/dist/watcher-event-gate.test.js +0 -191
- package/dist/watcher-event-gate.test.js.map +0 -1
- package/dist/watcher-replan-sweep.test.d.ts +0 -2
- package/dist/watcher-replan-sweep.test.d.ts.map +0 -1
- package/dist/watcher-replan-sweep.test.js +0 -245
- package/dist/watcher-replan-sweep.test.js.map +0 -1
- package/dist/watcher-stdout.test.d.ts +0 -2
- package/dist/watcher-stdout.test.d.ts.map +0 -1
- package/dist/watcher-stdout.test.js +0 -86
- package/dist/watcher-stdout.test.js.map +0 -1
- package/dist/watcher.test.d.ts +0 -2
- package/dist/watcher.test.d.ts.map +0 -1
- package/dist/watcher.test.js +0 -4301
- package/dist/watcher.test.js.map +0 -1
- package/dist/watcher.warmup-cancel.test.d.ts +0 -2
- package/dist/watcher.warmup-cancel.test.d.ts.map +0 -1
- package/dist/watcher.warmup-cancel.test.js +0 -59
- package/dist/watcher.warmup-cancel.test.js.map +0 -1
- package/dist/watcher.warmup-responsive.test.d.ts +0 -2
- package/dist/watcher.warmup-responsive.test.d.ts.map +0 -1
- package/dist/watcher.warmup-responsive.test.js +0 -62
- package/dist/watcher.warmup-responsive.test.js.map +0 -1
package/dist/object-io.test.js
DELETED
|
@@ -1,2376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the ObjectIO transport seam.
|
|
3
|
-
*
|
|
4
|
-
* Focus is the PresignObjectIO path (the new transport): it talks to a
|
|
5
|
-
* VaultClient-shaped stub for URL minting and a mocked global `fetch` for the
|
|
6
|
-
* byte movement. The S3SdkObjectIO path is already covered transitively by
|
|
7
|
-
* s3.test.ts (which exercises s3.ts through the default factory), plus a couple
|
|
8
|
-
* of factory-selection tests here.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
import * as http from "node:http";
|
|
13
|
-
import { PassThrough } from "node:stream";
|
|
14
|
-
import { once } from "node:events";
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
|
-
import { PresignObjectIO, S3SdkObjectIO, setObjectIOFactory, setObjectIOBodyIdleTimeoutMsForTesting, setObjectIORequestTimeoutMsForTesting, setPresignedGetTransportForTesting, setFilesAdmissionTimeoutMsForTesting, resolveObjectIO, presignObjectIOFactory, OBJECT_IO_REQUEST_TIMEOUT_MS, objectIORequestTimeoutMs, ObjectBodyIdleTimeoutError, ObjectLockChecksumRequiredError, } from "./object-io.js";
|
|
17
|
-
import * as objectIoModule from "./object-io.js";
|
|
18
|
-
import { isTransientNetworkError } from "./lib/net-errors.js";
|
|
19
|
-
import { BandwidthGovernor, setBandwidthGovernorForTesting } from "./bandwidth.js";
|
|
20
|
-
import { contentMd5Base64 } from "./lib/s3-content-checksum.js";
|
|
21
|
-
import { vaultErrorFromResponse } from "./vault-client.js";
|
|
22
|
-
import { PLAN_LIMIT_UPGRADE_URL, admissionAllowedBody, admissionUnsupportedBody, canonicalPlanLimitBody, companyPresignRefusalRow, } from "./plan-limit.fixtures.js";
|
|
23
|
-
import { isAccessDenied, isNotFound } from "./sync-core.js";
|
|
24
|
-
const COMPANY = "cmp_test";
|
|
25
|
-
function fetchBackedPresignedGetTransport() {
|
|
26
|
-
return {
|
|
27
|
-
async get(url, headers) {
|
|
28
|
-
const response = await fetch(url, { method: "GET", headers });
|
|
29
|
-
const body = response.body;
|
|
30
|
-
return {
|
|
31
|
-
status: response.status,
|
|
32
|
-
headers: response.headers,
|
|
33
|
-
body: body
|
|
34
|
-
? (async function* () {
|
|
35
|
-
const reader = body.getReader();
|
|
36
|
-
let exhausted = false;
|
|
37
|
-
try {
|
|
38
|
-
while (true) {
|
|
39
|
-
const { done, value } = await reader.read();
|
|
40
|
-
if (done) {
|
|
41
|
-
exhausted = true;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (value.byteLength > 0)
|
|
45
|
-
yield value;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
finally {
|
|
49
|
-
if (!exhausted) {
|
|
50
|
-
try {
|
|
51
|
-
await reader.cancel();
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// The response may already have been cancelled by destroy.
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
reader.releaseLock();
|
|
58
|
-
}
|
|
59
|
-
})()
|
|
60
|
-
: undefined,
|
|
61
|
-
destroy: () => {
|
|
62
|
-
void body?.cancel().catch(() => undefined);
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
setPresignedGetTransportForTesting(fetchBackedPresignedGetTransport());
|
|
70
|
-
});
|
|
71
|
-
afterEach(() => {
|
|
72
|
-
setPresignedGetTransportForTesting(null);
|
|
73
|
-
});
|
|
74
|
-
describe("realtime ObjectIO payload priority", () => {
|
|
75
|
-
afterEach(() => {
|
|
76
|
-
setBandwidthGovernorForTesting(null);
|
|
77
|
-
vi.unstubAllGlobals();
|
|
78
|
-
});
|
|
79
|
-
it("bypasses SDK GET and PUT pacing while retaining ordinary chore pacing", async () => {
|
|
80
|
-
const governor = new BandwidthGovernor({ maxBytesPerSec: 1 });
|
|
81
|
-
const acquire = vi.spyOn(governor, "acquire").mockResolvedValue();
|
|
82
|
-
setBandwidthGovernorForTesting(governor);
|
|
83
|
-
const body = (async function* () {
|
|
84
|
-
yield Buffer.from("payload");
|
|
85
|
-
})();
|
|
86
|
-
const io = new S3SdkObjectIO(ctx());
|
|
87
|
-
io.client = {
|
|
88
|
-
send: async (command) => command.constructor.name === "GetObjectCommand"
|
|
89
|
-
? { Body: body, Metadata: {} }
|
|
90
|
-
: { ETag: '"uploaded"' },
|
|
91
|
-
};
|
|
92
|
-
await io.putObject({
|
|
93
|
-
key: "realtime-put",
|
|
94
|
-
body: Buffer.from("realtime"),
|
|
95
|
-
contentType: "text/plain",
|
|
96
|
-
bypassBandwidthGovernor: true,
|
|
97
|
-
});
|
|
98
|
-
const realtimeGet = await io.getObjectStream("realtime-get", {
|
|
99
|
-
bypassBandwidthGovernor: true,
|
|
100
|
-
});
|
|
101
|
-
await realtimeGet.body[Symbol.asyncIterator]().next();
|
|
102
|
-
expect(acquire).not.toHaveBeenCalled();
|
|
103
|
-
await io.putObject({
|
|
104
|
-
key: "chore-put",
|
|
105
|
-
body: Buffer.from("chore"),
|
|
106
|
-
contentType: "text/plain",
|
|
107
|
-
});
|
|
108
|
-
expect(acquire).toHaveBeenCalledOnce();
|
|
109
|
-
});
|
|
110
|
-
it("bypasses presigned GET and PUT pacing for realtime transfers", async () => {
|
|
111
|
-
const governor = new BandwidthGovernor({ maxBytesPerSec: 1 });
|
|
112
|
-
const acquire = vi.spyOn(governor, "acquire").mockResolvedValue();
|
|
113
|
-
setBandwidthGovernorForTesting(governor);
|
|
114
|
-
const { vault, setPresign } = makeVault();
|
|
115
|
-
const fetchMock = vi.fn();
|
|
116
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
117
|
-
setPresign([
|
|
118
|
-
{ key: "realtime", op: "put", url: "https://s3/realtime-put" },
|
|
119
|
-
{ key: "realtime", op: "get", url: "https://s3/realtime-get" },
|
|
120
|
-
{ key: "chore", op: "put", url: "https://s3/chore-put" },
|
|
121
|
-
]);
|
|
122
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"ok"' } }));
|
|
123
|
-
setPresignedGetTransportForTesting({
|
|
124
|
-
get: async () => ({
|
|
125
|
-
status: 200,
|
|
126
|
-
headers: new Headers(),
|
|
127
|
-
body: (async function* () { yield Buffer.from("payload"); })(),
|
|
128
|
-
destroy: () => undefined,
|
|
129
|
-
}),
|
|
130
|
-
});
|
|
131
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
132
|
-
await io.putObject({
|
|
133
|
-
key: "realtime",
|
|
134
|
-
body: Buffer.from("realtime"),
|
|
135
|
-
contentType: "text/plain",
|
|
136
|
-
bypassBandwidthGovernor: true,
|
|
137
|
-
});
|
|
138
|
-
const realtimeGet = await io.getObjectStream("realtime", {
|
|
139
|
-
bypassBandwidthGovernor: true,
|
|
140
|
-
});
|
|
141
|
-
await realtimeGet.body[Symbol.asyncIterator]().next();
|
|
142
|
-
expect(acquire).not.toHaveBeenCalled();
|
|
143
|
-
await io.putObject({
|
|
144
|
-
key: "chore",
|
|
145
|
-
body: Buffer.from("chore"),
|
|
146
|
-
contentType: "text/plain",
|
|
147
|
-
});
|
|
148
|
-
expect(acquire).toHaveBeenCalledOnce();
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe("ObjectIO payload-sized request timeout", () => {
|
|
152
|
-
it("extends large PUT deadlines while keeping the small-object base unchanged", () => {
|
|
153
|
-
expect(objectIORequestTimeoutMs()).toBe(OBJECT_IO_REQUEST_TIMEOUT_MS);
|
|
154
|
-
expect(objectIORequestTimeoutMs(8 * 1024 * 1024)).toBe(124_000);
|
|
155
|
-
expect(objectIORequestTimeoutMs(48_800_000)).toBe(436_000);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe("ObjectIO idle timeout and bandwidth pacing", () => {
|
|
159
|
-
afterEach(() => {
|
|
160
|
-
setBandwidthGovernorForTesting(null);
|
|
161
|
-
setObjectIOBodyIdleTimeoutMsForTesting(undefined);
|
|
162
|
-
});
|
|
163
|
-
it("does not count governor wait time as socket idle time", async () => {
|
|
164
|
-
setObjectIOBodyIdleTimeoutMsForTesting(30);
|
|
165
|
-
const governor = new BandwidthGovernor({ maxBytesPerSec: 10 }, {
|
|
166
|
-
sleep: async (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
167
|
-
});
|
|
168
|
-
setBandwidthGovernorForTesting(governor);
|
|
169
|
-
const body = (async function* () {
|
|
170
|
-
yield Buffer.alloc(1);
|
|
171
|
-
})();
|
|
172
|
-
const io = new S3SdkObjectIO(ctx());
|
|
173
|
-
io.client = {
|
|
174
|
-
send: async () => ({ Body: body, Metadata: {} }),
|
|
175
|
-
};
|
|
176
|
-
const response = await io.getObjectStream("docs/paced.md");
|
|
177
|
-
const chunks = [];
|
|
178
|
-
for await (const chunk of response.body)
|
|
179
|
-
chunks.push(chunk);
|
|
180
|
-
expect(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))).toEqual(Buffer.alloc(1));
|
|
181
|
-
}, 5_000);
|
|
182
|
-
});
|
|
183
|
-
describe("ObjectIO adaptive bandwidth sampling", () => {
|
|
184
|
-
afterEach(() => {
|
|
185
|
-
setBandwidthGovernorForTesting(null);
|
|
186
|
-
vi.unstubAllGlobals();
|
|
187
|
-
});
|
|
188
|
-
for (const transport of ["S3 SDK", "presigned"]) {
|
|
189
|
-
it(`learns raw upload capacity from an upload-only ${transport} run`, async () => {
|
|
190
|
-
let now = 0;
|
|
191
|
-
const governor = new BandwidthGovernor({ percent: 50 }, { now: () => now, sleep: async (ms) => { now += ms; } });
|
|
192
|
-
setBandwidthGovernorForTesting(governor);
|
|
193
|
-
if (transport === "S3 SDK") {
|
|
194
|
-
const io = new S3SdkObjectIO(ctx());
|
|
195
|
-
io.client = {
|
|
196
|
-
send: async () => {
|
|
197
|
-
now += 1000;
|
|
198
|
-
return { ETag: '"uploaded"' };
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
await io.putObject({
|
|
202
|
-
key: "upload-only.bin",
|
|
203
|
-
body: Buffer.alloc(1_000_000),
|
|
204
|
-
contentType: "application/octet-stream",
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
const { vault, setPresign } = makeVault();
|
|
209
|
-
setPresign([{ key: "upload-only.bin", op: "put", url: "https://s3/upload-only" }]);
|
|
210
|
-
const fetchMock = vi.fn(async () => {
|
|
211
|
-
now += 1000;
|
|
212
|
-
return new Response(null, { status: 200, headers: { etag: '"uploaded"' } });
|
|
213
|
-
});
|
|
214
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
215
|
-
await new PresignObjectIO(vault, COMPANY).putObject({
|
|
216
|
-
key: "upload-only.bin",
|
|
217
|
-
body: Buffer.alloc(1_000_000),
|
|
218
|
-
contentType: "application/octet-stream",
|
|
219
|
-
});
|
|
220
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
221
|
-
}
|
|
222
|
-
expect(governor.effectiveRateBps()).toBe(500_000);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
it("samples transport arrivals before buffered GET chunks are pulled", async () => {
|
|
226
|
-
let now = 1000;
|
|
227
|
-
const governor = new BandwidthGovernor({ percent: 60 }, { now: () => now, sleep: async (ms) => { now += ms; } });
|
|
228
|
-
setBandwidthGovernorForTesting(governor);
|
|
229
|
-
const body = new PassThrough();
|
|
230
|
-
const io = new S3SdkObjectIO(ctx());
|
|
231
|
-
io.client = {
|
|
232
|
-
send: async () => ({ Body: body, Metadata: {} }),
|
|
233
|
-
};
|
|
234
|
-
const response = await io.getObjectStream("buffered.bin");
|
|
235
|
-
body.write(Buffer.alloc(4096));
|
|
236
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
237
|
-
now += 40;
|
|
238
|
-
body.write(Buffer.alloc(4096));
|
|
239
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
240
|
-
now += 40;
|
|
241
|
-
body.end(Buffer.alloc(4096));
|
|
242
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
243
|
-
expect(governor.effectiveRateBps()).toBeGreaterThanOrEqual(60_000);
|
|
244
|
-
expect(governor.effectiveRateBps()).toBeLessThanOrEqual(70_000);
|
|
245
|
-
let received = 0;
|
|
246
|
-
for await (const chunk of response.body)
|
|
247
|
-
received += chunk.byteLength;
|
|
248
|
-
expect(received).toBe(12_288);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
describe("ObjectIO chore transfer admission", () => {
|
|
252
|
-
afterEach(() => {
|
|
253
|
-
setBandwidthGovernorForTesting(null);
|
|
254
|
-
});
|
|
255
|
-
it("waits for a download slot before opening the next SDK GET", async () => {
|
|
256
|
-
setBandwidthGovernorForTesting(new BandwidthGovernor({ maxConcurrency: 1 }));
|
|
257
|
-
const opened = [];
|
|
258
|
-
const io = new S3SdkObjectIO(ctx());
|
|
259
|
-
io.client = {
|
|
260
|
-
send: async (command) => {
|
|
261
|
-
opened.push(command.input.Key);
|
|
262
|
-
return {
|
|
263
|
-
Body: (async function* () { yield Buffer.from(command.input.Key); })(),
|
|
264
|
-
Metadata: {},
|
|
265
|
-
};
|
|
266
|
-
},
|
|
267
|
-
};
|
|
268
|
-
const first = await io.getObjectStream("first.bin");
|
|
269
|
-
const secondPromise = io.getObjectStream("second.bin");
|
|
270
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
271
|
-
expect(opened).toEqual(["first.bin"]);
|
|
272
|
-
for await (const _chunk of first.body) { /* drain the first transfer */ }
|
|
273
|
-
const second = await secondPromise;
|
|
274
|
-
expect(opened).toEqual(["first.bin", "second.bin"]);
|
|
275
|
-
for await (const _chunk of second.body) { /* drain the second transfer */ }
|
|
276
|
-
});
|
|
277
|
-
it("waits for a download slot before opening the next presigned GET", async () => {
|
|
278
|
-
setBandwidthGovernorForTesting(new BandwidthGovernor({ maxConcurrency: 1 }));
|
|
279
|
-
const { vault, setPresign } = makeVault();
|
|
280
|
-
setPresign([
|
|
281
|
-
{ key: "first.bin", op: "get", url: "https://s3/first" },
|
|
282
|
-
{ key: "second.bin", op: "get", url: "https://s3/second" },
|
|
283
|
-
]);
|
|
284
|
-
const opened = [];
|
|
285
|
-
setPresignedGetTransportForTesting({
|
|
286
|
-
get: async (url) => {
|
|
287
|
-
opened.push(url);
|
|
288
|
-
return {
|
|
289
|
-
status: 200,
|
|
290
|
-
headers: new Headers(),
|
|
291
|
-
body: (async function* () { yield Buffer.from(url); })(),
|
|
292
|
-
destroy: () => undefined,
|
|
293
|
-
};
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
297
|
-
const first = await io.getObjectStream("first.bin");
|
|
298
|
-
const secondPromise = io.getObjectStream("second.bin");
|
|
299
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
300
|
-
expect(opened).toEqual(["https://s3/first"]);
|
|
301
|
-
for await (const _chunk of first.body) { /* drain the first transfer */ }
|
|
302
|
-
const second = await secondPromise;
|
|
303
|
-
expect(opened).toHaveLength(2);
|
|
304
|
-
for await (const _chunk of second.body) { /* drain the second transfer */ }
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
describe("ObjectIO ranged GET validators", () => {
|
|
308
|
-
afterEach(() => {
|
|
309
|
-
setObjectIOBodyIdleTimeoutMsForTesting(undefined);
|
|
310
|
-
setBandwidthGovernorForTesting(null);
|
|
311
|
-
});
|
|
312
|
-
it("sends Range and If-Match through the S3 SDK and exposes response identity", async () => {
|
|
313
|
-
const sent = [];
|
|
314
|
-
const body = (async function* () {
|
|
315
|
-
yield Buffer.from("suffix");
|
|
316
|
-
})();
|
|
317
|
-
const io = new S3SdkObjectIO(ctx());
|
|
318
|
-
io.client = {
|
|
319
|
-
send: async (command) => {
|
|
320
|
-
sent.push(command.input);
|
|
321
|
-
return {
|
|
322
|
-
Body: body,
|
|
323
|
-
Metadata: {},
|
|
324
|
-
ETag: '"etag-a"',
|
|
325
|
-
VersionId: "version-a",
|
|
326
|
-
ContentLength: 6,
|
|
327
|
-
ContentRange: "bytes 20-25/26",
|
|
328
|
-
$metadata: { httpStatusCode: 206 },
|
|
329
|
-
};
|
|
330
|
-
},
|
|
331
|
-
};
|
|
332
|
-
const streamed = await io.getObjectStream("resume.bin", {
|
|
333
|
-
rangeStart: 20,
|
|
334
|
-
ifMatch: '"etag-a"',
|
|
335
|
-
});
|
|
336
|
-
const identity = streamed;
|
|
337
|
-
expect(sent[0]).toMatchObject({ Range: "bytes=20-", IfMatch: '"etag-a"' });
|
|
338
|
-
expect(identity).toMatchObject({
|
|
339
|
-
etag: '"etag-a"',
|
|
340
|
-
versionId: "version-a",
|
|
341
|
-
contentLength: 6,
|
|
342
|
-
contentRange: "bytes 20-25/26",
|
|
343
|
-
statusCode: 206,
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
it("sends Range and If-Match through presigned GET and exposes response identity", async () => {
|
|
347
|
-
const { vault, setPresign } = makeVault();
|
|
348
|
-
setPresign([{ key: "resume.bin", op: "get", url: "https://s3/resume" }]);
|
|
349
|
-
const get = vi.fn(async () => ({
|
|
350
|
-
status: 206,
|
|
351
|
-
headers: new Headers({
|
|
352
|
-
etag: '"etag-a"',
|
|
353
|
-
"x-amz-version-id": "version-a",
|
|
354
|
-
"content-length": "6",
|
|
355
|
-
"content-range": "bytes 20-25/26",
|
|
356
|
-
}),
|
|
357
|
-
body: (async function* () { yield Buffer.from("suffix"); })(),
|
|
358
|
-
destroy: vi.fn(),
|
|
359
|
-
}));
|
|
360
|
-
setPresignedGetTransportForTesting({ get });
|
|
361
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
362
|
-
const streamed = await io.getObjectStream("resume.bin", {
|
|
363
|
-
rangeStart: 20,
|
|
364
|
-
ifMatch: '"etag-a"',
|
|
365
|
-
});
|
|
366
|
-
const identity = streamed;
|
|
367
|
-
expect(get).toHaveBeenCalledWith("https://s3/resume", expect.objectContaining({ Range: "bytes=20-", "If-Match": '"etag-a"' }));
|
|
368
|
-
expect(identity).toMatchObject({
|
|
369
|
-
etag: '"etag-a"',
|
|
370
|
-
versionId: "version-a",
|
|
371
|
-
contentLength: 6,
|
|
372
|
-
contentRange: "bytes 20-25/26",
|
|
373
|
-
statusCode: 206,
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
it("reports a ranged presigned 412 as a precondition failure", async () => {
|
|
377
|
-
const { vault, setPresign } = makeVault();
|
|
378
|
-
setPresign([{ key: "resume.bin", op: "get", url: "https://s3/resume" }]);
|
|
379
|
-
const destroy = vi.fn();
|
|
380
|
-
setPresignedGetTransportForTesting({
|
|
381
|
-
get: async () => ({
|
|
382
|
-
status: 412,
|
|
383
|
-
headers: new Headers(),
|
|
384
|
-
body: (async function* () { yield Buffer.from("stale etag"); })(),
|
|
385
|
-
destroy,
|
|
386
|
-
}),
|
|
387
|
-
});
|
|
388
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
389
|
-
await expect(io.getObjectStream("resume.bin", {
|
|
390
|
-
rangeStart: 20,
|
|
391
|
-
ifMatch: '"etag-a"',
|
|
392
|
-
})).rejects.toMatchObject({ name: "PreconditionFailed" });
|
|
393
|
-
expect(destroy).toHaveBeenCalledOnce();
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
function ctx() {
|
|
397
|
-
return {
|
|
398
|
-
uid: COMPANY,
|
|
399
|
-
slug: "test",
|
|
400
|
-
bucketName: "bucket-test",
|
|
401
|
-
region: "us-east-1",
|
|
402
|
-
credentials: {
|
|
403
|
-
accessKeyId: "AKIA",
|
|
404
|
-
secretAccessKey: "secret",
|
|
405
|
-
sessionToken: "token",
|
|
406
|
-
},
|
|
407
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Minimal VaultClient stub. Each method returns whatever the per-test config
|
|
412
|
-
* sets; presign records the inputs it was called with so tests can assert the
|
|
413
|
-
* op/metadata/key shape the seam sends.
|
|
414
|
-
*/
|
|
415
|
-
function makeVault() {
|
|
416
|
-
const presignCalls = [];
|
|
417
|
-
let nextPresign = [];
|
|
418
|
-
let nextList = { objects: [], cursor: null, truncated: false };
|
|
419
|
-
const vault = {
|
|
420
|
-
presign: async (input) => {
|
|
421
|
-
presignCalls.push({
|
|
422
|
-
companyUid: input.companyUid,
|
|
423
|
-
op: input.op,
|
|
424
|
-
keys: input.keys,
|
|
425
|
-
});
|
|
426
|
-
return { results: nextPresign, expiresAt: "2099-01-01T00:00:00.000Z" };
|
|
427
|
-
},
|
|
428
|
-
listFiles: async () => nextList,
|
|
429
|
-
};
|
|
430
|
-
return {
|
|
431
|
-
vault,
|
|
432
|
-
presignCalls,
|
|
433
|
-
setPresign: (rows) => {
|
|
434
|
-
nextPresign = rows;
|
|
435
|
-
},
|
|
436
|
-
setList: (v) => {
|
|
437
|
-
nextList = v;
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
function makeForbiddenPrefixVault(pages) {
|
|
442
|
-
const calls = [];
|
|
443
|
-
const forbiddenCharacters = new Set(["?", "#", "\\", "[", "]"]);
|
|
444
|
-
const vault = {
|
|
445
|
-
presign: async () => ({ results: [], expiresAt: "2099-01-01T00:00:00.000Z" }),
|
|
446
|
-
listFiles: async (companyUid, prefix, cursor) => {
|
|
447
|
-
calls.push({ companyUid, prefix, cursor });
|
|
448
|
-
if (prefix && [...prefix].some((character) => forbiddenCharacters.has(character))) {
|
|
449
|
-
throw Object.assign(new Error("Forbidden prefix characters"), {
|
|
450
|
-
name: "VaultClientError",
|
|
451
|
-
code: "INVALID_PREFIX_FORBIDDEN_CHARS",
|
|
452
|
-
statusCode: 400,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
return pages.get(cursor) ?? { objects: [], cursor: null, truncated: false };
|
|
456
|
-
},
|
|
457
|
-
};
|
|
458
|
-
return { vault, calls };
|
|
459
|
-
}
|
|
460
|
-
function vaultObject(key) {
|
|
461
|
-
return {
|
|
462
|
-
key,
|
|
463
|
-
size: 1,
|
|
464
|
-
lastModified: "2026-01-01T00:00:00.000Z",
|
|
465
|
-
etag: "etag",
|
|
466
|
-
permission: "read",
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function safeVaultListPrefixAncestor(prefix) {
|
|
470
|
-
const helper = objectIoModule.getSafeVaultListPrefixAncestor;
|
|
471
|
-
if (!helper)
|
|
472
|
-
throw new Error("getSafeVaultListPrefixAncestor is not exported");
|
|
473
|
-
return helper(prefix);
|
|
474
|
-
}
|
|
475
|
-
describe("factory selection", () => {
|
|
476
|
-
afterEach(() => setObjectIOFactory(null));
|
|
477
|
-
it("defaults to the S3 SDK transport", () => {
|
|
478
|
-
const io = resolveObjectIO(ctx());
|
|
479
|
-
expect(io).toBeInstanceOf(S3SdkObjectIO);
|
|
480
|
-
});
|
|
481
|
-
it("setObjectIOFactory(null) resets to the default", () => {
|
|
482
|
-
const { vault } = makeVault();
|
|
483
|
-
setObjectIOFactory(presignObjectIOFactory(vault));
|
|
484
|
-
expect(resolveObjectIO(ctx())).toBeInstanceOf(PresignObjectIO);
|
|
485
|
-
setObjectIOFactory(null);
|
|
486
|
-
expect(resolveObjectIO(ctx())).toBeInstanceOf(S3SdkObjectIO);
|
|
487
|
-
});
|
|
488
|
-
it("presign factory derives companyUid from ctx.uid", async () => {
|
|
489
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
490
|
-
setPresign([{ key: "k", op: "get", url: "https://x/get" }]);
|
|
491
|
-
vi.stubGlobal("fetch", vi.fn(async () => new Response(Buffer.from("body"), { status: 200 })));
|
|
492
|
-
const io = presignObjectIOFactory(vault)(ctx());
|
|
493
|
-
await io.getObject("k");
|
|
494
|
-
expect(presignCalls[0].companyUid).toBe(COMPANY);
|
|
495
|
-
vi.unstubAllGlobals();
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
describe("S3SdkObjectIO request timeout", () => {
|
|
499
|
-
afterEach(() => {
|
|
500
|
-
setObjectIORequestTimeoutMsForTesting(undefined);
|
|
501
|
-
setObjectIOBodyIdleTimeoutMsForTesting(undefined);
|
|
502
|
-
});
|
|
503
|
-
it("aborts a hung ListObjectsV2 within OBJECT_IO_REQUEST_TIMEOUT_MS as transient", async () => {
|
|
504
|
-
expect(OBJECT_IO_REQUEST_TIMEOUT_MS).toBe(60_000);
|
|
505
|
-
setObjectIORequestTimeoutMsForTesting(50);
|
|
506
|
-
const io = new S3SdkObjectIO(ctx());
|
|
507
|
-
io.client = {
|
|
508
|
-
send: (_cmd, opts) => new Promise((_resolve, reject) => {
|
|
509
|
-
const signal = opts?.abortSignal;
|
|
510
|
-
const fail = () => {
|
|
511
|
-
const reason = signal?.reason;
|
|
512
|
-
const err = Object.assign(new Error("This operation was aborted"), {
|
|
513
|
-
name: reason instanceof Error && typeof reason.name === "string"
|
|
514
|
-
? reason.name
|
|
515
|
-
: "AbortError",
|
|
516
|
-
});
|
|
517
|
-
reject(err);
|
|
518
|
-
};
|
|
519
|
-
if (!signal)
|
|
520
|
-
return;
|
|
521
|
-
if (signal.aborted) {
|
|
522
|
-
fail();
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
signal.addEventListener("abort", fail, { once: true });
|
|
526
|
-
}),
|
|
527
|
-
};
|
|
528
|
-
const started = Date.now();
|
|
529
|
-
let caught;
|
|
530
|
-
try {
|
|
531
|
-
await io.listObjects({});
|
|
532
|
-
}
|
|
533
|
-
catch (err) {
|
|
534
|
-
caught = err;
|
|
535
|
-
}
|
|
536
|
-
expect(caught).toBeInstanceOf(Error);
|
|
537
|
-
expect(isTransientNetworkError(caught)).toBe(true);
|
|
538
|
-
expect(Date.now() - started).toBeLessThan(1_000);
|
|
539
|
-
}, 5_000);
|
|
540
|
-
it("cancels a GET body that stops yielding after its headers arrive", async () => {
|
|
541
|
-
setObjectIOBodyIdleTimeoutMsForTesting(40);
|
|
542
|
-
let cancelled = false;
|
|
543
|
-
const body = {
|
|
544
|
-
[Symbol.asyncIterator]: () => ({
|
|
545
|
-
next: () => new Promise(() => { }),
|
|
546
|
-
return: async () => ({ done: true, value: undefined }),
|
|
547
|
-
}),
|
|
548
|
-
destroy: () => { cancelled = true; },
|
|
549
|
-
};
|
|
550
|
-
const io = new S3SdkObjectIO(ctx());
|
|
551
|
-
io.client = {
|
|
552
|
-
send: async () => ({ Body: body, Metadata: {} }),
|
|
553
|
-
};
|
|
554
|
-
const response = await io.getObjectStream("docs/stalled.md");
|
|
555
|
-
const started = Date.now();
|
|
556
|
-
let caught;
|
|
557
|
-
try {
|
|
558
|
-
await response.body[Symbol.asyncIterator]().next();
|
|
559
|
-
}
|
|
560
|
-
catch (error) {
|
|
561
|
-
caught = error;
|
|
562
|
-
}
|
|
563
|
-
expect(caught).toBeInstanceOf(ObjectBodyIdleTimeoutError);
|
|
564
|
-
expect(cancelled).toBe(true);
|
|
565
|
-
expect(Date.now() - started).toBeLessThan(1_000);
|
|
566
|
-
}, 5_000);
|
|
567
|
-
it("cancels a stalled presigned GET body", async () => {
|
|
568
|
-
setObjectIOBodyIdleTimeoutMsForTesting(40);
|
|
569
|
-
const { vault, setPresign } = makeVault();
|
|
570
|
-
setPresign([{ key: "docs/stalled.md", op: "get", url: "https://example.test/stalled" }]);
|
|
571
|
-
let destroyed = false;
|
|
572
|
-
const body = {
|
|
573
|
-
[Symbol.asyncIterator]: () => ({
|
|
574
|
-
next: () => new Promise(() => { }),
|
|
575
|
-
return: async () => ({ done: true, value: undefined }),
|
|
576
|
-
}),
|
|
577
|
-
};
|
|
578
|
-
setPresignedGetTransportForTesting({
|
|
579
|
-
get: async () => ({
|
|
580
|
-
status: 200,
|
|
581
|
-
headers: new Headers(),
|
|
582
|
-
body,
|
|
583
|
-
destroy: () => { destroyed = true; },
|
|
584
|
-
}),
|
|
585
|
-
});
|
|
586
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
587
|
-
const response = await io.getObjectStream("docs/stalled.md");
|
|
588
|
-
let caught;
|
|
589
|
-
try {
|
|
590
|
-
await response.body[Symbol.asyncIterator]().next();
|
|
591
|
-
}
|
|
592
|
-
catch (error) {
|
|
593
|
-
caught = error;
|
|
594
|
-
}
|
|
595
|
-
expect(caught).toBeInstanceOf(ObjectBodyIdleTimeoutError);
|
|
596
|
-
expect(destroyed).toBe(true);
|
|
597
|
-
}, 5_000);
|
|
598
|
-
});
|
|
599
|
-
describe("S3SdkObjectIO.putObject — conditional-write fence", () => {
|
|
600
|
-
it("maps ifMatch (re-quoted) and ifNoneMatch onto the PutObjectCommand", async () => {
|
|
601
|
-
const io = new S3SdkObjectIO(ctx());
|
|
602
|
-
const sent = [];
|
|
603
|
-
// Swap the private client for a recorder — the fence mapping is pure
|
|
604
|
-
// input-shaping, no wire needed.
|
|
605
|
-
io.client = {
|
|
606
|
-
send: async (cmd) => {
|
|
607
|
-
sent.push(cmd.input);
|
|
608
|
-
return { ETag: '"landed"' };
|
|
609
|
-
},
|
|
610
|
-
};
|
|
611
|
-
await io.putObject({ key: "a", body: Buffer.from("x"), contentType: "t", ifMatch: "abc" });
|
|
612
|
-
expect(sent[0].IfMatch).toBe('"abc"'); // stripped etags re-quoted for If-Match
|
|
613
|
-
await io.putObject({ key: "a2", body: Buffer.from("x"), contentType: "t", ifMatch: '"quoted"' });
|
|
614
|
-
expect(sent[1].IfMatch).toBe('"quoted"'); // already-quoted passes through
|
|
615
|
-
await io.putObject({ key: "b", body: Buffer.from("y"), contentType: "t", ifNoneMatch: "*" });
|
|
616
|
-
expect(sent[2].IfNoneMatch).toBe("*");
|
|
617
|
-
await io.putObject({ key: "c", body: Buffer.from("z"), contentType: "t" });
|
|
618
|
-
expect(sent[3].IfMatch).toBeUndefined(); // unfenced PUT stays unfenced
|
|
619
|
-
expect(sent[3].IfNoneMatch).toBeUndefined();
|
|
620
|
-
});
|
|
621
|
-
it("sets ChecksumAlgorithm SHA256 so Object Lock buckets accept the PUT", async () => {
|
|
622
|
-
const io = new S3SdkObjectIO(ctx());
|
|
623
|
-
const sent = [];
|
|
624
|
-
io.client = {
|
|
625
|
-
send: async (cmd) => {
|
|
626
|
-
sent.push(cmd.input);
|
|
627
|
-
return { ETag: '"ck"' };
|
|
628
|
-
},
|
|
629
|
-
};
|
|
630
|
-
await io.putObject({ key: "a", body: Buffer.from("x"), contentType: "t" });
|
|
631
|
-
expect(sent[0].ChecksumAlgorithm).toBe("SHA256");
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
describe("PresignObjectIO.putObject", () => {
|
|
635
|
-
let fetchMock;
|
|
636
|
-
beforeEach(() => {
|
|
637
|
-
fetchMock = vi.fn();
|
|
638
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
639
|
-
});
|
|
640
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
641
|
-
it("sends op:put with metadata and replays the signed headers verbatim", async () => {
|
|
642
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
643
|
-
setPresign([
|
|
644
|
-
{
|
|
645
|
-
key: "shared/a.md",
|
|
646
|
-
op: "put",
|
|
647
|
-
url: "https://s3/put-url",
|
|
648
|
-
headers: {
|
|
649
|
-
"content-type": "text/markdown",
|
|
650
|
-
"x-amz-server-side-encryption": "aws:kms",
|
|
651
|
-
"x-amz-meta-created-by": "me@getindigo.ai",
|
|
652
|
-
},
|
|
653
|
-
},
|
|
654
|
-
]);
|
|
655
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"abc123"' } }));
|
|
656
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
657
|
-
const res = await io.putObject({
|
|
658
|
-
key: "shared/a.md",
|
|
659
|
-
body: Buffer.from("hello"),
|
|
660
|
-
contentType: "text/markdown",
|
|
661
|
-
metadata: { "created-by": "me@getindigo.ai" },
|
|
662
|
-
});
|
|
663
|
-
// Presign request carried op + per-key metadata.
|
|
664
|
-
expect(presignCalls[0].op).toBe("put");
|
|
665
|
-
expect(presignCalls[0].keys[0]).toMatchObject({
|
|
666
|
-
key: "shared/a.md",
|
|
667
|
-
op: "put",
|
|
668
|
-
contentType: "text/markdown",
|
|
669
|
-
metadata: { "created-by": "me@getindigo.ai" },
|
|
670
|
-
});
|
|
671
|
-
// PUT replays the exact signed headers.
|
|
672
|
-
const [url, init] = fetchMock.mock.calls[0];
|
|
673
|
-
expect(url).toBe("https://s3/put-url");
|
|
674
|
-
expect(init.method).toBe("PUT");
|
|
675
|
-
expect(init.headers).toEqual({
|
|
676
|
-
"content-type": "text/markdown",
|
|
677
|
-
"x-amz-server-side-encryption": "aws:kms",
|
|
678
|
-
"x-amz-meta-created-by": "me@getindigo.ai",
|
|
679
|
-
"Content-MD5": "XUFAKrxLKna5cZ2REBfFkg==",
|
|
680
|
-
});
|
|
681
|
-
// ETag returned with quotes stripped.
|
|
682
|
-
expect(res.etag).toBe("abc123");
|
|
683
|
-
});
|
|
684
|
-
it("keeps the server upgrade URL on a plan-limit PUT denial", async () => {
|
|
685
|
-
const { vault, setPresign } = makeVault();
|
|
686
|
-
const upgradeUrl = "https://billing.example.test/company/acme";
|
|
687
|
-
setPresign([
|
|
688
|
-
{
|
|
689
|
-
key: "shared/paused.md",
|
|
690
|
-
op: "put",
|
|
691
|
-
error: "Plan limit reached",
|
|
692
|
-
code: "PLAN_LIMIT_REACHED",
|
|
693
|
-
upgradeUrl,
|
|
694
|
-
},
|
|
695
|
-
]);
|
|
696
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
697
|
-
await expect(io.putObject({
|
|
698
|
-
key: "shared/paused.md",
|
|
699
|
-
body: Buffer.from("hello"),
|
|
700
|
-
contentType: "text/markdown",
|
|
701
|
-
})).rejects.toMatchObject({
|
|
702
|
-
statusCode: 402,
|
|
703
|
-
code: "PLAN_LIMIT_REACHED",
|
|
704
|
-
upgradeUrl,
|
|
705
|
-
});
|
|
706
|
-
});
|
|
707
|
-
it("throws a typed plan-limit refusal for the server's per-item company presign row", async () => {
|
|
708
|
-
const { vault, setPresign } = makeVault();
|
|
709
|
-
setPresign([companyPresignRefusalRow("docs/paused.md")]);
|
|
710
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
711
|
-
const err = await io
|
|
712
|
-
.putObject({
|
|
713
|
-
key: "docs/paused.md",
|
|
714
|
-
body: Buffer.from("hello"),
|
|
715
|
-
contentType: "text/markdown",
|
|
716
|
-
})
|
|
717
|
-
.catch((error) => error);
|
|
718
|
-
expect(err).toBeInstanceOf(VaultClientError);
|
|
719
|
-
expect(err).toMatchObject({
|
|
720
|
-
statusCode: 402,
|
|
721
|
-
path: "docs/paused.md",
|
|
722
|
-
code: "PLAN_LIMIT_EXCEEDED",
|
|
723
|
-
upgradeUrl: PLAN_LIMIT_UPGRADE_URL,
|
|
724
|
-
message: "New files are paused while Acme is over its Starter limits.",
|
|
725
|
-
planLimit: {
|
|
726
|
-
code: "PLAN_LIMIT_EXCEEDED",
|
|
727
|
-
upgradeUrl: PLAN_LIMIT_UPGRADE_URL,
|
|
728
|
-
message: "New files are paused while Acme is over its Starter limits.",
|
|
729
|
-
},
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
it("sends Content-MD5 (base64 of raw MD5) for a known body when the presign did not sign a checksum", async () => {
|
|
733
|
-
const body = Buffer.from("hello");
|
|
734
|
-
expect(contentMd5Base64(body)).toBe("XUFAKrxLKna5cZ2REBfFkg==");
|
|
735
|
-
const { vault, setPresign } = makeVault();
|
|
736
|
-
setPresign([
|
|
737
|
-
{
|
|
738
|
-
key: "docs/a.md",
|
|
739
|
-
op: "put",
|
|
740
|
-
url: "https://s3.example/put?X-Amz-SignedHeaders=content-type%3Bx-amz-meta-created-by",
|
|
741
|
-
headers: { "content-type": "text/plain" },
|
|
742
|
-
},
|
|
743
|
-
]);
|
|
744
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"md5"' } }));
|
|
745
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
746
|
-
await io.putObject({ key: "docs/a.md", body, contentType: "text/plain" });
|
|
747
|
-
const init = fetchMock.mock.calls[0][1];
|
|
748
|
-
expect(init.headers["Content-MD5"]).toBe("XUFAKrxLKna5cZ2REBfFkg==");
|
|
749
|
-
expect(init.headers["x-amz-checksum-sha256"]).toBeUndefined();
|
|
750
|
-
});
|
|
751
|
-
it("replays a signed x-amz-checksum-sha256 instead of attaching Content-MD5", async () => {
|
|
752
|
-
const { vault, setPresign } = makeVault();
|
|
753
|
-
setPresign([
|
|
754
|
-
{
|
|
755
|
-
key: "docs/a.md",
|
|
756
|
-
op: "put",
|
|
757
|
-
url: "https://s3.example/put?X-Amz-SignedHeaders=content-type%3Bx-amz-checksum-sha256",
|
|
758
|
-
headers: {
|
|
759
|
-
"content-type": "text/plain",
|
|
760
|
-
"x-amz-checksum-sha256": "signed-sha256-value",
|
|
761
|
-
},
|
|
762
|
-
},
|
|
763
|
-
]);
|
|
764
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"ck"' } }));
|
|
765
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
766
|
-
await io.putObject({
|
|
767
|
-
key: "docs/a.md",
|
|
768
|
-
body: Buffer.from("hello"),
|
|
769
|
-
contentType: "text/plain",
|
|
770
|
-
});
|
|
771
|
-
const init = fetchMock.mock.calls[0][1];
|
|
772
|
-
expect(init.headers["x-amz-checksum-sha256"]).toBe("signed-sha256-value");
|
|
773
|
-
expect(init.headers["Content-MD5"]).toBeUndefined();
|
|
774
|
-
});
|
|
775
|
-
it("classifies Object Lock InvalidRequest 400 without retrying", async () => {
|
|
776
|
-
const xml = '<?xml version="1.0" encoding="UTF-8"?><Error><Code>InvalidRequest</Code>' +
|
|
777
|
-
"<Message>Content-MD5 OR x-amz-checksum- HTTP header is required for Put Object requests with Object Lock parameters</Message></Error>";
|
|
778
|
-
const { vault, setPresign } = makeVault();
|
|
779
|
-
setPresign([{ key: "docs/a.md", op: "put", url: "https://s3/put" }]);
|
|
780
|
-
fetchMock.mockResolvedValue(new Response(xml, { status: 400 }));
|
|
781
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
782
|
-
let caught;
|
|
783
|
-
try {
|
|
784
|
-
await io.putObject({
|
|
785
|
-
key: "docs/a.md",
|
|
786
|
-
body: Buffer.from("hello"),
|
|
787
|
-
contentType: "text/plain",
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
catch (err) {
|
|
791
|
-
caught = err;
|
|
792
|
-
}
|
|
793
|
-
expect(caught).toBeInstanceOf(ObjectLockChecksumRequiredError);
|
|
794
|
-
expect(caught).toMatchObject({
|
|
795
|
-
name: "ObjectLockChecksumRequired",
|
|
796
|
-
statusCode: 400,
|
|
797
|
-
retryable: false,
|
|
798
|
-
});
|
|
799
|
-
expect(caught.message).toMatch(/Object Lock requires a Content-MD5/);
|
|
800
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
801
|
-
expect(isTransientNetworkError(caught)).toBe(false);
|
|
802
|
-
});
|
|
803
|
-
it("throws on a per-key denial (error row)", async () => {
|
|
804
|
-
const { vault, setPresign } = makeVault();
|
|
805
|
-
setPresign([
|
|
806
|
-
{ key: "secret/x", op: "put", error: "forbidden", code: "ACL_DENY" },
|
|
807
|
-
]);
|
|
808
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
809
|
-
await expect(io.putObject({
|
|
810
|
-
key: "secret/x",
|
|
811
|
-
body: Buffer.from("z"),
|
|
812
|
-
contentType: "text/plain",
|
|
813
|
-
})).rejects.toThrow(/denied for secret\/x.*forbidden.*ACL_DENY/);
|
|
814
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
815
|
-
});
|
|
816
|
-
it("a write denial (FILES_PRESIGN_FORBIDDEN) throws the access-denied shape so the push skips it like a denied GET", async () => {
|
|
817
|
-
const { vault, setPresign } = makeVault();
|
|
818
|
-
// The server refuses to MINT a PUT URL for a key the caller can't write
|
|
819
|
-
// (private-by-default: members hold read-only on a shared knowledge prefix).
|
|
820
|
-
setPresign([
|
|
821
|
-
{
|
|
822
|
-
key: "knowledge/clients/acme/brief.md",
|
|
823
|
-
op: "put",
|
|
824
|
-
error: "Caller lacks write permission on 'knowledge/clients/acme/brief.md'",
|
|
825
|
-
code: "FILES_PRESIGN_FORBIDDEN",
|
|
826
|
-
},
|
|
827
|
-
]);
|
|
828
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
829
|
-
let caught;
|
|
830
|
-
await io
|
|
831
|
-
.putObject({
|
|
832
|
-
key: "knowledge/clients/acme/brief.md",
|
|
833
|
-
body: Buffer.from("z"),
|
|
834
|
-
contentType: "text/markdown",
|
|
835
|
-
})
|
|
836
|
-
.catch((e) => {
|
|
837
|
-
caught = e;
|
|
838
|
-
});
|
|
839
|
-
// Same shape a denied GET/HEAD already throws (name === "Forbidden"), so the
|
|
840
|
-
// share.ts push catch routes it through `isAccessDenied` into the silent
|
|
841
|
-
// per-path skip (scopeExcludedSet) + the "N paths skipped — outside your
|
|
842
|
-
// granted access" aggregate, instead of a fatal `type:"error"` that exits
|
|
843
|
-
// the runner non-zero and floods Sentry. This is the PUT/GET symmetry that
|
|
844
|
-
// fixes the presign-put-denied crash-loop across user machines.
|
|
845
|
-
expect(caught.name).toBe("Forbidden");
|
|
846
|
-
expect(isAccessDenied(caught)).toBe(true);
|
|
847
|
-
// Denied at mint time — the byte-moving PUT is never attempted.
|
|
848
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
849
|
-
});
|
|
850
|
-
it("US-006 AC5: a transcript-consent denial (FILES_PRESIGN_TRANSCRIPT_CONSENT_REQUIRED) is skip-with-log, not fatal", async () => {
|
|
851
|
-
const { vault, setPresign } = makeVault();
|
|
852
|
-
// The US-005 consent code ends `_REQUIRED`, not `_FORBIDDEN`, so the plain
|
|
853
|
-
// suffix test alone would MISS it and let it fall through to the fatal
|
|
854
|
-
// generic Error — the recurring partial-failure this AC5 fix prevents.
|
|
855
|
-
setPresign([
|
|
856
|
-
{
|
|
857
|
-
key: "sessions/prs_1/2026-07-11T10-00-00.json",
|
|
858
|
-
op: "get",
|
|
859
|
-
error: "transcript consent required",
|
|
860
|
-
code: "FILES_PRESIGN_TRANSCRIPT_CONSENT_REQUIRED",
|
|
861
|
-
},
|
|
862
|
-
]);
|
|
863
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
864
|
-
// Monkeypatch console.warn (vitest's console interception can shadow a spy)
|
|
865
|
-
// to assert the REAL skip log fires.
|
|
866
|
-
const warnings = [];
|
|
867
|
-
const originalWarn = console.warn;
|
|
868
|
-
console.warn = (...args) => {
|
|
869
|
-
warnings.push(args.map(String).join(" "));
|
|
870
|
-
};
|
|
871
|
-
let caught;
|
|
872
|
-
try {
|
|
873
|
-
await io
|
|
874
|
-
.getObject("sessions/prs_1/2026-07-11T10-00-00.json")
|
|
875
|
-
.catch((e) => {
|
|
876
|
-
caught = e;
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
finally {
|
|
880
|
-
console.warn = originalWarn;
|
|
881
|
-
}
|
|
882
|
-
// Same shape a denied GET already throws → isAccessDenied → the per-key
|
|
883
|
-
// scope-excluded skip, NOT a fatal `type:"error"` that poisons the run.
|
|
884
|
-
expect(caught.name).toBe("Forbidden");
|
|
885
|
-
expect(isAccessDenied(caught)).toBe(true);
|
|
886
|
-
// A one-line skip log fires so operators can see WHY the key was skipped.
|
|
887
|
-
expect(warnings.some((w) => w.includes("FILES_PRESIGN_TRANSCRIPT_CONSENT_REQUIRED"))).toBe(true);
|
|
888
|
-
});
|
|
889
|
-
it("US-006 AC5: a sessions structural-ACL denial (FILES_PRESIGN_SESSIONS_FORBIDDEN) is skip-with-log", async () => {
|
|
890
|
-
const { vault, setPresign } = makeVault();
|
|
891
|
-
// A spoofed/foreign `sessions/{uid}/` prefix the hq-pro structural gate
|
|
892
|
-
// refuses to presign. Ends `_FORBIDDEN`, so the suffix test already covers
|
|
893
|
-
// it — this pins that coverage as a regression contract for US-006.
|
|
894
|
-
setPresign([
|
|
895
|
-
{
|
|
896
|
-
key: "sessions/prs_other/y.json",
|
|
897
|
-
op: "get",
|
|
898
|
-
error: "not your session prefix",
|
|
899
|
-
code: "FILES_PRESIGN_SESSIONS_FORBIDDEN",
|
|
900
|
-
},
|
|
901
|
-
]);
|
|
902
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
903
|
-
let caught;
|
|
904
|
-
await io.getObject("sessions/prs_other/y.json").catch((e) => {
|
|
905
|
-
caught = e;
|
|
906
|
-
});
|
|
907
|
-
expect(caught.name).toBe("Forbidden");
|
|
908
|
-
expect(isAccessDenied(caught)).toBe(true);
|
|
909
|
-
});
|
|
910
|
-
it("a non-authorization presign failure stays a generic (fatal) error — not silently skipped", async () => {
|
|
911
|
-
const { vault, setPresign } = makeVault();
|
|
912
|
-
setPresign([
|
|
913
|
-
{ key: "secret/x", op: "put", error: "boom", code: "PRESIGN_INTERNAL" },
|
|
914
|
-
]);
|
|
915
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
916
|
-
let caught;
|
|
917
|
-
await io
|
|
918
|
-
.putObject({ key: "secret/x", body: Buffer.from("z"), contentType: "x" })
|
|
919
|
-
.catch((e) => {
|
|
920
|
-
caught = e;
|
|
921
|
-
});
|
|
922
|
-
// Only `*_FORBIDDEN` authorization denials are reclassified; a real backend
|
|
923
|
-
// failure must still surface loudly so it is not masked as an expected skip.
|
|
924
|
-
expect(isAccessDenied(caught)).toBe(false);
|
|
925
|
-
expect(caught.message).toMatch(/denied for secret\/x.*PRESIGN_INTERNAL/);
|
|
926
|
-
});
|
|
927
|
-
it("throws when the PUT itself fails", async () => {
|
|
928
|
-
const { vault, setPresign } = makeVault();
|
|
929
|
-
setPresign([{ key: "k", op: "put", url: "https://s3/put" }]);
|
|
930
|
-
fetchMock.mockResolvedValue(new Response("AccessDenied", { status: 403 }));
|
|
931
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
932
|
-
await expect(io.putObject({ key: "k", body: Buffer.from("z"), contentType: "x" })).rejects.toThrow(/presigned PUT failed for k: 403/);
|
|
933
|
-
});
|
|
934
|
-
it("F01: fails closed when a fenced PUT presign omits If-Match", async () => {
|
|
935
|
-
const previous = process.env.HQ_PRESIGN_FENCE_STRICT;
|
|
936
|
-
process.env.HQ_PRESIGN_FENCE_STRICT = "true";
|
|
937
|
-
try {
|
|
938
|
-
const { vault, setPresign } = makeVault();
|
|
939
|
-
setPresign([
|
|
940
|
-
{
|
|
941
|
-
key: "stale.md",
|
|
942
|
-
op: "put",
|
|
943
|
-
url: "https://s3/unfenced-put",
|
|
944
|
-
headers: { "content-type": "text/markdown" },
|
|
945
|
-
},
|
|
946
|
-
]);
|
|
947
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"v2"' } }));
|
|
948
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
949
|
-
await expect(io.putObject({
|
|
950
|
-
key: "stale.md",
|
|
951
|
-
body: Buffer.from("old bytes"),
|
|
952
|
-
contentType: "text/markdown",
|
|
953
|
-
ifMatch: '"v1"',
|
|
954
|
-
})).rejects.toThrow(/if-match|condition|precondition/i);
|
|
955
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
956
|
-
}
|
|
957
|
-
finally {
|
|
958
|
-
if (previous === undefined) {
|
|
959
|
-
delete process.env.HQ_PRESIGN_FENCE_STRICT;
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
process.env.HQ_PRESIGN_FENCE_STRICT = previous;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
});
|
|
966
|
-
it("F01: does not satisfy a fenced PUT with an unfenced primed URL", async () => {
|
|
967
|
-
const presignCalls = [];
|
|
968
|
-
const vault = {
|
|
969
|
-
presign: async (input) => {
|
|
970
|
-
presignCalls.push(input);
|
|
971
|
-
const isFenced = input.keys.some((k) => k.ifNoneMatch === "*");
|
|
972
|
-
return {
|
|
973
|
-
results: input.keys.map((k) => ({
|
|
974
|
-
key: k.key,
|
|
975
|
-
op: k.op ?? input.op ?? "put",
|
|
976
|
-
url: isFenced ? "https://s3/fenced-put" : "https://s3/unfenced-put",
|
|
977
|
-
headers: isFenced
|
|
978
|
-
? { "content-type": "text/plain", "if-none-match": "*" }
|
|
979
|
-
: { "content-type": "text/plain" },
|
|
980
|
-
expiresIn: 900,
|
|
981
|
-
})),
|
|
982
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
983
|
-
};
|
|
984
|
-
},
|
|
985
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
986
|
-
};
|
|
987
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"created"' } }));
|
|
988
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
989
|
-
await io.prime("put", [{ key: "new.md", op: "put", contentType: "text/plain" }]);
|
|
990
|
-
const afterPrime = presignCalls.length;
|
|
991
|
-
await io.putObject({
|
|
992
|
-
key: "new.md",
|
|
993
|
-
body: Buffer.from("new bytes"),
|
|
994
|
-
contentType: "text/plain",
|
|
995
|
-
ifNoneMatch: "*",
|
|
996
|
-
});
|
|
997
|
-
expect(presignCalls).toHaveLength(afterPrime + 1);
|
|
998
|
-
expect(presignCalls[afterPrime].keys[0]).toMatchObject({
|
|
999
|
-
key: "new.md",
|
|
1000
|
-
op: "put",
|
|
1001
|
-
ifNoneMatch: "*",
|
|
1002
|
-
});
|
|
1003
|
-
const [url, init] = fetchMock.mock.calls[0];
|
|
1004
|
-
expect(url).toBe("https://s3/fenced-put");
|
|
1005
|
-
expect(init.headers).toMatchObject({
|
|
1006
|
-
"content-type": "text/plain",
|
|
1007
|
-
"if-none-match": "*",
|
|
1008
|
-
});
|
|
1009
|
-
expect(init.headers["Content-MD5"]).toBe(createHash("md5").update(Buffer.from("new bytes")).digest("base64"));
|
|
1010
|
-
});
|
|
1011
|
-
it("forwards the conditional-write fence on the presign request (ifMatch stripped, ifNoneMatch verbatim)", async () => {
|
|
1012
|
-
// The server signs If-Match/If-None-Match into the URL (hq-pro follow-up)
|
|
1013
|
-
// and echoes the headers for replay; the client's job is to ASK. Servers
|
|
1014
|
-
// that predate the fields ignore them — never broken, just unconditional.
|
|
1015
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
1016
|
-
setPresign([{ key: "fenced.md", op: "put", url: "https://s3/put-url" }]);
|
|
1017
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"v2"' } }));
|
|
1018
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1019
|
-
await io.putObject({
|
|
1020
|
-
key: "fenced.md",
|
|
1021
|
-
body: Buffer.from("x"),
|
|
1022
|
-
contentType: "text/markdown",
|
|
1023
|
-
ifMatch: '"v1"',
|
|
1024
|
-
});
|
|
1025
|
-
expect(presignCalls[0].keys[0]).toMatchObject({
|
|
1026
|
-
key: "fenced.md",
|
|
1027
|
-
op: "put",
|
|
1028
|
-
ifMatch: "v1", // quotes stripped on the wire; server re-quotes when signing
|
|
1029
|
-
});
|
|
1030
|
-
await io.putObject({
|
|
1031
|
-
key: "fresh.md",
|
|
1032
|
-
body: Buffer.from("y"),
|
|
1033
|
-
contentType: "text/markdown",
|
|
1034
|
-
ifNoneMatch: "*",
|
|
1035
|
-
});
|
|
1036
|
-
expect(presignCalls[1].keys[0]).toMatchObject({
|
|
1037
|
-
key: "fresh.md",
|
|
1038
|
-
op: "put",
|
|
1039
|
-
ifNoneMatch: "*",
|
|
1040
|
-
});
|
|
1041
|
-
});
|
|
1042
|
-
it("maps a 412 PUT response to name:PreconditionFailed (the fence fired — conflict, not failure)", async () => {
|
|
1043
|
-
const { vault, setPresign } = makeVault();
|
|
1044
|
-
setPresign([{ key: "raced.md", op: "put", url: "https://s3/put-url" }]);
|
|
1045
|
-
fetchMock.mockResolvedValue(new Response("PreconditionFailed", { status: 412 }));
|
|
1046
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1047
|
-
await expect(io.putObject({
|
|
1048
|
-
key: "raced.md",
|
|
1049
|
-
body: Buffer.from("z"),
|
|
1050
|
-
contentType: "text/plain",
|
|
1051
|
-
ifMatch: "old-etag",
|
|
1052
|
-
})).rejects.toMatchObject({ name: "PreconditionFailed" });
|
|
1053
|
-
});
|
|
1054
|
-
it("PUT presign with hq-content-sha256 sends checksumSha256 as base64 of that hex", async () => {
|
|
1055
|
-
const hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
1056
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
1057
|
-
setPresign([{ key: "hashed.md", op: "put", url: "https://s3/put" }]);
|
|
1058
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"abc"' } }));
|
|
1059
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1060
|
-
await io.putObject({
|
|
1061
|
-
key: "hashed.md",
|
|
1062
|
-
body: Buffer.from(""),
|
|
1063
|
-
contentType: "text/plain",
|
|
1064
|
-
metadata: { "hq-content-sha256": hex },
|
|
1065
|
-
});
|
|
1066
|
-
expect(presignCalls[0].keys[0]).toMatchObject({
|
|
1067
|
-
key: "hashed.md",
|
|
1068
|
-
op: "put",
|
|
1069
|
-
checksumSha256: Buffer.from(hex, "hex").toString("base64"),
|
|
1070
|
-
});
|
|
1071
|
-
});
|
|
1072
|
-
it("PUT presign without hq-content-sha256 omits checksumSha256", async () => {
|
|
1073
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
1074
|
-
setPresign([{ key: "plain.md", op: "put", url: "https://s3/put" }]);
|
|
1075
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"abc"' } }));
|
|
1076
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1077
|
-
await io.putObject({
|
|
1078
|
-
key: "plain.md",
|
|
1079
|
-
body: Buffer.from("z"),
|
|
1080
|
-
contentType: "text/plain",
|
|
1081
|
-
});
|
|
1082
|
-
expect(presignCalls[0].keys[0]).toMatchObject({
|
|
1083
|
-
key: "plain.md",
|
|
1084
|
-
op: "put",
|
|
1085
|
-
});
|
|
1086
|
-
expect(presignCalls[0].keys[0].checksumSha256).toBeUndefined();
|
|
1087
|
-
});
|
|
1088
|
-
it("replays a server-echoed x-amz-checksum-sha256 header on the PUT unchanged", async () => {
|
|
1089
|
-
const hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
1090
|
-
const checksum = Buffer.from(hex, "hex").toString("base64");
|
|
1091
|
-
const { vault, setPresign } = makeVault();
|
|
1092
|
-
setPresign([
|
|
1093
|
-
{
|
|
1094
|
-
key: "locked.md",
|
|
1095
|
-
op: "put",
|
|
1096
|
-
url: "https://s3/put-locked",
|
|
1097
|
-
headers: {
|
|
1098
|
-
"content-type": "text/plain",
|
|
1099
|
-
"x-amz-checksum-sha256": checksum,
|
|
1100
|
-
},
|
|
1101
|
-
},
|
|
1102
|
-
]);
|
|
1103
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 200, headers: { etag: '"abc"' } }));
|
|
1104
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1105
|
-
await io.putObject({
|
|
1106
|
-
key: "locked.md",
|
|
1107
|
-
body: Buffer.from(""),
|
|
1108
|
-
contentType: "text/plain",
|
|
1109
|
-
metadata: { "hq-content-sha256": hex },
|
|
1110
|
-
});
|
|
1111
|
-
const [, init] = fetchMock.mock.calls[0];
|
|
1112
|
-
expect(init.headers).toEqual({
|
|
1113
|
-
"content-type": "text/plain",
|
|
1114
|
-
"x-amz-checksum-sha256": checksum,
|
|
1115
|
-
});
|
|
1116
|
-
});
|
|
1117
|
-
});
|
|
1118
|
-
describe("PresignObjectIO.getObject", () => {
|
|
1119
|
-
let fetchMock;
|
|
1120
|
-
beforeEach(() => {
|
|
1121
|
-
fetchMock = vi.fn();
|
|
1122
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1123
|
-
});
|
|
1124
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
1125
|
-
it("returns body bytes + x-amz-meta-* metadata", async () => {
|
|
1126
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
1127
|
-
setPresign([{ key: "shared/a.md", op: "get", url: "https://s3/get" }]);
|
|
1128
|
-
fetchMock.mockResolvedValue(new Response(Buffer.from("file-bytes"), {
|
|
1129
|
-
status: 200,
|
|
1130
|
-
headers: {
|
|
1131
|
-
"x-amz-meta-created-by": "me@getindigo.ai",
|
|
1132
|
-
"x-amz-meta-hq-mode": "640",
|
|
1133
|
-
"content-type": "text/markdown",
|
|
1134
|
-
},
|
|
1135
|
-
}));
|
|
1136
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1137
|
-
const res = await io.getObject("shared/a.md");
|
|
1138
|
-
expect(presignCalls[0].op).toBe("get");
|
|
1139
|
-
expect(res.body.toString("utf-8")).toBe("file-bytes");
|
|
1140
|
-
expect(res.metadata).toEqual({
|
|
1141
|
-
"created-by": "me@getindigo.ai",
|
|
1142
|
-
"hq-mode": "640",
|
|
1143
|
-
});
|
|
1144
|
-
});
|
|
1145
|
-
it("GET presigns never send checksumSha256", async () => {
|
|
1146
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
1147
|
-
setPresign([{ key: "shared/a.md", op: "get", url: "https://s3/get" }]);
|
|
1148
|
-
fetchMock.mockResolvedValue(new Response(Buffer.from("file-bytes"), { status: 200 }));
|
|
1149
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1150
|
-
await io.getObject("shared/a.md");
|
|
1151
|
-
expect(presignCalls[0].op).toBe("get");
|
|
1152
|
-
expect(presignCalls[0].keys[0].checksumSha256).toBeUndefined();
|
|
1153
|
-
});
|
|
1154
|
-
it("drains the injected transport body instead of returning empty content", async () => {
|
|
1155
|
-
const { vault, setPresign } = makeVault();
|
|
1156
|
-
setPresign([{ key: "shared/a.md", op: "get", url: "https://s3/get" }]);
|
|
1157
|
-
const transportGet = vi.fn(async () => ({
|
|
1158
|
-
status: 200,
|
|
1159
|
-
headers: new Headers({ "x-amz-meta-source": "transport" }),
|
|
1160
|
-
body: (async function* () {
|
|
1161
|
-
yield Buffer.from("transport-bytes");
|
|
1162
|
-
})(),
|
|
1163
|
-
destroy: () => undefined,
|
|
1164
|
-
}));
|
|
1165
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
1166
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1167
|
-
const res = await io.getObject("shared/a.md");
|
|
1168
|
-
expect(res.body.toString("utf-8")).toBe("transport-bytes");
|
|
1169
|
-
expect(res.metadata).toEqual({ source: "transport" });
|
|
1170
|
-
expect(transportGet).toHaveBeenCalledWith("https://s3/get", undefined);
|
|
1171
|
-
});
|
|
1172
|
-
it("throws a NotFound-named error on 404 (so s3.ts catch sites match)", async () => {
|
|
1173
|
-
const { vault, setPresign } = makeVault();
|
|
1174
|
-
setPresign([{ key: "gone", op: "get", url: "https://s3/get" }]);
|
|
1175
|
-
fetchMock.mockResolvedValue(new Response("", { status: 404 }));
|
|
1176
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1177
|
-
await expect(io.getObject("gone")).rejects.toMatchObject({
|
|
1178
|
-
name: "NotFound",
|
|
1179
|
-
});
|
|
1180
|
-
});
|
|
1181
|
-
it("throws a NotFound-named error on FILES_PRESIGN_NOT_FOUND (absence at mint, no GET issued)", async () => {
|
|
1182
|
-
// Per-key GET presign used to throw a generic "presign get denied …
|
|
1183
|
-
// Object not found (FILES_PRESIGN_NOT_FOUND)" Error. That is NOT
|
|
1184
|
-
// isAccessDenied and was NOT isNotFound, so sync treated it as a hard
|
|
1185
|
-
// per-file error and settled the company `partial`. Absence at mint is
|
|
1186
|
-
// the same shape as a presigned GET 404.
|
|
1187
|
-
const { vault, setPresign } = makeVault();
|
|
1188
|
-
setPresign([
|
|
1189
|
-
{
|
|
1190
|
-
key: "projects/runner/state/mac-google-check-marketer-2.json",
|
|
1191
|
-
op: "get",
|
|
1192
|
-
error: "Object not found: 'projects/runner/state/mac-google-check-marketer-2.json'",
|
|
1193
|
-
code: "FILES_PRESIGN_NOT_FOUND",
|
|
1194
|
-
},
|
|
1195
|
-
]);
|
|
1196
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1197
|
-
let caught;
|
|
1198
|
-
await io
|
|
1199
|
-
.getObject("projects/runner/state/mac-google-check-marketer-2.json")
|
|
1200
|
-
.catch((e) => {
|
|
1201
|
-
caught = e;
|
|
1202
|
-
});
|
|
1203
|
-
expect(caught.name).toBe("NotFound");
|
|
1204
|
-
expect(isNotFound(caught)).toBe(true);
|
|
1205
|
-
expect(isAccessDenied(caught)).toBe(false);
|
|
1206
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1207
|
-
});
|
|
1208
|
-
});
|
|
1209
|
-
describe("PresignObjectIO.listObjects", () => {
|
|
1210
|
-
it("maps listFiles rows into the RemoteObject shape (etag, Date, storage class)", async () => {
|
|
1211
|
-
const { vault, setList } = makeVault();
|
|
1212
|
-
setList({
|
|
1213
|
-
objects: [
|
|
1214
|
-
{
|
|
1215
|
-
key: "shared/a.md",
|
|
1216
|
-
size: 12,
|
|
1217
|
-
lastModified: "2026-01-01T00:00:00.000Z",
|
|
1218
|
-
storageClass: "DEEP_ARCHIVE",
|
|
1219
|
-
etag: "deadbeef",
|
|
1220
|
-
permission: "read",
|
|
1221
|
-
},
|
|
1222
|
-
{
|
|
1223
|
-
key: "shared/b.md",
|
|
1224
|
-
size: 0,
|
|
1225
|
-
lastModified: null,
|
|
1226
|
-
etag: null,
|
|
1227
|
-
permission: "write",
|
|
1228
|
-
},
|
|
1229
|
-
],
|
|
1230
|
-
cursor: "next-token",
|
|
1231
|
-
truncated: true,
|
|
1232
|
-
});
|
|
1233
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1234
|
-
const res = await io.listObjects({ prefix: "shared/" });
|
|
1235
|
-
expect(res.nextContinuationToken).toBe("next-token");
|
|
1236
|
-
expect(res.objects[0]).toEqual({
|
|
1237
|
-
key: "shared/a.md",
|
|
1238
|
-
size: 12,
|
|
1239
|
-
lastModified: new Date("2026-01-01T00:00:00.000Z"),
|
|
1240
|
-
etag: "deadbeef",
|
|
1241
|
-
storageClass: "DEEP_ARCHIVE",
|
|
1242
|
-
});
|
|
1243
|
-
// Omitted storageClass from an older hq-pro list server defaults Standard;
|
|
1244
|
-
// null etag → "" ; null lastModified → a Date (not crash).
|
|
1245
|
-
expect(res.objects[1].etag).toBe("");
|
|
1246
|
-
expect(res.objects[1].lastModified).toBeInstanceOf(Date);
|
|
1247
|
-
expect(res.objects[1].storageClass).toBe("STANDARD");
|
|
1248
|
-
});
|
|
1249
|
-
it("exhausted listing yields undefined nextContinuationToken", async () => {
|
|
1250
|
-
const { vault, setList } = makeVault();
|
|
1251
|
-
setList({ objects: [], cursor: null, truncated: false });
|
|
1252
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1253
|
-
const res = await io.listObjects({});
|
|
1254
|
-
expect(res.nextContinuationToken).toBeUndefined();
|
|
1255
|
-
});
|
|
1256
|
-
it("lists the safe parent and filters sibling keys for a forbidden prefix", async () => {
|
|
1257
|
-
const prefix = "a/api/issues/[id]/";
|
|
1258
|
-
const { vault, calls } = makeForbiddenPrefixVault(new Map([
|
|
1259
|
-
[undefined, {
|
|
1260
|
-
objects: [
|
|
1261
|
-
vaultObject("a/api/issues/[id]/route.ts"),
|
|
1262
|
-
vaultObject("a/api/issues/other.js"),
|
|
1263
|
-
],
|
|
1264
|
-
cursor: null,
|
|
1265
|
-
truncated: false,
|
|
1266
|
-
}],
|
|
1267
|
-
]));
|
|
1268
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1269
|
-
const res = await io.listObjects({ prefix });
|
|
1270
|
-
expect(calls).toEqual([
|
|
1271
|
-
{ companyUid: COMPANY, prefix: "a/api/issues/", cursor: undefined },
|
|
1272
|
-
]);
|
|
1273
|
-
expect(res.objects.map(({ key }) => key)).toEqual(["a/api/issues/[id]/route.ts"]);
|
|
1274
|
-
});
|
|
1275
|
-
it("preserves ancestor cursors across empty filtered pages", async () => {
|
|
1276
|
-
const prefix = "a/api/issues/[id]/";
|
|
1277
|
-
const { vault, calls } = makeForbiddenPrefixVault(new Map([
|
|
1278
|
-
["incoming-cursor", {
|
|
1279
|
-
objects: [vaultObject("a/api/issues/other.js")],
|
|
1280
|
-
cursor: "page-two-cursor",
|
|
1281
|
-
truncated: true,
|
|
1282
|
-
}],
|
|
1283
|
-
["page-two-cursor", {
|
|
1284
|
-
objects: [
|
|
1285
|
-
vaultObject("a/api/issues/[id]/route.ts"),
|
|
1286
|
-
vaultObject("a/api/issues/another.js"),
|
|
1287
|
-
],
|
|
1288
|
-
cursor: null,
|
|
1289
|
-
truncated: false,
|
|
1290
|
-
}],
|
|
1291
|
-
]));
|
|
1292
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1293
|
-
const first = await io.listObjects({ prefix, continuationToken: "incoming-cursor" });
|
|
1294
|
-
expect(first.objects).toEqual([]);
|
|
1295
|
-
expect(first.nextContinuationToken).toBe("page-two-cursor");
|
|
1296
|
-
const second = await io.listObjects({
|
|
1297
|
-
prefix,
|
|
1298
|
-
continuationToken: first.nextContinuationToken,
|
|
1299
|
-
});
|
|
1300
|
-
expect(second.objects.map(({ key }) => key)).toEqual(["a/api/issues/[id]/route.ts"]);
|
|
1301
|
-
expect(calls).toEqual([
|
|
1302
|
-
{ companyUid: COMPANY, prefix: "a/api/issues/", cursor: "incoming-cursor" },
|
|
1303
|
-
{ companyUid: COMPANY, prefix: "a/api/issues/", cursor: "page-two-cursor" },
|
|
1304
|
-
]);
|
|
1305
|
-
});
|
|
1306
|
-
it("leaves a normal prefix and its result page unchanged", async () => {
|
|
1307
|
-
const { vault, calls } = makeForbiddenPrefixVault(new Map([
|
|
1308
|
-
[undefined, {
|
|
1309
|
-
objects: [vaultObject("a/b/file.txt"), vaultObject("outside/file.txt")],
|
|
1310
|
-
cursor: null,
|
|
1311
|
-
truncated: false,
|
|
1312
|
-
}],
|
|
1313
|
-
]));
|
|
1314
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1315
|
-
const res = await io.listObjects({ prefix: "a/b/" });
|
|
1316
|
-
expect(calls).toEqual([
|
|
1317
|
-
{ companyUid: COMPANY, prefix: "a/b/", cursor: undefined },
|
|
1318
|
-
]);
|
|
1319
|
-
expect(res.objects.map(({ key }) => key)).toEqual([
|
|
1320
|
-
"a/b/file.txt",
|
|
1321
|
-
"outside/file.txt",
|
|
1322
|
-
]);
|
|
1323
|
-
});
|
|
1324
|
-
it("returns an empty ancestor when a bracket occurs in the first segment", () => {
|
|
1325
|
-
expect(safeVaultListPrefixAncestor("[id]/route.ts")).toBe("");
|
|
1326
|
-
});
|
|
1327
|
-
it.each([
|
|
1328
|
-
["?", "a/api/issues?state=open", "a/api/"],
|
|
1329
|
-
["#", "a/api/issues#fragment", "a/api/"],
|
|
1330
|
-
["\\", "a/api\\issues/route.ts", "a/"],
|
|
1331
|
-
["]", "a/api/issues]/route.ts", "a/api/"],
|
|
1332
|
-
])("computes the ancestor before %s", (_character, prefix, expected) => {
|
|
1333
|
-
expect(safeVaultListPrefixAncestor(prefix)).toBe(expected);
|
|
1334
|
-
});
|
|
1335
|
-
it("returns a prefix without forbidden characters unchanged", () => {
|
|
1336
|
-
expect(safeVaultListPrefixAncestor("a/b/")).toBe("a/b/");
|
|
1337
|
-
});
|
|
1338
|
-
});
|
|
1339
|
-
describe("PresignObjectIO.deleteObject", () => {
|
|
1340
|
-
let fetchMock;
|
|
1341
|
-
beforeEach(() => {
|
|
1342
|
-
fetchMock = vi.fn();
|
|
1343
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1344
|
-
});
|
|
1345
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
1346
|
-
it("sends op:delete and DELETEs the signed URL", async () => {
|
|
1347
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
1348
|
-
setPresign([{ key: "shared/a.md", op: "delete", url: "https://s3/del" }]);
|
|
1349
|
-
fetchMock.mockResolvedValue(new Response(null, { status: 204 }));
|
|
1350
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1351
|
-
await io.deleteObject("shared/a.md");
|
|
1352
|
-
expect(presignCalls[0].op).toBe("delete");
|
|
1353
|
-
expect(fetchMock.mock.calls[0][1].method).toBe("DELETE");
|
|
1354
|
-
});
|
|
1355
|
-
it("treats a 404 DELETE as success (idempotent)", async () => {
|
|
1356
|
-
const { vault, setPresign } = makeVault();
|
|
1357
|
-
setPresign([{ key: "gone", op: "delete", url: "https://s3/del" }]);
|
|
1358
|
-
fetchMock.mockResolvedValue(new Response("", { status: 404 }));
|
|
1359
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1360
|
-
await expect(io.deleteObject("gone")).resolves.toBeUndefined();
|
|
1361
|
-
});
|
|
1362
|
-
});
|
|
1363
|
-
describe("PresignObjectIO.headObject", () => {
|
|
1364
|
-
let fetchMock;
|
|
1365
|
-
beforeEach(() => {
|
|
1366
|
-
fetchMock = vi.fn();
|
|
1367
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1368
|
-
});
|
|
1369
|
-
afterEach(() => {
|
|
1370
|
-
vi.unstubAllGlobals();
|
|
1371
|
-
vi.restoreAllMocks();
|
|
1372
|
-
});
|
|
1373
|
-
it("reads headers (etag/size/lastModified/metadata) via a presigned GET", async () => {
|
|
1374
|
-
const { vault, setPresign } = makeVault();
|
|
1375
|
-
setPresign([{ key: "shared/a.md", op: "get", url: "https://s3/get" }]);
|
|
1376
|
-
fetchMock.mockResolvedValue(new Response(Buffer.from("ignored-body"), {
|
|
1377
|
-
status: 200,
|
|
1378
|
-
headers: {
|
|
1379
|
-
etag: '"feedface"',
|
|
1380
|
-
"content-length": "123",
|
|
1381
|
-
"last-modified": "Wed, 01 Jan 2026 00:00:00 GMT",
|
|
1382
|
-
"x-amz-meta-created-at": "2026-01-01T00:00:00.000Z",
|
|
1383
|
-
},
|
|
1384
|
-
}));
|
|
1385
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1386
|
-
const head = await io.headObject("shared/a.md");
|
|
1387
|
-
expect(head).not.toBeNull();
|
|
1388
|
-
expect(head.etag).toBe("feedface");
|
|
1389
|
-
expect(head.size).toBe(123);
|
|
1390
|
-
expect(head.metadata).toEqual({
|
|
1391
|
-
"created-at": "2026-01-01T00:00:00.000Z",
|
|
1392
|
-
});
|
|
1393
|
-
expect(head.lastModified.getTime()).toBe(Date.parse("Wed, 01 Jan 2026 00:00:00 GMT"));
|
|
1394
|
-
});
|
|
1395
|
-
it("returns null on 404", async () => {
|
|
1396
|
-
const { vault, setPresign } = makeVault();
|
|
1397
|
-
setPresign([{ key: "gone", op: "get", url: "https://s3/get" }]);
|
|
1398
|
-
fetchMock.mockResolvedValue(new Response("", { status: 404 }));
|
|
1399
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1400
|
-
expect(await io.headObject("gone")).toBeNull();
|
|
1401
|
-
});
|
|
1402
|
-
it("returns null on a server-confirmed FILES_PRESIGN_NOT_FOUND (absence, not denial — no GET issued)", async () => {
|
|
1403
|
-
// The presign lambda HEAD'd S3 and confirmed absence. Unlike a read
|
|
1404
|
-
// DENIAL (Forbidden, below), this is unambiguous "object missing", so
|
|
1405
|
-
// headObject reports absent (null) and the push creates the new object —
|
|
1406
|
-
// never reaching a 403-masked presigned GET.
|
|
1407
|
-
const { vault, setPresign } = makeVault();
|
|
1408
|
-
setPresign([
|
|
1409
|
-
{
|
|
1410
|
-
key: "new.md",
|
|
1411
|
-
op: "get",
|
|
1412
|
-
error: "Object not found: 'new.md'",
|
|
1413
|
-
code: "FILES_PRESIGN_NOT_FOUND",
|
|
1414
|
-
},
|
|
1415
|
-
]);
|
|
1416
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1417
|
-
expect(await io.headObject("new.md")).toBeNull();
|
|
1418
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1419
|
-
});
|
|
1420
|
-
it("reuses primed absence and refusal rows without confusing GET with PUT", async () => {
|
|
1421
|
-
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1422
|
-
const calls = [];
|
|
1423
|
-
let uploaded = false;
|
|
1424
|
-
const vault = {
|
|
1425
|
-
presign: async (input) => {
|
|
1426
|
-
calls.push({ op: input.op, keys: input.keys.map((item) => item.key) });
|
|
1427
|
-
return {
|
|
1428
|
-
results: input.keys.map((item) => {
|
|
1429
|
-
if (input.op === "put") {
|
|
1430
|
-
return { key: item.key, op: "put", url: "https://s3/put" };
|
|
1431
|
-
}
|
|
1432
|
-
if (item.key === "missing.md" && uploaded) {
|
|
1433
|
-
return { key: item.key, op: "get", url: "https://s3/get" };
|
|
1434
|
-
}
|
|
1435
|
-
return item.key === "missing.md"
|
|
1436
|
-
? {
|
|
1437
|
-
key: item.key,
|
|
1438
|
-
op: "get",
|
|
1439
|
-
error: "Object not found",
|
|
1440
|
-
code: "FILES_PRESIGN_NOT_FOUND",
|
|
1441
|
-
}
|
|
1442
|
-
: {
|
|
1443
|
-
key: item.key,
|
|
1444
|
-
op: "get",
|
|
1445
|
-
error: "permission refused",
|
|
1446
|
-
code: "FILES_PRESIGN_FORBIDDEN",
|
|
1447
|
-
};
|
|
1448
|
-
}),
|
|
1449
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1450
|
-
};
|
|
1451
|
-
},
|
|
1452
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1453
|
-
};
|
|
1454
|
-
fetchMock.mockImplementation(async (_url, init) => {
|
|
1455
|
-
if (init?.method === "PUT")
|
|
1456
|
-
uploaded = true;
|
|
1457
|
-
return new Response(null, { status: 200, headers: { etag: '"created"' } });
|
|
1458
|
-
});
|
|
1459
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1460
|
-
await io.prime("get", [{ key: "missing.md" }, { key: "denied.md" }]);
|
|
1461
|
-
expect(await io.headObject("missing.md")).toBeNull();
|
|
1462
|
-
await expect(io.headObject("denied.md")).rejects.toMatchObject({ name: "Forbidden" });
|
|
1463
|
-
await expect(io.headObject("denied.md")).rejects.toMatchObject({ name: "Forbidden" });
|
|
1464
|
-
await io.prime("put", [{ key: "missing.md" }]);
|
|
1465
|
-
await io.putObject({ key: "missing.md", body: Buffer.from("new"), contentType: "text/plain" });
|
|
1466
|
-
expect(await io.headObject("missing.md")).not.toBeNull();
|
|
1467
|
-
expect(calls).toEqual([
|
|
1468
|
-
{ op: "get", keys: ["missing.md", "denied.md"] },
|
|
1469
|
-
{ op: "put", keys: ["missing.md"] },
|
|
1470
|
-
{ op: "get", keys: ["missing.md"] },
|
|
1471
|
-
]);
|
|
1472
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1473
|
-
});
|
|
1474
|
-
it("throws Forbidden when presign denies the key — denial is NOT absence", async () => {
|
|
1475
|
-
// Regression: pre-fix this returned null ("absent"), which made
|
|
1476
|
-
// share.ts's push guard (`if (remoteMeta)`) skip every conflict check
|
|
1477
|
-
// and issue an unconditional PUT — a transient denial mid-pass silently
|
|
1478
|
-
// clobbered newer remote bytes (2026-06-10..12 vault regression storm).
|
|
1479
|
-
const { vault, setPresign } = makeVault();
|
|
1480
|
-
setPresign([{ key: "secret/x", op: "get", error: "forbidden" }]);
|
|
1481
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1482
|
-
await expect(io.headObject("secret/x")).rejects.toMatchObject({
|
|
1483
|
-
name: "Forbidden",
|
|
1484
|
-
});
|
|
1485
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1486
|
-
});
|
|
1487
|
-
it("throws Forbidden on a presigned-GET 403 — same SDK-parity contract", async () => {
|
|
1488
|
-
// Expired presigned URL, expired signing creds, KMS or bucket-policy
|
|
1489
|
-
// denial all surface as 403 on the signed GET. Unknown state — must
|
|
1490
|
-
// never read as "object missing".
|
|
1491
|
-
const { vault, setPresign } = makeVault();
|
|
1492
|
-
setPresign([{ key: "shared/a.md", op: "get", url: "https://s3/get" }]);
|
|
1493
|
-
fetchMock.mockResolvedValue(new Response("AccessDenied", { status: 403 }));
|
|
1494
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1495
|
-
await expect(io.headObject("shared/a.md")).rejects.toMatchObject({
|
|
1496
|
-
name: "Forbidden",
|
|
1497
|
-
});
|
|
1498
|
-
});
|
|
1499
|
-
it("still returns null on a definitive 404 (key truly absent)", async () => {
|
|
1500
|
-
const { vault, setPresign } = makeVault();
|
|
1501
|
-
setPresign([{ key: "gone2", op: "get", url: "https://s3/get" }]);
|
|
1502
|
-
fetchMock.mockResolvedValue(new Response("", { status: 404 }));
|
|
1503
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1504
|
-
expect(await io.headObject("gone2")).toBeNull();
|
|
1505
|
-
});
|
|
1506
|
-
});
|
|
1507
|
-
describe("PresignObjectIO.prime — refusal caching", () => {
|
|
1508
|
-
afterEach(() => vi.restoreAllMocks());
|
|
1509
|
-
it("uses only batch calls for 8,000 refused PUT keys and retries with a new instance", async () => {
|
|
1510
|
-
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1511
|
-
const batchCalls = [];
|
|
1512
|
-
let singleKeyCalls = 0;
|
|
1513
|
-
const vault = {
|
|
1514
|
-
presign: async (input) => {
|
|
1515
|
-
if (input.expiresIn === undefined)
|
|
1516
|
-
singleKeyCalls++;
|
|
1517
|
-
else
|
|
1518
|
-
batchCalls.push({ op: input.op, count: input.keys.length });
|
|
1519
|
-
return {
|
|
1520
|
-
results: input.keys.map((item) => input.op === "get"
|
|
1521
|
-
? {
|
|
1522
|
-
key: item.key,
|
|
1523
|
-
op: "get",
|
|
1524
|
-
error: "Object not found",
|
|
1525
|
-
code: "FILES_PRESIGN_NOT_FOUND",
|
|
1526
|
-
}
|
|
1527
|
-
: {
|
|
1528
|
-
key: item.key,
|
|
1529
|
-
op: "put",
|
|
1530
|
-
error: "forbidden",
|
|
1531
|
-
code: "FILES_PRESIGN_FORBIDDEN",
|
|
1532
|
-
}),
|
|
1533
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1534
|
-
};
|
|
1535
|
-
},
|
|
1536
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1537
|
-
};
|
|
1538
|
-
const keys = Array.from({ length: 8_000 }, (_, index) => ({ key: `write/${index}` }));
|
|
1539
|
-
const firstRun = new PresignObjectIO(vault, COMPANY);
|
|
1540
|
-
await firstRun.prime("get", keys);
|
|
1541
|
-
await firstRun.prime("put", keys);
|
|
1542
|
-
for (const { key } of keys) {
|
|
1543
|
-
expect(await firstRun.headObject(key)).toBeNull();
|
|
1544
|
-
expect(await firstRun.headObject(key)).toBeNull();
|
|
1545
|
-
expect(await firstRun.headObject(key)).toBeNull();
|
|
1546
|
-
await expect(firstRun.putObject({ key, body: Buffer.from("x"), contentType: "text/plain" }))
|
|
1547
|
-
.rejects.toMatchObject({ name: "Forbidden" });
|
|
1548
|
-
}
|
|
1549
|
-
expect(batchCalls).toEqual([
|
|
1550
|
-
...Array.from({ length: 8 }, () => ({ op: "get", count: 1_000 })),
|
|
1551
|
-
...Array.from({ length: 8 }, () => ({ op: "put", count: 1_000 })),
|
|
1552
|
-
]);
|
|
1553
|
-
expect(singleKeyCalls).toBe(0);
|
|
1554
|
-
const secondRun = new PresignObjectIO(vault, COMPANY);
|
|
1555
|
-
await expect(secondRun.putObject({
|
|
1556
|
-
key: "write/0",
|
|
1557
|
-
body: Buffer.from("x"),
|
|
1558
|
-
contentType: "text/plain",
|
|
1559
|
-
})).rejects.toMatchObject({ name: "Forbidden" });
|
|
1560
|
-
expect(singleKeyCalls).toBe(1);
|
|
1561
|
-
});
|
|
1562
|
-
it("uses only batch calls for 13,000 refused GET keys", async () => {
|
|
1563
|
-
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
1564
|
-
const batchCalls = [];
|
|
1565
|
-
let singleKeyCalls = 0;
|
|
1566
|
-
const vault = {
|
|
1567
|
-
presign: async (input) => {
|
|
1568
|
-
if (input.expiresIn === undefined)
|
|
1569
|
-
singleKeyCalls++;
|
|
1570
|
-
else
|
|
1571
|
-
batchCalls.push({ op: input.op, count: input.keys.length });
|
|
1572
|
-
return {
|
|
1573
|
-
results: input.keys.map((item) => ({
|
|
1574
|
-
key: item.key,
|
|
1575
|
-
op: "get",
|
|
1576
|
-
error: "permission refused",
|
|
1577
|
-
code: "FILES_PRESIGN_FORBIDDEN",
|
|
1578
|
-
})),
|
|
1579
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1580
|
-
};
|
|
1581
|
-
},
|
|
1582
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1583
|
-
};
|
|
1584
|
-
const keys = Array.from({ length: 13_000 }, (_, index) => ({ key: `journal/${index}` }));
|
|
1585
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1586
|
-
await io.prime("get", keys);
|
|
1587
|
-
for (const { key } of keys) {
|
|
1588
|
-
await expect(io.headObject(key)).rejects.toMatchObject({ name: "Forbidden" });
|
|
1589
|
-
}
|
|
1590
|
-
expect(batchCalls).toEqual(Array.from({ length: 13 }, () => ({ op: "get", count: 1_000 })));
|
|
1591
|
-
expect(singleKeyCalls).toBe(0);
|
|
1592
|
-
});
|
|
1593
|
-
});
|
|
1594
|
-
/**
|
|
1595
|
-
* Echo vault: presign returns one signed row per requested key (url derived
|
|
1596
|
-
* from op+key) and records call count + total keys, so prime/cache tests can
|
|
1597
|
-
* assert "N files → ceil(N/100) presign calls, then zero".
|
|
1598
|
-
*/
|
|
1599
|
-
function makeEchoVault() {
|
|
1600
|
-
let calls = 0;
|
|
1601
|
-
let totalKeys = 0;
|
|
1602
|
-
const vault = {
|
|
1603
|
-
presign: async (input) => {
|
|
1604
|
-
calls++;
|
|
1605
|
-
totalKeys += input.keys.length;
|
|
1606
|
-
return {
|
|
1607
|
-
results: input.keys.map((k) => ({
|
|
1608
|
-
key: k.key,
|
|
1609
|
-
op: k.op ?? input.op ?? "get",
|
|
1610
|
-
url: `https://signed/${k.op ?? input.op}/${encodeURIComponent(k.key)}`,
|
|
1611
|
-
...(k.op === "put" || input.op === "put"
|
|
1612
|
-
? { headers: { "content-type": "text/plain" } }
|
|
1613
|
-
: {}),
|
|
1614
|
-
expiresIn: input.expiresIn ?? 900,
|
|
1615
|
-
})),
|
|
1616
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1617
|
-
};
|
|
1618
|
-
},
|
|
1619
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1620
|
-
};
|
|
1621
|
-
return {
|
|
1622
|
-
vault,
|
|
1623
|
-
calls: () => calls,
|
|
1624
|
-
totalKeys: () => totalKeys,
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
describe("PresignObjectIO.prime — plan-limit refusals", () => {
|
|
1628
|
-
function refusingVault(mode) {
|
|
1629
|
-
const presignKeys = [];
|
|
1630
|
-
const vault = {
|
|
1631
|
-
presign: async (input) => {
|
|
1632
|
-
presignKeys.push(input.keys.map((k) => k.key));
|
|
1633
|
-
if (mode === "batch-402") {
|
|
1634
|
-
throw vaultErrorFromResponse(402, JSON.stringify(canonicalPlanLimitBody()));
|
|
1635
|
-
}
|
|
1636
|
-
return {
|
|
1637
|
-
results: input.keys.map((k) => companyPresignRefusalRow(k.key)),
|
|
1638
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1639
|
-
};
|
|
1640
|
-
},
|
|
1641
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1642
|
-
};
|
|
1643
|
-
return { vault, presignKeys };
|
|
1644
|
-
}
|
|
1645
|
-
for (const mode of ["batch-402", "per-item"]) {
|
|
1646
|
-
it(`reports a primed ${mode} refusal on upload without presigning the key again`, async () => {
|
|
1647
|
-
const { vault, presignKeys } = refusingVault(mode);
|
|
1648
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1649
|
-
await io.prime("put", [{ key: "a.md" }, { key: "b.md" }]);
|
|
1650
|
-
expect(presignKeys).toEqual([["a.md", "b.md"]]);
|
|
1651
|
-
for (const key of ["a.md", "b.md"]) {
|
|
1652
|
-
await expect(io.putObject({ key, body: Buffer.from("x"), contentType: "text/markdown" })).rejects.toMatchObject({
|
|
1653
|
-
statusCode: 402,
|
|
1654
|
-
path: key,
|
|
1655
|
-
upgradeUrl: PLAN_LIMIT_UPGRADE_URL,
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
// One batch presign for both keys; the uploads never presigned again.
|
|
1659
|
-
expect(presignKeys).toEqual([["a.md", "b.md"]]);
|
|
1660
|
-
// The cached refusal is consumed once: a later upload asks the server.
|
|
1661
|
-
await expect(io.putObject({ key: "a.md", body: Buffer.from("x"), contentType: "text/markdown" })).rejects.toMatchObject({ statusCode: 402 });
|
|
1662
|
-
expect(presignKeys).toEqual([["a.md", "b.md"], ["a.md"]]);
|
|
1663
|
-
});
|
|
1664
|
-
}
|
|
1665
|
-
it("does not cache refusals for a refused GET prime", async () => {
|
|
1666
|
-
const { vault, presignKeys } = refusingVault("batch-402");
|
|
1667
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1668
|
-
await io.prime("get", [{ key: "a.md" }]);
|
|
1669
|
-
await expect(io.putObject({ key: "a.md", body: Buffer.from("x"), contentType: "text/markdown" })).rejects.toMatchObject({ statusCode: 402 });
|
|
1670
|
-
expect(presignKeys).toEqual([["a.md"], ["a.md"]]);
|
|
1671
|
-
});
|
|
1672
|
-
});
|
|
1673
|
-
describe("PresignObjectIO.checkFilesAdmission", () => {
|
|
1674
|
-
afterEach(() => setFilesAdmissionTimeoutMsForTesting(undefined));
|
|
1675
|
-
function admissionVault(answer) {
|
|
1676
|
-
const calls = [];
|
|
1677
|
-
const vault = {
|
|
1678
|
-
presign: async () => ({ results: [], expiresAt: "2099-01-01T00:00:00.000Z" }),
|
|
1679
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1680
|
-
checkFilesAdmission: async (companyUid) => {
|
|
1681
|
-
calls.push(companyUid);
|
|
1682
|
-
return answer();
|
|
1683
|
-
},
|
|
1684
|
-
};
|
|
1685
|
-
return { vault, calls };
|
|
1686
|
-
}
|
|
1687
|
-
it("reports allowed on 200 {allowed:true}", async () => {
|
|
1688
|
-
const { vault, calls } = admissionVault(async () => admissionAllowedBody());
|
|
1689
|
-
await expect(new PresignObjectIO(vault, "cmp_acme").checkFilesAdmission()).resolves.toEqual({
|
|
1690
|
-
state: "allowed",
|
|
1691
|
-
});
|
|
1692
|
-
expect(calls).toEqual(["cmp_acme"]);
|
|
1693
|
-
});
|
|
1694
|
-
it("reports refused with the fresh refusal on a 402 plan-limit body", async () => {
|
|
1695
|
-
const { vault } = admissionVault(async () => {
|
|
1696
|
-
throw vaultErrorFromResponse(402, JSON.stringify(canonicalPlanLimitBody()));
|
|
1697
|
-
});
|
|
1698
|
-
const outcome = await new PresignObjectIO(vault, "cmp_acme").checkFilesAdmission();
|
|
1699
|
-
expect(outcome).toMatchObject({
|
|
1700
|
-
state: "refused",
|
|
1701
|
-
refusal: { upgradeUrl: PLAN_LIMIT_UPGRADE_URL, blocked: "files.create" },
|
|
1702
|
-
});
|
|
1703
|
-
});
|
|
1704
|
-
it.each([
|
|
1705
|
-
["400 from a server without the admission check", 400, admissionUnsupportedBody()],
|
|
1706
|
-
["403", 403, { message: "Forbidden" }],
|
|
1707
|
-
["404", 404, { message: "Not found" }],
|
|
1708
|
-
["503", 503, { message: "unavailable" }],
|
|
1709
|
-
])("reports unknown for %s", async (_label, status, body) => {
|
|
1710
|
-
const { vault } = admissionVault(async () => {
|
|
1711
|
-
throw vaultErrorFromResponse(status, JSON.stringify(body));
|
|
1712
|
-
});
|
|
1713
|
-
await expect(new PresignObjectIO(vault, "cmp_acme").checkFilesAdmission()).resolves.toEqual({
|
|
1714
|
-
state: "unknown",
|
|
1715
|
-
reason: `http-${status}`,
|
|
1716
|
-
});
|
|
1717
|
-
});
|
|
1718
|
-
it("reports unknown on a network error and on a timeout", async () => {
|
|
1719
|
-
const failing = admissionVault(async () => {
|
|
1720
|
-
throw new TypeError("fetch failed");
|
|
1721
|
-
});
|
|
1722
|
-
await expect(new PresignObjectIO(failing.vault, "cmp_acme").checkFilesAdmission()).resolves.toEqual({
|
|
1723
|
-
state: "unknown",
|
|
1724
|
-
reason: "request-failed",
|
|
1725
|
-
});
|
|
1726
|
-
setFilesAdmissionTimeoutMsForTesting(20);
|
|
1727
|
-
const hanging = admissionVault(() => new Promise(() => { }));
|
|
1728
|
-
await expect(new PresignObjectIO(hanging.vault, "cmp_acme").checkFilesAdmission()).resolves.toEqual({
|
|
1729
|
-
state: "unknown",
|
|
1730
|
-
reason: "timeout",
|
|
1731
|
-
});
|
|
1732
|
-
});
|
|
1733
|
-
it("never asks for a non-company vault or a client without the check", async () => {
|
|
1734
|
-
const { vault, calls } = admissionVault(async () => ({ allowed: true }));
|
|
1735
|
-
await expect(new PresignObjectIO(vault, "prs_me").checkFilesAdmission()).resolves.toMatchObject({
|
|
1736
|
-
state: "unknown",
|
|
1737
|
-
});
|
|
1738
|
-
expect(calls).toEqual([]);
|
|
1739
|
-
const { checkFilesAdmission: _drop, ...withoutCheck } = vault;
|
|
1740
|
-
await expect(new PresignObjectIO(withoutCheck, "cmp_acme").checkFilesAdmission()).resolves.toMatchObject({ state: "unknown" });
|
|
1741
|
-
});
|
|
1742
|
-
});
|
|
1743
|
-
describe("PresignObjectIO.prime — batch URL cache", () => {
|
|
1744
|
-
let fetchMock;
|
|
1745
|
-
beforeEach(() => {
|
|
1746
|
-
fetchMock = vi.fn(async () => new Response(Buffer.from("x"), { status: 200 }));
|
|
1747
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1748
|
-
});
|
|
1749
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
1750
|
-
it("chunks at 1000 keys per presign call (the server batch cap)", async () => {
|
|
1751
|
-
const { vault, calls, totalKeys } = makeEchoVault();
|
|
1752
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1753
|
-
const keys = Array.from({ length: 2500 }, (_, i) => ({ key: `k${i}` }));
|
|
1754
|
-
await io.prime("get", keys);
|
|
1755
|
-
expect(calls()).toBe(3); // 1000 + 1000 + 500
|
|
1756
|
-
expect(totalKeys()).toBe(2500);
|
|
1757
|
-
});
|
|
1758
|
-
it("primed getObject reuses the cache — zero extra presign calls", async () => {
|
|
1759
|
-
const { vault, calls } = makeEchoVault();
|
|
1760
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1761
|
-
const keys = Array.from({ length: 5 }, (_, i) => ({ key: `f${i}` }));
|
|
1762
|
-
await io.prime("get", keys);
|
|
1763
|
-
const afterPrime = calls(); // 1 chunk
|
|
1764
|
-
for (const k of keys)
|
|
1765
|
-
await io.getObject(k.key);
|
|
1766
|
-
expect(calls()).toBe(afterPrime); // no new presign calls
|
|
1767
|
-
expect(fetchMock).toHaveBeenCalledTimes(5); // but the bytes were fetched
|
|
1768
|
-
// The fetched URL is the primed (cached) one.
|
|
1769
|
-
expect(fetchMock.mock.calls[0][0]).toBe("https://signed/get/f0");
|
|
1770
|
-
});
|
|
1771
|
-
it("F21: consumes primed GET URLs instead of retaining them", async () => {
|
|
1772
|
-
const { vault, calls } = makeEchoVault();
|
|
1773
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1774
|
-
await io.prime("get", [{ key: "single" }]);
|
|
1775
|
-
const afterPrime = calls();
|
|
1776
|
-
await io.getObject("single");
|
|
1777
|
-
expect(calls()).toBe(afterPrime);
|
|
1778
|
-
await io.getObject("single");
|
|
1779
|
-
expect(calls()).toBe(afterPrime + 1);
|
|
1780
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1781
|
-
});
|
|
1782
|
-
it("R-F21: reuses a primed GET for later HEAD and clears stale primed PUT on fenced PUT", async () => {
|
|
1783
|
-
fetchMock.mockImplementation(async () => (new Response(Buffer.from("body"), {
|
|
1784
|
-
status: 200,
|
|
1785
|
-
headers: {
|
|
1786
|
-
etag: '"etag-1"',
|
|
1787
|
-
"content-length": "4",
|
|
1788
|
-
"last-modified": "Wed, 01 Jan 2026 00:00:00 GMT",
|
|
1789
|
-
},
|
|
1790
|
-
})));
|
|
1791
|
-
const { vault, calls } = makeEchoVault();
|
|
1792
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1793
|
-
await io.prime("get", [{ key: "doc.md" }]);
|
|
1794
|
-
const afterGetPrime = calls();
|
|
1795
|
-
const got = await io.getObject("doc.md");
|
|
1796
|
-
expect(got.body.toString("utf-8")).toBe("body");
|
|
1797
|
-
const head = await io.headObject("doc.md");
|
|
1798
|
-
expect(head?.etag).toBe("etag-1");
|
|
1799
|
-
expect(calls()).toBe(afterGetPrime);
|
|
1800
|
-
expect(fetchMock.mock.calls[0][0]).toBe("https://signed/get/doc.md");
|
|
1801
|
-
expect(fetchMock.mock.calls[1][0]).toBe("https://signed/get/doc.md");
|
|
1802
|
-
await io.prime("put", [
|
|
1803
|
-
{ key: "stale.md", op: "put", contentType: "text/plain" },
|
|
1804
|
-
]);
|
|
1805
|
-
expect(io.hasPrimedPut("stale.md")).toBe(true);
|
|
1806
|
-
const afterPutPrime = calls();
|
|
1807
|
-
await io.putObject({
|
|
1808
|
-
key: "stale.md",
|
|
1809
|
-
body: Buffer.from("new body"),
|
|
1810
|
-
contentType: "text/plain",
|
|
1811
|
-
ifMatch: "old-etag",
|
|
1812
|
-
});
|
|
1813
|
-
expect(calls()).toBe(afterPutPrime + 1);
|
|
1814
|
-
expect(io.hasPrimedPut("stale.md")).toBe(false);
|
|
1815
|
-
});
|
|
1816
|
-
it("primed GET cache also serves headObject (HEAD reuses GET URLs)", async () => {
|
|
1817
|
-
const { vault, calls } = makeEchoVault();
|
|
1818
|
-
fetchMock.mockResolvedValue(new Response(Buffer.from(""), {
|
|
1819
|
-
status: 200,
|
|
1820
|
-
headers: { etag: '"e"', "content-length": "0" },
|
|
1821
|
-
}));
|
|
1822
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1823
|
-
await io.prime("get", [{ key: "doc.md" }]);
|
|
1824
|
-
const afterPrime = calls();
|
|
1825
|
-
const head = await io.headObject("doc.md");
|
|
1826
|
-
expect(head).not.toBeNull();
|
|
1827
|
-
expect(calls()).toBe(afterPrime); // head used the cached GET url, no presign
|
|
1828
|
-
});
|
|
1829
|
-
it("a key NOT primed falls back to a single presign", async () => {
|
|
1830
|
-
const { vault, calls } = makeEchoVault();
|
|
1831
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1832
|
-
await io.prime("get", [{ key: "primed" }]);
|
|
1833
|
-
const afterPrime = calls();
|
|
1834
|
-
await io.getObject("not-primed");
|
|
1835
|
-
expect(calls()).toBe(afterPrime + 1);
|
|
1836
|
-
});
|
|
1837
|
-
it("prime(put) sends checksumSha256 per key from hq-content-sha256 metadata", async () => {
|
|
1838
|
-
const hexA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
1839
|
-
const hexB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
1840
|
-
const { vault, presignCalls } = makeVault();
|
|
1841
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1842
|
-
await io.prime("put", [
|
|
1843
|
-
{
|
|
1844
|
-
key: "a.md",
|
|
1845
|
-
op: "put",
|
|
1846
|
-
contentType: "text/plain",
|
|
1847
|
-
metadata: { "hq-content-sha256": hexA },
|
|
1848
|
-
},
|
|
1849
|
-
{
|
|
1850
|
-
key: "b.md",
|
|
1851
|
-
op: "put",
|
|
1852
|
-
contentType: "text/plain",
|
|
1853
|
-
metadata: { "hq-content-sha256": hexB },
|
|
1854
|
-
},
|
|
1855
|
-
{ key: "c.md", op: "put", contentType: "text/plain" },
|
|
1856
|
-
]);
|
|
1857
|
-
expect(presignCalls).toHaveLength(1);
|
|
1858
|
-
expect(presignCalls[0].op).toBe("put");
|
|
1859
|
-
const keys = presignCalls[0].keys;
|
|
1860
|
-
expect(keys[0]).toMatchObject({
|
|
1861
|
-
key: "a.md",
|
|
1862
|
-
checksumSha256: Buffer.from(hexA, "hex").toString("base64"),
|
|
1863
|
-
});
|
|
1864
|
-
expect(keys[1]).toMatchObject({
|
|
1865
|
-
key: "b.md",
|
|
1866
|
-
checksumSha256: Buffer.from(hexB, "hex").toString("base64"),
|
|
1867
|
-
});
|
|
1868
|
-
expect(keys[2].key).toBe("c.md");
|
|
1869
|
-
expect(keys[2].checksumSha256).toBeUndefined();
|
|
1870
|
-
});
|
|
1871
|
-
it("empty prime is a no-op (no presign call)", async () => {
|
|
1872
|
-
const { vault, calls } = makeEchoVault();
|
|
1873
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1874
|
-
await io.prime("get", []);
|
|
1875
|
-
expect(calls()).toBe(0);
|
|
1876
|
-
});
|
|
1877
|
-
it("a chunk that throws doesn't fail prime; those keys fall back per-file", async () => {
|
|
1878
|
-
let calls = 0;
|
|
1879
|
-
const vault = {
|
|
1880
|
-
presign: async (input) => {
|
|
1881
|
-
calls++;
|
|
1882
|
-
if (calls === 1)
|
|
1883
|
-
throw new Error("transient");
|
|
1884
|
-
return {
|
|
1885
|
-
results: input.keys.map((k) => ({
|
|
1886
|
-
key: k.key,
|
|
1887
|
-
op: "get",
|
|
1888
|
-
url: `https://signed/get/${k.key}`,
|
|
1889
|
-
expiresIn: 900,
|
|
1890
|
-
})),
|
|
1891
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1892
|
-
};
|
|
1893
|
-
},
|
|
1894
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1895
|
-
};
|
|
1896
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1897
|
-
// Single chunk that throws — prime resolves anyway, key stays uncached.
|
|
1898
|
-
await expect(io.prime("get", [{ key: "k" }])).resolves.toBeUndefined();
|
|
1899
|
-
// getObject then single-presigns (call #2 succeeds).
|
|
1900
|
-
await io.getObject("k");
|
|
1901
|
-
expect(calls).toBe(2);
|
|
1902
|
-
});
|
|
1903
|
-
});
|
|
1904
|
-
describe("presignObjectIOFactory — memoization", () => {
|
|
1905
|
-
afterEach(() => setObjectIOFactory(null));
|
|
1906
|
-
it("returns the SAME instance per companyUid so prime + transfer share a cache", () => {
|
|
1907
|
-
const { vault } = makeEchoVault();
|
|
1908
|
-
const factory = presignObjectIOFactory(vault);
|
|
1909
|
-
const a1 = factory(ctx());
|
|
1910
|
-
const a2 = factory(ctx());
|
|
1911
|
-
expect(a1).toBe(a2); // same company → same instance
|
|
1912
|
-
const other = factory({ ...ctx(), uid: "cmp_other" });
|
|
1913
|
-
expect(other).not.toBe(a1); // different company → different instance
|
|
1914
|
-
});
|
|
1915
|
-
});
|
|
1916
|
-
describe("PresignObjectIO — transient-failure retry (SDK parity)", () => {
|
|
1917
|
-
afterEach(() => {
|
|
1918
|
-
vi.useRealTimers();
|
|
1919
|
-
vi.unstubAllGlobals();
|
|
1920
|
-
});
|
|
1921
|
-
it("retries a transient 503 then succeeds", async () => {
|
|
1922
|
-
vi.useFakeTimers();
|
|
1923
|
-
const { vault, setPresign } = makeVault();
|
|
1924
|
-
setPresign([{ key: "k", op: "get", url: "https://s3/get" }]);
|
|
1925
|
-
const fetchMock = vi
|
|
1926
|
-
.fn()
|
|
1927
|
-
.mockResolvedValueOnce(new Response("SlowDown", { status: 503 }))
|
|
1928
|
-
.mockResolvedValueOnce(new Response(Buffer.from("ok"), { status: 200 }));
|
|
1929
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1930
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1931
|
-
const pr = io.getObject("k");
|
|
1932
|
-
await vi.runAllTimersAsync();
|
|
1933
|
-
const res = await pr;
|
|
1934
|
-
expect(res.body.toString("utf-8")).toBe("ok");
|
|
1935
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1936
|
-
});
|
|
1937
|
-
it("retries a network error (fetch throws) then succeeds", async () => {
|
|
1938
|
-
vi.useFakeTimers();
|
|
1939
|
-
const { vault, setPresign } = makeVault();
|
|
1940
|
-
setPresign([{ key: "k", op: "get", url: "https://s3/get" }]);
|
|
1941
|
-
const fetchMock = vi
|
|
1942
|
-
.fn()
|
|
1943
|
-
.mockRejectedValueOnce(new Error("socket hang up"))
|
|
1944
|
-
.mockResolvedValueOnce(new Response(Buffer.from("ok"), { status: 200 }));
|
|
1945
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1946
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1947
|
-
const pr = io.getObject("k");
|
|
1948
|
-
await vi.runAllTimersAsync();
|
|
1949
|
-
const res = await pr;
|
|
1950
|
-
expect(res.body.toString("utf-8")).toBe("ok");
|
|
1951
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1952
|
-
});
|
|
1953
|
-
it("throws after exhausting retries on persistent 503 (initial + 3 retries = 4)", async () => {
|
|
1954
|
-
vi.useFakeTimers();
|
|
1955
|
-
const { vault, setPresign } = makeVault();
|
|
1956
|
-
setPresign([{ key: "k", op: "get", url: "https://s3/get" }]);
|
|
1957
|
-
const fetchMock = vi.fn().mockResolvedValue(new Response("err", { status: 503 }));
|
|
1958
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1959
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1960
|
-
const pr = io.getObject("k").catch((e) => e);
|
|
1961
|
-
await vi.runAllTimersAsync();
|
|
1962
|
-
const err = await pr;
|
|
1963
|
-
expect(String(err)).toMatch(/503/);
|
|
1964
|
-
expect(fetchMock).toHaveBeenCalledTimes(4);
|
|
1965
|
-
});
|
|
1966
|
-
it("does NOT retry a definitive 404", async () => {
|
|
1967
|
-
const { vault, setPresign } = makeVault();
|
|
1968
|
-
setPresign([{ key: "gone", op: "get", url: "https://s3/get" }]);
|
|
1969
|
-
const fetchMock = vi.fn().mockResolvedValue(new Response("", { status: 404 }));
|
|
1970
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
1971
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
1972
|
-
await expect(io.getObject("gone")).rejects.toMatchObject({ name: "NotFound" });
|
|
1973
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1974
|
-
});
|
|
1975
|
-
/**
|
|
1976
|
-
* Pam Beesly (Indigo) 2026-08-13: bulk company pull left primed GETs past
|
|
1977
|
-
* X-Amz-Expires=1800. Pre-fix, 403 "Request has expired" was definitive —
|
|
1978
|
-
* no re-presign — so those keys permanently failed the pass (partial=true).
|
|
1979
|
-
*/
|
|
1980
|
-
it("re-presigns and retries when a primed GET URL returns 403 Request has expired", async () => {
|
|
1981
|
-
vi.useFakeTimers();
|
|
1982
|
-
let presignCalls = 0;
|
|
1983
|
-
const vault = {
|
|
1984
|
-
presign: async (input) => {
|
|
1985
|
-
presignCalls++;
|
|
1986
|
-
const n = presignCalls;
|
|
1987
|
-
return {
|
|
1988
|
-
results: input.keys.map((k) => ({
|
|
1989
|
-
key: k.key,
|
|
1990
|
-
op: "get",
|
|
1991
|
-
url: `https://s3/get-v${n}/${k.key}`,
|
|
1992
|
-
expiresIn: 1800,
|
|
1993
|
-
})),
|
|
1994
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
1995
|
-
};
|
|
1996
|
-
},
|
|
1997
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
1998
|
-
};
|
|
1999
|
-
const expiredXml = '<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
2000
|
-
"<Error><Code>AccessDenied</Code><Message>Request has expired</Message>" +
|
|
2001
|
-
"<X-Amz-Expires>1800</X-Amz-Expires></Error>";
|
|
2002
|
-
const transportGet = vi
|
|
2003
|
-
.fn()
|
|
2004
|
-
.mockResolvedValueOnce({
|
|
2005
|
-
status: 403,
|
|
2006
|
-
headers: new Headers(),
|
|
2007
|
-
body: (async function* () {
|
|
2008
|
-
yield Buffer.from(expiredXml);
|
|
2009
|
-
})(),
|
|
2010
|
-
destroy: () => undefined,
|
|
2011
|
-
})
|
|
2012
|
-
.mockResolvedValueOnce({
|
|
2013
|
-
status: 200,
|
|
2014
|
-
headers: new Headers({ "x-amz-meta-hq-mode": "644" }),
|
|
2015
|
-
body: (async function* () {
|
|
2016
|
-
yield Buffer.from("fresh-bytes");
|
|
2017
|
-
})(),
|
|
2018
|
-
destroy: () => undefined,
|
|
2019
|
-
});
|
|
2020
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
2021
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2022
|
-
await io.prime("get", [{ key: "projects/hq-desktop-installer/prd.json" }]);
|
|
2023
|
-
expect(presignCalls).toBe(1);
|
|
2024
|
-
const pr = io.getObject("projects/hq-desktop-installer/prd.json");
|
|
2025
|
-
await vi.runAllTimersAsync();
|
|
2026
|
-
const res = await pr;
|
|
2027
|
-
expect(res.body.toString("utf-8")).toBe("fresh-bytes");
|
|
2028
|
-
expect(res.metadata).toEqual({ "hq-mode": "644" });
|
|
2029
|
-
// prime + one re-presign after expiry
|
|
2030
|
-
expect(presignCalls).toBe(2);
|
|
2031
|
-
expect(transportGet).toHaveBeenCalledTimes(2);
|
|
2032
|
-
expect(transportGet.mock.calls[0][0]).toContain("get-v1/");
|
|
2033
|
-
expect(transportGet.mock.calls[1][0]).toContain("get-v2/");
|
|
2034
|
-
});
|
|
2035
|
-
/**
|
|
2036
|
-
* Memory-regression guard: getObjectStream must hand back a live body, not
|
|
2037
|
-
* drain the whole object first. Mid-stream EPIPE recovery is the
|
|
2038
|
-
* downloadFile whole-temp seam (s3.test.ts), not a buffer-in-transport loop.
|
|
2039
|
-
*/
|
|
2040
|
-
it("getObjectStream yields without buffering the full object first", async () => {
|
|
2041
|
-
const { vault, setPresign } = makeVault();
|
|
2042
|
-
setPresign([{ key: "projects/large.bin", op: "get", url: "https://s3/get" }]);
|
|
2043
|
-
let secondChunkPulled = false;
|
|
2044
|
-
let resolveSecondChunk;
|
|
2045
|
-
const secondChunkGate = new Promise((resolve) => {
|
|
2046
|
-
resolveSecondChunk = resolve;
|
|
2047
|
-
});
|
|
2048
|
-
const transportGet = vi.fn(async () => ({
|
|
2049
|
-
status: 200,
|
|
2050
|
-
headers: new Headers({ "x-amz-meta-hq-mode": "644" }),
|
|
2051
|
-
body: (async function* () {
|
|
2052
|
-
yield Buffer.from("first-chunk-");
|
|
2053
|
-
// Stall the rest of the body until the test has already received the
|
|
2054
|
-
// first yield — proves getObjectStream returned before full drain.
|
|
2055
|
-
await secondChunkGate;
|
|
2056
|
-
secondChunkPulled = true;
|
|
2057
|
-
yield Buffer.from("second-chunk");
|
|
2058
|
-
})(),
|
|
2059
|
-
destroy: () => undefined,
|
|
2060
|
-
}));
|
|
2061
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
2062
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2063
|
-
// Must resolve without waiting for second-chunk (true streaming).
|
|
2064
|
-
const streamed = await io.getObjectStream("projects/large.bin");
|
|
2065
|
-
expect(streamed.metadata).toEqual({ "hq-mode": "644" });
|
|
2066
|
-
expect(secondChunkPulled).toBe(false);
|
|
2067
|
-
const iterator = streamed.body[Symbol.asyncIterator]();
|
|
2068
|
-
const first = await iterator.next();
|
|
2069
|
-
expect(Buffer.from(first.value).toString("utf-8")).toBe("first-chunk-");
|
|
2070
|
-
expect(secondChunkPulled).toBe(false);
|
|
2071
|
-
resolveSecondChunk();
|
|
2072
|
-
const second = await iterator.next();
|
|
2073
|
-
expect(Buffer.from(second.value).toString("utf-8")).toBe("second-chunk");
|
|
2074
|
-
expect(secondChunkPulled).toBe(true);
|
|
2075
|
-
expect((await iterator.next()).done).toBe(true);
|
|
2076
|
-
expect(transportGet).toHaveBeenCalledTimes(1);
|
|
2077
|
-
});
|
|
2078
|
-
it("surfaces mid-stream EPIPE from getObject without buffering/retrying in the stream seam", async () => {
|
|
2079
|
-
const { vault, setPresign } = makeVault();
|
|
2080
|
-
setPresign([{ key: "projects/mission-control/prd.json", op: "get", url: "https://s3/get" }]);
|
|
2081
|
-
const transportGet = vi.fn(async () => ({
|
|
2082
|
-
status: 200,
|
|
2083
|
-
headers: new Headers(),
|
|
2084
|
-
body: (async function* () {
|
|
2085
|
-
yield Buffer.from("partial-");
|
|
2086
|
-
throw Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
|
|
2087
|
-
})(),
|
|
2088
|
-
destroy: () => undefined,
|
|
2089
|
-
}));
|
|
2090
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
2091
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2092
|
-
// getObject drains the live stream; mid-stream cuts propagate so the
|
|
2093
|
-
// downloadFile seam (not this layer) owns whole-attempt recovery.
|
|
2094
|
-
await expect(io.getObject("projects/mission-control/prd.json")).rejects.toMatchObject({ code: "EPIPE" });
|
|
2095
|
-
expect(transportGet).toHaveBeenCalledTimes(1);
|
|
2096
|
-
});
|
|
2097
|
-
it("does NOT re-presign or retry a non-expired 403 AccessDenied (auth denial)", async () => {
|
|
2098
|
-
const { vault, presignCalls, setPresign } = makeVault();
|
|
2099
|
-
setPresign([{ key: "secret/x", op: "get", url: "https://s3/get" }]);
|
|
2100
|
-
const transportGet = vi.fn(async () => ({
|
|
2101
|
-
status: 403,
|
|
2102
|
-
headers: new Headers(),
|
|
2103
|
-
body: (async function* () {
|
|
2104
|
-
yield Buffer.from("<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>");
|
|
2105
|
-
})(),
|
|
2106
|
-
destroy: () => undefined,
|
|
2107
|
-
}));
|
|
2108
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
2109
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2110
|
-
await expect(io.getObject("secret/x")).rejects.toMatchObject({
|
|
2111
|
-
name: "Forbidden",
|
|
2112
|
-
});
|
|
2113
|
-
expect(presignCalls).toHaveLength(1);
|
|
2114
|
-
expect(transportGet).toHaveBeenCalledTimes(1);
|
|
2115
|
-
});
|
|
2116
|
-
it("does NOT retry a non-transient 400 on presigned GET", async () => {
|
|
2117
|
-
const { vault, setPresign } = makeVault();
|
|
2118
|
-
setPresign([{ key: "k", op: "get", url: "https://s3/get" }]);
|
|
2119
|
-
const transportGet = vi.fn(async () => ({
|
|
2120
|
-
status: 400,
|
|
2121
|
-
headers: new Headers(),
|
|
2122
|
-
body: (async function* () {
|
|
2123
|
-
yield Buffer.from("Bad Request");
|
|
2124
|
-
})(),
|
|
2125
|
-
destroy: () => undefined,
|
|
2126
|
-
}));
|
|
2127
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
2128
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2129
|
-
await expect(io.getObject("k")).rejects.toThrow(/presigned GET failed for k: 400/);
|
|
2130
|
-
expect(transportGet).toHaveBeenCalledTimes(1);
|
|
2131
|
-
});
|
|
2132
|
-
it("headObject re-presigns on 403 Request has expired then returns metadata", async () => {
|
|
2133
|
-
vi.useFakeTimers();
|
|
2134
|
-
let presignCalls = 0;
|
|
2135
|
-
const vault = {
|
|
2136
|
-
presign: async (input) => {
|
|
2137
|
-
presignCalls++;
|
|
2138
|
-
return {
|
|
2139
|
-
results: input.keys.map((k) => ({
|
|
2140
|
-
key: k.key,
|
|
2141
|
-
op: "get",
|
|
2142
|
-
url: `https://s3/head-v${presignCalls}/${k.key}`,
|
|
2143
|
-
})),
|
|
2144
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
2145
|
-
};
|
|
2146
|
-
},
|
|
2147
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
2148
|
-
};
|
|
2149
|
-
const expiredXml = "<Error><Code>AccessDenied</Code><Message>Request has expired</Message>" +
|
|
2150
|
-
"<X-Amz-Expires>1800</X-Amz-Expires></Error>";
|
|
2151
|
-
const transportGet = vi
|
|
2152
|
-
.fn()
|
|
2153
|
-
.mockResolvedValueOnce({
|
|
2154
|
-
status: 403,
|
|
2155
|
-
headers: new Headers(),
|
|
2156
|
-
body: (async function* () {
|
|
2157
|
-
yield Buffer.from(expiredXml);
|
|
2158
|
-
})(),
|
|
2159
|
-
destroy: () => undefined,
|
|
2160
|
-
})
|
|
2161
|
-
.mockResolvedValueOnce({
|
|
2162
|
-
status: 200,
|
|
2163
|
-
headers: new Headers({
|
|
2164
|
-
etag: '"abc"',
|
|
2165
|
-
"content-length": "9",
|
|
2166
|
-
"last-modified": "Wed, 01 Jan 2026 00:00:00 GMT",
|
|
2167
|
-
}),
|
|
2168
|
-
body: (async function* () {
|
|
2169
|
-
yield Buffer.from("not-used");
|
|
2170
|
-
})(),
|
|
2171
|
-
destroy: () => undefined,
|
|
2172
|
-
});
|
|
2173
|
-
setPresignedGetTransportForTesting({ get: transportGet });
|
|
2174
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2175
|
-
const pr = io.headObject("shared/a.md");
|
|
2176
|
-
await vi.runAllTimersAsync();
|
|
2177
|
-
const head = await pr;
|
|
2178
|
-
expect(head).toMatchObject({ etag: "abc", size: 9 });
|
|
2179
|
-
expect(presignCalls).toBe(2);
|
|
2180
|
-
expect(transportGet).toHaveBeenCalledTimes(2);
|
|
2181
|
-
});
|
|
2182
|
-
});
|
|
2183
|
-
import { VaultClientError } from "./vault-client.js";
|
|
2184
|
-
describe("PresignObjectIO — 429 circuit breaker", () => {
|
|
2185
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
2186
|
-
it("trips on a 429: uncached keys fail fast (no further presign), primed keys still work", async () => {
|
|
2187
|
-
let presignCalls = 0;
|
|
2188
|
-
const vault = {
|
|
2189
|
-
presign: async (input) => {
|
|
2190
|
-
presignCalls++;
|
|
2191
|
-
if (input.keys.some((k) => k.key === "primed")) {
|
|
2192
|
-
return {
|
|
2193
|
-
results: input.keys.map((k) => ({
|
|
2194
|
-
key: k.key,
|
|
2195
|
-
op: k.op ?? "get",
|
|
2196
|
-
url: `https://s/${k.key}`,
|
|
2197
|
-
expiresIn: 900,
|
|
2198
|
-
})),
|
|
2199
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
2200
|
-
};
|
|
2201
|
-
}
|
|
2202
|
-
throw new VaultClientError("Rate limit exceeded: 100 requests per hour", 429);
|
|
2203
|
-
},
|
|
2204
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
2205
|
-
};
|
|
2206
|
-
vi.stubGlobal("fetch", vi.fn(async () => new Response(Buffer.from("x"), { status: 200 })));
|
|
2207
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2208
|
-
await io.prime("get", [{ key: "primed" }]); // caches "primed"
|
|
2209
|
-
await expect(io.getObject("uncached1")).rejects.toMatchObject({
|
|
2210
|
-
name: "RateLimited",
|
|
2211
|
-
});
|
|
2212
|
-
const afterTrip = presignCalls;
|
|
2213
|
-
// Second uncached key fails fast — NO new presign call.
|
|
2214
|
-
await expect(io.getObject("uncached2")).rejects.toMatchObject({
|
|
2215
|
-
name: "RateLimited",
|
|
2216
|
-
});
|
|
2217
|
-
expect(presignCalls).toBe(afterTrip);
|
|
2218
|
-
// The primed key still resolves from cache (no presign needed).
|
|
2219
|
-
const r = await io.getObject("primed");
|
|
2220
|
-
expect(r.body.toString("utf-8")).toBe("x");
|
|
2221
|
-
});
|
|
2222
|
-
it("prime stops minting once a chunk 429s (breaker trips, remaining chunks skipped)", async () => {
|
|
2223
|
-
let calls = 0;
|
|
2224
|
-
const vault = {
|
|
2225
|
-
presign: async () => {
|
|
2226
|
-
calls++;
|
|
2227
|
-
throw new VaultClientError("rate", 429);
|
|
2228
|
-
},
|
|
2229
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
2230
|
-
};
|
|
2231
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2232
|
-
// 10 chunks (10k keys @ 1000); concurrency is 4, so at most the first
|
|
2233
|
-
// concurrent batch fires before the breaker trips — far fewer than 10.
|
|
2234
|
-
await io.prime("get", Array.from({ length: 10_000 }, (_, i) => ({ key: `k${i}` })));
|
|
2235
|
-
expect(calls).toBeLessThanOrEqual(4);
|
|
2236
|
-
expect(calls).toBeLessThan(10);
|
|
2237
|
-
});
|
|
2238
|
-
});
|
|
2239
|
-
describe("PresignObjectIO.hasPrimedPut", () => {
|
|
2240
|
-
it("reflects a put prime", async () => {
|
|
2241
|
-
const { vault } = makeEchoVault();
|
|
2242
|
-
const io = new PresignObjectIO(vault, COMPANY);
|
|
2243
|
-
expect(io.hasPrimedPut("k")).toBe(false);
|
|
2244
|
-
await io.prime("put", [
|
|
2245
|
-
{ key: "k", op: "put", contentType: "text/plain", metadata: { "hq-mode": "644" } },
|
|
2246
|
-
]);
|
|
2247
|
-
expect(io.hasPrimedPut("k")).toBe(true);
|
|
2248
|
-
// A different, unprimed key stays false.
|
|
2249
|
-
expect(io.hasPrimedPut("other")).toBe(false);
|
|
2250
|
-
});
|
|
2251
|
-
});
|
|
2252
|
-
describe("presignObjectIOFactory — personal vaults route to S3 SDK", () => {
|
|
2253
|
-
it("routes a person entity (prs_*) to S3SdkObjectIO, a company (cmp_*) to presign", () => {
|
|
2254
|
-
const { vault } = makeEchoVault();
|
|
2255
|
-
const factory = presignObjectIOFactory(vault);
|
|
2256
|
-
const company = factory({ ...ctx(), uid: "cmp_real" });
|
|
2257
|
-
const personal = factory({ ...ctx(), uid: "prs_real" });
|
|
2258
|
-
expect(company).toBeInstanceOf(PresignObjectIO);
|
|
2259
|
-
// Personal vaults use the membership-less vend-self model; the
|
|
2260
|
-
// membership-gated presign endpoints 403 for them, so they stay on STS.
|
|
2261
|
-
expect(personal).toBeInstanceOf(S3SdkObjectIO);
|
|
2262
|
-
});
|
|
2263
|
-
});
|
|
2264
|
-
describe("PresignObjectIO — core HTTP presigned GET", () => {
|
|
2265
|
-
function streamingVault(baseUrl) {
|
|
2266
|
-
return {
|
|
2267
|
-
presign: async (input) => ({
|
|
2268
|
-
results: input.keys.map((key) => ({
|
|
2269
|
-
key: key.key,
|
|
2270
|
-
op: "get",
|
|
2271
|
-
url: `${baseUrl}/${key.key}`,
|
|
2272
|
-
})),
|
|
2273
|
-
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
2274
|
-
}),
|
|
2275
|
-
listFiles: async () => ({ objects: [], cursor: null, truncated: false }),
|
|
2276
|
-
};
|
|
2277
|
-
}
|
|
2278
|
-
it("destroys an early-exit response and rejects a FIN-short body", async () => {
|
|
2279
|
-
setPresignedGetTransportForTesting(null);
|
|
2280
|
-
let responseClosed;
|
|
2281
|
-
const server = http.createServer((req, res) => {
|
|
2282
|
-
if (req.url === "/short") {
|
|
2283
|
-
res.writeHead(200, { "content-length": "10" });
|
|
2284
|
-
res.end("short");
|
|
2285
|
-
return;
|
|
2286
|
-
}
|
|
2287
|
-
res.writeHead(200, { "content-length": "131072" });
|
|
2288
|
-
res.write(Buffer.alloc(65_536, 1));
|
|
2289
|
-
responseClosed = once(res, "close");
|
|
2290
|
-
const finish = setTimeout(() => res.end(Buffer.alloc(65_536, 2)), 1_000);
|
|
2291
|
-
res.once("close", () => clearTimeout(finish));
|
|
2292
|
-
});
|
|
2293
|
-
server.listen(0, "127.0.0.1");
|
|
2294
|
-
await once(server, "listening");
|
|
2295
|
-
const address = server.address();
|
|
2296
|
-
if (!address || typeof address === "string") {
|
|
2297
|
-
throw new Error("test server did not bind a TCP port");
|
|
2298
|
-
}
|
|
2299
|
-
try {
|
|
2300
|
-
const io = new PresignObjectIO(streamingVault(`http://127.0.0.1:${address.port}`), COMPANY);
|
|
2301
|
-
const streamed = await io.getObjectStream("large");
|
|
2302
|
-
const iterator = streamed.body[Symbol.asyncIterator]();
|
|
2303
|
-
expect((await iterator.next()).value).toBeInstanceOf(Uint8Array);
|
|
2304
|
-
await iterator.return?.();
|
|
2305
|
-
if (!responseClosed)
|
|
2306
|
-
throw new Error("response did not start streaming");
|
|
2307
|
-
await expect(responseClosed).resolves.toBeDefined();
|
|
2308
|
-
await expect(io.getObject("short")).rejects.toMatchObject({
|
|
2309
|
-
code: "ERR_HQ_TRUNCATED_RESPONSE",
|
|
2310
|
-
message: expect.stringMatching(/content-length mismatch/),
|
|
2311
|
-
});
|
|
2312
|
-
}
|
|
2313
|
-
finally {
|
|
2314
|
-
server.close();
|
|
2315
|
-
await once(server, "close");
|
|
2316
|
-
}
|
|
2317
|
-
});
|
|
2318
|
-
it.runIf(process.platform === "win32" && /^(22|24)\./.test(process.versions.node))("keeps a child FIN safe after the consumer has applied backpressure", async () => {
|
|
2319
|
-
setPresignedGetTransportForTesting(null);
|
|
2320
|
-
const child = spawn(process.execPath, [
|
|
2321
|
-
"-e",
|
|
2322
|
-
[
|
|
2323
|
-
"const http = require('node:http');",
|
|
2324
|
-
"const body = Buffer.alloc(524288, 7);",
|
|
2325
|
-
"const server = http.createServer((_req, res) => {",
|
|
2326
|
-
" res.writeHead(200, { 'content-length': String(body.length) });",
|
|
2327
|
-
" for (let offset = 0; offset < body.length; offset += 16384) res.write(body.subarray(offset, offset + 16384));",
|
|
2328
|
-
" res.end();",
|
|
2329
|
-
"});",
|
|
2330
|
-
"server.listen(0, '127.0.0.1', () => process.stdout.write(String(server.address().port) + '\\n'));",
|
|
2331
|
-
"process.on('SIGTERM', () => server.close(() => process.exit(0)));",
|
|
2332
|
-
].join("\n"),
|
|
2333
|
-
], { stdio: ["ignore", "pipe", "inherit"] });
|
|
2334
|
-
const port = await new Promise((resolve, reject) => {
|
|
2335
|
-
child.once("error", reject);
|
|
2336
|
-
child.stdout?.once("data", (data) => resolve(Number(data.toString())));
|
|
2337
|
-
child.once("exit", (code) => reject(new Error(`child server exited (${code})`)));
|
|
2338
|
-
});
|
|
2339
|
-
try {
|
|
2340
|
-
const io = new PresignObjectIO(streamingVault(`http://127.0.0.1:${port}`), COMPANY);
|
|
2341
|
-
const streamed = await io.getObjectStream("bulk");
|
|
2342
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2343
|
-
let bytes = 0;
|
|
2344
|
-
for await (const chunk of streamed.body)
|
|
2345
|
-
bytes += chunk.byteLength;
|
|
2346
|
-
expect(bytes).toBe(524_288);
|
|
2347
|
-
}
|
|
2348
|
-
finally {
|
|
2349
|
-
child.kill("SIGTERM");
|
|
2350
|
-
}
|
|
2351
|
-
});
|
|
2352
|
-
});
|
|
2353
|
-
describe("presigned GET node body wrapper", () => {
|
|
2354
|
-
it("keeps a socket error raised before iteration for the reader instead of throwing it", async () => {
|
|
2355
|
-
const { PassThrough } = await import("node:stream");
|
|
2356
|
-
const { checkedNodeBodyForTesting } = await import("./object-io.js");
|
|
2357
|
-
const message = new PassThrough();
|
|
2358
|
-
const unhandled = [];
|
|
2359
|
-
const onUncaught = (error) => { unhandled.push(error); };
|
|
2360
|
-
process.on("uncaughtException", onUncaught);
|
|
2361
|
-
try {
|
|
2362
|
-
const body = checkedNodeBodyForTesting(message, undefined);
|
|
2363
|
-
message.emit("error", new Error("socket reset before iteration"));
|
|
2364
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
2365
|
-
expect(unhandled).toEqual([]);
|
|
2366
|
-
const read = (async () => {
|
|
2367
|
-
for await (const _chunk of body) { /* drain */ }
|
|
2368
|
-
})();
|
|
2369
|
-
await expect(read).rejects.toThrow("socket reset before iteration");
|
|
2370
|
-
}
|
|
2371
|
-
finally {
|
|
2372
|
-
process.off("uncaughtException", onUncaught);
|
|
2373
|
-
}
|
|
2374
|
-
});
|
|
2375
|
-
});
|
|
2376
|
-
//# sourceMappingURL=object-io.test.js.map
|