@databricks/appkit 0.5.4 → 0.6.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 (195) hide show
  1. package/CLAUDE.md +12 -1
  2. package/NOTICE.md +2 -0
  3. package/bin/appkit.js +0 -0
  4. package/dist/appkit/package.js +1 -1
  5. package/dist/cache/index.js +2 -2
  6. package/dist/cache/index.js.map +1 -1
  7. package/dist/cache/storage/persistent.js.map +1 -1
  8. package/dist/cli/commands/plugins-sync.js +369 -0
  9. package/dist/cli/commands/plugins-sync.js.map +1 -0
  10. package/dist/cli/commands/plugins.js +19 -0
  11. package/dist/cli/commands/plugins.js.map +1 -0
  12. package/dist/cli/index.js +2 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/connectors/index.js +2 -2
  15. package/dist/connectors/{lakebase → lakebase-v1}/client.js +31 -17
  16. package/dist/connectors/lakebase-v1/client.js.map +1 -0
  17. package/dist/connectors/lakebase-v1/defaults.js +18 -0
  18. package/dist/connectors/lakebase-v1/defaults.js.map +1 -0
  19. package/dist/connectors/lakebase-v1/index.js +3 -0
  20. package/dist/core/appkit.d.ts.map +1 -1
  21. package/dist/core/appkit.js +5 -1
  22. package/dist/core/appkit.js.map +1 -1
  23. package/dist/index.d.ts +4 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/plugin/plugin.d.ts +95 -3
  27. package/dist/plugin/plugin.d.ts.map +1 -1
  28. package/dist/plugin/plugin.js +94 -6
  29. package/dist/plugin/plugin.js.map +1 -1
  30. package/dist/plugins/analytics/analytics.d.ts +3 -1
  31. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  32. package/dist/plugins/analytics/analytics.js +3 -1
  33. package/dist/plugins/analytics/analytics.js.map +1 -1
  34. package/dist/plugins/analytics/index.js +1 -0
  35. package/dist/plugins/analytics/manifest.js +21 -0
  36. package/dist/plugins/analytics/manifest.js.map +1 -0
  37. package/dist/plugins/analytics/manifest.json +36 -0
  38. package/dist/plugins/index.js +2 -0
  39. package/dist/plugins/server/index.d.ts +3 -1
  40. package/dist/plugins/server/index.d.ts.map +1 -1
  41. package/dist/plugins/server/index.js +3 -1
  42. package/dist/plugins/server/index.js.map +1 -1
  43. package/dist/plugins/server/manifest.js +21 -0
  44. package/dist/plugins/server/manifest.js.map +1 -0
  45. package/dist/plugins/server/manifest.json +36 -0
  46. package/dist/registry/index.js +5 -0
  47. package/dist/registry/manifest-loader.d.ts +44 -0
  48. package/dist/registry/manifest-loader.d.ts.map +1 -0
  49. package/dist/registry/manifest-loader.js +97 -0
  50. package/dist/registry/manifest-loader.js.map +1 -0
  51. package/dist/registry/resource-registry.d.ts +133 -0
  52. package/dist/registry/resource-registry.d.ts.map +1 -0
  53. package/dist/registry/resource-registry.js +297 -0
  54. package/dist/registry/resource-registry.js.map +1 -0
  55. package/dist/registry/types.d.ts +181 -0
  56. package/dist/registry/types.d.ts.map +1 -0
  57. package/dist/registry/types.js +89 -0
  58. package/dist/registry/types.js.map +1 -0
  59. package/dist/shared/src/plugin.d.ts +66 -1
  60. package/dist/shared/src/plugin.d.ts.map +1 -1
  61. package/docs/docs/api/appkit/Class.AppKitError/index.html +5 -5
  62. package/docs/docs/api/appkit/Class.AuthenticationError/index.html +4 -4
  63. package/docs/docs/api/appkit/Class.ConfigurationError/index.html +4 -4
  64. package/docs/docs/api/appkit/Class.ConnectionError/index.html +4 -4
  65. package/docs/docs/api/appkit/Class.ExecutionError/index.html +4 -4
  66. package/docs/docs/api/appkit/Class.InitializationError/index.html +4 -4
  67. package/docs/docs/api/appkit/Class.Plugin/index.html +28 -16
  68. package/docs/docs/api/appkit/Class.Plugin.md +90 -30
  69. package/docs/docs/api/appkit/Class.ResourceRegistry/index.html +150 -0
  70. package/docs/docs/api/appkit/Class.ResourceRegistry.md +301 -0
  71. package/docs/docs/api/appkit/Class.ServerError/index.html +5 -5
  72. package/docs/docs/api/appkit/Class.TunnelError/index.html +4 -4
  73. package/docs/docs/api/appkit/Class.ValidationError/index.html +4 -4
  74. package/docs/docs/api/appkit/Enumeration.ResourceType/index.html +66 -0
  75. package/docs/docs/api/appkit/Enumeration.ResourceType.md +135 -0
  76. package/docs/docs/api/appkit/Function.appKitTypesPlugin/index.html +4 -4
  77. package/docs/docs/api/appkit/Function.createApp/index.html +4 -4
  78. package/docs/docs/api/appkit/Function.getExecutionContext/index.html +5 -5
  79. package/docs/docs/api/appkit/Function.getPluginManifest/index.html +26 -0
  80. package/docs/docs/api/appkit/Function.getPluginManifest.md +24 -0
  81. package/docs/docs/api/appkit/Function.getResourceRequirements/index.html +28 -0
  82. package/docs/docs/api/appkit/Function.getResourceRequirements.md +42 -0
  83. package/docs/docs/api/appkit/Function.isSQLTypeMarker/index.html +5 -5
  84. package/docs/docs/api/appkit/Interface.BasePluginConfig/index.html +4 -4
  85. package/docs/docs/api/appkit/Interface.CacheConfig/index.html +4 -4
  86. package/docs/docs/api/appkit/Interface.ITelemetry/index.html +5 -5
  87. package/docs/docs/api/appkit/Interface.PluginManifest/index.html +63 -0
  88. package/docs/docs/api/appkit/Interface.PluginManifest.md +135 -0
  89. package/docs/docs/api/appkit/Interface.ResourceEntry/index.html +83 -0
  90. package/docs/docs/api/appkit/Interface.ResourceEntry.md +156 -0
  91. package/docs/docs/api/appkit/Interface.ResourceFieldEntry/index.html +26 -0
  92. package/docs/docs/api/appkit/Interface.ResourceFieldEntry.md +25 -0
  93. package/docs/docs/api/appkit/Interface.ResourceRequirement/index.html +51 -0
  94. package/docs/docs/api/appkit/Interface.ResourceRequirement.md +84 -0
  95. package/docs/docs/api/appkit/Interface.StreamExecutionSettings/index.html +5 -5
  96. package/docs/docs/api/appkit/Interface.TelemetryConfig/index.html +5 -5
  97. package/docs/docs/api/appkit/Interface.ValidationResult/index.html +29 -0
  98. package/docs/docs/api/appkit/Interface.ValidationResult.md +36 -0
  99. package/docs/docs/api/appkit/TypeAlias.ConfigSchema/index.html +21 -0
  100. package/docs/docs/api/appkit/TypeAlias.ConfigSchema.md +12 -0
  101. package/docs/docs/api/appkit/TypeAlias.IAppRouter/index.html +5 -5
  102. package/docs/docs/api/appkit/TypeAlias.ResourcePermission/index.html +18 -0
  103. package/docs/docs/api/appkit/TypeAlias.ResourcePermission.md +20 -0
  104. package/docs/docs/api/appkit/Variable.sql/index.html +5 -5
  105. package/docs/docs/api/appkit/index.html +10 -8
  106. package/docs/docs/api/appkit-ui/data/AreaChart/index.html +3 -3
  107. package/docs/docs/api/appkit-ui/data/BarChart/index.html +3 -3
  108. package/docs/docs/api/appkit-ui/data/DataTable/index.html +3 -3
  109. package/docs/docs/api/appkit-ui/data/DonutChart/index.html +3 -3
  110. package/docs/docs/api/appkit-ui/data/HeatmapChart/index.html +3 -3
  111. package/docs/docs/api/appkit-ui/data/LineChart/index.html +3 -3
  112. package/docs/docs/api/appkit-ui/data/PieChart/index.html +3 -3
  113. package/docs/docs/api/appkit-ui/data/RadarChart/index.html +3 -3
  114. package/docs/docs/api/appkit-ui/data/ScatterChart/index.html +3 -3
  115. package/docs/docs/api/appkit-ui/index.html +3 -3
  116. package/docs/docs/api/appkit-ui/styling/index.html +3 -3
  117. package/docs/docs/api/appkit-ui/ui/Accordion/index.html +3 -3
  118. package/docs/docs/api/appkit-ui/ui/Alert/index.html +3 -3
  119. package/docs/docs/api/appkit-ui/ui/AlertDialog/index.html +3 -3
  120. package/docs/docs/api/appkit-ui/ui/AspectRatio/index.html +3 -3
  121. package/docs/docs/api/appkit-ui/ui/Avatar/index.html +3 -3
  122. package/docs/docs/api/appkit-ui/ui/Badge/index.html +3 -3
  123. package/docs/docs/api/appkit-ui/ui/Breadcrumb/index.html +3 -3
  124. package/docs/docs/api/appkit-ui/ui/Button/index.html +3 -3
  125. package/docs/docs/api/appkit-ui/ui/ButtonGroup/index.html +3 -3
  126. package/docs/docs/api/appkit-ui/ui/Calendar/index.html +3 -3
  127. package/docs/docs/api/appkit-ui/ui/Card/index.html +3 -3
  128. package/docs/docs/api/appkit-ui/ui/Carousel/index.html +3 -3
  129. package/docs/docs/api/appkit-ui/ui/ChartContainer/index.html +3 -3
  130. package/docs/docs/api/appkit-ui/ui/Checkbox/index.html +3 -3
  131. package/docs/docs/api/appkit-ui/ui/Collapsible/index.html +3 -3
  132. package/docs/docs/api/appkit-ui/ui/Command/index.html +3 -3
  133. package/docs/docs/api/appkit-ui/ui/ContextMenu/index.html +3 -3
  134. package/docs/docs/api/appkit-ui/ui/Dialog/index.html +3 -3
  135. package/docs/docs/api/appkit-ui/ui/Drawer/index.html +3 -3
  136. package/docs/docs/api/appkit-ui/ui/DropdownMenu/index.html +3 -3
  137. package/docs/docs/api/appkit-ui/ui/Empty/index.html +3 -3
  138. package/docs/docs/api/appkit-ui/ui/Field/index.html +3 -3
  139. package/docs/docs/api/appkit-ui/ui/FormControl/index.html +3 -3
  140. package/docs/docs/api/appkit-ui/ui/HoverCard/index.html +3 -3
  141. package/docs/docs/api/appkit-ui/ui/Input/index.html +3 -3
  142. package/docs/docs/api/appkit-ui/ui/InputGroup/index.html +3 -3
  143. package/docs/docs/api/appkit-ui/ui/InputOTP/index.html +3 -3
  144. package/docs/docs/api/appkit-ui/ui/Item/index.html +3 -3
  145. package/docs/docs/api/appkit-ui/ui/Kbd/index.html +3 -3
  146. package/docs/docs/api/appkit-ui/ui/Label/index.html +3 -3
  147. package/docs/docs/api/appkit-ui/ui/Menubar/index.html +3 -3
  148. package/docs/docs/api/appkit-ui/ui/NavigationMenu/index.html +3 -3
  149. package/docs/docs/api/appkit-ui/ui/Pagination/index.html +3 -3
  150. package/docs/docs/api/appkit-ui/ui/Popover/index.html +3 -3
  151. package/docs/docs/api/appkit-ui/ui/Progress/index.html +3 -3
  152. package/docs/docs/api/appkit-ui/ui/RadioGroup/index.html +3 -3
  153. package/docs/docs/api/appkit-ui/ui/ResizableHandle/index.html +3 -3
  154. package/docs/docs/api/appkit-ui/ui/ScrollArea/index.html +3 -3
  155. package/docs/docs/api/appkit-ui/ui/Select/index.html +3 -3
  156. package/docs/docs/api/appkit-ui/ui/Separator/index.html +3 -3
  157. package/docs/docs/api/appkit-ui/ui/Sheet/index.html +3 -3
  158. package/docs/docs/api/appkit-ui/ui/Sidebar/index.html +3 -3
  159. package/docs/docs/api/appkit-ui/ui/Skeleton/index.html +3 -3
  160. package/docs/docs/api/appkit-ui/ui/Slider/index.html +3 -3
  161. package/docs/docs/api/appkit-ui/ui/Spinner/index.html +3 -3
  162. package/docs/docs/api/appkit-ui/ui/Switch/index.html +3 -3
  163. package/docs/docs/api/appkit-ui/ui/Table/index.html +3 -3
  164. package/docs/docs/api/appkit-ui/ui/Tabs/index.html +3 -3
  165. package/docs/docs/api/appkit-ui/ui/Textarea/index.html +3 -3
  166. package/docs/docs/api/appkit-ui/ui/Toaster/index.html +3 -3
  167. package/docs/docs/api/appkit-ui/ui/Toggle/index.html +3 -3
  168. package/docs/docs/api/appkit-ui/ui/ToggleGroup/index.html +3 -3
  169. package/docs/docs/api/appkit-ui/ui/Tooltip/index.html +3 -3
  170. package/docs/docs/api/appkit.md +44 -28
  171. package/docs/docs/api/index.html +3 -3
  172. package/docs/docs/app-management/index.html +3 -3
  173. package/docs/docs/architecture/index.html +4 -4
  174. package/docs/docs/architecture.md +1 -1
  175. package/docs/docs/category/development/index.html +3 -3
  176. package/docs/docs/configuration/index.html +3 -3
  177. package/docs/docs/core-principles/index.html +3 -3
  178. package/docs/docs/development/ai-assisted-development/index.html +3 -3
  179. package/docs/docs/development/index.html +3 -3
  180. package/docs/docs/development/llm-guide/index.html +3 -3
  181. package/docs/docs/development/local-development/index.html +3 -3
  182. package/docs/docs/development/project-setup/index.html +3 -3
  183. package/docs/docs/development/remote-bridge/index.html +3 -3
  184. package/docs/docs/development/type-generation/index.html +3 -3
  185. package/docs/docs/index.html +3 -3
  186. package/docs/docs/plugins/index.html +18 -8
  187. package/docs/docs/plugins.md +82 -4
  188. package/llms.txt +12 -1
  189. package/package.json +4 -1
  190. package/dist/connectors/lakebase/client.js.map +0 -1
  191. package/dist/connectors/lakebase/defaults.js +0 -13
  192. package/dist/connectors/lakebase/defaults.js.map +0 -1
  193. package/dist/connectors/lakebase/index.js +0 -3
  194. package/dist/utils/env-validator.js +0 -14
  195. package/dist/utils/env-validator.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-loader.d.ts","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":[],"mappings":";;;;;;AA4DA;;;;;AA4FA;;AAAgD,iBA5FhC,iBAAA,CA4FgC,MAAA,EA5FN,iBA4FM,CAAA,EA5Fc,cA4Fd;;;;;;;;;;;;;;;;;;;iBAAhC,uBAAA,SAAgC"}
