@databricks/appkit 0.21.0 → 0.23.0

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 (125) hide show
  1. package/CLAUDE.md +11 -0
  2. package/NOTICE.md +1 -0
  3. package/README.md +3 -20
  4. package/dist/appkit/package.js +1 -1
  5. package/dist/cli/commands/generate-types.js +15 -13
  6. package/dist/cli/commands/generate-types.js.map +1 -1
  7. package/dist/cli/commands/setup.js +2 -2
  8. package/dist/cli/commands/setup.js.map +1 -1
  9. package/dist/connectors/genie/client.js +50 -0
  10. package/dist/connectors/genie/client.js.map +1 -1
  11. package/dist/connectors/serving/client.js +47 -0
  12. package/dist/connectors/serving/client.js.map +1 -0
  13. package/dist/index.d.ts +6 -1
  14. package/dist/index.js +4 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugin/execution-result.d.ts +26 -0
  17. package/dist/plugin/execution-result.d.ts.map +1 -0
  18. package/dist/plugin/index.d.ts +1 -0
  19. package/dist/plugin/interceptors/retry.js +1 -1
  20. package/dist/plugin/interceptors/retry.js.map +1 -1
  21. package/dist/plugin/plugin.d.ts +54 -5
  22. package/dist/plugin/plugin.d.ts.map +1 -1
  23. package/dist/plugin/plugin.js +87 -7
  24. package/dist/plugin/plugin.js.map +1 -1
  25. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  26. package/dist/plugins/analytics/analytics.js +2 -3
  27. package/dist/plugins/analytics/analytics.js.map +1 -1
  28. package/dist/plugins/files/plugin.d.ts +2 -0
  29. package/dist/plugins/files/plugin.d.ts.map +1 -1
  30. package/dist/plugins/files/plugin.js +39 -59
  31. package/dist/plugins/files/plugin.js.map +1 -1
  32. package/dist/plugins/genie/genie.d.ts +1 -0
  33. package/dist/plugins/genie/genie.d.ts.map +1 -1
  34. package/dist/plugins/genie/genie.js +42 -3
  35. package/dist/plugins/genie/genie.js.map +1 -1
  36. package/dist/plugins/index.d.ts +4 -1
  37. package/dist/plugins/index.js +2 -0
  38. package/dist/plugins/server/base-server.js +4 -2
  39. package/dist/plugins/server/base-server.js.map +1 -1
  40. package/dist/plugins/server/client-config-sanitizer.js +184 -0
  41. package/dist/plugins/server/client-config-sanitizer.js.map +1 -0
  42. package/dist/plugins/server/index.d.ts +3 -2
  43. package/dist/plugins/server/index.d.ts.map +1 -1
  44. package/dist/plugins/server/index.js +27 -9
  45. package/dist/plugins/server/index.js.map +1 -1
  46. package/dist/plugins/server/remote-tunnel/denied.html +68 -0
  47. package/dist/plugins/server/remote-tunnel/index.html +165 -0
  48. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +2 -1
  49. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  50. package/dist/plugins/server/remote-tunnel/wait.html +158 -0
  51. package/dist/plugins/server/static-server.js +2 -2
  52. package/dist/plugins/server/static-server.js.map +1 -1
  53. package/dist/plugins/server/utils.js +28 -5
  54. package/dist/plugins/server/utils.js.map +1 -1
  55. package/dist/plugins/server/vite-dev-server.js +8 -3
  56. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  57. package/dist/plugins/serving/defaults.js +10 -0
  58. package/dist/plugins/serving/defaults.js.map +1 -0
  59. package/dist/plugins/serving/index.d.ts +2 -0
  60. package/dist/plugins/serving/index.js +3 -0
  61. package/dist/plugins/serving/manifest.js +53 -0
  62. package/dist/plugins/serving/manifest.js.map +1 -0
  63. package/dist/plugins/serving/schema-filter.js +52 -0
  64. package/dist/plugins/serving/schema-filter.js.map +1 -0
  65. package/dist/plugins/serving/serving.d.ts +38 -0
  66. package/dist/plugins/serving/serving.d.ts.map +1 -0
  67. package/dist/plugins/serving/serving.js +213 -0
  68. package/dist/plugins/serving/serving.js.map +1 -0
  69. package/dist/plugins/serving/types.d.ts +58 -0
  70. package/dist/plugins/serving/types.d.ts.map +1 -0
  71. package/dist/shared/src/execute.d.ts +1 -1
  72. package/dist/shared/src/plugin.d.ts +1 -0
  73. package/dist/shared/src/plugin.d.ts.map +1 -1
  74. package/dist/stream/stream-manager.js +1 -0
  75. package/dist/stream/stream-manager.js.map +1 -1
  76. package/dist/stream/types.js +2 -1
  77. package/dist/stream/types.js.map +1 -1
  78. package/dist/type-generator/cache.js +1 -1
  79. package/dist/type-generator/cache.js.map +1 -1
  80. package/dist/type-generator/index.js +13 -1
  81. package/dist/type-generator/index.js.map +1 -1
  82. package/dist/type-generator/query-registry.js +77 -4
  83. package/dist/type-generator/query-registry.js.map +1 -1
  84. package/dist/type-generator/serving/cache.js +38 -0
  85. package/dist/type-generator/serving/cache.js.map +1 -0
  86. package/dist/type-generator/serving/converter.js +108 -0
  87. package/dist/type-generator/serving/converter.js.map +1 -0
  88. package/dist/type-generator/serving/fetcher.js +54 -0
  89. package/dist/type-generator/serving/fetcher.js.map +1 -0
  90. package/dist/type-generator/serving/generator.js +185 -0
  91. package/dist/type-generator/serving/generator.js.map +1 -0
  92. package/dist/type-generator/serving/server-file-extractor.d.ts +22 -0
  93. package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -0
  94. package/dist/type-generator/serving/server-file-extractor.js +131 -0
  95. package/dist/type-generator/serving/server-file-extractor.js.map +1 -0
  96. package/dist/type-generator/serving/vite-plugin.d.ts +24 -0
  97. package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -0
  98. package/dist/type-generator/serving/vite-plugin.js +60 -0
  99. package/dist/type-generator/serving/vite-plugin.js.map +1 -0
  100. package/docs/api/appkit/Class.Plugin.md +83 -20
  101. package/docs/api/appkit/Function.appKitServingTypesPlugin.md +24 -0
  102. package/docs/api/appkit/Function.extractServingEndpoints.md +22 -0
  103. package/docs/api/appkit/Function.findServerFile.md +20 -0
  104. package/docs/api/appkit/Interface.EndpointConfig.md +23 -0
  105. package/docs/api/appkit/Interface.ServingEndpointEntry.md +30 -0
  106. package/docs/api/appkit/Interface.ServingEndpointRegistry.md +3 -0
  107. package/docs/api/appkit/TypeAlias.ExecutionResult.md +36 -0
  108. package/docs/api/appkit/TypeAlias.ServingFactory.md +15 -0
  109. package/docs/api/appkit.md +39 -31
  110. package/docs/app-management.md +1 -1
  111. package/docs/architecture.md +1 -1
  112. package/docs/development/ai-assisted-development.md +2 -2
  113. package/docs/development/local-development.md +1 -1
  114. package/docs/development/remote-bridge.md +1 -1
  115. package/docs/development/templates.md +93 -0
  116. package/docs/development.md +1 -1
  117. package/docs/faq.md +66 -0
  118. package/docs/plugins/caching.md +3 -1
  119. package/docs/plugins/execution-context.md +1 -1
  120. package/docs/plugins/lakebase.md +1 -1
  121. package/docs/plugins/serving.md +223 -0
  122. package/docs.md +2 -2
  123. package/llms.txt +11 -0
  124. package/package.json +37 -36
  125. package/sbom.cdx.json +1 -0
