@vizamodo/modo-dispatcher 1.1.77

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 (109) hide show
  1. package/README.md +43 -0
  2. package/dist/artifacts/builders.d.ts +32 -0
  3. package/dist/artifacts/builders.js +72 -0
  4. package/dist/artifacts/downloader.d.ts +28 -0
  5. package/dist/artifacts/downloader.js +102 -0
  6. package/dist/artifacts/normalizer.d.ts +22 -0
  7. package/dist/artifacts/normalizer.js +65 -0
  8. package/dist/artifacts/types.d.ts +94 -0
  9. package/dist/artifacts/types.js +1 -0
  10. package/dist/auth/identity/extract-identity.d.ts +8 -0
  11. package/dist/auth/identity/extract-identity.js +15 -0
  12. package/dist/auth/identity/normalize-teams.d.ts +1 -0
  13. package/dist/auth/identity/normalize-teams.js +5 -0
  14. package/dist/auth/identity/validate-claims.d.ts +2 -0
  15. package/dist/auth/identity/validate-claims.js +5 -0
  16. package/dist/auth/jwt/parse.d.ts +2 -0
  17. package/dist/auth/jwt/parse.js +16 -0
  18. package/dist/auth/jwt/verify.d.ts +1 -0
  19. package/dist/auth/jwt/verify.js +9 -0
  20. package/dist/auth/oauth/auth-error.d.ts +12 -0
  21. package/dist/auth/oauth/auth-error.js +47 -0
  22. package/dist/auth/oauth/auth-session-store.d.ts +30 -0
  23. package/dist/auth/oauth/auth-session-store.js +46 -0
  24. package/dist/auth/oauth/callback-server.d.ts +21 -0
  25. package/dist/auth/oauth/callback-server.js +319 -0
  26. package/dist/auth/oauth/github.d.ts +14 -0
  27. package/dist/auth/oauth/github.js +100 -0
  28. package/dist/auth/oauth/sse.d.ts +11 -0
  29. package/dist/auth/oauth/sse.js +67 -0
  30. package/dist/auth/oauth/templates/failed.html +629 -0
  31. package/dist/auth/oauth/templates/pending.html +620 -0
  32. package/dist/auth/oauth/templates/success.html +577 -0
  33. package/dist/auth/session/access-token.d.ts +15 -0
  34. package/dist/auth/session/access-token.js +64 -0
  35. package/dist/auth/session/login.d.ts +18 -0
  36. package/dist/auth/session/login.js +144 -0
  37. package/dist/auth/session/logout.d.ts +3 -0
  38. package/dist/auth/session/logout.js +21 -0
  39. package/dist/auth/session/refresh-token.d.ts +8 -0
  40. package/dist/auth/session/refresh-token.js +16 -0
  41. package/dist/auth/session/refresh.d.ts +2 -0
  42. package/dist/auth/session/refresh.js +61 -0
  43. package/dist/auth/session/rotate.d.ts +4 -0
  44. package/dist/auth/session/rotate.js +4 -0
  45. package/dist/auth/session/session-manager.d.ts +16 -0
  46. package/dist/auth/session/session-manager.js +54 -0
  47. package/dist/auth/session/token-state.d.ts +20 -0
  48. package/dist/auth/session/token-state.js +55 -0
  49. package/dist/auth/session/types.d.ts +35 -0
  50. package/dist/auth/session/types.js +1 -0
  51. package/dist/auth/storage/keychain.d.ts +16 -0
  52. package/dist/auth/storage/keychain.js +107 -0
  53. package/dist/auth/storage/memory.d.ts +10 -0
  54. package/dist/auth/storage/memory.js +15 -0
  55. package/dist/auth/storage/types.d.ts +5 -0
  56. package/dist/auth/storage/types.js +1 -0
  57. package/dist/config/defaults.d.ts +94 -0
  58. package/dist/config/defaults.js +116 -0
  59. package/dist/core/auth-bootstrap.d.ts +15 -0
  60. package/dist/core/auth-bootstrap.js +33 -0
  61. package/dist/core/bootstrap.d.ts +73 -0
  62. package/dist/core/bootstrap.js +248 -0
  63. package/dist/core/dispatcher.d.ts +2 -0
  64. package/dist/core/dispatcher.js +28 -0
  65. package/dist/core/ensure-bootstrap.d.ts +3 -0
  66. package/dist/core/ensure-bootstrap.js +25 -0
  67. package/dist/core/errors.d.ts +69 -0
  68. package/dist/core/errors.js +121 -0
  69. package/dist/core/runtime-orchestrator.d.ts +17 -0
  70. package/dist/core/runtime-orchestrator.js +47 -0
  71. package/dist/core/runtime.d.ts +16 -0
  72. package/dist/core/runtime.js +52 -0
  73. package/dist/core/validation.d.ts +2 -0
  74. package/dist/core/validation.js +21 -0
  75. package/dist/crypto/encrypt.d.ts +12 -0
  76. package/dist/crypto/encrypt.js +80 -0
  77. package/dist/flows/email-otp-flow.d.ts +7 -0
  78. package/dist/flows/email-otp-flow.js +249 -0
  79. package/dist/gateway/auth-header.d.ts +2 -0
  80. package/dist/gateway/auth-header.js +21 -0
  81. package/dist/gateway/client.d.ts +7 -0
  82. package/dist/gateway/client.js +24 -0
  83. package/dist/gateway/dispatch-with-bootstrap-retry.d.ts +25 -0
  84. package/dist/gateway/dispatch-with-bootstrap-retry.js +157 -0
  85. package/dist/gateway/dispatch.d.ts +23 -0
  86. package/dist/gateway/dispatch.js +110 -0
  87. package/dist/gateway/session-waiter.d.ts +11 -0
  88. package/dist/gateway/session-waiter.js +126 -0
  89. package/dist/gateway/session.d.ts +21 -0
  90. package/dist/gateway/session.js +74 -0
  91. package/dist/gateway/types.d.ts +113 -0
  92. package/dist/gateway/types.js +19 -0
  93. package/dist/index.d.ts +22 -0
  94. package/dist/index.js +21 -0
  95. package/dist/runtime/runtime-context.d.ts +6 -0
  96. package/dist/runtime/runtime-context.js +1 -0
  97. package/dist/types/dispatcher.d.ts +275 -0
  98. package/dist/types/dispatcher.js +1 -0
  99. package/dist/types/payload.d.ts +5 -0
  100. package/dist/types/payload.js +2 -0
  101. package/dist/types/runtime-env.d.ts +2 -0
  102. package/dist/types/runtime-env.js +5 -0
  103. package/dist/ui/banner.d.ts +4 -0
  104. package/dist/ui/banner.js +20 -0
  105. package/dist/utils/request-dedup.d.ts +75 -0
  106. package/dist/utils/request-dedup.js +102 -0
  107. package/dist/utils/type-guards.d.ts +173 -0
  108. package/dist/utils/type-guards.js +232 -0
  109. package/package.json +43 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Error taxonomy for cli-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,121 @@
