@toolplex/app-server 0.1.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 (43) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +193 -0
  3. package/dist/auth.d.ts +3 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +17 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/parsing.d.ts +4 -0
  12. package/dist/parsing.d.ts.map +1 -0
  13. package/dist/parsing.js +76 -0
  14. package/dist/parsing.js.map +1 -0
  15. package/dist/plugin.d.ts +6 -0
  16. package/dist/plugin.d.ts.map +1 -0
  17. package/dist/plugin.js +33 -0
  18. package/dist/plugin.js.map +1 -0
  19. package/dist/routes/actions.d.ts +4 -0
  20. package/dist/routes/actions.d.ts.map +1 -0
  21. package/dist/routes/actions.js +19 -0
  22. package/dist/routes/actions.js.map +1 -0
  23. package/dist/routes/context.d.ts +4 -0
  24. package/dist/routes/context.d.ts.map +1 -0
  25. package/dist/routes/context.js +70 -0
  26. package/dist/routes/context.js.map +1 -0
  27. package/dist/routes/data.d.ts +4 -0
  28. package/dist/routes/data.d.ts.map +1 -0
  29. package/dist/routes/data.js +23 -0
  30. package/dist/routes/data.js.map +1 -0
  31. package/dist/routes/pages.d.ts +8 -0
  32. package/dist/routes/pages.d.ts.map +1 -0
  33. package/dist/routes/pages.js +24 -0
  34. package/dist/routes/pages.js.map +1 -0
  35. package/dist/types.d.ts +216 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +5 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/validation.d.ts +5 -0
  40. package/dist/validation.d.ts.map +1 -0
  41. package/dist/validation.js +77 -0
  42. package/dist/validation.js.map +1 -0
  43. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,98 @@
