velocious 1.0.575 → 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 (66) 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/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  24. package/build/src/sync/sync-envelope-replay-service.js +9 -2
  25. package/build/sync/sync-envelope-replay-service.js +10 -1
  26. package/build/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +2 -1
  28. package/scripts/run-tests.js +61 -20
  29. package/scripts/test-browser.js +6 -1
  30. package/src/background-jobs/main.js +13 -2
  31. package/src/background-jobs/scheduler.js +11 -7
  32. package/src/background-jobs/store.js +18 -13
  33. package/src/environment-handlers/node/cli/commands/test.js +2 -0
  34. package/src/environment-handlers/node/source-peer-package.js +172 -0
  35. package/src/routes/resolver.js +3 -50
  36. package/src/sync/sync-envelope-replay-service.js +10 -1
  37. package/build/deployment-api/controller.js +0 -437
  38. package/build/deployment-api/index.js +0 -210
  39. package/build/deployment-api/path-matcher.js +0 -45
  40. package/build/deployment-api/registry.js +0 -84
  41. package/build/deployment-api/run-store.js +0 -798
  42. package/build/deployment-api/sanitize.js +0 -114
  43. package/build/src/deployment-api/controller.d.ts +0 -117
  44. package/build/src/deployment-api/controller.d.ts.map +0 -1
  45. package/build/src/deployment-api/controller.js +0 -384
  46. package/build/src/deployment-api/index.d.ts +0 -46
  47. package/build/src/deployment-api/index.d.ts.map +0 -1
  48. package/build/src/deployment-api/index.js +0 -178
  49. package/build/src/deployment-api/path-matcher.d.ts +0 -31
  50. package/build/src/deployment-api/path-matcher.d.ts.map +0 -1
  51. package/build/src/deployment-api/path-matcher.js +0 -39
  52. package/build/src/deployment-api/registry.d.ts +0 -106
  53. package/build/src/deployment-api/registry.d.ts.map +0 -1
  54. package/build/src/deployment-api/registry.js +0 -74
  55. package/build/src/deployment-api/run-store.d.ts +0 -402
  56. package/build/src/deployment-api/run-store.d.ts.map +0 -1
  57. package/build/src/deployment-api/run-store.js +0 -711
  58. package/build/src/deployment-api/sanitize.d.ts +0 -27
  59. package/build/src/deployment-api/sanitize.d.ts.map +0 -1
  60. package/build/src/deployment-api/sanitize.js +0 -100
  61. package/src/deployment-api/controller.js +0 -437
  62. package/src/deployment-api/index.js +0 -210
  63. package/src/deployment-api/path-matcher.js +0 -45
  64. package/src/deployment-api/registry.js +0 -84
  65. package/src/deployment-api/run-store.js +0 -798
  66. package/src/deployment-api/sanitize.js +0 -114