1
+ /**
2
+ * Error taxonomy for cli-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 var DispatcherExitCode;
10
+ (function (DispatcherExitCode) {
11
+ DispatcherExitCode[DispatcherExitCode["OK"] = 0] = "OK";
12
+ // Input / usage
13
+ DispatcherExitCode[DispatcherExitCode["INVALID_INPUT"] = 2] = "INVALID_INPUT";
14
+ // Auth / permission
15
+ DispatcherExitCode[DispatcherExitCode["GITHUB_NOT_LOGGED_IN"] = 10] = "GITHUB_NOT_LOGGED_IN";
16
+ DispatcherExitCode[DispatcherExitCode["GITHUB_FORBIDDEN"] = 11] = "GITHUB_FORBIDDEN";
17
+ // Network / gateway
18
+ DispatcherExitCode[DispatcherExitCode["DISPATCH_REJECTED"] = 20] = "DISPATCH_REJECTED";
19
+ DispatcherExitCode[DispatcherExitCode["SESSION_TIMEOUT"] = 21] = "SESSION_TIMEOUT";
20
+ DispatcherExitCode[DispatcherExitCode["GATEWAY_ERROR"] = 22] = "GATEWAY_ERROR";
21
+ // Artifacts / logs
22
+ DispatcherExitCode[DispatcherExitCode["ARTIFACT_NOT_FOUND"] = 30] = "ARTIFACT_NOT_FOUND";
23
+ DispatcherExitCode[DispatcherExitCode["ARTIFACT_TOO_LARGE"] = 31] = "ARTIFACT_TOO_LARGE";
24
+ DispatcherExitCode[DispatcherExitCode["ARTIFACT_INVALID_TYPE"] = 32] = "ARTIFACT_INVALID_TYPE";
25
+ // Internal / unexpected
26
+ DispatcherExitCode[DispatcherExitCode["INTERNAL_ERROR"] = 90] = "INTERNAL_ERROR";
27
+ })(DispatcherExitCode || (DispatcherExitCode = {}));
28
+ /**
29
+ * Base dispatcher error
30
+ */
31
+ export class DispatcherError extends Error {
32
+ exitCode;
33
+ details;
34
+ cause;
35
+ constructor(message, exitCode, options) {
36
+ super(message);
37
+ this.name = this.constructor.name;
38
+ this.exitCode = exitCode;
39
+ if (options?.details !== undefined) {
40
+ this.details = options.details;
41
+ }
42
+ this.cause = options?.cause;
43
+ }
44
+ }
45
+ /* -----------------------------
46
+ * Input / validation
47
+ * ----------------------------- */
48
+ export class InvalidInputError extends DispatcherError {
49
+ constructor(message, options) {
50
+ super(message, DispatcherExitCode.INVALID_INPUT, options);
51
+ }
52
+ }
53
+ /* -----------------------------
54
+ * GitHub auth
55
+ * ----------------------------- */
56
+ export class GitHubNotLoggedInError extends DispatcherError {
57
+ constructor(message = "Not logged in to GitHub", options) {
58
+ super(message, DispatcherExitCode.GITHUB_NOT_LOGGED_IN, options);
59
+ }
60
+ }
61
+ export class GitHubForbiddenError extends DispatcherError {
62
+ constructor(message = "GitHub permission denied", options) {
63
+ super(message, DispatcherExitCode.GITHUB_FORBIDDEN, options);
64
+ }
65
+ }
66
+ /* -----------------------------
67
+ * Gateway / dispatch
68
+ * ----------------------------- */
69
+ export class DispatchRejectedError extends DispatcherError {
70
+ constructor(message = "Dispatch request rejected", options) {
71
+ super(message, DispatcherExitCode.DISPATCH_REJECTED, options);
72
+ }
73
+ }
74
+ export class SessionTimeoutError extends DispatcherError {
75
+ constructor(message = "Dispatch session timed out", options) {
76
+ super(message, DispatcherExitCode.SESSION_TIMEOUT, options);
77
+ }
78
+ }
79
+ export class GatewayError extends DispatcherError {
80
+ constructor(message = "Gateway error", options) {
81
+ super(message, DispatcherExitCode.GATEWAY_ERROR, options);
82
+ }
83
+ }
84
+ /* -----------------------------
85
+ * Artifacts
86
+ * ----------------------------- */
87
+ export class ArtifactNotFoundError extends DispatcherError {
88
+ constructor(message = "Artifact not found. Possible cause: gateway R2 credentials rotated. Please redeploy the dispatch gateway and retry.", options) {
89
+ super(message, DispatcherExitCode.ARTIFACT_NOT_FOUND, options);
90
+ }
91
+ }
92
+ export class ArtifactTooLargeError extends DispatcherError {
93
+ constructor(message = "Artifact exceeds size limit", options) {
94
+ super(message, DispatcherExitCode.ARTIFACT_TOO_LARGE, options);
95
+ }
96
+ }
97
+ export class ArtifactInvalidTypeError extends DispatcherError {
98
+ constructor(message = "Invalid artifact content type", options) {
99
+ super(message, DispatcherExitCode.ARTIFACT_INVALID_TYPE, options);
100
+ }
101
+ }
102
+ /* -----------------------------
103
+ * Internal
104
+ * ----------------------------- */
105
+ export class InternalDispatcherError extends DispatcherError {
106
+ constructor(message = "Internal dispatcher error", options) {
107
+ super(message, DispatcherExitCode.INTERNAL_ERROR, options);
108
+ }
109
+ }
110
+ /**
111
+ * Normalize unknown errors into DispatcherError
112
+ * Used by viza-cli boundary
113
+ */
114
+ export function normalizeDispatcherError(err) {
115
+ if (err instanceof DispatcherError)
116
+ return err;
117
+ if (err instanceof Error) {
118
+ return new InternalDispatcherError(err.message, { cause: err });
119
+ }
120
+ return new InternalDispatcherError("Unknown error", { cause: err });
121
+ }
@@ -0,0 +1,17 @@
1
+ import { DispatchInput, DispatchMode, DispatchOptions, DispatchResult } from "../types/dispatcher.js";
2
+ import { RuntimeContext } from "../runtime/runtime-context.js";
3
+ export interface DispatchLifecycleResult {
4
+ runtimeContext: RuntimeContext;
5
+ result: DispatchResult;
6
+ sessionId: string;
7
+ }
8
+ /**
9
+ * Full dispatch lifecycle orchestration.
10
+ *
11
+ * Pipeline:
12
+ * 1. Auth & Bootstrap
13
+ * 2. Render banner
14
+ * 3. Dispatch to gateway (with bootstrap retry)
15
+ * 4. Wait for session result
16
+ */
17
+ export declare function executeDispatchLifecycle(input: DispatchInput, options: DispatchOptions, mode: DispatchMode): Promise<DispatchLifecycleResult>;
@@ -0,0 +1,47 @@
1
+ import { ensureAuthAndBootstrap } from "./auth-bootstrap.js";
2
+ import { dispatchWithBootstrapRetry } from "../gateway/dispatch-with-bootstrap-retry.js";
3
+ import chalk from "chalk";
4
+ import { extractSessionId } from "../utils/type-guards.js";
5
+ import { renderDispatchBanner } from "../ui/banner.js";
6
+ import { waitResultFromAccepted } from "../gateway/session-waiter.js";
7
+ /**
8
+ * Full dispatch lifecycle orchestration.
9
+ *
10
+ * Pipeline:
11
+ * 1. Auth & Bootstrap
12
+ * 2. Render banner
13
+ * 3. Dispatch to gateway (with bootstrap retry)
14
+ * 4. Wait for session result
15
+ */
16
+ export async function executeDispatchLifecycle(input, options, mode) {
17
+ // 1. Auth & Bootstrap
18
+ const { runtimeContext, bootstrapCfg } = await ensureAuthAndBootstrap(options);
19
+ // 2. Render banner
20
+ if (options.banner !== false) {
21
+ renderDispatchBanner(runtimeContext.claims.login ?? "unknown", runtimeContext.teams, input.allowedTeams, mode === "status");
22
+ }
23
+ // 3. Dispatch to gateway
24
+ const dispatched = await dispatchWithBootstrapRetry({
25
+ input,
26
+ mode,
27
+ teams: runtimeContext.teams,
28
+ bootstrap: bootstrapCfg,
29
+ targetEnv: options.auth.targetEnv,
30
+ });
31
+ const acceptedSessionId = extractSessionId(dispatched.accepted.artifacts) ?? "runtime";
32
+ if (dispatched.accepted.action === "dispatch" ||
33
+ dispatched.accepted.action === "reuse") {
34
+ const actionLabel = dispatched.accepted.action === "reuse"
35
+ ? chalk.yellow("REUSE")
36
+ : chalk.green("NEW");
37
+ console.log(chalk.gray("\n✓ Dispatch session established"));
38
+ console.log(`${chalk.gray("Session:")} ${acceptedSessionId}`);
39
+ console.log(`${chalk.gray("Mode :")} ${actionLabel}`);
40
+ }
41
+ // 4. Wait for session result
42
+ return {
43
+ runtimeContext,
44
+ result: await waitResultFromAccepted(dispatched.accepted, input),
45
+ sessionId: acceptedSessionId,
46
+ };
47
+ }
@@ -0,0 +1,16 @@
1
+ import { AccessTokenService } from "../auth/session/access-token.js";
2
+ import { TokenStateMachine } from "../auth/session/token-state.js";
3
+ import { RefreshTokenService } from "../auth/session/refresh-token.js";
4
+ export interface AuthRuntimeConfig {
5
+ refreshEndpoint: string;
6
+ }
7
+ export interface AuthRuntime {
8
+ accessTokenService: AccessTokenService;
9
+ refreshTokenService: RefreshTokenService;
10
+ tokenState: TokenStateMachine;
11
+ getBootstrapEndpoint(): string;
12
+ }
13
+ export declare function initAuthRuntime(config: AuthRuntimeConfig): AuthRuntime;
14
+ export declare function getAuthRuntime(): AuthRuntime;
15
+ export declare function tryGetAuthRuntime(): AuthRuntime | undefined;
16
+ export declare function getAuthRuntimeRefreshEndpoint(): string | undefined;
@@ -0,0 +1,52 @@
1
+ import { AccessTokenService } from "../auth/session/access-token.js";
2
+ import { TokenStateMachine } from "../auth/session/token-state.js";
3
+ import { RefreshTokenService } from "../auth/session/refresh-token.js";
4
+ import { KeychainStore } from "../auth/storage/keychain.js";
5
+ import { MemoryTokenStore } from "../auth/storage/memory.js";
6
+ let currentRuntime;
7
+ let currentRefreshEndpoint;
8
+ function deriveBootstrapEndpoint(refreshEndpoint) {
9
+ const url = new URL(refreshEndpoint);
10
+ const parts = url.pathname.split("/").filter(Boolean);
11
+ // Replace the last segment (usually "refresh") with "bootstrap"
12
+ parts.pop();
13
+ parts.push("bootstrap");
14
+ url.pathname = "/" + parts.join("/");
15
+ return url.toString();
16
+ }
17
+ export function initAuthRuntime(config) {
18
+ if (currentRuntime) {
19
+ if (currentRefreshEndpoint && currentRefreshEndpoint !== config.refreshEndpoint) {
20
+ throw new Error("Auth runtime already initialized with a different refresh endpoint");
21
+ }
22
+ return currentRuntime;
23
+ }
24
+ const memoryStore = new MemoryTokenStore();
25
+ const secureStore = new KeychainStore();
26
+ const accessTokenService = new AccessTokenService(memoryStore, secureStore);
27
+ const refreshTokenService = new RefreshTokenService(secureStore);
28
+ const tokenState = new TokenStateMachine(accessTokenService, refreshTokenService, {
29
+ refreshEndpoint: config.refreshEndpoint,
30
+ });
31
+ const bootstrapEndpoint = deriveBootstrapEndpoint(config.refreshEndpoint);
32
+ currentRuntime = {
33
+ accessTokenService,
34
+ refreshTokenService,
35
+ tokenState,
36
+ getBootstrapEndpoint: () => bootstrapEndpoint,
37
+ };
38
+ currentRefreshEndpoint = config.refreshEndpoint;
39
+ return currentRuntime;
40
+ }
41
+ export function getAuthRuntime() {
42
+ if (!currentRuntime) {
43
+ throw new Error("Auth runtime is not initialized");
44
+ }
45
+ return currentRuntime;
46
+ }
47
+ export function tryGetAuthRuntime() {
48
+ return currentRuntime;
49
+ }
50
+ export function getAuthRuntimeRefreshEndpoint() {
51
+ return currentRefreshEndpoint;
52
+ }
@@ -0,0 +1,2 @@
1
+ import { DispatchInput } from "../types/dispatcher.js";
2
+ export declare function assertDispatchInputStrict(input: DispatchInput): void;
@@ -0,0 +1,21 @@
1
+ import { InvalidInputError } from "./errors.js";
2
+ export function assertDispatchInputStrict(input) {
3
+ if (!input || typeof input !== "object") {
4
+ throw new InvalidInputError("Dispatch input must be an object");
5
+ }
6
+ if (!input.intent || typeof input.intent !== "string") {
7
+ throw new InvalidInputError("Missing dispatch intent");
8
+ }
9
+ if (!input.infraKey || typeof input.infraKey !== "string") {
10
+ throw new InvalidInputError("Missing infraKey");
11
+ }
12
+ if (!input.commandType || typeof input.commandType !== "string") {
13
+ throw new InvalidInputError("Missing commandType");
14
+ }
15
+ if (input.runType !== "infra" && input.runType !== "runtime") {
16
+ throw new InvalidInputError("runType must be 'infra' or 'runtime'");
17
+ }
18
+ if (!input.resourceId || typeof input.resourceId !== "string") {
19
+ throw new InvalidInputError("Missing resourceId");
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ import { EncryptedPayload } from "../gateway/types.js";
2
+ import { EncryptOptions } from "../types/dispatcher.js";
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 | null;
@@ -0,0 +1,80 @@
1
+ import crypto from "crypto";
2
+ /**
3
+ * Encrypt payload using X25519 + HKDF + AES-256-GCM.
4
+ *
5
+ * Security properties:
6
+ * - Ephemeral client key per invocation
7
+ * - Forward secrecy
8
+ * - AEAD (authenticated encryption)
9
+ * - No plaintext on wire
10
+ */
11
+ export function encryptPayload(payload, opts) {
12
+ if (!opts?.serverPublicKeyRawB64) {
13
+ // allow caller to retry bootstrap/dispatch instead of breaking flow
14
+ return null;
15
+ }
16
+ // Normalize payload into an object we can safely enrich
17
+ let normalized;
18
+ if (payload === null || payload === undefined) {
19
+ normalized = {};
20
+ }
21
+ else if (typeof payload === "object") {
22
+ normalized = payload;
23
+ }
24
+ else {
25
+ // Primitive payloads are wrapped
26
+ normalized = { value: payload };
27
+ }
28
+ const created = Date.now();
29
+ const jti = crypto.randomBytes(16).toString("base64url");
30
+ const payloadStr = JSON.stringify(normalized);
31
+ // Load server public key from RAW X25519 bytes (base64)
32
+ const serverRaw = Buffer.from(opts.serverPublicKeyRawB64, "base64");
33
+ if (serverRaw.length !== 32) {
34
+ // invalid key (likely stale bootstrap or encoding mismatch)
35
+ // return null so dispatcher can trigger retry / bootstrap refresh
36
+ return null;
37
+ }
38
+ // Build SPKI DER for X25519 public key and import as KeyObject
39
+ // DER prefix for X25519 SPKI (OID 1.3.101.110) + BIT STRING header
40
+ const x25519SpkiPrefix = Buffer.from("302a300506032b656e032100", "hex");
41
+ const serverSpkiDer = Buffer.concat([x25519SpkiPrefix, serverRaw]);
42
+ const serverPubKey = crypto.createPublicKey({
43
+ key: serverSpkiDer,
44
+ format: "der",
45
+ type: "spki",
46
+ });
47
+ // Generate ephemeral client key pair
48
+ const clientKeyPair = crypto.generateKeyPairSync("x25519");
49
+ const clientPriv = clientKeyPair.privateKey;
50
+ const clientPub = clientKeyPair.publicKey;
51
+ // Derive shared secret
52
+ const sharedSecret = crypto.diffieHellman({
53
+ privateKey: clientPriv,
54
+ publicKey: serverPubKey,
55
+ });
56
+ // Derive symmetric key via HKDF (dynamic salt per message)
57
+ const salt = Buffer.from(`${created}.${jti}`, "utf8");
58
+ const aesKey = Buffer.from(crypto.hkdfSync("sha256", sharedSecret, salt, "viza-ecdh-key", 32));
59
+ // Encrypt using AES-256-GCM
60
+ const iv = crypto.randomBytes(12);
61
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
62
+ cipher.setAAD(Buffer.from(`${created}.${jti}`));
63
+ const encrypted = Buffer.concat([
64
+ cipher.update(payloadStr, "utf8"),
65
+ cipher.final(),
66
+ ]);
67
+ const tag = cipher.getAuthTag();
68
+ const result = {
69
+ alg: "x25519-hkdf-aes256gcm",
70
+ clientPub: clientPub
71
+ .export({ type: "spki", format: "der" })
72
+ .toString("base64"),
73
+ iv: iv.toString("base64"),
74
+ tag: tag.toString("base64"),
75
+ data: encrypted.toString("base64"),
76
+ created,
77
+ jti,
78
+ };
79
+ return result;
80
+ }
@@ -0,0 +1,7 @@
1
+ import type { DispatchResult, DispatchInput, DispatchOptions, DispatchHandle, DispatchMode } from "../types/dispatcher.js";
2
+ type EmailOtpFlowHooks = {
3
+ onBeforePrompt?: () => void;
4
+ onAfterPrompt?: () => void;
5
+ };
6
+ export declare function runEmailOtpFlow(input: DispatchInput, options: DispatchOptions, hooks?: EmailOtpFlowHooks, dispatcherCall?: (flowInput: DispatchInput, flowOptions: DispatchOptions, mode?: DispatchMode) => Promise<DispatchHandle>): Promise<DispatchResult>;
7
+ export {};