1
+ License text copyright (c) 2025 ToolPlex LLC. All Rights Reserved.
2
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
3
+
4
+ Parameters
5
+
6
+ Licensor: ToolPlex LLC
7
+ Licensed Work: ToolPlex App Server, as published in this repository. The Licensed Work is (c) 2025
8
+ ToolPlex LLC.
9
+ Additional Use Grant: You may make production use of the Licensed Work, provided
10
+ your use does not involve offering it or derivative works on a
11
+ hosted or embedded basis to compete with ToolPlex's paid platform
12
+ or services. For purposes of this license:
13
+
14
+ A "competitive offering" is a product or service that is offered
15
+ to third parties on a paid basis (including through paid support),
16
+ and that significantly overlaps with ToolPlex's platform, including:
17
+ tool discovery, tool execution, agent control, or workflow/playbook
18
+ reuse.
19
+
20
+ "Product" means software made available to end users or used to
21
+ operate hosted services.
22
+
23
+ "Embedded" includes incorporating source or binary components of
24
+ the Licensed Work into another product or requiring the Licensed
25
+ Work for that product to function.
26
+
27
+ "Internal use" within your organization is permitted, even for
28
+ commercial purposes, as long as it is not redistributed or offered
29
+ to others.
30
+
31
+ "API Integration Restrictions": Use of the ToolPlex API or services
32
+ to build, enhance, or operate a competitive offering is prohibited,
33
+ regardless of whether the Licensed Work is directly incorporated.
34
+
35
+ "Derivative Works": Includes any modification, adaptation, or custom
36
+ implementation based on the Licensed Work, including but not limited
37
+ to custom clients for ToolPlex API interaction. While permitted under
38
+ this license, such derivatives may present technical challenges and
39
+ are not officially supported.
40
+
41
+ "Research and Academic Use": Non-commercial research, educational,
42
+ and academic use is permitted regardless of competitive overlap,
43
+ provided results are not used to develop commercial competitive
44
+ offerings.
45
+
46
+ "Official Client Recommendation": While custom clients are permitted
47
+ under this license, ToolPlex strongly recommends using the official
48
+ client to ensure compatibility, security, and optimal performance.
49
+
50
+ Hosting or using the Licensed Work for internal purposes within an
51
+ organization is not considered a competitive offering. ToolPlex
52
+ considers your organization to include all of your affiliates under
53
+ common control.
54
+
55
+ Change Date: None
56
+ Change License: MPL 2.0
57
+
58
+ For information about alternative licensing arrangements for the Licensed Work,
59
+ please contact support@toolplex.ai.
60
+
61
+ Notice
62
+
63
+ Business Source License 1.1
64
+
65
+ Terms
66
+
67
+ The Licensor hereby grants you the right to copy, modify, create derivative
68
+ works, redistribute, and make non-production use of the Licensed Work. The
69
+ Licensor may make an Additional Use Grant, above, permitting limited production use.
70
+
71
+ If your use of the Licensed Work does not comply with the requirements
72
+ currently in effect as described in this License, you must purchase a
73
+ commercial license from the Licensor, its affiliated entities, or authorized
74
+ resellers, or you must refrain from using the Licensed Work.
75
+
76
+ All copies of the original and modified Licensed Work, and derivative works
77
+ of the Licensed Work, are subject to this License. This License applies
78
+ separately for each version of the Licensed Work and the Change Date may vary
79
+ for each version of the Licensed Work released by Licensor.
80
+
81
+ You must conspicuously display this License on each original or modified copy
82
+ of the Licensed Work. If you receive the Licensed Work in original or
83
+ modified form from a third party, the terms and conditions set forth in this
84
+ License apply to your use of that work.
85
+
86
+ Any use of the Licensed Work in violation of this License will automatically
87
+ terminate your rights under this License for the current and all other
88
+ versions of the Licensed Work.
89
+
90
+ This License does not grant you any right in any trademark or logo of
91
+ Licensor or its affiliates (provided that you may use a trademark or logo of
92
+ Licensor as expressly required by this License).
93
+
94
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
95
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
96
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
97
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
98
+ TITLE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # @toolplex/app-server
2
+
3
+ Fastify plugin for serving [ToolPlex](https://toolplex.ai) App Pages. Define page layouts, data handlers, and actions — the plugin generates the HTTP endpoints that power interactive pages in the ToolPlex desktop and mobile apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @toolplex/app-server
9
+ ```
10
+
11
+ Requires Fastify 5+.
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import Fastify from 'fastify';
17
+ import { registerAppPages } from '@toolplex/app-server';
18
+
19
+ const server = Fastify();
20
+
21
+ await server.register(registerAppPages, {
22
+ authToken: process.env.TOOLPLEX_APP_TOKEN,
23
+
24
+ pages: {
25
+ 'production-report': {
26
+ title: 'Production Report',
27
+ filters: [
28
+ { key: 'month', type: 'dropdown', options: ['2026-01', '2026-02', '2026-03'] },
29
+ { key: 'department', type: 'dropdown', options: ['socks', 'accessories'] },
30
+ ],
31
+ sections: [
32
+ { type: 'card-row', source: 'kpis' },
33
+ { type: 'table', source: 'production', rowKey: 'id', columns: [
34
+ { key: 'department', label: 'Department' },
35
+ { key: 'units', label: 'Units', format: 'integer' },
36
+ { key: 'rate', label: 'Fulfillment', format: 'percent' },
37
+ { key: 'status', label: 'Status', format: { type: 'status', colors: { on_track: 'green', behind: 'yellow' } } },
38
+ ]},
39
+ ],
40
+ },
41
+ },
42
+
43
+ resources: {
44
+ production: {
45
+ fetch: async ({ page, pageSize, sort, filters }) => {
46
+ const rows = await db.query('SELECT * FROM production WHERE ...', filters);
47
+ const total = await db.count('production');
48
+ return { rows, total };
49
+ },
50
+ },
51
+ kpis: {
52
+ fetch: async () => ({
53
+ rows: [
54
+ { label: 'Fulfillment', value: 0.87, format: 'percent' },
55
+ { label: 'Units', value: 12400, format: 'integer' },
56
+ ],
57
+ total: 2,
58
+ }),
59
+ },
60
+ },
61
+
62
+ actions: {},
63
+ });
64
+
65
+ await server.listen({ port: 3100 });
66
+ ```
67
+
68
+ ## Generated Endpoints
69
+
70
+ | Route | Method | Description |
71
+ |-------|--------|-------------|
72
+ | `/pages` | GET | List all page definitions |
73
+ | `/pages/:pageId` | GET | Single page definition |
74
+ | `/data/:resource` | GET | Paginated data (query params: `page`, `pageSize`, `sort`, filters) |
75
+ | `/actions/:action` | POST | Execute an action (`{ ids, params, filters }`) |
76
+ | `/context/:resource` | GET | Agent context for a resource |
77
+ | `/context/page/:pageId` | GET | Agent context for an entire page |
78
+
79
+ All routes require `Authorization: Bearer <token>` matching the configured `authToken`.
80
+
81
+ ## Page Definition
82
+
83
+ ```typescript
84
+ {
85
+ title: string;
86
+ filters?: Filter[]; // Dropdown, text, or date filters
87
+ actions?: Action[]; // Toolbar or inline row actions
88
+ suggestions?: string[]; // Ghost suggestions for the agent sidebar
89
+ sections: (Section | Section[])[]; // Layout — single = full width, array = side-by-side grid
90
+ }
91
+ ```
92
+
93
+ ### Sections
94
+
95
+ Sections render top-to-bottom. Wrap sections in an array for side-by-side layout using a 12-column grid:
96
+
97
+ ```typescript
98
+ sections: [
99
+ { type: 'card-row', source: 'kpis' }, // Full width
100
+ [ // Side by side
101
+ { type: 'table', source: 'data', rowKey: 'id', span: 8, columns: [...] },
102
+ { type: 'card-column', source: 'detail', span: 4 },
103
+ ],
104
+ ]
105
+ ```
106
+
107
+ **Section types:**
108
+ - `card-row` — Horizontal row of metric cards
109
+ - `card-column` — Vertical stack of cards (useful as a sidebar)
110
+ - `table` — Paginated, sortable data grid with row selection
111
+
112
+ Tables support a `detail` field for a slide-out drawer:
113
+
114
+ ```typescript
115
+ { type: 'table', source: 'orders', rowKey: 'id', columns: [...],
116
+ detail: { source: 'order_detail' } }
117
+ ```
118
+
119
+ ### Column Formatting
120
+
121
+ Simple formats as strings, rich formats as objects:
122
+
123
+ ```typescript
124
+ { key: 'amount', label: 'Amount', format: 'currency' } // $1,234.00
125
+ { key: 'rate', label: 'Rate', format: 'percent' } // 87.5%
126
+ { key: 'active', label: 'Active', format: 'boolean' } // ✓ / ✗
127
+ { key: 'status', label: 'Status', format: { type: 'status', // Colored badge
128
+ colors: { active: 'green', pending: 'yellow' } } }
129
+ { key: 'change', label: 'YoY', format: { type: 'delta', // +12.3% / -5.1%
130
+ format: 'percent' } }
131
+ { key: 'done', label: 'Done', format: { type: 'progress' } } // Progress bar
132
+ { key: 'url', label: 'Link', format: { type: 'link' } } // Clickable URL
133
+ { key: 'photo', label: '', format: { type: 'image', width: 32 } } // Thumbnail
134
+ ```
135
+
136
+ ### Actions
137
+
138
+ ```typescript
139
+ actions: [
140
+ // Inline — button on each row
141
+ { label: 'Approve', action: 'approve', placement: 'inline' },
142
+
143
+ // Toolbar — operates on checkbox-selected rows
144
+ { label: 'Export', action: 'export_csv', placement: 'toolbar', selection_required: true },
145
+
146
+ // Global — no row selection needed
147
+ { label: 'Refresh', action: 'refresh_data', placement: 'toolbar' },
148
+ ]
149
+ ```
150
+
151
+ ### Detail Drawer
152
+
153
+ When a table has `detail: { source: 'order_detail' }`, clicking a row opens a slide-out panel. The detail resource returns typed blocks:
154
+
155
+ ```typescript
156
+ order_detail: {
157
+ fetch: async ({ selection }) => ({
158
+ rows: [
159
+ { type: 'header', value: 'Order #1234' },
160
+ { type: 'field', label: 'Customer', value: 'Acme Corp' },
161
+ { type: 'field', label: 'Total', value: 1250, format: 'currency' },
162
+ { type: 'list', label: 'Notes', items: [{ label: 'Rush delivery requested' }] },
163
+ { type: 'table', label: 'Line Items', columns: [...], rows: [...] },
164
+ { type: 'image', label: 'Receipt', url: 'https://...' },
165
+ ],
166
+ total: 1,
167
+ }),
168
+ }
169
+ ```
170
+
171
+ ## Handler Contracts
172
+
173
+ **Fetch** — receives `{ page, pageSize, sort?, filters?, selection? }`, returns `{ rows, total }`. The plugin wraps the response with pagination metadata.
174
+
175
+ **Action** — receives `{ ids, params, filters }`, returns `{ affected, message?, data? }`. `ids` can be empty for global actions.
176
+
177
+ **Context** — receives `{ filters?, selection? }`, returns `{ summary, selection?, suggestions? }`. Used by the ToolPlex agent to understand what's on screen.
178
+
179
+ ## Validation
180
+
181
+ The plugin validates configuration at startup. Misconfigurations throw immediately with descriptive errors:
182
+
183
+ ```
184
+ @toolplex/app-server configuration errors:
185
+ - Page "orders": table section with source "orders" is missing required "rowKey"
186
+ - Page "orders": references action "approve" but no action handler is defined
187
+ ```
188
+
189
+ Fetch and action handler responses are validated at runtime — missing `rows`, wrong `total` type, etc.
190
+
191
+ ## License
192
+
193
+ See [LICENSE](./LICENSE) for details.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { FastifyRequest, FastifyReply } from "fastify";
2
+ export declare function createAuthHook(expectedToken: string): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5D,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,IAEhD,SAAS,cAAc,EACvB,OAAO,YAAY,KAClB,OAAO,CAAC,IAAI,CAAC,CAiBjB"}
package/dist/auth.js ADDED
@@ -0,0 +1,17 @@
1
+ export function createAuthHook(expectedToken) {
2
+ return async function verifyBearerToken(request, reply) {
3
+ const header = request.headers.authorization;
4
+ if (!header?.startsWith("Bearer ")) {
5
+ reply
6
+ .code(401)
7
+ .send({ error: "Missing or malformed Authorization header" });
8
+ return reply;
9
+ }
10
+ const token = header.slice(7);
11
+ if (token !== expectedToken) {
12
+ reply.code(401).send({ error: "Invalid token" });
13
+ return reply;
14
+ }
15
+ };
16
+ }
17
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,cAAc,CAAC,aAAqB;IAClD,OAAO,KAAK,UAAU,iBAAiB,CACrC,OAAuB,EACvB,KAAmB;QAEnB,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QAE7C,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACnC,KAAK;iBACF,IAAI,CAAC,GAAG,CAAC;iBACT,IAAI,CAAC,EAAE,KAAK,EAAE,2CAA2C,EAAE,CAAC,CAAC;YAChE,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAE9B,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;YACjD,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { registerAppPages } from "./plugin.js";
2
+ export type { AppServerConfig, ResourceDefinition, PageDefinition, Section, CardRowSection, CardColumnSection, TableSection, Column, Filter, Action, ColumnFormat, SimpleFormat, RichFormat, StatusFormat, DeltaFormat, LinkFormat, ImageFormat, ProgressFormat, FetchRequest, FetchResponse, ActionRequest, ActionResponse, ContextRequest, ContextResponse, PageContextRequest, PaginatedResponse, CardData, DetailBlock, DetailHeader, DetailField, DetailList, DetailTable, DetailImage, FetchHandler, ActionHandler, ContextHandler, PageContextHandler, SortSpec, Selection, } from "./types.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,YAAY,EAEV,eAAe,EACf,kBAAkB,EAGlB,cAAc,EACd,OAAO,EACP,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,MAAM,EACN,MAAM,EACN,MAAM,EAGN,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,cAAc,EAGd,YAAY,EACZ,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EAGjB,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EAGX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,kBAAkB,EAGlB,QAAQ,EACR,SAAS,GACV,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // @toolplex/app-server — Fastify plugin for serving ToolPlex App Pages
2
+ export { registerAppPages } from "./plugin.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { FetchRequest, ContextRequest } from "./types.js";
2
+ export declare function parseFetchParams(query: Record<string, string | undefined>): FetchRequest;
3
+ export declare function parseContextParams(query: Record<string, string | undefined>): ContextRequest;
4
+ //# sourceMappingURL=parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../src/parsing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAuB,MAAM,YAAY,CAAC;AAUpF,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACxC,YAAY,CAQd;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACxC,cAAc,CAKhB"}
@@ -0,0 +1,76 @@
1
+ const DEFAULT_PAGE = 1;
2
+ const DEFAULT_PAGE_SIZE = 50;
3
+ const MAX_PAGE_SIZE = 500;
4
+ // ---------------------------------------------------------------------------
5
+ // Parse query params into typed request objects
6
+ // ---------------------------------------------------------------------------
7
+ export function parseFetchParams(query) {
8
+ const page = clampInt(query.page, DEFAULT_PAGE, 1);
9
+ const pageSize = clampInt(query.pageSize, DEFAULT_PAGE_SIZE, 1, MAX_PAGE_SIZE);
10
+ const sort = parseSort(query.sort);
11
+ const filters = parseFilters(query);
12
+ const selection = parseSelection(query.selection);
13
+ return { page, pageSize, sort, filters, selection };
14
+ }
15
+ export function parseContextParams(query) {
16
+ return {
17
+ filters: parseFilters(query),
18
+ selection: parseSelection(query.selection),
19
+ };
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+ function clampInt(raw, fallback, min, max) {
25
+ if (!raw)
26
+ return fallback;
27
+ const n = parseInt(raw, 10);
28
+ if (isNaN(n))
29
+ return fallback;
30
+ if (n < min)
31
+ return min;
32
+ if (max !== undefined && n > max)
33
+ return max;
34
+ return n;
35
+ }
36
+ function parseSort(raw) {
37
+ if (!raw)
38
+ return undefined;
39
+ const [key, dir] = raw.split(",");
40
+ if (!key)
41
+ return undefined;
42
+ return {
43
+ key,
44
+ direction: dir === "desc" ? "desc" : "asc",
45
+ };
46
+ }
47
+ function parseSelection(raw) {
48
+ if (!raw)
49
+ return undefined;
50
+ try {
51
+ const parsed = JSON.parse(raw);
52
+ if (!Array.isArray(parsed.ids))
53
+ return undefined;
54
+ return parsed;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Extract filter params from query string.
62
+ * Reserved keys (page, pageSize, sort, selection) are excluded.
63
+ */
64
+ const RESERVED_KEYS = new Set(["page", "pageSize", "sort", "selection"]);
65
+ function parseFilters(query) {
66
+ const filters = {};
67
+ let hasFilters = false;
68
+ for (const [key, value] of Object.entries(query)) {
69
+ if (RESERVED_KEYS.has(key) || value === undefined)
70
+ continue;
71
+ filters[key] = value;
72
+ hasFilters = true;
73
+ }
74
+ return hasFilters ? filters : undefined;
75
+ }
76
+ //# sourceMappingURL=parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsing.js","sourceRoot":"","sources":["../src/parsing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { AppServerConfig } from "./types.js";
3
+ declare function appServerPlugin(fastify: FastifyInstance, config: AppServerConfig): Promise<void>;
4
+ export declare const registerAppPages: typeof appServerPlugin;
5
+ export {};
6
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAQlD,iBAAe,eAAe,CAC5B,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,eAAO,MAAM,gBAAgB,wBAG3B,CAAC"}
package/dist/plugin.js ADDED
@@ -0,0 +1,33 @@
1
+ import fp from "fastify-plugin";
2
+ import { validateConfig } from "./validation.js";
3
+ import { createAuthHook } from "./auth.js";
4
+ import { registerPageRoutes } from "./routes/pages.js";
5
+ import { registerDataRoutes } from "./routes/data.js";
6
+ import { registerActionRoutes } from "./routes/actions.js";
7
+ import { registerContextRoutes } from "./routes/context.js";
8
+ async function appServerPlugin(fastify, config) {
9
+ // Validate everything at startup — fail fast on misconfiguration
10
+ validateConfig(config);
11
+ // Apply bearer token auth to all routes in this plugin scope
12
+ const authHook = createAuthHook(config.authToken);
13
+ fastify.addHook("onRequest", authHook);
14
+ // Register route groups
15
+ registerPageRoutes(fastify, config);
16
+ registerDataRoutes(fastify, config);
17
+ registerActionRoutes(fastify, config);
18
+ registerContextRoutes(fastify, config);
19
+ // Catch handler errors and return structured responses
20
+ fastify.setErrorHandler(async (error, _request, reply) => {
21
+ const status = error.statusCode ?? 500;
22
+ const message = status >= 500
23
+ ? "Internal server error in app-server handler"
24
+ : error.message;
25
+ fastify.log.error({ err: error, statusCode: status }, "app-server handler error");
26
+ return reply.code(status).send({ error: message });
27
+ });
28
+ }
29
+ export const registerAppPages = fp(appServerPlugin, {
30
+ name: "@toolplex/app-server",
31
+ fastify: ">=5.0.0",
32
+ });
33
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGhC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,KAAK,UAAU,eAAe,CAC5B,OAAwB,EACxB,MAAuB;IAEvB,iEAAiE;IACjE,cAAc,CAAC,MAAM,CAAC,CAAC;IAEvB,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAClD,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAEvC,wBAAwB;IACxB,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACtC,qBAAqB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAEvC,uDAAuD;IACvD,OAAO,CAAC,eAAe,CAAC,KAAK,EAAE,KAAsC,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACxF,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,IAAI,GAAG,CAAC;QACvC,MAAM,OAAO,GACX,MAAM,IAAI,GAAG;YACX,CAAC,CAAC,6CAA6C;YAC/C,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;QAEpB,OAAO,CAAC,GAAG,CAAC,KAAK,CACf,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,EAClC,0BAA0B,CAC3B,CAAC;QAEF,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC,eAAe,EAAE;IAClD,IAAI,EAAE,sBAAsB;IAC5B,OAAO,EAAE,SAAS;CACnB,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { AppServerConfig } from "../types.js";
3
+ export declare function registerActionRoutes(fastify: FastifyInstance, config: AppServerConfig): void;
4
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/routes/actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,eAAe,GACtB,IAAI,CAwBN"}
@@ -0,0 +1,19 @@
1
+ import { validateActionResponse } from "../validation.js";
2
+ export function registerActionRoutes(fastify, config) {
3
+ fastify.post("/actions/:action", async (request, reply) => {
4
+ const { action } = request.params;
5
+ const handler = config.actions[action];
6
+ if (!handler) {
7
+ return reply.code(404).send({ error: `Action "${action}" not found` });
8
+ }
9
+ const { ids, params, filters } = request.body ?? {};
10
+ const response = await handler({
11
+ ids: Array.isArray(ids) ? ids : [],
12
+ params: params ?? {},
13
+ filters: filters ?? {},
14
+ });
15
+ validateActionResponse(action, response);
16
+ return reply.send(response);
17
+ });
18
+ }
19
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/routes/actions.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,UAAU,oBAAoB,CAClC,OAAwB,EACxB,MAAuB;IAEvB,OAAO,CAAC,IAAI,CAGT,kBAAkB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC9C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEvC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,MAAM,aAAa,EAAE,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;QAEpD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;YAC7B,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;YAClC,MAAM,EAAE,MAAM,IAAI,EAAE;YACpB,OAAO,EAAE,OAAO,IAAI,EAAE;SACvB,CAAC,CAAC;QAEH,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAEzC,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { AppServerConfig } from "../types.js";
3
+ export declare function registerContextRoutes(fastify: FastifyInstance, config: AppServerConfig): void;
4
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/routes/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAA4B,MAAM,aAAa,CAAC;AAG7E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,eAAe,GACtB,IAAI,CA6CN"}
@@ -0,0 +1,70 @@
1
+ import { parseContextParams } from "../parsing.js";
2
+ export function registerContextRoutes(fastify, config) {
3
+ // Resource-level context
4
+ fastify.get("/context/:resource", async (request, reply) => {
5
+ const { resource } = request.params;
6
+ const definition = config.resources[resource];
7
+ if (!definition?.context) {
8
+ return reply
9
+ .code(404)
10
+ .send({ error: `No context handler for resource "${resource}"` });
11
+ }
12
+ const params = parseContextParams(request.query);
13
+ const response = await definition.context(params);
14
+ return reply.send(response);
15
+ });
16
+ // Page-level context
17
+ fastify.get("/context/page/:pageId", async (request, reply) => {
18
+ const { pageId } = request.params;
19
+ const page = config.pages[pageId];
20
+ if (!page) {
21
+ return reply.code(404).send({ error: `Page "${pageId}" not found` });
22
+ }
23
+ const params = parseContextParams(request.query);
24
+ const sectionSources = uniqueSources(page.sections);
25
+ // If page has a dedicated context handler, use it
26
+ if (page.context) {
27
+ const response = await page.context({ ...params, sections: sectionSources });
28
+ return reply.send(response);
29
+ }
30
+ // Fallback: aggregate resource-level context handlers
31
+ const response = await aggregateContext(config, sectionSources, params);
32
+ return reply.send(response);
33
+ });
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+ function uniqueSources(sections) {
39
+ const sources = new Set();
40
+ for (const entry of sections) {
41
+ const items = Array.isArray(entry) ? entry : [entry];
42
+ for (const section of items) {
43
+ sources.add(section.source);
44
+ }
45
+ }
46
+ return [...sources];
47
+ }
48
+ async function aggregateContext(config, sources, params) {
49
+ const summaries = [];
50
+ let selectionText;
51
+ const suggestions = [];
52
+ for (const source of sources) {
53
+ const handler = config.resources[source]?.context;
54
+ if (!handler)
55
+ continue;
56
+ const result = await handler(params);
57
+ if (result.summary)
58
+ summaries.push(result.summary);
59
+ if (result.selection && !selectionText)
60
+ selectionText = result.selection;
61
+ if (result.suggestions)
62
+ suggestions.push(...result.suggestions);
63
+ }
64
+ return {
65
+ summary: summaries.join(" "),
66
+ selection: selectionText,
67
+ suggestions: suggestions.length > 0 ? suggestions : undefined,
68
+ };
69
+ }
70
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/routes/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,UAAU,qBAAqB,CACnC,OAAwB,EACxB,MAAuB;IAEvB,yBAAyB;IACzB,OAAO,CAAC,GAAG,CAGR,oBAAoB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChD,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QACpC,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAE9C,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;YACzB,OAAO,KAAK;iBACT,IAAI,CAAC,GAAG,CAAC;iBACT,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,QAAQ,GAAG,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAClD,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,qBAAqB;IACrB,OAAO,CAAC,GAAG,CAGR,uBAAuB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACnD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QAClC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAElC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,MAAM,aAAa,EAAE,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEpD,kDAAkD;QAClD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC;YAC7E,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;QAED,sDAAsD;QACtD,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC;QACxE,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,aAAa,CAAC,QAAiC;IACtD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACrD,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtB,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,MAAuB,EACvB,OAAiB,EACjB,MAA6G;IAE7G,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,aAAiC,CAAC;IACtC,MAAM,WAAW,GAAa,EAAE,CAAC;IAEjC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAClD,IAAI,CAAC,OAAO;YAAE,SAAS;QAEvB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,OAAO;YAAE,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,MAAM,CAAC,SAAS,IAAI,CAAC,aAAa;YAAE,aAAa,GAAG,MAAM,CAAC,SAAS,CAAC;QACzE,IAAI,MAAM,CAAC,WAAW;YAAE,WAAW,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAClE,CAAC;IAED,OAAO;QACL,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;QAC5B,SAAS,EAAE,aAAa;QACxB,WAAW,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KAC9D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { AppServerConfig } from "../types.js";
3
+ export declare function registerDataRoutes(fastify: FastifyInstance, config: AppServerConfig): void;
4
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/routes/data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,aAAa,CAAC;AAItE,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,eAAe,GACtB,IAAI,CA2BN"}
@@ -0,0 +1,23 @@
1
+ import { parseFetchParams } from "../parsing.js";
2
+ import { validateFetchResponse } from "../validation.js";
3
+ export function registerDataRoutes(fastify, config) {
4
+ fastify.get("/data/:resource", async (request, reply) => {
5
+ const { resource } = request.params;
6
+ const definition = config.resources[resource];
7
+ if (!definition) {
8
+ return reply.code(404).send({ error: `Resource "${resource}" not found` });
9
+ }
10
+ const params = parseFetchParams(request.query);
11
+ const response = await definition.fetch(params);
12
+ validateFetchResponse(resource, response);
13
+ const result = {
14
+ rows: response.rows,
15
+ total: response.total,
16
+ page: params.page,
17
+ pageSize: params.pageSize,
18
+ totalPages: Math.ceil(response.total / params.pageSize),
19
+ };
20
+ return reply.send(result);
21
+ });
22
+ }
23
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.js","sourceRoot":"","sources":["../../src/routes/data.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,MAAM,UAAU,kBAAkB,CAChC,OAAwB,EACxB,MAAuB;IAEvB,OAAO,CAAC,GAAG,CAGR,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC7C,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QACpC,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAE9C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,aAAa,QAAQ,aAAa,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAEhD,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAE1C,MAAM,MAAM,GAAsB;YAChC,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC;SACxD,CAAC;QAEF,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { AppServerConfig } from "../types.js";
3
+ /**
4
+ * Serves page definitions. Context handlers are stripped since they're
5
+ * server-side functions that can't be serialized to JSON.
6
+ */
7
+ export declare function registerPageRoutes(fastify: FastifyInstance, config: AppServerConfig): void;
8
+ //# sourceMappingURL=pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/routes/pages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnE;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,eAAe,GACtB,IAAI,CAiBN"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Serves page definitions. Context handlers are stripped since they're
3
+ * server-side functions that can't be serialized to JSON.
4
+ */
5
+ export function registerPageRoutes(fastify, config) {
6
+ const serialized = buildSerializablePages(config);
7
+ fastify.get("/pages", async (_request, reply) => {
8
+ return reply.send(serialized);
9
+ });
10
+ fastify.get("/pages/:pageId", async (request, reply) => {
11
+ const page = serialized.find((p) => p.id === request.params.pageId);
12
+ if (!page) {
13
+ return reply.code(404).send({ error: "Page not found" });
14
+ }
15
+ return reply.send(page);
16
+ });
17
+ }
18
+ function buildSerializablePages(config) {
19
+ return Object.entries(config.pages).map(([id, page]) => {
20
+ const { context: _context, ...rest } = page;
21
+ return { id, ...rest };
22
+ });
23
+ }
24
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/routes/pages.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAwB,EACxB,MAAuB;IAEvB,MAAM,UAAU,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAElD,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CACT,gBAAgB,EAChB,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CACF,CAAC;AACJ,CAAC;AAQD,SAAS,sBAAsB,CAC7B,MAAuB;IAEvB,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;QACrD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QAC5C,OAAO,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC;IACzB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,216 @@
1
+ export interface PageDefinition {
2
+ id: string;
3
+ title: string;
4
+ filters?: Filter[];
5
+ actions?: Action[];
6
+ suggestions?: string[];
7
+ sections: (Section | Section[])[];
8
+ context?: PageContextHandler;
9
+ }
10
+ export type Section = CardRowSection | CardColumnSection | TableSection;
11
+ export interface CardRowSection {
12
+ type: "card-row";
13
+ source: string;
14
+ span?: number;
15
+ }
16
+ export interface CardColumnSection {
17
+ type: "card-column";
18
+ source: string;
19
+ span?: number;
20
+ }
21
+ export interface TableSection {
22
+ type: "table";
23
+ source: string;
24
+ rowKey: string;
25
+ columns: Column[];
26
+ span?: number;
27
+ detail?: {
28
+ source: string;
29
+ };
30
+ }
31
+ export interface Column {
32
+ key: string;
33
+ label: string;
34
+ format?: ColumnFormat;
35
+ width?: number;
36
+ }
37
+ export type ColumnFormat = SimpleFormat | RichFormat;
38
+ /** String shorthands — cover 90% of columns */
39
+ export type SimpleFormat = "text" | "integer" | "number" | "percent" | "currency" | "date" | "boolean";
40
+ /** Object format — richer visual treatment */
41
+ export type RichFormat = StatusFormat | DeltaFormat | LinkFormat | ImageFormat | ProgressFormat;
42
+ /** Colored badge — maps cell values to colors */
43
+ export interface StatusFormat {
44
+ type: "status";
45
+ colors?: Record<string, string>;
46
+ }
47
+ /** Signed delta — green positive, red negative */
48
+ export interface DeltaFormat {
49
+ type: "delta";
50
+ format?: "number" | "percent";
51
+ }
52
+ /** Clickable URL */
53
+ export interface LinkFormat {
54
+ type: "link";
55
+ label?: string;
56
+ }
57
+ /** Inline thumbnail */
58
+ export interface ImageFormat {
59
+ type: "image";
60
+ width?: number;
61
+ height?: number;
62
+ }
63
+ /** Progress bar for 0–1 values */
64
+ export interface ProgressFormat {
65
+ type: "progress";
66
+ }
67
+ export interface Filter {
68
+ key: string;
69
+ type: "dropdown" | "text" | "date";
70
+ label?: string;
71
+ options?: string[];
72
+ options_source?: string;
73
+ default?: string;
74
+ }
75
+ export interface Action {
76
+ label: string;
77
+ action: string;
78
+ params?: Record<string, unknown>;
79
+ confirm?: string;
80
+ placement?: "toolbar" | "inline";
81
+ selection_required?: boolean;
82
+ }
83
+ export interface FetchRequest {
84
+ page: number;
85
+ pageSize: number;
86
+ sort?: SortSpec;
87
+ filters?: Record<string, string>;
88
+ selection?: Selection;
89
+ }
90
+ export interface FetchResponse {
91
+ rows: Record<string, unknown>[];
92
+ total: number;
93
+ }
94
+ export interface ActionRequest {
95
+ ids: (string | number)[];
96
+ params: Record<string, unknown>;
97
+ filters: Record<string, string>;
98
+ }
99
+ export interface ActionResponse {
100
+ affected: number;
101
+ message?: string;
102
+ data?: Record<string, unknown>;
103
+ }
104
+ export interface ContextRequest {
105
+ filters?: Record<string, string>;
106
+ selection?: Selection;
107
+ }
108
+ export interface ContextResponse {
109
+ summary: string;
110
+ selection?: string;
111
+ suggestions?: string[];
112
+ }
113
+ export interface PageContextRequest extends ContextRequest {
114
+ sections: string[];
115
+ }
116
+ export type FetchHandler = (req: FetchRequest) => Promise<FetchResponse>;
117
+ export type ActionHandler = (req: ActionRequest) => Promise<ActionResponse>;
118
+ export type ContextHandler = (req: ContextRequest) => Promise<ContextResponse>;
119
+ export type PageContextHandler = (req: PageContextRequest) => Promise<ContextResponse>;
120
+ export interface ResourceDefinition {
121
+ fetch: FetchHandler;
122
+ context?: ContextHandler;
123
+ }
124
+ export interface AppServerConfig {
125
+ authToken: string;
126
+ pages: Record<string, Omit<PageDefinition, "id">>;
127
+ resources: Record<string, ResourceDefinition>;
128
+ actions: Record<string, ActionHandler>;
129
+ }
130
+ export interface SortSpec {
131
+ key: string;
132
+ direction: "asc" | "desc";
133
+ }
134
+ export interface Selection {
135
+ type: "row" | "rows";
136
+ ids?: (string | number)[];
137
+ }
138
+ /**
139
+ * Card resources use the same FetchHandler as tables, but each row
140
+ * should match this shape. The renderer reads these fields to display
141
+ * metric cards.
142
+ *
143
+ * Example:
144
+ * { label: "Open Issues", value: 45, format: "integer" }
145
+ * { label: "Completion", value: 0.71, format: "percent" }
146
+ * { label: "Revenue", value: 12500, format: "currency" }
147
+ */
148
+ export interface CardData {
149
+ label: string;
150
+ value: number | string;
151
+ format?: ColumnFormat;
152
+ }
153
+ /**
154
+ * Detail resources use the same FetchHandler, but each row should be a
155
+ * typed block. The drawer renderer uses the `type` field to pick the
156
+ * right component for each block.
157
+ *
158
+ * Example response from a detail fetch handler:
159
+ * rows: [
160
+ * { type: "header", value: "Issue #1705: Unmatched Customer" },
161
+ * { type: "field", label: "Raw Name", value: "ACJDM SALES CORP." },
162
+ * { type: "field", label: "Confidence", value: 0.92, format: "percent" },
163
+ * { type: "list", label: "Candidate Matches", items: [
164
+ * { label: "ACJDM Sales Corporation", value: 0.92, format: "percent", id: "c_441" },
165
+ * { label: "ACJDM Trading", value: 0.78, format: "percent", id: "c_209" },
166
+ * ]},
167
+ * { type: "table", label: "Sample Transactions", columns: [...], rows: [...] },
168
+ * ]
169
+ */
170
+ export type DetailBlock = DetailHeader | DetailField | DetailList | DetailTable | DetailImage;
171
+ export interface DetailHeader {
172
+ type: "header";
173
+ value: string;
174
+ }
175
+ export interface DetailField {
176
+ type: "field";
177
+ label: string;
178
+ value: string | number | boolean | null;
179
+ format?: ColumnFormat;
180
+ }
181
+ export interface DetailList {
182
+ type: "list";
183
+ label: string;
184
+ items: {
185
+ label: string;
186
+ value?: string | number;
187
+ format?: ColumnFormat;
188
+ id?: string | number;
189
+ }[];
190
+ }
191
+ export interface DetailTable {
192
+ type: "table";
193
+ label: string;
194
+ columns: {
195
+ key: string;
196
+ label: string;
197
+ format?: ColumnFormat;
198
+ }[];
199
+ rows: Record<string, unknown>[];
200
+ }
201
+ export interface DetailImage {
202
+ type: "image";
203
+ label?: string;
204
+ url: string;
205
+ alt?: string;
206
+ width?: number;
207
+ height?: number;
208
+ }
209
+ export interface PaginatedResponse {
210
+ rows: Record<string, unknown>[];
211
+ total: number;
212
+ page: number;
213
+ pageSize: number;
214
+ totalPages: number;
215
+ }
216
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Page Definition — the JSON schema that describes a page's layout and behavior
3
+ // ---------------------------------------------------------------------------
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E"}
@@ -0,0 +1,5 @@
1
+ import type { AppServerConfig, FetchResponse, ActionResponse } from "./types.js";
2
+ export declare function validateConfig(config: AppServerConfig): void;
3
+ export declare function validateFetchResponse(resource: string, response: unknown): asserts response is FetchResponse;
4
+ export declare function validateActionResponse(action: string, response: unknown): asserts response is ActionResponse;
5
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EAEf,aAAa,EACb,cAAc,EACf,MAAM,YAAY,CAAC;AAMpB,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAuD5D;AAMD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,QAAQ,IAAI,aAAa,CAuBnC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,QAAQ,IAAI,cAAc,CAgBpC"}
@@ -0,0 +1,77 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Startup validation — fail fast with descriptive errors
3
+ // ---------------------------------------------------------------------------
4
+ export function validateConfig(config) {
5
+ const errors = [];
6
+ for (const [pageId, page] of Object.entries(config.pages)) {
7
+ const sections = flattenSections(page.sections);
8
+ for (const section of sections) {
9
+ // Every section source must have a resource handler
10
+ if (!config.resources[section.source]) {
11
+ errors.push(`Page "${pageId}": section references resource "${section.source}" but no fetch handler is defined`);
12
+ }
13
+ // Table sections must have rowKey
14
+ if (section.type === "table" && !section.rowKey) {
15
+ errors.push(`Page "${pageId}": table section with source "${section.source}" is missing required "rowKey"`);
16
+ }
17
+ // Table detail source must reference a resource
18
+ if (section.type === "table" && section.detail) {
19
+ if (!config.resources[section.detail.source]) {
20
+ errors.push(`Page "${pageId}": table detail references resource "${section.detail.source}" but no fetch handler is defined`);
21
+ }
22
+ }
23
+ }
24
+ // Every action must have a handler
25
+ for (const action of page.actions ?? []) {
26
+ if (!config.actions[action.action]) {
27
+ errors.push(`Page "${pageId}": references action "${action.action}" but no action handler is defined`);
28
+ }
29
+ }
30
+ // Filter options_source must reference a resource
31
+ for (const filter of page.filters ?? []) {
32
+ if (filter.options_source && !config.resources[filter.options_source]) {
33
+ errors.push(`Page "${pageId}": filter "${filter.key}" has options_source "${filter.options_source}" but no resource handler is defined`);
34
+ }
35
+ }
36
+ }
37
+ if (errors.length > 0) {
38
+ throw new Error(`@toolplex/app-server configuration errors:\n - ${errors.join("\n - ")}`);
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Runtime response validation
43
+ // ---------------------------------------------------------------------------
44
+ export function validateFetchResponse(resource, response) {
45
+ if (!response || typeof response !== "object") {
46
+ throw new ResponseValidationError(resource, "fetch handler must return an object");
47
+ }
48
+ const r = response;
49
+ if (!Array.isArray(r.rows)) {
50
+ throw new ResponseValidationError(resource, 'fetch handler must return "rows" as an array');
51
+ }
52
+ if (typeof r.total !== "number" || r.total < 0) {
53
+ throw new ResponseValidationError(resource, 'fetch handler must return "total" as a non-negative number');
54
+ }
55
+ }
56
+ export function validateActionResponse(action, response) {
57
+ if (!response || typeof response !== "object") {
58
+ throw new ResponseValidationError(action, "action handler must return an object");
59
+ }
60
+ const r = response;
61
+ if (typeof r.affected !== "number") {
62
+ throw new ResponseValidationError(action, 'action handler must return "affected" as a number');
63
+ }
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+ function flattenSections(sections) {
69
+ return sections.flatMap((s) => (Array.isArray(s) ? s : [s]));
70
+ }
71
+ class ResponseValidationError extends Error {
72
+ constructor(name, detail) {
73
+ super(`@toolplex/app-server [${name}]: ${detail}`);
74
+ this.name = "ResponseValidationError";
75
+ }
76
+ }
77
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@toolplex/app-server",
3
+ "version": "0.1.0",
4
+ "description": "Fastify plugin for serving ToolPlex App Pages — page definitions, paginated data, actions, and agent context",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist"],
9
+ "license": "SEE LICENSE IN LICENSE",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/toolplex/app-server"
13
+ },
14
+ "homepage": "https://github.com/toolplex/app-server#readme",
15
+ "keywords": ["toolplex", "fastify", "fastify-plugin", "app-server", "pages", "dashboard"],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "build:watch": "tsc --watch",
19
+ "clean": "rm -rf dist",
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "eslint 'src/**/*.ts'",
22
+ "lint:fix": "eslint --fix 'src/**/*.ts'",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "fastify-plugin": "^5.0.1"
27
+ },
28
+ "peerDependencies": {
29
+ "fastify": ">=5.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@eslint/js": "^9.24.0",
33
+ "@types/node": "^20.5.0",
34
+ "eslint": "^9.24.0",
35
+ "fastify": "^5.2.2",
36
+ "tsx": "^4.19.2",
37
+ "typescript": "^5.0.0",
38
+ "typescript-eslint": "^8.29.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }