velocious 1.0.576 → 1.0.577

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 (62) hide show
  1. package/README.md +46 -1
  2. package/build/background-jobs/main.js +13 -2
  3. package/build/background-jobs/scheduler.js +11 -7
  4. package/build/background-jobs/store.js +18 -13
  5. package/build/environment-handlers/node/cli/commands/test.js +2 -0
  6. package/build/environment-handlers/node/source-peer-package.js +172 -0
  7. package/build/routes/resolver.js +3 -50
  8. package/build/src/background-jobs/main.d.ts.map +1 -1
  9. package/build/src/background-jobs/main.js +13 -3
  10. package/build/src/background-jobs/scheduler.d.ts +2 -2
  11. package/build/src/background-jobs/scheduler.d.ts.map +1 -1
  12. package/build/src/background-jobs/scheduler.js +11 -8
  13. package/build/src/background-jobs/store.d.ts +5 -4
  14. package/build/src/background-jobs/store.d.ts.map +1 -1
  15. package/build/src/background-jobs/store.js +17 -12
  16. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  17. package/build/src/environment-handlers/node/cli/commands/test.js +3 -1
  18. package/build/src/environment-handlers/node/source-peer-package.d.ts +46 -0
  19. package/build/src/environment-handlers/node/source-peer-package.d.ts.map +1 -0
  20. package/build/src/environment-handlers/node/source-peer-package.js +148 -0
  21. package/build/src/routes/resolver.d.ts.map +1 -1
  22. package/build/src/routes/resolver.js +4 -45
  23. package/build/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +2 -1
  25. package/scripts/run-tests.js +61 -20
  26. package/scripts/test-browser.js +6 -1
  27. package/src/background-jobs/main.js +13 -2
  28. package/src/background-jobs/scheduler.js +11 -7
  29. package/src/background-jobs/store.js +18 -13
  30. package/src/environment-handlers/node/cli/commands/test.js +2 -0
  31. package/src/environment-handlers/node/source-peer-package.js +172 -0
  32. package/src/routes/resolver.js +3 -50
  33. package/build/deployment-api/controller.js +0 -437
  34. package/build/deployment-api/index.js +0 -210
  35. package/build/deployment-api/path-matcher.js +0 -45
  36. package/build/deployment-api/registry.js +0 -84
  37. package/build/deployment-api/run-store.js +0 -798
  38. package/build/deployment-api/sanitize.js +0 -114
  39. package/build/src/deployment-api/controller.d.ts +0 -117
  40. package/build/src/deployment-api/controller.d.ts.map +0 -1
  41. package/build/src/deployment-api/controller.js +0 -384
  42. package/build/src/deployment-api/index.d.ts +0 -46
  43. package/build/src/deployment-api/index.d.ts.map +0 -1
  44. package/build/src/deployment-api/index.js +0 -178
  45. package/build/src/deployment-api/path-matcher.d.ts +0 -31
  46. package/build/src/deployment-api/path-matcher.d.ts.map +0 -1
  47. package/build/src/deployment-api/path-matcher.js +0 -39
  48. package/build/src/deployment-api/registry.d.ts +0 -106
  49. package/build/src/deployment-api/registry.d.ts.map +0 -1
  50. package/build/src/deployment-api/registry.js +0 -74
  51. package/build/src/deployment-api/run-store.d.ts +0 -402
  52. package/build/src/deployment-api/run-store.d.ts.map +0 -1
  53. package/build/src/deployment-api/run-store.js +0 -711
  54. package/build/src/deployment-api/sanitize.d.ts +0 -27
  55. package/build/src/deployment-api/sanitize.d.ts.map +0 -1
  56. package/build/src/deployment-api/sanitize.js +0 -100
  57. package/src/deployment-api/controller.js +0 -437
  58. package/src/deployment-api/index.js +0 -210
  59. package/src/deployment-api/path-matcher.js +0 -45
  60. package/src/deployment-api/registry.js +0 -84
  61. package/src/deployment-api/run-store.js +0 -798
  62. package/src/deployment-api/sanitize.js +0 -114
