rampway 0.3.0 → 0.3.1

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.
Files changed (40) hide show
  1. package/README.md +29 -0
  2. package/build/src/velocious/auth.d.ts +17 -0
  3. package/build/src/velocious/auth.d.ts.map +1 -0
  4. package/build/src/velocious/auth.js +30 -0
  5. package/build/src/velocious/auth.js.map +1 -0
  6. package/build/src/velocious/control-plane.d.ts +155 -0
  7. package/build/src/velocious/control-plane.d.ts.map +1 -0
  8. package/build/src/velocious/control-plane.js +275 -0
  9. package/build/src/velocious/control-plane.js.map +1 -0
  10. package/build/src/velocious/controller.d.ts +39 -0
  11. package/build/src/velocious/controller.d.ts.map +1 -0
  12. package/build/src/velocious/controller.js +86 -0
  13. package/build/src/velocious/controller.js.map +1 -0
  14. package/build/src/velocious/executor.d.ts +55 -0
  15. package/build/src/velocious/executor.d.ts.map +1 -0
  16. package/build/src/velocious/executor.js +142 -0
  17. package/build/src/velocious/executor.js.map +1 -0
  18. package/build/src/velocious/index.d.ts +28 -0
  19. package/build/src/velocious/index.d.ts.map +1 -0
  20. package/build/src/velocious/index.js +141 -0
  21. package/build/src/velocious/index.js.map +1 -0
  22. package/build/src/velocious/path-matcher.d.ts +18 -0
  23. package/build/src/velocious/path-matcher.d.ts.map +1 -0
  24. package/build/src/velocious/path-matcher.js +48 -0
  25. package/build/src/velocious/path-matcher.js.map +1 -0
  26. package/build/src/velocious/registry.d.ts +19 -0
  27. package/build/src/velocious/registry.d.ts.map +1 -0
  28. package/build/src/velocious/registry.js +36 -0
  29. package/build/src/velocious/registry.js.map +1 -0
  30. package/build/src/velocious/run-store.d.ts +297 -0
  31. package/build/src/velocious/run-store.d.ts.map +1 -0
  32. package/build/src/velocious/run-store.js +482 -0
  33. package/build/src/velocious/run-store.js.map +1 -0
  34. package/build/src/velocious/sanitize.d.ts +23 -0
  35. package/build/src/velocious/sanitize.d.ts.map +1 -0
  36. package/build/src/velocious/sanitize.js +154 -0
  37. package/build/src/velocious/sanitize.js.map +1 -0
  38. package/docs/docker-development-environment.md +103 -0
  39. package/docs/velocious-deployment-api.md +189 -0
  40. package/package.json +22 -3
