@rehearsal-db/core 0.1.0-rc.4 → 0.1.0-rc.8
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/BENCHMARKS.md +18 -0
- package/CHANGELOG.md +106 -1
- package/COMPATIBILITY.md +1 -1
- package/README.md +10 -0
- package/SECURITY.md +4 -3
- package/dist/package.json +10 -3
- package/dist/scripts/runtime/manage_database.mjs +12 -2
- package/dist/src/application/activity_contract.d.mts +28 -0
- package/dist/src/application/activity_contract.mjs +84 -0
- package/dist/src/application/activity_monitor.d.mts +20 -0
- package/dist/src/application/activity_monitor.mjs +191 -0
- package/dist/src/application/port_preflight.d.mts +12 -0
- package/dist/src/application/port_preflight.mjs +71 -0
- package/dist/src/application/process_group.d.mts +13 -0
- package/dist/src/application/process_group.mjs +55 -0
- package/dist/src/application/session.d.mts +11 -3
- package/dist/src/application/session.mjs +62 -20
- package/dist/src/baseline/artifact.d.mts +3 -1
- package/dist/src/baseline/artifact.mjs +41 -2
- package/dist/src/cli/application_command_handlers.mjs +1 -1
- package/dist/src/cli/arguments.d.mts +4 -0
- package/dist/src/cli/arguments.mjs +20 -0
- package/dist/src/cli/baseline_shape_rendering.d.mts +3 -0
- package/dist/src/cli/baseline_shape_rendering.mjs +32 -0
- package/dist/src/cli/basic_command_handlers.mjs +7 -1
- package/dist/src/cli/cli_commands.mjs +2 -0
- package/dist/src/cli/editor_command_handlers.d.mts +2 -0
- package/dist/src/cli/editor_command_handlers.mjs +60 -0
- package/dist/src/cli/guided.mjs +150 -135
- package/dist/src/cli/guided_updates.d.mts +14 -0
- package/dist/src/cli/guided_updates.mjs +148 -0
- package/dist/src/cli/identity_command_handlers.mjs +13 -9
- package/dist/src/cli/inspection_command_handlers.mjs +46 -0
- package/dist/src/cli/project_commands.d.mts +48 -0
- package/dist/src/cli/project_commands.mjs +11 -3
- package/dist/src/cli/rehearsal.mjs +59 -48
- package/dist/src/cli/renderers.d.mts +9 -0
- package/dist/src/cli/renderers.mjs +18 -0
- package/dist/src/cli/runtime_action_handlers.mjs +11 -7
- package/dist/src/cli/runtime_commands.d.mts +15 -204
- package/dist/src/cli/runtime_commands.mjs +143 -175
- package/dist/src/cli/runtime_failure_summary.d.mts +1 -0
- package/dist/src/cli/runtime_failure_summary.mjs +53 -0
- package/dist/src/cli/runtime_planning.d.mts +278 -0
- package/dist/src/cli/runtime_planning.mjs +130 -0
- package/dist/src/cli/runtime_stack_execution.d.mts +6 -0
- package/dist/src/cli/runtime_stack_execution.mjs +31 -0
- package/dist/src/cli/source_command_handlers.mjs +7 -0
- package/dist/src/cli/source_commands.d.mts +81 -0
- package/dist/src/cli/source_commands.mjs +38 -7
- package/dist/src/cli/update_advisory.d.mts +29 -0
- package/dist/src/cli/update_advisory.mjs +415 -0
- package/dist/src/index.d.mts +2 -1
- package/dist/src/index.mjs +0 -1
- package/dist/src/project/configuration.d.mts +1 -1
- package/dist/src/project/configuration_activity.d.mts +11 -0
- package/dist/src/project/configuration_activity.mjs +31 -0
- package/dist/src/project/configuration_application_urls.d.mts +10 -0
- package/dist/src/project/configuration_application_urls.mjs +18 -0
- package/dist/src/project/configuration_contract.d.mts +47 -0
- package/dist/src/project/configuration_contract.mjs +14 -1
- package/dist/src/project/configuration_normalization.d.mts +23 -0
- package/dist/src/project/configuration_normalization.mjs +60 -3
- package/dist/src/project/configuration_profiles.d.mts +12 -0
- package/dist/src/project/configuration_profiles.mjs +33 -0
- package/dist/src/project/configuration_rendering.mjs +45 -0
- package/dist/src/project/configuration_resolution.d.mts +24 -0
- package/dist/src/project/configuration_resolution.mjs +3 -0
- package/dist/src/project/configuration_resource_policy.d.mts +15 -0
- package/dist/src/project/configuration_resource_policy.mjs +65 -0
- package/dist/src/project/configuration_review.d.mts +46 -0
- package/dist/src/project/configuration_review.mjs +228 -0
- package/dist/src/project/configuration_schema.d.mts +2 -0
- package/dist/src/project/configuration_schema.mjs +28 -2
- package/dist/src/project/configuration_validation.d.mts +3 -0
- package/dist/src/project/configuration_validation.mjs +16 -3
- package/dist/src/runtime/cleanup.d.mts +10 -13
- package/dist/src/runtime/cleanup.mjs +64 -77
- package/dist/src/runtime/configuration_binding.d.mts +6 -0
- package/dist/src/runtime/configuration_binding.mjs +10 -0
- package/dist/src/runtime/container_engine_ownership.d.mts +46 -0
- package/dist/src/runtime/container_engine_ownership.mjs +240 -0
- package/dist/src/runtime/environment_classification.d.mts +45 -0
- package/dist/src/runtime/environment_classification.mjs +83 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +2 -1
- package/dist/src/runtime/lifecycle_engine.mjs +7 -2
- package/dist/src/runtime/local_docker_context.d.mts +27 -0
- package/dist/src/runtime/local_docker_context.mjs +92 -0
- package/dist/src/runtime/migration_plan.d.mts +52 -0
- package/dist/src/runtime/migration_plan.mjs +51 -0
- package/dist/src/runtime/plan.d.mts +129 -0
- package/dist/src/runtime/plan.mjs +56 -7
- package/dist/src/runtime/project_review.d.mts +90 -0
- package/dist/src/runtime/project_review.mjs +200 -0
- package/dist/src/runtime/resource_catalog.d.mts +80 -0
- package/dist/src/runtime/resource_catalog.mjs +170 -0
- package/dist/src/runtime/resource_images.d.mts +19 -0
- package/dist/src/runtime/resource_images.mjs +62 -0
- package/dist/src/runtime/resource_inventory.d.mts +93 -0
- package/dist/src/runtime/resource_inventory.mjs +509 -0
- package/dist/src/runtime/resource_policy.d.mts +38 -0
- package/dist/src/runtime/resource_policy.mjs +143 -0
- package/dist/src/runtime/resource_provenance.d.mts +31 -0
- package/dist/src/runtime/resource_provenance.mjs +143 -0
- package/dist/src/runtime/runtime_manager_guards.d.mts +11 -0
- package/dist/src/runtime/runtime_manager_guards.mjs +43 -0
- package/dist/src/runtime/runtime_startup_ownership.d.mts +36 -0
- package/dist/src/runtime/runtime_startup_ownership.mjs +221 -0
- package/dist/src/runtime/stale_environment_plan.d.mts +67 -0
- package/dist/src/runtime/stale_environment_plan.mjs +174 -0
- package/dist/src/runtime/stale_environment_stop.d.mts +12 -0
- package/dist/src/runtime/stale_environment_stop.mjs +98 -0
- package/dist/src/runtime/topology.d.mts +2 -1
- package/dist/src/runtime/topology.mjs +17 -3
- package/dist/src/runtime/topology_application_ports.d.mts +6 -0
- package/dist/src/runtime/topology_application_ports.mjs +38 -0
- package/dist/src/shared/cancellation.mjs +7 -1
- package/dist/src/shared/diagnostics.mjs +4 -0
- package/dist/src/shared/operation_guard.d.mts +6 -0
- package/dist/src/shared/operation_guard.mjs +80 -14
- package/dist/src/shared/operation_progress.d.mts +13 -0
- package/dist/src/shared/operation_progress.mjs +163 -0
- package/dist/src/shared/operation_progress_contract.d.mts +46 -0
- package/dist/src/shared/operation_progress_contract.mjs +135 -0
- package/dist/src/shared/operation_progress_storage.d.mts +24 -0
- package/dist/src/shared/operation_progress_storage.mjs +149 -0
- package/dist/src/shared/owned_process.mjs +9 -0
- package/dist/src/source/baseline.d.mts +3 -1
- package/dist/src/source/baseline.mjs +99 -9
- package/dist/src/source/shape.d.mts +8 -0
- package/dist/src/source/shape.mjs +6 -0
- package/dist/src/source/shape_contract.d.mts +121 -0
- package/dist/src/source/shape_contract.mjs +2 -0
- package/dist/src/source/shape_evidence.d.mts +27 -0
- package/dist/src/source/shape_evidence.mjs +504 -0
- package/dist/src/source/shape_files.d.mts +9 -0
- package/dist/src/source/shape_files.mjs +30 -0
- package/dist/src/source/shape_json.d.mts +7 -0
- package/dist/src/source/shape_json.mjs +75 -0
- package/dist/src/source/shape_keys.d.mts +16 -0
- package/dist/src/source/shape_keys.mjs +84 -0
- package/dist/src/source/shape_policy.d.mts +4 -0
- package/dist/src/source/shape_policy.mjs +361 -0
- package/dist/src/source/shape_preparation.d.mts +20 -0
- package/dist/src/source/shape_preparation.mjs +94 -0
- package/dist/src/source/shape_profiles.d.mts +16 -0
- package/dist/src/source/shape_profiles.mjs +234 -0
- package/dist/src/source/shape_sampler.d.mts +14 -0
- package/dist/src/source/shape_sampler.mjs +175 -0
- package/dist/src/targets/docker_resource_ownership.d.mts +11 -0
- package/dist/src/targets/docker_resource_ownership.mjs +28 -0
- package/dist/src/targets/postgresql.mjs +31 -35
- package/dist/src/targets/postgresql_process.d.mts +10 -1
- package/dist/src/targets/postgresql_process.mjs +37 -4
- package/dist/src/targets/supabase.d.mts +2 -7
- package/dist/src/targets/supabase.mjs +14 -13
- package/dist/src/targets/supabase_environment.d.mts +31 -3
- package/dist/src/targets/supabase_environment.mjs +205 -12
- package/dist/src/targets/supabase_process.d.mts +3 -1
- package/dist/src/targets/supabase_process.mjs +7 -4
- package/dist/src/targets/supabase_service_requirements.d.mts +6 -0
- package/dist/src/targets/supabase_service_requirements.mjs +42 -0
- package/dist/src/targets/supabase_start_failure.d.mts +6 -0
- package/dist/src/targets/supabase_start_failure.mjs +65 -0
- package/docs/architecture.md +13 -0
- package/docs/baselines.md +18 -0
- package/docs/commands.md +76 -14
- package/docs/configuration.md +252 -2
- package/docs/getting-started.md +20 -0
- package/docs/production-source.md +156 -0
- package/docs/releasing.md +116 -15
- package/docs/roadmap.md +6 -0
- package/docs/runtime-lifecycle.md +247 -8
- package/docs/security-model.md +38 -1
- package/docs/stability.md +4 -0
- package/docs/troubleshooting.md +29 -0
- package/docs/tutorial.md +19 -10
- package/docs/typescript.md +90 -7
- package/docs/vscode.md +54 -17
- package/package.json +10 -3
package/BENCHMARKS.md
CHANGED
|
@@ -55,6 +55,24 @@ the same proof remains required in GitHub Actions before public release.
|
|
|
55
55
|
|
|
56
56
|
## Regression policy
|
|
57
57
|
|
|
58
|
+
The installed PostgreSQL scenario also demonstrates a data-specific failure:
|
|
59
|
+
the exact same `SET NOT NULL` migration succeeds on an empty table and fails on a
|
|
60
|
+
synthetic table containing a known null. It then reviews a changed candidate that
|
|
61
|
+
backfills the synthetic value, verifies the corrected constraint through the
|
|
62
|
+
application's HTTP proof, and checks immutable baselines and resource cleanup.
|
|
63
|
+
This is a correctness control, not a timing benchmark or a claim about production
|
|
64
|
+
traffic. The synthetic backfill is not a recommendation for real business data.
|
|
65
|
+
|
|
66
|
+
Maintainers can repeat it with one exact artifact:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
REHEARSAL_CANDIDATE_ARTIFACT=/absolute/path/to/candidate.tgz npm run test:fixture:postgresql
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The result records the artifact and candidate hashes, empty/populated outcomes,
|
|
73
|
+
baseline preservation and cleanup. Do not attribute a candidate's result to a
|
|
74
|
+
different registry artifact merely because both have the same version number.
|
|
75
|
+
|
|
58
76
|
Record a new equivalent sample before a beta release or after changing baseline format,
|
|
59
77
|
streaming, restore, migration application, verification, or local-runtime lifecycle.
|
|
60
78
|
Investigate a repeatable increase greater than 25% in a phase. Do not weaken checks to
|
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,112 @@
|
|
|
3
3
|
All notable Rehearsal package changes will be documented here. The format follows Keep
|
|
4
4
|
a Changelog, and versions will follow Semantic Versioning after the package exists.
|
|
5
5
|
|
|
6
|
-
## Unreleased
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0-rc.8] - Unreleased candidate
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Optional interactive update reminders, with explicit network consent, daily
|
|
13
|
+
opening-session checks, defer/skip/disable choices and exact-version guidance.
|
|
14
|
+
Machine commands remain network-free, custom installations stay manual-only,
|
|
15
|
+
and no update installs automatically.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Invalid positive-integer configuration settings and rejected configuration proposals
|
|
20
|
+
report configuration failures rather than baseline or internal failures. Review
|
|
21
|
+
remains non-writing, and unexpected faults retain their original failure category.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- Separate released VSIX installation, dashboard access, and updates from contributor
|
|
26
|
+
setup and separately approved commercial launch requirements.
|
|
27
|
+
- Clarify that configured database and Storage data remain persistent and that no
|
|
28
|
+
separate temporary Docker-helper ownership or cleanup capability exists today.
|
|
29
|
+
- Explain profile startup and resource measurements without equating cached-image
|
|
30
|
+
container checks with a cold VM or whole-machine memory benchmark.
|
|
31
|
+
|
|
32
|
+
## [0.1.0-rc.7] - 2026-10-15
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Release approval checks accept GitHub form line endings without changing the exact
|
|
37
|
+
owner, environment, operation, run, attempt, source or checksum bindings. Other text
|
|
38
|
+
changes and stale or ambiguous approvals remain refused.
|
|
39
|
+
|
|
40
|
+
rc.6's tag and failed GitHub publication history are preserved; it was not published
|
|
41
|
+
to npm. rc.7 does not add or change database, application or environment behavior.
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- The shared GitHub/npm README visibly links to both the source repository and npm
|
|
46
|
+
package page.
|
|
47
|
+
|
|
48
|
+
## [0.1.0-rc.6] - 2026-10-15
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Release ancestry verification no longer requests a paginated comparison when the
|
|
53
|
+
immutable source tag already points to the exact verified main commit. Older source
|
|
54
|
+
commits still require bounded, unpaged ancestry evidence; incomplete responses remain
|
|
55
|
+
refused. Human approval, provenance and immutable-tag protections are unchanged.
|
|
56
|
+
|
|
57
|
+
rc.5's GitHub candidate and tag are retained, but its npm publication stopped at the
|
|
58
|
+
read-only preflight. rc.6 carries the same product features and a new tested artifact;
|
|
59
|
+
no runtime or environment behavior was added during this publisher correction.
|
|
60
|
+
|
|
61
|
+
## [0.1.0-rc.5] - 2026-10-15
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- Project-owned resource inventory, exact long-running-environment stop review, and
|
|
66
|
+
individually selected unused-image cleanup. Volumes remain tied to reviewed runtime
|
|
67
|
+
discard; other projects and the shared engine are protected.
|
|
68
|
+
- Friendly environment labels, protection, named display profiles, resource quotas and
|
|
69
|
+
review schedules, with validated configuration metadata and fingerprint-bound proposals.
|
|
70
|
+
- Ordered migration review, baseline/privacy/authentication summaries, and bounded
|
|
71
|
+
operation stages and recent receipts through public JSON contracts.
|
|
72
|
+
- Optional target-local resource provenance reports recorded baseline and migration
|
|
73
|
+
digests, restore-writer version, and configured ports without claiming fresh
|
|
74
|
+
verification, listening services or activity. Legacy receipts remain explicitly unknown.
|
|
75
|
+
- Environment classification separately reports ownership-proven creation, recorded
|
|
76
|
+
format/provider compatibility, and age-based review due status. Missing verification
|
|
77
|
+
does not mean a database was never created; differing writer versions do not imply
|
|
78
|
+
incompatible data. Configured sandbox volumes remain persistent after workload stop.
|
|
79
|
+
- Opt-in, bounded graph sampling and measured distribution goals, with immutable
|
|
80
|
+
post-sanitization fidelity evidence and required-goal refusal before activation.
|
|
81
|
+
- Lightweight Supabase service profiles and explicit required-service checks.
|
|
82
|
+
- Opt-in foreground sandbox idle shutdown using an explicit application-owned activity
|
|
83
|
+
contract. Unknown or busy activity preserves workloads; no background daemon is added.
|
|
84
|
+
- A disposable PostgreSQL regression demo showing an empty-database migration pass,
|
|
85
|
+
populated-data refusal, and verified recovery with a reviewed backfill.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- Partial startup and interrupted work clean up only workloads proved owned by the
|
|
90
|
+
operation, preserving existing running environments, data and shared engines.
|
|
91
|
+
- Opted-in owned-engine shutdown checks for newly appeared workloads immediately before
|
|
92
|
+
stopping and preserves a replacement ownership receipt from a later operation.
|
|
93
|
+
- Application startup refuses occupied local ports without replacing another process.
|
|
94
|
+
- Application and profile ports are checked across the complete configured topology;
|
|
95
|
+
configuration proposals can explicitly bind a dependent edit to its parent graph.
|
|
96
|
+
- Application teardown retires its owned process group even after the launcher exits.
|
|
97
|
+
- Supabase startup diagnostics preserve safe service/health categories while withholding
|
|
98
|
+
logs, credentials, URLs and private paths. Unknown causes stay unknown.
|
|
99
|
+
- Public JSON retains strictly false privacy attestations without exposing credentials;
|
|
100
|
+
normalized shape policies can be safely validated repeatedly.
|
|
101
|
+
- Failed migrations retain their baseline/candidate phase instead of inferring it from
|
|
102
|
+
earlier progress; summaries omit Node source excerpts and provider SQL output.
|
|
103
|
+
- Release preparation, publication and tag repair now refuse missing or ambiguous live
|
|
104
|
+
repository/environment controls; prereleases preserve a stable `latest` version even
|
|
105
|
+
when that version has build metadata.
|
|
106
|
+
- Read-only release CI drift-checks an owner-observed settings record when GitHub hides
|
|
107
|
+
bypass metadata. Publication still requires an actual owner deployment review bound
|
|
108
|
+
to the exact run, attempt, source, artifact and settings-record checksums.
|
|
109
|
+
|
|
110
|
+
These changes belong to rc.5, not the existing rc.4 artifact. Scheduled review is
|
|
111
|
+
not unattended cleanup, and container age is not proof of user inactivity.
|
|
7
112
|
|
|
8
113
|
## [0.1.0-rc.4] - 2026-10-11
|
|
9
114
|
|
package/COMPATIBILITY.md
CHANGED
|
@@ -17,7 +17,7 @@ A change to any of those requires either a compatible extension or a deliberate
|
|
|
17
17
|
transition. Rehearsal must never silently reinterpret an old config, baseline, digest,
|
|
18
18
|
or successful verification receipt.
|
|
19
19
|
|
|
20
|
-
The first
|
|
20
|
+
The first `1.0.0` release requires external beta evidence, a support policy, and a settled
|
|
21
21
|
public API. Supporting additional database families, package managers, or operating
|
|
22
22
|
systems is not implied by the 0.x contract.
|
|
23
23
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="https://ddupasquier.github.io/rehearsal-db/">Read the documentation</a> ·
|
|
18
18
|
<a href="https://www.npmjs.com/package/@rehearsal-db/core">View on npm</a> ·
|
|
19
|
+
<a href="https://github.com/Ddupasquier/rehearsal-db">Source on GitHub</a> ·
|
|
19
20
|
<a href="https://github.com/Ddupasquier/rehearsal-db/issues">Report a problem</a>
|
|
20
21
|
</p>
|
|
21
22
|
|
|
@@ -129,6 +130,15 @@ When local disk space gets tight, choose **Clean up disk space** in the guide. R
|
|
|
129
130
|
previews old baseline generations first and keeps runtime or shared-image removal
|
|
130
131
|
explicit.
|
|
131
132
|
|
|
133
|
+
Run `npx rehearsal environments` for a read-only view of this project's owned runtimes,
|
|
134
|
+
container state, current owned-container CPU and memory, Docker resource totals, and
|
|
135
|
+
measurable Colima disk capacity. Rehearsal
|
|
136
|
+
warns before the disk becomes critical and refuses expensive startup without configured
|
|
137
|
+
headroom. PostgreSQL uses one container for SQL-only proofs; Supabase's default
|
|
138
|
+
`application` profile keeps application services while omitting development-only extras.
|
|
139
|
+
Optional idle shutdown can stop only a Colima boot Rehearsal proves it started, and only
|
|
140
|
+
after every container on the shared engine is already stopped.
|
|
141
|
+
|
|
132
142
|
Press `Ctrl+Z` at any guided prompt to exit the whole session. Choose **Get help**, or run
|
|
133
143
|
`npx rehearsal support`, to create a privacy-safe diagnostic report.
|
|
134
144
|
|
package/SECURITY.md
CHANGED
|
@@ -4,9 +4,10 @@ Rehearsal handles sensitive database topology and may process sanitized derivati
|
|
|
4
4
|
production data. Do not include credentials, connection strings, raw data, or a real
|
|
5
5
|
baseline in a public report.
|
|
6
6
|
|
|
7
|
-
Report suspected vulnerabilities through
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Report suspected vulnerabilities through the
|
|
8
|
+
[private security form](https://github.com/Ddupasquier/rehearsal-db/security/advisories/new)
|
|
9
|
+
in this repository. A GitHub account is required. Do not open a
|
|
10
|
+
public issue containing an exploit, secret, hosted target, or source record.
|
|
10
11
|
|
|
11
12
|
Supported security updates cover the latest released 0.x minor during beta. A security
|
|
12
13
|
fix may deliberately tighten configuration or refuse a workflow that an earlier beta
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-db/core",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
"rehearsal": "dist/src/cli/rehearsal.mjs"
|
|
24
24
|
},
|
|
25
25
|
"rehearsalContracts": {
|
|
26
|
-
"projectDiscovery": 1
|
|
26
|
+
"projectDiscovery": 1,
|
|
27
|
+
"resourceInventory": 1,
|
|
28
|
+
"resourceManager": 1,
|
|
29
|
+
"migrationReview": 1,
|
|
30
|
+
"configurationReview": 1,
|
|
31
|
+
"baselineShape": 1
|
|
27
32
|
},
|
|
28
33
|
"exports": {
|
|
29
34
|
".": {
|
|
@@ -38,7 +43,8 @@
|
|
|
38
43
|
"./privacy": "./dist/src/baseline/privacy_engine.mjs",
|
|
39
44
|
"./schema": "./dist/src/baseline/schema_snapshot.mjs",
|
|
40
45
|
"./service-environment": "./dist/src/runtime/service_environment.mjs",
|
|
41
|
-
"./source-access": "./dist/src/source/access.mjs"
|
|
46
|
+
"./source-access": "./dist/src/source/access.mjs",
|
|
47
|
+
"./shape": "./dist/src/source/shape.mjs"
|
|
42
48
|
},
|
|
43
49
|
"files": [
|
|
44
50
|
"dist",
|
|
@@ -69,6 +75,7 @@
|
|
|
69
75
|
"test:fixture:typescript": "npm run build && node scripts/verification/run_scenario.mjs typescript",
|
|
70
76
|
"test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
|
|
71
77
|
"test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
|
|
78
|
+
"test:fixture:resources": "npm run build && node scripts/verification/run_scenario.mjs resources",
|
|
72
79
|
"release:verify": "node scripts/verification/release_gate.mjs",
|
|
73
80
|
"check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
|
|
74
81
|
"check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
|
|
@@ -7,15 +7,25 @@ import { loadRehearsalConfig } from "../../src/project/configuration.mjs";
|
|
|
7
7
|
import { parseRuntimeInvocation } from "../../src/targets/target.mjs";
|
|
8
8
|
import { runRuntimeTarget } from "../../src/targets/driver.mjs";
|
|
9
9
|
import { runWithOperationCancellation } from "../../src/shared/cancellation.mjs";
|
|
10
|
+
import { withRuntimeStartupCleanup } from "../../src/runtime/runtime_startup_ownership.mjs";
|
|
11
|
+
import { guardRuntimeManagerAction } from "../../src/runtime/runtime_manager_guards.mjs";
|
|
10
12
|
const run = async () => {
|
|
11
13
|
const invocation = parseRuntimeInvocation();
|
|
12
|
-
const
|
|
14
|
+
const loaded = await loadRehearsalConfig({
|
|
13
15
|
projectRoot: process.cwd(),
|
|
14
16
|
...(invocation.configPath === undefined
|
|
15
17
|
? {}
|
|
16
18
|
: { configPath: invocation.configPath }),
|
|
17
19
|
});
|
|
18
|
-
await
|
|
20
|
+
await guardRuntimeManagerAction(loaded, invocation.action);
|
|
21
|
+
await withRuntimeStartupCleanup({
|
|
22
|
+
projectRoot: loaded.projectRoot,
|
|
23
|
+
configPath: loaded.configPath,
|
|
24
|
+
config: loaded.config,
|
|
25
|
+
action: invocation.action,
|
|
26
|
+
ownerOperationId: process.env.REHEARSAL_OWNER_OPERATION_ID,
|
|
27
|
+
task: () => runRuntimeTarget(loaded.config.runtime.target),
|
|
28
|
+
});
|
|
19
29
|
};
|
|
20
30
|
if (process.env.REHEARSAL_CANCELLABLE_OPERATION === "1") {
|
|
21
31
|
await runWithOperationCancellation({ task: run });
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Explicit application-owned activity; never infer use from container or editor age. */
|
|
2
|
+
export interface ApplicationActivity {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ApplicationActivitySample {
|
|
6
|
+
readonly schemaVersion: 1;
|
|
7
|
+
readonly busy: boolean;
|
|
8
|
+
readonly activitySequence: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ApplicationIdleWindow {
|
|
11
|
+
readonly sequence: number | null;
|
|
12
|
+
readonly quietSince: number | null;
|
|
13
|
+
readonly lastObservedAt: number | null;
|
|
14
|
+
readonly validSamples: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const emptyApplicationIdleWindow: () => ApplicationIdleWindow;
|
|
17
|
+
export declare const validateApplicationActivityUrl: (value: unknown, readinessUrl: string | null) => string;
|
|
18
|
+
export declare const parseApplicationActivitySample: (value: unknown) => ApplicationActivitySample | null;
|
|
19
|
+
/** `now` is a monotonic clock; missing evidence cannot carry a previous quiet window. */
|
|
20
|
+
export declare const observeApplicationActivity: ({ previous, sample, now, timeoutMs, }: {
|
|
21
|
+
readonly previous: ApplicationIdleWindow;
|
|
22
|
+
readonly sample: ApplicationActivitySample | null;
|
|
23
|
+
readonly now: number;
|
|
24
|
+
readonly timeoutMs: number;
|
|
25
|
+
}) => {
|
|
26
|
+
readonly window: ApplicationIdleWindow;
|
|
27
|
+
readonly idle: boolean;
|
|
28
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const emptyApplicationIdleWindow = () => ({
|
|
2
|
+
sequence: null,
|
|
3
|
+
quietSince: null,
|
|
4
|
+
lastObservedAt: null,
|
|
5
|
+
validSamples: 0,
|
|
6
|
+
});
|
|
7
|
+
export const validateApplicationActivityUrl = (value, readinessUrl) => {
|
|
8
|
+
const message = "Application activity requires a bounded credential-free loopback HTTP(S) URL on the exact readiness origin, with an explicit non-privileged port and no query or fragment.";
|
|
9
|
+
if (typeof value !== "string" ||
|
|
10
|
+
value.length > 500 ||
|
|
11
|
+
/[\x00-\x20\x7f]/u.test(value))
|
|
12
|
+
throw new Error(message);
|
|
13
|
+
let url;
|
|
14
|
+
let readiness;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(value);
|
|
17
|
+
readiness = new URL(readinessUrl ?? "");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
if (!/^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):[0-9]+(?:\/|$)/iu.test(value) ||
|
|
23
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
24
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
|
|
25
|
+
url.username ||
|
|
26
|
+
url.password ||
|
|
27
|
+
url.search ||
|
|
28
|
+
url.hash ||
|
|
29
|
+
Number(url.port) < 1024 ||
|
|
30
|
+
Number(url.port) > 65535 ||
|
|
31
|
+
readiness.username ||
|
|
32
|
+
readiness.password ||
|
|
33
|
+
url.origin !== readiness.origin)
|
|
34
|
+
throw new Error(message);
|
|
35
|
+
return url.href;
|
|
36
|
+
};
|
|
37
|
+
export const parseApplicationActivitySample = (value) => {
|
|
38
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
39
|
+
return null;
|
|
40
|
+
const sample = value;
|
|
41
|
+
if (Object.keys(sample).length !== 3 ||
|
|
42
|
+
Object.keys(sample).some((key) => !["schemaVersion", "busy", "activitySequence"].includes(key)) ||
|
|
43
|
+
sample.schemaVersion !== 1 ||
|
|
44
|
+
typeof sample.busy !== "boolean" ||
|
|
45
|
+
typeof sample.activitySequence !== "number" ||
|
|
46
|
+
!Number.isSafeInteger(sample.activitySequence) ||
|
|
47
|
+
sample.activitySequence < 0)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
busy: sample.busy,
|
|
52
|
+
activitySequence: sample.activitySequence,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** `now` is a monotonic clock; missing evidence cannot carry a previous quiet window. */
|
|
56
|
+
export const observeApplicationActivity = ({ previous, sample, now, timeoutMs, }) => {
|
|
57
|
+
if (!Number.isFinite(now) ||
|
|
58
|
+
!Number.isFinite(timeoutMs) ||
|
|
59
|
+
timeoutMs <= 0 ||
|
|
60
|
+
(previous.lastObservedAt !== null && now < previous.lastObservedAt) ||
|
|
61
|
+
!sample ||
|
|
62
|
+
(previous.sequence !== null && sample.activitySequence < previous.sequence))
|
|
63
|
+
return {
|
|
64
|
+
window: { ...previous, quietSince: null, validSamples: 0 },
|
|
65
|
+
idle: false,
|
|
66
|
+
};
|
|
67
|
+
const changed = previous.sequence === null || sample.activitySequence > previous.sequence;
|
|
68
|
+
const reset = changed || sample.busy || previous.quietSince === null;
|
|
69
|
+
const quietSince = sample.busy ? null : reset ? now : previous.quietSince;
|
|
70
|
+
const validSamples = sample.busy
|
|
71
|
+
? 0
|
|
72
|
+
: reset
|
|
73
|
+
? 1
|
|
74
|
+
: Math.min(previous.validSamples + 1, 2);
|
|
75
|
+
return {
|
|
76
|
+
window: {
|
|
77
|
+
sequence: sample.activitySequence,
|
|
78
|
+
quietSince,
|
|
79
|
+
validSamples,
|
|
80
|
+
lastObservedAt: now,
|
|
81
|
+
},
|
|
82
|
+
idle: quietSince !== null && validSamples >= 2 && now - quietSince >= timeoutMs,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ApplicationActivitySample } from "./activity_contract.mjs";
|
|
2
|
+
export declare const readApplicationActivity: ({ url, signal, fetchImplementation, timeoutMs, }: {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly signal: AbortSignal;
|
|
5
|
+
readonly fetchImplementation?: typeof fetch;
|
|
6
|
+
readonly timeoutMs?: number;
|
|
7
|
+
}) => Promise<ApplicationActivitySample | null>;
|
|
8
|
+
export declare const waitForApplicationIdle: ({ url, readinessUrl, timeoutSeconds, signal, fetchImplementation, now, pollIntervalMs, requestTimeoutMs, onAvailability, }: {
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly readinessUrl: string;
|
|
11
|
+
readonly timeoutSeconds: number;
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
readonly fetchImplementation?: typeof fetch;
|
|
14
|
+
readonly now?: () => number;
|
|
15
|
+
readonly pollIntervalMs?: number;
|
|
16
|
+
readonly requestTimeoutMs?: number;
|
|
17
|
+
readonly onAvailability?: (available: boolean) => void;
|
|
18
|
+
}) => Promise<Readonly<{
|
|
19
|
+
reason: "idle-timeout";
|
|
20
|
+
}>>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** Foreground-only serial polling of an explicitly declared application activity route. */
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { emptyApplicationIdleWindow, observeApplicationActivity, parseApplicationActivitySample, validateApplicationActivityUrl, } from "./activity_contract.mjs";
|
|
4
|
+
const MAXIMUM_ACTIVITY_BYTES = 4096;
|
|
5
|
+
const aborted = () => Object.assign(new Error("Application activity monitoring stopped."), {
|
|
6
|
+
name: "AbortError",
|
|
7
|
+
});
|
|
8
|
+
class ActivityTransportUnresponsive extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Application activity transport did not stop within its bound; idle shutdown is disabled for this foreground session.");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const settleWithin = async (promise, milliseconds) => {
|
|
14
|
+
let timer;
|
|
15
|
+
try {
|
|
16
|
+
return await Promise.race([
|
|
17
|
+
promise.then(() => true, () => true),
|
|
18
|
+
new Promise((resolve) => {
|
|
19
|
+
timer = setTimeout(() => resolve(false), milliseconds);
|
|
20
|
+
}),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (timer)
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const wait = (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
29
|
+
if (signal.aborted) {
|
|
30
|
+
reject(aborted());
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const finish = (cancelled) => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal.removeEventListener("abort", onAbort);
|
|
36
|
+
if (cancelled)
|
|
37
|
+
reject(aborted());
|
|
38
|
+
else
|
|
39
|
+
resolve();
|
|
40
|
+
};
|
|
41
|
+
const onAbort = () => finish(true);
|
|
42
|
+
const timer = setTimeout(() => finish(false), milliseconds);
|
|
43
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
});
|
|
45
|
+
export const readApplicationActivity = async ({ url, signal, fetchImplementation = fetch, timeoutMs = 2000, }) => {
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
throw aborted();
|
|
48
|
+
const started = performance.now();
|
|
49
|
+
const budget = Math.min(Math.max(timeoutMs, 1), 2000);
|
|
50
|
+
const request = new AbortController();
|
|
51
|
+
let rejectOwner = () => undefined;
|
|
52
|
+
const ownerAborted = new Promise((_resolve, reject) => {
|
|
53
|
+
rejectOwner = reject;
|
|
54
|
+
});
|
|
55
|
+
const onAbort = () => {
|
|
56
|
+
request.abort();
|
|
57
|
+
rejectOwner(aborted());
|
|
58
|
+
};
|
|
59
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
60
|
+
const abortTimer = setTimeout(() => request.abort(), Math.max(1, budget - 50));
|
|
61
|
+
let deadlineTimer;
|
|
62
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
63
|
+
deadlineTimer = setTimeout(() => {
|
|
64
|
+
request.abort();
|
|
65
|
+
reject(new ActivityTransportUnresponsive());
|
|
66
|
+
}, budget);
|
|
67
|
+
});
|
|
68
|
+
let reader;
|
|
69
|
+
const read = async () => {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetchImplementation(url, {
|
|
72
|
+
signal: request.signal,
|
|
73
|
+
method: "GET",
|
|
74
|
+
redirect: "error",
|
|
75
|
+
headers: { accept: "application/json", "cache-control": "no-store" },
|
|
76
|
+
});
|
|
77
|
+
if (request.signal.aborted)
|
|
78
|
+
return null;
|
|
79
|
+
if (response.status !== 200 ||
|
|
80
|
+
response.redirected ||
|
|
81
|
+
!/^application\/json(?:\s*;|$)/iu.test(response.headers.get("content-type") ?? "") ||
|
|
82
|
+
!response.body)
|
|
83
|
+
return null;
|
|
84
|
+
const declared = response.headers.get("content-length");
|
|
85
|
+
if (declared !== null &&
|
|
86
|
+
(!/^[0-9]+$/u.test(declared) ||
|
|
87
|
+
Number(declared) > MAXIMUM_ACTIVITY_BYTES))
|
|
88
|
+
return null;
|
|
89
|
+
reader = response.body.getReader();
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let length = 0;
|
|
92
|
+
while (true) {
|
|
93
|
+
if (request.signal.aborted)
|
|
94
|
+
return null;
|
|
95
|
+
const result = await reader.read();
|
|
96
|
+
if (request.signal.aborted)
|
|
97
|
+
return null;
|
|
98
|
+
if (result.done)
|
|
99
|
+
break;
|
|
100
|
+
length += result.value.byteLength;
|
|
101
|
+
if (length > MAXIMUM_ACTIVITY_BYTES)
|
|
102
|
+
return null;
|
|
103
|
+
chunks.push(result.value);
|
|
104
|
+
}
|
|
105
|
+
const bytes = Buffer.concat(chunks, length);
|
|
106
|
+
return parseApplicationActivitySample(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
return await Promise.race([read(), deadline, ownerAborted]);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
clearTimeout(abortTimer);
|
|
117
|
+
if (deadlineTimer)
|
|
118
|
+
clearTimeout(deadlineTimer);
|
|
119
|
+
signal.removeEventListener("abort", onAbort);
|
|
120
|
+
request.abort();
|
|
121
|
+
const cancelled = reader?.cancel().catch(() => undefined);
|
|
122
|
+
const cancellationFinished = cancelled === undefined ||
|
|
123
|
+
(await settleWithin(cancelled, Math.max(1, Math.min(50, budget - (performance.now() - started)))));
|
|
124
|
+
try {
|
|
125
|
+
reader?.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* A stuck transport is disabled below. */
|
|
129
|
+
}
|
|
130
|
+
if (signal.aborted)
|
|
131
|
+
throw aborted();
|
|
132
|
+
if (!cancellationFinished)
|
|
133
|
+
throw new ActivityTransportUnresponsive();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
export const waitForApplicationIdle = async ({ url, readinessUrl, timeoutSeconds, signal, fetchImplementation = fetch, now = () => performance.now(), pollIntervalMs = 5000, requestTimeoutMs = 2000, onAvailability = () => undefined, }) => {
|
|
137
|
+
const endpoint = validateApplicationActivityUrl(url, readinessUrl);
|
|
138
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
|
|
139
|
+
throw new Error("Application idle monitoring requires an explicit positive timeout.");
|
|
140
|
+
let window = emptyApplicationIdleWindow();
|
|
141
|
+
let available;
|
|
142
|
+
while (!signal.aborted) {
|
|
143
|
+
let sample;
|
|
144
|
+
try {
|
|
145
|
+
sample = await readApplicationActivity({
|
|
146
|
+
url: endpoint,
|
|
147
|
+
signal,
|
|
148
|
+
fetchImplementation,
|
|
149
|
+
timeoutMs: requestTimeoutMs,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (signal.aborted)
|
|
154
|
+
throw aborted();
|
|
155
|
+
if (!(error instanceof ActivityTransportUnresponsive))
|
|
156
|
+
throw error;
|
|
157
|
+
onAvailability(false);
|
|
158
|
+
// Do not accumulate new requests behind a transport that ignored abort/cancel.
|
|
159
|
+
await new Promise((_resolve, reject) => {
|
|
160
|
+
const stop = () => reject(aborted());
|
|
161
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
162
|
+
if (signal.aborted) {
|
|
163
|
+
signal.removeEventListener("abort", stop);
|
|
164
|
+
stop();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
throw aborted();
|
|
168
|
+
}
|
|
169
|
+
if (signal.aborted)
|
|
170
|
+
throw aborted();
|
|
171
|
+
const current = observeApplicationActivity({
|
|
172
|
+
previous: window,
|
|
173
|
+
sample,
|
|
174
|
+
now: now(),
|
|
175
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
176
|
+
});
|
|
177
|
+
window = current.window;
|
|
178
|
+
const nextAvailable = sample !== null &&
|
|
179
|
+
(window.sequence === null ||
|
|
180
|
+
sample.activitySequence >= window.sequence) &&
|
|
181
|
+
window.lastObservedAt !== null;
|
|
182
|
+
if (available !== nextAvailable) {
|
|
183
|
+
available = nextAvailable;
|
|
184
|
+
onAvailability(available);
|
|
185
|
+
}
|
|
186
|
+
if (current.idle)
|
|
187
|
+
return { reason: "idle-timeout" };
|
|
188
|
+
await wait(Math.max(1, pollIntervalMs), signal);
|
|
189
|
+
}
|
|
190
|
+
throw aborted();
|
|
191
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LocalApplicationEndpoint {
|
|
2
|
+
readonly host: "localhost" | "127.0.0.1" | "::1";
|
|
3
|
+
readonly port: number;
|
|
4
|
+
}
|
|
5
|
+
type ProbeState = "free" | "occupied" | "unavailable" | "unsupported";
|
|
6
|
+
export type ApplicationPortProbe = (host: "127.0.0.1" | "::1", port: number) => Promise<ProbeState>;
|
|
7
|
+
export declare const parseLocalApplicationEndpoint: (value: unknown) => LocalApplicationEndpoint;
|
|
8
|
+
export declare const assertApplicationPortAvailable: ({ url, probe, }: {
|
|
9
|
+
readonly url: unknown;
|
|
10
|
+
readonly probe?: ApplicationPortProbe;
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Startup-only, bounded loopback listener check; never touches an existing process. */
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
export const parseLocalApplicationEndpoint = (value) => {
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
throw new Error();
|
|
8
|
+
parsed = new URL(value);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error("Application readiness requires a valid local HTTP or HTTPS endpoint.");
|
|
12
|
+
}
|
|
13
|
+
const host = parsed.hostname === "[::1]" ? "::1" : parsed.hostname;
|
|
14
|
+
const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
|
|
15
|
+
if (!["http:", "https:"].includes(parsed.protocol) ||
|
|
16
|
+
!["localhost", "127.0.0.1", "::1"].includes(host) ||
|
|
17
|
+
parsed.username ||
|
|
18
|
+
parsed.password ||
|
|
19
|
+
!Number.isSafeInteger(port) ||
|
|
20
|
+
port < 1 ||
|
|
21
|
+
port > 65535)
|
|
22
|
+
throw new Error("Application readiness requires a credential-free loopback HTTP or HTTPS endpoint.");
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
host: host,
|
|
25
|
+
port,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const probeLoopbackPort = (host, port) => new Promise((resolve) => {
|
|
29
|
+
const socket = createConnection({
|
|
30
|
+
host,
|
|
31
|
+
port,
|
|
32
|
+
family: host === "::1" ? 6 : 4,
|
|
33
|
+
});
|
|
34
|
+
let settled = false;
|
|
35
|
+
const finish = (state) => {
|
|
36
|
+
if (settled)
|
|
37
|
+
return;
|
|
38
|
+
settled = true;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
socket.destroy();
|
|
41
|
+
resolve(state);
|
|
42
|
+
};
|
|
43
|
+
const timer = setTimeout(() => finish("unavailable"), 750);
|
|
44
|
+
socket.once("connect", () => finish("occupied"));
|
|
45
|
+
socket.once("error", (error) => finish(error.code === "ECONNREFUSED"
|
|
46
|
+
? "free"
|
|
47
|
+
: [
|
|
48
|
+
"EAFNOSUPPORT",
|
|
49
|
+
"EPROTONOSUPPORT",
|
|
50
|
+
"ENETUNREACH",
|
|
51
|
+
"EHOSTUNREACH",
|
|
52
|
+
].includes(error.code ?? "") && host === "::1"
|
|
53
|
+
? "unsupported"
|
|
54
|
+
: "unavailable"));
|
|
55
|
+
});
|
|
56
|
+
export const assertApplicationPortAvailable = async ({ url, probe = probeLoopbackPort, }) => {
|
|
57
|
+
const endpoint = parseLocalApplicationEndpoint(url);
|
|
58
|
+
const hosts = endpoint.host === "localhost" ? ["127.0.0.1", "::1"] : [endpoint.host];
|
|
59
|
+
let results;
|
|
60
|
+
try {
|
|
61
|
+
results = await Promise.all(hosts.map((host) => probe(host, endpoint.port)));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error("Application port availability could not be proved. No application was started; diagnose the local endpoint before retrying.");
|
|
65
|
+
}
|
|
66
|
+
if (results.includes("occupied"))
|
|
67
|
+
throw new Error(`Application port ${endpoint.port} is already in use. No application was started or stopped. Stop the existing application yourself, or choose a different reviewed local port and update its start command.`);
|
|
68
|
+
if (results.some((state) => !["free", "unsupported"].includes(state)) ||
|
|
69
|
+
!results.includes("free"))
|
|
70
|
+
throw new Error("Application port availability could not be proved. No application was started; diagnose the local endpoint before retrying.");
|
|
71
|
+
};
|