@@ -0,0 +1,97 @@
1
+ import { createLogger } from "../logging/logger.js";
2
+ import { ConfigurationError } from "../errors/configuration.js";
3
+ import { init_errors } from "../errors/index.js";
4
+ import { PERMISSIONS_BY_TYPE, ResourceType } from "./types.js";
5
+
6
+ //#region src/registry/manifest-loader.ts
7
+ init_errors();
8
+ const logger = createLogger("manifest-loader");
9
+ function normalizeType(s) {
10
+ const v = Object.values(ResourceType).find((x) => x === s);
11
+ if (v !== void 0) return v;
12
+ throw new ConfigurationError(`Invalid resource type: "${s}". Valid: ${Object.values(ResourceType).join(", ")}`);
13
+ }
14
+ function normalizePermission(type, s) {
15
+ const allowed = PERMISSIONS_BY_TYPE[type];
16
+ if (allowed.includes(s)) return s;
17
+ throw new ConfigurationError(`Invalid permission "${s}" for type ${type}. Valid: ${allowed.join(", ")}`);
18
+ }
19
+ function normalizeResource(r) {
20
+ const type = normalizeType(r.type);
21
+ const permission = normalizePermission(type, r.permission);
22
+ return {
23
+ ...r,
24
+ type,
25
+ permission,
26
+ required: false
27
+ };
28
+ }
29
+ /**
30
+ * Loads and validates the manifest from a plugin constructor.
31
+ * Normalizes string type/permission to strict ResourceType/ResourcePermission.
32
+ *
33
+ * @param plugin - The plugin constructor class
34
+ * @returns The validated, normalized plugin manifest
35
+ * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission
36
+ */
37
+ function getPluginManifest(plugin) {
38
+ const pluginName = plugin.name || "unknown";
39
+ if (!plugin.manifest) throw new ConfigurationError(`Plugin ${pluginName} is missing a manifest. All plugins must declare a static manifest property.`);
40
+ const raw = plugin.manifest;
41
+ if (!raw.name || typeof raw.name !== "string") throw new ConfigurationError(`Plugin ${pluginName} manifest has missing or invalid 'name' field`);
42
+ if (!raw.displayName || typeof raw.displayName !== "string") throw new ConfigurationError(`Plugin ${raw.name} manifest has missing or invalid 'displayName' field`);
43
+ if (!raw.description || typeof raw.description !== "string") throw new ConfigurationError(`Plugin ${raw.name} manifest has missing or invalid 'description' field`);
44
+ if (!raw.resources) throw new ConfigurationError(`Plugin ${raw.name} manifest is missing 'resources' field`);
45
+ if (!Array.isArray(raw.resources.required)) throw new ConfigurationError(`Plugin ${raw.name} manifest has invalid 'resources.required' field (expected array)`);
46
+ if (raw.resources.optional !== void 0 && !Array.isArray(raw.resources.optional)) throw new ConfigurationError(`Plugin ${raw.name} manifest has invalid 'resources.optional' field (expected array)`);
47
+ const required = raw.resources.required.map((r) => {
48
+ const { required: _, ...rest } = normalizeResource(r);
49
+ return rest;
50
+ });
51
+ const optional = (raw.resources.optional || []).map((r) => {
52
+ const { required: _, ...rest } = normalizeResource(r);
53
+ return rest;
54
+ });
55
+ logger.debug("Loaded manifest for plugin %s: %d required resources, %d optional resources", raw.name, required.length, optional.length);
56
+ return {
57
+ ...raw,
58
+ resources: {
59
+ required,
60
+ optional
61
+ }
62
+ };
63
+ }
64
+ /**
65
+ * Gets the resource requirements from a plugin's manifest.
66
+ *
67
+ * Combines required and optional resources into a single array with the
68
+ * `required` flag set appropriately.
69
+ *
70
+ * @param plugin - The plugin constructor class
71
+ * @returns Combined array of required and optional resources
72
+ * @throws {ConfigurationError} If the plugin manifest is missing or invalid
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const resources = getResourceRequirements(AnalyticsPlugin);
77
+ * for (const resource of resources) {
78
+ * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);
79
+ * }
80
+ * ```
81
+ */
82
+ function getResourceRequirements(plugin) {
83
+ const manifest = getPluginManifest(plugin);
84
+ const required = manifest.resources.required.map((r) => ({
85
+ ...r,
86
+ required: true
87
+ }));
88
+ const optional = (manifest.resources.optional || []).map((r) => ({
89
+ ...r,
90
+ required: false
91
+ }));
92
+ return [...required, ...optional];
93
+ }
94
+
95
+ //#endregion
96
+ export { getPluginManifest, getResourceRequirements };
97
+ //# sourceMappingURL=manifest-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-loader.js","names":[],"sources":["../../src/registry/manifest-loader.ts"],"sourcesContent":["import type { PluginConstructor } from \"shared\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type {\n PluginManifest,\n ResourcePermission,\n ResourceRequirement,\n} from \"./types\";\nimport { PERMISSIONS_BY_TYPE, ResourceType } from \"./types\";\n\nconst logger = createLogger(\"manifest-loader\");\n\n/** Loose resource from shared/manifest (string type and permission). */\ninterface LooseResource {\n type: string;\n alias: string;\n resourceKey: string;\n description: string;\n permission: string;\n fields: Record<string, { env: string; description?: string }>;\n}\n\nfunction normalizeType(s: string): ResourceType {\n const v = Object.values(ResourceType).find((x) => x === s);\n if (v !== undefined) return v;\n throw new ConfigurationError(\n `Invalid resource type: \"${s}\". Valid: ${Object.values(ResourceType).join(\", \")}`,\n );\n}\n\nfunction normalizePermission(\n type: ResourceType,\n s: string,\n): ResourcePermission {\n const allowed = PERMISSIONS_BY_TYPE[type];\n if (allowed.includes(s as ResourcePermission)) return s as ResourcePermission;\n throw new ConfigurationError(\n `Invalid permission \"${s}\" for type ${type}. Valid: ${allowed.join(\", \")}`,\n );\n}\n\nfunction normalizeResource(r: LooseResource): ResourceRequirement {\n const type = normalizeType(r.type);\n const permission = normalizePermission(type, r.permission);\n return {\n ...r,\n type,\n permission,\n required: false,\n };\n}\n\n/**\n * Loads and validates the manifest from a plugin constructor.\n * Normalizes string type/permission to strict ResourceType/ResourcePermission.\n *\n * @param plugin - The plugin constructor class\n * @returns The validated, normalized plugin manifest\n * @throws {ConfigurationError} If the manifest is missing, invalid, or has invalid resource type/permission\n */\nexport function getPluginManifest(plugin: PluginConstructor): PluginManifest {\n const pluginName = plugin.name || \"unknown\";\n\n if (!plugin.manifest) {\n throw new ConfigurationError(\n `Plugin ${pluginName} is missing a manifest. All plugins must declare a static manifest property.`,\n );\n }\n\n const raw = plugin.manifest;\n\n if (!raw.name || typeof raw.name !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${pluginName} manifest has missing or invalid 'name' field`,\n );\n }\n\n if (!raw.displayName || typeof raw.displayName !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'displayName' field`,\n );\n }\n\n if (!raw.description || typeof raw.description !== \"string\") {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has missing or invalid 'description' field`,\n );\n }\n\n if (!raw.resources) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest is missing 'resources' field`,\n );\n }\n\n if (!Array.isArray(raw.resources.required)) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.required' field (expected array)`,\n );\n }\n\n if (\n raw.resources.optional !== undefined &&\n !Array.isArray(raw.resources.optional)\n ) {\n throw new ConfigurationError(\n `Plugin ${raw.name} manifest has invalid 'resources.optional' field (expected array)`,\n );\n }\n\n const required = raw.resources.required.map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n const optional = (raw.resources.optional || []).map((r) => {\n const norm = normalizeResource(r as LooseResource);\n const { required: _, ...rest } = norm;\n return rest;\n });\n\n logger.debug(\n \"Loaded manifest for plugin %s: %d required resources, %d optional resources\",\n raw.name,\n required.length,\n optional.length,\n );\n\n return {\n ...raw,\n resources: { required, optional },\n };\n}\n\n/**\n * Gets the resource requirements from a plugin's manifest.\n *\n * Combines required and optional resources into a single array with the\n * `required` flag set appropriately.\n *\n * @param plugin - The plugin constructor class\n * @returns Combined array of required and optional resources\n * @throws {ConfigurationError} If the plugin manifest is missing or invalid\n *\n * @example\n * ```typescript\n * const resources = getResourceRequirements(AnalyticsPlugin);\n * for (const resource of resources) {\n * console.log(`${resource.type}: ${resource.description} (required: ${resource.required})`);\n * }\n * ```\n */\nexport function getResourceRequirements(plugin: PluginConstructor) {\n const manifest = getPluginManifest(plugin);\n\n const required = manifest.resources.required.map((r) => ({\n ...r,\n required: true,\n }));\n const optional = (manifest.resources.optional || []).map((r) => ({\n ...r,\n required: false,\n }));\n\n return [...required, ...optional];\n}\n\n/**\n * Validates a manifest object structure.\n *\n * @param manifest - The manifest object to validate\n * @returns true if the manifest is valid, false otherwise\n *\n * @internal\n */\nexport function isValidManifest(manifest: unknown): manifest is PluginManifest {\n if (!manifest || typeof manifest !== \"object\") {\n return false;\n }\n\n const m = manifest as Record<string, unknown>;\n\n // Check required fields\n if (typeof m.name !== \"string\") return false;\n if (typeof m.displayName !== \"string\") return false;\n if (typeof m.description !== \"string\") return false;\n\n // Check resources structure\n if (!m.resources || typeof m.resources !== \"object\") return false;\n\n const resources = m.resources as Record<string, unknown>;\n if (!Array.isArray(resources.required)) return false;\n\n // Optional field can be missing or must be an array\n if (resources.optional !== undefined && !Array.isArray(resources.optional)) {\n return false;\n }\n\n return true;\n}\n"],"mappings":";;;;;;aAC+C;AAS/C,MAAM,SAAS,aAAa,kBAAkB;AAY9C,SAAS,cAAc,GAAyB;CAC9C,MAAM,IAAI,OAAO,OAAO,aAAa,CAAC,MAAM,MAAM,MAAM,EAAE;AAC1D,KAAI,MAAM,OAAW,QAAO;AAC5B,OAAM,IAAI,mBACR,2BAA2B,EAAE,YAAY,OAAO,OAAO,aAAa,CAAC,KAAK,KAAK,GAChF;;AAGH,SAAS,oBACP,MACA,GACoB;CACpB,MAAM,UAAU,oBAAoB;AACpC,KAAI,QAAQ,SAAS,EAAwB,CAAE,QAAO;AACtD,OAAM,IAAI,mBACR,uBAAuB,EAAE,aAAa,KAAK,WAAW,QAAQ,KAAK,KAAK,GACzE;;AAGH,SAAS,kBAAkB,GAAuC;CAChE,MAAM,OAAO,cAAc,EAAE,KAAK;CAClC,MAAM,aAAa,oBAAoB,MAAM,EAAE,WAAW;AAC1D,QAAO;EACL,GAAG;EACH;EACA;EACA,UAAU;EACX;;;;;;;;;;AAWH,SAAgB,kBAAkB,QAA2C;CAC3E,MAAM,aAAa,OAAO,QAAQ;AAElC,KAAI,CAAC,OAAO,SACV,OAAM,IAAI,mBACR,UAAU,WAAW,8EACtB;CAGH,MAAM,MAAM,OAAO;AAEnB,KAAI,CAAC,IAAI,QAAQ,OAAO,IAAI,SAAS,SACnC,OAAM,IAAI,mBACR,UAAU,WAAW,+CACtB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,eAAe,OAAO,IAAI,gBAAgB,SACjD,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,sDACpB;AAGH,KAAI,CAAC,IAAI,UACP,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,wCACpB;AAGH,KAAI,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CACxC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;AAGH,KACE,IAAI,UAAU,aAAa,UAC3B,CAAC,MAAM,QAAQ,IAAI,UAAU,SAAS,CAEtC,OAAM,IAAI,mBACR,UAAU,IAAI,KAAK,mEACpB;CAGH,MAAM,WAAW,IAAI,UAAU,SAAS,KAAK,MAAM;EAEjD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;CACF,MAAM,YAAY,IAAI,UAAU,YAAY,EAAE,EAAE,KAAK,MAAM;EAEzD,MAAM,EAAE,UAAU,GAAG,GAAG,SADX,kBAAkB,EAAmB;AAElD,SAAO;GACP;AAEF,QAAO,MACL,+EACA,IAAI,MACJ,SAAS,QACT,SAAS,OACV;AAED,QAAO;EACL,GAAG;EACH,WAAW;GAAE;GAAU;GAAU;EAClC;;;;;;;;;;;;;;;;;;;;AAqBH,SAAgB,wBAAwB,QAA2B;CACjE,MAAM,WAAW,kBAAkB,OAAO;CAE1C,MAAM,WAAW,SAAS,UAAU,SAAS,KAAK,OAAO;EACvD,GAAG;EACH,UAAU;EACX,EAAE;CACH,MAAM,YAAY,SAAS,UAAU,YAAY,EAAE,EAAE,KAAK,OAAO;EAC/D,GAAG;EACH,UAAU;EACX,EAAE;AAEH,QAAO,CAAC,GAAG,UAAU,GAAG,SAAS"}
@@ -0,0 +1,133 @@
1
+ import { PluginConstructor, PluginData } from "../shared/src/plugin.js";
2
+ import { ResourceEntry, ResourceRequirement, ValidationResult } from "./types.js";
3
+
4
+ //#region src/registry/resource-registry.d.ts
5
+
6
+ /**
7
+ * Central registry for tracking plugin resource requirements.
8
+ * Deduplication uses type + resourceKey (machine-stable); alias is for display only.
9
+ */
10
+ declare class ResourceRegistry {
11
+ private resources;
12
+ /**
13
+ * Registers a resource requirement for a plugin.
14
+ * If a resource with the same type+resourceKey already exists, merges them:
15
+ * - Combines plugin names (comma-separated)
16
+ * - Uses the most permissive permission (per-type hierarchy)
17
+ * - Marks as required if any plugin requires it
18
+ * - Combines descriptions if they differ
19
+ * - Merges fields; warns when same field name uses different env vars
20
+ *
21
+ * @param plugin - Name of the plugin registering the resource
22
+ * @param resource - Resource requirement specification
23
+ */
24
+ register(plugin: string, resource: ResourceRequirement): void;
25
+ /**
26
+ * Collects and registers resource requirements from an array of plugins.
27
+ * For each plugin, loads its manifest (required) and runtime resource requirements.
28
+ *
29
+ * @param rawPlugins - Array of plugin data entries from createApp configuration
30
+ * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid
31
+ */
32
+ collectResources(rawPlugins: PluginData<PluginConstructor, unknown, string>[]): void;
33
+ /**
34
+ * Merges a new resource requirement with an existing entry.
35
+ * Applies intelligent merging logic for conflicting properties.
36
+ */
37
+ private mergeResources;
38
+ /**
39
+ * Retrieves all registered resources.
40
+ * Returns a copy of the array to prevent external mutations.
41
+ *
42
+ * @returns Array of all registered resource entries
43
+ */
44
+ getAll(): ResourceEntry[];
45
+ /**
46
+ * Gets a specific resource by type and resourceKey (dedup key).
47
+ *
48
+ * @param type - Resource type
49
+ * @param resourceKey - Stable machine key (not alias; alias is for display only)
50
+ * @returns The resource entry if found, undefined otherwise
51
+ */
52
+ get(type: string, resourceKey: string): ResourceEntry | undefined;
53
+ /**
54
+ * Clears all registered resources.
55
+ * Useful for testing or when rebuilding the registry.
56
+ */
57
+ clear(): void;
58
+ /**
59
+ * Returns the number of registered resources.
60
+ */
61
+ size(): number;
62
+ /**
63
+ * Gets all resources required by a specific plugin.
64
+ *
65
+ * @param pluginName - Name of the plugin
66
+ * @returns Array of resources where the plugin is listed as a requester
67
+ */
68
+ getByPlugin(pluginName: string): ResourceEntry[];
69
+ /**
70
+ * Gets all required resources (where required=true).
71
+ *
72
+ * @returns Array of required resource entries
73
+ */
74
+ getRequired(): ResourceEntry[];
75
+ /**
76
+ * Gets all optional resources (where required=false).
77
+ *
78
+ * @returns Array of optional resource entries
79
+ */
80
+ getOptional(): ResourceEntry[];
81
+ /**
82
+ * Validates all registered resources against the environment.
83
+ *
84
+ * Checks each resource's field environment variables to determine if it's resolved.
85
+ * Updates the `resolved` and `values` fields on each resource entry.
86
+ *
87
+ * Only required resources affect the `valid` status - optional resources
88
+ * are checked but don't cause validation failure.
89
+ *
90
+ * @returns ValidationResult with validity status, missing resources, and all resources
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const registry = ResourceRegistry.getInstance();
95
+ * const result = registry.validate();
96
+ *
97
+ * if (!result.valid) {
98
+ * console.error("Missing resources:", result.missing.map(r => Object.values(r.fields).map(f => f.env)));
99
+ * }
100
+ * ```
101
+ */
102
+ validate(): ValidationResult;
103
+ /**
104
+ * Validates all registered resources and enforces the result.
105
+ *
106
+ * - In production: throws a {@link ConfigurationError} if any required resources are missing.
107
+ * - In development (`NODE_ENV=development`): logs a warning but continues, unless
108
+ * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.
109
+ * - When all resources are valid: logs a debug message with the count.
110
+ *
111
+ * @returns ValidationResult with validity status, missing resources, and all resources
112
+ * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true
113
+ */
114
+ enforceValidation(): ValidationResult;
115
+ /**
116
+ * Formats missing resources into a human-readable error message.
117
+ *
118
+ * @param missing - Array of missing resource entries
119
+ * @returns Formatted error message string
120
+ */
121
+ static formatMissingResources(missing: ResourceEntry[]): string;
122
+ /**
123
+ * Formats a highly visible warning banner for dev-mode missing resources.
124
+ * Uses box drawing to ensure the message is impossible to miss in scrolling logs.
125
+ *
126
+ * @param missing - Array of missing resource entries
127
+ * @returns Formatted banner string
128
+ */
129
+ static formatDevWarningBanner(missing: ResourceEntry[]): string;
130
+ }
131
+ //#endregion
132
+ export { ResourceRegistry };
133
+ //# sourceMappingURL=resource-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-registry.d.ts","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAsbgD,cAjYnC,gBAAA,CAiYmC;EAAa,QAAA,SAAA;;;;;;;;;;;;;qCAlXjB;;;;;;;;+BA4B5B,WAAW;;;;;;;;;;;;YAqIR;;;;;;;;0CAW8B;;;;;;;;;;;;;;;;mCAyBP;;;;;;iBAWlB;;;;;;iBASA;;;;;;;;;;;;;;;;;;;;;;cAyBH;;;;;;;;;;;;uBA8DS;;;;;;;yCA6CkB;;;;;;;;yCAqBA"}
@@ -0,0 +1,297 @@
1
+ import { createLogger } from "../logging/logger.js";
2
+ import { ConfigurationError } from "../errors/configuration.js";
3
+ import { init_errors } from "../errors/index.js";
4
+ import { PERMISSION_HIERARCHY_BY_TYPE } from "./types.js";
5
+ import { getPluginManifest } from "./manifest-loader.js";
6
+
7
+ //#region src/registry/resource-registry.ts
8
+ init_errors();
9
+ const logger = createLogger("resource-registry");
10
+ /**
11
+ * Dedup key for registry: type + resourceKey (machine-stable).
12
+ * alias is for UI/display only.
13
+ */
14
+ function getDedupKey(type, resourceKey) {
15
+ return `${type}:${resourceKey}`;
16
+ }
17
+ /**
18
+ * Returns the most permissive permission for a given resource type.
19
+ * Uses per-type hierarchy; unknown permissions are treated as least permissive.
20
+ */
21
+ function getMostPermissivePermission(resourceType, p1, p2) {
22
+ const hierarchy = PERMISSION_HIERARCHY_BY_TYPE[resourceType];
23
+ return (hierarchy?.indexOf(p1) ?? -1) > (hierarchy?.indexOf(p2) ?? -1) ? p1 : p2;
24
+ }
25
+ /**
26
+ * Central registry for tracking plugin resource requirements.
27
+ * Deduplication uses type + resourceKey (machine-stable); alias is for display only.
28
+ */
29
+ var ResourceRegistry = class ResourceRegistry {
30
+ resources = /* @__PURE__ */ new Map();
31
+ /**
32
+ * Registers a resource requirement for a plugin.
33
+ * If a resource with the same type+resourceKey already exists, merges them:
34
+ * - Combines plugin names (comma-separated)
35
+ * - Uses the most permissive permission (per-type hierarchy)
36
+ * - Marks as required if any plugin requires it
37
+ * - Combines descriptions if they differ
38
+ * - Merges fields; warns when same field name uses different env vars
39
+ *
40
+ * @param plugin - Name of the plugin registering the resource
41
+ * @param resource - Resource requirement specification
42
+ */
43
+ register(plugin, resource) {
44
+ const key = getDedupKey(resource.type, resource.resourceKey);
45
+ const existing = this.resources.get(key);
46
+ if (existing) {
47
+ const merged = this.mergeResources(existing, plugin, resource);
48
+ this.resources.set(key, merged);
49
+ } else {
50
+ const entry = {
51
+ ...resource,
52
+ plugin,
53
+ resolved: false,
54
+ permissionSources: { [plugin]: resource.permission }
55
+ };
56
+ this.resources.set(key, entry);
57
+ }
58
+ }
59
+ /**
60
+ * Collects and registers resource requirements from an array of plugins.
61
+ * For each plugin, loads its manifest (required) and runtime resource requirements.
62
+ *
63
+ * @param rawPlugins - Array of plugin data entries from createApp configuration
64
+ * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid
65
+ */
66
+ collectResources(rawPlugins) {
67
+ for (const pluginData of rawPlugins) {
68
+ if (!pluginData?.plugin) continue;
69
+ const pluginName = pluginData.name;
70
+ const manifest = getPluginManifest(pluginData.plugin);
71
+ for (const resource of manifest.resources.required) this.register(pluginName, {
72
+ ...resource,
73
+ required: true
74
+ });
75
+ for (const resource of manifest.resources.optional || []) this.register(pluginName, {
76
+ ...resource,
77
+ required: false
78
+ });
79
+ if (typeof pluginData.plugin.getResourceRequirements === "function") {
80
+ const runtimeResources = pluginData.plugin.getResourceRequirements(pluginData.config);
81
+ for (const resource of runtimeResources) this.register(pluginName, resource);
82
+ }
83
+ logger.debug("Collected resources from plugin %s: %d total", pluginName, this.getByPlugin(pluginName).length);
84
+ }
85
+ }
86
+ /**
87
+ * Merges a new resource requirement with an existing entry.
88
+ * Applies intelligent merging logic for conflicting properties.
89
+ */
90
+ mergeResources(existing, newPlugin, newResource) {
91
+ const plugins = existing.plugin.split(", ");
92
+ if (!plugins.includes(newPlugin)) plugins.push(newPlugin);
93
+ const permissionSources = {
94
+ ...existing.permissionSources ?? {},
95
+ [newPlugin]: newResource.permission
96
+ };
97
+ const permission = getMostPermissivePermission(existing.type, existing.permission, newResource.permission);
98
+ if (permission !== existing.permission) logger.warn("Resource %s:%s permission escalated from \"%s\" to \"%s\" due to plugin \"%s\" (previously requested by: %s). Review plugin permissions to ensure least-privilege.", existing.type, existing.resourceKey, existing.permission, permission, newPlugin, existing.plugin);
99
+ const required = existing.required || newResource.required;
100
+ let description = existing.description;
101
+ if (newResource.description && newResource.description !== existing.description) {
102
+ if (!existing.description.includes(newResource.description)) description = `${existing.description}; ${newResource.description}`;
103
+ }
104
+ const fields = { ...existing.fields ?? {} };
105
+ for (const [fieldName, newField] of Object.entries(newResource.fields ?? {})) {
106
+ const existingField = fields[fieldName];
107
+ if (existingField) {
108
+ if (existingField.env !== newField.env) logger.warn("Resource %s:%s field \"%s\": conflicting env vars \"%s\" (from %s) vs \"%s\" (from %s). Using first.", existing.type, existing.resourceKey, fieldName, existingField.env, existing.plugin, newField.env, newPlugin);
109
+ } else fields[fieldName] = newField;
110
+ }
111
+ return {
112
+ ...existing,
113
+ plugin: plugins.join(", "),
114
+ permission,
115
+ permissionSources,
116
+ required,
117
+ description,
118
+ fields
119
+ };
120
+ }
121
+ /**
122
+ * Retrieves all registered resources.
123
+ * Returns a copy of the array to prevent external mutations.
124
+ *
125
+ * @returns Array of all registered resource entries
126
+ */
127
+ getAll() {
128
+ return Array.from(this.resources.values());
129
+ }
130
+ /**
131
+ * Gets a specific resource by type and resourceKey (dedup key).
132
+ *
133
+ * @param type - Resource type
134
+ * @param resourceKey - Stable machine key (not alias; alias is for display only)
135
+ * @returns The resource entry if found, undefined otherwise
136
+ */
137
+ get(type, resourceKey) {
138
+ return this.resources.get(getDedupKey(type, resourceKey));
139
+ }
140
+ /**
141
+ * Clears all registered resources.
142
+ * Useful for testing or when rebuilding the registry.
143
+ */
144
+ clear() {
145
+ this.resources.clear();
146
+ }
147
+ /**
148
+ * Returns the number of registered resources.
149
+ */
150
+ size() {
151
+ return this.resources.size;
152
+ }
153
+ /**
154
+ * Gets all resources required by a specific plugin.
155
+ *
156
+ * @param pluginName - Name of the plugin
157
+ * @returns Array of resources where the plugin is listed as a requester
158
+ */
159
+ getByPlugin(pluginName) {
160
+ return this.getAll().filter((entry) => entry.plugin.split(", ").includes(pluginName));
161
+ }
162
+ /**
163
+ * Gets all required resources (where required=true).
164
+ *
165
+ * @returns Array of required resource entries
166
+ */
167
+ getRequired() {
168
+ return this.getAll().filter((entry) => entry.required);
169
+ }
170
+ /**
171
+ * Gets all optional resources (where required=false).
172
+ *
173
+ * @returns Array of optional resource entries
174
+ */
175
+ getOptional() {
176
+ return this.getAll().filter((entry) => !entry.required);
177
+ }
178
+ /**
179
+ * Validates all registered resources against the environment.
180
+ *
181
+ * Checks each resource's field environment variables to determine if it's resolved.
182
+ * Updates the `resolved` and `values` fields on each resource entry.
183
+ *
184
+ * Only required resources affect the `valid` status - optional resources
185
+ * are checked but don't cause validation failure.
186
+ *
187
+ * @returns ValidationResult with validity status, missing resources, and all resources
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * const registry = ResourceRegistry.getInstance();
192
+ * const result = registry.validate();
193
+ *
194
+ * if (!result.valid) {
195
+ * console.error("Missing resources:", result.missing.map(r => Object.values(r.fields).map(f => f.env)));
196
+ * }
197
+ * ```
198
+ */
199
+ validate() {
200
+ const missing = [];
201
+ for (const entry of this.resources.values()) {
202
+ const values = {};
203
+ let allSet = true;
204
+ for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {
205
+ const val = process.env[fieldDef.env];
206
+ if (val !== void 0 && val !== "") values[fieldName] = val;
207
+ else allSet = false;
208
+ }
209
+ if (allSet) {
210
+ entry.resolved = true;
211
+ entry.values = values;
212
+ logger.debug("Resource %s:%s resolved from fields", entry.type, entry.alias);
213
+ } else {
214
+ entry.resolved = false;
215
+ entry.values = Object.keys(values).length > 0 ? values : void 0;
216
+ if (entry.required) {
217
+ missing.push(entry);
218
+ logger.debug("Required resource %s:%s missing (fields: %s)", entry.type, entry.alias, Object.keys(entry.fields).join(", "));
219
+ } else logger.debug("Optional resource %s:%s not configured (fields: %s)", entry.type, entry.alias, Object.keys(entry.fields).join(", "));
220
+ }
221
+ }
222
+ return {
223
+ valid: missing.length === 0,
224
+ missing,
225
+ all: this.getAll()
226
+ };
227
+ }
228
+ /**
229
+ * Validates all registered resources and enforces the result.
230
+ *
231
+ * - In production: throws a {@link ConfigurationError} if any required resources are missing.
232
+ * - In development (`NODE_ENV=development`): logs a warning but continues, unless
233
+ * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.
234
+ * - When all resources are valid: logs a debug message with the count.
235
+ *
236
+ * @returns ValidationResult with validity status, missing resources, and all resources
237
+ * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true
238
+ */
239
+ enforceValidation() {
240
+ const validation = this.validate();
241
+ const isDevelopment = process.env.NODE_ENV === "development";
242
+ const strictValidation = process.env.APPKIT_STRICT_VALIDATION === "true" || process.env.APPKIT_STRICT_VALIDATION === "1";
243
+ if (!validation.valid) {
244
+ const errorMessage = ResourceRegistry.formatMissingResources(validation.missing);
245
+ if (!isDevelopment || strictValidation) throw new ConfigurationError(errorMessage, { context: { missingResources: validation.missing.map((r) => ({
246
+ type: r.type,
247
+ alias: r.alias,
248
+ plugin: r.plugin,
249
+ envVars: Object.values(r.fields).map((f) => f.env)
250
+ })) } });
251
+ const banner = ResourceRegistry.formatDevWarningBanner(validation.missing);
252
+ logger.warn("\n%s", banner);
253
+ } else if (this.size() > 0) logger.debug("All %d resources validated successfully", this.size());
254
+ return validation;
255
+ }
256
+ /**
257
+ * Formats missing resources into a human-readable error message.
258
+ *
259
+ * @param missing - Array of missing resource entries
260
+ * @returns Formatted error message string
261
+ */
262
+ static formatMissingResources(missing) {
263
+ if (missing.length === 0) return "No missing resources";
264
+ return `Missing required resources:\n${missing.map((entry) => {
265
+ const envHint = ` (set ${Object.values(entry.fields).map((f) => f.env).join(", ")})`;
266
+ return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;
267
+ }).join("\n")}`;
268
+ }
269
+ /**
270
+ * Formats a highly visible warning banner for dev-mode missing resources.
271
+ * Uses box drawing to ensure the message is impossible to miss in scrolling logs.
272
+ *
273
+ * @param missing - Array of missing resource entries
274
+ * @returns Formatted banner string
275
+ */
276
+ static formatDevWarningBanner(missing) {
277
+ const contentLines = ["MISSING REQUIRED RESOURCES (dev mode — would fail in production)", ""];
278
+ for (const entry of missing) {
279
+ const envVars = Object.values(entry.fields).map((f) => f.env);
280
+ contentLines.push(` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`);
281
+ contentLines.push(` Set: ${envVars.join(", ")}`);
282
+ }
283
+ contentLines.push("");
284
+ contentLines.push("Add these to your .env file or environment to suppress this warning.");
285
+ const maxLen = Math.max(...contentLines.map((l) => l.length));
286
+ const border = "=".repeat(maxLen + 4);
287
+ return [
288
+ border,
289
+ ...contentLines.map((line) => `| ${line.padEnd(maxLen)} |`),
290
+ border
291
+ ].join("\n");
292
+ }
293
+ };
294
+
295
+ //#endregion
296
+ export { ResourceRegistry };
297
+ //# sourceMappingURL=resource-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-registry.js","names":[],"sources":["../../src/registry/resource-registry.ts"],"sourcesContent":["/**\n * Resource Registry\n *\n * Central registry that tracks all resource requirements across all plugins.\n * Provides visibility into Databricks resources needed by the application\n * and handles deduplication when multiple plugins require the same resource\n * (dedup key: type + resourceKey).\n *\n * Use `new ResourceRegistry()` for instance-scoped usage (e.g. createApp).\n * getInstance() / resetInstance() remain for backward compatibility in tests.\n */\n\nimport type { BasePluginConfig, PluginConstructor, PluginData } from \"shared\";\nimport { ConfigurationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { getPluginManifest } from \"./manifest-loader\";\nimport type {\n ResourceEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./types\";\nimport { PERMISSION_HIERARCHY_BY_TYPE, type ResourceType } from \"./types\";\n\nconst logger = createLogger(\"resource-registry\");\n\n/**\n * Dedup key for registry: type + resourceKey (machine-stable).\n * alias is for UI/display only.\n */\nfunction getDedupKey(type: string, resourceKey: string): string {\n return `${type}:${resourceKey}`;\n}\n\n/**\n * Returns the most permissive permission for a given resource type.\n * Uses per-type hierarchy; unknown permissions are treated as least permissive.\n */\nfunction getMostPermissivePermission(\n resourceType: ResourceType,\n p1: ResourcePermission,\n p2: ResourcePermission,\n): ResourcePermission {\n const hierarchy = PERMISSION_HIERARCHY_BY_TYPE[resourceType as ResourceType];\n const index1 = hierarchy?.indexOf(p1) ?? -1;\n const index2 = hierarchy?.indexOf(p2) ?? -1;\n return index1 > index2 ? p1 : p2;\n}\n\n/**\n * Central registry for tracking plugin resource requirements.\n * Deduplication uses type + resourceKey (machine-stable); alias is for display only.\n */\nexport class ResourceRegistry {\n private resources: Map<string, ResourceEntry> = new Map();\n\n /**\n * Registers a resource requirement for a plugin.\n * If a resource with the same type+resourceKey already exists, merges them:\n * - Combines plugin names (comma-separated)\n * - Uses the most permissive permission (per-type hierarchy)\n * - Marks as required if any plugin requires it\n * - Combines descriptions if they differ\n * - Merges fields; warns when same field name uses different env vars\n *\n * @param plugin - Name of the plugin registering the resource\n * @param resource - Resource requirement specification\n */\n public register(plugin: string, resource: ResourceRequirement): void {\n const key = getDedupKey(resource.type, resource.resourceKey);\n const existing = this.resources.get(key);\n\n if (existing) {\n // Merge with existing resource\n const merged = this.mergeResources(existing, plugin, resource);\n this.resources.set(key, merged);\n } else {\n // Create new resource entry with permission source tracking\n const entry: ResourceEntry = {\n ...resource,\n plugin,\n resolved: false,\n permissionSources: { [plugin]: resource.permission },\n };\n this.resources.set(key, entry);\n }\n }\n\n /**\n * Collects and registers resource requirements from an array of plugins.\n * For each plugin, loads its manifest (required) and runtime resource requirements.\n *\n * @param rawPlugins - Array of plugin data entries from createApp configuration\n * @throws {ConfigurationError} If any plugin is missing a manifest or manifest is invalid\n */\n public collectResources(\n rawPlugins: PluginData<PluginConstructor, unknown, string>[],\n ): void {\n for (const pluginData of rawPlugins) {\n if (!pluginData?.plugin) continue;\n\n const pluginName = pluginData.name;\n const manifest = getPluginManifest(pluginData.plugin);\n\n // Register required resources\n for (const resource of manifest.resources.required) {\n this.register(pluginName, { ...resource, required: true });\n }\n\n // Register optional resources\n for (const resource of manifest.resources.optional || []) {\n this.register(pluginName, { ...resource, required: false });\n }\n\n // Check for runtime resource requirements\n if (typeof pluginData.plugin.getResourceRequirements === \"function\") {\n const runtimeResources = pluginData.plugin.getResourceRequirements(\n pluginData.config as BasePluginConfig,\n );\n for (const resource of runtimeResources) {\n this.register(pluginName, resource as ResourceRequirement);\n }\n }\n\n logger.debug(\n \"Collected resources from plugin %s: %d total\",\n pluginName,\n this.getByPlugin(pluginName).length,\n );\n }\n }\n\n /**\n * Merges a new resource requirement with an existing entry.\n * Applies intelligent merging logic for conflicting properties.\n */\n private mergeResources(\n existing: ResourceEntry,\n newPlugin: string,\n newResource: ResourceRequirement,\n ): ResourceEntry {\n // Combine plugin names if not already included\n const plugins = existing.plugin.split(\", \");\n if (!plugins.includes(newPlugin)) {\n plugins.push(newPlugin);\n }\n\n // Track per-plugin permission sources\n const permissionSources: Record<string, ResourcePermission> = {\n ...(existing.permissionSources ?? {}),\n [newPlugin]: newResource.permission,\n };\n\n // Use the most permissive permission for this resource type; warn when escalating\n const permission = getMostPermissivePermission(\n existing.type as ResourceType,\n existing.permission,\n newResource.permission,\n );\n\n if (permission !== existing.permission) {\n logger.warn(\n 'Resource %s:%s permission escalated from \"%s\" to \"%s\" due to plugin \"%s\" ' +\n \"(previously requested by: %s). Review plugin permissions to ensure least-privilege.\",\n existing.type,\n existing.resourceKey,\n existing.permission,\n permission,\n newPlugin,\n existing.plugin,\n );\n }\n\n // Mark as required if any plugin requires it\n const required = existing.required || newResource.required;\n\n // Combine descriptions if they differ\n let description = existing.description;\n if (\n newResource.description &&\n newResource.description !== existing.description\n ) {\n if (!existing.description.includes(newResource.description)) {\n description = `${existing.description}; ${newResource.description}`;\n }\n }\n\n // Merge fields: union of field names; warn when same field name uses different env\n const fields = { ...(existing.fields ?? {}) };\n for (const [fieldName, newField] of Object.entries(\n newResource.fields ?? {},\n )) {\n const existingField = fields[fieldName];\n if (existingField) {\n if (existingField.env !== newField.env) {\n logger.warn(\n 'Resource %s:%s field \"%s\": conflicting env vars \"%s\" (from %s) vs \"%s\" (from %s). Using first.',\n existing.type,\n existing.resourceKey,\n fieldName,\n existingField.env,\n existing.plugin,\n newField.env,\n newPlugin,\n );\n }\n // keep existing\n } else {\n fields[fieldName] = newField;\n }\n }\n\n return {\n ...existing,\n plugin: plugins.join(\", \"),\n permission,\n permissionSources,\n required,\n description,\n fields,\n };\n }\n\n /**\n * Retrieves all registered resources.\n * Returns a copy of the array to prevent external mutations.\n *\n * @returns Array of all registered resource entries\n */\n public getAll(): ResourceEntry[] {\n return Array.from(this.resources.values());\n }\n\n /**\n * Gets a specific resource by type and resourceKey (dedup key).\n *\n * @param type - Resource type\n * @param resourceKey - Stable machine key (not alias; alias is for display only)\n * @returns The resource entry if found, undefined otherwise\n */\n public get(type: string, resourceKey: string): ResourceEntry | undefined {\n return this.resources.get(getDedupKey(type, resourceKey));\n }\n\n /**\n * Clears all registered resources.\n * Useful for testing or when rebuilding the registry.\n */\n public clear(): void {\n this.resources.clear();\n }\n\n /**\n * Returns the number of registered resources.\n */\n public size(): number {\n return this.resources.size;\n }\n\n /**\n * Gets all resources required by a specific plugin.\n *\n * @param pluginName - Name of the plugin\n * @returns Array of resources where the plugin is listed as a requester\n */\n public getByPlugin(pluginName: string): ResourceEntry[] {\n return this.getAll().filter((entry) =>\n entry.plugin.split(\", \").includes(pluginName),\n );\n }\n\n /**\n * Gets all required resources (where required=true).\n *\n * @returns Array of required resource entries\n */\n public getRequired(): ResourceEntry[] {\n return this.getAll().filter((entry) => entry.required);\n }\n\n /**\n * Gets all optional resources (where required=false).\n *\n * @returns Array of optional resource entries\n */\n public getOptional(): ResourceEntry[] {\n return this.getAll().filter((entry) => !entry.required);\n }\n\n /**\n * Validates all registered resources against the environment.\n *\n * Checks each resource's field environment variables to determine if it's resolved.\n * Updates the `resolved` and `values` fields on each resource entry.\n *\n * Only required resources affect the `valid` status - optional resources\n * are checked but don't cause validation failure.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n *\n * @example\n * ```typescript\n * const registry = ResourceRegistry.getInstance();\n * const result = registry.validate();\n *\n * if (!result.valid) {\n * console.error(\"Missing resources:\", result.missing.map(r => Object.values(r.fields).map(f => f.env)));\n * }\n * ```\n */\n public validate(): ValidationResult {\n const missing: ResourceEntry[] = [];\n\n for (const entry of this.resources.values()) {\n const values: Record<string, string> = {};\n let allSet = true;\n for (const [fieldName, fieldDef] of Object.entries(entry.fields)) {\n const val = process.env[fieldDef.env];\n if (val !== undefined && val !== \"\") {\n values[fieldName] = val;\n } else {\n allSet = false;\n }\n }\n if (allSet) {\n entry.resolved = true;\n entry.values = values;\n logger.debug(\n \"Resource %s:%s resolved from fields\",\n entry.type,\n entry.alias,\n );\n } else {\n entry.resolved = false;\n entry.values = Object.keys(values).length > 0 ? values : undefined;\n if (entry.required) {\n missing.push(entry);\n logger.debug(\n \"Required resource %s:%s missing (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n } else {\n logger.debug(\n \"Optional resource %s:%s not configured (fields: %s)\",\n entry.type,\n entry.alias,\n Object.keys(entry.fields).join(\", \"),\n );\n }\n }\n }\n\n return {\n valid: missing.length === 0,\n missing,\n all: this.getAll(),\n };\n }\n\n /**\n * Validates all registered resources and enforces the result.\n *\n * - In production: throws a {@link ConfigurationError} if any required resources are missing.\n * - In development (`NODE_ENV=development`): logs a warning but continues, unless\n * `APPKIT_STRICT_VALIDATION=true` is set, in which case throws like production.\n * - When all resources are valid: logs a debug message with the count.\n *\n * @returns ValidationResult with validity status, missing resources, and all resources\n * @throws {ConfigurationError} In production when required resources are missing, or in dev when APPKIT_STRICT_VALIDATION=true\n */\n public enforceValidation(): ValidationResult {\n const validation = this.validate();\n const isDevelopment = process.env.NODE_ENV === \"development\";\n const strictValidation =\n process.env.APPKIT_STRICT_VALIDATION === \"true\" ||\n process.env.APPKIT_STRICT_VALIDATION === \"1\";\n\n if (!validation.valid) {\n const errorMessage = ResourceRegistry.formatMissingResources(\n validation.missing,\n );\n\n const shouldThrow = !isDevelopment || strictValidation;\n\n if (shouldThrow) {\n throw new ConfigurationError(errorMessage, {\n context: {\n missingResources: validation.missing.map((r) => ({\n type: r.type,\n alias: r.alias,\n plugin: r.plugin,\n envVars: Object.values(r.fields).map((f) => f.env),\n })),\n },\n });\n }\n\n // Dev mode without strict: use a visually prominent box so the warning can't be missed\n const banner = ResourceRegistry.formatDevWarningBanner(\n validation.missing,\n );\n logger.warn(\"\\n%s\", banner);\n } else if (this.size() > 0) {\n logger.debug(\"All %d resources validated successfully\", this.size());\n }\n\n return validation;\n }\n\n /**\n * Formats missing resources into a human-readable error message.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted error message string\n */\n public static formatMissingResources(missing: ResourceEntry[]): string {\n if (missing.length === 0) {\n return \"No missing resources\";\n }\n\n const lines = missing.map((entry) => {\n const envVars = Object.values(entry.fields).map((f) => f.env);\n const envHint = ` (set ${envVars.join(\", \")})`;\n return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;\n });\n\n return `Missing required resources:\\n${lines.join(\"\\n\")}`;\n }\n\n /**\n * Formats a highly visible warning banner for dev-mode missing resources.\n * Uses box drawing to ensure the message is impossible to miss in scrolling logs.\n *\n * @param missing - Array of missing resource entries\n * @returns Formatted banner string\n */\n public static formatDevWarningBanner(missing: ResourceEntry[]): string {\n const contentLines: string[] = [\n \"MISSING REQUIRED RESOURCES (dev mode — would fail in production)\",\n \"\",\n ];\n\n for (const entry of missing) {\n const envVars = Object.values(entry.fields).map((f) => f.env);\n contentLines.push(\n ` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,\n );\n contentLines.push(` Set: ${envVars.join(\", \")}`);\n }\n\n contentLines.push(\"\");\n contentLines.push(\n \"Add these to your .env file or environment to suppress this warning.\",\n );\n\n const maxLen = Math.max(...contentLines.map((l) => l.length));\n const border = \"=\".repeat(maxLen + 4);\n\n const boxed = contentLines.map((line) => `| ${line.padEnd(maxLen)} |`);\n\n return [border, ...boxed, border].join(\"\\n\");\n }\n}\n"],"mappings":"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"}