@@ -0,0 +1,154 @@
1
+ // @ts-check
2
+ const MAX_STRING_LENGTH = 4000;
3
+ const MAX_DEPTH = 20;
4
+ const MAX_COLLECTION_ENTRIES = 100;
5
+ const MAX_VISITED_VALUES = 1000;
6
+ const MAX_SERIALIZED_BYTES = 64 * 1024;
7
+ const MAX_PREVIEW_LENGTH = 8000;
8
+ const REDACTED = "[redacted]";
9
+ const TRUNCATION_MARKER = "[truncated]";
10
+ const TRUNCATED_VALUE = Symbol("truncated");
11
+ /** @typedef {{visited: number}} SanitizerState */
12
+ /**
13
+ * @param {string} value - Value to redact.
14
+ * @param {string[]} secrets - Exact configured secrets.
15
+ * @returns {string} Redacted and bounded value.
16
+ */
17
+ function redactString(value, secrets) {
18
+ let result = value;
19
+ for (const secret of secrets) {
20
+ if (secret.length > 0)
21
+ result = result.split(secret).join(REDACTED);
22
+ }
23
+ return result.length > MAX_STRING_LENGTH ? `${result.slice(0, MAX_STRING_LENGTH)}…` : result;
24
+ }
25
+ /**
26
+ * Converts an integration value to bounded JSON data while redacting secrets
27
+ * in values and keys. Colliding redacted keys are retained with stable suffixes.
28
+ *
29
+ * @param {unknown} value - Integration value.
30
+ * @param {string[]} secrets - Secrets to remove.
31
+ * @param {number} [depth] - Current depth.
32
+ * @returns {any} JSON-safe value or undefined.
33
+ */
34
+ export function sanitizeAdapterValue(value, secrets, depth = 0) {
35
+ const sanitized = sanitizeValue(value, secrets, depth, { visited: 0 });
36
+ if (sanitized === undefined)
37
+ return undefined;
38
+ if (sanitized === TRUNCATED_VALUE)
39
+ return { reason: "Sanitized output exceeded traversal limits", truncated: true };
40
+ const serialized = JSON.stringify(sanitized);
41
+ if (Buffer.byteLength(serialized) <= MAX_SERIALIZED_BYTES)
42
+ return sanitized;
43
+ return {
44
+ preview: serialized.slice(0, MAX_PREVIEW_LENGTH),
45
+ reason: `Sanitized output exceeded ${MAX_SERIALIZED_BYTES} bytes`,
46
+ truncated: true
47
+ };
48
+ }
49
+ /**
50
+ * @param {unknown} value - Integration value.
51
+ * @param {string[]} secrets - Secrets to remove.
52
+ * @param {number} depth - Current depth.
53
+ * @param {SanitizerState} state - Shared traversal budget.
54
+ * @returns {any} JSON-safe value, undefined, or the truncation sentinel.
55
+ */
56
+ function sanitizeValue(value, secrets, depth, state) {
57
+ if (state.visited >= MAX_VISITED_VALUES || depth > MAX_DEPTH)
58
+ return TRUNCATED_VALUE;
59
+ state.visited++;
60
+ if (value === null)
61
+ return null;
62
+ if (typeof value === "string")
63
+ return redactString(value, secrets);
64
+ if (typeof value === "number")
65
+ return Number.isFinite(value) ? value : null;
66
+ if (typeof value === "boolean")
67
+ return value;
68
+ if (Array.isArray(value)) {
69
+ const result = [];
70
+ let truncated = value.length > MAX_COLLECTION_ENTRIES;
71
+ const contentLimit = truncated ? MAX_COLLECTION_ENTRIES - 1 : value.length;
72
+ for (let index = 0; index < contentLimit; index++) {
73
+ const sanitized = sanitizeValue(value[index], secrets, depth + 1, state);
74
+ if (sanitized === TRUNCATED_VALUE) {
75
+ truncated = true;
76
+ break;
77
+ }
78
+ if (sanitized !== undefined)
79
+ result.push(sanitized);
80
+ }
81
+ if (truncated)
82
+ result.push(TRUNCATION_MARKER);
83
+ return result;
84
+ }
85
+ if (typeof value !== "object")
86
+ return undefined;
87
+ const prototype = Object.getPrototypeOf(value);
88
+ if (prototype !== Object.prototype && prototype !== null)
89
+ return undefined;
90
+ /** @type {Record<string, any>} */
91
+ const result = {};
92
+ const keys = [];
93
+ let truncated = false;
94
+ for (const key in value) {
95
+ if (!Object.hasOwn(value, key))
96
+ continue;
97
+ keys.push(key);
98
+ if (keys.length > MAX_COLLECTION_ENTRIES) {
99
+ truncated = true;
100
+ keys.length = MAX_COLLECTION_ENTRIES - 1;
101
+ break;
102
+ }
103
+ }
104
+ for (const key of keys) {
105
+ const sanitized = sanitizeValue(/** @type {Record<string, unknown>} */ (value)[key], secrets, depth + 1, state);
106
+ if (sanitized === TRUNCATED_VALUE) {
107
+ truncated = true;
108
+ break;
109
+ }
110
+ if (sanitized === undefined)
111
+ continue;
112
+ defineAvailableProperty(result, redactString(key, secrets), sanitized);
113
+ }
114
+ if (truncated)
115
+ defineAvailableProperty(result, TRUNCATION_MARKER, true);
116
+ return result;
117
+ }
118
+ /**
119
+ * Defines a property without dropping a colliding diagnostic key.
120
+ *
121
+ * @param {Record<string, any>} target - Sanitized object.
122
+ * @param {string} key - Desired key.
123
+ * @param {unknown} value - Sanitized value.
124
+ * @returns {void}
125
+ */
126
+ function defineAvailableProperty(target, key, value) {
127
+ let availableKey = key;
128
+ let collision = 2;
129
+ while (Object.hasOwn(target, availableKey)) {
130
+ availableKey = `${key}#${collision}`;
131
+ collision++;
132
+ }
133
+ Object.defineProperty(target, availableKey, {
134
+ configurable: true,
135
+ enumerable: true,
136
+ value,
137
+ writable: true
138
+ });
139
+ }
140
+ /**
141
+ * @param {unknown} error - Deployment failure.
142
+ * @param {string[]} secrets - Secrets to remove.
143
+ * @returns {{message: string, recovery: any}} Safe failure payload.
144
+ */
145
+ export function sanitizeErrorPayload(error, secrets) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ const recovery = error instanceof Error ? /** @type {Error & {recovery?: unknown}} */ (error).recovery : undefined;
148
+ const payload = sanitizeAdapterValue({ message, recovery }, secrets);
149
+ if (payload && typeof payload === "object" && typeof payload.message === "string") {
150
+ return { message: payload.message, recovery: payload.recovery };
151
+ }
152
+ return { message: "Deployment failure diagnostics were truncated", recovery: payload };
153
+ }
154
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../../src/velocious/sanitize.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ # Docker development environment
2
+
3
+ Rampway has one canonical root development image and one persistent Compose service. The image is digest-pinned Ubuntu 26.04 LTS with exact NodeSource Node 24, a checksummed signing key, the universal coding/debugging baseline, and the current OpenCode, Codex, Claude Code, and official Kimi Code CLIs. The build is source-independent: `.dockerignore` closes the build context to the Dockerfile metadata, the image copies no repository content, and project dependencies are installed only inside the running service.
4
+
5
+ ## First use
6
+
7
+ Prerequisites are Docker with Compose v2 and a dedicated UID/GID-1000 development home containing this checkout at `$DEV_HOME_PATH/rampway`.
8
+
9
+ Copy the portable template and confirm its required read-only credential sources:
10
+
11
+ ```bash
12
+ cp .env.example .env
13
+ test -d "${GH_CONFIG_SOURCE_PATH:-$HOME/.config/gh}"
14
+ test -f "${CODEX_AUTH_SOURCE_PATH:-$HOME/.codex/auth.json}"
15
+ ```
16
+
17
+ Set `DEV_HOME_PATH` in `.env` to a dedicated home path containing this checkout at `$DEV_HOME_PATH/rampway`. Do not use an unrelated general host home or recursively change its ownership in the container. The environment template defaults to `/home/dev`; adjust it for any non-default host layout.
18
+
19
+ Run the preparation helper to create the required mount-target parent directories with correct permissions, then start the persistent service and install locked dependencies:
20
+
21
+ ```bash
22
+ scripts/prepare-dev-home.sh
23
+ docker compose up --build --detach dev
24
+ scripts/docker-run.sh npm ci
25
+ ```
26
+
27
+ `scripts/docker-run.sh` uses `docker compose exec -T dev` and preserves every command argument exactly. The service must already be running. Dependencies, caches, the checkout, and ordinary tool state persist naturally under the complete `/home/dev` bind.
28
+
29
+ ## Isolation and mounts
30
+
31
+ Compose declares the default project name `rampway`. Override it with `COMPOSE_PROJECT_NAME` (or `-p`) and use a distinct `DEV_HOME_PATH` for each concurrent instance. Run `scripts/prepare-dev-home.sh` to create required mount-target parent directories before the first start:
32
+
33
+ ```bash
34
+ COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
35
+ scripts/prepare-dev-home.sh
36
+ COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
37
+ docker compose up --build --detach dev
38
+ ```
39
+
40
+ The `dev` service has exactly these mounts:
41
+
42
+ - `${DEV_HOME_PATH:?Set DEV_HOME_PATH in .env}` at `/home/dev`;
43
+ - `${GH_CONFIG_SOURCE_PATH:?Set GH_CONFIG_SOURCE_PATH in .env}` at `/home/dev/.config/gh`, read-only;
44
+ - `${CODEX_AUTH_SOURCE_PATH:?Set CODEX_AUTH_SOURCE_PATH in .env}` at `/home/dev/.codex/auth.json`, read-only.
45
+
46
+ The GitHub config directory is selected through `GH_CONFIG_DIR`. Only the narrow Codex auth file is authorized; SSH keys, Docker sockets, broad host auth mounts, and separate Claude/Kimi/OpenCode secret paths are not. The tracked topology also forbids source split mounts, named source/session volumes, fixed names/ports, privileged mode, sidecars, and container-side ownership fixups.
47
+
48
+ Provider CLI installation is not provider authorization. Threadwire remains parent-owned and is resolved outside this service through bare unversioned `npx threadwire`; Rampway installs no global or project Threadwire package and tracks no provider workspace profile.
49
+
50
+ ## Static validation
51
+
52
+ The dependency-free verifier checks the image pin, NodeSource verification, provider specs, complete apt baseline, non-root identity, exact mounts, fail-closed interpolation, closed build context, forbidden topology, ignored `.env`, and helper contract. Its focused tests also inject representative regressions:
53
+
54
+ ```bash
55
+ npm run verify:docker-dev-environment
56
+ npm run test:docker-dev-environment
57
+ ```
58
+
59
+ ## Docker-capable coordinator validation
60
+
61
+ The bootstrap service has no Docker access. A coordinator must render Compose and build the image cleanly:
62
+
63
+ ```bash
64
+ export COMPOSE_PROJECT_NAME=rampway-canonical-validation
65
+ docker compose config --quiet
66
+ docker compose config
67
+ docker compose build --pull --no-cache dev
68
+ docker compose up --detach dev
69
+ docker compose exec -T dev sh -lc 'test "$HOME" = /home/dev && test "$PWD" = /home/dev/rampway && test "$(id -u)" = 1000 && test "$(id -g)" = 1000'
70
+ docker compose exec -T dev npm ci
71
+ docker compose exec -T dev npm run verify:docker-dev-environment
72
+ ```
73
+
74
+ To prove two-instance isolation, prepare two UID/GID-1000 development homes with Rampway at `<home>/rampway`, start both, and compare their container IDs:
75
+
76
+ ```bash
77
+ COMPOSE_PROJECT_NAME=rampway-isolation-a DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-a docker compose up --build --detach dev
78
+ COMPOSE_PROJECT_NAME=rampway-isolation-b DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-b docker compose up --build --detach dev
79
+ test "$(COMPOSE_PROJECT_NAME=rampway-isolation-a docker compose ps -q dev)" != "$(COMPOSE_PROJECT_NAME=rampway-isolation-b docker compose ps -q dev)"
80
+ ```
81
+
82
+ For fresh cutover, provision a new dedicated home with Rampway already checked out at `<home>/rampway`, then perform a clean image build and dependency install:
83
+
84
+ ```bash
85
+ export RAMPWAY_COMPOSE_PROJECT=rampway-cutover-fresh
86
+ export RAMPWAY_DEV_HOME=/srv/rampway-dev-homes/cutover-fresh
87
+ test -d "$RAMPWAY_DEV_HOME/rampway/.git"
88
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose build --pull --no-cache dev
89
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
90
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T dev npm ci
91
+ ```
92
+
93
+ For exact resume, reuse the retained home and project name without reinstalling or relocating state, then prove the same checkout revision is visible:
94
+
95
+ ```bash
96
+ test -n "$RAMPWAY_COMPOSE_PROJECT"
97
+ test -d "$RAMPWAY_DEV_HOME/rampway/.git"
98
+ export RAMPWAY_EXPECTED_HEAD="$(git -C "$RAMPWAY_DEV_HOME/rampway" rev-parse HEAD)"
99
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
100
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T -e RAMPWAY_EXPECTED_HEAD="$RAMPWAY_EXPECTED_HEAD" dev sh -lc 'test "$PWD" = /home/dev/rampway && test "$(git rev-parse HEAD)" = "$RAMPWAY_EXPECTED_HEAD"'
101
+ ```
102
+
103
+ The obsolete `/workspace` and named-volume state is not imported. Verify the new service before stopping the old project; removal of old named volumes is a separate destructive coordinator decision.
@@ -0,0 +1,189 @@
1
+ # Velocious callable deployment API
2
+
3
+ `rampway/velocious` is Rampway's authenticated deployment control plane for a
4
+ Velocious application. Rampway owns every deployment-specific decision and
5
+ calls its normal deploy lifecycle; Velocious supplies only route mounting,
6
+ request/connection scopes, database persistence, background execution, and
7
+ framework error events.
8
+
9
+ The API accepts no command, path, repository, branch, environment value, log
10
+ request, or arbitrary Git ref. A caller can select only a configured
11
+ project/stage pair, an immutable lowercase 40-character commit SHA reachable
12
+ from that target's approved release branch, and an idempotency key.
13
+ The idempotency key must be a non-secret client-generated value of at most 255
14
+ characters and must not contain any configured bearer token; matching is
15
+ case-sensitive, and such requests are rejected with `422` before run lookup or
16
+ persistence.
17
+
18
+ ## Install and mount
19
+
20
+ Install Velocious in the application that mounts this optional integration.
21
+ Rampway declares it as an optional peer so applications that only use the CLI
22
+ do not acquire a web-framework dependency.
23
+
24
+ ```sh
25
+ npm install rampway velocious
26
+ ```
27
+
28
+ In the application's Velocious routes file:
29
+
30
+ ```js
31
+ import Routes from "velocious/build/src/routes/index.js"
32
+ import RampwayDeploymentApi from "rampway/velocious"
33
+ import deploymentSecrets from "../config/secrets/deployments.js"
34
+
35
+ const routes = new Routes()
36
+
37
+ routes.draw((route) => {
38
+ route.mount(RampwayDeploymentApi, {
39
+ at: "/rampway/deployments",
40
+ accessTokens: deploymentSecrets.rampwayAccessTokens,
41
+ projects: {
42
+ "my-app": {
43
+ stages: {
44
+ production: {
45
+ configPath: "/srv/my-app/control/rampway.config.mjs",
46
+ releaseBranch: "main"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ })
52
+ })
53
+
54
+ export default {routes}
55
+ ```
56
+
57
+ `accessTokens` and every `configPath` are explicit backend configuration.
58
+ Provision the secrets module outside source control using the application's
59
+ normal secret mechanism. There is deliberately no environment-variable
60
+ fallback. `configPath` must be absolute, and its selected Rampway stage must
61
+ use `remote-git`; the deployment target must have Git access to the stage's
62
+ configured repository. Every `releaseBranch` is checked against Git heads-ref
63
+ rules during mounting, so invalid names such as a trailing dot or `.lock`
64
+ component fail at startup rather than during deployment.
65
+
66
+ Mount options:
67
+
68
+ | Option | Required | Meaning |
69
+ | --- | --- | --- |
70
+ | `at` | yes | HTTP mount prefix. |
71
+ | `accessTokens` | yes | One or more non-empty backend-held bearer tokens. |
72
+ | `projects` | yes | Explicit project/stage allowlist with absolute Rampway config paths and approved release branches. |
73
+ | `databaseIdentifier` | no | Velocious database used for runs/audits; default `default`. |
74
+ | `staleRunTimeoutMs` | no | Ownership lease; default `60000`, minimum `4`. |
75
+
76
+ ## Client calls
77
+
78
+ Create a run from a client whose token is also loaded from explicit backend
79
+ secrets (not an environment fallback or command argument):
80
+
81
+ ```js
82
+ import deploymentClientSecrets from "./config/secrets/deployment-client.js"
83
+
84
+ const baseUrl = "https://app.example/rampway/deployments"
85
+ const headers = {
86
+ Authorization: `Bearer ${deploymentClientSecrets.rampwayAccessToken}`,
87
+ "Content-Type": "application/json"
88
+ }
89
+ const createResponse = await fetch(`${baseUrl}/runs`, {
90
+ method: "POST",
91
+ headers,
92
+ body: JSON.stringify({
93
+ project: "my-app",
94
+ stage: "production",
95
+ revision: "0123456789abcdef0123456789abcdef01234567",
96
+ idempotencyKey: "release-2026-08-03-1"
97
+ })
98
+ })
99
+ const created = await createResponse.json()
100
+ ```
101
+
102
+ Read the durable result:
103
+
104
+ ```js
105
+ const runResponse = await fetch(`${baseUrl}/runs/${encodeURIComponent(created.run.id)}`, {headers})
106
+ const {run, current} = await runResponse.json()
107
+ ```
108
+
109
+ Authentication is accepted only from the `Authorization` header. Never place
110
+ the token in a URL or command argument that is logged; clients should obtain it
111
+ from their own secret store and configure the HTTP library's header directly.
112
+ Rampway never includes tokens in response data, run state, audit payloads, Git
113
+ commands, deploy commands, or deployment reports. Adapter values and object
114
+ keys are JSON-normalized and recursively redacted before persistence. Sanitized
115
+ structures are limited to 100 entries per array/object, 1,000 visited values,
116
+ 20 levels, 4,000 characters per string, and 64 KiB total serialized output;
117
+ explicit truncation markers and a bounded redacted preview retain diagnostic
118
+ context.
119
+
120
+ Create responses are bounded:
121
+
122
+ - `202`: created; execution continues in a fresh Velocious connection context.
123
+ - `200`: matching idempotent replay; the original run is returned.
124
+ - `401`: missing or invalid bearer token.
125
+ - `404`: project/stage is not allowlisted.
126
+ - `409`: key conflict, an active deployment, or operator reconciliation is required.
127
+ - `422`: invalid request parameters.
128
+
129
+ Run status is one of `pending`, `running`, `succeeded`, `failed`, `interrupted`,
130
+ or `reconciliation_required`. Failed runs include sanitized recovery state.
131
+ An immutable revision that is not reachable from the approved branch is
132
+ recorded as a sanitized failed run rather than discarded before admission.
133
+ Readback also reports the current/previous Rampway release and active revision.
134
+ If live status cannot be read, the response still returns the durable run with
135
+ `live_status: null` and a sanitized `live_status_error` annotation; the outage
136
+ is also sent to Velocious framework error reporting.
137
+
138
+ ## Execution and recovery guarantees
139
+
140
+ Rampway serializes durable admission before validating reachability on the
141
+ deployment target, so an idempotent retry storm performs the Git fetch only for
142
+ the created run. It then invokes the same `deploy()` used by the CLI: deploy
143
+ lock, immutable remote-Git release, linked state, build/migration tasks, runtime
144
+ handoff, health checks, `current` link, cleanup, and report persistence. It does
145
+ not implement an alternate deployment engine or arbitrary command bridge. If
146
+ failure occurs after the requested revision becomes active, the executor calls
147
+ Rampway's existing rollback lifecycle with the captured prior current release
148
+ as its explicit target and records whether that previous live state was
149
+ restored.
150
+
151
+ Runs and audits are stored in `rampway_deployment_runs` and
152
+ `rampway_deployment_api_audit_events`, scoped by a stable hash of the mount.
153
+ Target and idempotency advisory locks prevent duplicate concurrent admission,
154
+ including one key racing across targets. Pending/running/terminal transitions
155
+ are fenced by mount, run, owner, status, and (for stale reconciliation) the
156
+ observed heartbeat. A renewed live owner therefore cannot be reclaimed, and an
157
+ expired owner cannot overwrite a reconciled run.
158
+
159
+ Pending work whose lease expires becomes `interrupted` and no longer blocks a
160
+ new run. A stale running run, or a known successful activation whose success
161
+ record could not be persisted, becomes `reconciliation_required` and continues
162
+ to block duplicates until an operator verifies live state. Audit persistence
163
+ failure is reported through both Velocious `framework-error` and `all-error`
164
+ events and never strands deployment execution.
165
+ Keyed retries and run-status polling evaluate the lease before returning, so a
166
+ dead owner's stale `pending` or `running` state is reconciled without requiring
167
+ a different-key deployment attempt.
168
+
169
+ ## Token rotation and revocation
170
+
171
+ Use overlapping tokens for zero-downtime rotation:
172
+
173
+ 1. Add a new random token to the backend secrets list and restart/deploy every
174
+ web worker.
175
+ 2. Move every authorized client to the new token.
176
+ 3. Remove the old token from backend secrets and restart/deploy every worker
177
+ again.
178
+ 4. Confirm the old token receives `401` on every instance.
179
+
180
+ For emergency revocation, remove the compromised token and restart all workers
181
+ immediately. Revocation affects new HTTP requests; an already authenticated,
182
+ durably admitted run continues under its owner lease. Inspect the run and audit
183
+ records, reconcile any ambiguous running result, and rotate any repository or
184
+ host credentials independently if they may also be compromised.
185
+
186
+ Do not print tokens while diagnosing authentication. Record only token version
187
+ or fingerprint metadata in an external secret inventory, never the token
188
+ itself. Rampway framework-error payloads deliberately omit the HTTP request so
189
+ listeners cannot accidentally serialize its Authorization header.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampway",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "JS-native Capistrano-style release-directory deployments with Rollbridge integration.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -54,6 +54,14 @@
54
54
  "types": "./build/src/test-helpers.d.ts",