@@ -0,0 +1,184 @@
1
+ import { createLogger } from "../../logging/logger.js";
2
+ import pc from "picocolors";
3
+
4
+ //#region src/plugins/server/client-config-sanitizer.ts
5
+ const logger = createLogger("server:config");
6
+ const REDACTED_CLIENT_CONFIG_VALUE = "[redacted by appkit]";
7
+ const MIN_SUBSTRING_LENGTH = 3;
8
+ const DISALLOWED_CLIENT_CONFIG_KEYS = new Set([
9
+ "__proto__",
10
+ "constructor",
11
+ "prototype"
12
+ ]);
13
+ /**
14
+ * Builds a Map of non-public env var values (value -> key names)
15
+ * and a Set of public env var values for overlap resolution.
16
+ */
17
+ function getEnvValueSets() {
18
+ const nonPublic = /* @__PURE__ */ new Map();
19
+ const publicValues = /* @__PURE__ */ new Set();
20
+ for (const [key, value] of Object.entries(process.env)) {
21
+ if (!value) continue;
22
+ if (key.startsWith("PUBLIC_APPKIT_")) publicValues.add(value);
23
+ else {
24
+ const existing = nonPublic.get(value);
25
+ if (existing) existing.push(key);
26
+ else nonPublic.set(value, [key]);
27
+ }
28
+ }
29
+ return {
30
+ nonPublic,
31
+ publicValues
32
+ };
33
+ }
34
+ function getMatchRanges(haystack, needle) {
35
+ const ranges = [];
36
+ let startIndex = 0;
37
+ while (startIndex < haystack.length) {
38
+ const matchIndex = haystack.indexOf(needle, startIndex);
39
+ if (matchIndex === -1) break;
40
+ ranges.push([matchIndex, matchIndex + needle.length]);
41
+ startIndex = matchIndex + 1;
42
+ }
43
+ return ranges;
44
+ }
45
+ function isSecretCoveredByPublicValue(value, envValue, publicValues) {
46
+ const publicRanges = [...publicValues].filter((publicValue) => publicValue.includes(envValue)).flatMap((publicValue) => getMatchRanges(value, publicValue));
47
+ if (publicRanges.length === 0) return false;
48
+ return getMatchRanges(value, envValue).every(([secretStart, secretEnd]) => publicRanges.some(([publicStart, publicEnd]) => publicStart <= secretStart && publicEnd >= secretEnd));
49
+ }
50
+ function isPlainObject(value) {
51
+ const proto = Object.getPrototypeOf(value);
52
+ return proto === Object.prototype || proto === null;
53
+ }
54
+ function invalidClientConfig(pluginName, path, message) {
55
+ return /* @__PURE__ */ new Error(`Plugin '${pluginName}' clientConfig() ${message} at ${path}. Only JSON-serializable plain data is supported.`);
56
+ }
57
+ function assertSafeClientConfigKey(pluginName, key, path) {
58
+ if (DISALLOWED_CLIENT_CONFIG_KEYS.has(key)) throw invalidClientConfig(pluginName, `${path}.${key}`, "contains a reserved key");
59
+ }
60
+ function validateClientConfigValue(pluginName, value, path, stack) {
61
+ if (value === null) return null;
62
+ switch (typeof value) {
63
+ case "string":
64
+ case "boolean": return value;
65
+ case "number":
66
+ if (!Number.isFinite(value)) throw invalidClientConfig(pluginName, path, "contains a non-finite number");
67
+ return value;
68
+ case "bigint": throw invalidClientConfig(pluginName, path, "contains a BigInt");
69
+ case "undefined": return;
70
+ case "function": throw invalidClientConfig(pluginName, path, "contains a function");
71
+ case "symbol": throw invalidClientConfig(pluginName, path, "contains a symbol");
72
+ }
73
+ if (Array.isArray(value)) {
74
+ if (stack.has(value)) throw invalidClientConfig(pluginName, path, "contains a circular reference");
75
+ stack.add(value);
76
+ const result = value.map((item, index) => validateClientConfigValue(pluginName, item, `${path}[${index}]`, stack) ?? null);
77
+ stack.delete(value);
78
+ return result;
79
+ }
80
+ if (typeof value === "object") {
81
+ if (!isPlainObject(value)) throw invalidClientConfig(pluginName, path, "contains a non-plain object");
82
+ if (stack.has(value)) throw invalidClientConfig(pluginName, path, "contains a circular reference");
83
+ stack.add(value);
84
+ const result = {};
85
+ for (const [key, nestedValue] of Object.entries(value)) {
86
+ assertSafeClientConfigKey(pluginName, key, path);
87
+ const normalizedValue = validateClientConfigValue(pluginName, nestedValue, `${path}.${key}`, stack);
88
+ if (normalizedValue !== void 0) result[key] = normalizedValue;
89
+ }
90
+ stack.delete(value);
91
+ return result;
92
+ }
93
+ throw invalidClientConfig(pluginName, path, "contains an unsupported value");
94
+ }
95
+ function validateClientConfig(pluginName, config) {
96
+ if (config === null || typeof config !== "object" || Array.isArray(config)) throw new Error(`Plugin '${pluginName}' clientConfig() must return a plain object.`);
97
+ return validateClientConfigValue(pluginName, config, "clientConfig()", /* @__PURE__ */ new WeakSet());
98
+ }
99
+ /**
100
+ * Redacts a string when it contains a non-public env var value. Exact matches
101
+ * are caught regardless of length; substring containment requires the env value
102
+ * to be at least MIN_SUBSTRING_LENGTH chars to avoid false positives from very
103
+ * short values.
104
+ */
105
+ function redactLeakedString(value, nonPublicValues, publicValues, leakedVars) {
106
+ for (const [envValue, envKeys] of nonPublicValues) {
107
+ if (value === envValue && !publicValues.has(envValue)) {
108
+ for (const k of envKeys) leakedVars.add(k);
109
+ return REDACTED_CLIENT_CONFIG_VALUE;
110
+ }
111
+ if (envValue.length >= MIN_SUBSTRING_LENGTH && value.includes(envValue) && !isSecretCoveredByPublicValue(value, envValue, publicValues)) {
112
+ for (const k of envKeys) leakedVars.add(k);
113
+ return REDACTED_CLIENT_CONFIG_VALUE;
114
+ }
115
+ }
116
+ return value;
117
+ }
118
+ function redactLeakedValues(obj, nonPublicValues, publicValues, leakedVars) {
119
+ if (typeof obj === "string") return redactLeakedString(obj, nonPublicValues, publicValues, leakedVars);
120
+ if (Array.isArray(obj)) return obj.map((item) => redactLeakedValues(item, nonPublicValues, publicValues, leakedVars));
121
+ if (obj !== null && typeof obj === "object") {
122
+ const result = {};
123
+ for (const [key, value] of Object.entries(obj)) {
124
+ const uniqueKey = getUniqueObjectKey(redactLeakedString(key, nonPublicValues, publicValues, leakedVars), result);
125
+ result[uniqueKey] = redactLeakedValues(value, nonPublicValues, publicValues, leakedVars);
126
+ }
127
+ return result;
128
+ }
129
+ return obj;
130
+ }
131
+ function getUniqueObjectKey(key, result) {
132
+ if (!Object.hasOwn(result, key)) return key;
133
+ let suffix = 2;
134
+ let candidate = `${key} (${suffix})`;
135
+ while (Object.hasOwn(result, candidate)) {
136
+ suffix += 1;
137
+ candidate = `${key} (${suffix})`;
138
+ }
139
+ return candidate;
140
+ }
141
+ /**
142
+ * Scans a plugin's clientConfig return value for string values that
143
+ * match or contain non-public environment variable values. Matches are
144
+ * replaced with "[redacted by appkit]" and a warning is logged.
145
+ *
146
+ * Only env vars prefixed with `PUBLIC_APPKIT_` are allowed through;
147
+ * all other process.env values are treated as sensitive.
148
+ */
149
+ function sanitizeClientConfig(pluginName, config) {
150
+ const validated = validateClientConfig(pluginName, config);
151
+ const { nonPublic, publicValues } = getEnvValueSets();
152
+ if (nonPublic.size === 0) return validated;
153
+ const leakedVars = /* @__PURE__ */ new Set();
154
+ const sanitized = redactLeakedValues(validated, nonPublic, publicValues, leakedVars);
155
+ if (leakedVars.size > 0) {
156
+ const banner = formatLeakedVarsBanner(pluginName, leakedVars);
157
+ logger.warn("\n\n%s\n", banner);
158
+ }
159
+ return sanitized;
160
+ }
161
+ function formatLeakedVarsBanner(pluginName, leakedVars) {
162
+ const s = leakedVars.size === 1 ? "" : "s";
163
+ const contentLines = [
164
+ `${pc.bold(pluginName)}.clientConfig() contained ${pc.bold(String(leakedVars.size))} env var value${s}`,
165
+ `that would have been sent to the browser. AppKit ${pc.green("redacted")} them automatically.`,
166
+ "",
167
+ ...Array.from(leakedVars, (v) => ` ${pc.red("-")} ${pc.yellow(v)}`),
168
+ "",
169
+ `To intentionally expose a value, set a matching ${pc.green("PUBLIC_APPKIT_")} variable.`,
170
+ `Example: ${pc.dim("PUBLIC_APPKIT_MY_VAR=\"safe-value\"")}`
171
+ ];
172
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, "");
173
+ const maxLen = Math.max(...contentLines.map((l) => stripAnsi(l).length));
174
+ const border = pc.yellow("=".repeat(maxLen + 4));
175
+ return [
176
+ border,
177
+ ...contentLines.map((line) => `${pc.yellow("|")} ${line}${" ".repeat(maxLen - stripAnsi(line).length)} ${pc.yellow("|")}`),
178
+ border
179
+ ].join("\n");
180
+ }
181
+
182
+ //#endregion
183
+ export { sanitizeClientConfig };
184
+ //# sourceMappingURL=client-config-sanitizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-config-sanitizer.js","names":[],"sources":["../../../src/plugins/server/client-config-sanitizer.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"server:config\");\n\nconst REDACTED_CLIENT_CONFIG_VALUE = \"[redacted by appkit]\";\nconst MIN_SUBSTRING_LENGTH = 3;\nconst DISALLOWED_CLIENT_CONFIG_KEYS = new Set([\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n]);\n\n/**\n * Builds a Map of non-public env var values (value -> key names)\n * and a Set of public env var values for overlap resolution.\n */\nfunction getEnvValueSets(): {\n nonPublic: Map<string, string[]>;\n publicValues: Set<string>;\n} {\n const nonPublic = new Map<string, string[]>();\n const publicValues = new Set<string>();\n for (const [key, value] of Object.entries(process.env)) {\n if (!value) continue;\n if (key.startsWith(\"PUBLIC_APPKIT_\")) {\n publicValues.add(value);\n } else {\n const existing = nonPublic.get(value);\n if (existing) {\n existing.push(key);\n } else {\n nonPublic.set(value, [key]);\n }\n }\n }\n return { nonPublic, publicValues };\n}\n\nfunction getMatchRanges(\n haystack: string,\n needle: string,\n): Array<[number, number]> {\n const ranges: Array<[number, number]> = [];\n let startIndex = 0;\n\n while (startIndex < haystack.length) {\n const matchIndex = haystack.indexOf(needle, startIndex);\n if (matchIndex === -1) {\n break;\n }\n ranges.push([matchIndex, matchIndex + needle.length]);\n startIndex = matchIndex + 1;\n }\n\n return ranges;\n}\n\nfunction isSecretCoveredByPublicValue(\n value: string,\n envValue: string,\n publicValues: Set<string>,\n): boolean {\n const publicRanges = [...publicValues]\n .filter((publicValue) => publicValue.includes(envValue))\n .flatMap((publicValue) => getMatchRanges(value, publicValue));\n\n if (publicRanges.length === 0) {\n return false;\n }\n\n return getMatchRanges(value, envValue).every(([secretStart, secretEnd]) =>\n publicRanges.some(\n ([publicStart, publicEnd]) =>\n publicStart <= secretStart && publicEnd >= secretEnd,\n ),\n );\n}\n\nfunction isPlainObject(value: object): boolean {\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction invalidClientConfig(\n pluginName: string,\n path: string,\n message: string,\n): Error {\n return new Error(\n `Plugin '${pluginName}' clientConfig() ${message} at ${path}. Only JSON-serializable plain data is supported.`,\n );\n}\n\nfunction assertSafeClientConfigKey(\n pluginName: string,\n key: string,\n path: string,\n): void {\n if (DISALLOWED_CLIENT_CONFIG_KEYS.has(key)) {\n throw invalidClientConfig(\n pluginName,\n `${path}.${key}`,\n \"contains a reserved key\",\n );\n }\n}\n\nfunction validateClientConfigValue(\n pluginName: string,\n value: unknown,\n path: string,\n stack: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return value;\n case \"number\":\n if (!Number.isFinite(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-finite number\",\n );\n }\n return value;\n case \"bigint\":\n throw invalidClientConfig(pluginName, path, \"contains a BigInt\");\n case \"undefined\":\n return undefined;\n case \"function\":\n throw invalidClientConfig(pluginName, path, \"contains a function\");\n case \"symbol\":\n throw invalidClientConfig(pluginName, path, \"contains a symbol\");\n }\n\n if (Array.isArray(value)) {\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result = value.map(\n (item, index) =>\n validateClientConfigValue(\n pluginName,\n item,\n `${path}[${index}]`,\n stack,\n ) ?? null,\n );\n stack.delete(value);\n return result;\n }\n\n if (typeof value === \"object\") {\n if (!isPlainObject(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a non-plain object\",\n );\n }\n if (stack.has(value)) {\n throw invalidClientConfig(\n pluginName,\n path,\n \"contains a circular reference\",\n );\n }\n\n stack.add(value);\n const result: Record<string, unknown> = {};\n for (const [key, nestedValue] of Object.entries(value)) {\n assertSafeClientConfigKey(pluginName, key, path);\n const normalizedValue = validateClientConfigValue(\n pluginName,\n nestedValue,\n `${path}.${key}`,\n stack,\n );\n if (normalizedValue !== undefined) {\n result[key] = normalizedValue;\n }\n }\n stack.delete(value);\n return result;\n }\n\n throw invalidClientConfig(pluginName, path, \"contains an unsupported value\");\n}\n\nfunction validateClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n if (config === null || typeof config !== \"object\" || Array.isArray(config)) {\n throw new Error(\n `Plugin '${pluginName}' clientConfig() must return a plain object.`,\n );\n }\n\n return validateClientConfigValue(\n pluginName,\n config,\n \"clientConfig()\",\n new WeakSet(),\n ) as Record<string, unknown>;\n}\n\n/**\n * Redacts a string when it contains a non-public env var value. Exact matches\n * are caught regardless of length; substring containment requires the env value\n * to be at least MIN_SUBSTRING_LENGTH chars to avoid false positives from very\n * short values.\n */\nfunction redactLeakedString(\n value: string,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): string {\n for (const [envValue, envKeys] of nonPublicValues) {\n if (value === envValue && !publicValues.has(envValue)) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n if (\n envValue.length >= MIN_SUBSTRING_LENGTH &&\n value.includes(envValue) &&\n !isSecretCoveredByPublicValue(value, envValue, publicValues)\n ) {\n for (const k of envKeys) leakedVars.add(k);\n return REDACTED_CLIENT_CONFIG_VALUE;\n }\n }\n\n return value;\n}\n\nfunction redactLeakedValues(\n obj: unknown,\n nonPublicValues: Map<string, string[]>,\n publicValues: Set<string>,\n leakedVars: Set<string>,\n): unknown {\n if (typeof obj === \"string\") {\n return redactLeakedString(obj, nonPublicValues, publicValues, leakedVars);\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) =>\n redactLeakedValues(item, nonPublicValues, publicValues, leakedVars),\n );\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const redactedKey = redactLeakedString(\n key,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n const uniqueKey = getUniqueObjectKey(redactedKey, result);\n result[uniqueKey] = redactLeakedValues(\n value,\n nonPublicValues,\n publicValues,\n leakedVars,\n );\n }\n return result;\n }\n\n return obj;\n}\n\nfunction getUniqueObjectKey(\n key: string,\n result: Record<string, unknown>,\n): string {\n if (!Object.hasOwn(result, key)) {\n return key;\n }\n\n let suffix = 2;\n let candidate = `${key} (${suffix})`;\n while (Object.hasOwn(result, candidate)) {\n suffix += 1;\n candidate = `${key} (${suffix})`;\n }\n\n return candidate;\n}\n\n/**\n * Scans a plugin's clientConfig return value for string values that\n * match or contain non-public environment variable values. Matches are\n * replaced with \"[redacted by appkit]\" and a warning is logged.\n *\n * Only env vars prefixed with `PUBLIC_APPKIT_` are allowed through;\n * all other process.env values are treated as sensitive.\n */\nexport function sanitizeClientConfig(\n pluginName: string,\n config: unknown,\n): Record<string, unknown> {\n const validated = validateClientConfig(pluginName, config);\n const { nonPublic, publicValues } = getEnvValueSets();\n if (nonPublic.size === 0) return validated;\n\n const leakedVars = new Set<string>();\n const sanitized = redactLeakedValues(\n validated,\n nonPublic,\n publicValues,\n leakedVars,\n ) as Record<string, unknown>;\n\n if (leakedVars.size > 0) {\n const banner = formatLeakedVarsBanner(pluginName, leakedVars);\n logger.warn(\"\\n\\n%s\\n\", banner);\n }\n\n return sanitized;\n}\n\nfunction formatLeakedVarsBanner(\n pluginName: string,\n leakedVars: Set<string>,\n): string {\n const s = leakedVars.size === 1 ? \"\" : \"s\";\n const contentLines: string[] = [\n `${pc.bold(pluginName)}.clientConfig() contained ${pc.bold(String(leakedVars.size))} env var value${s}`,\n `that would have been sent to the browser. AppKit ${pc.green(\"redacted\")} them automatically.`,\n \"\",\n ...Array.from(leakedVars, (v) => ` ${pc.red(\"-\")} ${pc.yellow(v)}`),\n \"\",\n `To intentionally expose a value, set a matching ${pc.green(\"PUBLIC_APPKIT_\")} variable.`,\n `Example: ${pc.dim('PUBLIC_APPKIT_MY_VAR=\"safe-value\"')}`,\n ];\n\n // biome-ignore lint: stripping ANSI escape sequences requires matching the ESC control character\n const stripAnsi = (str: string) => str.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n const maxLen = Math.max(...contentLines.map((l) => stripAnsi(l).length));\n const border = pc.yellow(\"=\".repeat(maxLen + 4));\n const boxed = contentLines.map(\n (line) =>\n `${pc.yellow(\"|\")} ${line}${\" \".repeat(maxLen - stripAnsi(line).length)} ${pc.yellow(\"|\")}`,\n );\n\n return [border, ...boxed, border].join(\"\\n\");\n}\n"],"mappings":";;;;AAGA,MAAM,SAAS,aAAa,gBAAgB;AAE5C,MAAM,+BAA+B;AACrC,MAAM,uBAAuB;AAC7B,MAAM,gCAAgC,IAAI,IAAI;CAC5C;CACA;CACA;CACD,CAAC;;;;;AAMF,SAAS,kBAGP;CACA,MAAM,4BAAY,IAAI,KAAuB;CAC7C,MAAM,+BAAe,IAAI,KAAa;AACtC,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,IAAI,EAAE;AACtD,MAAI,CAAC,MAAO;AACZ,MAAI,IAAI,WAAW,iBAAiB,CAClC,cAAa,IAAI,MAAM;OAClB;GACL,MAAM,WAAW,UAAU,IAAI,MAAM;AACrC,OAAI,SACF,UAAS,KAAK,IAAI;OAElB,WAAU,IAAI,OAAO,CAAC,IAAI,CAAC;;;AAIjC,QAAO;EAAE;EAAW;EAAc;;AAGpC,SAAS,eACP,UACA,QACyB;CACzB,MAAM,SAAkC,EAAE;CAC1C,IAAI,aAAa;AAEjB,QAAO,aAAa,SAAS,QAAQ;EACnC,MAAM,aAAa,SAAS,QAAQ,QAAQ,WAAW;AACvD,MAAI,eAAe,GACjB;AAEF,SAAO,KAAK,CAAC,YAAY,aAAa,OAAO,OAAO,CAAC;AACrD,eAAa,aAAa;;AAG5B,QAAO;;AAGT,SAAS,6BACP,OACA,UACA,cACS;CACT,MAAM,eAAe,CAAC,GAAG,aAAa,CACnC,QAAQ,gBAAgB,YAAY,SAAS,SAAS,CAAC,CACvD,SAAS,gBAAgB,eAAe,OAAO,YAAY,CAAC;AAE/D,KAAI,aAAa,WAAW,EAC1B,QAAO;AAGT,QAAO,eAAe,OAAO,SAAS,CAAC,OAAO,CAAC,aAAa,eAC1D,aAAa,MACV,CAAC,aAAa,eACb,eAAe,eAAe,aAAa,UAC9C,CACF;;AAGH,SAAS,cAAc,OAAwB;CAC7C,MAAM,QAAQ,OAAO,eAAe,MAAM;AAC1C,QAAO,UAAU,OAAO,aAAa,UAAU;;AAGjD,SAAS,oBACP,YACA,MACA,SACO;AACP,wBAAO,IAAI,MACT,WAAW,WAAW,mBAAmB,QAAQ,MAAM,KAAK,mDAC7D;;AAGH,SAAS,0BACP,YACA,KACA,MACM;AACN,KAAI,8BAA8B,IAAI,IAAI,CACxC,OAAM,oBACJ,YACA,GAAG,KAAK,GAAG,OACX,0BACD;;AAIL,SAAS,0BACP,YACA,OACA,MACA,OACS;AACT,KAAI,UAAU,KAAM,QAAO;AAE3B,SAAQ,OAAO,OAAf;EACE,KAAK;EACL,KAAK,UACH,QAAO;EACT,KAAK;AACH,OAAI,CAAC,OAAO,SAAS,MAAM,CACzB,OAAM,oBACJ,YACA,MACA,+BACD;AAEH,UAAO;EACT,KAAK,SACH,OAAM,oBAAoB,YAAY,MAAM,oBAAoB;EAClE,KAAK,YACH;EACF,KAAK,WACH,OAAM,oBAAoB,YAAY,MAAM,sBAAsB;EACpE,KAAK,SACH,OAAM,oBAAoB,YAAY,MAAM,oBAAoB;;AAGpE,KAAI,MAAM,QAAQ,MAAM,EAAE;AACxB,MAAI,MAAM,IAAI,MAAM,CAClB,OAAM,oBACJ,YACA,MACA,gCACD;AAGH,QAAM,IAAI,MAAM;EAChB,MAAM,SAAS,MAAM,KAClB,MAAM,UACL,0BACE,YACA,MACA,GAAG,KAAK,GAAG,MAAM,IACjB,MACD,IAAI,KACR;AACD,QAAM,OAAO,MAAM;AACnB,SAAO;;AAGT,KAAI,OAAO,UAAU,UAAU;AAC7B,MAAI,CAAC,cAAc,MAAM,CACvB,OAAM,oBACJ,YACA,MACA,8BACD;AAEH,MAAI,MAAM,IAAI,MAAM,CAClB,OAAM,oBACJ,YACA,MACA,gCACD;AAGH,QAAM,IAAI,MAAM;EAChB,MAAM,SAAkC,EAAE;AAC1C,OAAK,MAAM,CAAC,KAAK,gBAAgB,OAAO,QAAQ,MAAM,EAAE;AACtD,6BAA0B,YAAY,KAAK,KAAK;GAChD,MAAM,kBAAkB,0BACtB,YACA,aACA,GAAG,KAAK,GAAG,OACX,MACD;AACD,OAAI,oBAAoB,OACtB,QAAO,OAAO;;AAGlB,QAAM,OAAO,MAAM;AACnB,SAAO;;AAGT,OAAM,oBAAoB,YAAY,MAAM,gCAAgC;;AAG9E,SAAS,qBACP,YACA,QACyB;AACzB,KAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MACR,WAAW,WAAW,8CACvB;AAGH,QAAO,0BACL,YACA,QACA,kCACA,IAAI,SAAS,CACd;;;;;;;;AASH,SAAS,mBACP,OACA,iBACA,cACA,YACQ;AACR,MAAK,MAAM,CAAC,UAAU,YAAY,iBAAiB;AACjD,MAAI,UAAU,YAAY,CAAC,aAAa,IAAI,SAAS,EAAE;AACrD,QAAK,MAAM,KAAK,QAAS,YAAW,IAAI,EAAE;AAC1C,UAAO;;AAET,MACE,SAAS,UAAU,wBACnB,MAAM,SAAS,SAAS,IACxB,CAAC,6BAA6B,OAAO,UAAU,aAAa,EAC5D;AACA,QAAK,MAAM,KAAK,QAAS,YAAW,IAAI,EAAE;AAC1C,UAAO;;;AAIX,QAAO;;AAGT,SAAS,mBACP,KACA,iBACA,cACA,YACS;AACT,KAAI,OAAO,QAAQ,SACjB,QAAO,mBAAmB,KAAK,iBAAiB,cAAc,WAAW;AAG3E,KAAI,MAAM,QAAQ,IAAI,CACpB,QAAO,IAAI,KAAK,SACd,mBAAmB,MAAM,iBAAiB,cAAc,WAAW,CACpE;AAGH,KAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,SAAkC,EAAE;AAC1C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,EAAE;GAO9C,MAAM,YAAY,mBANE,mBAClB,KACA,iBACA,cACA,WACD,EACiD,OAAO;AACzD,UAAO,aAAa,mBAClB,OACA,iBACA,cACA,WACD;;AAEH,SAAO;;AAGT,QAAO;;AAGT,SAAS,mBACP,KACA,QACQ;AACR,KAAI,CAAC,OAAO,OAAO,QAAQ,IAAI,CAC7B,QAAO;CAGT,IAAI,SAAS;CACb,IAAI,YAAY,GAAG,IAAI,IAAI,OAAO;AAClC,QAAO,OAAO,OAAO,QAAQ,UAAU,EAAE;AACvC,YAAU;AACV,cAAY,GAAG,IAAI,IAAI,OAAO;;AAGhC,QAAO;;;;;;;;;;AAWT,SAAgB,qBACd,YACA,QACyB;CACzB,MAAM,YAAY,qBAAqB,YAAY,OAAO;CAC1D,MAAM,EAAE,WAAW,iBAAiB,iBAAiB;AACrD,KAAI,UAAU,SAAS,EAAG,QAAO;CAEjC,MAAM,6BAAa,IAAI,KAAa;CACpC,MAAM,YAAY,mBAChB,WACA,WACA,cACA,WACD;AAED,KAAI,WAAW,OAAO,GAAG;EACvB,MAAM,SAAS,uBAAuB,YAAY,WAAW;AAC7D,SAAO,KAAK,YAAY,OAAO;;AAGjC,QAAO;;AAGT,SAAS,uBACP,YACA,YACQ;CACR,MAAM,IAAI,WAAW,SAAS,IAAI,KAAK;CACvC,MAAM,eAAyB;EAC7B,GAAG,GAAG,KAAK,WAAW,CAAC,4BAA4B,GAAG,KAAK,OAAO,WAAW,KAAK,CAAC,CAAC,gBAAgB;EACpG,oDAAoD,GAAG,MAAM,WAAW,CAAC;EACzE;EACA,GAAG,MAAM,KAAK,aAAa,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,GAAG,OAAO,EAAE,GAAG;EACpE;EACA,mDAAmD,GAAG,MAAM,iBAAiB,CAAC;EAC9E,YAAY,GAAG,IAAI,sCAAoC;EACxD;CAGD,MAAM,aAAa,QAAgB,IAAI,QAAQ,mBAAmB,GAAG;CACrE,MAAM,SAAS,KAAK,IAAI,GAAG,aAAa,KAAK,MAAM,UAAU,EAAE,CAAC,OAAO,CAAC;CACxE,MAAM,SAAS,GAAG,OAAO,IAAI,OAAO,SAAS,EAAE,CAAC;AAMhD,QAAO;EAAC;EAAQ,GALF,aAAa,KACxB,SACC,GAAG,GAAG,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI,OAAO,SAAS,UAAU,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,OAAO,IAAI,GAC5F;EAEyB;EAAO,CAAC,KAAK,KAAK"}
@@ -5,8 +5,8 @@ import "../../plugin/index.js";
5
5
  import { PluginManifest } from "../../registry/types.js";
6
6
  import "../../registry/index.js";
7
7
  import { ServerConfig } from "./types.js";
8
- import express from "express";
9
8
  import { Server } from "node:http";
9
+ import express from "express";
10
10
 
11
11
  //#region src/plugins/server/index.d.ts
12
12
  /**
@@ -84,7 +84,8 @@ declare class ServerPlugin extends Plugin {
84
84
  * Setup the routes with the plugins.
85
85
  *
86
86
  * This method goes through all the plugins and injects the routes into the server application.
87
- * Returns a map of plugin names to their registered named endpoints.
87
+ * Returns a map of plugin names to their registered named endpoints,
88
+ * and a map of plugin names to their client-exposed configs.
88
89
  */
89
90
  private extendRoutes;
90
91
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;AAoCA;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;;EAqUO;EAAA,OA9Td,QAAA,EAAuB,cAAA;EAAA,QACtB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAVb;EAuBD,KAAA,CAAA,GAAK,OAAA;EAtBH;EA6BR,SAAA,CAAA;IAAA;;;;;;gBAPW,gBAAA;EAAA;;EAcX,eAAA,CAAA;EA3BY;;;;;;;;EAuCN,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;;;;;;;;;EA2D/B,SAAA,CAAA,GAAa,MAAA;EAAA;;;;;;;EAmBb,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EA+FV;;;;;;EAAA,QAhFD,YAAA;EA7FiB;;;;;;EAAA,QA0IjB,aAAA;EAAA,eAmCC,cAAA;EAAA,QAaP,cAAA;EAAA,QA4BM,iBAAA;;;;;EAiDd,OAAA,CAAA;wCAvQe,OAAA,CAAQ,OAAA,CAAQ,WAAA;gBA6Qf,GAAA,EAAK,OAAA,CAAQ,WAAA,2BAiBlB;qBAnOE,MAAA,EAmO6B;;;;;;;;kBAnOnB,gBAAA;IAAA;EAAA;AAAA;;;;cAmOZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/plugins/server/index.ts"],"mappings":";;;;;;;;;;;;;;;;AAqCA;;;;;;;;;cAAa,YAAA,SAAqB,MAAA;EAAA,OAClB,cAAA;;;;;EAoWO;EAAA,OA7Vd,QAAA,EAAuB,cAAA;EAAA,QACtB,iBAAA;EAAA,QACA,MAAA;EAAA,QACA,aAAA;EAAA,QACA,sBAAA;EAAA,UACU,MAAA,EAAQ,YAAA;EAAA,QAClB,gBAAA;EAAA,QACA,YAAA;EAAA,OACD,KAAA,EAAO,WAAA;cAEF,MAAA,EAAQ,YAAA;EAVb;EAuBD,KAAA,CAAA,GAAK,OAAA;EAtBH;EA6BR,SAAA,CAAA;IAAA;;;;;;gBAPW,gBAAA;EAAA;;EAcX,eAAA,CAAA;EA3BY;;;;;;;;EAuCN,KAAA,CAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,WAAA;;;;;;;;;EA2D/B,SAAA,CAAA,GAAa,MAAA;EAAA;;;;;;;EAmBb,MAAA,CAAO,EAAA,GAAK,GAAA,EAAK,OAAA,CAAQ,WAAA;EA8HV;;;;;;;EAAA,QA9GD,YAAA;;;;;;;UAkEA,aAAA;EAAA,eA4CC,cAAA;EAAA,QAaP,cAAA;EAAA,QA4BM,iBAAA;;;;;EAiDd,OAAA,CAAA;wCAtSe,OAAA,CAAQ,OAAA,CAAQ,WAAA;gBA4Sf,GAAA,EAAK,OAAA,CAAQ,WAAA,YAiBa,eAAA;qBAlQ7B,MAAA,EAkQI;;;;;;;;kBAlQM,gBAAA;IAAA;EAAA;AAAA;;;;cAkQZ,MAAA,EAAM,QAAA,QAAA,YAAA,EAAA,YAAA"}
@@ -6,6 +6,7 @@ import "../../telemetry/index.js";
6
6
  import { Plugin } from "../../plugin/plugin.js";
