@mentra/sdk 2.1.26 → 2.1.28
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/dist/app/session/api-client.d.ts.map +1 -1
- package/dist/app/session/dashboard.d.ts +5 -8
- package/dist/app/session/dashboard.d.ts.map +1 -1
- package/dist/app/session/events.d.ts +5 -2
- package/dist/app/session/events.d.ts.map +1 -1
- package/dist/app/session/index.d.ts +62 -3
- package/dist/app/session/index.d.ts.map +1 -1
- package/dist/app/session/modules/audio.d.ts +33 -4
- package/dist/app/session/modules/audio.d.ts.map +1 -1
- package/dist/app/session/modules/camera-managed-extension.d.ts +2 -3
- package/dist/app/session/modules/camera-managed-extension.d.ts.map +1 -1
- package/dist/app/session/modules/camera.d.ts +5 -5
- package/dist/app/session/modules/camera.d.ts.map +1 -1
- package/dist/app/session/modules/led.d.ts +141 -0
- package/dist/app/session/modules/led.d.ts.map +1 -0
- package/dist/app/session/modules/location.d.ts +1 -2
- package/dist/app/session/modules/location.d.ts.map +1 -1
- package/dist/app/session/modules/simple-storage.d.ts.map +1 -1
- package/dist/constants/log-messages/color.d.ts +5 -0
- package/dist/constants/log-messages/color.d.ts.map +1 -0
- package/dist/constants/log-messages/logos.d.ts +4 -0
- package/dist/constants/log-messages/logos.d.ts.map +1 -0
- package/dist/constants/{messages.d.ts → log-messages/updates.d.ts} +2 -3
- package/dist/constants/log-messages/updates.d.ts.map +1 -0
- package/dist/constants/log-messages/warning.d.ts +8 -0
- package/dist/constants/log-messages/warning.d.ts.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5347 -112
- package/dist/index.js.map +45 -0
- package/dist/logging/logger.d.ts +2 -1
- package/dist/logging/logger.d.ts.map +1 -1
- package/dist/types/capabilities.d.ts +3 -0
- package/dist/types/capabilities.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -14
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/message-types.d.ts +12 -3
- package/dist/types/message-types.d.ts.map +1 -1
- package/dist/types/messages/app-to-cloud.d.ts +48 -2
- package/dist/types/messages/app-to-cloud.d.ts.map +1 -1
- package/dist/types/messages/cloud-to-app.d.ts +25 -6
- package/dist/types/messages/cloud-to-app.d.ts.map +1 -1
- package/dist/types/messages/cloud-to-glasses.d.ts +43 -1
- package/dist/types/messages/cloud-to-glasses.d.ts.map +1 -1
- package/dist/types/messages/glasses-to-cloud.d.ts +32 -1
- package/dist/types/messages/glasses-to-cloud.d.ts.map +1 -1
- package/dist/types/rtmp-stream.d.ts +1 -1
- package/dist/types/rtmp-stream.d.ts.map +1 -1
- package/dist/types/streams.d.ts +28 -1
- package/dist/types/streams.d.ts.map +1 -1
- package/dist/utils/permissions-utils.d.ts +8 -0
- package/dist/utils/permissions-utils.d.ts.map +1 -0
- package/package.json +12 -3
- package/dist/app/index.js +0 -24
- package/dist/app/server/index.js +0 -658
- package/dist/app/session/api-client.js +0 -101
- package/dist/app/session/dashboard.js +0 -149
- package/dist/app/session/events.js +0 -305
- package/dist/app/session/index.js +0 -1571
- package/dist/app/session/layouts.js +0 -372
- package/dist/app/session/modules/audio.js +0 -321
- package/dist/app/session/modules/camera-managed-extension.js +0 -310
- package/dist/app/session/modules/camera.js +0 -603
- package/dist/app/session/modules/index.js +0 -19
- package/dist/app/session/modules/location.js +0 -58
- package/dist/app/session/modules/simple-storage.js +0 -232
- package/dist/app/session/settings.js +0 -358
- package/dist/app/token/index.js +0 -22
- package/dist/app/token/utils.js +0 -144
- package/dist/app/webview/index.js +0 -382
- package/dist/constants/index.js +0 -16
- package/dist/constants/messages.d.ts.map +0 -1
- package/dist/constants/messages.js +0 -57
- package/dist/examples/managed-rtmp-streaming-example.js +0 -158
- package/dist/examples/managed-rtmp-streaming-with-restream-example.js +0 -124
- package/dist/examples/rtmp-streaming-example.js +0 -102
- package/dist/logging/logger.js +0 -79
- package/dist/types/capabilities.js +0 -9
- package/dist/types/dashboard/index.js +0 -12
- package/dist/types/enums.js +0 -75
- package/dist/types/index.js +0 -101
- package/dist/types/layouts.js +0 -3
- package/dist/types/message-types.js +0 -207
- package/dist/types/messages/app-to-cloud.js +0 -95
- package/dist/types/messages/base.js +0 -3
- package/dist/types/messages/cloud-to-app.js +0 -78
- package/dist/types/messages/cloud-to-glasses.js +0 -68
- package/dist/types/messages/glasses-to-cloud.js +0 -139
- package/dist/types/models.js +0 -101
- package/dist/types/photo-data.js +0 -2
- package/dist/types/rtmp-stream.js +0 -3
- package/dist/types/streams.js +0 -306
- package/dist/types/token.js +0 -7
- package/dist/types/webhooks.js +0 -28
- package/dist/utils/animation-utils.js +0 -340
- package/dist/utils/bitmap-utils.js +0 -475
- package/dist/utils/resource-tracker.js +0 -153
package/dist/app/server/index.js
DELETED
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.TpaServer = exports.AppServer = exports.GIVE_APP_CONTROL_OF_TOOL_RESPONSE = void 0;
|
|
7
|
-
/**
|
|
8
|
-
* 🚀 App Server Module
|
|
9
|
-
*
|
|
10
|
-
* Creates and manages a server for Apps in the MentraOS ecosystem.
|
|
11
|
-
* Handles webhook endpoints, session management, and cleanup.
|
|
12
|
-
*/
|
|
13
|
-
const express_1 = __importDefault(require("express"));
|
|
14
|
-
const path_1 = __importDefault(require("path"));
|
|
15
|
-
const fs_1 = __importDefault(require("fs"));
|
|
16
|
-
const index_1 = require("../session/index");
|
|
17
|
-
const webview_1 = require("../webview");
|
|
18
|
-
const messages_1 = require("../../constants/messages");
|
|
19
|
-
const types_1 = require("../../types");
|
|
20
|
-
const logger_1 = require("../../logging/logger");
|
|
21
|
-
const axios_1 = __importDefault(require("axios"));
|
|
22
|
-
exports.GIVE_APP_CONTROL_OF_TOOL_RESPONSE = "GIVE_APP_CONTROL_OF_TOOL_RESPONSE";
|
|
23
|
-
/**
|
|
24
|
-
* 🎯 App Server Implementation
|
|
25
|
-
*
|
|
26
|
-
* Base class for creating App servers. Handles:
|
|
27
|
-
* - 🔄 Session lifecycle management
|
|
28
|
-
* - 📡 Webhook endpoints for MentraOS Cloud
|
|
29
|
-
* - 📂 Static file serving
|
|
30
|
-
* - ❤️ Health checks
|
|
31
|
-
* - 🧹 Cleanup on shutdown
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* class MyAppServer extends AppServer {
|
|
36
|
-
* protected async onSession(session: AppSession, sessionId: string, userId: string) {
|
|
37
|
-
* // Handle new user sessions here
|
|
38
|
-
* session.events.onTranscription((data) => {
|
|
39
|
-
* session.layouts.showTextWall(data.text);
|
|
40
|
-
* });
|
|
41
|
-
* }
|
|
42
|
-
* }
|
|
43
|
-
*
|
|
44
|
-
* const server = new MyAppServer({
|
|
45
|
-
* packageName: 'org.example.myapp',
|
|
46
|
-
* apiKey: 'your_api_key',
|
|
47
|
-
* publicDir: "/public",
|
|
48
|
-
* });
|
|
49
|
-
*
|
|
50
|
-
* await server.start();
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
class AppServer {
|
|
54
|
-
constructor(config) {
|
|
55
|
-
this.config = config;
|
|
56
|
-
/** Map of active user sessions by sessionId */
|
|
57
|
-
this.activeSessions = new Map();
|
|
58
|
-
/** Map of active user sessions by userId */
|
|
59
|
-
this.activeSessionsByUserId = new Map();
|
|
60
|
-
/** Array of cleanup handlers to run on shutdown */
|
|
61
|
-
this.cleanupHandlers = [];
|
|
62
|
-
/** App instructions string shown to the user */
|
|
63
|
-
this.appInstructions = null;
|
|
64
|
-
// Set defaults and merge with provided config
|
|
65
|
-
this.config = {
|
|
66
|
-
port: 7010,
|
|
67
|
-
webhookPath: "/webhook",
|
|
68
|
-
publicDir: false,
|
|
69
|
-
healthCheck: true,
|
|
70
|
-
...config,
|
|
71
|
-
};
|
|
72
|
-
this.logger = logger_1.logger.child({
|
|
73
|
-
app: this.config.packageName,
|
|
74
|
-
packageName: this.config.packageName,
|
|
75
|
-
service: "app-server",
|
|
76
|
-
});
|
|
77
|
-
// Initialize Express app
|
|
78
|
-
this.app = (0, express_1.default)();
|
|
79
|
-
this.app.use(express_1.default.json());
|
|
80
|
-
const cookieParser = require("cookie-parser");
|
|
81
|
-
this.app.use(cookieParser(this.config.cookieSecret ||
|
|
82
|
-
`AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`));
|
|
83
|
-
// Apply authentication middleware
|
|
84
|
-
this.app.use((0, webview_1.createAuthMiddleware)({
|
|
85
|
-
apiKey: this.config.apiKey,
|
|
86
|
-
packageName: this.config.packageName,
|
|
87
|
-
getAppSessionForUser: (userId) => {
|
|
88
|
-
return this.activeSessionsByUserId.get(userId) || null;
|
|
89
|
-
},
|
|
90
|
-
cookieSecret: this.config.cookieSecret ||
|
|
91
|
-
`AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`,
|
|
92
|
-
}));
|
|
93
|
-
this.appInstructions = config.appInstructions || null;
|
|
94
|
-
// Setup server features
|
|
95
|
-
this.setupWebhook();
|
|
96
|
-
this.setupSettingsEndpoint();
|
|
97
|
-
this.setupHealthCheck();
|
|
98
|
-
this.setupToolCallEndpoint();
|
|
99
|
-
this.setupPhotoUploadEndpoint();
|
|
100
|
-
this.setupMentraAuthRedirect();
|
|
101
|
-
this.setupPublicDir();
|
|
102
|
-
this.setupShutdown();
|
|
103
|
-
}
|
|
104
|
-
// Expose Express app for custom routes.
|
|
105
|
-
// This is useful for adding custom API routes or middleware.
|
|
106
|
-
getExpressApp() {
|
|
107
|
-
return this.app;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* 👥 Session Handler
|
|
111
|
-
* Override this method to handle new App sessions.
|
|
112
|
-
* This is where you implement your app's core functionality.
|
|
113
|
-
*
|
|
114
|
-
* @param session - App session instance for the user
|
|
115
|
-
* @param sessionId - Unique identifier for this session
|
|
116
|
-
* @param userId - User's identifier
|
|
117
|
-
*/
|
|
118
|
-
async onSession(session, sessionId, userId) {
|
|
119
|
-
this.logger.info(`🚀 Starting new session handling for session ${sessionId} and user ${userId}`);
|
|
120
|
-
// Core session handling logic (onboarding removed)
|
|
121
|
-
this.logger.info(`✅ Session handling completed for session ${sessionId} and user ${userId}`);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* 👥 Stop Handler
|
|
125
|
-
* Override this method to handle stop requests.
|
|
126
|
-
* This is where you can clean up resources when a session is stopped.
|
|
127
|
-
*
|
|
128
|
-
* @param sessionId - Unique identifier for this session
|
|
129
|
-
* @param userId - User's identifier
|
|
130
|
-
* @param reason - Reason for stopping
|
|
131
|
-
*/
|
|
132
|
-
async onStop(sessionId, userId, reason) {
|
|
133
|
-
this.logger.debug(`Session ${sessionId} stopped for user ${userId}. Reason: ${reason}`);
|
|
134
|
-
// Default implementation: close the session if it exists
|
|
135
|
-
const session = this.activeSessions.get(sessionId);
|
|
136
|
-
if (session) {
|
|
137
|
-
session.disconnect();
|
|
138
|
-
this.activeSessions.delete(sessionId);
|
|
139
|
-
this.activeSessionsByUserId.delete(userId);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* 🛠️ Tool Call Handler
|
|
144
|
-
* Override this method to handle tool calls from MentraOS Cloud.
|
|
145
|
-
* This is where you implement your app's tool functionality.
|
|
146
|
-
*
|
|
147
|
-
* @param toolCall - The tool call request containing tool details and parameters
|
|
148
|
-
* @returns Optional string response that will be sent back to MentraOS Cloud
|
|
149
|
-
*/
|
|
150
|
-
async onToolCall(toolCall) {
|
|
151
|
-
this.logger.debug(`Tool call received: ${toolCall.toolId}`);
|
|
152
|
-
this.logger.debug(`Parameters: ${JSON.stringify(toolCall.toolParameters)}`);
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* 🚀 Start the Server
|
|
157
|
-
* Starts listening for incoming connections and webhook calls.
|
|
158
|
-
*
|
|
159
|
-
* @returns Promise that resolves when server is ready
|
|
160
|
-
*/
|
|
161
|
-
start() {
|
|
162
|
-
return new Promise((resolve) => {
|
|
163
|
-
this.app.listen(this.config.port, async () => {
|
|
164
|
-
this.logger.info(`🎯 App server running at http://localhost:${this.config.port}`);
|
|
165
|
-
if (this.config.publicDir) {
|
|
166
|
-
this.logger.info(`📂 Serving static files from ${this.config.publicDir}`);
|
|
167
|
-
}
|
|
168
|
-
// 🔑 Grab SDK version
|
|
169
|
-
try {
|
|
170
|
-
// Look for the actual installed @mentra/sdk package.json in node_modules
|
|
171
|
-
const sdkPkgPath = path_1.default.resolve(process.cwd(), "node_modules/@mentra/sdk/package.json");
|
|
172
|
-
let currentVersion = "unknown";
|
|
173
|
-
if (fs_1.default.existsSync(sdkPkgPath)) {
|
|
174
|
-
const sdkPkg = JSON.parse(fs_1.default.readFileSync(sdkPkgPath, "utf-8"));
|
|
175
|
-
// Get the actual installed version
|
|
176
|
-
currentVersion = sdkPkg.version || "not-found"; // located in the node module
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
this.logger.debug({ sdkPkgPath }, "No @mentra/sdk package.json found at path");
|
|
180
|
-
}
|
|
181
|
-
// this.logger.debug(`Developer is using SDK version: ${currentVersion}`);
|
|
182
|
-
// Fetch latest SDK version from the API endpoint
|
|
183
|
-
let latest = "2.1.25"; // fallback version
|
|
184
|
-
try {
|
|
185
|
-
const cloudHost = "api.mentra.glass";
|
|
186
|
-
const response = await axios_1.default.get(`https://${cloudHost}/api/sdk/version`);
|
|
187
|
-
if (response.data && response.data.success && response.data.data) {
|
|
188
|
-
latest = response.data.data.latest; // Changed from "recommended" to "latest"
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch (fetchError) {
|
|
192
|
-
this.logger.debug({ fetchError }, "Failed to fetch latest SDK version from API, using fallback");
|
|
193
|
-
}
|
|
194
|
-
if (currentVersion === "not-found") {
|
|
195
|
-
this.logger.warn(`⚠️ @mentra/sdk not found in your project dependencies. Please install it with: npm install @mentra/sdk`);
|
|
196
|
-
}
|
|
197
|
-
else if (latest && latest !== currentVersion) {
|
|
198
|
-
this.logger.warn((0, messages_1.newSDKUpdate)(latest));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
this.logger.error(err, "Version check failed");
|
|
203
|
-
}
|
|
204
|
-
resolve();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* 🛑 Stop the Server
|
|
210
|
-
* Gracefully shuts down the server and cleans up all sessions.
|
|
211
|
-
*/
|
|
212
|
-
stop() {
|
|
213
|
-
this.logger.info("\n🛑 Shutting down...");
|
|
214
|
-
this.cleanup();
|
|
215
|
-
process.exit(0);
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* 🔐 Generate a App token for a user
|
|
219
|
-
* This should be called when handling a session webhook request.
|
|
220
|
-
*
|
|
221
|
-
* @param userId - User identifier
|
|
222
|
-
* @param sessionId - Session identifier
|
|
223
|
-
* @param secretKey - Secret key for signing the token
|
|
224
|
-
* @returns JWT token string
|
|
225
|
-
*/
|
|
226
|
-
generateToken(userId, sessionId, secretKey) {
|
|
227
|
-
const { createToken } = require("../token/utils");
|
|
228
|
-
return createToken({
|
|
229
|
-
userId,
|
|
230
|
-
packageName: this.config.packageName,
|
|
231
|
-
sessionId,
|
|
232
|
-
}, { secretKey });
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* 🧹 Add Cleanup Handler
|
|
236
|
-
* Register a function to be called during server shutdown.
|
|
237
|
-
*
|
|
238
|
-
* @param handler - Function to call during cleanup
|
|
239
|
-
*/
|
|
240
|
-
addCleanupHandler(handler) {
|
|
241
|
-
this.cleanupHandlers.push(handler);
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* 🎯 Setup Webhook Endpoint
|
|
245
|
-
* Creates the webhook endpoint that MentraOS Cloud calls to start new sessions.
|
|
246
|
-
*/
|
|
247
|
-
setupWebhook() {
|
|
248
|
-
if (!this.config.webhookPath) {
|
|
249
|
-
this.logger.error("❌ Webhook path not set");
|
|
250
|
-
throw new Error("Webhook path not set");
|
|
251
|
-
}
|
|
252
|
-
this.app.post(this.config.webhookPath, async (req, res) => {
|
|
253
|
-
try {
|
|
254
|
-
const webhookRequest = req.body;
|
|
255
|
-
// Handle session request
|
|
256
|
-
if (webhookRequest.type === types_1.WebhookRequestType.SESSION_REQUEST) {
|
|
257
|
-
await this.handleSessionRequest(webhookRequest, res);
|
|
258
|
-
}
|
|
259
|
-
// Handle stop request
|
|
260
|
-
else if (webhookRequest.type === types_1.WebhookRequestType.STOP_REQUEST) {
|
|
261
|
-
await this.handleStopRequest(webhookRequest, res);
|
|
262
|
-
}
|
|
263
|
-
// Unknown webhook type
|
|
264
|
-
else {
|
|
265
|
-
this.logger.error("❌ Unknown webhook request type");
|
|
266
|
-
res.status(400).json({
|
|
267
|
-
status: "error",
|
|
268
|
-
message: "Unknown webhook request type",
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
catch (error) {
|
|
273
|
-
this.logger.error(error, "❌ Error handling webhook: " + error.message);
|
|
274
|
-
res.status(500).json({
|
|
275
|
-
status: "error",
|
|
276
|
-
message: "Error handling webhook: " + error.message,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* 🛠️ Setup Tool Call Endpoint
|
|
283
|
-
* Creates a /tool endpoint for handling tool calls from MentraOS Cloud.
|
|
284
|
-
*/
|
|
285
|
-
setupToolCallEndpoint() {
|
|
286
|
-
this.app.post("/tool", async (req, res) => {
|
|
287
|
-
try {
|
|
288
|
-
const toolCall = req.body;
|
|
289
|
-
if (this.activeSessionsByUserId.has(toolCall.userId)) {
|
|
290
|
-
toolCall.activeSession =
|
|
291
|
-
this.activeSessionsByUserId.get(toolCall.userId) || null;
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
toolCall.activeSession = null;
|
|
295
|
-
}
|
|
296
|
-
this.logger.info({ body: req.body }, `🔧 Received tool call: ${toolCall.toolId}`);
|
|
297
|
-
// Call the onToolCall handler and get the response
|
|
298
|
-
const response = await this.onToolCall(toolCall);
|
|
299
|
-
// Send back the response if one was provided
|
|
300
|
-
if (response !== undefined) {
|
|
301
|
-
res.json({ status: "success", reply: response });
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
res.json({ status: "success", reply: null });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
catch (error) {
|
|
308
|
-
this.logger.error(error, "❌ Error handling tool call:");
|
|
309
|
-
res.status(500).json({
|
|
310
|
-
status: "error",
|
|
311
|
-
message: error instanceof Error
|
|
312
|
-
? error.message
|
|
313
|
-
: "Unknown error occurred calling tool",
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
this.app.get("/tool", async (req, res) => {
|
|
318
|
-
res.json({ status: "success", reply: "Hello, world!" });
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Handle a session request webhook
|
|
323
|
-
*/
|
|
324
|
-
async handleSessionRequest(request, res) {
|
|
325
|
-
const { sessionId, userId, mentraOSWebsocketUrl, augmentOSWebsocketUrl } = request;
|
|
326
|
-
this.logger.info({ userId }, `🗣️ Received session request for user ${userId}, session ${sessionId}\n\n`);
|
|
327
|
-
// Create new App session
|
|
328
|
-
const session = new index_1.AppSession({
|
|
329
|
-
packageName: this.config.packageName,
|
|
330
|
-
apiKey: this.config.apiKey,
|
|
331
|
-
mentraOSWebsocketUrl: mentraOSWebsocketUrl || augmentOSWebsocketUrl, // The websocket URL for the specific MentraOS server that this userSession is connecting to.
|
|
332
|
-
appServer: this,
|
|
333
|
-
userId,
|
|
334
|
-
});
|
|
335
|
-
// Setup session event handlers
|
|
336
|
-
const cleanupDisconnect = session.events.onDisconnected((info) => {
|
|
337
|
-
// Handle different disconnect info formats (string or object)
|
|
338
|
-
if (typeof info === "string") {
|
|
339
|
-
this.logger.info(`👋 Session ${sessionId} disconnected: ${info}`);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
// It's an object with detailed disconnect information
|
|
343
|
-
this.logger.info(`👋 Session ${sessionId} disconnected: ${info.message} (code: ${info.code}, reason: ${info.reason})`);
|
|
344
|
-
// Check if this is a user session end event
|
|
345
|
-
// This happens when the UserSession is disposed after 1 minute grace period
|
|
346
|
-
if (info.sessionEnded === true) {
|
|
347
|
-
this.logger.info(`🛑 User session ended for session ${sessionId}, calling onStop`);
|
|
348
|
-
// Call onStop with session end reason
|
|
349
|
-
// This allows apps to clean up resources when the user's session ends
|
|
350
|
-
this.onStop(sessionId, userId, "User session ended").catch((error) => {
|
|
351
|
-
this.logger.error(error, `❌ Error in onStop handler for session end:`);
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
// Check if this is a permanent disconnection after exhausted reconnection attempts
|
|
355
|
-
else if (info.permanent === true) {
|
|
356
|
-
this.logger.info(`🛑 Permanent disconnection detected for session ${sessionId}, calling onStop`);
|
|
357
|
-
// Keep track of the original session before removal
|
|
358
|
-
// const session = this.activeSessions.get(sessionId);
|
|
359
|
-
const _session = this.activeSessions.get(sessionId);
|
|
360
|
-
// Call onStop with a reconnection failure reason
|
|
361
|
-
this.onStop(sessionId, userId, `Connection permanently lost: ${info.reason}`).catch((error) => {
|
|
362
|
-
this.logger.error(error, `❌ Error in onStop handler for permanent disconnection:`);
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
// Remove the session from active sessions in all cases
|
|
367
|
-
this.activeSessions.delete(sessionId);
|
|
368
|
-
this.activeSessionsByUserId.delete(userId);
|
|
369
|
-
});
|
|
370
|
-
const cleanupError = session.events.onError((error) => {
|
|
371
|
-
this.logger.error(error, `❌ [Session ${sessionId}] Error:`);
|
|
372
|
-
});
|
|
373
|
-
// Start the session
|
|
374
|
-
try {
|
|
375
|
-
await session.connect(sessionId);
|
|
376
|
-
this.activeSessions.set(sessionId, session);
|
|
377
|
-
this.activeSessionsByUserId.set(userId, session);
|
|
378
|
-
await this.onSession(session, sessionId, userId);
|
|
379
|
-
res.status(200).json({ status: "success" });
|
|
380
|
-
}
|
|
381
|
-
catch (error) {
|
|
382
|
-
this.logger.error(error, "❌ Failed to connect:");
|
|
383
|
-
cleanupDisconnect();
|
|
384
|
-
cleanupError();
|
|
385
|
-
res.status(500).json({
|
|
386
|
-
status: "error",
|
|
387
|
-
message: "Failed to connect",
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Handle a stop request webhook
|
|
393
|
-
*/
|
|
394
|
-
async handleStopRequest(request, res) {
|
|
395
|
-
const { sessionId, userId, reason } = request;
|
|
396
|
-
this.logger.info(`\n\n🛑 Received stop request for user ${userId}, session ${sessionId}, reason: ${reason}\n\n`);
|
|
397
|
-
try {
|
|
398
|
-
await this.onStop(sessionId, userId, reason);
|
|
399
|
-
res.status(200).json({ status: "success" });
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
this.logger.error(error, "❌ Error handling stop request:");
|
|
403
|
-
res.status(500).json({
|
|
404
|
-
status: "error",
|
|
405
|
-
message: "Failed to process stop request",
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* ❤️ Setup Health Check Endpoint
|
|
411
|
-
* Creates a /health endpoint for monitoring server status.
|
|
412
|
-
*/
|
|
413
|
-
setupHealthCheck() {
|
|
414
|
-
if (this.config.healthCheck) {
|
|
415
|
-
this.app.get("/health", (req, res) => {
|
|
416
|
-
res.json({
|
|
417
|
-
status: "healthy",
|
|
418
|
-
app: this.config.packageName,
|
|
419
|
-
activeSessions: this.activeSessions.size,
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* ⚙️ Setup Settings Endpoint
|
|
426
|
-
* Creates a /settings endpoint that the MentraOS Cloud can use to update settings.
|
|
427
|
-
*/
|
|
428
|
-
setupSettingsEndpoint() {
|
|
429
|
-
this.app.post("/settings", async (req, res) => {
|
|
430
|
-
try {
|
|
431
|
-
const { userIdForSettings, settings } = req.body;
|
|
432
|
-
if (!userIdForSettings || !Array.isArray(settings)) {
|
|
433
|
-
return res.status(400).json({
|
|
434
|
-
status: "error",
|
|
435
|
-
message: "Missing userId or settings array in request body",
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
this.logger.info(`⚙️ Received settings update for user ${userIdForSettings}`);
|
|
439
|
-
// Find all active sessions for this user
|
|
440
|
-
const userSessions = [];
|
|
441
|
-
// Look through all active sessions
|
|
442
|
-
this.activeSessions.forEach((session, _sessionId) => {
|
|
443
|
-
// Check if the session has this userId (not directly accessible)
|
|
444
|
-
// We're relying on the webhook handler to have already verified this
|
|
445
|
-
if (session.userId === userIdForSettings) {
|
|
446
|
-
userSessions.push(session);
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
if (userSessions.length === 0) {
|
|
450
|
-
this.logger.warn(`⚠️ No active sessions found for user ${userIdForSettings}`);
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
this.logger.info(`🔄 Updating settings for ${userSessions.length} active sessions`);
|
|
454
|
-
}
|
|
455
|
-
// Update settings for all of the user's sessions
|
|
456
|
-
for (const session of userSessions) {
|
|
457
|
-
session.updateSettingsForTesting(settings);
|
|
458
|
-
}
|
|
459
|
-
// Allow subclasses to handle settings updates if they implement the method
|
|
460
|
-
if (typeof this.onSettingsUpdate === "function") {
|
|
461
|
-
await this.onSettingsUpdate(userIdForSettings, settings);
|
|
462
|
-
}
|
|
463
|
-
res.json({
|
|
464
|
-
status: "success",
|
|
465
|
-
message: "Settings updated successfully",
|
|
466
|
-
sessionsUpdated: userSessions.length,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
this.logger.error(error, "❌ Error handling settings update:");
|
|
471
|
-
res.status(500).json({
|
|
472
|
-
status: "error",
|
|
473
|
-
message: "Internal server error processing settings update",
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* 📂 Setup Static File Serving
|
|
480
|
-
* Configures Express to serve static files from the specified directory.
|
|
481
|
-
*/
|
|
482
|
-
setupPublicDir() {
|
|
483
|
-
if (this.config.publicDir) {
|
|
484
|
-
const publicPath = path_1.default.resolve(this.config.publicDir);
|
|
485
|
-
this.app.use(express_1.default.static(publicPath));
|
|
486
|
-
this.logger.info(`📂 Serving static files from ${publicPath}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* 🛑 Setup Shutdown Handlers
|
|
491
|
-
* Registers process signal handlers for graceful shutdown.
|
|
492
|
-
*/
|
|
493
|
-
setupShutdown() {
|
|
494
|
-
process.on("SIGTERM", () => this.stop());
|
|
495
|
-
process.on("SIGINT", () => this.stop());
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* 🧹 Cleanup
|
|
499
|
-
* Closes all active sessions and runs cleanup handlers.
|
|
500
|
-
*/
|
|
501
|
-
cleanup() {
|
|
502
|
-
// Close all active sessions
|
|
503
|
-
for (const [sessionId, session] of this.activeSessions) {
|
|
504
|
-
this.logger.info(`👋 Closing session ${sessionId}`);
|
|
505
|
-
session.disconnect();
|
|
506
|
-
}
|
|
507
|
-
this.activeSessions.clear();
|
|
508
|
-
this.activeSessionsByUserId.clear();
|
|
509
|
-
// Run cleanup handlers
|
|
510
|
-
this.cleanupHandlers.forEach((handler) => handler());
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* 🎯 Setup Photo Upload Endpoint
|
|
514
|
-
* Creates a /photo-upload endpoint for receiving photos directly from ASG glasses
|
|
515
|
-
*/
|
|
516
|
-
setupPhotoUploadEndpoint() {
|
|
517
|
-
const multer = require("multer");
|
|
518
|
-
// Configure multer for handling multipart form data
|
|
519
|
-
const upload = multer({
|
|
520
|
-
storage: multer.memoryStorage(),
|
|
521
|
-
limits: {
|
|
522
|
-
fileSize: 10 * 1024 * 1024, // 10MB limit
|
|
523
|
-
},
|
|
524
|
-
fileFilter: (req, file, cb) => {
|
|
525
|
-
// Accept image files only
|
|
526
|
-
if (file.mimetype && file.mimetype.startsWith("image/")) {
|
|
527
|
-
cb(null, true);
|
|
528
|
-
}
|
|
529
|
-
else {
|
|
530
|
-
cb(new Error("Only image files are allowed"), false);
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
});
|
|
534
|
-
this.app.post("/photo-upload", upload.single("photo"), async (req, res) => {
|
|
535
|
-
try {
|
|
536
|
-
const { requestId, type, success, errorCode, errorMessage } = req.body;
|
|
537
|
-
const photoFile = req.file;
|
|
538
|
-
console.log("Received photo response: ", req.body);
|
|
539
|
-
this.logger.info({ requestId, type, success, errorCode }, `📸 Received photo response: ${requestId} (type: ${type})`);
|
|
540
|
-
if (!requestId) {
|
|
541
|
-
this.logger.error("No requestId in photo response");
|
|
542
|
-
return res.status(400).json({
|
|
543
|
-
success: false,
|
|
544
|
-
error: "No requestId provided",
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
// Find the corresponding session that made this photo request
|
|
548
|
-
const session = this.findSessionByPhotoRequestId(requestId);
|
|
549
|
-
if (!session) {
|
|
550
|
-
this.logger.warn({ requestId }, "No active session found for photo request");
|
|
551
|
-
return res.status(404).json({
|
|
552
|
-
success: false,
|
|
553
|
-
error: "No active session found for this photo request",
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
// Handle error response (no photo file, but has error info)
|
|
557
|
-
if (type === "photo_error" || !success) {
|
|
558
|
-
// Create error response object
|
|
559
|
-
const errorResponse = {
|
|
560
|
-
requestId,
|
|
561
|
-
success: false,
|
|
562
|
-
error: {
|
|
563
|
-
code: errorCode || "UNKNOWN_ERROR",
|
|
564
|
-
message: errorMessage || "Unknown error occurred",
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
// Deliver error to the session (logging happens in camera module)
|
|
568
|
-
session.camera.handlePhotoError(errorResponse);
|
|
569
|
-
// Respond to ASG client
|
|
570
|
-
return res.json({
|
|
571
|
-
success: true,
|
|
572
|
-
requestId,
|
|
573
|
-
message: "Photo error received successfully",
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
// Handle successful photo upload
|
|
577
|
-
if (!photoFile) {
|
|
578
|
-
this.logger.error({ requestId }, "No photo file in successful upload");
|
|
579
|
-
return res.status(400).json({
|
|
580
|
-
success: false,
|
|
581
|
-
error: "No photo file provided for successful upload",
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
// Create photo data object
|
|
585
|
-
const photoData = {
|
|
586
|
-
buffer: photoFile.buffer,
|
|
587
|
-
mimeType: photoFile.mimetype,
|
|
588
|
-
filename: photoFile.originalname || "photo.jpg",
|
|
589
|
-
requestId,
|
|
590
|
-
size: photoFile.size,
|
|
591
|
-
timestamp: new Date(),
|
|
592
|
-
};
|
|
593
|
-
// Deliver photo to the session
|
|
594
|
-
session.camera.handlePhotoReceived(photoData);
|
|
595
|
-
// Respond to ASG client
|
|
596
|
-
res.json({
|
|
597
|
-
success: true,
|
|
598
|
-
requestId,
|
|
599
|
-
message: "Photo received successfully",
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
this.logger.error(error, "❌ Error handling photo response");
|
|
604
|
-
res.status(500).json({
|
|
605
|
-
success: false,
|
|
606
|
-
error: "Internal server error processing photo response",
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* 🔐 Setup Mentra Auth Redirect Endpoint
|
|
613
|
-
* Creates a /mentra-auth endpoint that redirects to the MentraOS OAuth flow.
|
|
614
|
-
*/
|
|
615
|
-
setupMentraAuthRedirect() {
|
|
616
|
-
this.app.get("/mentra-auth", (req, res) => {
|
|
617
|
-
// Redirect to the account.mentra.glass OAuth flow with the app's package name
|
|
618
|
-
const authUrl = `https://account.mentra.glass/auth?packagename=${encodeURIComponent(this.config.packageName)}`;
|
|
619
|
-
this.logger.info(`🔐 Redirecting to MentraOS OAuth flow: ${authUrl}`);
|
|
620
|
-
res.redirect(302, authUrl);
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Find session that has a pending photo request for the given requestId
|
|
625
|
-
*/
|
|
626
|
-
findSessionByPhotoRequestId(requestId) {
|
|
627
|
-
for (const [_sessionId, session] of this.activeSessions) {
|
|
628
|
-
if (session.camera.hasPhotoPendingRequest(requestId)) {
|
|
629
|
-
return session;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return undefined;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
exports.AppServer = AppServer;
|
|
636
|
-
/**
|
|
637
|
-
* @deprecated Use `AppServer` instead. `TpaServer` is deprecated and will be removed in a future version.
|
|
638
|
-
* This is an alias for backward compatibility only.
|
|
639
|
-
*
|
|
640
|
-
* @example
|
|
641
|
-
* ```typescript
|
|
642
|
-
* // ❌ Deprecated - Don't use this
|
|
643
|
-
* class MyServer extends TpaServer { ... }
|
|
644
|
-
*
|
|
645
|
-
* // ✅ Use this instead
|
|
646
|
-
* class MyServer extends AppServer { ... }
|
|
647
|
-
* ```
|
|
648
|
-
*/
|
|
649
|
-
class TpaServer extends AppServer {
|
|
650
|
-
constructor(config) {
|
|
651
|
-
super(config);
|
|
652
|
-
// Emit a deprecation warning to help developers migrate
|
|
653
|
-
console.warn("⚠️ DEPRECATION WARNING: TpaServer is deprecated and will be removed in a future version. " +
|
|
654
|
-
"Please use AppServer instead. " +
|
|
655
|
-
'Simply replace "TpaServer" with "AppServer" in your code.');
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
exports.TpaServer = TpaServer;
|