@amtp/protocol 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/USAGE_GUIDE.md +722 -0
- package/bin/amtp.ts +387 -0
- package/dist/client/amtp-client.d.ts +164 -0
- package/dist/client/amtp-client.js +460 -0
- package/dist/client/amtp-client.js.map +1 -0
- package/dist/client/examples/basic-client.d.ts +6 -0
- package/dist/client/examples/basic-client.js +35 -0
- package/dist/client/examples/basic-client.js.map +1 -0
- package/dist/crawler/amtp-crawler.d.ts +125 -0
- package/dist/crawler/amtp-crawler.js +359 -0
- package/dist/crawler/amtp-crawler.js.map +1 -0
- package/dist/crawler/examples/basic-crawler.d.ts +6 -0
- package/dist/crawler/examples/basic-crawler.js +28 -0
- package/dist/crawler/examples/basic-crawler.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/server/adapters/fastify-adapter.d.ts +86 -0
- package/dist/server/adapters/fastify-adapter.js +169 -0
- package/dist/server/adapters/fastify-adapter.js.map +1 -0
- package/dist/server/amtp-ql-executor.d.ts +24 -0
- package/dist/server/amtp-ql-executor.js +198 -0
- package/dist/server/amtp-ql-executor.js.map +1 -0
- package/dist/server/amtp-ql-parser.d.ts +30 -0
- package/dist/server/amtp-ql-parser.js +212 -0
- package/dist/server/amtp-ql-parser.js.map +1 -0
- package/dist/server/amtp-server.d.ts +183 -0
- package/dist/server/amtp-server.js +650 -0
- package/dist/server/amtp-server.js.map +1 -0
- package/dist/server/examples/basic-server.d.ts +6 -0
- package/dist/server/examples/basic-server.js +215 -0
- package/dist/server/examples/basic-server.js.map +1 -0
- package/dist/server/examples/saas-dashboard-server.d.ts +44 -0
- package/dist/server/examples/saas-dashboard-server.js +387 -0
- package/dist/server/examples/saas-dashboard-server.js.map +1 -0
- package/dist/server/markdown-parser.d.ts +31 -0
- package/dist/server/markdown-parser.js +463 -0
- package/dist/server/markdown-parser.js.map +1 -0
- package/dist/server/notifications.d.ts +40 -0
- package/dist/server/notifications.js +134 -0
- package/dist/server/notifications.js.map +1 -0
- package/dist/server/permissions.d.ts +40 -0
- package/dist/server/permissions.js +156 -0
- package/dist/server/permissions.js.map +1 -0
- package/dist/server/security.d.ts +127 -0
- package/dist/server/security.js +368 -0
- package/dist/server/security.js.map +1 -0
- package/dist/types/amtp.types.d.ts +720 -0
- package/dist/types/amtp.types.js +224 -0
- package/dist/types/amtp.types.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Basic AMTP Server Example
|
|
4
|
+
*
|
|
5
|
+
* Run with: npm run server
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const amtp_server_js_1 = require("../amtp-server.js");
|
|
9
|
+
const amtp_types_js_1 = require("../../types/amtp.types.js");
|
|
10
|
+
const server = new amtp_server_js_1.AMTPServer({
|
|
11
|
+
port: parseInt(process.env.PORT || "3000", 10),
|
|
12
|
+
host: "0.0.0.0",
|
|
13
|
+
enableCORS: true,
|
|
14
|
+
enableCompression: true,
|
|
15
|
+
enableRateLimit: true,
|
|
16
|
+
});
|
|
17
|
+
// Register a simple product page
|
|
18
|
+
server.register("GET", "/products/:id", (req, res) => {
|
|
19
|
+
const { id } = req.params;
|
|
20
|
+
const doc = {
|
|
21
|
+
type: "document",
|
|
22
|
+
version: "1.0",
|
|
23
|
+
title: `Product ${id}`,
|
|
24
|
+
path: `/products/${id}`,
|
|
25
|
+
nodes: [
|
|
26
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: `This is the AMTP representation of product ${id}.` },
|
|
27
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: "Details: Price $99.99 In stock Yes" },
|
|
28
|
+
],
|
|
29
|
+
actions: [
|
|
30
|
+
{ id: "BUY", label: "Buy Now", method: amtp_types_js_1.HTTPMethod.POST, endpoint: `/products/${id}/buy` },
|
|
31
|
+
{ id: "ADD_TO_CART", label: "Add to Cart", method: amtp_types_js_1.HTTPMethod.POST, endpoint: `/products/${id}/cart` },
|
|
32
|
+
],
|
|
33
|
+
links: [
|
|
34
|
+
{ text: "Reviews", url: `/products/${id}/reviews`, type: amtp_types_js_1.LinkType.INTERNAL },
|
|
35
|
+
{ text: "Related", url: "/products/related", type: amtp_types_js_1.LinkType.INTERNAL },
|
|
36
|
+
],
|
|
37
|
+
metadata: { productId: id },
|
|
38
|
+
forms: [],
|
|
39
|
+
structured_data: [],
|
|
40
|
+
};
|
|
41
|
+
res.json(doc);
|
|
42
|
+
});
|
|
43
|
+
// Simple action handler
|
|
44
|
+
server.register("POST", "/products/:id/buy", (req, res) => {
|
|
45
|
+
const { id } = req.params;
|
|
46
|
+
const { quantity = 1 } = req.body || {};
|
|
47
|
+
const doc = {
|
|
48
|
+
type: "document",
|
|
49
|
+
version: "1.0",
|
|
50
|
+
title: "Purchase Complete",
|
|
51
|
+
path: `/products/${id}/buy`,
|
|
52
|
+
nodes: [
|
|
53
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: `Thank you! Purchased ${quantity} of product ${id}.` },
|
|
54
|
+
],
|
|
55
|
+
actions: [],
|
|
56
|
+
links: [{ text: "Back to product", url: `/products/${id}`, type: amtp_types_js_1.LinkType.INTERNAL }],
|
|
57
|
+
metadata: {},
|
|
58
|
+
forms: [],
|
|
59
|
+
structured_data: [],
|
|
60
|
+
};
|
|
61
|
+
// v2.0: Emit live notification (picked up by SSE and webhooks)
|
|
62
|
+
notifications_1.notificationBus.emitEvent({
|
|
63
|
+
id: `evt_${Date.now()}`,
|
|
64
|
+
type: "order.updated",
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
data: { productId: id, quantity, status: "purchased" },
|
|
67
|
+
});
|
|
68
|
+
res.status(201).json(doc);
|
|
69
|
+
});
|
|
70
|
+
// Batch action handler (v1.1 stub)
|
|
71
|
+
server.register("POST", "/api/amtp/batch", (req, res) => {
|
|
72
|
+
const body = req.body || {};
|
|
73
|
+
const results = (body.actions || []).map((item, idx) => ({
|
|
74
|
+
actionId: item.action,
|
|
75
|
+
requestId: item.requestId || `req_${idx}`,
|
|
76
|
+
status: "success",
|
|
77
|
+
result: { processed: true, batch: true, echo: item.parameters || {} },
|
|
78
|
+
}));
|
|
79
|
+
const response = {
|
|
80
|
+
batchId: body.batchId || `batch_${Date.now()}`,
|
|
81
|
+
status: "success",
|
|
82
|
+
results,
|
|
83
|
+
};
|
|
84
|
+
res.json(response);
|
|
85
|
+
});
|
|
86
|
+
// AMTP-QL endpoint (v2.0) — query the document with a tiny GraphQL-like language
|
|
87
|
+
server.register("POST", "/api/amtp/query", async (req, res) => {
|
|
88
|
+
const body = (req.body || {});
|
|
89
|
+
// Demo document rich enough to exercise all AMTP-QL projections
|
|
90
|
+
const demoDoc = {
|
|
91
|
+
type: "document",
|
|
92
|
+
version: "2.0",
|
|
93
|
+
title: "AMTP-QL Demo Product",
|
|
94
|
+
path: "/demo/ql",
|
|
95
|
+
nodes: [
|
|
96
|
+
{ type: amtp_types_js_1.MarkdownNodeType.HEADING, content: "Overview" },
|
|
97
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: "This is a demonstration document for AMTP-QL." },
|
|
98
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: "It contains actions, forms, structured data and pagination." },
|
|
99
|
+
],
|
|
100
|
+
actions: [
|
|
101
|
+
{ id: "BUY", label: "Buy", method: amtp_types_js_1.HTTPMethod.POST, endpoint: "/demo/buy", description: "Purchase item" },
|
|
102
|
+
{ id: "WISHLIST", label: "Add to Wishlist", method: amtp_types_js_1.HTTPMethod.POST, endpoint: "/demo/wishlist" },
|
|
103
|
+
],
|
|
104
|
+
forms: [
|
|
105
|
+
{ id: "review", action: "/demo/review", method: amtp_types_js_1.HTTPMethod.POST, endpoint: "/demo/review", fields: [
|
|
106
|
+
{ name: "rating", type: "number", required: true },
|
|
107
|
+
{ name: "comment", type: "string" },
|
|
108
|
+
] },
|
|
109
|
+
],
|
|
110
|
+
links: [
|
|
111
|
+
{ text: "Docs", url: "/docs", type: amtp_types_js_1.LinkType.EXTERNAL },
|
|
112
|
+
],
|
|
113
|
+
metadata: { pageType: "product", category: "demo" },
|
|
114
|
+
structured_data: [
|
|
115
|
+
{ "@type": "Product", data: { price: 199, currency: "USD", inStock: true } },
|
|
116
|
+
],
|
|
117
|
+
pagination: {
|
|
118
|
+
pageInfo: { hasNextPage: true, hasPreviousPage: false, startCursor: "c1", endCursor: "c2" },
|
|
119
|
+
nextCursor: "c2",
|
|
120
|
+
itemsPerPage: 10,
|
|
121
|
+
totalItems: 42,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const result = await server.handleAMTPQL(body, () => demoDoc);
|
|
125
|
+
res.json(result);
|
|
126
|
+
});
|
|
127
|
+
// =====================================================
|
|
128
|
+
// v2.0: Webhook + SSE Push Notifications
|
|
129
|
+
// =====================================================
|
|
130
|
+
const notifications_1 = require("../notifications");
|
|
131
|
+
const amtp_types_1 = require("../../types/amtp.types");
|
|
132
|
+
// Register a webhook (agent provides URL to receive POSTs)
|
|
133
|
+
server.register("POST", "/api/webhooks", (req, res) => {
|
|
134
|
+
const { url, events, secret, description } = req.body || {};
|
|
135
|
+
if (!url || !events?.length) {
|
|
136
|
+
return res.status(400).json({ error: "url and events are required" });
|
|
137
|
+
}
|
|
138
|
+
const sub = notifications_1.notificationBus.registerWebhook({
|
|
139
|
+
url,
|
|
140
|
+
events,
|
|
141
|
+
secret,
|
|
142
|
+
description,
|
|
143
|
+
});
|
|
144
|
+
res.status(201).json({
|
|
145
|
+
subscription: sub,
|
|
146
|
+
testCommand: `curl -X POST ${url} -H "Content-Type: application/json" -d '{"type":"test"}'`,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// List webhooks
|
|
150
|
+
server.register("GET", "/api/webhooks", (_req, res) => {
|
|
151
|
+
res.json({ webhooks: notifications_1.notificationBus.listWebhooks() });
|
|
152
|
+
});
|
|
153
|
+
// Delete webhook
|
|
154
|
+
server.register("DELETE", "/api/webhooks/:id", (req, res) => {
|
|
155
|
+
const ok = notifications_1.notificationBus.deleteWebhook(req.params.id);
|
|
156
|
+
res.json({ deleted: ok });
|
|
157
|
+
});
|
|
158
|
+
// SSE Stream - agents connect here for live updates
|
|
159
|
+
// Example: GET /api/amtp/stream?events=order.updated,workspace.updated
|
|
160
|
+
server.register("GET", "/api/amtp/stream", (req, res) => {
|
|
161
|
+
const requestedEvents = (req.query.events || "").split(",").filter(Boolean);
|
|
162
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
163
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
164
|
+
res.setHeader("Connection", "keep-alive");
|
|
165
|
+
res.setHeader("X-Accel-Buffering", "no"); // disable nginx buffering
|
|
166
|
+
const sendEvent = (event) => {
|
|
167
|
+
res.write(`event: ${event.type}\n`);
|
|
168
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
169
|
+
};
|
|
170
|
+
const unsubscribe = notifications_1.notificationBus.subscribeSSE(requestedEvents.length ? requestedEvents : Object.values(amtp_types_1.NotificationEventType), sendEvent);
|
|
171
|
+
// Send initial heartbeat
|
|
172
|
+
res.write(`: connected\n\n`);
|
|
173
|
+
req.on("close", () => {
|
|
174
|
+
unsubscribe();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
// Demo: emit a notification when a buy happens (in the buy handler above, we can call this)
|
|
178
|
+
// For demo, expose an endpoint to manually emit
|
|
179
|
+
server.register("POST", "/api/demo/emit", (req, res) => {
|
|
180
|
+
const { type, data } = req.body || {};
|
|
181
|
+
const event = {
|
|
182
|
+
id: `evt_${Date.now()}`,
|
|
183
|
+
type: type || "order.updated",
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
data: data || { demo: true },
|
|
186
|
+
};
|
|
187
|
+
notifications_1.notificationBus.emitEvent(event);
|
|
188
|
+
res.json({ emitted: event });
|
|
189
|
+
});
|
|
190
|
+
// Health
|
|
191
|
+
server.register("GET", "/health", (_req, res) => {
|
|
192
|
+
res.json({ status: "ok", protocol: "AMTP", version: "1.0" });
|
|
193
|
+
});
|
|
194
|
+
// Home
|
|
195
|
+
server.register("GET", "/", (_req, res) => {
|
|
196
|
+
const doc = {
|
|
197
|
+
type: "document",
|
|
198
|
+
version: "1.0",
|
|
199
|
+
title: "AMTP Basic Server",
|
|
200
|
+
path: "/",
|
|
201
|
+
nodes: [
|
|
202
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: "Welcome to the basic AMTP reference server." },
|
|
203
|
+
{ type: amtp_types_js_1.MarkdownNodeType.PARAGRAPH, content: "Try: GET /products/demo with Accept: text/amtp+markdown" },
|
|
204
|
+
],
|
|
205
|
+
actions: [],
|
|
206
|
+
links: [{ text: "Product Demo", url: "/products/demo", type: amtp_types_js_1.LinkType.INTERNAL }],
|
|
207
|
+
metadata: {},
|
|
208
|
+
forms: [],
|
|
209
|
+
structured_data: [],
|
|
210
|
+
};
|
|
211
|
+
res.json(doc);
|
|
212
|
+
});
|
|
213
|
+
server.start().catch(console.error);
|
|
214
|
+
console.log("Basic AMTP server example starting...");
|
|
215
|
+
//# sourceMappingURL=basic-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"basic-server.js","sourceRoot":"","sources":["../../../src/server/examples/basic-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AMTP — SaaS Multi-Tenant Workspace Dashboard (Example Server)
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates AMTP used as the primary API surface for a real
|
|
5
|
+
* SaaS application: shared-workspace project-management tool with
|
|
6
|
+
* SQLite persistence and JWT authentication.
|
|
7
|
+
*
|
|
8
|
+
* Schema (sqlite3)
|
|
9
|
+
* ┌────────────────────────────────────────────────┐
|
|
10
|
+
* │ tenants │ users │
|
|
11
|
+
* │─────────────── │───────────────────────────────│
|
|
12
|
+
* │ id TEXT PK │ id TEXT PK │
|
|
13
|
+
* │ name TEXT │ tenantId TEXT FK → tenants │
|
|
14
|
+
* │ plan TEXT │ email TEXT UNIQUE │
|
|
15
|
+
* │ createdAt TEXT │ passwordHash TEXT │
|
|
16
|
+
* │ │ name TEXT │
|
|
17
|
+
* │ │ role TEXT (owner|admin|member) │
|
|
18
|
+
* │ │ createdAt TEXT │
|
|
19
|
+
* ├───────────────┴───────────────────────────────┤
|
|
20
|
+
* │ workspaces │
|
|
21
|
+
* │────────────────────────────────────────────────────│
|
|
22
|
+
* │ id TEXT PK │
|
|
23
|
+
* │ tenantId TEXT FK → tenants │
|
|
24
|
+
* │ name TEXT │
|
|
25
|
+
* │ description TEXT │
|
|
26
|
+
* │ createdAt TEXT │
|
|
27
|
+
* │ updatedAt TEXT │
|
|
28
|
+
* │────────────────── ─ ──────────────────────────────│
|
|
29
|
+
* │ workspace_members │
|
|
30
|
+
* │────────────────────────────────────────────────────│
|
|
31
|
+
* │ workspaceId TEXT FK → workspaces │
|
|
32
|
+
* │ userId TEXT FK → users │
|
|
33
|
+
* │ role TEXT (owner|editor|viewer) │
|
|
34
|
+
* │ joinedAt TEXT │
|
|
35
|
+
* │ PRIMARY KEY (workspaceId, userId) │
|
|
36
|
+
* └────────────────────────────────────────────────────┘
|
|
37
|
+
*
|
|
38
|
+
* Run:
|
|
39
|
+
* npm run server:saas
|
|
40
|
+
* # or
|
|
41
|
+
* ts-node src/server/examples/saas-dashboard-server.ts
|
|
42
|
+
*/
|
|
43
|
+
declare const app: import("express-serve-static-core").Express;
|
|
44
|
+
export default app;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AMTP — SaaS Multi-Tenant Workspace Dashboard (Example Server)
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates AMTP used as the primary API surface for a real
|
|
6
|
+
* SaaS application: shared-workspace project-management tool with
|
|
7
|
+
* SQLite persistence and JWT authentication.
|
|
8
|
+
*
|
|
9
|
+
* Schema (sqlite3)
|
|
10
|
+
* ┌────────────────────────────────────────────────┐
|
|
11
|
+
* │ tenants │ users │
|
|
12
|
+
* │─────────────── │───────────────────────────────│
|
|
13
|
+
* │ id TEXT PK │ id TEXT PK │
|
|
14
|
+
* │ name TEXT │ tenantId TEXT FK → tenants │
|
|
15
|
+
* │ plan TEXT │ email TEXT UNIQUE │
|
|
16
|
+
* │ createdAt TEXT │ passwordHash TEXT │
|
|
17
|
+
* │ │ name TEXT │
|
|
18
|
+
* │ │ role TEXT (owner|admin|member) │
|
|
19
|
+
* │ │ createdAt TEXT │
|
|
20
|
+
* ├───────────────┴───────────────────────────────┤
|
|
21
|
+
* │ workspaces │
|
|
22
|
+
* │────────────────────────────────────────────────────│
|
|
23
|
+
* │ id TEXT PK │
|
|
24
|
+
* │ tenantId TEXT FK → tenants │
|
|
25
|
+
* │ name TEXT │
|
|
26
|
+
* │ description TEXT │
|
|
27
|
+
* │ createdAt TEXT │
|
|
28
|
+
* │ updatedAt TEXT │
|
|
29
|
+
* │────────────────── ─ ──────────────────────────────│
|
|
30
|
+
* │ workspace_members │
|
|
31
|
+
* │────────────────────────────────────────────────────│
|
|
32
|
+
* │ workspaceId TEXT FK → workspaces │
|
|
33
|
+
* │ userId TEXT FK → users │
|
|
34
|
+
* │ role TEXT (owner|editor|viewer) │
|
|
35
|
+
* │ joinedAt TEXT │
|
|
36
|
+
* │ PRIMARY KEY (workspaceId, userId) │
|
|
37
|
+
* └────────────────────────────────────────────────────┘
|
|
38
|
+
*
|
|
39
|
+
* Run:
|
|
40
|
+
* npm run server:saas
|
|
41
|
+
* # or
|
|
42
|
+
* ts-node src/server/examples/saas-dashboard-server.ts
|
|
43
|
+
*/
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
const express_1 = __importDefault(require("express"));
|
|
49
|
+
const sqlite3_1 = require("sqlite3");
|
|
50
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
51
|
+
// @ts-ignore - bcryptjs has no types, used only in this demo server
|
|
52
|
+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
53
|
+
// @ts-ignore - jsonwebtoken has no bundled types in this setup
|
|
54
|
+
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
55
|
+
const amtp_server_js_1 = require("../amtp-server.js");
|
|
56
|
+
const amtp_types_js_1 = require("../../types/amtp.types.js");
|
|
57
|
+
/* ================================================================
|
|
58
|
+
CONFIGURATION
|
|
59
|
+
================================================================ */
|
|
60
|
+
const PORT = parseInt(process.env.AMTP_PORT || "3000", 10);
|
|
61
|
+
const JWT_SECRET = process.env.JWT_SECRET || "amtp-saas-jwt-dev-change-me";
|
|
62
|
+
const DB_PATH = process.env.DB_PATH || "./.workspace.db";
|
|
63
|
+
/* ================================================================
|
|
64
|
+
DATABASE — SCHEMA + SEED
|
|
65
|
+
================================================================ */
|
|
66
|
+
const sqlite = new sqlite3_1.Database(DB_PATH);
|
|
67
|
+
sqlite.serialize(() => {
|
|
68
|
+
// Tenants
|
|
69
|
+
sqlite.run(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS tenants (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
name TEXT NOT NULL,
|
|
73
|
+
plan TEXT NOT NULL DEFAULT 'free',
|
|
74
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
75
|
+
)
|
|
76
|
+
`);
|
|
77
|
+
// Users (per tenant)
|
|
78
|
+
sqlite.run(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
tenantId TEXT NOT NULL REFERENCES tenants(id),
|
|
82
|
+
email TEXT NOT NULL UNIQUE,
|
|
83
|
+
passwordHash TEXT NOT NULL,
|
|
84
|
+
name TEXT NOT NULL,
|
|
85
|
+
role TEXT NOT NULL CHECK(role IN ('owner','admin','member')),
|
|
86
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
// Workspaces
|
|
90
|
+
sqlite.run(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
tenantId TEXT NOT NULL REFERENCES tenants(id),
|
|
94
|
+
name TEXT NOT NULL,
|
|
95
|
+
description TEXT DEFAULT '',
|
|
96
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
97
|
+
updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
98
|
+
)
|
|
99
|
+
`);
|
|
100
|
+
// Workspace members
|
|
101
|
+
sqlite.run(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS workspace_members (
|
|
103
|
+
workspaceId TEXT NOT NULL REFERENCES workspaces(id),
|
|
104
|
+
userId TEXT NOT NULL REFERENCES users(id),
|
|
105
|
+
role TEXT NOT NULL CHECK(role IN ('owner','editor','viewer')),
|
|
106
|
+
joinedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
107
|
+
PRIMARY KEY (workspaceId, userId)
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
// Seed a demo tenant + user if empty
|
|
111
|
+
sqlite.get("SELECT COUNT(*) AS c FROM tenants", (err, row) => {
|
|
112
|
+
if (err || row.c > 0)
|
|
113
|
+
return;
|
|
114
|
+
const tenantId = "t_demo";
|
|
115
|
+
sqlite.run(`INSERT INTO tenants (id, name, plan) VALUES (?, ?, ?)`, [
|
|
116
|
+
tenantId, "Demo Tenant", "pro",
|
|
117
|
+
], () => {
|
|
118
|
+
const userId = "u_demo";
|
|
119
|
+
const pwd = "demo-password";
|
|
120
|
+
bcryptjs_1.default.hash(pwd, 10, (err2, hash) => {
|
|
121
|
+
if (err2)
|
|
122
|
+
return console.error("BCRYPT ERROR", err2);
|
|
123
|
+
sqlite.run(`INSERT INTO users (id, tenantId, email, passwordHash, name, role) VALUES (?,?,?,?,?,?)`, [userId, tenantId, "demo@example.com", hash, "Demo User", "owner"], () => {
|
|
124
|
+
const wsId = "w_main";
|
|
125
|
+
sqlite.run(`INSERT INTO workspaces (id, tenantId, name, description) VALUES (?,?,?,?)`, [wsId, tenantId, "Main Workspace", "Primary demo workspace"], () => {
|
|
126
|
+
sqlite.run(`INSERT INTO workspace_members (workspaceId, userId, role) VALUES (?,?,?)`, [wsId, userId, "owner"]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
/* ================================================================
|
|
134
|
+
AUTH HELPERS
|
|
135
|
+
================================================================ */
|
|
136
|
+
function genId(prefix) {
|
|
137
|
+
return `${prefix}_${Date.now().toString(36)}_${crypto_1.default.randomBytes(4).toString("hex")}`;
|
|
138
|
+
}
|
|
139
|
+
function signJwt(userId, tenantId, role) {
|
|
140
|
+
return (0, jsonwebtoken_1.sign)({ userId, tenantId, role }, JWT_SECRET, { expiresIn: "24h" });
|
|
141
|
+
}
|
|
142
|
+
function requireAuth(req) {
|
|
143
|
+
const header = (req.header("authorization") || "").replace(/^Bearer\s+/, "");
|
|
144
|
+
if (!header)
|
|
145
|
+
return null;
|
|
146
|
+
try {
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
148
|
+
const decoded = (0, jsonwebtoken_1.verify)(header, JWT_SECRET);
|
|
149
|
+
return decoded;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function tenantGuard(db, user, callback) {
|
|
156
|
+
db.get("SELECT role, tenantId FROM users WHERE id = ?", [user.userId], (err, row) => {
|
|
157
|
+
if (err || !row)
|
|
158
|
+
return callback("UNAUTHORIZED");
|
|
159
|
+
callback(row.tenantId);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/* ================================================================
|
|
163
|
+
REST API — AUTH & TENANT CRUD
|
|
164
|
+
================================================================ */
|
|
165
|
+
const app = (0, express_1.default)();
|
|
166
|
+
app.use(express_1.default.json());
|
|
167
|
+
/* ── Health ── */
|
|
168
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", service: "amtp-saas-dashboard" }));
|
|
169
|
+
/* ── Register ── */
|
|
170
|
+
app.post("/api/v1/auth/register", (req, res) => {
|
|
171
|
+
const { tenantName, email, password, name } = req.body || {};
|
|
172
|
+
if (!tenantName || !email || !password || !name) {
|
|
173
|
+
return res.status(400).json({ error: "tenantName, email, password, name are required" });
|
|
174
|
+
}
|
|
175
|
+
const tenantId = genId("t");
|
|
176
|
+
const userId = genId("u");
|
|
177
|
+
bcryptjs_1.default.hash(password, 10, (err, hash) => {
|
|
178
|
+
if (err)
|
|
179
|
+
return res.status(500).json({ error: "hasher error" });
|
|
180
|
+
sqlite.serialize(() => {
|
|
181
|
+
sqlite.run("INSERT INTO tenants (id, name) VALUES (?,?)", [tenantId, tenantName], () => {
|
|
182
|
+
sqlite.run("INSERT INTO users (id, tenantId, email, passwordHash, name, role) VALUES (?,?,?,?,?,?)", [userId, tenantId, email, hash, name, "owner"], () => {
|
|
183
|
+
const wsId = genId("w");
|
|
184
|
+
sqlite.run("INSERT INTO workspaces (id, tenantId, name) VALUES (?,?,?)", [wsId, tenantId, name], () => {
|
|
185
|
+
sqlite.run("INSERT INTO workspace_members (workspaceId, userId, role) VALUES (?,?,?)", [wsId, userId, "owner"], () => {
|
|
186
|
+
const token = signJwt(userId, tenantId, "owner");
|
|
187
|
+
res.status(201).json({ token, tenantId, userId, name, role: "owner" });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
/* ── Login ── */
|
|
196
|
+
app.post("/api/v1/auth/login", (req, res) => {
|
|
197
|
+
const { email, password } = req.body || {};
|
|
198
|
+
if (!email || !password)
|
|
199
|
+
return res.status(400).json({ error: "email + password required" });
|
|
200
|
+
sqlite.get("SELECT * FROM users WHERE email = ?", [email], (err2, user) => {
|
|
201
|
+
if (err2 || !user)
|
|
202
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
203
|
+
bcryptjs_1.default.compare(password, user.passwordHash, (err3, ok) => {
|
|
204
|
+
if (err3 || !ok)
|
|
205
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
206
|
+
res.json({ token: signJwt(user.id, user.tenantId, user.role), userId: user.id, tenantId: user.tenantId, role: user.role });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
/* ── Me ── */
|
|
211
|
+
app.get("/api/v1/me", (req, res) => {
|
|
212
|
+
const user = requireAuth(req);
|
|
213
|
+
if (!user)
|
|
214
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
215
|
+
sqlite.get("SELECT id, email, name, role, createdAt FROM users WHERE id = ?", [user.userId], (err, row) => {
|
|
216
|
+
if (err || !row)
|
|
217
|
+
return res.status(404).json({ error: "Not found" });
|
|
218
|
+
res.json(row);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
/* ── Workspaces ── */
|
|
222
|
+
app.get("/api/v1/workspaces", (req, res) => {
|
|
223
|
+
const user = requireAuth(req);
|
|
224
|
+
if (!user)
|
|
225
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
226
|
+
const sql = `
|
|
227
|
+
SELECT w.*, wm.role AS myRole
|
|
228
|
+
FROM workspaces w
|
|
229
|
+
JOIN workspace_members wm ON w.id = wm.workspaceId
|
|
230
|
+
WHERE wm.userId = ?
|
|
231
|
+
ORDER BY w.updatedAt DESC
|
|
232
|
+
`;
|
|
233
|
+
sqlite.all(sql, [user.userId], (err, rows) => {
|
|
234
|
+
if (err)
|
|
235
|
+
return res.status(500).json({ error: err.message });
|
|
236
|
+
res.json(rows);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
app.post("/api/v1/workspaces", (req, res) => {
|
|
240
|
+
const user = requireAuth(req);
|
|
241
|
+
if (!user)
|
|
242
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
243
|
+
const { name, description } = req.body || {};
|
|
244
|
+
if (!name)
|
|
245
|
+
return res.status(400).json({ error: "name is required" });
|
|
246
|
+
const wsId = genId("w");
|
|
247
|
+
sqlite.run("INSERT INTO workspaces (id, tenantId, name, description) VALUES (?,?,?,?)", [wsId, user.tenantId, name, description || ""], (err) => {
|
|
248
|
+
if (err)
|
|
249
|
+
return res.status(500).json({ error: err.message });
|
|
250
|
+
sqlite.run("INSERT INTO workspace_members (workspaceId, userId, role) VALUES (?,?,?)", [wsId, user.userId, "owner"], () => {
|
|
251
|
+
res.status(201).json({ id: wsId, name, description: description || "", myRole: "owner" });
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
app.get("/api/v1/workspaces/:id", (req, res) => {
|
|
256
|
+
const user = requireAuth(req);
|
|
257
|
+
if (!user)
|
|
258
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
259
|
+
const { id } = req.params;
|
|
260
|
+
sqlite.get(`SELECT w.*, wm.role AS myRole
|
|
261
|
+
FROM workspaces w
|
|
262
|
+
JOIN workspace_members wm ON w.id = wm.workspaceId
|
|
263
|
+
WHERE w.id = ? AND wm.userId = ?`, [id, user.userId], (err, row) => {
|
|
264
|
+
if (err)
|
|
265
|
+
return res.status(500).json({ error: err.message });
|
|
266
|
+
if (!row)
|
|
267
|
+
return res.status(404).json({ error: "Workspace not found" });
|
|
268
|
+
res.json(row);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
app.delete("/api/v1/workspaces/:id", (req, res) => {
|
|
272
|
+
const user = requireAuth(req);
|
|
273
|
+
if (!user)
|
|
274
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
275
|
+
const { id } = req.params;
|
|
276
|
+
sqlite.run("DELETE FROM workspaces WHERE id = ?", [id], (err) => {
|
|
277
|
+
if (err)
|
|
278
|
+
return res.status(500).json({ error: err.message });
|
|
279
|
+
res.status(204).send("");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
/* ── Members ── */
|
|
283
|
+
app.get("/api/v1/workspaces/:wId/members", (req, res) => {
|
|
284
|
+
const user = requireAuth(req);
|
|
285
|
+
if (!user)
|
|
286
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
287
|
+
const { wId } = req.params;
|
|
288
|
+
sqlite.get("SELECT 1 AS ok FROM workspace_members WHERE workspaceId=? AND userId=?", [wId, user.userId], (err, row) => {
|
|
289
|
+
if (err || !row)
|
|
290
|
+
return res.status(403).json({ error: "Access denied" });
|
|
291
|
+
sqlite.all(`SELECT u.id, u.email, u.name, u.role, wm.role AS memberRole, wm.joinedAt
|
|
292
|
+
FROM workspace_members wm
|
|
293
|
+
JOIN users u ON wm.userId = u.id
|
|
294
|
+
WHERE wm.workspaceId = ?`, [wId], (err2, rows) => { if (err2)
|
|
295
|
+
return res.status(500).json({ error: err2.message }); res.json(rows); });
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
/* ── AMTP HTML muxer ── */
|
|
299
|
+
app.get("/amtp/*", (req, res) => {
|
|
300
|
+
const authHeader = (req.header("authorization") || "");
|
|
301
|
+
const accept = (req.header("accept") || "");
|
|
302
|
+
const isAmtp = accept.includes("amtp") || accept.includes("markdown");
|
|
303
|
+
if (isAmtp && authHeader) {
|
|
304
|
+
const me = requireAuth(req);
|
|
305
|
+
if (!me)
|
|
306
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
307
|
+
sqlite.all(`SELECT w.*, wm.role AS myRole
|
|
308
|
+
FROM workspaces w
|
|
309
|
+
JOIN workspace_members wm ON w.id = wm.workspaceId
|
|
310
|
+
WHERE w.tenantId = ?
|
|
311
|
+
ORDER BY w.updatedAt DESC LIMIT 20`, [me.tenantId], (_err, rows) => {
|
|
312
|
+
if (_err)
|
|
313
|
+
return res.status(500).json({ error: _err.message });
|
|
314
|
+
const lines = [
|
|
315
|
+
`# Workspaces`,
|
|
316
|
+
"",
|
|
317
|
+
`Welcome back — ${rows.length} workspace(s)`,
|
|
318
|
+
"",
|
|
319
|
+
...["", "## Actions", "[CREATE_WORKSPACE] — Create a new workspace", "", "## Workspaces"]
|
|
320
|
+
.concat(rows.map((w) => `[${w.name}](/api/v1/workspaces/${w.id})`))
|
|
321
|
+
.concat([""]),
|
|
322
|
+
].join("\n");
|
|
323
|
+
const builder = new amtp_server_js_1.AMTPResponseBuilder();
|
|
324
|
+
const { headers, body } = builder.build(lines, {
|
|
325
|
+
sessionId: `sess_${me.userId}`,
|
|
326
|
+
});
|
|
327
|
+
res.setHeader("content-type", amtp_types_js_1.MIMEType.AMTP_MARKDOWN);
|
|
328
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
329
|
+
if (v !== undefined)
|
|
330
|
+
res.setHeader(k, v);
|
|
331
|
+
}
|
|
332
|
+
res.send(body);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const simpleHTML = `<!DOCTYPE html><html><body><h1>AMTP SaaS Dashboard</h1><p>Use the <tt>Accept: text/amtp+markdown</tt> header to get AMTP documents, or open the <a href="/amtp/workspaces">AMTP overview</a>.</p></body></html>`;
|
|
337
|
+
res.setHeader("content-type", "text/html");
|
|
338
|
+
res.send(simpleHTML);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
/* ── Root landing with content negotiation ── */
|
|
342
|
+
app.get("/", (req, res) => {
|
|
343
|
+
const accept = (req.header("accept") || "");
|
|
344
|
+
const isAmtp = accept.includes("amtp") || accept.includes("markdown");
|
|
345
|
+
if (isAmtp) {
|
|
346
|
+
const md = `# AMTP SaaS Dashboard\n\nMulti-tenant workspace demo powered by AMTP.\n\nUse Authorization: Bearer <token> for authenticated AMTP routes under /amtp/*\n\n## Demo\n\n- Login: POST /api/v1/auth/login\n- AMTP Workspaces: GET /amtp/workspaces\n\n## Actions\n\n[LOGIN] [VIEW_WORKSPACES]`;
|
|
347
|
+
const builder = new amtp_server_js_1.AMTPResponseBuilder();
|
|
348
|
+
const { headers, body } = builder.build(md);
|
|
349
|
+
res.setHeader("content-type", amtp_types_js_1.MIMEType.AMTP_MARKDOWN);
|
|
350
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
351
|
+
if (v !== undefined)
|
|
352
|
+
res.setHeader(k, v);
|
|
353
|
+
}
|
|
354
|
+
res.send(body);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
res.setHeader("content-type", "text/html");
|
|
358
|
+
res.send(`<!DOCTYPE html><html><body><h1>AMTP SaaS Dashboard</h1><p>AMTP-enabled demo. <a href="/amtp/workspaces">Try AMTP view</a> (use Accept header for full experience).</p></body></html>`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
/* ================================================================
|
|
362
|
+
START
|
|
363
|
+
================================================================ */
|
|
364
|
+
app.listen(PORT, () => {
|
|
365
|
+
sqlite.serialize(() => {
|
|
366
|
+
sqlite.each("SELECT * FROM tenants", (_err, _row) => { });
|
|
367
|
+
});
|
|
368
|
+
const tenantRows = [
|
|
369
|
+
"╔════════════════════════════════════════════════════════════════╗",
|
|
370
|
+
"║ AMTP SaaS Dashboard — Dev Server (SQLite) ║",
|
|
371
|
+
"╠════════════════════════════════════════════════════════════════╣",
|
|
372
|
+
];
|
|
373
|
+
console.log(tenantRows.join("\n"));
|
|
374
|
+
console.log(`║ 🚀 Server : http://localhost:${PORT}/ ║`);
|
|
375
|
+
console.log(`║ 📊 Workspaces : GET http://localhost:${PORT}/amtp/workspaces ║`);
|
|
376
|
+
console.log(`║ 🔑 Login : POST http://localhost:${PORT}/api/v1/auth/login ║`);
|
|
377
|
+
console.log(`║ ║`);
|
|
378
|
+
console.log(`║ Demo user : demo@example.com / demo-password ║`);
|
|
379
|
+
console.log(`╚════════════════════════════════════════════════════════════════╝\n`);
|
|
380
|
+
});
|
|
381
|
+
process.on("SIGINT", () => {
|
|
382
|
+
console.log("\n[amtp-saas] Shutting down...");
|
|
383
|
+
sqlite.close();
|
|
384
|
+
process.exit(0);
|
|
385
|
+
});
|
|
386
|
+
exports.default = app;
|
|
387
|
+
//# sourceMappingURL=saas-dashboard-server.js.map
|