7
7
  import { toPlugin } from "../../plugin/to-plugin.js";
8
8
  import "../../plugin/index.js";
9
+ import { sanitizeClientConfig } from "./client-config-sanitizer.js";
9
10
  import manifest_default from "./manifest.js";
10
11
  import { RemoteTunnelController } from "./remote-tunnel/remote-tunnel-controller.js";
11
12
  import { getRoutes, printRoutes } from "./utils.js";
@@ -84,11 +85,11 @@ var ServerPlugin = class ServerPlugin extends Plugin {
84
85
  if (urlPath && this.rawBodyPaths.has(urlPath)) return false;
85
86
  return (req.headers["content-type"] ?? "").includes("json");
86
87
  } }));
87
- const endpoints = await this.extendRoutes();
88
+ const { endpoints, pluginConfigs } = await this.extendRoutes();
88
89
  for (const extension of this.serverExtensions) extension(this.serverApplication);
89
90
  this.remoteTunnelController = new RemoteTunnelController(this.devFileReader);
90
91
  this.serverApplication.use(this.remoteTunnelController.middleware);
91
- await this.setupFrontend(endpoints);
92
+ await this.setupFrontend(endpoints, pluginConfigs);
92
93
  const server = this.serverApplication.listen(this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port, this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host, () => this.logStartupInfo());
