@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,94 @@
1
+ /**
2
+ * Centralized default configuration values for cli-dispatcher.
3
+ *
4
+ * All hardcoded values should be moved here for SSOT compliance.
5
+ * Environment variables can override these defaults.
6
+ *
7
+ * @module config/defaults
8
+ */
9
+ /**
10
+ * Default configuration values used across the dispatcher.
11
+ * These can be overridden via environment variables.
12
+ */
13
+ export declare const DEFAULTS: {
14
+ /**
15
+ * Base URL for authentication endpoints.
16
+ * @env VIZA_AUTH_BASE
17
+ */
18
+ readonly AUTH_BASE: "https://auth.viza.io.vn/auth";
19
+ /**
20
+ * Default OAuth callback server port.
21
+ * @env VIZA_OAUTH_PORT
22
+ */
23
+ readonly OAUTH_CALLBACK_PORT: 39123;
24
+ /**
25
+ * Default gateway request timeout in milliseconds.
26
+ * @env VIZA_GATEWAY_TIMEOUT_MS
27
+ */
28
+ readonly GATEWAY_TIMEOUT_MS: 15000;
29
+ /**
30
+ * Default session wait timeout in milliseconds (10 minutes).
31
+ * @env VIZA_SESSION_TIMEOUT_MS
32
+ */
33
+ readonly SESSION_TIMEOUT_MS: number;
34
+ /**
35
+ * Default OAuth flow timeout in milliseconds (5 minutes).
36
+ * @env VIZA_OAUTH_TIMEOUT_MS
37
+ */
38
+ readonly OAUTH_TIMEOUT_MS: number;
39
+ /**
40
+ * Default token refresh buffer in seconds.
41
+ * Access tokens are considered expired this many seconds before actual expiry.
42
+ */
43
+ readonly TOKEN_REFRESH_BUFFER_SECONDS: 60;
44
+ /**
45
+ * Default WebSocket wait timeout in milliseconds (10 minutes).
46
+ */
47
+ readonly WEBSOCKET_TIMEOUT_MS: number;
48
+ };
49
+ /**
50
+ * Environment variable names for configuration overrides.
51
+ */
52
+ export declare const ENV_KEYS: {
53
+ readonly AUTH_BASE: "VIZA_AUTH_BASE";
54
+ readonly OAUTH_PORT: "VIZA_OAUTH_PORT";
55
+ readonly GATEWAY_TIMEOUT_MS: "VIZA_GATEWAY_TIMEOUT_MS";
56
+ readonly SESSION_TIMEOUT_MS: "VIZA_SESSION_TIMEOUT_MS";
57
+ readonly OAUTH_TIMEOUT_MS: "VIZA_OAUTH_TIMEOUT_MS";
58
+ };
59
+ /**
60
+ * Runtime configuration derived from defaults and environment variables.
61
+ */
62
+ export interface DispatcherConfig {
63
+ /** Base URL for authentication endpoints */
64
+ authBase: string;
65
+ /** OAuth callback server port */
66
+ oauthPort: number;
67
+ /** Gateway request timeout in ms */
68
+ gatewayTimeoutMs: number;
69
+ /** Session wait timeout in ms */
70
+ sessionTimeoutMs: number;
71
+ /** OAuth flow timeout in ms */
72
+ oauthTimeoutMs: number;
73
+ }
74
+ /**
75
+ * Create configuration with environment variable overrides.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * import { createConfig } from './config/defaults.js';
80
+ *
81
+ * const config = createConfig();
82
+ * console.log(config.authBase); // 'https://auth.viza.io.vn/auth' or env override
83
+ * ```
84
+ */
85
+ export declare function createConfig(): DispatcherConfig;
86
+ /**
87
+ * Get the global configuration instance.
88
+ * Creates and caches configuration on first call.
89
+ */
90
+ export declare function getConfig(): DispatcherConfig;
91
+ /**
92
+ * Reset configuration (useful for testing).
93
+ */
94
+ export declare function resetConfig(): void;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Centralized default configuration values for cli-dispatcher.
3
+ *
4
+ * All hardcoded values should be moved here for SSOT compliance.
5
+ * Environment variables can override these defaults.
6
+ *
7
+ * @module config/defaults
8
+ */
9
+ /**
10
+ * Default configuration values used across the dispatcher.
11
+ * These can be overridden via environment variables.
12
+ */
13
+ export const DEFAULTS = {
14
+ /**
15
+ * Base URL for authentication endpoints.
16
+ * @env VIZA_AUTH_BASE
17
+ */
18
+ AUTH_BASE: 'https://auth.viza.io.vn/auth',
19
+ /**
20
+ * Default OAuth callback server port.
21
+ * @env VIZA_OAUTH_PORT
22
+ */
23
+ OAUTH_CALLBACK_PORT: 39123,
24
+ /**
25
+ * Default gateway request timeout in milliseconds.
26
+ * @env VIZA_GATEWAY_TIMEOUT_MS
27
+ */
28
+ GATEWAY_TIMEOUT_MS: 15_000,
29
+ /**
30
+ * Default session wait timeout in milliseconds (10 minutes).
31
+ * @env VIZA_SESSION_TIMEOUT_MS
32
+ */
33
+ SESSION_TIMEOUT_MS: 10 * 60 * 1000,
34
+ /**
35
+ * Default OAuth flow timeout in milliseconds (5 minutes).
36
+ * @env VIZA_OAUTH_TIMEOUT_MS
37
+ */
38
+ OAUTH_TIMEOUT_MS: 5 * 60 * 1000,
39
+ /**
40
+ * Default token refresh buffer in seconds.
41
+ * Access tokens are considered expired this many seconds before actual expiry.
42
+ */
43
+ TOKEN_REFRESH_BUFFER_SECONDS: 60,
44
+ /**
45
+ * Default WebSocket wait timeout in milliseconds (10 minutes).
46
+ */
47
+ WEBSOCKET_TIMEOUT_MS: 10 * 60 * 1000,
48
+ };
49
+ /**
50
+ * Environment variable names for configuration overrides.
51
+ */
52
+ export const ENV_KEYS = {
53
+ AUTH_BASE: 'VIZA_AUTH_BASE',
54
+ OAUTH_PORT: 'VIZA_OAUTH_PORT',
55
+ GATEWAY_TIMEOUT_MS: 'VIZA_GATEWAY_TIMEOUT_MS',
56
+ SESSION_TIMEOUT_MS: 'VIZA_SESSION_TIMEOUT_MS',
57
+ OAUTH_TIMEOUT_MS: 'VIZA_OAUTH_TIMEOUT_MS',
58
+ };
59
+ /**
60
+ * Parse a positive integer from environment variable.
61
+ * Returns undefined if not set or invalid.
62
+ */
63
+ function parseEnvInt(value) {
64
+ if (!value)
65
+ return undefined;
66
+ const parsed = parseInt(value, 10);
67
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
68
+ }
69
+ /**
70
+ * Parse a string from environment variable.
71
+ * Returns undefined if empty or not set.
72
+ */
73
+ function parseEnvString(value) {
74
+ return value?.trim() || undefined;
75
+ }
76
+ /**
77
+ * Create configuration with environment variable overrides.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * import { createConfig } from './config/defaults.js';
82
+ *
83
+ * const config = createConfig();
84
+ * console.log(config.authBase); // 'https://auth.viza.io.vn/auth' or env override
85
+ * ```
86
+ */
87
+ export function createConfig() {
88
+ return {
89
+ authBase: parseEnvString(process.env[ENV_KEYS.AUTH_BASE]) ?? DEFAULTS.AUTH_BASE,
90
+ oauthPort: parseEnvInt(process.env[ENV_KEYS.OAUTH_PORT]) ?? DEFAULTS.OAUTH_CALLBACK_PORT,
91
+ gatewayTimeoutMs: parseEnvInt(process.env[ENV_KEYS.GATEWAY_TIMEOUT_MS]) ?? DEFAULTS.GATEWAY_TIMEOUT_MS,
92
+ sessionTimeoutMs: parseEnvInt(process.env[ENV_KEYS.SESSION_TIMEOUT_MS]) ?? DEFAULTS.SESSION_TIMEOUT_MS,
93
+ oauthTimeoutMs: parseEnvInt(process.env[ENV_KEYS.OAUTH_TIMEOUT_MS]) ?? DEFAULTS.OAUTH_TIMEOUT_MS,
94
+ };
95
+ }
96
+ /**
97
+ * Cached configuration instance.
98
+ * Lazily initialized on first access.
99
+ */
100
+ let _config;
101
+ /**
102
+ * Get the global configuration instance.
103
+ * Creates and caches configuration on first call.
104
+ */
105
+ export function getConfig() {
106
+ if (!_config) {
107
+ _config = createConfig();
108
+ }
109
+ return _config;
110
+ }
111
+ /**
112
+ * Reset configuration (useful for testing).
113
+ */
114
+ export function resetConfig() {
115
+ _config = undefined;
116
+ }
@@ -0,0 +1,15 @@
1
+ import type { DispatchOptions } from "../types/dispatcher.js";
2
+ import type { RuntimeContext } from "../runtime/runtime-context.js";
3
+ import type { GatewayBootstrapConfig } from "./bootstrap.js";
4
+ export interface AuthBootstrapResult {
5
+ runtimeContext: RuntimeContext;
6
+ bootstrapCfg: GatewayBootstrapConfig;
7
+ }
8
+ /**
9
+ * Ensure the dispatcher runtime is authenticated and bootstrapped
10
+ * for the given target environment.
11
+ *
12
+ * Returns both the runtime context (for identity/claims) and
13
+ * the bootstrap config (for gateway dispatch).
14
+ */
15
+ export declare function ensureAuthAndBootstrap(options: DispatchOptions): Promise<AuthBootstrapResult>;
@@ -0,0 +1,33 @@
1
+ import { InvalidInputError } from "./errors.js";
2
+ import { getConfig } from "../config/defaults.js";
3
+ import { getAuthRuntime, getAuthRuntimeRefreshEndpoint, initAuthRuntime } from "./runtime.js";
4
+ import { ensureAuthenticated } from "../auth/session/session-manager.js";
5
+ import { ensureBootstrapped } from "./ensure-bootstrap.js";
6
+ /**
7
+ * Ensure the dispatcher runtime is authenticated and bootstrapped
8
+ * for the given target environment.
9
+ *
10
+ * Returns both the runtime context (for identity/claims) and
11
+ * the bootstrap config (for gateway dispatch).
12
+ */
13
+ export async function ensureAuthAndBootstrap(options) {
14
+ if (!options.auth) {
15
+ throw new InvalidInputError("Missing authentication options. Anonymous dispatch is not allowed.");
16
+ }
17
+ const config = getConfig();
18
+ const refreshEndpoint = `${config.authBase}/refresh`;
19
+ if (getAuthRuntimeRefreshEndpoint() !== refreshEndpoint) {
20
+ initAuthRuntime({ refreshEndpoint });
21
+ }
22
+ const runtimeContext = await ensureAuthenticated(getAuthRuntime(), {
23
+ loginEndpoint: `${config.authBase}/login`,
24
+ exchangeEndpoint: `${config.authBase}/exchange`,
25
+ refreshEndpoint,
26
+ logoutEndpoint: `${config.authBase}/logout`,
27
+ });
28
+ const bootstrapCfg = await ensureBootstrapped(options.auth.targetEnv);
29
+ if (!bootstrapCfg) {
30
+ throw new InvalidInputError("Bootstrap config is required for dispatch operations.");
31
+ }
32
+ return { runtimeContext, bootstrapCfg };
33
+ }
@@ -0,0 +1,73 @@
1
+ import { RuntimeEnv } from "../types/runtime-env.js";
2
+ export type GatewayBootstrapConfig = {
3
+ endpoint: string;
4
+ encrypt: {
5
+ alg: "X25519";
6
+ format: "raw";
7
+ encoding: "base64";
8
+ publicKey: string;
9
+ };
10
+ auth?: {
11
+ loginEndpoint?: string;
12
+ exchangeEndpoint?: string;
13
+ refreshEndpoint?: string;
14
+ logoutEndpoint?: string;
15
+ };
16
+ };
17
+ /**
18
+ * Bootstrap config map returned by the gateway exchange & bootstrap endpoints.
19
+ * Keys are environment names (e.g. "dev", "prod").
20
+ */
21
+ export type GatewayBootstrapMap = Record<string, GatewayBootstrapConfig>;
22
+ export declare function getLocalGatewayConfigPath(runtimeEnv: RuntimeEnv): string;
23
+ export declare function readLocalGatewayConfig(runtimeEnv: RuntimeEnv): GatewayBootstrapConfig | null;
24
+ export declare function writeLocalGatewayConfig(runtimeEnv: RuntimeEnv, cfg: GatewayBootstrapConfig): void;
25
+ /**
26
+ * Persist all bootstrap configs from a gateway bootstrap map to local storage.
27
+ * Overwrites existing cached configs.
28
+ */
29
+ export declare function persistBootstrapMap(bootstrapMap: GatewayBootstrapMap): void;
30
+ /**
31
+ * Refresh bootstrap config(s) from the authenticated gateway.
32
+ *
33
+ * Calls GET /auth/bootstrap with the current access token,
34
+ * then persists received configs locally.
35
+ *
36
+ * The bootstrap endpoint is derived internally from the auth runtime
37
+ * (never accepted from untrusted client input).
38
+ *
39
+ * @param runtimeEnv - If provided, only this env's config is returned.
40
+ * If omitted, all configs are fetched and persisted.
41
+ */
42
+ export declare function refreshBootstrapFromGateway(runtimeEnv?: RuntimeEnv): Promise<GatewayBootstrapConfig>;
43
+ /**
44
+ * Load bootstrap config for the given runtime environment.
45
+ *
46
+ * Strategy:
47
+ * 1. Read from local cache. If valid, return immediately.
48
+ * 2. If forceRefresh=true, call gateway /auth/bootstrap.
49
+ * 3. If autoRefresh=true and no local cache, call gateway /auth/bootstrap.
50
+ * 4. Otherwise throw.
51
+ */
52
+ export interface LoadGatewayBootstrapConfigOptions {
53
+ autoRefresh?: boolean;
54
+ forceRefresh?: boolean;
55
+ /**
56
+ * If true, return null instead of throwing when bootstrap isn't available
57
+ * and there's no authenticated session to refresh from.
58
+ * This is useful for auth flows (login/logout) that shouldn't be blocked
59
+ * by missing bootstrap config.
60
+ */
61
+ optionalIfNoAuth?: boolean;
62
+ }
63
+ export declare function loadGatewayBootstrapConfig(runtimeEnv: RuntimeEnv, options?: LoadGatewayBootstrapConfigOptions): Promise<GatewayBootstrapConfig | null>;
64
+ /**
65
+ * Manually refresh gateway bootstrap config for a specific environment.
66
+ * This is the function exposed to the CLI `bootstrap` command.
67
+ *
68
+ * The bootstrap endpoint is derived internally from the auth runtime
69
+ * (never accepted from untrusted client input).
70
+ *
71
+ * @param runtimeEnv - Target environment (e.g. "dev", "prod")
72
+ */
73
+ export declare function bootstrapGatewayConfig(runtimeEnv: RuntimeEnv): Promise<GatewayBootstrapConfig>;
@@ -0,0 +1,248 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { assertRuntimeEnv } from "../types/runtime-env.js";
5
+ import { buildAuthHeader } from "../gateway/auth-header.js";
6
+ import { getAuthRuntime, initAuthRuntime, tryGetAuthRuntime } from "./runtime.js";
7
+ export function getLocalGatewayConfigPath(runtimeEnv) {
8
+ if (process.platform === "win32") {
9
+ const appData = process.env["APPDATA"]?.trim() ||
10
+ (process.env["USERPROFILE"]?.trim()
11
+ ? path.join(process.env["USERPROFILE"], "AppData", "Roaming")
12
+ : undefined);
13
+ if (!appData) {
14
+ throw new Error("\nMissing APPDATA/USERPROFILE env on Windows");
15
+ }
16
+ return path.join(appData, "viza", `gateway-${runtimeEnv}.json`);
17
+ }
18
+ return path.join(os.homedir(), ".config", "viza", `gateway-${runtimeEnv}.json`);
19
+ }
20
+ export function readLocalGatewayConfig(runtimeEnv) {
21
+ const filePath = getLocalGatewayConfigPath(runtimeEnv);
22
+ if (!fs.existsSync(filePath)) {
23
+ return null;
24
+ }
25
+ try {
26
+ const raw = fs.readFileSync(filePath, "utf8");
27
+ const data = JSON.parse(raw);
28
+ assertGatewayBootstrapConfig(data, runtimeEnv);
29
+ return data;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function assertGatewayBootstrapConfig(data, runtimeEnv) {
36
+ if (!data || typeof data !== "object") {
37
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: expected an object`);
38
+ }
39
+ if (typeof data.endpoint !== "string" || !data.endpoint) {
40
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: missing endpoint`);
41
+ }
42
+ assertHttpsUrl(data.endpoint, `gateway-${runtimeEnv}.endpoint`);
43
+ const encrypt = data.encrypt;
44
+ if (!encrypt || typeof encrypt !== "object") {
45
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: missing encrypt`);
46
+ }
47
+ if (encrypt.alg !== "X25519") {
48
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: encrypt.alg must be X25519`);
49
+ }
50
+ if (encrypt.format !== "raw") {
51
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: encrypt.format must be raw`);
52
+ }
53
+ if (encrypt.encoding !== "base64") {
54
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: encrypt.encoding must be base64`);
55
+ }
56
+ if (typeof encrypt.publicKey !== "string" || !encrypt.publicKey) {
57
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: missing encrypt.publicKey`);
58
+ }
59
+ assertX25519RawPublicKeyBase64(encrypt.publicKey, `gateway-${runtimeEnv}.encrypt.publicKey`);
60
+ if (data.auth !== undefined) {
61
+ if (!data.auth || typeof data.auth !== "object") {
62
+ throw new Error(`\nInvalid gateway-${runtimeEnv}.json: auth must be an object`);
63
+ }
64
+ if (data.auth.loginEndpoint !== undefined) {
65
+ assertHttpsUrl(data.auth.loginEndpoint, `gateway-${runtimeEnv}.auth.loginEndpoint`);
66
+ }
67
+ if (data.auth.exchangeEndpoint !== undefined) {
68
+ assertHttpsUrl(data.auth.exchangeEndpoint, `gateway-${runtimeEnv}.auth.exchangeEndpoint`);
69
+ }
70
+ if (data.auth.refreshEndpoint !== undefined) {
71
+ assertHttpsUrl(data.auth.refreshEndpoint, `gateway-${runtimeEnv}.auth.refreshEndpoint`);
72
+ }
73
+ if (data.auth.logoutEndpoint !== undefined) {
74
+ assertHttpsUrl(data.auth.logoutEndpoint, `gateway-${runtimeEnv}.auth.logoutEndpoint`);
75
+ }
76
+ }
77
+ }
78
+ function assertHttpsUrl(value, label) {
79
+ let u;
80
+ try {
81
+ u = new URL(value);
82
+ }
83
+ catch {
84
+ throw new Error(`\nInvalid ${label}: must be a valid URL`);
85
+ }
86
+ if (u.protocol !== "https:") {
87
+ throw new Error(`\nInvalid ${label}: must use https`);
88
+ }
89
+ }
90
+ function assertX25519RawPublicKeyBase64(value, label) {
91
+ const raw = Buffer.from(value, "base64");
92
+ if (raw.length !== 32) {
93
+ throw new Error(`\nInvalid ${label}: expected 32-byte X25519 raw key`);
94
+ }
95
+ }
96
+ export function writeLocalGatewayConfig(runtimeEnv, cfg) {
97
+ const filePath = getLocalGatewayConfigPath(runtimeEnv);
98
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
99
+ fs.writeFileSync(filePath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
100
+ }
101
+ /**
102
+ * Persist all bootstrap configs from a gateway bootstrap map to local storage.
103
+ * Overwrites existing cached configs.
104
+ */
105
+ export function persistBootstrapMap(bootstrapMap) {
106
+ for (const [env, cfg] of Object.entries(bootstrapMap)) {
107
+ const runtimeEnv = env;
108
+ assertRuntimeEnv(runtimeEnv);
109
+ assertGatewayBootstrapConfig(cfg, runtimeEnv);
110
+ writeLocalGatewayConfig(runtimeEnv, cfg);
111
+ }
112
+ }
113
+ function getBootstrapEndpoint() {
114
+ return getAuthRuntime().getBootstrapEndpoint();
115
+ }
116
+ /**
117
+ * Refresh bootstrap config(s) from the authenticated gateway.
118
+ *
119
+ * Calls GET /auth/bootstrap with the current access token,
120
+ * then persists received configs locally.
121
+ *
122
+ * The bootstrap endpoint is derived internally from the auth runtime
123
+ * (never accepted from untrusted client input).
124
+ *
125
+ * @param runtimeEnv - If provided, only this env's config is returned.
126
+ * If omitted, all configs are fetched and persisted.
127
+ */
128
+ export async function refreshBootstrapFromGateway(runtimeEnv) {
129
+ console.log("[auth] refreshing gateway bootstrap config...");
130
+ const bootstrapEndpoint = getBootstrapEndpoint();
131
+ let res;
132
+ try {
133
+ const headers = await buildAuthHeader();
134
+ res = await fetch(bootstrapEndpoint, {
135
+ method: "GET",
136
+ headers,
137
+ });
138
+ }
139
+ catch (err) {
140
+ console.error(`[auth] failed to refresh gateway bootstrap config: ${err.message}`);
141
+ throw new Error(`\nFailed to reach gateway bootstrap endpoint.\n${err.message}`);
142
+ }
143
+ if (!res.ok) {
144
+ const body = await res.text().catch(() => "");
145
+ console.error(`[auth] failed to refresh gateway bootstrap config: HTTP ${res.status}`);
146
+ throw new Error(`\nGateway bootstrap refresh failed: HTTP ${res.status}${body ? `\n${body}` : ""}`);
147
+ }
148
+ let json;
149
+ try {
150
+ json = (await res.json());
151
+ }
152
+ catch {
153
+ console.error("\n[auth] failed to refresh gateway bootstrap config: invalid JSON response");
154
+ throw new Error("\nGateway bootstrap response is not valid JSON");
155
+ }
156
+ if (json.success !== true || !json.bootstrap) {
157
+ console.error("\n[auth] failed to refresh gateway bootstrap config: invalid response structure");
158
+ throw new Error("\nGateway bootstrap response missing success or bootstrap field");
159
+ }
160
+ // Persist all received configs locally
161
+ persistBootstrapMap(json.bootstrap);
162
+ console.log("\n[auth] gateway bootstrap config refreshed successfully");
163
+ // Return the requested env's config, or the only one available
164
+ if (runtimeEnv) {
165
+ const cfg = json.bootstrap[runtimeEnv];
166
+ if (!cfg) {
167
+ const available = Object.keys(json.bootstrap).join(", ");
168
+ throw new Error(`\nGateway bootstrap response does not contain config for "${runtimeEnv}". Available: [${available}]`);
169
+ }
170
+ return cfg;
171
+ }
172
+ // If no specific env requested and there's exactly one, return it
173
+ const keys = Object.keys(json.bootstrap);
174
+ if (keys.length === 1) {
175
+ return json.bootstrap[keys[0]];
176
+ }
177
+ throw new Error("\nMultiple bootstrap environments returned but no target specified. " +
178
+ `Available: [${keys.join(", ")}]`);
179
+ }
180
+ export async function loadGatewayBootstrapConfig(runtimeEnv, options) {
181
+ assertRuntimeEnv(runtimeEnv);
182
+ if (options?.forceRefresh) {
183
+ return await refreshBootstrapFromGateway(runtimeEnv);
184
+ }
185
+ const local = readLocalGatewayConfig(runtimeEnv);
186
+ if (local) {
187
+ return local;
188
+ }
189
+ if (options?.autoRefresh) {
190
+ // Only attempt auto-refresh when an auth runtime is available and
191
+ // there is a refresh token to use. Calling the gateway bootstrap
192
+ // endpoint without a refresh token leads to noisy failures when
193
+ // running auth login/logout (the CLI may check bootstrap before
194
+ // login). Avoid making the network call in that case and return null
195
+ // to indicate bootstrap is not available yet.
196
+ //
197
+ // IMPORTANT: This breaks the circular dependency:
198
+ // - login no longer requires bootstrap to exist
199
+ // - bootstrap refresh only happens AFTER authentication succeeds
200
+ const rt = tryGetAuthRuntime();
201
+ if (!rt) {
202
+ // No auth runtime - return null to indicate bootstrap not available.
203
+ // This allows auth flows (login/logout) to proceed without requiring
204
+ // bootstrap config. Bootstrap will be fetched after successful login.
205
+ return null;
206
+ }
207
+ // Check for presence of refresh token before attempting network refresh.
208
+ // If there's no refresh token, don't call the gateway (it would fail
209
+ // with a confusing "Missing refresh token" error). Return null instead.
210
+ const refreshToken = await rt.refreshTokenService.get();
211
+ if (!refreshToken) {
212
+ // No refresh token - return null to allow login flow to proceed.
213
+ return null;
214
+ }
215
+ return await refreshBootstrapFromGateway(runtimeEnv);
216
+ }
217
+ // No autoRefresh - if optionalIfNoAuth is true, return null instead of throwing
218
+ if (options?.optionalIfNoAuth) {
219
+ return null;
220
+ }
221
+ throw new Error(`\nLocal gateway-${runtimeEnv}.json missing or invalid. Please log in to refresh.`);
222
+ }
223
+ // ─── Client-facing bootstrap command ──────────────────────────────────────
224
+ /**
225
+ * Manually refresh gateway bootstrap config for a specific environment.
226
+ * This is the function exposed to the CLI `bootstrap` command.
227
+ *
228
+ * The bootstrap endpoint is derived internally from the auth runtime
229
+ * (never accepted from untrusted client input).
230
+ *
231
+ * @param runtimeEnv - Target environment (e.g. "dev", "prod")
232
+ */
233
+ export async function bootstrapGatewayConfig(runtimeEnv) {
234
+ console.log(`\n[auth] bootstrapping gateway config for ${runtimeEnv}...`);
235
+ // Ensure the auth runtime is initialized. The bootstrap flow needs the
236
+ // auth runtime to derive the bootstrap endpoint (it transforms the
237
+ // configured refresh endpoint into a bootstrap endpoint). When the CLI
238
+ // calls this function directly it may not have initialized the runtime
239
+ // yet, so initialize it with a sensible default (or override via
240
+ // VIZA_AUTH_REFRESH_ENDPOINT).
241
+ if (!tryGetAuthRuntime()) {
242
+ const defaultRefresh = process.env["VIZA_AUTH_REFRESH_ENDPOINT"] ?? "https://auth.viza.io.vn/auth/refresh";
243
+ initAuthRuntime({ refreshEndpoint: defaultRefresh });
244
+ }
245
+ const cfg = await refreshBootstrapFromGateway(runtimeEnv);
246
+ console.log(`\n[auth] gateway config for ${runtimeEnv} bootstrapped successfully`);
247
+ return cfg;
248
+ }
@@ -0,0 +1,2 @@
1
+ import { DispatchHandle, DispatchInput, DispatchMode, DispatchOptions } from "../types/dispatcher.js";
2
+ export declare function dispatcherDispatch(input: DispatchInput, options: DispatchOptions, mode?: DispatchMode): Promise<DispatchHandle>;
@@ -0,0 +1,28 @@
1
+ import { runEmailOtpFlow } from "../flows/email-otp-flow.js";
2
+ import { assertDispatchInputStrict } from "./validation.js";
3
+ import { executeDispatchLifecycle } from "./runtime-orchestrator.js";
4
+ export async function dispatcherDispatch(input, options, mode = "dispatch") {
5
+ assertDispatchInputStrict(input);
6
+ if (options.interactiveAuthFlow !== false && input.intent === "email.provision.otp") {
7
+ return {
8
+ sessionId: "interactive-email-otp",
9
+ wsUrl: "",
10
+ // Interactive flows may start before access token is available.
11
+ currentUser: "interactive",
12
+ async wait() {
13
+ // Forward CLI-provided prompt hooks so the runtime spinner /
14
+ // async log renderer can be suspended while readline owns stdout.
15
+ return runEmailOtpFlow(input, options, options.interactivePromptHooks, dispatcherDispatch);
16
+ },
17
+ };
18
+ }
19
+ const lifecycle = await executeDispatchLifecycle(input, options, mode);
20
+ return {
21
+ sessionId: lifecycle.sessionId,
22
+ wsUrl: "",
23
+ currentUser: lifecycle.runtimeContext.claims.login ?? "unknown",
24
+ async wait() {
25
+ return lifecycle.result;
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,3 @@
1
+ import { GatewayBootstrapConfig } from "./bootstrap.js";
2
+ import { RuntimeEnv } from "../types/runtime-env.js";
3
+ export declare function ensureBootstrapped(runtimeEnv: RuntimeEnv): Promise<GatewayBootstrapConfig>;
@@ -0,0 +1,25 @@
1
+ import { loadGatewayBootstrapConfig, } from "./bootstrap.js";
2
+ import { DispatchRejectedError } from "./errors.js";
3
+ export async function ensureBootstrapped(runtimeEnv) {
4
+ // First try to load from local cache (non-auto refresh)
5
+ const local = await loadGatewayBootstrapConfig(runtimeEnv);
6
+ if (local) {
7
+ return local;
8
+ }
9
+ // Attempt auto-refresh using authenticated gateway (this may throw)
10
+ try {
11
+ const refreshed = await loadGatewayBootstrapConfig(runtimeEnv, {
12
+ autoRefresh: true,
13
+ });
14
+ if (refreshed) {
15
+ return refreshed;
16
+ }
17
+ }
18
+ catch (e) {
19
+ throw new DispatchRejectedError("Missing gateway bootstrap config and auto-bootstrap failed.\n" +
20
+ "Please log in to refresh your bootstrap config.", { cause: e });
21
+ }
22
+ // If we reached here, bootstrap was not available and auto-refresh did not produce one
23
+ throw new DispatchRejectedError("Missing gateway bootstrap config and auto-bootstrap failed.\n" +
24
+ "Please log in to refresh your bootstrap config.");
25
+ }