@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 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,3 @@
1
+ "use strict";
2
+ // TODO: implement
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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 });
File without changes
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ // TODO: implement
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
File without changes
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ // TODO: implement
File without changes
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ // TODO: implement
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
+ }