93
94
  this.server = server;
94
95
  this.remoteTunnelController.setServer(server);
@@ -126,11 +127,16 @@ var ServerPlugin = class ServerPlugin extends Plugin {
126
127
  * Setup the routes with the plugins.
127
128
  *
128
129
  * This method goes through all the plugins and injects the routes into the server application.
129
- * Returns a map of plugin names to their registered named endpoints.
130
+ * Returns a map of plugin names to their registered named endpoints,
131
+ * and a map of plugin names to their client-exposed configs.
130
132
  */
131
133
  async extendRoutes() {
132
134
  const endpoints = {};
133
- if (!this.config.plugins) return endpoints;
135
+ const pluginConfigs = {};
136
+ if (!this.config.plugins) return {
137
+ endpoints,
138
+ pluginConfigs
139
+ };
134
140
  this.serverApplication.get("/health", (_, res) => {
135
141
  res.status(200).json({ status: "ok" });
136
142
  });
@@ -145,8 +151,20 @@ var ServerPlugin = class ServerPlugin extends Plugin {
145
151
  endpoints[plugin.name] = plugin.getEndpoints();
146
152
  if (plugin.getSkipBodyParsingPaths && typeof plugin.getSkipBodyParsingPaths === "function") for (const p of plugin.getSkipBodyParsingPaths()) this.rawBodyPaths.add(p);
147
153
  }
154
+ if (typeof plugin.clientConfig === "function") try {
155
+ const raw = plugin.clientConfig();
156
+ if (raw != null) {
157
+ const sanitized = sanitizeClientConfig(plugin.name, raw);
158
+ if (Object.keys(sanitized).length > 0) pluginConfigs[plugin.name] = sanitized;
159
+ }
160
+ } catch (error) {
161
+ logger.error("Plugin '%s' clientConfig() failed, skipping its config: %O", plugin.name, error);
162
+ }
148
163
  }
149
- return endpoints;
164
+ return {
165
+ endpoints,
166
+ pluginConfigs
167
+ };
150
168
  }
