@vizamodo/viza-dispatcher 1.3.28
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 +1 -0
- package/dist/artifacts/downloader.d.ts +22 -0
- package/dist/artifacts/downloader.js +56 -0
- package/dist/artifacts/types.d.ts +23 -0
- package/dist/artifacts/types.js +3 -0
- package/dist/auth/github.d.ts +30 -0
- package/dist/auth/github.js +74 -0
- package/dist/core/dispatcher.d.ts +22 -0
- package/dist/core/dispatcher.js +95 -0
- package/dist/core/errors.d.ts +69 -0
- package/dist/core/errors.js +131 -0
- package/dist/crypto/encrypt.d.ts +12 -0
- package/dist/crypto/encrypt.js +68 -0
- package/dist/gateway/dispatch.d.ts +22 -0
- package/dist/gateway/dispatch.js +75 -0
- package/dist/gateway/session.d.ts +16 -0
- package/dist/gateway/session.js +70 -0
- package/dist/gateway/types.d.ts +90 -0
- package/dist/gateway/types.js +11 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +2 -0
- package/dist/types/dispatcher.d.ts +119 -0
- package/dist/types/dispatcher.js +2 -0
- package/dist/types/payload.d.ts +0 -0
- package/dist/types/payload.js +2 -0
- package/dist/types/result.d.ts +0 -0
- package/dist/types/result.js +2 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// TODO: implement
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ArtifactRef } from "./types";
|
|
2
|
+
export interface DownloadArtifactOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum allowed artifact size in bytes.
|
|
5
|
+
* Default: 50 MB
|
|
6
|
+
*/
|
|
7
|
+
maxSizeBytes?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Download artifact from a presigned URL with safety guards.
|
|
11
|
+
*
|
|
12
|
+
* This function:
|
|
13
|
+
* - validates HTTP status
|
|
14
|
+
* - enforces size limits
|
|
15
|
+
* - validates content-type (if provided)
|
|
16
|
+
*
|
|
17
|
+
* It does NOT:
|
|
18
|
+
* - retry
|
|
19
|
+
* - log
|
|
20
|
+
* - exit process
|
|
21
|
+
*/
|
|
22
|
+
export declare function downloadArtifact(ref: ArtifactRef, options?: DownloadArtifactOptions): Promise<Buffer>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.downloadArtifact = downloadArtifact;
|
|
4
|
+
/**
|
|
5
|
+
* Download artifact from a presigned URL with safety guards.
|
|
6
|
+
*
|
|
7
|
+
* This function:
|
|
8
|
+
* - validates HTTP status
|
|
9
|
+
* - enforces size limits
|
|
10
|
+
* - validates content-type (if provided)
|
|
11
|
+
*
|
|
12
|
+
* It does NOT:
|
|
13
|
+
* - retry
|
|
14
|
+
* - log
|
|
15
|
+
* - exit process
|
|
16
|
+
*/
|
|
17
|
+
async function downloadArtifact(ref, options = {}) {
|
|
18
|
+
const { maxSizeBytes = 50 * 1024 * 1024 } = options;
|
|
19
|
+
if (!ref?.url) {
|
|
20
|
+
throw new Error("Invalid artifact reference: missing url");
|
|
21
|
+
}
|
|
22
|
+
const res = await fetch(ref.url);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`Failed to download artifact: HTTP ${res.status} ${res.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
const contentType = res.headers.get("content-type") || undefined;
|
|
27
|
+
if (ref.contentType && contentType && !contentType.includes(ref.contentType)) {
|
|
28
|
+
throw new Error(`Artifact content-type mismatch: expected ${ref.contentType}, got ${contentType}`);
|
|
29
|
+
}
|
|
30
|
+
const contentLengthHeader = res.headers.get("content-length");
|
|
31
|
+
if (contentLengthHeader) {
|
|
32
|
+
const size = Number(contentLengthHeader);
|
|
33
|
+
if (!Number.isNaN(size) && size > maxSizeBytes) {
|
|
34
|
+
throw new Error(`Artifact too large: ${size} bytes exceeds limit ${maxSizeBytes}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const reader = res.body?.getReader();
|
|
38
|
+
if (!reader) {
|
|
39
|
+
throw new Error("Response body is not readable");
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let received = 0;
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done)
|
|
46
|
+
break;
|
|
47
|
+
if (value) {
|
|
48
|
+
received += value.byteLength;
|
|
49
|
+
if (received > maxSizeBytes) {
|
|
50
|
+
throw new Error(`Artifact too large: received ${received} bytes exceeds limit ${maxSizeBytes}`);
|
|
51
|
+
}
|
|
52
|
+
chunks.push(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact reference returned by the gateway or dispatcher.
|
|
3
|
+
* This is a pure data contract (no logic).
|
|
4
|
+
*/
|
|
5
|
+
export interface ArtifactRef {
|
|
6
|
+
/**
|
|
7
|
+
* Presigned download URL.
|
|
8
|
+
* Must be an absolute HTTPS URL.
|
|
9
|
+
*/
|
|
10
|
+
url: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional expected content-type substring.
|
|
13
|
+
* Example: "application/zip", "application/json".
|
|
14
|
+
*
|
|
15
|
+
* If provided, downloader must validate it.
|
|
16
|
+
*/
|
|
17
|
+
contentType?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Optional human-readable artifact name.
|
|
20
|
+
* Used only for diagnostics or UI, never for logic.
|
|
21
|
+
*/
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface GitHubAuthOptions {
|
|
2
|
+
/**
|
|
3
|
+
* GitHub organization to validate against
|
|
4
|
+
* Example: "Modo-Core"
|
|
5
|
+
*/
|
|
6
|
+
org: string;
|
|
7
|
+
/**
|
|
8
|
+
* REQUIRED.
|
|
9
|
+
* Target team responsible for this dispatch.
|
|
10
|
+
* Example: "dev", "billing", "design"
|
|
11
|
+
*/
|
|
12
|
+
targetTeam: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GitHubIdentity {
|
|
15
|
+
login: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ensure GitHub CLI is authenticated and user is authorized.
|
|
19
|
+
*
|
|
20
|
+
* This function FAILS FAST:
|
|
21
|
+
* - not logged in → throw
|
|
22
|
+
* - not org member → throw
|
|
23
|
+
* - not in allowed teams → throw
|
|
24
|
+
*
|
|
25
|
+
* It does NOT:
|
|
26
|
+
* - perform dispatch
|
|
27
|
+
* - call gateway
|
|
28
|
+
* - print UX
|
|
29
|
+
*/
|
|
30
|
+
export declare function ensureGitHubAuth(options: GitHubAuthOptions): Promise<GitHubIdentity>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureGitHubAuth = ensureGitHubAuth;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
7
|
+
/**
|
|
8
|
+
* Ensure GitHub CLI is authenticated and user is authorized.
|
|
9
|
+
*
|
|
10
|
+
* This function FAILS FAST:
|
|
11
|
+
* - not logged in → throw
|
|
12
|
+
* - not org member → throw
|
|
13
|
+
* - not in allowed teams → throw
|
|
14
|
+
*
|
|
15
|
+
* It does NOT:
|
|
16
|
+
* - perform dispatch
|
|
17
|
+
* - call gateway
|
|
18
|
+
* - print UX
|
|
19
|
+
*/
|
|
20
|
+
async function ensureGitHubAuth(options) {
|
|
21
|
+
// ----------------------------------------------------------
|
|
22
|
+
// 1. Check gh CLI authentication status
|
|
23
|
+
// ----------------------------------------------------------
|
|
24
|
+
try {
|
|
25
|
+
await execFileAsync("gh", ["auth", "status"]);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("GitHub CLI not authenticated. Run `gh auth login` first.");
|
|
29
|
+
}
|
|
30
|
+
// ----------------------------------------------------------
|
|
31
|
+
// 2. Resolve current GitHub user
|
|
32
|
+
// ----------------------------------------------------------
|
|
33
|
+
let login;
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await execFileAsync("gh", [
|
|
36
|
+
"api",
|
|
37
|
+
"user",
|
|
38
|
+
"--jq",
|
|
39
|
+
".login",
|
|
40
|
+
]);
|
|
41
|
+
login = stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error("Failed to resolve GitHub user identity.");
|
|
45
|
+
}
|
|
46
|
+
if (!login) {
|
|
47
|
+
throw new Error("GitHub user identity is empty.");
|
|
48
|
+
}
|
|
49
|
+
// ----------------------------------------------------------
|
|
50
|
+
// 3. Check organization membership
|
|
51
|
+
// ----------------------------------------------------------
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync("gh", [
|
|
54
|
+
"api",
|
|
55
|
+
`orgs/${options.org}/members/${login}`,
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error(`User ${login} is not a member of GitHub organization ${options.org}.`);
|
|
60
|
+
}
|
|
61
|
+
// ----------------------------------------------------------
|
|
62
|
+
// 4. Mandatory team membership check
|
|
63
|
+
// ----------------------------------------------------------
|
|
64
|
+
try {
|
|
65
|
+
await execFileAsync("gh", [
|
|
66
|
+
"api",
|
|
67
|
+
`orgs/${options.org}/teams/${options.targetTeam}/memberships/${login}`,
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`User ${login} is not a member of required team ${options.targetTeam} in ${options.org}.`);
|
|
72
|
+
}
|
|
73
|
+
return { login };
|
|
74
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DispatchInput, DispatchOptions, DispatchResult } from "../types/dispatcher";
|
|
2
|
+
export interface DispatchHandle {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
wsUrl: string;
|
|
5
|
+
/**
|
|
6
|
+
* Wait until the dispatch session is completed.
|
|
7
|
+
*/
|
|
8
|
+
wait(): Promise<DispatchResult>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Main dispatcher entrypoint.
|
|
12
|
+
*
|
|
13
|
+
* This function orchestrates the full dispatch lifecycle:
|
|
14
|
+
* - validate
|
|
15
|
+
* - POST /dispatch
|
|
16
|
+
* - wait for final result via WebSocket
|
|
17
|
+
* - artifact download
|
|
18
|
+
*
|
|
19
|
+
* It NEVER calls process.exit().
|
|
20
|
+
* All errors are thrown for the caller (viza-cli) to handle.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dispatch(input: DispatchInput, options: DispatchOptions): Promise<DispatchHandle>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dispatch = dispatch;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const dispatch_1 = require("../gateway/dispatch");
|
|
6
|
+
const session_1 = require("../gateway/session");
|
|
7
|
+
const downloader_1 = require("../artifacts/downloader");
|
|
8
|
+
const github_1 = require("../auth/github");
|
|
9
|
+
const encrypt_1 = require("../crypto/encrypt");
|
|
10
|
+
/**
|
|
11
|
+
* Main dispatcher entrypoint.
|
|
12
|
+
*
|
|
13
|
+
* This function orchestrates the full dispatch lifecycle:
|
|
14
|
+
* - validate
|
|
15
|
+
* - POST /dispatch
|
|
16
|
+
* - wait for final result via WebSocket
|
|
17
|
+
* - artifact download
|
|
18
|
+
*
|
|
19
|
+
* It NEVER calls process.exit().
|
|
20
|
+
* All errors are thrown for the caller (viza-cli) to handle.
|
|
21
|
+
*/
|
|
22
|
+
async function dispatch(input, options) {
|
|
23
|
+
const { gateway } = options;
|
|
24
|
+
if (!gateway?.endpoint) {
|
|
25
|
+
throw new errors_1.InvalidInputError("Missing gateway endpoint");
|
|
26
|
+
}
|
|
27
|
+
// ----------------------------------------------------------
|
|
28
|
+
// 2. GitHub authentication & authorization (fail-fast)
|
|
29
|
+
// ----------------------------------------------------------
|
|
30
|
+
if (!options.auth) {
|
|
31
|
+
throw new errors_1.InvalidInputError("Missing authentication options. Anonymous dispatch is not allowed.");
|
|
32
|
+
}
|
|
33
|
+
await (0, github_1.ensureGitHubAuth)({
|
|
34
|
+
org: options.auth.org,
|
|
35
|
+
targetTeam: options.auth.targetTeam,
|
|
36
|
+
});
|
|
37
|
+
if (!input.encrypt) {
|
|
38
|
+
throw new errors_1.InvalidInputError("Encryption is mandatory for dispatch payloads");
|
|
39
|
+
}
|
|
40
|
+
const encrypted = await (0, encrypt_1.encryptPayload)(input.payload, input.encrypt);
|
|
41
|
+
const gatewayReq = {
|
|
42
|
+
intent: input.intent,
|
|
43
|
+
target: input.target,
|
|
44
|
+
eventType: input.eventType,
|
|
45
|
+
payload: encrypted,
|
|
46
|
+
};
|
|
47
|
+
let accepted;
|
|
48
|
+
try {
|
|
49
|
+
accepted = await (0, dispatch_1.dispatchToGateway)(gatewayReq, {
|
|
50
|
+
endpoint: gateway.endpoint,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new errors_1.DispatchRejectedError("Dispatch request rejected by gateway", {
|
|
55
|
+
cause: err,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
sessionId: accepted.sessionId,
|
|
60
|
+
wsUrl: accepted.wsUrl,
|
|
61
|
+
async wait() {
|
|
62
|
+
let done;
|
|
63
|
+
try {
|
|
64
|
+
done = await (0, session_1.waitForSessionDone)(accepted.wsUrl);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err?.name === "SessionTimeoutError") {
|
|
68
|
+
throw new errors_1.SessionTimeoutError();
|
|
69
|
+
}
|
|
70
|
+
throw new errors_1.GatewayError("Gateway session error", { cause: err });
|
|
71
|
+
}
|
|
72
|
+
let logBuffer;
|
|
73
|
+
if (done.artifacts?.log) {
|
|
74
|
+
try {
|
|
75
|
+
logBuffer = await (0, downloader_1.downloadArtifact)(done.artifacts.log);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const msg = String(err?.message || "");
|
|
79
|
+
if (msg.includes("too large")) {
|
|
80
|
+
throw new errors_1.ArtifactTooLargeError();
|
|
81
|
+
}
|
|
82
|
+
if (msg.includes("content-type")) {
|
|
83
|
+
throw new errors_1.ArtifactInvalidTypeError();
|
|
84
|
+
}
|
|
85
|
+
throw new errors_1.ArtifactNotFoundError(undefined, { cause: err });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
status: done.status,
|
|
90
|
+
artifacts: done.artifacts,
|
|
91
|
+
logBuffer,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy for viza-dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* - Deterministic exit codes for viza-cli
|
|
6
|
+
* - Fail-fast, no side effects
|
|
7
|
+
* - Machine-readable + human-readable
|
|
8
|
+
*/
|
|
9
|
+
export declare enum DispatcherExitCode {
|
|
10
|
+
OK = 0,
|
|
11
|
+
INVALID_INPUT = 2,
|
|
12
|
+
GITHUB_NOT_LOGGED_IN = 10,
|
|
13
|
+
GITHUB_FORBIDDEN = 11,
|
|
14
|
+
DISPATCH_REJECTED = 20,
|
|
15
|
+
SESSION_TIMEOUT = 21,
|
|
16
|
+
GATEWAY_ERROR = 22,
|
|
17
|
+
ARTIFACT_NOT_FOUND = 30,
|
|
18
|
+
ARTIFACT_TOO_LARGE = 31,
|
|
19
|
+
ARTIFACT_INVALID_TYPE = 32,
|
|
20
|
+
INTERNAL_ERROR = 90
|
|
21
|
+
}
|
|
22
|
+
export interface DispatcherErrorOptions {
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
details?: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Base dispatcher error
|
|
28
|
+
*/
|
|
29
|
+
export declare class DispatcherError extends Error {
|
|
30
|
+
readonly exitCode: DispatcherExitCode;
|
|
31
|
+
readonly details?: Record<string, any>;
|
|
32
|
+
readonly cause?: unknown;
|
|
33
|
+
constructor(message: string, exitCode: DispatcherExitCode, options?: DispatcherErrorOptions);
|
|
34
|
+
}
|
|
35
|
+
export declare class InvalidInputError extends DispatcherError {
|
|
36
|
+
constructor(message: string, options?: DispatcherErrorOptions);
|
|
37
|
+
}
|
|
38
|
+
export declare class GitHubNotLoggedInError extends DispatcherError {
|
|
39
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
40
|
+
}
|
|
41
|
+
export declare class GitHubForbiddenError extends DispatcherError {
|
|
42
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
43
|
+
}
|
|
44
|
+
export declare class DispatchRejectedError extends DispatcherError {
|
|
45
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
46
|
+
}
|
|
47
|
+
export declare class SessionTimeoutError extends DispatcherError {
|
|
48
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
49
|
+
}
|
|
50
|
+
export declare class GatewayError extends DispatcherError {
|
|
51
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
52
|
+
}
|
|
53
|
+
export declare class ArtifactNotFoundError extends DispatcherError {
|
|
54
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
55
|
+
}
|
|
56
|
+
export declare class ArtifactTooLargeError extends DispatcherError {
|
|
57
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
58
|
+
}
|
|
59
|
+
export declare class ArtifactInvalidTypeError extends DispatcherError {
|
|
60
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
61
|
+
}
|
|
62
|
+
export declare class InternalDispatcherError extends DispatcherError {
|
|
63
|
+
constructor(message?: string, options?: DispatcherErrorOptions);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Normalize unknown errors into DispatcherError
|
|
67
|
+
* Used by viza-cli boundary
|
|
68
|
+
*/
|
|
69
|
+
export declare function normalizeDispatcherError(err: unknown): DispatcherError;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Error taxonomy for viza-dispatcher
|
|
4
|
+
*
|
|
5
|
+
* Goals:
|
|
6
|
+
* - Deterministic exit codes for viza-cli
|
|
7
|
+
* - Fail-fast, no side effects
|
|
8
|
+
* - Machine-readable + human-readable
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.InternalDispatcherError = exports.ArtifactInvalidTypeError = exports.ArtifactTooLargeError = exports.ArtifactNotFoundError = exports.GatewayError = exports.SessionTimeoutError = exports.DispatchRejectedError = exports.GitHubForbiddenError = exports.GitHubNotLoggedInError = exports.InvalidInputError = exports.DispatcherError = exports.DispatcherExitCode = void 0;
|
|
12
|
+
exports.normalizeDispatcherError = normalizeDispatcherError;
|
|
13
|
+
var DispatcherExitCode;
|
|
14
|
+
(function (DispatcherExitCode) {
|
|
15
|
+
DispatcherExitCode[DispatcherExitCode["OK"] = 0] = "OK";
|
|
16
|
+
// Input / usage
|
|
17
|
+
DispatcherExitCode[DispatcherExitCode["INVALID_INPUT"] = 2] = "INVALID_INPUT";
|
|
18
|
+
// Auth / permission
|
|
19
|
+
DispatcherExitCode[DispatcherExitCode["GITHUB_NOT_LOGGED_IN"] = 10] = "GITHUB_NOT_LOGGED_IN";
|
|
20
|
+
DispatcherExitCode[DispatcherExitCode["GITHUB_FORBIDDEN"] = 11] = "GITHUB_FORBIDDEN";
|
|
21
|
+
// Network / gateway
|
|
22
|
+
DispatcherExitCode[DispatcherExitCode["DISPATCH_REJECTED"] = 20] = "DISPATCH_REJECTED";
|
|
23
|
+
DispatcherExitCode[DispatcherExitCode["SESSION_TIMEOUT"] = 21] = "SESSION_TIMEOUT";
|
|
24
|
+
DispatcherExitCode[DispatcherExitCode["GATEWAY_ERROR"] = 22] = "GATEWAY_ERROR";
|
|
25
|
+
// Artifacts / logs
|
|
26
|
+
DispatcherExitCode[DispatcherExitCode["ARTIFACT_NOT_FOUND"] = 30] = "ARTIFACT_NOT_FOUND";
|
|
27
|
+
DispatcherExitCode[DispatcherExitCode["ARTIFACT_TOO_LARGE"] = 31] = "ARTIFACT_TOO_LARGE";
|
|
28
|
+
DispatcherExitCode[DispatcherExitCode["ARTIFACT_INVALID_TYPE"] = 32] = "ARTIFACT_INVALID_TYPE";
|
|
29
|
+
// Internal / unexpected
|
|
30
|
+
DispatcherExitCode[DispatcherExitCode["INTERNAL_ERROR"] = 90] = "INTERNAL_ERROR";
|
|
31
|
+
})(DispatcherExitCode || (exports.DispatcherExitCode = DispatcherExitCode = {}));
|
|
32
|
+
/**
|
|
33
|
+
* Base dispatcher error
|
|
34
|
+
*/
|
|
35
|
+
class DispatcherError extends Error {
|
|
36
|
+
constructor(message, exitCode, options) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = this.constructor.name;
|
|
39
|
+
this.exitCode = exitCode;
|
|
40
|
+
this.details = options?.details;
|
|
41
|
+
this.cause = options?.cause;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.DispatcherError = DispatcherError;
|
|
45
|
+
/* -----------------------------
|
|
46
|
+
* Input / validation
|
|
47
|
+
* ----------------------------- */
|
|
48
|
+
class InvalidInputError extends DispatcherError {
|
|
49
|
+
constructor(message, options) {
|
|
50
|
+
super(message, DispatcherExitCode.INVALID_INPUT, options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.InvalidInputError = InvalidInputError;
|
|
54
|
+
/* -----------------------------
|
|
55
|
+
* GitHub auth
|
|
56
|
+
* ----------------------------- */
|
|
57
|
+
class GitHubNotLoggedInError extends DispatcherError {
|
|
58
|
+
constructor(message = "Not logged in to GitHub", options) {
|
|
59
|
+
super(message, DispatcherExitCode.GITHUB_NOT_LOGGED_IN, options);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.GitHubNotLoggedInError = GitHubNotLoggedInError;
|
|
63
|
+
class GitHubForbiddenError extends DispatcherError {
|
|
64
|
+
constructor(message = "GitHub permission denied", options) {
|
|
65
|
+
super(message, DispatcherExitCode.GITHUB_FORBIDDEN, options);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.GitHubForbiddenError = GitHubForbiddenError;
|
|
69
|
+
/* -----------------------------
|
|
70
|
+
* Gateway / dispatch
|
|
71
|
+
* ----------------------------- */
|
|
72
|
+
class DispatchRejectedError extends DispatcherError {
|
|
73
|
+
constructor(message = "Dispatch request rejected", options) {
|
|
74
|
+
super(message, DispatcherExitCode.DISPATCH_REJECTED, options);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.DispatchRejectedError = DispatchRejectedError;
|
|
78
|
+
class SessionTimeoutError extends DispatcherError {
|
|
79
|
+
constructor(message = "Dispatch session timed out", options) {
|
|
80
|
+
super(message, DispatcherExitCode.SESSION_TIMEOUT, options);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.SessionTimeoutError = SessionTimeoutError;
|
|
84
|
+
class GatewayError extends DispatcherError {
|
|
85
|
+
constructor(message = "Gateway error", options) {
|
|
86
|
+
super(message, DispatcherExitCode.GATEWAY_ERROR, options);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.GatewayError = GatewayError;
|
|
90
|
+
/* -----------------------------
|
|
91
|
+
* Artifacts
|
|
92
|
+
* ----------------------------- */
|
|
93
|
+
class ArtifactNotFoundError extends DispatcherError {
|
|
94
|
+
constructor(message = "Artifact not found", options) {
|
|
95
|
+
super(message, DispatcherExitCode.ARTIFACT_NOT_FOUND, options);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.ArtifactNotFoundError = ArtifactNotFoundError;
|
|
99
|
+
class ArtifactTooLargeError extends DispatcherError {
|
|
100
|
+
constructor(message = "Artifact exceeds size limit", options) {
|
|
101
|
+
super(message, DispatcherExitCode.ARTIFACT_TOO_LARGE, options);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.ArtifactTooLargeError = ArtifactTooLargeError;
|
|
105
|
+
class ArtifactInvalidTypeError extends DispatcherError {
|
|
106
|
+
constructor(message = "Invalid artifact content type", options) {
|
|
107
|
+
super(message, DispatcherExitCode.ARTIFACT_INVALID_TYPE, options);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.ArtifactInvalidTypeError = ArtifactInvalidTypeError;
|
|
111
|
+
/* -----------------------------
|
|
112
|
+
* Internal
|
|
113
|
+
* ----------------------------- */
|
|
114
|
+
class InternalDispatcherError extends DispatcherError {
|
|
115
|
+
constructor(message = "Internal dispatcher error", options) {
|
|
116
|
+
super(message, DispatcherExitCode.INTERNAL_ERROR, options);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.InternalDispatcherError = InternalDispatcherError;
|
|
120
|
+
/**
|
|
121
|
+
* Normalize unknown errors into DispatcherError
|
|
122
|
+
* Used by viza-cli boundary
|
|
123
|
+
*/
|
|
124
|
+
function normalizeDispatcherError(err) {
|
|
125
|
+
if (err instanceof DispatcherError)
|
|
126
|
+
return err;
|
|
127
|
+
if (err instanceof Error) {
|
|
128
|
+
return new InternalDispatcherError(err.message, { cause: err });
|
|
129
|
+
}
|
|
130
|
+
return new InternalDispatcherError("Unknown error", { cause: err });
|
|
131
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EncryptedPayload } from "../gateway/types";
|
|
2
|
+
import { EncryptOptions } from "../types/dispatcher";
|
|
3
|
+
/**
|
|
4
|
+
* Encrypt payload using X25519 + HKDF + AES-256-GCM.
|
|
5
|
+
*
|
|
6
|
+
* Security properties:
|
|
7
|
+
* - Ephemeral client key per invocation
|
|
8
|
+
* - Forward secrecy
|
|
9
|
+
* - AEAD (authenticated encryption)
|
|
10
|
+
* - No plaintext on wire
|
|
11
|
+
*/
|
|
12
|
+
export declare function encryptPayload(payload: Record<string, any>, opts: EncryptOptions): EncryptedPayload;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.encryptPayload = encryptPayload;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
/**
|
|
9
|
+
* Encrypt payload using X25519 + HKDF + AES-256-GCM.
|
|
10
|
+
*
|
|
11
|
+
* Security properties:
|
|
12
|
+
* - Ephemeral client key per invocation
|
|
13
|
+
* - Forward secrecy
|
|
14
|
+
* - AEAD (authenticated encryption)
|
|
15
|
+
* - No plaintext on wire
|
|
16
|
+
*/
|
|
17
|
+
function encryptPayload(payload, opts) {
|
|
18
|
+
if (!opts?.serverPublicKeyPem) {
|
|
19
|
+
throw new Error("Missing server public key for encryption");
|
|
20
|
+
}
|
|
21
|
+
// Normalize payload into an object we can safely enrich
|
|
22
|
+
let normalized;
|
|
23
|
+
if (payload === null || payload === undefined) {
|
|
24
|
+
normalized = {};
|
|
25
|
+
}
|
|
26
|
+
else if (typeof payload === "object") {
|
|
27
|
+
normalized = payload;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Primitive payloads are wrapped
|
|
31
|
+
normalized = { value: payload };
|
|
32
|
+
}
|
|
33
|
+
// Record signing time (server validates freshness using |now - created| window)
|
|
34
|
+
normalized.created = Date.now();
|
|
35
|
+
const payloadStr = JSON.stringify(normalized);
|
|
36
|
+
// Load server public key (injected, not read from FS)
|
|
37
|
+
const serverPubKey = crypto_1.default.createPublicKey(opts.serverPublicKeyPem);
|
|
38
|
+
// Generate ephemeral client key pair
|
|
39
|
+
const clientKeyPair = crypto_1.default.generateKeyPairSync("x25519");
|
|
40
|
+
const clientPriv = clientKeyPair.privateKey;
|
|
41
|
+
const clientPub = clientKeyPair.publicKey;
|
|
42
|
+
// Derive shared secret
|
|
43
|
+
const sharedSecret = crypto_1.default.diffieHellman({
|
|
44
|
+
privateKey: clientPriv,
|
|
45
|
+
publicKey: serverPubKey,
|
|
46
|
+
});
|
|
47
|
+
// Derive symmetric key via HKDF
|
|
48
|
+
const aesKey = Buffer.from(crypto_1.default.hkdfSync("sha256", sharedSecret, Buffer.alloc(0), "viza-ecdh-key", 32));
|
|
49
|
+
// Encrypt using AES-256-GCM
|
|
50
|
+
const iv = crypto_1.default.randomBytes(12);
|
|
51
|
+
const cipher = crypto_1.default.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
52
|
+
const encrypted = Buffer.concat([
|
|
53
|
+
cipher.update(payloadStr, "utf8"),
|
|
54
|
+
cipher.final(),
|
|
55
|
+
]);
|
|
56
|
+
const tag = cipher.getAuthTag();
|
|
57
|
+
const result = {
|
|
58
|
+
alg: "x25519-hkdf-aes256gcm",
|
|
59
|
+
clientPub: clientPub
|
|
60
|
+
.export({ type: "spki", format: "der" })
|
|
61
|
+
.toString("base64"),
|
|
62
|
+
iv: iv.toString("base64"),
|
|
63
|
+
tag: tag.toString("base64"),
|
|
64
|
+
data: encrypted.toString("base64"),
|
|
65
|
+
created: normalized.created,
|
|
66
|
+
};
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { GatewayDispatchRequest, GatewayDispatchAccepted } from "./types";
|
|
2
|
+
export interface DispatchToGatewayOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Base HTTPS endpoint of the gateway worker
|
|
5
|
+
* Example: https://dispatch.viza.io
|
|
6
|
+
*/
|
|
7
|
+
endpoint: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional request timeout in ms (default: 15s)
|
|
10
|
+
*/
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* POST /dispatch
|
|
15
|
+
*
|
|
16
|
+
* Low-level gateway call.
|
|
17
|
+
* This function:
|
|
18
|
+
* - does NOT retry
|
|
19
|
+
* - does NOT handle UX
|
|
20
|
+
* - does NOT swallow errors
|
|
21
|
+
*/
|
|
22
|
+
export declare function dispatchToGateway(req: GatewayDispatchRequest, options: DispatchToGatewayOptions): Promise<GatewayDispatchAccepted>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dispatchToGateway = dispatchToGateway;
|
|
4
|
+
/**
|
|
5
|
+
* POST /dispatch
|
|
6
|
+
*
|
|
7
|
+
* Low-level gateway call.
|
|
8
|
+
* This function:
|
|
9
|
+
* - does NOT retry
|
|
10
|
+
* - does NOT handle UX
|
|
11
|
+
* - does NOT swallow errors
|
|
12
|
+
*/
|
|
13
|
+
async function dispatchToGateway(req, options) {
|
|
14
|
+
const { endpoint, timeoutMs = 15000 } = options;
|
|
15
|
+
if (!endpoint.startsWith("https://")) {
|
|
16
|
+
throw new Error(`Invalid gateway endpoint: ${endpoint}`);
|
|
17
|
+
}
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
+
let res;
|
|
21
|
+
try {
|
|
22
|
+
res = await fetch(`${endpoint}/dispatch`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(req),
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
if (err?.name === "AbortError") {
|
|
34
|
+
throw new Error("Gateway dispatch request timed out");
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Failed to reach gateway: ${err?.message ?? err}`);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
}
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const text = await safeReadText(res);
|
|
43
|
+
throw new Error(`Gateway HTTP error ${res.status}: ${text || res.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
let json;
|
|
46
|
+
try {
|
|
47
|
+
json = (await res.json());
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error("Gateway returned invalid JSON");
|
|
51
|
+
}
|
|
52
|
+
if (!json || typeof json !== "object" || !("ok" in json)) {
|
|
53
|
+
throw new Error("Gateway response does not match dispatch contract");
|
|
54
|
+
}
|
|
55
|
+
if (json.ok !== true) {
|
|
56
|
+
throw new Error(json.message
|
|
57
|
+
? `Dispatch rejected: ${json.message}`
|
|
58
|
+
: `Dispatch rejected: ${json.error}`);
|
|
59
|
+
}
|
|
60
|
+
// Basic sanity checks
|
|
61
|
+
if (typeof json.sessionId !== "string" ||
|
|
62
|
+
typeof json.wsUrl !== "string" ||
|
|
63
|
+
typeof json.expiresAt !== "number") {
|
|
64
|
+
throw new Error("Gateway accepted response is malformed");
|
|
65
|
+
}
|
|
66
|
+
return json;
|
|
67
|
+
}
|
|
68
|
+
async function safeReadText(res) {
|
|
69
|
+
try {
|
|
70
|
+
return await res.text();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { GatewaySessionDoneEvent } from "./types";
|
|
2
|
+
export interface WaitForSessionOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum time to wait for session completion (ms)
|
|
5
|
+
* Default: 10 minutes
|
|
6
|
+
*/
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wait for session completion via WebSocket.
|
|
11
|
+
*
|
|
12
|
+
* Contract:
|
|
13
|
+
* - Gateway sends exactly ONE message: { type: "done", ... }
|
|
14
|
+
* - Connection is closed after that
|
|
15
|
+
*/
|
|
16
|
+
export declare function waitForSessionDone(wsUrl: string, options?: WaitForSessionOptions): Promise<GatewaySessionDoneEvent>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.waitForSessionDone = waitForSessionDone;
|
|
4
|
+
/**
|
|
5
|
+
* Wait for session completion via WebSocket.
|
|
6
|
+
*
|
|
7
|
+
* Contract:
|
|
8
|
+
* - Gateway sends exactly ONE message: { type: "done", ... }
|
|
9
|
+
* - Connection is closed after that
|
|
10
|
+
*/
|
|
11
|
+
function waitForSessionDone(wsUrl, options = {}) {
|
|
12
|
+
const { timeoutMs = 10 * 60 * 1000 } = options;
|
|
13
|
+
if (!wsUrl.startsWith("wss://")) {
|
|
14
|
+
return Promise.reject(new Error(`Invalid WebSocket URL: ${wsUrl}`));
|
|
15
|
+
}
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let settled = false;
|
|
18
|
+
const ws = new WebSocket(wsUrl);
|
|
19
|
+
const timeout = setTimeout(() => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
ws.close();
|
|
24
|
+
reject(new Error("Session wait timed out"));
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
ws.onmessage = (event) => {
|
|
27
|
+
if (settled)
|
|
28
|
+
return;
|
|
29
|
+
let data;
|
|
30
|
+
try {
|
|
31
|
+
data = JSON.parse(String(event.data));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
ws.close();
|
|
37
|
+
reject(new Error("Invalid JSON received from gateway"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!data ||
|
|
41
|
+
typeof data !== "object" ||
|
|
42
|
+
data.type !== "done") {
|
|
43
|
+
settled = true;
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
ws.close();
|
|
46
|
+
reject(new Error("Unexpected WebSocket message from gateway"));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
settled = true;
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
ws.close();
|
|
52
|
+
resolve(data);
|
|
53
|
+
};
|
|
54
|
+
ws.onerror = () => {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
ws.close();
|
|
60
|
+
reject(new Error("WebSocket error while waiting for session"));
|
|
61
|
+
};
|
|
62
|
+
ws.onclose = () => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
reject(new Error("WebSocket closed before session completion"));
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface EncryptedPayload {
|
|
2
|
+
alg: "x25519-hkdf-aes256gcm";
|
|
3
|
+
clientPub: string;
|
|
4
|
+
iv: string;
|
|
5
|
+
tag: string;
|
|
6
|
+
data: string;
|
|
7
|
+
created: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GatewayDispatchRequest {
|
|
10
|
+
/**
|
|
11
|
+
* Intent identifier
|
|
12
|
+
* e.g. "deploy-base-worker", "publish-billing", "query-repo"
|
|
13
|
+
*/
|
|
14
|
+
intent: string;
|
|
15
|
+
/**
|
|
16
|
+
* Maps to GitHub `repository_dispatch.event_type`
|
|
17
|
+
*/
|
|
18
|
+
eventType: string;
|
|
19
|
+
/**
|
|
20
|
+
* Target repository information
|
|
21
|
+
*/
|
|
22
|
+
target: {
|
|
23
|
+
owner: string;
|
|
24
|
+
repo: string;
|
|
25
|
+
ref?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Payload is ALWAYS encrypted.
|
|
29
|
+
* Gateway must decrypt before forwarding.
|
|
30
|
+
*/
|
|
31
|
+
payload: EncryptedPayload;
|
|
32
|
+
}
|
|
33
|
+
export interface GatewayDispatchAccepted {
|
|
34
|
+
ok: true;
|
|
35
|
+
/**
|
|
36
|
+
* Unique session identifier
|
|
37
|
+
*/
|
|
38
|
+
sessionId: string;
|
|
39
|
+
/**
|
|
40
|
+
* WebSocket endpoint for session completion notification
|
|
41
|
+
*
|
|
42
|
+
* Example:
|
|
43
|
+
* wss://dispatch.viza.io/session/<sessionId>
|
|
44
|
+
*/
|
|
45
|
+
wsUrl: string;
|
|
46
|
+
/**
|
|
47
|
+
* Unix timestamp (seconds)
|
|
48
|
+
* After this time the session may be garbage collected
|
|
49
|
+
*/
|
|
50
|
+
expiresAt: number;
|
|
51
|
+
}
|
|
52
|
+
export interface GatewayDispatchRejected {
|
|
53
|
+
ok: false;
|
|
54
|
+
/**
|
|
55
|
+
* Stable error code for client handling
|
|
56
|
+
*/
|
|
57
|
+
error: string;
|
|
58
|
+
/**
|
|
59
|
+
* Human-readable explanation (for CLI display)
|
|
60
|
+
*/
|
|
61
|
+
message?: string;
|
|
62
|
+
}
|
|
63
|
+
export type GatewayDispatchResponse = GatewayDispatchAccepted | GatewayDispatchRejected;
|
|
64
|
+
export interface GatewaySessionDoneEvent {
|
|
65
|
+
type: "done";
|
|
66
|
+
sessionId: string;
|
|
67
|
+
/**
|
|
68
|
+
* Final execution status
|
|
69
|
+
*/
|
|
70
|
+
status: "success" | "failed" | "cancelled";
|
|
71
|
+
/**
|
|
72
|
+
* Artifacts generated by the workflow
|
|
73
|
+
* These are presigned URLs (read-only)
|
|
74
|
+
*/
|
|
75
|
+
artifacts?: {
|
|
76
|
+
log?: ArtifactRef;
|
|
77
|
+
result?: ArtifactRef;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export interface ArtifactRef {
|
|
81
|
+
/**
|
|
82
|
+
* Presigned download URL
|
|
83
|
+
*/
|
|
84
|
+
url: string;
|
|
85
|
+
/**
|
|
86
|
+
* Optional metadata
|
|
87
|
+
*/
|
|
88
|
+
contentType?: string;
|
|
89
|
+
size?: number;
|
|
90
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* ============================================================
|
|
3
|
+
* Gateway ⇄ Dispatcher Contract (Encryption-Only)
|
|
4
|
+
* ============================================================
|
|
5
|
+
* Notes:
|
|
6
|
+
* - Gateway MUST NOT stream logs
|
|
7
|
+
* - Logs are fetched later via presigned artifact URLs
|
|
8
|
+
* - WebSocket is used ONLY for final status notification
|
|
9
|
+
* ============================================================
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/index.d.ts
ADDED
|
File without changes
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { GitHubAuthOptions } from "../auth/github";
|
|
2
|
+
import { GatewayDispatchRequest } from "../gateway/types";
|
|
3
|
+
export interface EncryptOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Server public key in PEM (SPKI) format.
|
|
6
|
+
* This key is used to derive the shared secret via X25519.
|
|
7
|
+
*/
|
|
8
|
+
serverPublicKeyPem: string;
|
|
9
|
+
}
|
|
10
|
+
/****
|
|
11
|
+
* Public types for viza-dispatcher.
|
|
12
|
+
* These types form the stable contract between viza-cli and viza-dispatcher.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Normalized repository target.
|
|
16
|
+
* Parsed by viza-cli, forwarded verbatim by viza-dispatcher.
|
|
17
|
+
*/
|
|
18
|
+
export type DispatchRepoTarget = GatewayDispatchRequest["target"];
|
|
19
|
+
/**
|
|
20
|
+
* Input describing what to dispatch.
|
|
21
|
+
*/
|
|
22
|
+
export interface DispatchInput {
|
|
23
|
+
/**
|
|
24
|
+
* Intent name (e.g. "deploy-worker", "publish-app").
|
|
25
|
+
* Interpreted by the gateway.
|
|
26
|
+
*/
|
|
27
|
+
intent: string;
|
|
28
|
+
/**
|
|
29
|
+
* Normalized dispatch target.
|
|
30
|
+
* Must already conform to gateway contract.
|
|
31
|
+
*/
|
|
32
|
+
target: DispatchRepoTarget;
|
|
33
|
+
/**
|
|
34
|
+
* Plain business payload.
|
|
35
|
+
* Payload is ALWAYS encrypted before being sent to gateway.
|
|
36
|
+
* Plain payload never leaves dispatcher.
|
|
37
|
+
*/
|
|
38
|
+
payload: unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Maps to GitHub repository_dispatch.event_type.
|
|
41
|
+
*/
|
|
42
|
+
eventType: string;
|
|
43
|
+
/**
|
|
44
|
+
* Encryption options.
|
|
45
|
+
* Encryption is mandatory; dispatcher will encrypt payload
|
|
46
|
+
* and emit an encrypted DispatchPayloadEnvelope.
|
|
47
|
+
*/
|
|
48
|
+
encrypt: EncryptOptions;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Gateway-related options.
|
|
52
|
+
*/
|
|
53
|
+
export interface DispatchGatewayOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Base HTTPS endpoint of the dispatch gateway.
|
|
56
|
+
* Example: https://dispatch.viza.io
|
|
57
|
+
*/
|
|
58
|
+
endpoint: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* UI-related toggles.
|
|
62
|
+
* NOTE: Dispatcher itself does NOT render UI.
|
|
63
|
+
* These flags are only passed through for caller coordination.
|
|
64
|
+
*/
|
|
65
|
+
export interface DispatchUIOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Whether banner/spinner/logs are enabled.
|
|
68
|
+
* Dispatcher does not interpret these flags directly.
|
|
69
|
+
*/
|
|
70
|
+
banner?: boolean;
|
|
71
|
+
spinner?: boolean;
|
|
72
|
+
showLog?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Options controlling dispatch behavior.
|
|
76
|
+
*/
|
|
77
|
+
export interface DispatchOptions {
|
|
78
|
+
gateway: DispatchGatewayOptions;
|
|
79
|
+
auth: GitHubAuthOptions;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Artifact reference returned by the gateway.
|
|
83
|
+
*/
|
|
84
|
+
export interface DispatchArtifactRef {
|
|
85
|
+
/**
|
|
86
|
+
* Presigned download URL.
|
|
87
|
+
*/
|
|
88
|
+
url: string;
|
|
89
|
+
/**
|
|
90
|
+
* Optional content type hint.
|
|
91
|
+
*/
|
|
92
|
+
contentType?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Artifacts produced by a dispatch session.
|
|
96
|
+
*/
|
|
97
|
+
export interface DispatchArtifacts {
|
|
98
|
+
log?: DispatchArtifactRef;
|
|
99
|
+
result?: DispatchArtifactRef;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Final result returned by the dispatcher.
|
|
103
|
+
* This is consumed by viza-cli.
|
|
104
|
+
*/
|
|
105
|
+
export interface DispatchResult {
|
|
106
|
+
/**
|
|
107
|
+
* Final status of the session.
|
|
108
|
+
*/
|
|
109
|
+
status: "success" | "failed" | "cancelled";
|
|
110
|
+
/**
|
|
111
|
+
* Artifact references (if any).
|
|
112
|
+
*/
|
|
113
|
+
artifacts?: DispatchArtifacts;
|
|
114
|
+
/**
|
|
115
|
+
* Raw log archive buffer (zip), if downloaded.
|
|
116
|
+
* CLI decides how to render or ignore it.
|
|
117
|
+
*/
|
|
118
|
+
logBuffer?: Buffer;
|
|
119
|
+
}
|
|
File without changes
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vizamodo/viza-dispatcher",
|
|
3
|
+
"version": "1.3.28",
|
|
4
|
+
"description": "Dispatcher module for GitHub workflow automation across Viza projects",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build",
|
|
10
|
+
"release:dev": "npm version prerelease --preid=dev && npm publish --tag dev --access public",
|
|
11
|
+
"release:beta": "npm version prerelease --preid=beta && npm publish --tag beta --access public",
|
|
12
|
+
"release:prod": "rm -rf dist && npx npm-check-updates -u && npm install && git add package.json package-lock.json && git commit -m 'chore(deps): auto update dependencies before release' || echo 'No changes' && node versioning.js && npm login && npm publish --tag latest --access public && git push"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
".keys"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"viza-dispatcher": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org/",
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"author": "Viza Team",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"adm-zip": "^0.5.16",
|
|
29
|
+
"chalk": "^5.6.2"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/adm-zip": "^0.5.7",
|
|
33
|
+
"@types/node": "^25.0.6",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"type": "module"
|
|
37
|
+
}
|