55
55
  "default": "./build/src/test-helpers.js"
56
56
  },
57
+ "./velocious": {
58
+ "types": "./build/src/velocious/index.d.ts",
59
+ "default": "./build/src/velocious/index.js"
60
+ },
61
+ "./velocious.js": {
62
+ "types": "./build/src/velocious/index.d.ts",
63
+ "default": "./build/src/velocious/index.js"
64
+ },
57
65
  "./package.json": "./package.json"
58
66
  },
59
67
  "imports": {
@@ -64,8 +72,10 @@
64
72
  },
65
73
  "scripts": {
66
74
  "test": "npm run build && node --test tests/**/*.test.js",
67
- "lint": "eslint src/ tests/",
75
+ "lint": "eslint src/ tests/ scripts/",
68
76
  "typecheck": "tsc --noEmit",
77
+ "verify:docker-dev-environment": "node scripts/verify-docker-dev-environment.js",
78
+ "test:docker-dev-environment": "node --test tests/docker-dev-environment.test.js",
69
79
  "clean": "node scripts/clean.js",
70
80
  "compile": "tsc -b",
71
81
  "postcompile": "node scripts/ensure-cli-executable.js",
@@ -84,7 +94,16 @@
84
94
  "devDependencies": {
85
95
  "eslint": "^9.39.2",
86
96
  "eslint-plugin-jsdoc": "^61.5.0",
87
- "typescript": "^5.9.3"
97
+ "typescript": "^5.9.3",
98
+ "velocious": "^1.0.574"
99
+ },
100
+ "peerDependencies": {
101
+ "velocious": "^1.0.574"
102
+ },
103
+ "peerDependenciesMeta": {
104
+ "velocious": {
105
+ "optional": true
106
+ }
88
107
  },
89
108
  "files": [
90
109
  "build",