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.
- package/README.md +29 -0
- package/build/src/velocious/auth.d.ts +17 -0
- package/build/src/velocious/auth.d.ts.map +1 -0
- package/build/src/velocious/auth.js +30 -0
- package/build/src/velocious/auth.js.map +1 -0
- package/build/src/velocious/control-plane.d.ts +155 -0
- package/build/src/velocious/control-plane.d.ts.map +1 -0
- package/build/src/velocious/control-plane.js +275 -0
- package/build/src/velocious/control-plane.js.map +1 -0
- package/build/src/velocious/controller.d.ts +39 -0
- package/build/src/velocious/controller.d.ts.map +1 -0
- package/build/src/velocious/controller.js +86 -0
- package/build/src/velocious/controller.js.map +1 -0
- package/build/src/velocious/executor.d.ts +55 -0
- package/build/src/velocious/executor.d.ts.map +1 -0
- package/build/src/velocious/executor.js +142 -0
- package/build/src/velocious/executor.js.map +1 -0
- package/build/src/velocious/index.d.ts +28 -0
- package/build/src/velocious/index.d.ts.map +1 -0
- package/build/src/velocious/index.js +141 -0
- package/build/src/velocious/index.js.map +1 -0
- package/build/src/velocious/path-matcher.d.ts +18 -0
- package/build/src/velocious/path-matcher.d.ts.map +1 -0
- package/build/src/velocious/path-matcher.js +48 -0
- package/build/src/velocious/path-matcher.js.map +1 -0
- package/build/src/velocious/registry.d.ts +19 -0
- package/build/src/velocious/registry.d.ts.map +1 -0
- package/build/src/velocious/registry.js +36 -0
- package/build/src/velocious/registry.js.map +1 -0
- package/build/src/velocious/run-store.d.ts +297 -0
- package/build/src/velocious/run-store.d.ts.map +1 -0
- package/build/src/velocious/run-store.js +482 -0
- package/build/src/velocious/run-store.js.map +1 -0
- package/build/src/velocious/sanitize.d.ts +23 -0
- package/build/src/velocious/sanitize.d.ts.map +1 -0
- package/build/src/velocious/sanitize.js +154 -0
- package/build/src/velocious/sanitize.js.map +1 -0
- package/docs/docker-development-environment.md +103 -0
- package/docs/velocious-deployment-api.md +189 -0
- 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.
|
|
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",
|