@@ -1,210 +0,0 @@
1
- // @ts-check
2
-
3
- import VelociousDeploymentApiController from "./controller.js"
4
- import {matchDeploymentApiPath} from "./path-matcher.js"
5
- import {normalizeMountPrefix} from "../utils/mount-prefix.js"
6
- import {deploymentMountIdentifier, registerDeploymentMount} from "./registry.js"
7
-
8
- const IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9_-]*$/
9
- const MAX_IDENTIFIER_LENGTH = 64
10
- const BRANCH_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*(\/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$/
11
-
12
- /**
13
- * Validates one allowlist identifier (project or stage). Bounded identifiers
14
- * keep every value the API handles safe to pass to the adapter as data.
15
- * @param {?} value - Raw identifier.
16
- * @param {string} name - Human-readable name for error messages.
17
- * @returns {string} - The validated identifier.
18
- */
19
- function validateIdentifier(value, name) {
20
- if (typeof value !== "string" || value.length > MAX_IDENTIFIER_LENGTH || !IDENTIFIER_PATTERN.test(value)) {
21
- throw new Error(`Invalid ${name} identifier: ${String(value)}`)
22
- }
23
-
24
- return value
25
- }
26
-
27
- /**
28
- * Validates the projects allowlist and returns a normalized copy.
29
- * @param {?} projects - Raw projects option.
30
- * @returns {Record<string, import("./registry.js").DeploymentProjectOptions>} - Normalized allowlist.
31
- */
32
- function validateProjects(projects) {
33
- if (!projects || typeof projects !== "object" || Array.isArray(projects)) {
34
- throw new Error("VelociousDeploymentApi requires a 'projects' allowlist object")
35
- }
36
-
37
- // Null-prototype maps so request-controlled names like "__proto__" or
38
- // "constructor" can never resolve inherited properties as allowlisted
39
- // projects/stages.
40
- /** @type {Record<string, import("./registry.js").DeploymentProjectOptions>} */
41
- const normalized = Object.create(null)
42
-
43
- for (const [project, projectOptions] of Object.entries(projects)) {
44
- validateIdentifier(project, "project")
45
-
46
- const stages = /** @type {Record<string, ?>} */ (projectOptions)?.stages
47
-
48
- if (!stages || typeof stages !== "object" || Array.isArray(stages) || Object.keys(stages).length === 0) {
49
- throw new Error(`Project ${project} must allowlist at least one stage`)
50
- }
51
-
52
- /** @type {Record<string, import("./registry.js").DeploymentStageOptions>} */
53
- const normalizedStages = Object.create(null)
54
-
55
- for (const [stage, stageOptions] of Object.entries(stages)) {
56
- validateIdentifier(stage, "stage")
57
-
58
- const releaseBranch = /** @type {Record<string, ?>} */ (stageOptions)?.releaseBranch
59
-
60
- if (typeof releaseBranch !== "string" || !BRANCH_PATTERN.test(releaseBranch) || releaseBranch.includes("..")) {
61
- throw new Error(`Invalid release branch for ${project}/${stage}: ${String(releaseBranch)}`)
62
- }
63
-
64
- normalizedStages[stage] = {releaseBranch}
65
- }
66
-
67
- normalized[project] = {stages: normalizedStages}
68
- }
69
-
70
- if (Object.keys(normalized).length === 0) {
71
- throw new Error("VelociousDeploymentApi requires at least one allowlisted project")
72
- }
73
-
74
- return normalized
75
- }
76
-
77
- /**
78
- * Validates the adapter contract so misconfiguration fails at boot rather than
79
- * on the first request.
80
- * @param {?} adapter - Raw adapter option.
81
- * @returns {import("./registry.js").DeploymentAdapter} - The validated adapter.
82
- */
83
- function validateAdapter(adapter) {
84
- if (!adapter || typeof adapter !== "object") {
85
- throw new Error("VelociousDeploymentApi requires an 'adapter' object")
86
- }
87
-
88
- const candidate = /** @type {Record<string, ?>} */ (adapter)
89
-
90
- for (const methodName of ["validateRevision", "deploy"]) {
91
- if (typeof candidate[methodName] !== "function") {
92
- throw new TypeError(`VelociousDeploymentApi adapter must respond to ${methodName}()`)
93
- }
94
- }
95
-
96
- if (candidate.readStatus !== undefined && typeof candidate.readStatus !== "function") {
97
- throw new TypeError("VelociousDeploymentApi adapter readStatus must be a function when given")
98
- }
99
-
100
- return /** @type {import("./registry.js").DeploymentAdapter} */ (adapter)
101
- }
102
-
103
- /**
104
- * Validates the access tokens. The API fails closed: without at least one
105
- * configured token every request is unauthorized, so mounting without tokens
106
- * is a configuration error.
107
- * @param {?} accessTokens - Raw access tokens option.
108
- * @returns {string[]} - The validated tokens.
109
- */
110
- function validateAccessTokens(accessTokens) {
111
- if (!Array.isArray(accessTokens)) {
112
- throw new Error("VelociousDeploymentApi requires an 'accessTokens' array with at least one token")
113
- }
114
-
115
- for (const token of accessTokens) {
116
- if (typeof token !== "string" || token.length === 0) {
117
- throw new Error("VelociousDeploymentApi access tokens must all be non-empty strings")
118
- }
119
- }
120
-
121
- if (accessTokens.length === 0) {
122
- throw new Error("VelociousDeploymentApi requires at least one non-empty access token; the API fails closed without one")
123
- }
124
-
125
- return [...accessTokens]
126
- }
127
-
128
- const DEFAULT_STALE_RUN_TIMEOUT_MS = 60000
129
-
130
- /**
131
- * Validates the stale-run lease timeout: after this many milliseconds without
132
- * an ownership heartbeat, a later request reconciles pending work as
133
- * interrupted and running work as requiring operator reconciliation.
134
- * @param {?} value - Raw option value.
135
- * @returns {number} - The timeout in milliseconds.
136
- */
137
- function validateStaleRunTimeoutMs(value) {
138
- if (value === undefined) return DEFAULT_STALE_RUN_TIMEOUT_MS
139
-
140
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
141
- throw new Error(`VelociousDeploymentApi staleRunTimeoutMs must be a positive integer, got: ${String(value)}`)
142
- }
143
-
144
- return value
145
- }
146
-
147
- /**
148
- * Mountable authenticated deployment API. A narrowly configured consumer
149
- * mounts it in its routes file and supplies an adapter owned by the deployment
150
- * integration (e.g. Rampway) that performs the actual lock/build/release/
151
- * health/rollback/cleanup work:
152
- *
153
- * ```js
154
- * routes.draw((route) => {
155
- * route.mount(VelociousDeploymentApi, {
156
- * at: "/velocious/deployments",
157
- * accessTokens: [secrets.deploymentApiToken],
158
- * adapter: rampwayDeploymentAdapter,
159
- * projects: {
160
- * "my-app": {stages: {production: {releaseBranch: "master"}}}
161
- * }
162
- * })
163
- * })
164
- * ```
165
- */
166
- export default class VelociousDeploymentApi {
167
- /**
168
- * Registers the deployment API under `at`. Implemented as a route-resolver
169
- * hook so the controller can live inside the velocious package rather than
170
- * the host app's `src/routes` directory. Invoked by the routing layer for
171
- * each `route.mount(...)` registration.
172
- * @param {object} args - Options.
173
- * @param {import("../configuration.js").default} args.configuration - Configuration instance.
174
- * @param {string} args.at - Mount path prefix (e.g. "/velocious/deployments").
175
- * @param {string[]} args.accessTokens - Accepted bearer tokens; requests authenticate with `Authorization: Bearer <token>` only.
176
- * @param {import("./registry.js").DeploymentAdapter} args.adapter - Deployment integration adapter that owns execution.
177
- * @param {Record<string, import("./registry.js").DeploymentProjectOptions>} args.projects - Allowlisted projects/stages with their approved release branches.
178
- * @param {string} [args.databaseIdentifier] - Database identifier the run store reads from.
179
- * @param {number} [args.staleRunTimeoutMs] - Lease timeout after which an active run without a heartbeat is reconciled according to its execution state; defaults to 60000.
180
- * @returns {void} - No return value.
181
- */
182
- static mountInto({accessTokens, adapter, at, configuration, databaseIdentifier, projects, staleRunTimeoutMs}) {
183
- if (!configuration) throw new Error("No configuration given")
184
-
185
- const prefix = normalizeMountPrefix(at)
186
- const options = {
187
- accessTokens: validateAccessTokens(accessTokens),
188
- adapter: validateAdapter(adapter),
189
- databaseIdentifier,
190
- mountIdentifier: deploymentMountIdentifier(prefix),
191
- projects: validateProjects(projects),
192
- staleRunTimeoutMs: validateStaleRunTimeoutMs(staleRunTimeoutMs)
193
- }
194
-
195
- registerDeploymentMount(configuration, prefix, options)
196
-
197
- configuration.addRouteResolverHook(({currentPath, request}) => {
198
- const match = matchDeploymentApiPath({method: request.httpMethod(), path: currentPath, prefix})
199
-
200
- if (!match) return null
201
-
202
- return {
203
- action: match.action,
204
- controller: "velociousDeploymentApi",
205
- controllerClass: VelociousDeploymentApiController,
206
- params: {...match.params, velociousDeploymentMountAt: prefix}
207
- }
208
- })
209
- }
210
- }
@@ -1,45 +0,0 @@
1
- // @ts-check
2
-
3
- import {mountSubPath} from "../utils/mount-prefix.js"
4
-
5
- /**
6
- * @typedef {object} DeploymentApiMatch
7
- * @property {string} action - Controller action to run.
8
- * @property {Record<string, string>} params - Extra params extracted from the path.
9
- */
10
-
11
- /**
12
- * Matches an incoming request against the deployment API routes that live
13
- * under the mount prefix. Returns the controller action plus any extracted
14
- * params, or null when the path/method isn't part of the deployment API.
15
- * @param {object} args - Options.
16
- * @param {string} args.prefix - Normalized mount prefix.
17
- * @param {string} args.path - Request path without query string.
18
- * @param {string} args.method - HTTP method.
19
- * @returns {DeploymentApiMatch | null} - Matched action or null.
20
- */
21
- export function matchDeploymentApiPath({prefix, path, method}) {
22
- const subPath = mountSubPath({prefix, path})
23
-
24
- if (subPath === null) return null
25
-
26
- if (method === "POST" && subPath === "/runs") return {action: "create", params: {}}
27
-
28
- if (method === "GET") {
29
- const runMatch = subPath.match(/^\/runs\/([^/]+)$/)
30
-
31
- if (runMatch) {
32
- let id
33
-
34
- try {
35
- id = decodeURIComponent(runMatch[1])
36
- } catch {
37
- return null
38
- }
39
-
40
- return {action: "show", params: {id}}
41
- }
42
- }
43
-
44
- return null
45
- }
@@ -1,84 +0,0 @@
1
- // @ts-check
2
-
3
- import {createHash} from "node:crypto"
4
-
5
- /**
6
- * DeploymentAdapter type. The configured deployment integration (e.g. Rampway)
7
- * owns the implementation; Velocious only calls this contract. All values are
8
- * passed as data — the framework never builds commands, paths, or refs.
9
- * @typedef {object} DeploymentAdapter
10
- * @property {(args: {configuration: import("../configuration.js").default, project: string, releaseBranch: string, revision: string, stage: string}) => Promise<boolean>} validateRevision - Returns whether the full revision is reachable from the approved release branch.
11
- * @property {(args: {configuration: import("../configuration.js").default, project: string, releaseBranch: string, revision: string, runId: string, stage: string}) => Promise<Record<string, ?>>} deploy - Runs the integration's normal lock/build/migrate/release/health/rollback/cleanup deploy. The report shape is integration-owned and gets JSON-sanitized and redacted before persistence.
12
- * @property {(args: {configuration: import("../configuration.js").default, project: string, stage: string}) => Promise<Record<string, ?>>} [readStatus] - Optional bounded readback of the live release (active revision, current/previous release).
13
- */
14
-
15
- /**
16
- * DeploymentStageOptions type.
17
- * @typedef {object} DeploymentStageOptions
18
- * @property {string} releaseBranch - Approved release branch the requested revision must be reachable from.
19
- */
20
-
21
- /**
22
- * DeploymentProjectOptions type.
23
- * @typedef {object} DeploymentProjectOptions
24
- * @property {Record<string, DeploymentStageOptions>} stages - Allowlisted stages keyed by identifier.
25
- */
26
-
27
- /**
28
- * DeploymentMountOptions type.
29
- * @typedef {object} DeploymentMountOptions
30
- * @property {string[]} accessTokens - Accepted bearer tokens (required, fail closed when empty).
31
- * @property {DeploymentAdapter} adapter - Deployment integration adapter that owns execution.
32
- * @property {string} mountIdentifier - Stable hash of the normalized mount path used to scope persisted state.
33
- * @property {Record<string, DeploymentProjectOptions>} projects - Allowlisted projects keyed by identifier (null-prototype map).
34
- * @property {string} [databaseIdentifier] - Database identifier the run store reads from.
35
- * @property {number} staleRunTimeoutMs - Lease timeout after which an active run without a heartbeat is reconciled according to its execution state.
36
- */
37
-
38
- /**
39
- * Mount options are keyed by configuration so multiple configurations (e.g.
40
- * across tests) never share state, and by mount path so a single configuration
41
- * can mount the API at more than one prefix. Functions in the options (the
42
- * adapter) can't travel through route params, so the controller looks them up
43
- * here using the plain `at` string it receives.
44
- * @type {WeakMap<import("../configuration.js").default, Map<string, DeploymentMountOptions>>}
45
- */
46
- const registry = new WeakMap()
47
-
48
- /**
49
- * Builds the stable, bounded identifier persisted for one normalized mount.
50
- * Hashing keeps composite indexes portable even when the URL prefix is long.
51
- * @param {string} at - Normalized mount path.
52
- * @returns {string} - Stable mount identifier.
53
- */
54
- export function deploymentMountIdentifier(at) {
55
- return createHash("sha256").update(at).digest("hex")
56
- }
57
-
58
- /**
59
- * Registers mount options for a configuration and mount path.
60
- * @param {import("../configuration.js").default} configuration - Configuration instance.
61
- * @param {string} at - Normalized mount path.
62
- * @param {DeploymentMountOptions} options - Mount options.
63
- * @returns {void} - No return value.
64
- */
65
- export function registerDeploymentMount(configuration, at, options) {
66
- let byPath = registry.get(configuration)
67
-
68
- if (!byPath) {
69
- byPath = new Map()
70
- registry.set(configuration, byPath)
71
- }
72
-
73
- byPath.set(at, options)
74
- }
75
-
76
- /**
77
- * Returns the mount options for a configuration and mount path.
78
- * @param {import("../configuration.js").default} configuration - Configuration instance.
79
- * @param {string} at - Normalized mount path.
80
- * @returns {DeploymentMountOptions | undefined} - Mount options if registered.
81
- */
82
- export function getDeploymentMount(configuration, at) {
83
- return registry.get(configuration)?.get(at)
84
- }