151
169
  /**
152
170
  * Setup frontend serving based on environment:
@@ -154,19 +172,19 @@ var ServerPlugin = class ServerPlugin extends Plugin {
154
172
  * - Dev mode (no staticPath): Vite for HMR
155
173
  * - Production (no staticPath): Static files auto-detected
156
174
  */
157
- async setupFrontend(endpoints) {
175
+ async setupFrontend(endpoints, pluginConfigs) {
158
176
  const isDev = process.env.NODE_ENV === "development";
159
177
  if (this.config.staticPath !== void 0) {
160
- new StaticServer(this.serverApplication, this.config.staticPath, endpoints).setup();
178
+ new StaticServer(this.serverApplication, this.config.staticPath, endpoints, pluginConfigs).setup();
161
179
  return;
162
180
  }
163
181
  if (isDev) {
164
- this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints);
182
+ this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints, pluginConfigs);
165
183
  await this.viteDevServer.setup();
166
184
  return;
167
185
  }
168
186
  const staticPath = ServerPlugin.findStaticPath();
169
- if (staticPath) new StaticServer(this.serverApplication, staticPath, endpoints).setup();
187
+ if (staticPath) new StaticServer(this.serverApplication, staticPath, endpoints, pluginConfigs).setup();
170
188
  }