@@ -1,27 +0,0 @@
1
- /**
2
- * Converts an integration-owned value (adapter report, live status, audit
3
- * payload fragment) into a bounded JSON-safe structure: plain objects, arrays,
4
- * strings, finite numbers, booleans, and null survive; everything else
5
- * (functions, class instances, undefined) is dropped. String values and object
6
- * keys are redacted and capped in length; deterministic suffixes preserve every
7
- * value when redacted keys collide.
8
- * @param {?} value - Raw value from the integration.
9
- * @param {string[]} secrets - Secret values to redact.
10
- * @param {number} [depth] - Current recursion depth.
11
- * @returns {?} - JSON-safe redacted value, or undefined when the value cannot be represented.
12
- */
13
- export function sanitizeAdapterValue(value: unknown, secrets: string[], depth?: number): unknown;
14
- /**
15
- * Builds the sanitized failure payload persisted for a failed run. The message
16
- * is redacted; an integration-provided plain-object `error.recovery` (e.g.
17
- * Rampway reporting restoration of the previous release) is included after
18
- * sanitization.
19
- * @param {?} error - Thrown error from the adapter.
20
- * @param {string[]} secrets - Secret values to redact.
21
- * @returns {{message: string, recovery: (? | undefined)}} - Sanitized failure payload.
22
- */
23
- export function sanitizeErrorPayload(error: unknown, secrets: string[]): {
24
- message: string;
25
- recovery: (unknown | undefined);
26
- };
27
- //# sourceMappingURL=sanitize.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../../src/deployment-api/sanitize.js"],"names":[],"mappings":"AA2BA;;;;;;;;;;;GAWG;AACH,4CALW,OAAC,WACD,MAAM,EAAE,UACR,MAAM,GACJ,OAAC,CA2Db;AAED;;;;;;;;GAQG;AACH,4CAJW,OAAC,WACD,MAAM,EAAE,GACN;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,OAAC,GAAG,SAAS,CAAC,CAAA;CAAC,CAQxD"}
@@ -1,100 +0,0 @@
1
- // @ts-check
2
- const MAX_STRING_LENGTH = 4000;
3
- const MAX_DEPTH = 20;
4
- const REDACTED = "[redacted]";
5
- /**
6
- * Replaces every occurrence of each configured secret in a string so tokens
7
- * never leak into persisted run state, audit payloads, or API responses.
8
- * @param {string} value - Raw string.
9
- * @param {string[]} secrets - Secret values to redact.
10
- * @returns {string} - Redacted string.
11
- */
12
- function redactString(value, secrets) {
13
- let result = value;
14
- for (const secret of secrets) {
15
- if (secret.length > 0)
16
- result = result.split(secret).join(REDACTED);
17
- }
18
- if (result.length > MAX_STRING_LENGTH) {
19
- result = `${result.slice(0, MAX_STRING_LENGTH)}…`;
20
- }
21
- return result;
22
- }
23
- /**
24
- * Converts an integration-owned value (adapter report, live status, audit
25
- * payload fragment) into a bounded JSON-safe structure: plain objects, arrays,
26
- * strings, finite numbers, booleans, and null survive; everything else
27
- * (functions, class instances, undefined) is dropped. String values and object
28
- * keys are redacted and capped in length; deterministic suffixes preserve every
29
- * value when redacted keys collide.
30
- * @param {?} value - Raw value from the integration.
31
- * @param {string[]} secrets - Secret values to redact.
32
- * @param {number} [depth] - Current recursion depth.
33
- * @returns {?} - JSON-safe redacted value, or undefined when the value cannot be represented.
34
- */
35
- export function sanitizeAdapterValue(value, secrets, depth = 0) {
36
- if (value === null)
37
- return null;
38
- if (depth > MAX_DEPTH)
39
- return undefined;
40
- const valueType = typeof value;
41
- if (valueType === "string")
42
- return redactString(value, secrets);
43
- if (valueType === "number")
44
- return Number.isFinite(value) ? value : null;
45
- if (valueType === "boolean")
46
- return value;
47
- if (Array.isArray(value)) {
48
- /** @type {Array<?>} */
49
- const result = [];
50
- for (const entry of value) {
51
- const sanitized = sanitizeAdapterValue(entry, secrets, depth + 1);
52
- if (sanitized !== undefined)
53
- result.push(sanitized);
54
- }
55
- return result;
56
- }
57
- if (valueType === "object") {
58
- const prototype = Object.getPrototypeOf(value);
59
- if (prototype !== Object.prototype && prototype !== null)
60
- return undefined;
61
- /** @type {Record<string, ?>} */
62
- const result = {};
63
- for (const [key, entry] of Object.entries(value)) {
64
- const sanitized = sanitizeAdapterValue(entry, secrets, depth + 1);
65
- if (sanitized === undefined)
66
- continue;
67
- const redactedKey = redactString(key, secrets);
68
- let availableKey = redactedKey;
69
- let collisionNumber = 2;
70
- while (Object.hasOwn(result, availableKey)) {
71
- availableKey = `${redactedKey}#${collisionNumber}`;
72
- collisionNumber++;
73
- }
74
- Object.defineProperty(result, availableKey, {
75
- configurable: true,
76
- enumerable: true,
77
- value: sanitized,
78
- writable: true
79
- });
80
- }
81
- return result;
82
- }
83
- return undefined;
84
- }
85
- /**
86
- * Builds the sanitized failure payload persisted for a failed run. The message
87
- * is redacted; an integration-provided plain-object `error.recovery` (e.g.
88
- * Rampway reporting restoration of the previous release) is included after
89
- * sanitization.
90
- * @param {?} error - Thrown error from the adapter.
91
- * @param {string[]} secrets - Secret values to redact.
92
- * @returns {{message: string, recovery: (? | undefined)}} - Sanitized failure payload.
93
- */
94
- export function sanitizeErrorPayload(error, secrets) {
95
- const rawMessage = error instanceof Error ? error.message : String(error);
96
- const rawRecovery = error instanceof Error ? /** @type {Record<string, ?>} */ (error).recovery : undefined;
97
- const recovery = sanitizeAdapterValue(rawRecovery, secrets);
98
- return { message: redactString(rawMessage, secrets), recovery };
99
- }
100
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2FuaXRpemUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZGVwbG95bWVudC1hcGkvc2FuaXRpemUuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWTtBQUVaLE1BQU0saUJBQWlCLEdBQUcsSUFBSSxDQUFBO0FBQzlCLE1BQU0sU0FBUyxHQUFHLEVBQUUsQ0FBQTtBQUNwQixNQUFNLFFBQVEsR0FBRyxZQUFZLENBQUE7QUFFN0I7Ozs7OztHQU1HO0FBQ0gsU0FBUyxZQUFZLENBQUMsS0FBSyxFQUFFLE9BQU87SUFDbEMsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFBO0lBRWxCLEtBQUssTUFBTSxNQUFNLElBQUksT0FBTyxFQUFFLENBQUM7UUFDN0IsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUM7WUFBRSxNQUFNLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUE7SUFDckUsQ0FBQztJQUVELElBQUksTUFBTSxDQUFDLE1BQU0sR0FBRyxpQkFBaUIsRUFBRSxDQUFDO1FBQ3RDLE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLGlCQUFpQixDQUFDLEdBQUcsQ0FBQTtJQUNuRCxDQUFDO0lBRUQsT0FBTyxNQUFNLENBQUE7QUFDZixDQUFDO0FBRUQ7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxLQUFLLEdBQUcsQ0FBQztJQUM1RCxJQUFJLEtBQUssS0FBSyxJQUFJO1FBQUUsT0FBTyxJQUFJLENBQUE7SUFDL0IsSUFBSSxLQUFLLEdBQUcsU0FBUztRQUFFLE9BQU8sU0FBUyxDQUFBO0lBRXZDLE1BQU0sU0FBUyxHQUFHLE9BQU8sS0FBSyxDQUFBO0lBRTlCLElBQUksU0FBUyxLQUFLLFFBQVE7UUFBRSxPQUFPLFlBQVksQ0FBQyxLQUFLLEVB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QUMvRCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbmNvbnN0IE1BWF9TVFJJTkdfTEVOR1RIID0gNDAwMFxuY29uc3QgTUFYX0RFUFRIID0gMjBcbmNvbnN0IFJFREFDVEVEID0gXCJbcmVkYWN0ZWRdXCJcblxuLyoqXG4gKiBSZXBsYWNlcyBldmVyeSBvY2N1cnJlbmNlIG9mIGVhY2ggY29uZmlndXJlZCBzZWNyZXQgaW4gYSBzdHJpbmcgc28gdG9rZW5zXG4gKiBuZXZlciBsZWFrIGludG8gcGVyc2lzdGVkIHJ1biBzdGF0ZSwgYXVkaXQgcGF5bG9hZHMsIG9yIEFQSSByZXNwb25zZXMuXG4gKiBAcGFyYW0ge3N0cmluZ30gdmFsdWUgLSBSYXcgc3RyaW5nLlxuICogQHBhcmFtIHtzdHJpbmdbXX0gc2VjcmV0cyAtIFNlY3JldCB2YWx1ZXMgdG8gcmVkYWN0LlxuICogQHJldHVybnMge3N0cmluZ30gLSBSZWRhY3RlZCBzdHJpbmcuXG4gKi9cbmZ1bmN0aW9uIHJlZGFjdFN0cmluZyh2YWx1ZSwgc2VjcmV0cykge1xuICBsZXQgcmVzdWx0ID0gdmFsdWVcblxuICBmb3IgKGNvbnN0IHNlY3JldCBvZiBzZWNyZXRzKSB7XG4gICAgaWYgKHNlY3JldC5sZW5ndGggPiAwKSByZXN1bHQgPSByZXN1bHQuc3BsaXQoc2VjcmV0KS5qb2luKFJFREFDVEVEKVxuICB9XG5cbiAgaWYgKHJlc3VsdC5sZW5ndGggPiBNQVhfU1RSSU5HX0xFTkdUSCkge1xuICAgIHJlc3VsdCA9IGAke3Jlc3VsdC5zbGljZSgwLCBNQVhfU1RSSU5HX0xFTkdUSCl94oCmYFxuICB9XG5cbiAgcmV0dXJuIHJlc3VsdFxufVxuXG4vKipcbiAqIENvbnZlcnRzIGFuIGludGVncmF0aW9uLW93bmVkIHZhbHVlIChhZGFwdGVyIHJlcG9ydCwgbGl2ZSBzdGF0dXMsIGF1ZGl0XG4gKiBwYXlsb2FkIGZyYWdtZW50KSBpbnRvIGEgYm91bmRlZCBKU09OLXNhZmUgc3RydWN0dXJlOiBwbGFpbiBvYmplY3RzLCBhcnJheXMsXG4gKiBzdHJpbmdzLCBmaW5pdGUgbnVtYmVycywgYm9vbGVhbnMsIGFuZCBudWxsIHN1cnZpdmU7IGV2ZXJ5dGhpbmcgZWxzZVxuICogKGZ1bmN0aW9ucywgY2xhc3MgaW5zdGFuY2VzLCB1bmRlZmluZWQpIGlzIGRyb3BwZWQuIFN0cmluZyB2YWx1ZXMgYW5kIG9iamVjdFxuICoga2V5cyBhcmUgcmVkYWN0ZWQgYW5kIGNhcHBlZCBpbiBsZW5ndGg7IGRldGVybWluaXN0aWMgc3VmZml4ZXMgcHJlc2VydmUgZXZlcnlcbiAqIHZhbHVlIHdoZW4gcmVkYWN0ZWQga2V5cyBjb2xsaWRlLlxuICogQHBhcmFtIHs/fSB2YWx1ZSAtIFJhdyB2YWx1ZSBmcm9tIHRoZSBpbnRlZ3JhdGlvbi5cbiAqIEBwYXJhbSB7c3RyaW5nW119IHNlY3JldHMgLSBTZWNyZXQgdmFsdWVzIHRvIHJlZGFjdC5cbiAqIEBwYXJhbSB7bnVtYmVyfSBbZGVwdGhdIC0gQ3VycmVudCByZWN1cnNpb24gZGVwdGguXG4gKiBAcmV0dXJucyB7P30gLSBKU09OLXNhZmUgcmVkYWN0ZWQgdmFsdWUsIG9yIHVuZGVmaW5lZCB3aGVuIHRoZSB2YWx1ZSBjYW5ub3QgYmUgcmVwcmVzZW50ZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzYW5pdGl6ZUFkYXB0ZXJWYWx1ZSh2YWx1ZSwgc2VjcmV0cywgZGVwdGggPSAwKSB7XG4gIGlmICh2YWx1ZSA9PT0gbnVsbCkgcmV0dXJuIG51bGxcbiAgaWYgKGRlcHRoID4gTUFYX0RFUFRIKSByZXR1cm4gdW5kZWZpbmVkXG5cbiAgY29uc3QgdmFsdWVUeXBlID0gdHlwZW9mIHZhbHVlXG5cbiAgaWYgKHZhbHVlVHlwZSA9PT0gXCJzdHJpbmdcIikgcmV0dXJuIHJlZGFjdFN0cmluZyh2YWx1ZSwgc2VjcmV0cylcbiAgaWYgKHZhbHVlVHlwZSA9PT0gXCJudW1iZXJcIikgcmV0dXJuIE51bWJlci5pc0Zpbml0ZSh2YWx1ZSkgPyB2YWx1ZSA6IG51bGxcbiAgaWYgKHZhbHVlVHlwZSA9PT0gXCJib29sZWFuXCIpIHJldHVybiB2YWx1ZVxuXG4gIGlmIChBcnJheS5pc0FycmF5KHZhbHVlKSkge1xuICAgIC8qKiBAdHlwZSB7QXJyYXk8Pz59ICovXG4gICAgY29uc3QgcmVzdWx0ID0gW11cblxuICAgIGZvciAoY29uc3QgZW50cnkgb2YgdmFsdWUpIHtcbiAgICAgIGNvbnN0IHNhbml0aXplZCA9IHNhbml0aXplQWRhcHRlclZhbHVlKGVudHJ5LCBzZWNyZXRzLCBkZXB0aCArIDEpXG5cbiAgICAgIGlmIChzYW5pdGl6ZWQgIT09IHVuZGVmaW5lZCkgcmVzdWx0LnB1c2goc2FuaXRpemVkKVxuICAgIH1cblxuICAgIHJldHVybiByZXN1bHRcbiAgfVxuXG4gIGlmICh2YWx1ZVR5cGUgPT09IFwib2JqZWN0XCIpIHtcbiAgICBjb25zdCBwcm90b3R5cGUgPSBPYmplY3QuZ2V0UHJvdG90eXBlT2YodmFsdWUpXG5cbiAgICBpZiAocHJvdG90eXBlICE9PSBPYmplY3QucHJvdG90eXBlICYmIHByb3RvdHlwZSAhPT0gbnVsbCkgcmV0dXJuIHVuZGVmaW5lZFxuXG4gICAgLyoqIEB0eXBlIHtSZWNvcmQ8c3RyaW5nLCA/Pn0gKi9cbiAgICBjb25zdCByZXN1bHQgPSB7fVxuXG4gICAgZm9yIChjb25zdCBba2V5LCBlbnRyeV0gb2YgT2JqZWN0LmVudHJpZXModmFsdWUpKSB7XG4gICAgICBjb25zdCBzYW5pdGl6ZWQgPSBzYW5pdGl6ZUFkYXB0ZXJWYWx1ZShlbnRyeSwgc2VjcmV0cywgZGVwdGggKyAxKVxuXG4gICAgICBpZiAoc2FuaXRpemVkID09PSB1bmRlZmluZWQpIGNvbnRpbnVlXG5cbiAgICAgIGNvbnN0IHJlZGFjdGVkS2V5ID0gcmVkYWN0U3RyaW5nKGtleSwgc2VjcmV0cylcbiAgICAgIGxldCBhdmFpbGFibGVLZXkgPSByZWRhY3RlZEtleVxuICAgICAgbGV0IGNvbGxpc2lvbk51bWJlciA9IDJcblxuICAgICAgd2hpbGUgKE9iamVjdC5oYXNPd24ocmVzdWx0LCBhdmFpbGFibGVLZXkpKSB7XG4gICAgICAgIGF2YWlsYWJsZUtleSA9IGAke3JlZGFjdGVkS2V5fSMke2NvbGxpc2lvbk51bWJlcn1gXG4gICAgICAgIGNvbGxpc2lvbk51bWJlcisrXG4gICAgICB9XG5cbiAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShyZXN1bHQsIGF2YWlsYWJsZUtleSwge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgICAgIHZhbHVlOiBzYW5pdGl6ZWQsXG4gICAgICAgIHdyaXRhYmxlOiB0cnVlXG4gICAgICB9KVxuICAgIH1cblxuICAgIHJldHVybiByZXN1bHRcbiAgfVxuXG4gIHJldHVybiB1bmRlZmluZWRcbn1cblxuLyoqXG4gKiBCdWlsZHMgdGhlIHNhbml0aXplZCBmYWlsdXJlIHBheWxvYWQgcGVyc2lzdGVkIGZvciBhIGZhaWxlZCBydW4uIFRoZSBtZXNzYWdlXG4gKiBpcyByZWRhY3RlZDsgYW4gaW50ZWdyYXRpb24tcHJvdmlkZWQgcGxhaW4tb2JqZWN0IGBlcnJvci5yZWNvdmVyeWAgKGUuZy5cbiAqIFJhbXB3YXkgcmVwb3J0aW5nIHJlc3RvcmF0aW9uIG9mIHRoZSBwcmV2aW91cyByZWxlYXNlKSBpcyBpbmNsdWRlZCBhZnRlclxuICogc2FuaXRpemF0aW9uLlxuICogQHBhcmFtIHs/fSBlcnJvciAtIFRocm93biBlcnJvciBmcm9tIHRoZSBhZGFwdGVyLlxuICogQHBhcmFtIHtzdHJpbmdbXX0gc2VjcmV0cyAtIFNlY3JldCB2YWx1ZXMgdG8gcmVkYWN0LlxuICogQHJldHVybnMge3ttZXNzYWdlOiBzdHJpbmcsIHJlY292ZXJ5OiAoPyB8IHVuZGVmaW5lZCl9fSAtIFNhbml0aXplZCBmYWlsdXJlIHBheWxvYWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzYW5pdGl6ZUVycm9yUGF5bG9hZChlcnJvciwgc2VjcmV0cykge1xuICBjb25zdCByYXdNZXNzYWdlID0gZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBTdHJpbmcoZXJyb3IpXG4gIGNvbnN0IHJhd1JlY292ZXJ5ID0gZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IC8qKiBAdHlwZSB7UmVjb3JkPHN0cmluZywgPz59ICovIChlcnJvcikucmVjb3ZlcnkgOiB1bmRlZmluZWRcbiAgY29uc3QgcmVjb3ZlcnkgPSBzYW5pdGl6ZUFkYXB0ZXJWYWx1ZShyYXdSZWNvdmVyeSwgc2VjcmV0cylcblxuICByZXR1cm4ge21lc3NhZ2U6IHJlZGFjdFN0cmluZyhyYXdNZXNzYWdlLCBzZWNyZXRzKSwgcmVjb3Zlcnl9XG59XG4iXX0=
@@ -1,437 +0,0 @@
1
- // @ts-check
2
-
3
- import Controller from "../controller.js"
4
- import DeploymentRunStore, {registerActiveDeploymentRun, unregisterActiveDeploymentRun} from "./run-store.js"
5
- import {bearerToken, constantTimeEqual} from "../utils/bearer-token.js"
6
- import {getDeploymentMount} from "./registry.js"
7
- import {sanitizeAdapterValue, sanitizeErrorPayload} from "./sanitize.js"
8
-
9
- const REVISION_PATTERN = /^[0-9a-f]{40}$/
10
- const MAX_IDEMPOTENCY_KEY_LENGTH = 255
11
-
12
- /**
13
- * Resolves allowlisted stage options with own-property checks only, so
14
- * request-controlled names like "__proto__" or "constructor" can never
15
- * resolve inherited values. The normalized maps are also null-prototype, so
16
- * this is defense in depth.
17
- * @param {import("./registry.js").DeploymentMountOptions} options - Mount options.
18
- * @param {string} project - Requested project identifier.
19
- * @param {string} stage - Requested stage identifier.
20
- * @returns {import("./registry.js").DeploymentStageOptions | undefined} - Stage options when allowlisted.
21
- */
22
- function lookupStageOptions(options, project, stage) {
23
- if (!Object.hasOwn(options.projects, project)) return undefined
24
-
25
- const stages = options.projects[project].stages
26
-
27
- if (!Object.hasOwn(stages, stage)) return undefined
28
-
29
- return stages[stage]
30
- }
31
-
32
- /**
33
- * Authenticated HTTP API for callable deployments. Mounted by
34
- * {@link import("./index.js").default} as a route-resolver hook so it can ship
35
- * inside the velocious package. Every action is gated by a bearer-token check
36
- * against the configured access tokens; the API exposes only allowlisted
37
- * project/stage pairs and full immutable revisions, and delegates all
38
- * execution to the configured adapter. It never accepts commands, paths,
39
- * arbitrary refs, environment variables, or raw log output.
40
- */
41
- export default class VelociousDeploymentApiController extends Controller {
42
- /**
43
- * Runs mount options.
44
- * @returns {import("./registry.js").DeploymentMountOptions} - Options for the mount that matched this request.
45
- */
46
- _mountOptions() {
47
- const at = /** @type {string} */ (this.params().velociousDeploymentMountAt)
48
- const options = getDeploymentMount(this.getConfiguration(), at)
49
-
50
- if (!options) throw new Error(`No deployment API mount registered at ${at}`)
51
-
52
- return options
53
- }
54
-
55
- /**
56
- * Runs store.
57
- * @returns {DeploymentRunStore} - Run store scoped to the mount's database.
58
- */
59
- _store() {
60
- if (!this._deploymentRunStore) {
61
- this._deploymentRunStore = new DeploymentRunStore({
62
- configuration: this.getConfiguration(),
63
- databaseIdentifier: this._mountOptions().databaseIdentifier,
64
- mountIdentifier: this._mountOptions().mountIdentifier,
65
- staleRunTimeoutMs: this._mountOptions().staleRunTimeoutMs
66
- })
67
- }
68
-
69
- return this._deploymentRunStore
70
- }
71
-
72
- /**
73
- * Reports one internally consumed framework failure on both documented
74
- * error channels so framework-specific and unified reporters see the same
75
- * payload.
76
- * @param {object} args - Options.
77
- * @param {string} args.context - Deployment API failure context.
78
- * @param {?} args.error - Consumed error.
79
- * @returns {void} - No return value.
80
- */
81
- _emitFrameworkError({context, error}) {
82
- const errorEvents = this.getConfiguration().getErrorEvents()
83
- const payload = {context, error, request: this.getRequest()}
84
-
85
- errorEvents.emit("framework-error", payload)
86
- errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
87
- }
88
-
89
- /**
90
- * Authorizes the request with a constant-time bearer-token comparison and
91
- * runs the action body only when authorized. Renders a 401 otherwise. The
92
- * base controller has no before-action halting, so authorization is enforced
93
- * here per action. Tokens are only accepted through the Authorization header
94
- * — never through URLs — and are never rendered back.
95
- * @param {() => Promise<void>} actionFn - Action body.
96
- * @returns {Promise<void>} - Resolves when complete.
97
- */
98
- async _respond(actionFn) {
99
- const token = bearerToken(this.request())
100
- let authorized = false
101
-
102
- if (token) {
103
- for (const accessToken of this._mountOptions().accessTokens) {
104
- if (constantTimeEqual(token, accessToken)) {
105
- authorized = true
106
- break
107
- }
108
- }
109
- }
110
-
111
- if (!authorized) {
112
- await this.render({json: {error: "unauthorized"}, status: 401})
113
- return
114
- }
115
-
116
- await actionFn()
117
- }
118
-
119
- /**
120
- * Creates a deployment run for an allowlisted project/stage and a full
121
- * immutable revision reachable from the approved release branch. Idempotent:
122
- * a retried idempotency key reads the original run, a reused key with a
123
- * different payload conflicts, and an active run for the same project/stage
124
- * returns a bounded conflict.
125
- * @returns {Promise<void>} - Resolves when complete.
126
- */
127
- async create() {
128
- await this._respond(async () => {
129
- const params = this.params()
130
- const revision = typeof params.revision === "string" ? params.revision : null
131
- const idempotencyKey = typeof params.idempotencyKey === "string" ? params.idempotencyKey : null
132
- const invalidFields = []
133
-
134
- if (!revision || !REVISION_PATTERN.test(revision)) invalidFields.push("revision")
135
- if (!idempotencyKey || idempotencyKey.length === 0 || idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
136
- invalidFields.push("idempotencyKey")
137
- }
138
-
139
- if (invalidFields.length > 0) {
140
- await this.render({json: {error: "invalid_params", fields: invalidFields}, status: 422})
141
- return
142
- }
143
-
144
- const options = this._mountOptions()
145
- const project = typeof params.project === "string" ? params.project : ""
146
- const stage = typeof params.stage === "string" ? params.stage : ""
147
- const stageOptions = lookupStageOptions(options, project, stage)
148
-
149
- if (!stageOptions) {
150
- await this.render({json: {error: "not_found"}, status: 404})
151
- return
152
- }
153
-
154
- const store = this._store()
155
- const validRevision = /** @type {string} */ (revision)
156
- const validIdempotencyKey = /** @type {string} */ (idempotencyKey)
157
-
158
- // Retries read the original run before anything else — a replay must
159
- // never re-validate or re-deploy.
160
- const existingRun = await store.findRunByKey(validIdempotencyKey)
161
-
162
- if (existingRun) {
163
- await this._renderExistingRun({existingRun, project, revision: validRevision, stage})
164
- return
165
- }
166
-
167
- const reachable = await options.adapter.validateRevision({
168
- configuration: this.getConfiguration(),
169
- project,
170
- releaseBranch: stageOptions.releaseBranch,
171
- revision: validRevision,
172
- stage
173
- })
174
-
175
- if (!reachable) {
176
- await this.render({json: {error: "revision_not_reachable"}, status: 422})
177
- return
178
- }
179
-
180
- const outcome = await store.createRunIfPossible({
181
- idempotencyKey: validIdempotencyKey,
182
- project,
183
- revision: validRevision,
184
- stage
185
- })
186
-
187
- if (outcome.outcome === "replay" || outcome.outcome === "conflict") {
188
- const existingFromStore = outcome.run
189
-
190
- if (!existingFromStore) throw new Error(`Deployment run store reported '${outcome.outcome}' without a run`)
191
-
192
- await this._renderExistingRun({existingRun: existingFromStore, project, revision: validRevision, stage})
193
- return
194
- }
195
-
196
- if (outcome.outcome === "in_progress") {
197
- const activeRun = outcome.run
198
-
199
- await this.render({json: {error: "deployment_in_progress", runId: activeRun ? activeRun.id : undefined}, status: 409})
200
- return
201
- }
202
-
203
- if (outcome.outcome === "reconciliation_required") {
204
- const blockedRun = outcome.run
205
-
206
- if (!blockedRun) throw new Error("Deployment run store reported 'reconciliation_required' without a run")
207
-
208
- await this.render({json: {error: "deployment_reconciliation_required", runId: blockedRun.id}, status: 409})
209
- return
210
- }
211
-
212
- const run = outcome.run
213
-
214
- if (!run) throw new Error("Deployment run store reported 'created' without a run")
215
-
216
- await this._audit({event: "run_requested", payload: {project, revision: validRevision, stage}, runId: run.id})
217
-
218
- // Execution is deliberately not awaited: the deploy runs under the
219
- // integration's own lock/build/health/rollback semantics and the caller
220
- // reads progress back through the show action.
221
- this._executeRun({options, run}).catch((error) => {
222
- this._emitFrameworkError({context: "deployment-api-execute-run", error})
223
- })
224
-
225
- await this.render({json: {run: this._serializeRun(run)}, status: 202})
226
- })
227
- }
228
-
229
- /**
230
- * Renders a previously created run for an idempotency-key hit: a replay when
231
- * the payload matches, a bounded conflict when it doesn't.
232
- * @param {object} args - Options.
233
- * @param {import("./run-store.js").DeploymentRunRow} args.existingRun - The stored run.
234
- * @param {string} args.project - Requested project.
235
- * @param {string} args.revision - Requested revision.
236
- * @param {string} args.stage - Requested stage.
237
- * @returns {Promise<void>} - Resolves when complete.
238
- */
239
- async _renderExistingRun({existingRun, project, revision, stage}) {
240
- const samePayload = existingRun.project === project && existingRun.stage === stage && existingRun.revision === revision
241
-
242
- if (!samePayload) {
243
- await this.render({json: {error: "idempotency_conflict", runId: existingRun.id}, status: 409})
244
- return
245
- }
246
-
247
- await this.render({json: {replayed: true, run: this._serializeRun(existingRun)}, status: 200})
248
- }
249
-
250
- /**
251
- * Returns the bounded state of a single run, enriched with the adapter's
252
- * live status when the integration provides one.
253
- * @returns {Promise<void>} - Resolves when complete.
254
- */
255
- async show() {
256
- await this._respond(async () => {
257
- const run = await this._store().findRunById(/** @type {string} */ (this.params().id))
258
-
259
- if (!run) {
260
- await this.render({json: {error: "not_found"}, status: 404})
261
- return
262
- }
263
-
264
- const options = this._mountOptions()
265
- /** @type {Record<string, ?>} */
266
- const body = {run: this._serializeRun(run)}
267
-
268
- if (options.adapter.readStatus) {
269
- const liveStatus = await options.adapter.readStatus({
270
- configuration: this.getConfiguration(),
271
- project: run.project,
272
- stage: run.stage
273
- })
274
-
275
- body.current = sanitizeAdapterValue(liveStatus, options.accessTokens) ?? null
276
- }
277
-
278
- await this.render({json: body, status: 200})
279
- })
280
- }
281
-
282
- /**
283
- * Executes a created run asynchronously: registers it as active in this
284
- * process, marks it running, heartbeats its ownership lease while the
285
- * adapter deploys, and records the sanitized outcome. A deployment failure
286
- * is an expected operational result — it is persisted with its sanitized
287
- * recovery information instead of being raised, so it stays visible through
288
- * readback and audit rather than crashing the worker.
289
- * @param {object} args - Options.
290
- * @param {import("./registry.js").DeploymentMountOptions} args.options - Mount options.
291
- * @param {import("./run-store.js").DeploymentRunRow} args.run - The created run.
292
- * @returns {Promise<void>} - Resolves when the outcome is recorded.
293
- */
294
- async _executeRun({options, run}) {
295
- const store = this._store()
296
- const secrets = options.accessTokens
297
- const stageOptions = lookupStageOptions(options, run.project, run.stage)
298
-
299
- if (!stageOptions) throw new Error(`Deployment run ${run.id} references non-allowlisted ${run.project}/${run.stage}`)
300
- if (!run.ownerToken) throw new Error(`Deployment run ${run.id} has no execution owner token`)
301
-
302
- const ownerToken = run.ownerToken
303
-
304
- registerActiveDeploymentRun(run.id)
305
-
306
- /** @type {ReturnType<typeof setInterval> | null} */
307
- let heartbeatTimer = null
308
-
309
- try {
310
- await store.markRunning({id: run.id, startedAtMs: Date.now()})
311
-
312
- // Renew the ownership lease while the deploy runs so reconciliation
313
- // never reclaims this genuinely active run; unref'd so the timer alone
314
- // keeps no process alive.
315
- const heartbeatIntervalMs = Math.max(1000, Math.floor(options.staleRunTimeoutMs / 4))
316
-
317
- heartbeatTimer = setInterval(() => {
318
- store.heartbeat({heartbeatAtMs: Date.now(), id: run.id}).catch((error) => {
319
- this._emitFrameworkError({context: "deployment-api-heartbeat", error})
320
- })
321
- }, heartbeatIntervalMs)
322
- heartbeatTimer.unref()
323
-
324
- await this._audit({event: "run_started", payload: {project: run.project, revision: run.revision, stage: run.stage}, runId: run.id})
325
-
326
- let report
327
-
328
- try {
329
- report = await options.adapter.deploy({
330
- configuration: this.getConfiguration(),
331
- project: run.project,
332
- releaseBranch: stageOptions.releaseBranch,
333
- revision: run.revision,
334
- runId: run.id,
335
- stage: run.stage
336
- })
337
- } catch (error) {
338
- const errorPayload = sanitizeErrorPayload(error, secrets)
339
-
340
- try {
341
- await store.markFailed({error: errorPayload, finishedAtMs: Date.now(), id: run.id, ownerToken})
342
- await this._audit({
343
- event: "run_failed",
344
- payload: {message: errorPayload.message, project: run.project, revision: run.revision, stage: run.stage},
345
- runId: run.id
346
- })
347
- } catch (storeError) {
348
- // Recording the failure itself failed — that is an unexpected bug
349
- // and must surface to process-level error reporters.
350
- this._emitFrameworkError({context: "deployment-api-record-failure", error: storeError})
351
- }
352
-
353
- return
354
- }
355
-
356
- try {
357
- const result = sanitizeAdapterValue(report ?? {}, secrets) ?? {}
358
-
359
- await store.markSucceeded({finishedAtMs: Date.now(), id: run.id, ownerToken, result})
360
- await this._audit({event: "run_succeeded", payload: {project: run.project, revision: run.revision, stage: run.stage}, runId: run.id})
361
- } catch (error) {
362
- // The adapter already returned success. Surface the recording error,
363
- // then fence the run in a durable non-retryable state rather than
364
- // falsely recording an external success as a deployment failure.
365
- this._emitFrameworkError({context: "deployment-api-record-success", error})
366
-
367
- const reconciliationError = {
368
- message: "Deployment activation succeeded, but its result could not be persisted; operator reconciliation is required"
369
- }
370
-
371
- try {
372
- await store.markReconciliationRequired({
373
- error: reconciliationError,
374
- finishedAtMs: Date.now(),
375
- id: run.id,
376
- ownerToken
377
- })
378
- await this._audit({
379
- event: "run_reconciliation_required",
380
- payload: {message: reconciliationError.message, project: run.project, revision: run.revision, stage: run.stage},
381
- runId: run.id
382
- })
383
- } catch (reconciliationErrorPersistenceError) {
384
- this._emitFrameworkError({
385
- context: "deployment-api-record-reconciliation-required",
386
- error: reconciliationErrorPersistenceError
387
- })
388
- }
389
- }
390
- } finally {
391
- if (heartbeatTimer) clearInterval(heartbeatTimer)
392
- unregisterActiveDeploymentRun(run.id)
393
- }
394
- }
395
-
396
- /**
397
- * Records a sanitized audit event. Audit persistence must never strand or
398
- * suppress a deployment, so a failure here is reported on the
399
- * framework-error and unified all-error channels (where process-level bug
400
- * reporters capture it), and execution continues.
401
- * @param {object} args - Options.
402
- * @param {string} args.event - Event name.
403
- * @param {Record<string, ?>} args.payload - Payload; sanitized and redacted before persistence.
404
- * @param {string | null} args.runId - Owning run id.
405
- * @returns {Promise<void>} - Resolves when recorded or reported.
406
- */
407
- async _audit({event, payload, runId}) {
408
- const sanitized = sanitizeAdapterValue(payload, this._mountOptions().accessTokens) ?? {}
409
-
410
- try {
411
- await this._store().addAuditEvent({event, payload: sanitized, runId})
412
- } catch (error) {
413
- this._emitFrameworkError({context: "deployment-api-audit", error})
414
- }
415
- }
416
-
417
- /**
418
- * Serializes a run for the API.
419
- * @param {import("./run-store.js").DeploymentRunRow} run - Run row.
420
- * @returns {Record<string, ?>} - Serialized run.
421
- */
422
- _serializeRun(run) {
423
- return {
424
- error: run.error,
425
- finishedAtMs: run.finishedAtMs,
426
- id: run.id,
427
- idempotencyKey: run.idempotencyKey,
428
- project: run.project,
429
- requestedAtMs: run.requestedAtMs,
430
- result: run.result,
431
- revision: run.revision,
432
- stage: run.stage,
433
- startedAtMs: run.startedAtMs,
434
- status: run.status
435
- }
436
- }
437
- }