171
189
  static findStaticPath() {
172
190
  const staticPaths = [
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport type { PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), telemetryExamples(), analytics({})],\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n autoStart: true,\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n /** Setup the server plugin. */\n async setup() {\n if (this.shouldAutoStart()) {\n await this.start();\n }\n }\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /** Check if the server should auto start. */\n shouldAutoStart() {\n return this.config.autoStart;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(\n express.json({\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const endpoints = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints);\n\n const server = this.serverApplication.listen(\n this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server is not started or autoStart is true.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"get server\");\n }\n\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n * @throws {Error} If autoStart is true.\n */\n extend(fn: (app: express.Application) => void) {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"extend server\");\n }\n\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints.\n */\n private async extendRoutes(): Promise<PluginEndpoints> {\n const endpoints: PluginEndpoints = {};\n\n if (!this.config.plugins) return endpoints;\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of Object.values(this.config.plugins)) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n // Collect named endpoints from the plugin\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n }\n\n return endpoints;\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(endpoints: PluginEndpoints) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(this.serverApplication, endpoints);\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Start the server */\n start: this.start,\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["manifest"],"sources":["../../../src/plugins/server/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport express from \"express\";\nimport type { PluginClientConfigs, PluginPhase } from \"shared\";\nimport { ServerError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { instrumentations } from \"../../telemetry\";\nimport { sanitizeClientConfig } from \"./client-config-sanitizer\";\nimport manifest from \"./manifest.json\";\nimport { RemoteTunnelController } from \"./remote-tunnel/remote-tunnel-controller\";\nimport { StaticServer } from \"./static-server\";\nimport type { ServerConfig } from \"./types\";\nimport { getRoutes, type PluginEndpoints, printRoutes } from \"./utils\";\nimport { ViteDevServer } from \"./vite-dev-server\";\n\ndotenv.config({ path: path.resolve(process.cwd(), \"./.env\") });\n\nconst logger = createLogger(\"server\");\n\n/**\n * Server plugin for the AppKit.\n *\n * This plugin is responsible for starting the server and serving the static files.\n * It also handles the remote tunneling for development purposes.\n *\n * @example\n * ```ts\n * createApp({\n * plugins: [server(), telemetryExamples(), analytics({})],\n * });\n * ```\n *\n */\nexport class ServerPlugin extends Plugin {\n public static DEFAULT_CONFIG = {\n autoStart: true,\n host: process.env.FLASK_RUN_HOST || \"0.0.0.0\",\n port: Number(process.env.DATABRICKS_APP_PORT) || 8000,\n };\n\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"server\">;\n private serverApplication: express.Application;\n private server: HTTPServer | null;\n private viteDevServer?: ViteDevServer;\n private remoteTunnelController?: RemoteTunnelController;\n protected declare config: ServerConfig;\n private serverExtensions: ((app: express.Application) => void)[] = [];\n private rawBodyPaths: Set<string> = new Set();\n static phase: PluginPhase = \"deferred\";\n\n constructor(config: ServerConfig) {\n super(config);\n this.config = config;\n this.serverApplication = express();\n this.server = null;\n this.serverExtensions = [];\n this.telemetry.registerInstrumentations([\n instrumentations.http,\n instrumentations.express,\n ]);\n }\n\n /** Setup the server plugin. */\n async setup() {\n if (this.shouldAutoStart()) {\n await this.start();\n }\n }\n\n /** Get the server configuration. */\n getConfig() {\n const { plugins: _plugins, ...config } = this.config;\n\n return config;\n }\n\n /** Check if the server should auto start. */\n shouldAutoStart() {\n return this.config.autoStart;\n }\n\n /**\n * Start the server.\n *\n * This method starts the server and sets up the frontend.\n * It also sets up the remote tunneling if enabled.\n *\n * @returns The express application.\n */\n async start(): Promise<express.Application> {\n this.serverApplication.use(\n express.json({\n type: (req) => {\n // Skip JSON parsing for routes that declared skipBodyParsing\n // (e.g. file uploads where the raw body must flow through).\n // rawBodyPaths is populated by extendRoutes() below; the type\n // callback runs per-request so the set is already filled.\n const urlPath = req.url?.split(\"?\")[0];\n if (urlPath && this.rawBodyPaths.has(urlPath)) return false;\n const ct = req.headers[\"content-type\"] ?? \"\";\n return ct.includes(\"json\");\n },\n }),\n );\n\n const { endpoints, pluginConfigs } = await this.extendRoutes();\n\n for (const extension of this.serverExtensions) {\n extension(this.serverApplication);\n }\n\n // register remote tunnel controller (before static/vite)\n this.remoteTunnelController = new RemoteTunnelController(\n this.devFileReader,\n );\n this.serverApplication.use(this.remoteTunnelController.middleware);\n\n await this.setupFrontend(endpoints, pluginConfigs);\n\n const server = this.serverApplication.listen(\n this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port,\n this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host,\n () => this.logStartupInfo(),\n );\n\n this.server = server;\n\n // attach server to remote tunnel controller\n this.remoteTunnelController.setServer(server);\n\n process.on(\"SIGTERM\", () => this._gracefulShutdown());\n process.on(\"SIGINT\", () => this._gracefulShutdown());\n\n if (process.env.NODE_ENV === \"development\") {\n const allRoutes = getRoutes(this.serverApplication._router.stack);\n printRoutes(allRoutes);\n }\n return this.serverApplication;\n }\n\n /**\n * Get the low level node.js http server instance.\n *\n * Only use this method if you need to access the server instance for advanced usage like a custom websocket server, etc.\n *\n * @throws {Error} If the server is not started or autoStart is true.\n * @returns {HTTPServer} The server instance.\n */\n getServer(): HTTPServer {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"get server\");\n }\n\n if (!this.server) {\n throw ServerError.notStarted();\n }\n\n return this.server;\n }\n\n /**\n * Extend the server with custom routes or middleware.\n *\n * @param fn - A function that receives the express application.\n * @returns The server plugin instance for chaining.\n * @throws {Error} If autoStart is true.\n */\n extend(fn: (app: express.Application) => void) {\n if (this.shouldAutoStart()) {\n throw ServerError.autoStartConflict(\"extend server\");\n }\n\n this.serverExtensions.push(fn);\n return this;\n }\n\n /**\n * Setup the routes with the plugins.\n *\n * This method goes through all the plugins and injects the routes into the server application.\n * Returns a map of plugin names to their registered named endpoints,\n * and a map of plugin names to their client-exposed configs.\n */\n private async extendRoutes(): Promise<{\n endpoints: PluginEndpoints;\n pluginConfigs: PluginClientConfigs;\n }> {\n const endpoints: PluginEndpoints = {};\n const pluginConfigs: PluginClientConfigs = {};\n\n if (!this.config.plugins) return { endpoints, pluginConfigs };\n\n this.serverApplication.get(\"/health\", (_, res) => {\n res.status(200).json({ status: \"ok\" });\n });\n this.registerEndpoint(\"health\", \"/health\");\n\n for (const plugin of Object.values(this.config.plugins)) {\n if (EXCLUDED_PLUGINS.includes(plugin.name)) continue;\n\n if (plugin?.injectRoutes && typeof plugin.injectRoutes === \"function\") {\n const router = express.Router();\n\n plugin.injectRoutes(router);\n\n const basePath = `/api/${plugin.name}`;\n this.serverApplication.use(basePath, router);\n\n endpoints[plugin.name] = plugin.getEndpoints();\n\n // Collect paths that should skip body parsing\n if (\n plugin.getSkipBodyParsingPaths &&\n typeof plugin.getSkipBodyParsingPaths === \"function\"\n ) {\n for (const p of plugin.getSkipBodyParsingPaths()) {\n this.rawBodyPaths.add(p);\n }\n }\n }\n\n if (typeof plugin.clientConfig === \"function\") {\n try {\n const raw = plugin.clientConfig();\n if (raw != null) {\n const sanitized = sanitizeClientConfig(plugin.name, raw);\n if (Object.keys(sanitized).length > 0) {\n pluginConfigs[plugin.name] = sanitized;\n }\n }\n } catch (error) {\n logger.error(\n \"Plugin '%s' clientConfig() failed, skipping its config: %O\",\n plugin.name,\n error,\n );\n }\n }\n }\n\n return { endpoints, pluginConfigs };\n }\n\n /**\n * Setup frontend serving based on environment:\n * - If staticPath is explicitly provided: use static server\n * - Dev mode (no staticPath): Vite for HMR\n * - Production (no staticPath): Static files auto-detected\n */\n private async setupFrontend(\n endpoints: PluginEndpoints,\n pluginConfigs: PluginClientConfigs,\n ) {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n\n // explict static path provided\n if (hasExplicitStaticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n this.config.staticPath as string,\n endpoints,\n pluginConfigs,\n );\n staticServer.setup();\n return;\n }\n\n // auto-detection based on environment\n if (isDev) {\n this.viteDevServer = new ViteDevServer(\n this.serverApplication,\n endpoints,\n pluginConfigs,\n );\n await this.viteDevServer.setup();\n return;\n }\n\n // auto-detection based on static path\n const staticPath = ServerPlugin.findStaticPath();\n if (staticPath) {\n const staticServer = new StaticServer(\n this.serverApplication,\n staticPath,\n endpoints,\n pluginConfigs,\n );\n\n staticServer.setup();\n }\n }\n\n private static findStaticPath() {\n const staticPaths = [\"dist\", \"client/dist\", \"build\", \"public\", \"out\"];\n const cwd = process.cwd();\n for (const p of staticPaths) {\n const fullPath = path.resolve(cwd, p);\n if (fs.existsSync(path.resolve(fullPath, \"index.html\"))) {\n logger.debug(\"Static files: serving from %s\", fullPath);\n return fullPath;\n }\n }\n return undefined;\n }\n\n private logStartupInfo() {\n const isDev = process.env.NODE_ENV === \"development\";\n const hasExplicitStaticPath = this.config.staticPath !== undefined;\n const port = this.config.port ?? ServerPlugin.DEFAULT_CONFIG.port;\n const host = this.config.host ?? ServerPlugin.DEFAULT_CONFIG.host;\n\n logger.info(\"Server running on http://%s:%d\", host, port);\n\n if (hasExplicitStaticPath) {\n logger.info(\"Mode: static (%s)\", this.config.staticPath);\n } else if (isDev) {\n logger.info(\"Mode: development (Vite HMR)\");\n } else {\n logger.info(\"Mode: production (static)\");\n }\n\n const remoteServerController = this.remoteTunnelController;\n if (!remoteServerController) {\n logger.debug(\"Remote tunnel: disabled (controller not initialized)\");\n } else {\n logger.debug(\n \"Remote tunnel: %s; %s\",\n remoteServerController.isAllowedByEnv() ? \"allowed\" : \"blocked\",\n remoteServerController.isActive() ? \"active\" : \"inactive\",\n );\n }\n }\n\n private async _gracefulShutdown() {\n logger.info(\"Starting graceful shutdown...\");\n\n if (this.viteDevServer) {\n await this.viteDevServer.close();\n }\n\n if (this.remoteTunnelController) {\n this.remoteTunnelController.cleanup();\n }\n\n // 1. abort active operations from plugins\n if (this.config.plugins) {\n for (const plugin of Object.values(this.config.plugins)) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n }\n\n // 2. close the server\n if (this.server) {\n this.server.close(() => {\n logger.debug(\"Server closed gracefully\");\n process.exit(0);\n });\n\n // 3. timeout to force shutdown after 15 seconds\n setTimeout(() => {\n logger.debug(\"Force shutdown after timeout\");\n process.exit(1);\n }, 15000);\n } else {\n process.exit(0);\n }\n }\n\n /**\n * Returns the public exports for the server plugin.\n * Exposes server management methods.\n */\n exports() {\n const self = this;\n return {\n /** Start the server */\n start: this.start,\n /** Extend the server with custom routes or middleware */\n extend(fn: (app: express.Application) => void) {\n self.extend(fn);\n return this;\n },\n /** Get the underlying HTTP server instance */\n getServer: this.getServer,\n /** Get the server configuration */\n getConfig: this.getConfig,\n };\n }\n}\n\nconst EXCLUDED_PLUGINS: string[] = [ServerPlugin.manifest.name];\n\n/**\n * @internal\n */\nexport const server = toPlugin(ServerPlugin);\n// Export manifest and types\n"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8" />
4
+ <title>Access Denied</title>
5
+ <style>
6
+ body {
7
+ background: #1b1b1d;
8
+ height: 100vh;
9
+ margin: 0;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif;
14
+ }
15
+ .container {
16
+ text-align: center;
17
+ padding: 50px 70px;
18
+ max-width: 500px;
19
+ }
20
+ .icon {
21
+ font-size: 64px;
22
+ margin-bottom: 24px;
23
+ }
24
+ h1 {
25
+ font-size: 1.5rem;
26
+ color: #ffffff;
27
+ margin: 0 0 16px 0;
28
+ font-weight: 500;
29
+ }
30
+ p {
31
+ font-size: 0.875rem;
32
+ color: rgba(255, 255, 255, 0.6);
33
+ line-height: 1.6;
34
+ margin: 0 0 24px 0;
35
+ }
36
+ .retry-btn {
37
+ background: #ff3621;
38
+ color: white;
39
+ border: none;
40
+ padding: 12px 24px;
41
+ border-radius: 6px;
42
+ font-size: 0.875rem;
43
+ font-weight: 500;
44
+ cursor: pointer;
45
+ text-decoration: none;
46
+ display: inline-block;
47
+ }
48
+ .retry-btn:hover {
49
+ background: #e62e1a;
50
+ }
51
+ .tunnel-id {
52
+ font-size: 0.75rem;
53
+ color: rgba(255, 255, 255, 0.4);
54
+ margin-top: 24px;
55
+ font-family: monospace;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="container">
61
+ <div class="icon">🚫</div>
62
+ <h1>Access Denied</h1>
63
+ <p>The tunnel owner has declined your request to view their local environment.</p>
64
+ <a href="?dev={{tunnelId}}&retry=true" class="retry-btn">Request Access Again</a>
65
+ <div class="tunnel-id">Tunnel ID: {{tunnelId}}</div>
66
+ </div>
67
+ </body>
68
+ </html>
@@ -0,0 +1,165 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Local Environment Preview</title>
6
+ </head>
7
+ <body>
8
+ <div id="root">
9
+ <style>
10
+ body {
11
+ background: #1b1b1d;
12
+ height: 100vh;
13
+ margin: 0;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
18
+ "Helvetica", "Arial", sans-serif;
19
+ }
20
+ .loader-container {
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ padding: 50px 70px;
25
+ }
26
+ .logo {
27
+ width: 64px;
28
+ height: 64px;
29
+ margin-bottom: 32px;
30
+ position: relative;
31
+ }
32
+ .logo svg {
33
+ width: 100%;
34
+ height: 100%;
35
+ }
36
+ .spinner {
37
+ width: 32px;
38
+ height: 32px;
39
+ border: 3px solid rgba(255, 255, 255, 0.1);
40
+ border-top: 3px solid #ff3621;
41
+ border-radius: 50%;
42
+ animation: spin 0.8s linear infinite;
43
+ margin-bottom: 32px;
44
+ }
45
+ @keyframes spin {
46
+ 0% {
47
+ transform: rotate(0deg);
48
+ }
49
+ 100% {
50
+ transform: rotate(360deg);
51
+ }
52
+ }
53
+ .loading-text {
54
+ font-size: 1.125rem;
55
+ color: #ffffff;
56
+ letter-spacing: 0.02em;
57
+ font-weight: 400;
58
+ text-align: center;
59
+ margin-top: 0;
60
+ opacity: 0.9;
61
+ }
62
+ .databricks-brand {
63
+ font-size: 0.875rem;
64
+ color: rgba(255, 255, 255, 0.5);
65
+ margin-top: 16px;
66
+ letter-spacing: 0.05em;
67
+ text-transform: uppercase;
68
+ font-weight: 500;
69
+ }
70
+ .dots {
71
+ display: inline-block;
72
+ animation: dots 1.5s steps(4, end) infinite;
73
+ }
74
+ @keyframes dots {
75
+ 0%,
76
+ 20% {
77
+ content: ".";
78
+ }
79
+ 40% {
80
+ content: "..";
81
+ }
82
+ 60%,
83
+ 100% {
84
+ content: "...";
85
+ }
86
+ }
87
+ </style>
88
+ <div class="loader-container">
89
+ <div class="logo">
90
+ <svg
91
+ version="1.1"
92
+ id="Layer_1"
93
+ xmlns:x="ns_extend;"
94
+ xmlns:i="ns_ai;"
95
+ xmlns:graph="ns_graphs;"
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ xmlns:xlink="http://www.w3.org/1999/xlink"
98
+ x="0px"
99
+ y="0px"
100
+ viewBox="0 0 40.1 42"
101
+ style="enable-background: new 0 0 40.1 42"
102
+ xml:space="preserve"
103
+ >
104
+ <style type="text/css">
105
+ .st0 {
106
+ fill: #ff3621;
107
+ }
108
+ </style>
109
+ <metadata>
110
+ <sfw xmlns="ns_sfw;">
111
+ <slices></slices>
112
+ <sliceSourceBounds
113
+ bottomLeftOrigin="true"
114
+ height="42"
115
+ width="40.1"
116
+ x="-69.1"
117
+ y="-10.5"
118
+ ></sliceSourceBounds>
119
+ </sfw>
120
+ </metadata>
121
+ <g>
122
+ <path
123
+ class="st0"
124
+ d="M40.1,31.1v-7.4l-0.8-0.5L20.1,33.7l-18.2-10l0-4.3l18.2,9.9l20.1-10.9v-7.3l-0.8-0.5L20.1,21.2L2.6,11.6
125
+ L20.1,2l14.1,7.7l1.1-0.6V8.3L20.1,0L0,10.9V12L20.1,23l18.2-10v4.4l-18.2,10L0.8,16.8L0,17.3v7.4l20.1,10.9l18.2-9.9v4.3l-18.2,10
126
+ L0.8,29.5L0,30v1.1L20.1,42L40.1,31.1z"
127
+ ></path>
128
+ </g>
129
+ </svg>
130
+ </div>
131
+
132
+ <div class="spinner"></div>
133
+ <div class="loading-text">
134
+ Loading local environment<span class="dots">...</span>
135
+ </div>
136
+ <div class="databricks-brand">Databricks</div>
137
+ </div>
138
+ </div>
139
+ <script type="module">
140
+ import RefreshRuntime from "/@react-refresh";
141
+ if (!window.__vite_plugin_react_preamble_installed__) {
142
+ RefreshRuntime.injectIntoGlobalHook(window);
143
+ window.$RefreshReg$ = () => {};
144
+ window.$RefreshSig$ = () => (type) => type;
145
+ window.__vite_plugin_react_preamble_installed__ = true;
146
+ }
147
+ </script>
148
+
149
+ <script id="vite-client" type="module" src="/@vite/client"></script>
150
+ <script type="module" src="/src/main.tsx"></script>
151
+ <script>
152
+ const viteClient = document.getElementById("vite-client");
153
+ if (viteClient) {
154
+ viteClient.onload = () => {
155
+ console.log("Vite client loaded");
156
+ };
157
+ viteClient.onerror = () => {
158
+ setTimeout(() => {
159
+ window.location.reload();
160
+ }, 1000);
161
+ };
162
+ }
163
+ </script>
164
+ </body>
165
+ </html>
@@ -113,7 +113,8 @@ var RemoteTunnelManager = class {
113
113
  if (approvalResponse) return approvalResponse;
114
114
  }
115
115
  res.cookie("dev-tunnel-id", tunnelId, {
116
- httpOnly: false,
116
+ httpOnly: true,
117
+ secure: true,
117
118
  sameSite: "lax"
118
119
  });
119
120
  const indexPath = path.join(__dirname, "index.html");