@makemind/sdk 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.
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/dist/index.d.mts +3080 -0
- package/dist/index.d.ts +3080 -0
- package/dist/index.js +4131 -0
- package/dist/index.mjs +4074 -0
- package/package.json +66 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4074 @@
|
|
|
1
|
+
// src/utils/errors.ts
|
|
2
|
+
var MAKEMINDError = class _MAKEMINDError extends Error {
|
|
3
|
+
constructor(code, message, details) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "MAKEMINDError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.details = details;
|
|
8
|
+
}
|
|
9
|
+
static fromApiError(error) {
|
|
10
|
+
const err = error;
|
|
11
|
+
const code = err.code || "UNKNOWN_ERROR";
|
|
12
|
+
const message = err.message || "An unknown error occurred";
|
|
13
|
+
return new _MAKEMINDError(code, message, err.details);
|
|
14
|
+
}
|
|
15
|
+
static fromHttpStatus(status, body) {
|
|
16
|
+
const statusMapping = {
|
|
17
|
+
400: ErrorCodes.VALIDATION_ERROR,
|
|
18
|
+
401: ErrorCodes.UNAUTHENTICATED,
|
|
19
|
+
403: ErrorCodes.PERMISSION_DENIED,
|
|
20
|
+
404: ErrorCodes.NOT_FOUND,
|
|
21
|
+
409: ErrorCodes.ALREADY_EXISTS,
|
|
22
|
+
429: ErrorCodes.QUOTA_EXCEEDED,
|
|
23
|
+
500: ErrorCodes.INTERNAL_ERROR,
|
|
24
|
+
503: ErrorCodes.SERVICE_UNAVAILABLE
|
|
25
|
+
};
|
|
26
|
+
const code = statusMapping[status] || "UNKNOWN_ERROR";
|
|
27
|
+
const bodyObj = body;
|
|
28
|
+
const message = bodyObj?.message || `HTTP error ${status}`;
|
|
29
|
+
return new _MAKEMINDError(code, message, bodyObj?.details);
|
|
30
|
+
}
|
|
31
|
+
toJSON() {
|
|
32
|
+
return {
|
|
33
|
+
code: this.code,
|
|
34
|
+
message: this.message,
|
|
35
|
+
details: this.details
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var ErrorCodes = {
|
|
40
|
+
// Auth errors
|
|
41
|
+
UNAUTHENTICATED: "UNAUTHENTICATED",
|
|
42
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
43
|
+
INVALID_TOKEN: "INVALID_TOKEN",
|
|
44
|
+
// Data errors
|
|
45
|
+
NOT_FOUND: "NOT_FOUND",
|
|
46
|
+
ALREADY_EXISTS: "ALREADY_EXISTS",
|
|
47
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
48
|
+
// Tenant errors
|
|
49
|
+
TENANT_NOT_FOUND: "TENANT_NOT_FOUND",
|
|
50
|
+
TENANT_SUSPENDED: "TENANT_SUSPENDED",
|
|
51
|
+
NOT_A_MEMBER: "NOT_A_MEMBER",
|
|
52
|
+
// Quota errors
|
|
53
|
+
QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
|
|
54
|
+
// Storage errors
|
|
55
|
+
STORAGE_ERROR: "STORAGE_ERROR",
|
|
56
|
+
// System errors
|
|
57
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
58
|
+
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
59
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/services/auth.ts
|
|
63
|
+
var STORAGE_KEYS = {
|
|
64
|
+
AUTH_TOKEN: "makemindAuthToken",
|
|
65
|
+
REFRESH_TOKEN: "makemindRefreshToken",
|
|
66
|
+
USER_DATA: "makemindUserData",
|
|
67
|
+
MEMBERSHIPS: "makemindMemberships"
|
|
68
|
+
};
|
|
69
|
+
var MAKEMINDAuth = class {
|
|
70
|
+
constructor(config) {
|
|
71
|
+
this._currentUser = null;
|
|
72
|
+
this._memberships = [];
|
|
73
|
+
this.refreshToken = null;
|
|
74
|
+
this.authStateCallbacks = /* @__PURE__ */ new Set();
|
|
75
|
+
this.apiClient = config.apiClient;
|
|
76
|
+
this.applicationId = config.applicationId;
|
|
77
|
+
this.onAuthStateChange = config.onAuthStateChange;
|
|
78
|
+
this.setAuthToken = config.setAuthToken;
|
|
79
|
+
this.restoreSession();
|
|
80
|
+
}
|
|
81
|
+
restoreSession() {
|
|
82
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const storedToken = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
|
87
|
+
const storedRefreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
88
|
+
if (storedRefreshToken) localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
89
|
+
const storedUserData = localStorage.getItem(STORAGE_KEYS.USER_DATA);
|
|
90
|
+
const storedMemberships = localStorage.getItem(STORAGE_KEYS.MEMBERSHIPS);
|
|
91
|
+
if (storedToken && storedUserData) {
|
|
92
|
+
this._currentUser = JSON.parse(storedUserData);
|
|
93
|
+
this.refreshToken = storedRefreshToken;
|
|
94
|
+
this._memberships = storedMemberships ? JSON.parse(storedMemberships) : [];
|
|
95
|
+
this.setAuthToken(storedToken);
|
|
96
|
+
this.onAuthStateChange(this._currentUser);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
this.clearStoredSession();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
persistSession(user, token, refreshToken, memberships) {
|
|
103
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, token);
|
|
108
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
109
|
+
localStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(user));
|
|
110
|
+
if (memberships) {
|
|
111
|
+
localStorage.setItem(STORAGE_KEYS.MEMBERSHIPS, JSON.stringify(memberships));
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
clearStoredSession() {
|
|
117
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
|
122
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
123
|
+
localStorage.removeItem(STORAGE_KEYS.USER_DATA);
|
|
124
|
+
localStorage.removeItem(STORAGE_KEYS.MEMBERSHIPS);
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
get currentUser() {
|
|
129
|
+
return this._currentUser;
|
|
130
|
+
}
|
|
131
|
+
get memberships() {
|
|
132
|
+
return this._memberships;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Set the session directly from an externally-obtained token — e.g. a Firebase
|
|
136
|
+
* ID token from Google sign-in, which the gateway accepts as a Bearer. Mirrors
|
|
137
|
+
* the post-authentication state update of signInWithEmail (set token, persist
|
|
138
|
+
* to localStorage, notify auth-state listeners) without a /auth/signin
|
|
139
|
+
* round-trip. Also used by the console's token-refresh bridge to swap in a
|
|
140
|
+
* freshly-refreshed Firebase token. Existing memberships are preserved when
|
|
141
|
+
* `memberships` is omitted.
|
|
142
|
+
*/
|
|
143
|
+
setSession(user, token, refreshToken, memberships) {
|
|
144
|
+
this._currentUser = user;
|
|
145
|
+
this.refreshToken = refreshToken;
|
|
146
|
+
if (memberships) {
|
|
147
|
+
this._memberships = memberships;
|
|
148
|
+
}
|
|
149
|
+
this.setAuthToken(token);
|
|
150
|
+
this.persistSession(user, token, refreshToken, memberships ?? this._memberships);
|
|
151
|
+
this.onAuthStateChange(user);
|
|
152
|
+
this.notifyAuthStateCallbacks(user);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Load the caller's tenant memberships into the session.
|
|
156
|
+
*
|
|
157
|
+
* `signInWithEmail` gets them in its response, but a session established from
|
|
158
|
+
* an external credential (a Firebase ID token from a Google popup) has no
|
|
159
|
+
* such response — it arrives with claims only. Claims carry the *current*
|
|
160
|
+
* tenant, not the list, and a platform admin's claims carry no tenant roles
|
|
161
|
+
* at all, so a console that gates its navigation on `admin`/`owner` shows an
|
|
162
|
+
* almost empty menu until this runs.
|
|
163
|
+
*
|
|
164
|
+
* @return {Promise<TenantMembership[]>} The loaded memberships.
|
|
165
|
+
*/
|
|
166
|
+
async loadMemberships() {
|
|
167
|
+
const res = await this.apiClient.dispatch(
|
|
168
|
+
"getUserTenants",
|
|
169
|
+
{}
|
|
170
|
+
);
|
|
171
|
+
const memberships = (res?.tenants ?? []).map((t) => ({
|
|
172
|
+
tenantId: String(t.id ?? ""),
|
|
173
|
+
tenantName: String(t.name ?? t.id ?? ""),
|
|
174
|
+
tenantDisplayName: String(t.displayName ?? t.name ?? t.id ?? ""),
|
|
175
|
+
roles: Array.isArray(t.roles) ? t.roles : [],
|
|
176
|
+
// The op returns the tenant's own status; a membership on a live tenant
|
|
177
|
+
// is what a consumer means by active.
|
|
178
|
+
status: String(t.status ?? "active")
|
|
179
|
+
}));
|
|
180
|
+
this._memberships = memberships;
|
|
181
|
+
if (this._currentUser) {
|
|
182
|
+
localStorage.setItem(STORAGE_KEYS.MEMBERSHIPS, JSON.stringify(memberships));
|
|
183
|
+
}
|
|
184
|
+
return memberships;
|
|
185
|
+
}
|
|
186
|
+
async signInWithEmail(email, password) {
|
|
187
|
+
try {
|
|
188
|
+
const response = await this.apiClient.post("/auth/signin", {
|
|
189
|
+
email,
|
|
190
|
+
password,
|
|
191
|
+
applicationId: this.applicationId
|
|
192
|
+
}, { skipAuth: true });
|
|
193
|
+
this._currentUser = response.user;
|
|
194
|
+
this.refreshToken = response.refreshToken ?? null;
|
|
195
|
+
this._memberships = response.memberships || [];
|
|
196
|
+
this.setAuthToken(response.token);
|
|
197
|
+
this.persistSession(response.user, response.token, response.refreshToken, response.memberships);
|
|
198
|
+
this.onAuthStateChange(response.user);
|
|
199
|
+
this.notifyAuthStateCallbacks(response.user);
|
|
200
|
+
return response.user;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof MAKEMINDError) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
throw new MAKEMINDError("AUTH_ERROR", "Failed to sign in");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Alias for Firebase compatibility
|
|
209
|
+
async signInWithEmailAndPassword(email, password) {
|
|
210
|
+
return this.signInWithEmail(email, password);
|
|
211
|
+
}
|
|
212
|
+
async signUp(email, password, options) {
|
|
213
|
+
try {
|
|
214
|
+
const response = await this.apiClient.post("/auth/signup", {
|
|
215
|
+
email,
|
|
216
|
+
password,
|
|
217
|
+
displayName: options?.displayName,
|
|
218
|
+
phoneNumber: options?.phoneNumber,
|
|
219
|
+
applicationId: this.applicationId
|
|
220
|
+
}, { skipAuth: true });
|
|
221
|
+
this._currentUser = response.user;
|
|
222
|
+
this.refreshToken = response.refreshToken ?? null;
|
|
223
|
+
this._memberships = response.memberships || [];
|
|
224
|
+
this.setAuthToken(response.token);
|
|
225
|
+
this.persistSession(response.user, response.token, response.refreshToken, response.memberships);
|
|
226
|
+
this.onAuthStateChange(response.user);
|
|
227
|
+
this.notifyAuthStateCallbacks(response.user);
|
|
228
|
+
return response.user;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error instanceof MAKEMINDError) {
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
throw new MAKEMINDError("AUTH_ERROR", "Failed to sign up");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async signOut() {
|
|
237
|
+
try {
|
|
238
|
+
await this.apiClient.post("/auth/signout", {});
|
|
239
|
+
} catch {
|
|
240
|
+
} finally {
|
|
241
|
+
this._currentUser = null;
|
|
242
|
+
this._memberships = [];
|
|
243
|
+
this.refreshToken = null;
|
|
244
|
+
this.setAuthToken(null);
|
|
245
|
+
this.clearStoredSession();
|
|
246
|
+
this.onAuthStateChange(null);
|
|
247
|
+
this.notifyAuthStateCallbacks(null);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async sendPasswordResetEmail(email) {
|
|
251
|
+
await this.apiClient.post("/auth/password/reset", {
|
|
252
|
+
email,
|
|
253
|
+
applicationId: this.applicationId
|
|
254
|
+
}, { skipAuth: true });
|
|
255
|
+
}
|
|
256
|
+
onAuthStateChanged(callback) {
|
|
257
|
+
this.authStateCallbacks.add(callback);
|
|
258
|
+
callback(this._currentUser);
|
|
259
|
+
return () => {
|
|
260
|
+
this.authStateCallbacks.delete(callback);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
notifyAuthStateCallbacks(user) {
|
|
264
|
+
this.authStateCallbacks.forEach((callback) => {
|
|
265
|
+
try {
|
|
266
|
+
callback(user);
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async refreshAuthToken() {
|
|
272
|
+
try {
|
|
273
|
+
const response = await this.apiClient.post(
|
|
274
|
+
"/auth/refresh",
|
|
275
|
+
this.refreshToken ? { refreshToken: this.refreshToken } : {},
|
|
276
|
+
{ skipAuth: true }
|
|
277
|
+
);
|
|
278
|
+
this.refreshToken = response.refreshToken ?? null;
|
|
279
|
+
this.setAuthToken(response.token);
|
|
280
|
+
if (response.memberships) {
|
|
281
|
+
this._memberships = response.memberships;
|
|
282
|
+
}
|
|
283
|
+
if (response.user) {
|
|
284
|
+
this._currentUser = response.user;
|
|
285
|
+
this.onAuthStateChange(response.user);
|
|
286
|
+
this.notifyAuthStateCallbacks(response.user);
|
|
287
|
+
this.persistSession(response.user, response.token, response.refreshToken, response.memberships);
|
|
288
|
+
} else if (this._currentUser) {
|
|
289
|
+
this.persistSession(this._currentUser, response.token, response.refreshToken, response.memberships);
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
await this.signOut();
|
|
293
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "Session expired");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async switchTenant(tenantId) {
|
|
297
|
+
await this.apiClient.dispatch(
|
|
298
|
+
"switchTenant",
|
|
299
|
+
{ tenantId }
|
|
300
|
+
);
|
|
301
|
+
await this.refreshAuthToken();
|
|
302
|
+
if (!this._currentUser) {
|
|
303
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "Session lost after tenant switch");
|
|
304
|
+
}
|
|
305
|
+
return this._currentUser;
|
|
306
|
+
}
|
|
307
|
+
// Returns tenants the authenticated user belongs to.
|
|
308
|
+
// Backend dispatch 'authGetUserTenants' → { tenants: Record<string, unknown>[] }
|
|
309
|
+
async getUserMemberships() {
|
|
310
|
+
const response = await this.apiClient.dispatch(
|
|
311
|
+
"authGetUserTenants"
|
|
312
|
+
);
|
|
313
|
+
return (response.tenants || []).map((t) => ({
|
|
314
|
+
tenantId: t["id"],
|
|
315
|
+
tenantName: t["displayName"] ?? t["name"],
|
|
316
|
+
roles: t["roles"] ?? [],
|
|
317
|
+
status: t["status"] ?? "active"
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
// Permission helpers
|
|
321
|
+
hasScope(scope) {
|
|
322
|
+
if (!this._currentUser) return false;
|
|
323
|
+
return this._currentUser.scopes.includes(scope) || this._currentUser.scopes.includes("*");
|
|
324
|
+
}
|
|
325
|
+
hasRole(role) {
|
|
326
|
+
if (!this._currentUser) return false;
|
|
327
|
+
return this._currentUser.roles.includes(role);
|
|
328
|
+
}
|
|
329
|
+
hasAnyRole(roles) {
|
|
330
|
+
if (!this._currentUser) return false;
|
|
331
|
+
return roles.some((role) => this._currentUser.roles.includes(role));
|
|
332
|
+
}
|
|
333
|
+
hasAllScopes(scopes) {
|
|
334
|
+
if (!this._currentUser) return false;
|
|
335
|
+
if (this._currentUser.scopes.includes("*")) return true;
|
|
336
|
+
return scopes.every((scope) => this._currentUser.scopes.includes(scope));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get the current ID token for use with external services
|
|
340
|
+
*/
|
|
341
|
+
async getIdToken(forceRefresh = false) {
|
|
342
|
+
if (!this._currentUser) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (forceRefresh) {
|
|
346
|
+
await this.refreshAuthToken();
|
|
347
|
+
}
|
|
348
|
+
if (typeof localStorage !== "undefined") {
|
|
349
|
+
return localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Update the current user's password.
|
|
355
|
+
* Backend dispatch 'updatePassword' → data { currentPassword, newPassword }.
|
|
356
|
+
*/
|
|
357
|
+
async updatePassword(currentPassword, newPassword) {
|
|
358
|
+
if (!this._currentUser) {
|
|
359
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No user signed in");
|
|
360
|
+
}
|
|
361
|
+
await this.apiClient.dispatch("updatePassword", { currentPassword, newPassword });
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Send email verification.
|
|
365
|
+
* Backend dispatch 'resendVerificationEmail' — uid resolved from auth context, no payload needed.
|
|
366
|
+
*/
|
|
367
|
+
async sendEmailVerification() {
|
|
368
|
+
if (!this._currentUser) {
|
|
369
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No user signed in");
|
|
370
|
+
}
|
|
371
|
+
await this.apiClient.dispatch("resendVerificationEmail");
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Verify email with code
|
|
375
|
+
*/
|
|
376
|
+
async verifyEmail(code) {
|
|
377
|
+
await this.apiClient.post("/auth/verify-email", {
|
|
378
|
+
code,
|
|
379
|
+
applicationId: this.applicationId
|
|
380
|
+
}, { skipAuth: true });
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Confirm password reset with code
|
|
384
|
+
*/
|
|
385
|
+
async confirmPasswordReset(code, newPassword) {
|
|
386
|
+
await this.apiClient.post("/auth/confirm-reset", {
|
|
387
|
+
code,
|
|
388
|
+
newPassword,
|
|
389
|
+
applicationId: this.applicationId
|
|
390
|
+
}, { skipAuth: true });
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* OAuth redirect-based sign-in is NOT supported by this backend.
|
|
394
|
+
* The backend has no /oauth/initiate or /oauth/callback routes.
|
|
395
|
+
* Use signInWithOAuthToken() with a provider-issued ID token instead,
|
|
396
|
+
* or use the Firebase Client SDK directly for OAuth redirect flows.
|
|
397
|
+
*/
|
|
398
|
+
async signInWithOAuth(_provider) {
|
|
399
|
+
throw new MAKEMINDError(
|
|
400
|
+
"NOT_SUPPORTED",
|
|
401
|
+
"OAuth redirect flow is not supported. Use signInWithOAuthToken() with a provider ID token, or the Firebase Client SDK."
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* OAuth redirect callback handling is NOT supported by this backend.
|
|
406
|
+
* Use signInWithOAuthToken() with a provider-issued ID token instead,
|
|
407
|
+
* or use the Firebase Client SDK directly for OAuth redirect flows.
|
|
408
|
+
*/
|
|
409
|
+
async handleOAuthCallback(_code, _state) {
|
|
410
|
+
throw new MAKEMINDError(
|
|
411
|
+
"NOT_SUPPORTED",
|
|
412
|
+
"OAuth redirect callback is not supported. Use signInWithOAuthToken() with a provider ID token, or the Firebase Client SDK."
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Sign in with OAuth ID token directly (for native apps)
|
|
417
|
+
* Use this when you have already obtained the ID token from the OAuth provider
|
|
418
|
+
*/
|
|
419
|
+
async signInWithOAuthToken(provider, idToken) {
|
|
420
|
+
try {
|
|
421
|
+
const response = await this.apiClient.post("/auth/oauth/token", {
|
|
422
|
+
provider,
|
|
423
|
+
idToken,
|
|
424
|
+
applicationId: this.applicationId
|
|
425
|
+
}, { skipAuth: true });
|
|
426
|
+
this._currentUser = response.user;
|
|
427
|
+
this.refreshToken = response.refreshToken ?? null;
|
|
428
|
+
this._memberships = response.memberships || [];
|
|
429
|
+
this.setAuthToken(response.token);
|
|
430
|
+
this.persistSession(response.user, response.token, response.refreshToken, response.memberships);
|
|
431
|
+
this.onAuthStateChange(response.user);
|
|
432
|
+
this.notifyAuthStateCallbacks(response.user);
|
|
433
|
+
return response.user;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error instanceof MAKEMINDError) {
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
throw new MAKEMINDError("AUTH_ERROR", "OAuth token authentication failed");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* OAuth redirect-based account linking is NOT supported by this backend.
|
|
443
|
+
* The backend has no /oauth/link/initiate route.
|
|
444
|
+
* Use the Firebase Client SDK linkWithRedirect() or linkWithPopup() instead.
|
|
445
|
+
*/
|
|
446
|
+
async linkWithOAuth(_provider) {
|
|
447
|
+
throw new MAKEMINDError(
|
|
448
|
+
"NOT_SUPPORTED",
|
|
449
|
+
"OAuth redirect link flow is not supported. Use the Firebase Client SDK linkWithRedirect() or linkWithPopup()."
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* OAuth redirect link callback is NOT supported by this backend.
|
|
454
|
+
* The backend has no /oauth/link/callback route.
|
|
455
|
+
* Use the Firebase Client SDK for OAuth redirect link flows.
|
|
456
|
+
*/
|
|
457
|
+
async handleOAuthLinkCallback(_code, _state) {
|
|
458
|
+
throw new MAKEMINDError(
|
|
459
|
+
"NOT_SUPPORTED",
|
|
460
|
+
"OAuth redirect link callback is not supported. Use the Firebase Client SDK for OAuth link flows."
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Unlink OAuth provider from current account
|
|
465
|
+
*/
|
|
466
|
+
async unlinkOAuth(provider) {
|
|
467
|
+
if (!this._currentUser) {
|
|
468
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No user signed in");
|
|
469
|
+
}
|
|
470
|
+
await this.apiClient.post("/auth/oauth/unlink", {
|
|
471
|
+
provider
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get linked OAuth providers for current user
|
|
476
|
+
*/
|
|
477
|
+
async getLinkedProviders() {
|
|
478
|
+
if (!this._currentUser) {
|
|
479
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No user signed in");
|
|
480
|
+
}
|
|
481
|
+
const response = await this.apiClient.get("/auth/oauth/providers");
|
|
482
|
+
return response.providers;
|
|
483
|
+
}
|
|
484
|
+
// Update the current user in the session (e.g., after tenant creation)
|
|
485
|
+
updateCurrentUser(userData, membership) {
|
|
486
|
+
const user = {
|
|
487
|
+
uid: userData.uid,
|
|
488
|
+
email: userData.email,
|
|
489
|
+
displayName: userData.displayName || null,
|
|
490
|
+
photoURL: this._currentUser?.photoURL || null,
|
|
491
|
+
tenantId: userData.tenantId,
|
|
492
|
+
roles: userData.roles,
|
|
493
|
+
scopes: userData.scopes
|
|
494
|
+
};
|
|
495
|
+
this._currentUser = user;
|
|
496
|
+
this.onAuthStateChange(user);
|
|
497
|
+
this.notifyAuthStateCallbacks(user);
|
|
498
|
+
if (membership) {
|
|
499
|
+
const existingIndex = this._memberships.findIndex((m) => m.tenantId === membership.tenantId);
|
|
500
|
+
if (existingIndex >= 0) {
|
|
501
|
+
this._memberships[existingIndex] = membership;
|
|
502
|
+
} else {
|
|
503
|
+
this._memberships.push(membership);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const storedToken = typeof localStorage !== "undefined" ? localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN) : null;
|
|
507
|
+
if (storedToken) {
|
|
508
|
+
this.persistSession(user, storedToken, this.refreshToken ?? "", this._memberships);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/services/data.ts
|
|
514
|
+
var MAKEMINDData = class {
|
|
515
|
+
constructor(config) {
|
|
516
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
517
|
+
this.apiClient = config.apiClient;
|
|
518
|
+
this.applicationId = config.applicationId;
|
|
519
|
+
this.getTenantId = config.tenantId;
|
|
520
|
+
}
|
|
521
|
+
get tenantId() {
|
|
522
|
+
const id = this.getTenantId();
|
|
523
|
+
if (!id) {
|
|
524
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No tenant context set");
|
|
525
|
+
}
|
|
526
|
+
return id;
|
|
527
|
+
}
|
|
528
|
+
async query(options) {
|
|
529
|
+
const filters = options.where?.map(({ field, op, value }) => ({
|
|
530
|
+
field,
|
|
531
|
+
operator: op,
|
|
532
|
+
value
|
|
533
|
+
}));
|
|
534
|
+
const res = await this.apiClient.dispatch("dataQuery", {
|
|
535
|
+
collection: options.collection,
|
|
536
|
+
// The service applies field-level conditions from `filters` (it adds the
|
|
537
|
+
// `data.` prefix automatically); `where` is reserved for $and/$or logic.
|
|
538
|
+
filters,
|
|
539
|
+
sort: options.orderBy,
|
|
540
|
+
limit: options.limit,
|
|
541
|
+
startAfter: options.startAfter
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
data: res.results ?? [],
|
|
545
|
+
metadata: { hasMore: res.hasMore ?? false, lastDocId: res.nextCursor }
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async get(options) {
|
|
549
|
+
try {
|
|
550
|
+
const response = await this.apiClient.get(
|
|
551
|
+
`/data/${options.collection}/${options.documentId}`
|
|
552
|
+
);
|
|
553
|
+
return response;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (error instanceof MAKEMINDError && error.code === "NOT_FOUND") {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async create(options) {
|
|
562
|
+
const response = await this.apiClient.post(`/data/${options.collection}`, {
|
|
563
|
+
...options.data,
|
|
564
|
+
documentId: options.documentId
|
|
565
|
+
});
|
|
566
|
+
return response;
|
|
567
|
+
}
|
|
568
|
+
async update(options) {
|
|
569
|
+
await this.apiClient.patch(`/data/${options.collection}/${options.documentId}`, options.data);
|
|
570
|
+
}
|
|
571
|
+
async delete(options) {
|
|
572
|
+
await this.apiClient.delete(`/data/${options.collection}/${options.documentId}`);
|
|
573
|
+
}
|
|
574
|
+
onSnapshot(options, callback) {
|
|
575
|
+
const subscriptionId = `doc:${options.collection}:${options.documentId}`;
|
|
576
|
+
this.createSubscription(
|
|
577
|
+
`/data/subscribe/${options.collection}/${options.documentId}`,
|
|
578
|
+
subscriptionId,
|
|
579
|
+
callback
|
|
580
|
+
);
|
|
581
|
+
return () => {
|
|
582
|
+
this.closeSubscription(subscriptionId);
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
onCollectionSnapshot(options, callback) {
|
|
586
|
+
const subscriptionId = `collection:${options.collection}:${JSON.stringify(options)}`;
|
|
587
|
+
this.createCollectionSubscription(options, subscriptionId, callback);
|
|
588
|
+
return () => {
|
|
589
|
+
this.closeSubscription(subscriptionId);
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
createSubscription(endpoint, subscriptionId, callback) {
|
|
593
|
+
this.closeSubscription(subscriptionId);
|
|
594
|
+
if (typeof EventSource !== "undefined") {
|
|
595
|
+
const url = `${this.apiClient.getBaseUrl()}${endpoint}`;
|
|
596
|
+
const eventSource = new EventSource(url, { withCredentials: true });
|
|
597
|
+
eventSource.onmessage = (event) => {
|
|
598
|
+
try {
|
|
599
|
+
const message = JSON.parse(event.data);
|
|
600
|
+
if (message.type === "data") {
|
|
601
|
+
callback(message.data);
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
eventSource.onerror = () => {
|
|
607
|
+
};
|
|
608
|
+
this.subscriptions.set(subscriptionId, eventSource);
|
|
609
|
+
} else {
|
|
610
|
+
this.pollForUpdates(endpoint, subscriptionId, callback);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
createCollectionSubscription(options, subscriptionId, callback) {
|
|
614
|
+
this.closeSubscription(subscriptionId);
|
|
615
|
+
const poll = async () => {
|
|
616
|
+
try {
|
|
617
|
+
const result = await this.query(options);
|
|
618
|
+
callback(result.data);
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
poll();
|
|
623
|
+
const intervalId = setInterval(poll, 5e3);
|
|
624
|
+
this.subscriptions.set(subscriptionId, {
|
|
625
|
+
close: () => clearInterval(intervalId)
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
pollForUpdates(endpoint, subscriptionId, callback) {
|
|
629
|
+
const poll = async () => {
|
|
630
|
+
try {
|
|
631
|
+
const data = await this.apiClient.get(endpoint);
|
|
632
|
+
callback(data);
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
poll();
|
|
637
|
+
const intervalId = setInterval(poll, 5e3);
|
|
638
|
+
this.subscriptions.set(subscriptionId, {
|
|
639
|
+
close: () => clearInterval(intervalId)
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
closeSubscription(subscriptionId) {
|
|
643
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
644
|
+
if (subscription) {
|
|
645
|
+
if (subscription instanceof EventSource) {
|
|
646
|
+
subscription.close();
|
|
647
|
+
} else if (typeof subscription.close === "function") {
|
|
648
|
+
subscription.close();
|
|
649
|
+
}
|
|
650
|
+
this.subscriptions.delete(subscriptionId);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// collectionGroupQuery: no backend route or onCall handler exists for cross-
|
|
654
|
+
// collection group queries. REST /data/collection-group-query is not registered
|
|
655
|
+
// and no `dataCollectionGroupQuery` onCall is exported from data-platform.
|
|
656
|
+
// Callers must use `query()` against a specific collection. This method is
|
|
657
|
+
// intentionally left as a not-implemented guard so TS callers compile and get
|
|
658
|
+
// a clear runtime error rather than a silent 404 or wrong-handler response.
|
|
659
|
+
async collectionGroupQuery(_options) {
|
|
660
|
+
throw new MAKEMINDError(
|
|
661
|
+
"NOT_IMPLEMENTED",
|
|
662
|
+
"collectionGroupQuery is not yet supported by the backend. Use query() against a specific collection."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
// Convenience methods with tenant isolation
|
|
666
|
+
async queryTenant(collectionName, additionalConditions, orderByOptions, limitCount) {
|
|
667
|
+
const conditions = [
|
|
668
|
+
...additionalConditions || []
|
|
669
|
+
];
|
|
670
|
+
return this.query({
|
|
671
|
+
collection: collectionName,
|
|
672
|
+
where: conditions,
|
|
673
|
+
orderBy: orderByOptions,
|
|
674
|
+
limit: limitCount
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
// Batch operations
|
|
678
|
+
//
|
|
679
|
+
// All three batch methods route through the `dataBatchWrite` onCall handler
|
|
680
|
+
// (dispatch op `dataBatchWrite`). The backend BatchWriteInput shape is:
|
|
681
|
+
// { operations: [{ type, collection?, id?, data? }] }
|
|
682
|
+
// There are no separate REST PATCH /batch or POST /batch-delete routes.
|
|
683
|
+
async batchCreate(collection, documents) {
|
|
684
|
+
const operations = documents.map(({ data, documentId }) => ({
|
|
685
|
+
type: "create",
|
|
686
|
+
collection,
|
|
687
|
+
...documentId ? { id: documentId } : {},
|
|
688
|
+
data
|
|
689
|
+
}));
|
|
690
|
+
const response = await this.apiClient.dispatch(
|
|
691
|
+
"dataBatchWrite",
|
|
692
|
+
{ operations }
|
|
693
|
+
);
|
|
694
|
+
return response.results.map((r) => ({ id: r.id, data: {} }));
|
|
695
|
+
}
|
|
696
|
+
async batchUpdate(collection, updates) {
|
|
697
|
+
const operations = updates.map(({ documentId, data }) => ({
|
|
698
|
+
type: "update",
|
|
699
|
+
id: documentId,
|
|
700
|
+
data
|
|
701
|
+
}));
|
|
702
|
+
await this.apiClient.dispatch("dataBatchWrite", { operations });
|
|
703
|
+
}
|
|
704
|
+
async batchDelete(collection, documentIds) {
|
|
705
|
+
const operations = documentIds.map((id) => ({
|
|
706
|
+
type: "delete",
|
|
707
|
+
id
|
|
708
|
+
}));
|
|
709
|
+
await this.apiClient.dispatch("dataBatchWrite", { operations });
|
|
710
|
+
}
|
|
711
|
+
// Export/Import operations
|
|
712
|
+
//
|
|
713
|
+
// Backend uses async job pattern via onCall handlers:
|
|
714
|
+
// dataExportCollection → returns { exportId, status }
|
|
715
|
+
// dataImportCollection → returns { importId, status } (sourcePath required)
|
|
716
|
+
// dataGetExportStatus → returns full ExportJob by { exportId }
|
|
717
|
+
// dataGetImportStatus → returns full ImportJob by { importId }
|
|
718
|
+
//
|
|
719
|
+
// There are no flat REST routes /data/export or /data/import; the collection-
|
|
720
|
+
// scoped synchronous REST routes (POST /:collection/export, POST /:collection/import)
|
|
721
|
+
// exist for simple inline operations but do not produce jobIds. All job-based
|
|
722
|
+
// export/import goes through dispatch.
|
|
723
|
+
//
|
|
724
|
+
// Import requires the caller to upload the file to Cloud Storage first (under
|
|
725
|
+
// imports/<tenantId>/ or uploads/<tenantId>/) and pass the GCS path as
|
|
726
|
+
// `sourcePath`. The `data` parameter accepted here must be a GCS path string;
|
|
727
|
+
// array-of-records import is not supported by the onCall handler.
|
|
728
|
+
async exportCollection(options) {
|
|
729
|
+
const res = await this.apiClient.dispatch(
|
|
730
|
+
"dataExportCollection",
|
|
731
|
+
{
|
|
732
|
+
collection: options.collection,
|
|
733
|
+
format: options.format,
|
|
734
|
+
filter: options.filter
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
return { jobId: res.exportId, status: res.status };
|
|
738
|
+
}
|
|
739
|
+
async importCollection(options) {
|
|
740
|
+
const res = await this.apiClient.dispatch(
|
|
741
|
+
"dataImportCollection",
|
|
742
|
+
{
|
|
743
|
+
collection: options.collection,
|
|
744
|
+
sourcePath: options.sourcePath,
|
|
745
|
+
format: options.format,
|
|
746
|
+
upsert: options.upsert
|
|
747
|
+
}
|
|
748
|
+
);
|
|
749
|
+
return { jobId: res.importId, status: res.status };
|
|
750
|
+
}
|
|
751
|
+
async getExportStatus(jobId) {
|
|
752
|
+
const res = await this.apiClient.dispatch("dataGetExportStatus", { exportId: jobId });
|
|
753
|
+
return {
|
|
754
|
+
jobId: res.id,
|
|
755
|
+
status: res.status,
|
|
756
|
+
documentCount: res.documentCount,
|
|
757
|
+
downloadUrl: res.downloadUrl,
|
|
758
|
+
error: res.error,
|
|
759
|
+
completedAt: res.completedAt
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async getImportStatus(jobId) {
|
|
763
|
+
const res = await this.apiClient.dispatch("dataGetImportStatus", { importId: jobId });
|
|
764
|
+
return {
|
|
765
|
+
jobId: res.id,
|
|
766
|
+
status: res.status,
|
|
767
|
+
totalRecords: res.totalRecords,
|
|
768
|
+
processedRecords: res.processedRecords,
|
|
769
|
+
skippedRecords: res.skippedRecords,
|
|
770
|
+
failedRecords: res.failedRecords,
|
|
771
|
+
error: res.error,
|
|
772
|
+
completedAt: res.completedAt
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
// Aggregation operations
|
|
776
|
+
//
|
|
777
|
+
// Backend onCall: `dataAggregate` — accepts AggregateInput:
|
|
778
|
+
// { collection, filters: [{ field, operator, value }], aggregations }
|
|
779
|
+
// No REST route /data/aggregate exists.
|
|
780
|
+
async aggregate(options) {
|
|
781
|
+
const filters = options.filters?.map(({ field, op, value }) => ({
|
|
782
|
+
field,
|
|
783
|
+
operator: op,
|
|
784
|
+
value
|
|
785
|
+
}));
|
|
786
|
+
return this.apiClient.dispatch("dataAggregate", {
|
|
787
|
+
collection: options.collection,
|
|
788
|
+
filters,
|
|
789
|
+
aggregations: options.aggregations
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
// Full-text search
|
|
793
|
+
//
|
|
794
|
+
// Backend onCall: `dataSearch` — accepts:
|
|
795
|
+
// { collection, query, fields, limit? }
|
|
796
|
+
// No REST route /data/search exists.
|
|
797
|
+
async search(options) {
|
|
798
|
+
return this.apiClient.dispatch("dataSearch", {
|
|
799
|
+
collection: options.collection,
|
|
800
|
+
query: options.query,
|
|
801
|
+
fields: options.fields,
|
|
802
|
+
limit: options.limit
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/services/logic.ts
|
|
808
|
+
var MAKEMINDLogic = class {
|
|
809
|
+
constructor(config) {
|
|
810
|
+
this.apiClient = config.apiClient;
|
|
811
|
+
this.getTenantId = config.tenantId;
|
|
812
|
+
}
|
|
813
|
+
// Generic transition for any workflow
|
|
814
|
+
async transition(options) {
|
|
815
|
+
const response = await this.apiClient.post("/logic/transition", {
|
|
816
|
+
...options,
|
|
817
|
+
tenantId: this.getTenantId()
|
|
818
|
+
});
|
|
819
|
+
return response;
|
|
820
|
+
}
|
|
821
|
+
// Call a logic function directly
|
|
822
|
+
async call(functionName, input) {
|
|
823
|
+
const response = await this.apiClient.post("/logic/call", {
|
|
824
|
+
functionName,
|
|
825
|
+
input,
|
|
826
|
+
tenantId: this.getTenantId()
|
|
827
|
+
});
|
|
828
|
+
return response;
|
|
829
|
+
}
|
|
830
|
+
// Execute a flow
|
|
831
|
+
async executeFlow(flowId, input, options) {
|
|
832
|
+
const response = await this.apiClient.post("/logic/execute", {
|
|
833
|
+
flowId,
|
|
834
|
+
input,
|
|
835
|
+
idempotencyKey: options?.idempotencyKey,
|
|
836
|
+
tenantId: this.getTenantId()
|
|
837
|
+
});
|
|
838
|
+
return response;
|
|
839
|
+
}
|
|
840
|
+
// Get execution status
|
|
841
|
+
async getExecution(executionId) {
|
|
842
|
+
return this.apiClient.get(`/logic/executions/${executionId}`);
|
|
843
|
+
}
|
|
844
|
+
// List executions
|
|
845
|
+
async listExecutions(options) {
|
|
846
|
+
const params = new URLSearchParams();
|
|
847
|
+
if (options?.flowId) params.append("flowId", options.flowId);
|
|
848
|
+
if (options?.status) params.append("status", options.status);
|
|
849
|
+
if (options?.limit) params.append("limit", String(options.limit));
|
|
850
|
+
if (options?.startAfter) params.append("startAfter", options.startAfter);
|
|
851
|
+
return this.apiClient.get(`/logic/executions?${params.toString()}`);
|
|
852
|
+
}
|
|
853
|
+
// Send signal to a waiting workflow
|
|
854
|
+
async sendSignal(executionId, signalName, payload) {
|
|
855
|
+
return this.apiClient.post(`/logic/executions/${executionId}/signal`, {
|
|
856
|
+
signalName,
|
|
857
|
+
payload
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Cancel a running workflow execution
|
|
862
|
+
*/
|
|
863
|
+
async cancelWorkflow(executionId, reason) {
|
|
864
|
+
return this.apiClient.post(`/logic/executions/${executionId}/cancel`, {
|
|
865
|
+
reason
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Retry a failed workflow execution
|
|
870
|
+
*/
|
|
871
|
+
async retryWorkflow(executionId) {
|
|
872
|
+
return this.apiClient.post(`/logic/executions/${executionId}/retry`, {});
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Pause a running workflow execution
|
|
876
|
+
*/
|
|
877
|
+
async pauseWorkflow(executionId) {
|
|
878
|
+
return this.apiClient.post(`/logic/executions/${executionId}/pause`, {});
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Resume a paused workflow execution
|
|
882
|
+
*/
|
|
883
|
+
async resumeWorkflow(executionId) {
|
|
884
|
+
return this.apiClient.post(`/logic/executions/${executionId}/resume`, {});
|
|
885
|
+
}
|
|
886
|
+
// ECO (Engineering Change Order) workflow helpers
|
|
887
|
+
async ecoTransition(ecoId, action, data) {
|
|
888
|
+
return this.transition({
|
|
889
|
+
workflowName: "eco",
|
|
890
|
+
documentId: ecoId,
|
|
891
|
+
action,
|
|
892
|
+
data
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
// NCR (Non-Conformance Report) workflow helpers
|
|
896
|
+
async ncrTransition(issueId, action, data) {
|
|
897
|
+
return this.transition({
|
|
898
|
+
workflowName: "ncr",
|
|
899
|
+
documentId: issueId,
|
|
900
|
+
action,
|
|
901
|
+
data
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
// Bid workflow helpers
|
|
905
|
+
async bidTransition(requestId, bidId, action, data) {
|
|
906
|
+
return this.transition({
|
|
907
|
+
workflowName: "bid",
|
|
908
|
+
documentId: `${requestId}:${bidId}`,
|
|
909
|
+
action,
|
|
910
|
+
data
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
// Gate approval
|
|
914
|
+
async gateApproval(productId, gate, action, comment) {
|
|
915
|
+
return this.transition({
|
|
916
|
+
workflowName: "gate",
|
|
917
|
+
documentId: productId,
|
|
918
|
+
action: `${gate}_${action}`,
|
|
919
|
+
data: comment ? { comment } : void 0
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
// Document access with watermark
|
|
923
|
+
async accessDocument(documentId, action) {
|
|
924
|
+
return this.apiClient.post("/logic/document-access", {
|
|
925
|
+
documentId,
|
|
926
|
+
action,
|
|
927
|
+
tenantId: this.getTenantId()
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
// Flow management
|
|
931
|
+
async createFlow(flow) {
|
|
932
|
+
return this.apiClient.post("/logic/flows", {
|
|
933
|
+
...flow,
|
|
934
|
+
tenantId: this.getTenantId()
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async updateFlow(flowId, updates) {
|
|
938
|
+
await this.apiClient.patch(`/logic/flows/${flowId}`, updates);
|
|
939
|
+
}
|
|
940
|
+
async publishFlow(flowId) {
|
|
941
|
+
await this.apiClient.post(`/logic/flows/${flowId}/publish`, {});
|
|
942
|
+
}
|
|
943
|
+
async archiveFlow(flowId) {
|
|
944
|
+
await this.apiClient.post(`/logic/flows/${flowId}/archive`, {});
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* A page of the tenant's flows.
|
|
948
|
+
*
|
|
949
|
+
* Took no arguments and returned a bare array, so a caller wanting one
|
|
950
|
+
* state — published flows, to offer as event handlers — pulled every state
|
|
951
|
+
* and filtered locally. The server has always been able to filter; asking
|
|
952
|
+
* it to means the page is spent on rows the caller wants.
|
|
953
|
+
*
|
|
954
|
+
* `hasMore` is returned rather than implied. A full page and a truncated
|
|
955
|
+
* one are the same array, and a caller that cannot tell them apart quietly
|
|
956
|
+
* drops whatever did not fit.
|
|
957
|
+
*/
|
|
958
|
+
async listFlows(params = {}) {
|
|
959
|
+
const query = new URLSearchParams();
|
|
960
|
+
if (params.status) query.set("status", params.status);
|
|
961
|
+
if (params.mode) query.set("mode", params.mode);
|
|
962
|
+
if (params.limit !== void 0) query.set("limit", String(params.limit));
|
|
963
|
+
if (params.cursor) query.set("cursor", params.cursor);
|
|
964
|
+
const suffix = query.toString();
|
|
965
|
+
const response = await this.apiClient.get(`/logic/flows${suffix ? `?${suffix}` : ""}`);
|
|
966
|
+
return {
|
|
967
|
+
flows: response.flows,
|
|
968
|
+
hasMore: response.hasMore === true,
|
|
969
|
+
nextCursor: response.nextCursor
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
async getFlow(flowId) {
|
|
973
|
+
return this.apiClient.get(`/logic/flows/${flowId}`);
|
|
974
|
+
}
|
|
975
|
+
async testFlow(flowId, testInput) {
|
|
976
|
+
return this.apiClient.post(`/logic/flows/${flowId}/test`, {
|
|
977
|
+
input: testInput
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
async cloneFlow(flowId, newName) {
|
|
981
|
+
return this.apiClient.post(`/logic/flows/${flowId}/clone`, {
|
|
982
|
+
name: newName
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
// Variables management
|
|
986
|
+
async listVariables() {
|
|
987
|
+
const response = await this.apiClient.get("/logic/variables");
|
|
988
|
+
return response.variables;
|
|
989
|
+
}
|
|
990
|
+
async getVariable(variableId) {
|
|
991
|
+
return this.apiClient.get(`/logic/variables/${variableId}`);
|
|
992
|
+
}
|
|
993
|
+
async createVariable(data) {
|
|
994
|
+
return this.apiClient.post("/logic/variables", {
|
|
995
|
+
...data,
|
|
996
|
+
tenantId: this.getTenantId()
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
async updateVariable(variableId, data) {
|
|
1000
|
+
await this.apiClient.patch(`/logic/variables/${variableId}`, data);
|
|
1001
|
+
}
|
|
1002
|
+
async deleteVariable(variableId) {
|
|
1003
|
+
await this.apiClient.delete(`/logic/variables/${variableId}`);
|
|
1004
|
+
}
|
|
1005
|
+
// Templates management
|
|
1006
|
+
async listTemplates() {
|
|
1007
|
+
const response = await this.apiClient.get("/logic/templates");
|
|
1008
|
+
return response.templates;
|
|
1009
|
+
}
|
|
1010
|
+
async getTemplate(templateId) {
|
|
1011
|
+
return this.apiClient.get(`/logic/templates/${templateId}`);
|
|
1012
|
+
}
|
|
1013
|
+
async createFlowFromTemplate(templateId, data) {
|
|
1014
|
+
return this.apiClient.post(`/logic/templates/${templateId}/instantiate`, {
|
|
1015
|
+
...data,
|
|
1016
|
+
tenantId: this.getTenantId()
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
// Webhooks management
|
|
1020
|
+
async listWebhooks() {
|
|
1021
|
+
const response = await this.apiClient.get("/logic/webhooks");
|
|
1022
|
+
return response.webhooks;
|
|
1023
|
+
}
|
|
1024
|
+
async createWebhook(data) {
|
|
1025
|
+
return this.apiClient.post("/logic/webhooks", {
|
|
1026
|
+
...data,
|
|
1027
|
+
tenantId: this.getTenantId()
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
async deleteWebhook(webhookId) {
|
|
1031
|
+
await this.apiClient.delete(`/logic/webhooks/${webhookId}`);
|
|
1032
|
+
}
|
|
1033
|
+
async rotateWebhookSecret(webhookId) {
|
|
1034
|
+
return this.apiClient.post(`/logic/webhooks/${webhookId}/rotate-secret`, {});
|
|
1035
|
+
}
|
|
1036
|
+
// Approvals management
|
|
1037
|
+
async listPendingApprovals() {
|
|
1038
|
+
const response = await this.apiClient.get("/logic/approvals/pending");
|
|
1039
|
+
return response.approvals;
|
|
1040
|
+
}
|
|
1041
|
+
async approveRequest(approvalId, comment) {
|
|
1042
|
+
await this.apiClient.post(`/logic/approvals/${approvalId}/approve`, { comment });
|
|
1043
|
+
}
|
|
1044
|
+
async rejectRequest(approvalId, reason) {
|
|
1045
|
+
await this.apiClient.post(`/logic/approvals/${approvalId}/reject`, { reason });
|
|
1046
|
+
}
|
|
1047
|
+
// Data Triggers management
|
|
1048
|
+
async listDataTriggers() {
|
|
1049
|
+
const response = await this.apiClient.get("/logic/data-triggers");
|
|
1050
|
+
return response.triggers;
|
|
1051
|
+
}
|
|
1052
|
+
async createDataTrigger(data) {
|
|
1053
|
+
return this.apiClient.post("/logic/data-triggers", {
|
|
1054
|
+
...data,
|
|
1055
|
+
tenantId: this.getTenantId()
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
async deleteDataTrigger(triggerId) {
|
|
1059
|
+
await this.apiClient.delete(`/logic/data-triggers/${triggerId}`);
|
|
1060
|
+
}
|
|
1061
|
+
async toggleDataTrigger(triggerId, isActive) {
|
|
1062
|
+
await this.apiClient.patch(`/logic/data-triggers/${triggerId}`, { isActive });
|
|
1063
|
+
}
|
|
1064
|
+
// AI Assistant
|
|
1065
|
+
async generateFlowFromPrompt(prompt) {
|
|
1066
|
+
return this.apiClient.post("/logic/ai/generate", {
|
|
1067
|
+
prompt,
|
|
1068
|
+
tenantId: this.getTenantId()
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
async explainFlow(flowId) {
|
|
1072
|
+
return this.apiClient.post(`/logic/flows/${flowId}/explain`, {});
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// src/services/tenant.ts
|
|
1077
|
+
var MAKEMINDTenant = class {
|
|
1078
|
+
constructor(config) {
|
|
1079
|
+
this.apiClient = config.apiClient;
|
|
1080
|
+
this.getTenantId = config.tenantId;
|
|
1081
|
+
this.onTenantCreated = config.onTenantCreated;
|
|
1082
|
+
}
|
|
1083
|
+
get tenantId() {
|
|
1084
|
+
const id = this.getTenantId();
|
|
1085
|
+
if (!id) {
|
|
1086
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No tenant context set");
|
|
1087
|
+
}
|
|
1088
|
+
return id;
|
|
1089
|
+
}
|
|
1090
|
+
// Create a new tenant via API Gateway
|
|
1091
|
+
async createTenant(data) {
|
|
1092
|
+
const requestBody = {
|
|
1093
|
+
name: data.name,
|
|
1094
|
+
displayName: data.settings?.displayName || data.name,
|
|
1095
|
+
type: data.settings?.type || "company",
|
|
1096
|
+
mode: data.mode,
|
|
1097
|
+
settings: data.settings
|
|
1098
|
+
};
|
|
1099
|
+
const response = await this.apiClient.post("/tenants", requestBody);
|
|
1100
|
+
if (this.onTenantCreated) {
|
|
1101
|
+
await this.onTenantCreated(response.tenantId, response.customToken);
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
id: response.tenantId,
|
|
1105
|
+
name: data.name,
|
|
1106
|
+
workspaceId: response.workspaceId,
|
|
1107
|
+
status: response.status,
|
|
1108
|
+
user: response.user,
|
|
1109
|
+
customToken: response.customToken
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
// Get current tenant
|
|
1113
|
+
async getCurrent() {
|
|
1114
|
+
try {
|
|
1115
|
+
const response = await this.apiClient.get(`/tenants/${this.tenantId}`);
|
|
1116
|
+
return response;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
if (error instanceof MAKEMINDError && error.code === "NOT_FOUND") {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// Get tenant by ID
|
|
1125
|
+
async getTenant(tenantId) {
|
|
1126
|
+
try {
|
|
1127
|
+
const response = await this.apiClient.get(`/tenants/${tenantId}`);
|
|
1128
|
+
return response;
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
if (error instanceof MAKEMINDError && error.code === "NOT_FOUND") {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// Stream tenant changes (polling-based for API)
|
|
1137
|
+
onTenantSnapshot(callback) {
|
|
1138
|
+
let isSubscribed = true;
|
|
1139
|
+
const poll = async () => {
|
|
1140
|
+
if (!isSubscribed) return;
|
|
1141
|
+
try {
|
|
1142
|
+
const tenant = await this.getCurrent();
|
|
1143
|
+
callback(tenant);
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
if (isSubscribed) {
|
|
1147
|
+
setTimeout(poll, 1e4);
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
poll();
|
|
1151
|
+
return () => {
|
|
1152
|
+
isSubscribed = false;
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
// Update tenant settings.
|
|
1156
|
+
// Backend allowedFields: displayName, settings, tags, metadata, canHostSubtenants, maxSubtenants.
|
|
1157
|
+
// The field 'name' is NOT accepted by the backend PATCH /tenants/:tenantId handler.
|
|
1158
|
+
async updateTenant(updates) {
|
|
1159
|
+
await this.apiClient.patch(`/tenants/${this.tenantId}`, updates);
|
|
1160
|
+
}
|
|
1161
|
+
// Member operations
|
|
1162
|
+
// List tenant members
|
|
1163
|
+
async listMembers() {
|
|
1164
|
+
const response = await this.apiClient.get(
|
|
1165
|
+
`/tenants/${this.tenantId}/members`
|
|
1166
|
+
);
|
|
1167
|
+
return response.members;
|
|
1168
|
+
}
|
|
1169
|
+
// Invite a member.
|
|
1170
|
+
// Backend body: { email, roles: string[] } — uses 'roles' array, not singular 'role'.
|
|
1171
|
+
async inviteMember(data) {
|
|
1172
|
+
await this.apiClient.post(`/tenants/${this.tenantId}/members/invite`, {
|
|
1173
|
+
email: data.email,
|
|
1174
|
+
roles: data.roles
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
// Update member roles.
|
|
1178
|
+
// Backend body: { roles: string[] } — uses 'roles' array, not singular 'role'.
|
|
1179
|
+
async updateMember(userId, updates) {
|
|
1180
|
+
await this.apiClient.patch(`/tenants/${this.tenantId}/members/${userId}`, {
|
|
1181
|
+
roles: updates.roles
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
// Remove a member
|
|
1185
|
+
async removeMember(userId) {
|
|
1186
|
+
await this.apiClient.delete(`/tenants/${this.tenantId}/members/${userId}`);
|
|
1187
|
+
}
|
|
1188
|
+
// Workspace operations
|
|
1189
|
+
// List workspaces
|
|
1190
|
+
async listWorkspaces() {
|
|
1191
|
+
const response = await this.apiClient.dispatch(
|
|
1192
|
+
"listWorkspaces",
|
|
1193
|
+
{}
|
|
1194
|
+
);
|
|
1195
|
+
return response.workspaces;
|
|
1196
|
+
}
|
|
1197
|
+
// Create workspace via gateway dispatch (createWorkspace onCall).
|
|
1198
|
+
// Backend data: { name, displayName, settings?, type? } → returns { workspaceId: string }.
|
|
1199
|
+
async createWorkspace(data) {
|
|
1200
|
+
return this.apiClient.dispatch("createWorkspace", {
|
|
1201
|
+
name: data.name,
|
|
1202
|
+
displayName: data.displayName,
|
|
1203
|
+
settings: data.settings,
|
|
1204
|
+
type: data.type
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
// Update workspace via gateway dispatch (updateWorkspace onCall).
|
|
1208
|
+
// Backend data: { workspaceId, displayName?, settings? } → returns { success: boolean }.
|
|
1209
|
+
async updateWorkspace(workspaceId, updates) {
|
|
1210
|
+
return this.apiClient.dispatch("updateWorkspace", {
|
|
1211
|
+
workspaceId,
|
|
1212
|
+
displayName: updates.displayName,
|
|
1213
|
+
settings: updates.settings
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
// Delete workspace via gateway dispatch (deleteWorkspace onCall).
|
|
1217
|
+
// Backend data: { workspaceId } → returns { success: boolean }.
|
|
1218
|
+
async deleteWorkspace(workspaceId) {
|
|
1219
|
+
return this.apiClient.dispatch("deleteWorkspace", { workspaceId });
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// src/services/messaging.ts
|
|
1224
|
+
var MAKEMINDMessaging = class {
|
|
1225
|
+
constructor(config) {
|
|
1226
|
+
this.apiClient = config.apiClient;
|
|
1227
|
+
this.applicationId = config.applicationId;
|
|
1228
|
+
this.getTenantId = config.tenantId;
|
|
1229
|
+
this.getUserId = config.userId;
|
|
1230
|
+
this.firebaseApp = config.firebaseApp;
|
|
1231
|
+
this.vapidKey = config.vapidKey;
|
|
1232
|
+
}
|
|
1233
|
+
get tenantId() {
|
|
1234
|
+
const id = this.getTenantId();
|
|
1235
|
+
if (!id) {
|
|
1236
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No tenant context set");
|
|
1237
|
+
}
|
|
1238
|
+
return id;
|
|
1239
|
+
}
|
|
1240
|
+
get userId() {
|
|
1241
|
+
const id = this.getUserId();
|
|
1242
|
+
if (!id) {
|
|
1243
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No user context set");
|
|
1244
|
+
}
|
|
1245
|
+
return id;
|
|
1246
|
+
}
|
|
1247
|
+
async getMessaging() {
|
|
1248
|
+
if (!this.messaging) {
|
|
1249
|
+
if (!this.firebaseApp) {
|
|
1250
|
+
throw new MAKEMINDError("CONFIGURATION_ERROR", "Firebase app not configured for push notifications");
|
|
1251
|
+
}
|
|
1252
|
+
const { getMessaging } = await import("firebase/messaging");
|
|
1253
|
+
this.messaging = getMessaging(this.firebaseApp);
|
|
1254
|
+
}
|
|
1255
|
+
return this.messaging;
|
|
1256
|
+
}
|
|
1257
|
+
generateDeviceId() {
|
|
1258
|
+
return `device_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Send a message using a template via the sendBatchMessages onCall.
|
|
1262
|
+
* Maps SDK SendMessageOptions to the batch service: one call per channel.
|
|
1263
|
+
*/
|
|
1264
|
+
async send(options) {
|
|
1265
|
+
const channels = options.channels || ["push", "email"];
|
|
1266
|
+
const recipients = options.recipients.map((userId) => ({ userId }));
|
|
1267
|
+
let firstMessageId = "";
|
|
1268
|
+
for (const channelType of channels) {
|
|
1269
|
+
const response = await this.apiClient.dispatch(
|
|
1270
|
+
"messaging-sendBatchMessages",
|
|
1271
|
+
{
|
|
1272
|
+
channelType,
|
|
1273
|
+
templateId: options.template,
|
|
1274
|
+
recipients,
|
|
1275
|
+
params: options.data
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
if (!firstMessageId && response.results.length > 0) {
|
|
1279
|
+
firstMessageId = response.results[0].messageId;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return { messageId: firstMessageId };
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Send notification to specific users across channels.
|
|
1286
|
+
* Uses sendBatchMessages onCall (one dispatch per channel).
|
|
1287
|
+
*/
|
|
1288
|
+
async notify(userIds, notification) {
|
|
1289
|
+
const channels = notification.channels || ["push"];
|
|
1290
|
+
const recipients = userIds.map((userId) => ({ userId }));
|
|
1291
|
+
let firstMessageId = "";
|
|
1292
|
+
for (const channelType of channels) {
|
|
1293
|
+
const response = await this.apiClient.dispatch(
|
|
1294
|
+
"messaging-sendBatchMessages",
|
|
1295
|
+
{
|
|
1296
|
+
channelType,
|
|
1297
|
+
recipients,
|
|
1298
|
+
title: notification.title,
|
|
1299
|
+
body: notification.body,
|
|
1300
|
+
priority: "normal"
|
|
1301
|
+
}
|
|
1302
|
+
);
|
|
1303
|
+
if (!firstMessageId && response.results.length > 0) {
|
|
1304
|
+
firstMessageId = response.results[0].messageId;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return { messageId: firstMessageId };
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Register device for push notifications.
|
|
1311
|
+
* SDK internally handles token acquisition (Firebase FCM).
|
|
1312
|
+
* Uses the REST /messaging/devices route (already correct).
|
|
1313
|
+
*/
|
|
1314
|
+
async registerDevice(options) {
|
|
1315
|
+
const { platform, deviceId } = options;
|
|
1316
|
+
const finalDeviceId = deviceId || this.generateDeviceId();
|
|
1317
|
+
const token = await this.acquirePushToken();
|
|
1318
|
+
await this.apiClient.post("/messaging/devices", {
|
|
1319
|
+
token,
|
|
1320
|
+
platform
|
|
1321
|
+
});
|
|
1322
|
+
this.currentDeviceId = finalDeviceId;
|
|
1323
|
+
return { deviceId: finalDeviceId };
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Internal: Acquire push token from the platform's push service.
|
|
1327
|
+
* Currently uses Firebase FCM, abstracted for future cloud-agnostic support.
|
|
1328
|
+
*/
|
|
1329
|
+
async acquirePushToken() {
|
|
1330
|
+
if (!this.vapidKey) {
|
|
1331
|
+
throw new MAKEMINDError("CONFIGURATION_ERROR", "VAPID key not configured for push notifications");
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
const messaging = await this.getMessaging();
|
|
1335
|
+
const { getToken } = await import("firebase/messaging");
|
|
1336
|
+
const token = await getToken(messaging, { vapidKey: this.vapidKey });
|
|
1337
|
+
if (!token) {
|
|
1338
|
+
throw new MAKEMINDError("MESSAGING_ERROR", "Failed to acquire push token");
|
|
1339
|
+
}
|
|
1340
|
+
return token;
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
if (error instanceof MAKEMINDError) throw error;
|
|
1343
|
+
throw new MAKEMINDError("MESSAGING_ERROR", "Failed to acquire push token: " + (error instanceof Error ? error.message : "Unknown error"));
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Unregister device from push notifications.
|
|
1348
|
+
* Uses the REST DELETE /messaging/devices/:token route.
|
|
1349
|
+
* The route key is the raw FCM token (server base64-encodes it to deviceId).
|
|
1350
|
+
*/
|
|
1351
|
+
async unregisterDevice(deviceId) {
|
|
1352
|
+
await this.apiClient.delete(`/messaging/devices/${deviceId}`);
|
|
1353
|
+
if (this.currentDeviceId === deviceId) {
|
|
1354
|
+
this.currentDeviceId = void 0;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Listen for foreground push messages.
|
|
1359
|
+
*/
|
|
1360
|
+
onPushMessage(callback) {
|
|
1361
|
+
let attached = null;
|
|
1362
|
+
let cancelled = false;
|
|
1363
|
+
void (async () => {
|
|
1364
|
+
const messaging = await this.getMessaging();
|
|
1365
|
+
const { onMessage } = await import("firebase/messaging");
|
|
1366
|
+
if (cancelled) return;
|
|
1367
|
+
attached = onMessage(messaging, (payload) => {
|
|
1368
|
+
callback({
|
|
1369
|
+
messageId: payload.messageId || "",
|
|
1370
|
+
title: payload.notification?.title,
|
|
1371
|
+
body: payload.notification?.body || "",
|
|
1372
|
+
data: payload.data
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
})();
|
|
1376
|
+
return () => {
|
|
1377
|
+
cancelled = true;
|
|
1378
|
+
attached?.();
|
|
1379
|
+
attached = null;
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Get in-app notifications for the current user.
|
|
1384
|
+
* Dispatch op: messaging-getInAppNotifications
|
|
1385
|
+
* Service contract: { userId?, unreadOnly?, limit?, cursor? }
|
|
1386
|
+
* Response: { notifications, hasMore, nextCursor }
|
|
1387
|
+
*/
|
|
1388
|
+
async getNotifications(options) {
|
|
1389
|
+
const response = await this.apiClient.dispatch(
|
|
1390
|
+
"messaging-getInAppNotifications",
|
|
1391
|
+
{
|
|
1392
|
+
unreadOnly: options?.unreadOnly ?? false,
|
|
1393
|
+
limit: options?.limit ?? 50,
|
|
1394
|
+
cursor: options?.cursor
|
|
1395
|
+
}
|
|
1396
|
+
);
|
|
1397
|
+
return {
|
|
1398
|
+
notifications: response.notifications,
|
|
1399
|
+
nextCursor: response.nextCursor ?? void 0
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Get in-app notifications stream (polling-based).
|
|
1404
|
+
*/
|
|
1405
|
+
onNotifications(callback, options) {
|
|
1406
|
+
let isSubscribed = true;
|
|
1407
|
+
const poll = async () => {
|
|
1408
|
+
if (!isSubscribed) return;
|
|
1409
|
+
try {
|
|
1410
|
+
const result = await this.getNotifications({
|
|
1411
|
+
unreadOnly: options?.unreadOnly,
|
|
1412
|
+
limit: options?.limitCount || 50
|
|
1413
|
+
});
|
|
1414
|
+
callback(result.notifications);
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
if (isSubscribed) {
|
|
1418
|
+
setTimeout(poll, 5e3);
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
poll();
|
|
1422
|
+
return () => {
|
|
1423
|
+
isSubscribed = false;
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Get unread notification count.
|
|
1428
|
+
* Dispatch op: messaging-getUnreadNotificationCount (the verified onCall service).
|
|
1429
|
+
* Response: { userId, unreadCount }
|
|
1430
|
+
*/
|
|
1431
|
+
async getUnreadCount() {
|
|
1432
|
+
const response = await this.apiClient.dispatch(
|
|
1433
|
+
"messaging-getUnreadNotificationCount",
|
|
1434
|
+
{}
|
|
1435
|
+
);
|
|
1436
|
+
return response.unreadCount;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Get unread notification count stream (polling-based).
|
|
1440
|
+
*/
|
|
1441
|
+
onUnreadCount(callback) {
|
|
1442
|
+
let isSubscribed = true;
|
|
1443
|
+
const poll = async () => {
|
|
1444
|
+
if (!isSubscribed) return;
|
|
1445
|
+
try {
|
|
1446
|
+
const count = await this.getUnreadCount();
|
|
1447
|
+
callback(count);
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
if (isSubscribed) {
|
|
1451
|
+
setTimeout(poll, 1e4);
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
poll();
|
|
1455
|
+
return () => {
|
|
1456
|
+
isSubscribed = false;
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Mark a notification as read.
|
|
1461
|
+
* Dispatch op: messaging-markNotificationRead
|
|
1462
|
+
* Service contract: { notificationId } or { notificationIds }
|
|
1463
|
+
*/
|
|
1464
|
+
async markAsRead(notificationId) {
|
|
1465
|
+
await this.apiClient.dispatch(
|
|
1466
|
+
"messaging-markNotificationRead",
|
|
1467
|
+
{ notificationId }
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Mark all notifications as read for the current user.
|
|
1472
|
+
* Dispatch op: messaging-markAllNotificationsRead (new onCall — no existing match).
|
|
1473
|
+
* Implemented in backend send.ts + service/send.ts below.
|
|
1474
|
+
*/
|
|
1475
|
+
async markAllAsRead() {
|
|
1476
|
+
await this.apiClient.dispatch(
|
|
1477
|
+
"messaging-markAllNotificationsRead",
|
|
1478
|
+
{}
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
// PLM-specific notification helpers
|
|
1482
|
+
/**
|
|
1483
|
+
* Notify ECO reviewers.
|
|
1484
|
+
*/
|
|
1485
|
+
async notifyEcoReviewers(eco, reviewerIds) {
|
|
1486
|
+
return this.send({
|
|
1487
|
+
template: "ecoReviewRequest",
|
|
1488
|
+
recipients: reviewerIds,
|
|
1489
|
+
data: {
|
|
1490
|
+
ecoNumber: eco.requestNumber,
|
|
1491
|
+
title: eco.title,
|
|
1492
|
+
priority: eco.priority,
|
|
1493
|
+
productCode: eco.productCode
|
|
1494
|
+
},
|
|
1495
|
+
channels: ["push", "email"]
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Notify quality issue critical.
|
|
1500
|
+
*/
|
|
1501
|
+
async notifyQualityCritical(issue, recipientIds) {
|
|
1502
|
+
return this.send({
|
|
1503
|
+
template: "qualityIssueCritical",
|
|
1504
|
+
recipients: recipientIds,
|
|
1505
|
+
data: {
|
|
1506
|
+
issueNumber: issue.issueNumber,
|
|
1507
|
+
title: issue.title,
|
|
1508
|
+
severity: issue.severity
|
|
1509
|
+
},
|
|
1510
|
+
channels: ["push", "email", "sms"]
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Notify bid received.
|
|
1515
|
+
*/
|
|
1516
|
+
async notifyBidReceived(request, bidder, ownerId) {
|
|
1517
|
+
return this.send({
|
|
1518
|
+
template: "bidReceived",
|
|
1519
|
+
recipients: [ownerId],
|
|
1520
|
+
data: {
|
|
1521
|
+
requestNumber: request.requestNumber,
|
|
1522
|
+
title: request.title,
|
|
1523
|
+
bidderName: bidder.name
|
|
1524
|
+
},
|
|
1525
|
+
channels: ["push", "email"]
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
// Template Management
|
|
1529
|
+
/**
|
|
1530
|
+
* List available message templates.
|
|
1531
|
+
* Dispatch op: messaging-msgGetTemplates (the verified onCall service).
|
|
1532
|
+
* Service contract: { channelType? }
|
|
1533
|
+
* Response: { templates }
|
|
1534
|
+
*/
|
|
1535
|
+
async listTemplates(options) {
|
|
1536
|
+
const response = await this.apiClient.dispatch(
|
|
1537
|
+
"messaging-msgGetTemplates",
|
|
1538
|
+
options?.channel ? { channelType: options.channel } : {}
|
|
1539
|
+
);
|
|
1540
|
+
return response.templates;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Get a specific template by ID.
|
|
1544
|
+
* Dispatch op: messaging-msgGetTemplate
|
|
1545
|
+
* Service contract: { templateId }
|
|
1546
|
+
* Response: template document (FirebaseFirestore.DocumentData)
|
|
1547
|
+
*/
|
|
1548
|
+
async getTemplate(templateId) {
|
|
1549
|
+
const response = await this.apiClient.dispatch(
|
|
1550
|
+
"messaging-msgGetTemplate",
|
|
1551
|
+
{ templateId }
|
|
1552
|
+
);
|
|
1553
|
+
return response;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Create a new message template.
|
|
1557
|
+
* Dispatch op: messaging-msgCreateTemplate
|
|
1558
|
+
* Service contract: { name, channelType, subject?, body, bodyHtml?, language?, variables?, description? }
|
|
1559
|
+
* Response: { templateId }
|
|
1560
|
+
* Note: backend returns { templateId }; SDK surface returns full MessageTemplate.
|
|
1561
|
+
* We re-fetch the created template to satisfy the return type.
|
|
1562
|
+
*/
|
|
1563
|
+
async createTemplate(template) {
|
|
1564
|
+
const response = await this.apiClient.dispatch(
|
|
1565
|
+
"messaging-msgCreateTemplate",
|
|
1566
|
+
{
|
|
1567
|
+
name: template.name,
|
|
1568
|
+
channelType: template.channel,
|
|
1569
|
+
subject: template.subject,
|
|
1570
|
+
body: template.body,
|
|
1571
|
+
variables: template.variables,
|
|
1572
|
+
language: template.language
|
|
1573
|
+
}
|
|
1574
|
+
);
|
|
1575
|
+
return this.getTemplate(response.templateId);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Update an existing template.
|
|
1579
|
+
* Dispatch op: messaging-msgUpdateTemplate
|
|
1580
|
+
* Service contract: { templateId, subject?, body?, bodyHtml?, language?, variables?, description? }
|
|
1581
|
+
* Response: { success }
|
|
1582
|
+
* We re-fetch the updated template to satisfy the return type.
|
|
1583
|
+
*/
|
|
1584
|
+
async updateTemplate(templateId, updates) {
|
|
1585
|
+
await this.apiClient.dispatch(
|
|
1586
|
+
"messaging-msgUpdateTemplate",
|
|
1587
|
+
{
|
|
1588
|
+
templateId,
|
|
1589
|
+
subject: updates.subject,
|
|
1590
|
+
body: updates.body,
|
|
1591
|
+
language: updates.language,
|
|
1592
|
+
variables: updates.variables
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
return this.getTemplate(templateId);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Delete a template.
|
|
1599
|
+
* Dispatch op: messaging-msgDeleteTemplate
|
|
1600
|
+
* Service contract: { templateId }
|
|
1601
|
+
* Response: { success }
|
|
1602
|
+
*/
|
|
1603
|
+
async deleteTemplate(templateId) {
|
|
1604
|
+
await this.apiClient.dispatch(
|
|
1605
|
+
"messaging-msgDeleteTemplate",
|
|
1606
|
+
{ templateId }
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
// Topic Subscription
|
|
1610
|
+
/**
|
|
1611
|
+
* Subscribe to a topic for push notifications.
|
|
1612
|
+
* Dispatch op: messaging-subscribeToTopic
|
|
1613
|
+
* Service contract: { token, topic, tenantId }
|
|
1614
|
+
* Token is acquired from the current device registration.
|
|
1615
|
+
* If no token is available, the subscription request is sent with userId context only.
|
|
1616
|
+
*/
|
|
1617
|
+
async subscribeToTopic(topic) {
|
|
1618
|
+
if (!this.vapidKey) {
|
|
1619
|
+
throw new MAKEMINDError("CONFIGURATION_ERROR", "VAPID key required for topic subscriptions");
|
|
1620
|
+
}
|
|
1621
|
+
const token = await this.acquirePushToken();
|
|
1622
|
+
await this.apiClient.dispatch(
|
|
1623
|
+
"messaging-subscribeToTopic",
|
|
1624
|
+
{
|
|
1625
|
+
token,
|
|
1626
|
+
topic,
|
|
1627
|
+
tenantId: this.tenantId
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Unsubscribe from a topic.
|
|
1633
|
+
* Dispatch op: messaging-unsubscribeFromTopic
|
|
1634
|
+
* Service contract: { token, topic, tenantId }
|
|
1635
|
+
*/
|
|
1636
|
+
async unsubscribeFromTopic(topic) {
|
|
1637
|
+
if (!this.vapidKey) {
|
|
1638
|
+
throw new MAKEMINDError("CONFIGURATION_ERROR", "VAPID key required for topic subscriptions");
|
|
1639
|
+
}
|
|
1640
|
+
const token = await this.acquirePushToken();
|
|
1641
|
+
await this.apiClient.dispatch(
|
|
1642
|
+
"messaging-unsubscribeFromTopic",
|
|
1643
|
+
{
|
|
1644
|
+
token,
|
|
1645
|
+
topic,
|
|
1646
|
+
tenantId: this.tenantId
|
|
1647
|
+
}
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* List subscribed topics for the current user.
|
|
1652
|
+
* Dispatch op: messaging-getMyTopics
|
|
1653
|
+
* Service contract: { tenantId? }
|
|
1654
|
+
* Response: { topics: Array<{ topic, fullTopic, tenantId, subscribedAt }> }
|
|
1655
|
+
*/
|
|
1656
|
+
async listSubscribedTopics() {
|
|
1657
|
+
const response = await this.apiClient.dispatch(
|
|
1658
|
+
"messaging-getMyTopics",
|
|
1659
|
+
{ tenantId: this.tenantId }
|
|
1660
|
+
);
|
|
1661
|
+
return response.topics.map((t) => t.topic);
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Send message to a topic.
|
|
1665
|
+
* Dispatch op: messaging-sendTopicMessage
|
|
1666
|
+
* Service contract: { topic, subject?, body, imageUrl?, data? }
|
|
1667
|
+
* Response: { messageId, fcmMessageId }
|
|
1668
|
+
*/
|
|
1669
|
+
async sendToTopic(topic, message) {
|
|
1670
|
+
const response = await this.apiClient.dispatch(
|
|
1671
|
+
"messaging-sendTopicMessage",
|
|
1672
|
+
{
|
|
1673
|
+
topic,
|
|
1674
|
+
subject: message.title,
|
|
1675
|
+
body: message.body,
|
|
1676
|
+
data: message.data
|
|
1677
|
+
}
|
|
1678
|
+
);
|
|
1679
|
+
return { messageId: response.messageId };
|
|
1680
|
+
}
|
|
1681
|
+
// Message History
|
|
1682
|
+
/**
|
|
1683
|
+
* Get message history for this tenant.
|
|
1684
|
+
* Dispatch op: messaging-getMessages
|
|
1685
|
+
* Service contract: { channelType?, status?, limit? }
|
|
1686
|
+
* Response: { messages }
|
|
1687
|
+
*/
|
|
1688
|
+
async getMessageHistory(options) {
|
|
1689
|
+
const response = await this.apiClient.dispatch(
|
|
1690
|
+
"messaging-getMessages",
|
|
1691
|
+
{
|
|
1692
|
+
channelType: options?.channel,
|
|
1693
|
+
status: options?.status,
|
|
1694
|
+
limit: options?.limit ?? 50
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
return {
|
|
1698
|
+
messages: response.messages
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Get full details of a single message.
|
|
1703
|
+
* Dispatch op: messaging-getMessage
|
|
1704
|
+
* Service contract: { messageId }
|
|
1705
|
+
* Response: full message document (FirebaseFirestore.DocumentData)
|
|
1706
|
+
*/
|
|
1707
|
+
async getMessageDetails(messageId) {
|
|
1708
|
+
const response = await this.apiClient.dispatch(
|
|
1709
|
+
"messaging-getMessage",
|
|
1710
|
+
{ messageId }
|
|
1711
|
+
);
|
|
1712
|
+
return response;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Retry a failed message.
|
|
1716
|
+
* No backend messaging onCall exists for retry (confirmed: two searches — grep
|
|
1717
|
+
* across messaging/service/* and messaging/*.ts both returned no match).
|
|
1718
|
+
* Dispatch op: messaging-retryFailedMessage (new onCall implemented below).
|
|
1719
|
+
*/
|
|
1720
|
+
async retryMessage(messageId) {
|
|
1721
|
+
const response = await this.apiClient.dispatch(
|
|
1722
|
+
"messaging-retryFailedMessage",
|
|
1723
|
+
{ messageId }
|
|
1724
|
+
);
|
|
1725
|
+
return { messageId: response.messageId };
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
// src/services/settlement.ts
|
|
1730
|
+
var MAKEMINDSettlement = class {
|
|
1731
|
+
constructor(config) {
|
|
1732
|
+
this.apiClient = config.apiClient;
|
|
1733
|
+
this.getTenantId = config.tenantId;
|
|
1734
|
+
}
|
|
1735
|
+
get tenantId() {
|
|
1736
|
+
const id = this.getTenantId();
|
|
1737
|
+
if (!id) {
|
|
1738
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No tenant context set");
|
|
1739
|
+
}
|
|
1740
|
+
return id;
|
|
1741
|
+
}
|
|
1742
|
+
// Record a usage event
|
|
1743
|
+
async recordUsage(options) {
|
|
1744
|
+
await this.apiClient.dispatch("settlement-recordUsage", {
|
|
1745
|
+
metric: options.eventType,
|
|
1746
|
+
value: options.quantity ?? 1,
|
|
1747
|
+
unit: "count",
|
|
1748
|
+
sourceService: options.resourceId || "sdk",
|
|
1749
|
+
metadata: options.metadata
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Record a sale and distribute it to n recipients in one atomic posting.
|
|
1754
|
+
*
|
|
1755
|
+
* The revenue leg and every share leg commit together or not at all. Do not
|
|
1756
|
+
* emulate this by recording the sale and then each share separately — that
|
|
1757
|
+
* makes every partial-failure combination yours to recover, and a
|
|
1758
|
+
* half-distributed sale cannot be repaired from outside the ledger.
|
|
1759
|
+
*
|
|
1760
|
+
* The split rule is the caller's: send the recipients and amounts you
|
|
1761
|
+
* computed. Each cut is either a percentage or a fixed amount, and the two
|
|
1762
|
+
* may be mixed; the combined claim may not exceed the sale. Recipients are settlement accounts, so the same call serves an
|
|
1763
|
+
* End User seller, another party in the tenant, or a platform cut.
|
|
1764
|
+
*
|
|
1765
|
+
* Shares are floored to the currency's minor unit; the remainder stays with
|
|
1766
|
+
* the receiving party. A share too small to express posts no leg and comes
|
|
1767
|
+
* back as `0` / `null`.
|
|
1768
|
+
*/
|
|
1769
|
+
async recordSaleWithShares(options) {
|
|
1770
|
+
return this.apiClient.dispatch("settlement-recordSaleWithShares", options);
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Reverse a split sale, clawing back each recipient's share in the same
|
|
1774
|
+
* posting. Partial refunds claw back proportionally (floored, so a claw-back
|
|
1775
|
+
* never exceeds what the recipient received; the difference is yours).
|
|
1776
|
+
*
|
|
1777
|
+
* Use this rather than a plain refund for anything recorded with
|
|
1778
|
+
* `recordSaleWithShares` — otherwise the buyer is refunded while the shares
|
|
1779
|
+
* stay paid out, and the ledger is short by exactly the shares.
|
|
1780
|
+
*/
|
|
1781
|
+
async reverseSaleWithShares(options) {
|
|
1782
|
+
return this.apiClient.dispatch("settlement-reverseSaleWithShares", options);
|
|
1783
|
+
}
|
|
1784
|
+
// Get usage statistics.
|
|
1785
|
+
// Backend REST GET /settlement/usage/summary accepts startDate/endDate/metric
|
|
1786
|
+
// query params (not a 'period' string). The response is
|
|
1787
|
+
// { data: { summary: Record<metric, { total, count, unit }> } }.
|
|
1788
|
+
// Mapped to UsageStats shape here.
|
|
1789
|
+
async getUsageStats(options) {
|
|
1790
|
+
const params = new URLSearchParams();
|
|
1791
|
+
if (options?.startDate) params.append("startDate", options.startDate);
|
|
1792
|
+
if (options?.endDate) params.append("endDate", options.endDate);
|
|
1793
|
+
if (options?.metric) params.append("metric", options.metric);
|
|
1794
|
+
const raw = await this.apiClient.get(`/settlement/usage/summary?${params.toString()}`);
|
|
1795
|
+
const summary = (raw.data?.summary ?? raw.summary) || {};
|
|
1796
|
+
const events = {};
|
|
1797
|
+
let totalUsage = 0;
|
|
1798
|
+
for (const [key, val] of Object.entries(summary)) {
|
|
1799
|
+
events[key] = val.total;
|
|
1800
|
+
totalUsage += val.total;
|
|
1801
|
+
}
|
|
1802
|
+
return {
|
|
1803
|
+
period: options?.startDate && options?.endDate ? `${options.startDate}/${options.endDate}` : "all",
|
|
1804
|
+
events,
|
|
1805
|
+
totalUsage
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
// Get billing summary
|
|
1809
|
+
// NOTE: No backend aggregate function exists. This assembles the summary from
|
|
1810
|
+
// the current subscription (for plan name) and the main account balance (for
|
|
1811
|
+
// current balance + pending charges) via two sequential REST calls. A
|
|
1812
|
+
// dedicated settlement-getBillingSummary onCall is not yet deployed.
|
|
1813
|
+
async getBillingSummary() {
|
|
1814
|
+
const [subResult, accountResult] = await Promise.allSettled([
|
|
1815
|
+
this.apiClient.get(
|
|
1816
|
+
"/settlement/subscriptions/current"
|
|
1817
|
+
),
|
|
1818
|
+
this.apiClient.get(
|
|
1819
|
+
"/settlement/accounts/main/balance"
|
|
1820
|
+
)
|
|
1821
|
+
]);
|
|
1822
|
+
const sub = subResult.status === "fulfilled" ? subResult.value : null;
|
|
1823
|
+
const account = accountResult.status === "fulfilled" ? accountResult.value : { balance: 0, pendingBalance: 0, currency: "USD" };
|
|
1824
|
+
return {
|
|
1825
|
+
currentBalance: account.balance ?? 0,
|
|
1826
|
+
pendingCharges: account.pendingBalance ?? 0,
|
|
1827
|
+
currency: account.currency ?? "USD",
|
|
1828
|
+
plan: sub?.planName ?? "free"
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
// Get quota status
|
|
1832
|
+
// TODO(gateway): no dedicated quota endpoint exists in the backend. A
|
|
1833
|
+
// settlement-getQuotaStatus onCall or REST route that maps plan limits against
|
|
1834
|
+
// live usage records has not yet been implemented. Requires new business logic
|
|
1835
|
+
// (scope: plan limits table + usage aggregation per metric).
|
|
1836
|
+
async getQuotaStatus(_resourceType) {
|
|
1837
|
+
throw new MAKEMINDError(
|
|
1838
|
+
"NOT_IMPLEMENTED",
|
|
1839
|
+
"getQuotaStatus requires a backend quota endpoint that is not yet available"
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
// List invoices.
|
|
1843
|
+
// Backend InvoiceStatus includes 'cancelled' and 'refunded' in addition to
|
|
1844
|
+
// the four values the type/index.ts Invoice interface declares; the filter
|
|
1845
|
+
// param remains the narrower set since the SDK contract targets tenant-portal
|
|
1846
|
+
// display (admin-console can query all statuses directly).
|
|
1847
|
+
async listInvoices(options) {
|
|
1848
|
+
const params = new URLSearchParams();
|
|
1849
|
+
if (options?.status) params.append("status", options.status);
|
|
1850
|
+
if (options?.limit) params.append("limit", String(options.limit));
|
|
1851
|
+
const response = await this.apiClient.get(
|
|
1852
|
+
`/settlement/invoices?${params.toString()}`
|
|
1853
|
+
);
|
|
1854
|
+
return response.items.map((invoice) => ({
|
|
1855
|
+
...invoice,
|
|
1856
|
+
periodStart: new Date(invoice.periodStart),
|
|
1857
|
+
periodEnd: new Date(invoice.periodEnd),
|
|
1858
|
+
createdAt: new Date(invoice.createdAt)
|
|
1859
|
+
}));
|
|
1860
|
+
}
|
|
1861
|
+
// Get invoice by ID
|
|
1862
|
+
async getInvoice(invoiceId) {
|
|
1863
|
+
return this.apiClient.get(`/settlement/invoices/${invoiceId}`);
|
|
1864
|
+
}
|
|
1865
|
+
// Download invoice PDF
|
|
1866
|
+
// TODO(gateway): invoice PDF generation (rendering, signing, storage) is not
|
|
1867
|
+
// yet implemented in the backend. Requires a new Cloud Function and/or storage
|
|
1868
|
+
// pipeline (scope: PDF renderer + signed GCS URL generation + optional caching).
|
|
1869
|
+
async downloadInvoice(_invoiceId) {
|
|
1870
|
+
throw new MAKEMINDError(
|
|
1871
|
+
"NOT_IMPLEMENTED",
|
|
1872
|
+
"downloadInvoice requires a PDF generation pipeline that is not yet available"
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
// Get current plan
|
|
1876
|
+
// Resolves via two dispatch calls: (1) get the active subscription to learn the
|
|
1877
|
+
// productId, then (2) fetch the product record for features/limits.
|
|
1878
|
+
async getCurrentPlan() {
|
|
1879
|
+
const sub = await this.apiClient.get(
|
|
1880
|
+
"/settlement/subscriptions/current"
|
|
1881
|
+
);
|
|
1882
|
+
const productId = sub?.planId;
|
|
1883
|
+
if (!productId) {
|
|
1884
|
+
return { id: "free", name: "Free", features: [], limits: {} };
|
|
1885
|
+
}
|
|
1886
|
+
const product = await this.apiClient.dispatch("settlement-getProduct", { productId });
|
|
1887
|
+
return {
|
|
1888
|
+
id: product.id,
|
|
1889
|
+
name: product.name,
|
|
1890
|
+
features: product.features ?? [],
|
|
1891
|
+
limits: product.includedUsage ?? {}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
// List available plans
|
|
1895
|
+
// Routes to settlement-listProducts filtering on productType='saasPlan'. The
|
|
1896
|
+
// response shape is adapted to the SDK plan contract.
|
|
1897
|
+
async listPlans() {
|
|
1898
|
+
const result = await this.apiClient.dispatch("settlement-listProducts", {
|
|
1899
|
+
productType: "saasPlan",
|
|
1900
|
+
status: "active"
|
|
1901
|
+
});
|
|
1902
|
+
return (result.products ?? []).map((p) => ({
|
|
1903
|
+
id: p.id,
|
|
1904
|
+
name: p.name,
|
|
1905
|
+
description: p.description ?? "",
|
|
1906
|
+
price: p.price ?? 0,
|
|
1907
|
+
currency: p.currency ?? "USD",
|
|
1908
|
+
features: p.features ?? [],
|
|
1909
|
+
limits: p.includedUsage ?? {}
|
|
1910
|
+
}));
|
|
1911
|
+
}
|
|
1912
|
+
// Upgrade/change plan
|
|
1913
|
+
// Routes to POST /settlement/subscriptions which cancels the existing active
|
|
1914
|
+
// subscription and creates a new one for the target plan in a single batch.
|
|
1915
|
+
async changePlan(planId) {
|
|
1916
|
+
await this.apiClient.post("/settlement/subscriptions", { planId });
|
|
1917
|
+
}
|
|
1918
|
+
// ============================================================================
|
|
1919
|
+
// Subscription Management
|
|
1920
|
+
// ============================================================================
|
|
1921
|
+
/**
|
|
1922
|
+
* Get current subscription details
|
|
1923
|
+
*/
|
|
1924
|
+
async getCurrentSubscription() {
|
|
1925
|
+
try {
|
|
1926
|
+
return await this.apiClient.get("/settlement/subscriptions/current");
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
if (error instanceof MAKEMINDError && error.code === "NOT_FOUND") {
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
throw error;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Subscribe to a plan
|
|
1936
|
+
*/
|
|
1937
|
+
async subscribe(options) {
|
|
1938
|
+
return this.apiClient.post("/settlement/subscriptions", {
|
|
1939
|
+
planId: options.planId,
|
|
1940
|
+
billingCycle: options.billingCycle || "monthly",
|
|
1941
|
+
paymentMethod: options.paymentMethodId,
|
|
1942
|
+
couponCode: options.couponCode,
|
|
1943
|
+
startImmediately: options.startImmediately ?? true
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Update current subscription (e.g., change plan, billing cycle).
|
|
1948
|
+
* The backend has no in-place PATCH; a plan update is implemented by creating
|
|
1949
|
+
* a new subscription (which cancels the current one server-side). billingCycle
|
|
1950
|
+
* and prorate are forwarded as metadata but only planId is acted upon today.
|
|
1951
|
+
*/
|
|
1952
|
+
async updateSubscription(options) {
|
|
1953
|
+
if (!options.planId) {
|
|
1954
|
+
throw new MAKEMINDError(
|
|
1955
|
+
"VALIDATION_ERROR",
|
|
1956
|
+
"planId is required for updateSubscription"
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
return this.apiClient.post("/settlement/subscriptions", {
|
|
1960
|
+
planId: options.planId,
|
|
1961
|
+
billingCycle: options.billingCycle
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Cancel current subscription.
|
|
1966
|
+
* Resolves the active subscriptionId first, then dispatches to
|
|
1967
|
+
* settlement-cancelSubscription. Backend field is cancelImmediately (not
|
|
1968
|
+
* cancelAtPeriodEnd); meaning is inverted — SDK default cancelAtPeriodEnd=true
|
|
1969
|
+
* maps to cancelImmediately=false.
|
|
1970
|
+
*/
|
|
1971
|
+
async cancelSubscription(options) {
|
|
1972
|
+
const sub = await this.getCurrentSubscription();
|
|
1973
|
+
if (!sub) {
|
|
1974
|
+
throw new MAKEMINDError("NOT_FOUND", "No active subscription to cancel");
|
|
1975
|
+
}
|
|
1976
|
+
await this.apiClient.dispatch("settlement-cancelSubscription", {
|
|
1977
|
+
subscriptionId: sub.id,
|
|
1978
|
+
cancelImmediately: !(options?.cancelAtPeriodEnd ?? true),
|
|
1979
|
+
reason: options?.reason
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Pause subscription (if supported by plan).
|
|
1984
|
+
* Backend accepts only { subscriptionId }; pauseUntil is not supported yet.
|
|
1985
|
+
*/
|
|
1986
|
+
async pauseSubscription(_options) {
|
|
1987
|
+
const sub = await this.getCurrentSubscription();
|
|
1988
|
+
if (!sub) {
|
|
1989
|
+
throw new MAKEMINDError("NOT_FOUND", "No active subscription to pause");
|
|
1990
|
+
}
|
|
1991
|
+
await this.apiClient.dispatch("settlement-pauseSubscription", {
|
|
1992
|
+
subscriptionId: sub.id
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Resume a paused subscription.
|
|
1997
|
+
*/
|
|
1998
|
+
async resumeSubscription() {
|
|
1999
|
+
const sub = await this.getCurrentSubscription();
|
|
2000
|
+
if (!sub) {
|
|
2001
|
+
throw new MAKEMINDError("NOT_FOUND", "No subscription found to resume");
|
|
2002
|
+
}
|
|
2003
|
+
return this.apiClient.dispatch("settlement-resumeSubscription", {
|
|
2004
|
+
subscriptionId: sub.id
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Reactivate a canceled subscription (before period end).
|
|
2009
|
+
* Routes to settlement-resumeSubscription: fetches the most-recently-canceled
|
|
2010
|
+
* subscription and resumes it. This is semantically equivalent for the
|
|
2011
|
+
* cancel-at-period-end case where the subscription is still within its period.
|
|
2012
|
+
*/
|
|
2013
|
+
async reactivateSubscription() {
|
|
2014
|
+
const sub = await this.getCurrentSubscription();
|
|
2015
|
+
if (!sub) {
|
|
2016
|
+
throw new MAKEMINDError("NOT_FOUND", "No subscription found to reactivate");
|
|
2017
|
+
}
|
|
2018
|
+
return this.apiClient.dispatch("settlement-resumeSubscription", {
|
|
2019
|
+
subscriptionId: sub.id
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Get subscription history.
|
|
2024
|
+
* TODO(gateway): no dedicated subscription history table/function exists in the
|
|
2025
|
+
* backend. Requires a new settlement_subscription_events table or equivalent
|
|
2026
|
+
* audit trail (scope: schema + backfill trigger + query function).
|
|
2027
|
+
*/
|
|
2028
|
+
async getSubscriptionHistory(_options) {
|
|
2029
|
+
throw new MAKEMINDError(
|
|
2030
|
+
"NOT_IMPLEMENTED",
|
|
2031
|
+
"getSubscriptionHistory requires a subscription audit trail that is not yet available"
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Preview subscription change (get prorated amounts).
|
|
2036
|
+
* TODO(gateway): proration calculation requires billing period overlap logic
|
|
2037
|
+
* that is not yet implemented in the backend (scope: period credits, immediate
|
|
2038
|
+
* charge delta, next-billing projection — needs a new onCall or REST endpoint).
|
|
2039
|
+
*/
|
|
2040
|
+
async previewSubscriptionChange(_newPlanId) {
|
|
2041
|
+
throw new MAKEMINDError(
|
|
2042
|
+
"NOT_IMPLEMENTED",
|
|
2043
|
+
"previewSubscriptionChange requires proration logic that is not yet available"
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Apply a coupon to current subscription.
|
|
2048
|
+
* TODO(gateway): coupon engine (validation, redemption ledger, discount
|
|
2049
|
+
* application) is not yet implemented (scope: coupon collection + validation
|
|
2050
|
+
* function + subscription discount field update).
|
|
2051
|
+
*/
|
|
2052
|
+
async applyCoupon(_couponCode) {
|
|
2053
|
+
throw new MAKEMINDError(
|
|
2054
|
+
"NOT_IMPLEMENTED",
|
|
2055
|
+
"applyCoupon requires a coupon engine that is not yet available"
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
// Get payment methods
|
|
2059
|
+
// Gateway exposes no /settlement/payment-methods REST route; routed through the
|
|
2060
|
+
// settlement dispatch operation instead.
|
|
2061
|
+
async getPaymentMethods() {
|
|
2062
|
+
const response = await this.apiClient.dispatch("settlement-listPaymentMethods", {});
|
|
2063
|
+
return response.paymentMethods;
|
|
2064
|
+
}
|
|
2065
|
+
// Add payment method.
|
|
2066
|
+
// Backend onCall expects { provider, stripe?, toss?, kakaoPay?, manual? }.
|
|
2067
|
+
async addPaymentMethod(data) {
|
|
2068
|
+
return this.apiClient.dispatch(
|
|
2069
|
+
"settlement-addPaymentMethod",
|
|
2070
|
+
data
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
// Remove payment method.
|
|
2074
|
+
// Backend field name is paymentAccountId (not paymentMethodId).
|
|
2075
|
+
async removePaymentMethod(paymentAccountId) {
|
|
2076
|
+
await this.apiClient.dispatch("settlement-removePaymentMethod", { paymentAccountId });
|
|
2077
|
+
}
|
|
2078
|
+
// Set default payment method.
|
|
2079
|
+
// Backend field name is paymentAccountId (not paymentMethodId).
|
|
2080
|
+
async setDefaultPaymentMethod(paymentAccountId) {
|
|
2081
|
+
await this.apiClient.dispatch("settlement-setDefaultPaymentMethod", { paymentAccountId });
|
|
2082
|
+
}
|
|
2083
|
+
// PLM-specific usage events
|
|
2084
|
+
// Record product creation
|
|
2085
|
+
async recordProductCreation(productId) {
|
|
2086
|
+
return this.recordUsage({
|
|
2087
|
+
eventType: "productCreated",
|
|
2088
|
+
resourceId: productId
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
// Record document upload
|
|
2092
|
+
async recordDocumentUpload(documentId, fileSizeMb) {
|
|
2093
|
+
return this.recordUsage({
|
|
2094
|
+
eventType: "documentUpload",
|
|
2095
|
+
resourceId: documentId,
|
|
2096
|
+
metadata: { fileSizeMb }
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
// Record ECO submission
|
|
2100
|
+
async recordEcoSubmission(ecoId) {
|
|
2101
|
+
return this.recordUsage({
|
|
2102
|
+
eventType: "ecoSubmitted",
|
|
2103
|
+
resourceId: ecoId
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
// Record marketplace bid
|
|
2107
|
+
async recordMarketplaceBid(bidId, requestId) {
|
|
2108
|
+
return this.recordUsage({
|
|
2109
|
+
eventType: "marketplaceBidSubmitted",
|
|
2110
|
+
resourceId: bidId,
|
|
2111
|
+
metadata: { requestId }
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
// Record participant addition
|
|
2115
|
+
async recordParticipantAdded(productId, participantTenantId) {
|
|
2116
|
+
return this.recordUsage({
|
|
2117
|
+
eventType: "participantAdded",
|
|
2118
|
+
resourceId: productId,
|
|
2119
|
+
metadata: { participantTenantId }
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
// src/services/partners.ts
|
|
2125
|
+
var MAKEMINDPartners = class {
|
|
2126
|
+
constructor(config) {
|
|
2127
|
+
this.apiClient = config.apiClient;
|
|
2128
|
+
}
|
|
2129
|
+
// --- Partner self-service ---
|
|
2130
|
+
/** Register the signed-in individual as a partner (requires a verified email). */
|
|
2131
|
+
async registerSelf(displayName) {
|
|
2132
|
+
return this.apiClient.dispatch("settlement-partnersRegisterSelf", displayName ? { displayName } : {});
|
|
2133
|
+
}
|
|
2134
|
+
/** Browse the open partner-program directory. */
|
|
2135
|
+
async listPrograms() {
|
|
2136
|
+
return this.apiClient.dispatch("settlement-partnersListPrograms", {});
|
|
2137
|
+
}
|
|
2138
|
+
/** Apply to a tenant's program (creates a pending registration). */
|
|
2139
|
+
async apply(tenantId) {
|
|
2140
|
+
return this.apiClient.dispatch("settlement-partnersApply", { tenantId });
|
|
2141
|
+
}
|
|
2142
|
+
// --- M2M: referral capture + derivation (product-site backend) ---
|
|
2143
|
+
/** Auto-attribute a subject to a partner via a referral code (at checkout/entry). */
|
|
2144
|
+
async registerReferral(input) {
|
|
2145
|
+
return this.apiClient.dispatch("settlement-partnersRegisterReferral", input);
|
|
2146
|
+
}
|
|
2147
|
+
/** Derive commission for a settled sale for any partner that referred the subject. */
|
|
2148
|
+
async deriveCommission(input) {
|
|
2149
|
+
return this.apiClient.dispatch("settlement-partnersDeriveCommission", input);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Canonical settlement intake: report a revenue event; the engine derives the
|
|
2153
|
+
* full line set (tax + commission + net waterfall) and records partner
|
|
2154
|
+
* commissions when a saleId is given.
|
|
2155
|
+
*/
|
|
2156
|
+
async deriveSettlementForEvent(input) {
|
|
2157
|
+
return this.apiClient.dispatch("settlement-deriveSettlementForEvent", input);
|
|
2158
|
+
}
|
|
2159
|
+
// --- Tenant admin (also usable by platform admin with an explicit tenantId) ---
|
|
2160
|
+
/** Publish/update the tenant's partner program (directory listing). */
|
|
2161
|
+
async publishProgram(input) {
|
|
2162
|
+
return this.apiClient.dispatch("settlement-partnersPublishProgram", input);
|
|
2163
|
+
}
|
|
2164
|
+
/** Approve a pending/invited registration (optionally overriding the rate). */
|
|
2165
|
+
async approve(input) {
|
|
2166
|
+
return this.apiClient.dispatch("settlement-partnersApprove", input);
|
|
2167
|
+
}
|
|
2168
|
+
/** Add an already-registered partner directly by id (arranged relationship). */
|
|
2169
|
+
async addById(input) {
|
|
2170
|
+
return this.apiClient.dispatch("settlement-partnersAddById", input);
|
|
2171
|
+
}
|
|
2172
|
+
/** Invite a partner by email (fallback for someone not yet self-registered). */
|
|
2173
|
+
async invite(input) {
|
|
2174
|
+
return this.apiClient.dispatch("settlement-partnersInvite", input);
|
|
2175
|
+
}
|
|
2176
|
+
/** List the partners registered in the current tenant. */
|
|
2177
|
+
async list(tenantId) {
|
|
2178
|
+
return this.apiClient.dispatch("settlement-partnersList", tenantId ? { tenantId } : {});
|
|
2179
|
+
}
|
|
2180
|
+
/** Update a registration's rate and/or status. */
|
|
2181
|
+
async update(input) {
|
|
2182
|
+
return this.apiClient.dispatch("settlement-partnersUpdateRegistration", input);
|
|
2183
|
+
}
|
|
2184
|
+
/** Record a commission accrual for the current tenant (idempotent on idempotencyKey). */
|
|
2185
|
+
async recordCommission(input) {
|
|
2186
|
+
return this.apiClient.dispatch("settlement-partnersRecordCommission", input);
|
|
2187
|
+
}
|
|
2188
|
+
/** Claw back a commission entry. */
|
|
2189
|
+
async reverseCommission(entryId) {
|
|
2190
|
+
return this.apiClient.dispatch("settlement-partnersReverseCommission", { entryId });
|
|
2191
|
+
}
|
|
2192
|
+
/** Record a manual payout against a partner's owed commission. */
|
|
2193
|
+
async recordPayout(input) {
|
|
2194
|
+
return this.apiClient.dispatch("settlement-partnersRecordPayout", input);
|
|
2195
|
+
}
|
|
2196
|
+
// --- Partner self-view ---
|
|
2197
|
+
/** Accept an invitation, claiming the partner identity (requires a verified email). */
|
|
2198
|
+
async acceptInvitation(partnerId, tenantId) {
|
|
2199
|
+
return this.apiClient.dispatch("settlement-partnersAcceptInvitation", { partnerId, tenantId });
|
|
2200
|
+
}
|
|
2201
|
+
/** The signed-in partner's own cross-tenant commission statement (union). */
|
|
2202
|
+
async myCommissions() {
|
|
2203
|
+
return this.apiClient.dispatch("settlement-partnersMyCommissions", {});
|
|
2204
|
+
}
|
|
2205
|
+
// --- Platform admin (Guild) ---
|
|
2206
|
+
/** Any partner's statement (platform admin only). */
|
|
2207
|
+
async commissionsOf(partnerId) {
|
|
2208
|
+
return this.apiClient.dispatch("settlement-partnersGetStatement", { partnerId });
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
|
|
2212
|
+
// src/services/storage.ts
|
|
2213
|
+
var MAKEMINDStorage = class {
|
|
2214
|
+
constructor(config) {
|
|
2215
|
+
this.apiClient = config.apiClient;
|
|
2216
|
+
this.getTenantId = config.tenantId;
|
|
2217
|
+
}
|
|
2218
|
+
get tenantId() {
|
|
2219
|
+
const id = this.getTenantId();
|
|
2220
|
+
if (!id) {
|
|
2221
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No tenant context set");
|
|
2222
|
+
}
|
|
2223
|
+
return id;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Encode a storage object path for use as a path parameter, preserving the
|
|
2227
|
+
* `/` separators (the gateway captures the full sub-path via a wildcard).
|
|
2228
|
+
*/
|
|
2229
|
+
encodePath(path) {
|
|
2230
|
+
return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Upload a file
|
|
2234
|
+
*/
|
|
2235
|
+
async upload(options) {
|
|
2236
|
+
const { uploadUrl } = await this.apiClient.post(
|
|
2237
|
+
"/storage/upload",
|
|
2238
|
+
{
|
|
2239
|
+
path: options.path,
|
|
2240
|
+
contentType: options.file.type || "application/octet-stream",
|
|
2241
|
+
metadata: options.metadata
|
|
2242
|
+
}
|
|
2243
|
+
);
|
|
2244
|
+
await this.uploadToSignedUrl(uploadUrl, options.file, options.onProgress);
|
|
2245
|
+
return {
|
|
2246
|
+
path: options.path,
|
|
2247
|
+
url: await this.getDownloadUrl(options.path),
|
|
2248
|
+
size: options.file.size,
|
|
2249
|
+
contentType: options.file.type || "application/octet-stream",
|
|
2250
|
+
metadata: options.metadata
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
async uploadToSignedUrl(url, file, onProgress) {
|
|
2254
|
+
return new Promise((resolve, reject) => {
|
|
2255
|
+
const xhr = new XMLHttpRequest();
|
|
2256
|
+
if (onProgress) {
|
|
2257
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
2258
|
+
if (event.lengthComputable) {
|
|
2259
|
+
const progress = event.loaded / event.total * 100;
|
|
2260
|
+
onProgress(progress);
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
xhr.addEventListener("load", () => {
|
|
2265
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
2266
|
+
resolve();
|
|
2267
|
+
} else {
|
|
2268
|
+
reject(new MAKEMINDError("STORAGE_ERROR", `Upload failed with status ${xhr.status}`));
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
xhr.addEventListener("error", () => {
|
|
2272
|
+
reject(new MAKEMINDError("STORAGE_ERROR", "Upload failed"));
|
|
2273
|
+
});
|
|
2274
|
+
xhr.open("PUT", url);
|
|
2275
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
2276
|
+
xhr.send(file);
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Get download URL for a file
|
|
2281
|
+
*/
|
|
2282
|
+
async getDownloadUrl(path) {
|
|
2283
|
+
const response = await this.apiClient.get(
|
|
2284
|
+
`/storage/download/${this.encodePath(path)}`
|
|
2285
|
+
);
|
|
2286
|
+
return response.downloadUrl;
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Download a file directly
|
|
2290
|
+
* Returns the file as a Blob
|
|
2291
|
+
*/
|
|
2292
|
+
async download(path) {
|
|
2293
|
+
const url = await this.getDownloadUrl(path);
|
|
2294
|
+
const response = await fetch(url, {
|
|
2295
|
+
method: "GET"
|
|
2296
|
+
});
|
|
2297
|
+
if (!response.ok) {
|
|
2298
|
+
throw new MAKEMINDError("STORAGE_ERROR", `Download failed with status ${response.status}`);
|
|
2299
|
+
}
|
|
2300
|
+
return response.blob();
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Download a file and trigger browser download
|
|
2304
|
+
*/
|
|
2305
|
+
async downloadToFile(path, filename) {
|
|
2306
|
+
const blob = await this.download(path);
|
|
2307
|
+
const downloadFilename = filename || path.split("/").pop() || "download";
|
|
2308
|
+
const link = document.createElement("a");
|
|
2309
|
+
link.href = URL.createObjectURL(blob);
|
|
2310
|
+
link.download = downloadFilename;
|
|
2311
|
+
document.body.appendChild(link);
|
|
2312
|
+
link.click();
|
|
2313
|
+
document.body.removeChild(link);
|
|
2314
|
+
URL.revokeObjectURL(link.href);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Get file metadata
|
|
2318
|
+
*/
|
|
2319
|
+
async getMetadata(path) {
|
|
2320
|
+
const response = await this.apiClient.get(`/storage/${this.encodePath(path)}/metadata`);
|
|
2321
|
+
return {
|
|
2322
|
+
name: response.path.split("/").pop() || response.path,
|
|
2323
|
+
path: response.path,
|
|
2324
|
+
size: response.size,
|
|
2325
|
+
contentType: response.contentType,
|
|
2326
|
+
createdAt: new Date(response.created),
|
|
2327
|
+
updatedAt: new Date(response.updated),
|
|
2328
|
+
metadata: response.metadata
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Update file metadata
|
|
2333
|
+
*/
|
|
2334
|
+
async updateMetadata(path, metadata) {
|
|
2335
|
+
await this.apiClient.patch(`/storage/${this.encodePath(path)}/metadata`, {
|
|
2336
|
+
metadata
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* List files in a directory
|
|
2341
|
+
*/
|
|
2342
|
+
async listFiles(options) {
|
|
2343
|
+
const params = new URLSearchParams();
|
|
2344
|
+
if (options?.prefix) params.append("prefix", options.prefix);
|
|
2345
|
+
if (options?.maxResults) params.append("limit", String(options.maxResults));
|
|
2346
|
+
if (options?.pageToken) params.append("cursor", options.pageToken);
|
|
2347
|
+
const items = await this.apiClient.get(`/storage/list?${params.toString()}`);
|
|
2348
|
+
return {
|
|
2349
|
+
files: items.map((file) => ({
|
|
2350
|
+
name: file.path.split("/").pop() || file.path,
|
|
2351
|
+
path: file.path,
|
|
2352
|
+
size: file.size,
|
|
2353
|
+
contentType: file.contentType,
|
|
2354
|
+
createdAt: new Date(file.updated),
|
|
2355
|
+
updatedAt: new Date(file.updated)
|
|
2356
|
+
}))
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Delete a file
|
|
2361
|
+
*/
|
|
2362
|
+
async delete(path) {
|
|
2363
|
+
await this.apiClient.delete(`/storage/${this.encodePath(path)}`);
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Delete multiple files
|
|
2367
|
+
*/
|
|
2368
|
+
// TODO(gateway): no matching route — no batch delete route nor dispatch operation;
|
|
2369
|
+
// falls back to issuing individual DELETE /storage/:path requests.
|
|
2370
|
+
async deleteMany(paths) {
|
|
2371
|
+
await Promise.all(paths.map((path) => this.delete(path)));
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Copy a file
|
|
2375
|
+
*/
|
|
2376
|
+
async copy(sourcePath, destinationPath) {
|
|
2377
|
+
return this.apiClient.post("/storage/copy", {
|
|
2378
|
+
source: sourcePath,
|
|
2379
|
+
destination: destinationPath
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Move a file
|
|
2384
|
+
*/
|
|
2385
|
+
async move(sourcePath, destinationPath) {
|
|
2386
|
+
return this.apiClient.post("/storage/move", {
|
|
2387
|
+
source: sourcePath,
|
|
2388
|
+
destination: destinationPath
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
// PLM-specific helpers
|
|
2392
|
+
/**
|
|
2393
|
+
* Upload a document for a product
|
|
2394
|
+
*/
|
|
2395
|
+
async uploadProductDocument(productId, file, options) {
|
|
2396
|
+
const path = `products/${productId}/documents/${file.name}`;
|
|
2397
|
+
return this.upload({
|
|
2398
|
+
path,
|
|
2399
|
+
file,
|
|
2400
|
+
metadata: {
|
|
2401
|
+
productId,
|
|
2402
|
+
documentType: options?.documentType || "general",
|
|
2403
|
+
version: options?.version || "1.0"
|
|
2404
|
+
},
|
|
2405
|
+
onProgress: options?.onProgress
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Upload an ECO attachment
|
|
2410
|
+
*/
|
|
2411
|
+
async uploadEcoAttachment(ecoId, file, onProgress) {
|
|
2412
|
+
const path = `eco/${ecoId}/attachments/${file.name}`;
|
|
2413
|
+
return this.upload({
|
|
2414
|
+
path,
|
|
2415
|
+
file,
|
|
2416
|
+
metadata: {
|
|
2417
|
+
ecoId
|
|
2418
|
+
},
|
|
2419
|
+
onProgress
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Upload a quality issue attachment
|
|
2424
|
+
*/
|
|
2425
|
+
async uploadQualityAttachment(issueId, file, onProgress) {
|
|
2426
|
+
const path = `quality/${issueId}/attachments/${file.name}`;
|
|
2427
|
+
return this.upload({
|
|
2428
|
+
path,
|
|
2429
|
+
file,
|
|
2430
|
+
metadata: {
|
|
2431
|
+
issueId
|
|
2432
|
+
},
|
|
2433
|
+
onProgress
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* Get all documents for a product
|
|
2438
|
+
*/
|
|
2439
|
+
async getProductDocuments(productId) {
|
|
2440
|
+
const result = await this.listFiles({
|
|
2441
|
+
prefix: `products/${productId}/documents/`
|
|
2442
|
+
});
|
|
2443
|
+
return result.files;
|
|
2444
|
+
}
|
|
2445
|
+
// Bucket Management (05-data-platform-saas.md)
|
|
2446
|
+
/**
|
|
2447
|
+
* List all buckets for the tenant
|
|
2448
|
+
*/
|
|
2449
|
+
async listBuckets() {
|
|
2450
|
+
const response = await this.apiClient.get("/storage/buckets");
|
|
2451
|
+
return response.buckets;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Create a new bucket.
|
|
2455
|
+
*
|
|
2456
|
+
* The REST route expects { name, settings } where settings carries the
|
|
2457
|
+
* bucket policy fields. publicAccess, corsConfig, and lifecycleRules from
|
|
2458
|
+
* CreateBucketOptions are folded into the settings envelope so the backend
|
|
2459
|
+
* handler can read them from req.body.settings.
|
|
2460
|
+
*/
|
|
2461
|
+
async createBucket(options) {
|
|
2462
|
+
return this.apiClient.post("/storage/buckets", {
|
|
2463
|
+
name: options.name,
|
|
2464
|
+
settings: {
|
|
2465
|
+
publicAccess: options.publicAccess ?? false,
|
|
2466
|
+
corsConfig: options.corsConfig,
|
|
2467
|
+
lifecycleRules: options.lifecycleRules
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Get bucket info
|
|
2473
|
+
*/
|
|
2474
|
+
async getBucket(bucketId) {
|
|
2475
|
+
return this.apiClient.get(`/storage/buckets/${bucketId}`);
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Update bucket settings.
|
|
2479
|
+
*
|
|
2480
|
+
* The REST route reads { settings } from req.body. Flatten the
|
|
2481
|
+
* CreateBucketOptions partial into a settings envelope so the backend
|
|
2482
|
+
* merges the diff correctly.
|
|
2483
|
+
*/
|
|
2484
|
+
async updateBucket(bucketId, updates) {
|
|
2485
|
+
const { name: _name, ...rest } = updates;
|
|
2486
|
+
return this.apiClient.patch(`/storage/buckets/${bucketId}`, {
|
|
2487
|
+
settings: rest
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Delete a bucket (must be empty)
|
|
2492
|
+
*/
|
|
2493
|
+
async deleteBucket(bucketId) {
|
|
2494
|
+
await this.apiClient.delete(`/storage/buckets/${bucketId}`);
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Get a pre-signed URL for direct upload (bypassing SDK)
|
|
2498
|
+
*/
|
|
2499
|
+
async getUploadUrl(path, options) {
|
|
2500
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
2501
|
+
const fileName = segments.pop() ?? path;
|
|
2502
|
+
const folder = segments.join("/");
|
|
2503
|
+
const response = await this.apiClient.dispatch("dataGetUploadUrl", {
|
|
2504
|
+
fileName,
|
|
2505
|
+
contentType: options?.contentType || "application/octet-stream",
|
|
2506
|
+
...folder ? { folder } : {}
|
|
2507
|
+
});
|
|
2508
|
+
return { uploadUrl: response.uploadUrl, fileId: response.fileId };
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Check if a file exists
|
|
2512
|
+
*/
|
|
2513
|
+
async exists(path) {
|
|
2514
|
+
try {
|
|
2515
|
+
await this.getMetadata(path);
|
|
2516
|
+
return true;
|
|
2517
|
+
} catch {
|
|
2518
|
+
return false;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Get storage usage statistics
|
|
2523
|
+
*/
|
|
2524
|
+
async getUsageStats() {
|
|
2525
|
+
return this.apiClient.get("/storage/stats");
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
|
|
2529
|
+
// src/services/ai.ts
|
|
2530
|
+
var MAKEMINDAI = class {
|
|
2531
|
+
constructor(config) {
|
|
2532
|
+
this.apiClient = config.apiClient;
|
|
2533
|
+
this.getTenantId = config.tenantId;
|
|
2534
|
+
}
|
|
2535
|
+
// Chat completion
|
|
2536
|
+
async chat(messages, options) {
|
|
2537
|
+
const requestMessages = options?.systemPrompt ? [{ role: "system", content: options.systemPrompt }, ...messages] : messages;
|
|
2538
|
+
const response = await this.apiClient.dispatch("ai-aiChat", {
|
|
2539
|
+
tenantId: this.getTenantId(),
|
|
2540
|
+
messages: requestMessages,
|
|
2541
|
+
model: options?.model,
|
|
2542
|
+
temperature: options?.temperature,
|
|
2543
|
+
maxTokens: options?.maxTokens
|
|
2544
|
+
});
|
|
2545
|
+
return {
|
|
2546
|
+
message: { role: "assistant", content: response.content },
|
|
2547
|
+
usage: {
|
|
2548
|
+
promptTokens: response.usage.inputTokens,
|
|
2549
|
+
completionTokens: response.usage.outputTokens,
|
|
2550
|
+
totalTokens: response.usage.totalTokens
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Stream chat completion via the aiStreamChat onCall backend.
|
|
2556
|
+
*
|
|
2557
|
+
* The backend writes chunks progressively to Firestore
|
|
2558
|
+
* (aiStreamChunks/{streamId}/chunks). This method polls the stream
|
|
2559
|
+
* document until the status reaches 'completed' or 'failed', invoking
|
|
2560
|
+
* onChunk for each content chunk in order. Callers that need real-time
|
|
2561
|
+
* granularity should subscribe to the Firestore collection directly using
|
|
2562
|
+
* the returned streamId from aiStreamChat.
|
|
2563
|
+
*
|
|
2564
|
+
* @param messages - Chat messages
|
|
2565
|
+
* @param onChunk - Callback for each streamed content chunk
|
|
2566
|
+
* @param options - Chat options and optional onDone callback
|
|
2567
|
+
* @returns Promise that resolves when streaming is complete
|
|
2568
|
+
*/
|
|
2569
|
+
async chatStream(messages, onChunk, options) {
|
|
2570
|
+
const requestMessages = options?.systemPrompt ? [{ role: "system", content: options.systemPrompt }, ...messages] : messages;
|
|
2571
|
+
const startResult = await this.apiClient.dispatch("ai-aiStreamChat", {
|
|
2572
|
+
tenantId: this.getTenantId(),
|
|
2573
|
+
messages: requestMessages,
|
|
2574
|
+
model: options?.model,
|
|
2575
|
+
temperature: options?.temperature,
|
|
2576
|
+
maxTokens: options?.maxTokens
|
|
2577
|
+
});
|
|
2578
|
+
const { streamId } = startResult;
|
|
2579
|
+
const pollInterval = 500;
|
|
2580
|
+
const maxWaitMs = 12e4;
|
|
2581
|
+
const startAt = Date.now();
|
|
2582
|
+
let lastContent = "";
|
|
2583
|
+
while (Date.now() - startAt < maxWaitMs) {
|
|
2584
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
2585
|
+
const poll = await this.apiClient.dispatch("ai-aiGetStreamResult", { streamId });
|
|
2586
|
+
if (poll.status === "failed") {
|
|
2587
|
+
throw new Error(poll.error ?? "Stream failed");
|
|
2588
|
+
}
|
|
2589
|
+
if (poll.status === "completed") {
|
|
2590
|
+
const fullContent = poll.content ?? "";
|
|
2591
|
+
const delta2 = fullContent.slice(lastContent.length);
|
|
2592
|
+
if (delta2) {
|
|
2593
|
+
onChunk(delta2);
|
|
2594
|
+
}
|
|
2595
|
+
const rawUsage = poll.usage;
|
|
2596
|
+
const finalResponse = {
|
|
2597
|
+
message: { role: "assistant", content: fullContent },
|
|
2598
|
+
usage: {
|
|
2599
|
+
promptTokens: rawUsage?.inputTokens ?? 0,
|
|
2600
|
+
completionTokens: rawUsage?.outputTokens ?? 0,
|
|
2601
|
+
totalTokens: rawUsage?.totalTokens ?? 0
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
if (options?.onDone) {
|
|
2605
|
+
options.onDone(finalResponse);
|
|
2606
|
+
}
|
|
2607
|
+
return finalResponse;
|
|
2608
|
+
}
|
|
2609
|
+
const currentContent = poll.content ?? "";
|
|
2610
|
+
const delta = currentContent.slice(lastContent.length);
|
|
2611
|
+
if (delta) {
|
|
2612
|
+
onChunk(delta);
|
|
2613
|
+
lastContent = currentContent;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
throw new Error("Stream timed out");
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Stream chat using async generator for easier consumption.
|
|
2620
|
+
* Yields content deltas via polling on aiGetStreamResult.
|
|
2621
|
+
*/
|
|
2622
|
+
async *streamChat(messages, options) {
|
|
2623
|
+
const requestMessages = options?.systemPrompt ? [{ role: "system", content: options.systemPrompt }, ...messages] : messages;
|
|
2624
|
+
const startResult = await this.apiClient.dispatch("ai-aiStreamChat", {
|
|
2625
|
+
tenantId: this.getTenantId(),
|
|
2626
|
+
messages: requestMessages,
|
|
2627
|
+
model: options?.model,
|
|
2628
|
+
temperature: options?.temperature,
|
|
2629
|
+
maxTokens: options?.maxTokens
|
|
2630
|
+
});
|
|
2631
|
+
const { streamId } = startResult;
|
|
2632
|
+
const pollInterval = 500;
|
|
2633
|
+
const maxWaitMs = 12e4;
|
|
2634
|
+
const startAt = Date.now();
|
|
2635
|
+
let lastContent = "";
|
|
2636
|
+
while (Date.now() - startAt < maxWaitMs) {
|
|
2637
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
2638
|
+
const poll = await this.apiClient.dispatch("ai-aiGetStreamResult", { streamId });
|
|
2639
|
+
if (poll.status === "failed") {
|
|
2640
|
+
throw new Error(poll.error ?? "Stream failed");
|
|
2641
|
+
}
|
|
2642
|
+
const currentContent = poll.content ?? "";
|
|
2643
|
+
const delta = currentContent.slice(lastContent.length);
|
|
2644
|
+
if (delta) {
|
|
2645
|
+
yield delta;
|
|
2646
|
+
lastContent = currentContent;
|
|
2647
|
+
}
|
|
2648
|
+
if (poll.status === "completed") {
|
|
2649
|
+
const rawUsage = poll.usage;
|
|
2650
|
+
return {
|
|
2651
|
+
message: { role: "assistant", content: currentContent },
|
|
2652
|
+
usage: {
|
|
2653
|
+
promptTokens: rawUsage?.inputTokens ?? 0,
|
|
2654
|
+
completionTokens: rawUsage?.outputTokens ?? 0,
|
|
2655
|
+
totalTokens: rawUsage?.totalTokens ?? 0
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
throw new Error("Stream timed out");
|
|
2661
|
+
}
|
|
2662
|
+
// Embeddings
|
|
2663
|
+
async createEmbedding(text, options) {
|
|
2664
|
+
const texts = Array.isArray(text) ? text : [text];
|
|
2665
|
+
const response = await this.apiClient.dispatch("ai-aiEmbeddings", {
|
|
2666
|
+
tenantId: this.getTenantId(),
|
|
2667
|
+
input: texts,
|
|
2668
|
+
model: options?.model
|
|
2669
|
+
});
|
|
2670
|
+
return { embeddings: response.embeddings, usage: response.usage };
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Vector search over a tenant knowledge collection.
|
|
2674
|
+
* Routes to the knowledge domain dispatch (knowledge-knowledgeSearch),
|
|
2675
|
+
* which performs semantic search with embeddings over indexed documents.
|
|
2676
|
+
*/
|
|
2677
|
+
async vectorSearch(collection, query, options) {
|
|
2678
|
+
const response = await this.apiClient.dispatch("knowledge-knowledgeSearch", {
|
|
2679
|
+
tenantId: this.getTenantId(),
|
|
2680
|
+
collectionId: collection,
|
|
2681
|
+
query,
|
|
2682
|
+
limit: options?.limit,
|
|
2683
|
+
filter: options?.filter
|
|
2684
|
+
});
|
|
2685
|
+
return response.results;
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Vision analysis for a publicly accessible image URL.
|
|
2689
|
+
* Routes to ai-aiVision (aiVision onCall). The image is passed as a
|
|
2690
|
+
* ChatCompletionRequest message using the imageUrl ContentPart type.
|
|
2691
|
+
*/
|
|
2692
|
+
async analyzeImage(imageUrl, prompt, options) {
|
|
2693
|
+
const response = await this.apiClient.dispatch("ai-aiVision", {
|
|
2694
|
+
tenantId: this.getTenantId(),
|
|
2695
|
+
model: options?.model,
|
|
2696
|
+
maxTokens: options?.maxTokens,
|
|
2697
|
+
messages: [
|
|
2698
|
+
{
|
|
2699
|
+
role: "user",
|
|
2700
|
+
content: [
|
|
2701
|
+
{ type: "imageUrl", imageUrl: { url: imageUrl, detail: "auto" } },
|
|
2702
|
+
{ type: "text", text: prompt }
|
|
2703
|
+
]
|
|
2704
|
+
}
|
|
2705
|
+
]
|
|
2706
|
+
});
|
|
2707
|
+
return {
|
|
2708
|
+
content: response.content,
|
|
2709
|
+
usage: { totalTokens: response.usage.totalTokens }
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Vision analysis for a base64-encoded image.
|
|
2714
|
+
* The image is embedded as a data URI in the imageUrl ContentPart.
|
|
2715
|
+
*/
|
|
2716
|
+
async analyzeImageBase64(base64Image, mimeType, prompt, options) {
|
|
2717
|
+
const dataUri = `data:${mimeType};base64,${base64Image}`;
|
|
2718
|
+
const response = await this.apiClient.dispatch("ai-aiVision", {
|
|
2719
|
+
tenantId: this.getTenantId(),
|
|
2720
|
+
model: options?.model,
|
|
2721
|
+
maxTokens: options?.maxTokens,
|
|
2722
|
+
messages: [
|
|
2723
|
+
{
|
|
2724
|
+
role: "user",
|
|
2725
|
+
content: [
|
|
2726
|
+
{ type: "imageUrl", imageUrl: { url: dataUri, detail: "auto" } },
|
|
2727
|
+
{ type: "text", text: prompt }
|
|
2728
|
+
]
|
|
2729
|
+
}
|
|
2730
|
+
]
|
|
2731
|
+
});
|
|
2732
|
+
return {
|
|
2733
|
+
content: response.content,
|
|
2734
|
+
usage: { totalTokens: response.usage.totalTokens }
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Chat completion with tool definitions (function calling).
|
|
2739
|
+
* Routes to ai-aiChat — the aiChat onCall natively supports the tools field
|
|
2740
|
+
* in ChatCompletionRequest, no separate endpoint required.
|
|
2741
|
+
*/
|
|
2742
|
+
async chatWithTools(messages, tools, options) {
|
|
2743
|
+
const requestMessages = options?.systemPrompt ? [{ role: "system", content: options.systemPrompt }, ...messages] : messages;
|
|
2744
|
+
const aiTools = tools.map((t) => ({
|
|
2745
|
+
type: "function",
|
|
2746
|
+
function: {
|
|
2747
|
+
name: t.name,
|
|
2748
|
+
description: t.description,
|
|
2749
|
+
parameters: t.parameters
|
|
2750
|
+
}
|
|
2751
|
+
}));
|
|
2752
|
+
const response = await this.apiClient.dispatch("ai-aiChat", {
|
|
2753
|
+
tenantId: this.getTenantId(),
|
|
2754
|
+
messages: requestMessages,
|
|
2755
|
+
tools: aiTools,
|
|
2756
|
+
model: options?.model,
|
|
2757
|
+
temperature: options?.temperature,
|
|
2758
|
+
maxTokens: options?.maxTokens
|
|
2759
|
+
});
|
|
2760
|
+
const toolCalls = response.toolCalls?.map((tc) => ({
|
|
2761
|
+
name: tc.function.name,
|
|
2762
|
+
arguments: (() => {
|
|
2763
|
+
try {
|
|
2764
|
+
return JSON.parse(tc.function.arguments);
|
|
2765
|
+
} catch {
|
|
2766
|
+
return {};
|
|
2767
|
+
}
|
|
2768
|
+
})()
|
|
2769
|
+
}));
|
|
2770
|
+
return {
|
|
2771
|
+
message: { role: "assistant", content: response.content },
|
|
2772
|
+
toolCalls,
|
|
2773
|
+
usage: {
|
|
2774
|
+
promptTokens: response.usage.inputTokens,
|
|
2775
|
+
completionTokens: response.usage.outputTokens,
|
|
2776
|
+
totalTokens: response.usage.totalTokens
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Document analysis (summary, field extraction, or QA).
|
|
2782
|
+
* No dedicated onCall exists in the ai domain; implemented via ai-aiChat
|
|
2783
|
+
* with a structured system prompt that targets the requested analysis type.
|
|
2784
|
+
* The document content must be fetched by the caller and passed as text —
|
|
2785
|
+
* the backend has no URL-fetch / OCR pipeline for onCall yet.
|
|
2786
|
+
*/
|
|
2787
|
+
async analyzeDocument(documentUrl, analysisType, options) {
|
|
2788
|
+
const systemPromptMap = {
|
|
2789
|
+
summary: "You are a document summarization assistant. Summarize the provided document concisely.",
|
|
2790
|
+
extract: `You are a document extraction assistant. Extract the following fields from the document as JSON: ${(options?.extractFields ?? []).join(", ")}.`,
|
|
2791
|
+
qa: `You are a document QA assistant. Answer the following question based on the document: ${options?.question ?? ""}`
|
|
2792
|
+
};
|
|
2793
|
+
const response = await this.apiClient.dispatch("ai-aiChat", {
|
|
2794
|
+
tenantId: this.getTenantId(),
|
|
2795
|
+
messages: [
|
|
2796
|
+
{ role: "system", content: systemPromptMap[analysisType] },
|
|
2797
|
+
{ role: "user", content: `Document URL: ${documentUrl}` }
|
|
2798
|
+
]
|
|
2799
|
+
});
|
|
2800
|
+
let result = response.content;
|
|
2801
|
+
if (analysisType === "extract") {
|
|
2802
|
+
try {
|
|
2803
|
+
result = JSON.parse(response.content);
|
|
2804
|
+
} catch {
|
|
2805
|
+
result = response.content;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return {
|
|
2809
|
+
result,
|
|
2810
|
+
usage: { totalTokens: response.usage.totalTokens }
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
// Text generation
|
|
2814
|
+
async generate(prompt, options) {
|
|
2815
|
+
const response = await this.chat(
|
|
2816
|
+
[{ role: "user", content: prompt }],
|
|
2817
|
+
options
|
|
2818
|
+
);
|
|
2819
|
+
return {
|
|
2820
|
+
text: response.message.content,
|
|
2821
|
+
usage: { totalTokens: response.usage.totalTokens }
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Text summarization via ai-aiChat.
|
|
2826
|
+
* No dedicated onCall exists; implemented as a structured chat request
|
|
2827
|
+
* with a summarization system prompt.
|
|
2828
|
+
*/
|
|
2829
|
+
async summarize(text, options) {
|
|
2830
|
+
const styleInstruction = options?.style === "bullet" ? "Use bullet points." : "Write in paragraph form.";
|
|
2831
|
+
const lengthInstruction = options?.maxLength ? ` Keep the summary under ${options.maxLength} words.` : "";
|
|
2832
|
+
const response = await this.apiClient.dispatch("ai-aiChat", {
|
|
2833
|
+
tenantId: this.getTenantId(),
|
|
2834
|
+
messages: [
|
|
2835
|
+
{
|
|
2836
|
+
role: "system",
|
|
2837
|
+
content: `You are a summarization assistant. Summarize the text concisely. ${styleInstruction}${lengthInstruction}`
|
|
2838
|
+
},
|
|
2839
|
+
{ role: "user", content: text }
|
|
2840
|
+
]
|
|
2841
|
+
});
|
|
2842
|
+
return {
|
|
2843
|
+
summary: response.content,
|
|
2844
|
+
usage: { totalTokens: response.usage.totalTokens }
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Text translation via ai-aiChat.
|
|
2849
|
+
* No dedicated onCall exists; implemented as a structured chat request
|
|
2850
|
+
* with a translation system prompt.
|
|
2851
|
+
*/
|
|
2852
|
+
async translate(text, targetLanguage, sourceLanguage) {
|
|
2853
|
+
const sourceInstruction = sourceLanguage ? ` The source language is ${sourceLanguage}.` : " Detect the source language automatically.";
|
|
2854
|
+
const response = await this.apiClient.dispatch("ai-aiChat", {
|
|
2855
|
+
tenantId: this.getTenantId(),
|
|
2856
|
+
messages: [
|
|
2857
|
+
{
|
|
2858
|
+
role: "system",
|
|
2859
|
+
content: `You are a translation assistant.${sourceInstruction} Translate the user's text to ${targetLanguage}. Return only the translation, nothing else.`
|
|
2860
|
+
},
|
|
2861
|
+
{ role: "user", content: text }
|
|
2862
|
+
]
|
|
2863
|
+
});
|
|
2864
|
+
return {
|
|
2865
|
+
translation: response.content,
|
|
2866
|
+
usage: { totalTokens: response.usage.totalTokens }
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2871
|
+
// src/services/search.ts
|
|
2872
|
+
var MAKEMINDSearch = class {
|
|
2873
|
+
constructor(config) {
|
|
2874
|
+
this.apiClient = config.apiClient;
|
|
2875
|
+
this.getTenantId = config.tenantId;
|
|
2876
|
+
}
|
|
2877
|
+
async query(options) {
|
|
2878
|
+
const params = new URLSearchParams();
|
|
2879
|
+
params.set("q", options.q);
|
|
2880
|
+
if (options.collections) params.set("collections", options.collections.join(","));
|
|
2881
|
+
if (options.page) params.set("page", String(options.page));
|
|
2882
|
+
if (options.perPage) params.set("perPage", String(options.perPage));
|
|
2883
|
+
return this.apiClient.get(`/search?${params.toString()}`);
|
|
2884
|
+
}
|
|
2885
|
+
async advanced(options) {
|
|
2886
|
+
return this.apiClient.post("/search/advanced", options);
|
|
2887
|
+
}
|
|
2888
|
+
async suggestions(options) {
|
|
2889
|
+
const params = new URLSearchParams();
|
|
2890
|
+
params.set("prefix", options.prefix);
|
|
2891
|
+
params.set("collection", options.collection);
|
|
2892
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
2893
|
+
return this.apiClient.get(`/search/suggestions?${params.toString()}`);
|
|
2894
|
+
}
|
|
2895
|
+
async getAnalytics(options) {
|
|
2896
|
+
const params = new URLSearchParams();
|
|
2897
|
+
if (options?.startDate) params.set("startDate", options.startDate);
|
|
2898
|
+
if (options?.endDate) params.set("endDate", options.endDate);
|
|
2899
|
+
const queryString = params.toString();
|
|
2900
|
+
return this.apiClient.get(`/search/analytics${queryString ? `?${queryString}` : ""}`);
|
|
2901
|
+
}
|
|
2902
|
+
async bulkReindex(options) {
|
|
2903
|
+
return this.apiClient.post("/search/reindex", options || {});
|
|
2904
|
+
}
|
|
2905
|
+
async getBulkReindexStatus(options) {
|
|
2906
|
+
return this.apiClient.get(`/search/reindex/${options.jobId}`);
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
|
|
2910
|
+
// src/services/apps.ts
|
|
2911
|
+
var MAKEMINDApps = class {
|
|
2912
|
+
constructor(config) {
|
|
2913
|
+
this.apiClient = config.apiClient;
|
|
2914
|
+
this.getTenantId = config.tenantId;
|
|
2915
|
+
}
|
|
2916
|
+
get tenantId() {
|
|
2917
|
+
const id = this.getTenantId();
|
|
2918
|
+
if (!id) {
|
|
2919
|
+
throw new MAKEMINDError("UNAUTHENTICATED", "No tenant context set");
|
|
2920
|
+
}
|
|
2921
|
+
return id;
|
|
2922
|
+
}
|
|
2923
|
+
// ============================================
|
|
2924
|
+
// App Management
|
|
2925
|
+
// ============================================
|
|
2926
|
+
async createApp(params) {
|
|
2927
|
+
const response = await this.apiClient.post("/apps", { ...params });
|
|
2928
|
+
return response;
|
|
2929
|
+
}
|
|
2930
|
+
async getApp(appId) {
|
|
2931
|
+
try {
|
|
2932
|
+
const response = await this.apiClient.get(`/apps/${appId}`);
|
|
2933
|
+
return response.app;
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
if (error instanceof MAKEMINDError && error.code === "NOT_FOUND") {
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
throw error;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async listApps(params = {}) {
|
|
2942
|
+
const query = new URLSearchParams();
|
|
2943
|
+
if (params.status) query.append("status", params.status);
|
|
2944
|
+
if (params.limit) query.append("limit", String(params.limit));
|
|
2945
|
+
if (params.startAfter) query.append("startAfter", params.startAfter);
|
|
2946
|
+
const qs = query.toString();
|
|
2947
|
+
const response = await this.apiClient.get(`/apps${qs ? `?${qs}` : ""}`);
|
|
2948
|
+
return response;
|
|
2949
|
+
}
|
|
2950
|
+
async updateApp(appId, params) {
|
|
2951
|
+
await this.apiClient.put(`/apps/${appId}`, params);
|
|
2952
|
+
}
|
|
2953
|
+
async deleteApp(appId, hardDelete = false) {
|
|
2954
|
+
await this.apiClient.delete(`/apps/${appId}${hardDelete ? "?hardDelete=true" : ""}`);
|
|
2955
|
+
}
|
|
2956
|
+
async duplicateApp(appId, newName) {
|
|
2957
|
+
const response = await this.apiClient.post(
|
|
2958
|
+
`/apps/${appId}/duplicate`,
|
|
2959
|
+
{ name: newName }
|
|
2960
|
+
);
|
|
2961
|
+
return { appId: response.appId };
|
|
2962
|
+
}
|
|
2963
|
+
// Stream app changes (polling-based)
|
|
2964
|
+
onAppSnapshot(appId, callback) {
|
|
2965
|
+
let isSubscribed = true;
|
|
2966
|
+
const poll = async () => {
|
|
2967
|
+
if (!isSubscribed) return;
|
|
2968
|
+
try {
|
|
2969
|
+
const app = await this.getApp(appId);
|
|
2970
|
+
callback(app);
|
|
2971
|
+
} catch {
|
|
2972
|
+
}
|
|
2973
|
+
if (isSubscribed) {
|
|
2974
|
+
setTimeout(poll, 5e3);
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
poll();
|
|
2978
|
+
return () => {
|
|
2979
|
+
isSubscribed = false;
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
// ============================================
|
|
2983
|
+
// Page Management
|
|
2984
|
+
// ============================================
|
|
2985
|
+
async createPage(appId, params) {
|
|
2986
|
+
const response = await this.apiClient.post(
|
|
2987
|
+
`/apps/${appId}/pages`,
|
|
2988
|
+
params
|
|
2989
|
+
);
|
|
2990
|
+
return response;
|
|
2991
|
+
}
|
|
2992
|
+
async getPage(appId, pageId) {
|
|
2993
|
+
try {
|
|
2994
|
+
const response = await this.apiClient.get(
|
|
2995
|
+
`/apps/${appId}/pages/${pageId}`
|
|
2996
|
+
);
|
|
2997
|
+
return response.page;
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
if (error instanceof MAKEMINDError && error.code === "NOT_FOUND") {
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
throw error;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
async listPages(appId) {
|
|
3006
|
+
const response = await this.apiClient.get(`/apps/${appId}/pages`);
|
|
3007
|
+
return response.pages;
|
|
3008
|
+
}
|
|
3009
|
+
async updatePage(appId, pageId, params) {
|
|
3010
|
+
await this.apiClient.put(`/apps/${appId}/pages/${pageId}`, params);
|
|
3011
|
+
}
|
|
3012
|
+
async deletePage(appId, pageId) {
|
|
3013
|
+
await this.apiClient.delete(`/apps/${appId}/pages/${pageId}`);
|
|
3014
|
+
}
|
|
3015
|
+
async reorderPages(appId, order) {
|
|
3016
|
+
await this.apiClient.put(`/apps/${appId}/pages/reorder`, { order });
|
|
3017
|
+
}
|
|
3018
|
+
async duplicatePage(appId, pageId, newName, newPath) {
|
|
3019
|
+
const response = await this.apiClient.post(
|
|
3020
|
+
`/apps/${appId}/pages/${pageId}/duplicate`,
|
|
3021
|
+
{ newName, newPath }
|
|
3022
|
+
);
|
|
3023
|
+
return response;
|
|
3024
|
+
}
|
|
3025
|
+
// ============================================
|
|
3026
|
+
// Publishing & Versioning
|
|
3027
|
+
// ============================================
|
|
3028
|
+
async publish(appId, changelog) {
|
|
3029
|
+
const response = await this.apiClient.post(`/apps/${appId}/publish`, { changelog });
|
|
3030
|
+
return response;
|
|
3031
|
+
}
|
|
3032
|
+
async unpublish(appId) {
|
|
3033
|
+
await this.apiClient.post(`/apps/${appId}/unpublish`, {});
|
|
3034
|
+
}
|
|
3035
|
+
async rollback(appId, targetVersion) {
|
|
3036
|
+
const response = await this.apiClient.post(`/apps/${appId}/rollback`, { targetVersion });
|
|
3037
|
+
return response;
|
|
3038
|
+
}
|
|
3039
|
+
async getVersions(appId, limit = 20) {
|
|
3040
|
+
const response = await this.apiClient.get(
|
|
3041
|
+
`/apps/${appId}/versions?limit=${limit}`
|
|
3042
|
+
);
|
|
3043
|
+
return response.versions;
|
|
3044
|
+
}
|
|
3045
|
+
async getVersionDetail(appId, versionId) {
|
|
3046
|
+
const response = await this.apiClient.get(`/apps/${appId}/versions/${versionId}`);
|
|
3047
|
+
return response.version;
|
|
3048
|
+
}
|
|
3049
|
+
async compareVersions(appId, version1, version2) {
|
|
3050
|
+
const response = await this.apiClient.get(`/apps/${appId}/versions/compare?version1=${version1}&version2=${version2}`);
|
|
3051
|
+
return response.comparison;
|
|
3052
|
+
}
|
|
3053
|
+
// ============================================
|
|
3054
|
+
// Domain Management
|
|
3055
|
+
// ============================================
|
|
3056
|
+
/**
|
|
3057
|
+
* Add a platform subdomain (`<subdomain>.<platform-host>`) for an app.
|
|
3058
|
+
* `target` is an optional generic reverse-proxy routing target.
|
|
3059
|
+
*
|
|
3060
|
+
* Requires domain-service activation. The control-plane call is live, but
|
|
3061
|
+
* a registered domain/target does not serve traffic until the
|
|
3062
|
+
* domain-serving edge is provisioned (Phase 2). Custom and nested
|
|
3063
|
+
* (sub-tenant) domains additionally require an active domain entitlement
|
|
3064
|
+
* (paid SKU); pricing is not yet set. Subdomains are free.
|
|
3065
|
+
*/
|
|
3066
|
+
async addSubdomain(appId, subdomain, target) {
|
|
3067
|
+
const response = await this.apiClient.post(`/apps/${appId}/domains/subdomain`, { subdomain, target });
|
|
3068
|
+
return response;
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Add a custom (BYO) domain for an app. Returns a verification token and
|
|
3072
|
+
* DNS records the caller must publish before `verifyDomain` will pass.
|
|
3073
|
+
* `target` is an optional generic reverse-proxy routing target.
|
|
3074
|
+
*
|
|
3075
|
+
* Requires domain-service activation. The control-plane call is live, but
|
|
3076
|
+
* a registered domain/target does not serve traffic until the
|
|
3077
|
+
* domain-serving edge is provisioned (Phase 2). Custom and nested
|
|
3078
|
+
* (sub-tenant) domains additionally require an active domain entitlement
|
|
3079
|
+
* (paid SKU); pricing is not yet set. Subdomains are free.
|
|
3080
|
+
*/
|
|
3081
|
+
async addCustomDomain(appId, domain, target) {
|
|
3082
|
+
const response = await this.apiClient.post(`/apps/${appId}/domains/custom`, { domain, target });
|
|
3083
|
+
return response;
|
|
3084
|
+
}
|
|
3085
|
+
async verifyDomain(appId, domain) {
|
|
3086
|
+
const response = await this.apiClient.post(`/apps/${appId}/domains/${encodeURIComponent(domain)}/verify`, {});
|
|
3087
|
+
return response;
|
|
3088
|
+
}
|
|
3089
|
+
async removeDomain(appId, domain) {
|
|
3090
|
+
await this.apiClient.delete(`/apps/${appId}/domains/${encodeURIComponent(domain)}`);
|
|
3091
|
+
}
|
|
3092
|
+
async getDomainStatus(appId) {
|
|
3093
|
+
const response = await this.apiClient.get(
|
|
3094
|
+
`/apps/${appId}/domains`
|
|
3095
|
+
);
|
|
3096
|
+
return response.domains;
|
|
3097
|
+
}
|
|
3098
|
+
// ============================================
|
|
3099
|
+
// Web Entries — an address that opens an MCP origin as a web app
|
|
3100
|
+
//
|
|
3101
|
+
// An entry gives an app a public address: visiting it serves the renderer
|
|
3102
|
+
// and a per-host document that declares which origin to open. Dispatch ops
|
|
3103
|
+
// under the `apps-` prefix, `domains:read` / `domains:write` — an entry IS a
|
|
3104
|
+
// registry hostname, so splitting authority over one resource across a
|
|
3105
|
+
// second scope name would only make it ambiguous.
|
|
3106
|
+
// ============================================
|
|
3107
|
+
/**
|
|
3108
|
+
* Mint an address for an app.
|
|
3109
|
+
*
|
|
3110
|
+
* The caller proposes a **label**, not a hostname: the platform derives and
|
|
3111
|
+
* reserves the host under its own uniqueness and reserved-name rules. A
|
|
3112
|
+
* caller cannot claim a hostname it does not own, and the origin is derived
|
|
3113
|
+
* from the app being bound rather than supplied — the whole capability rests
|
|
3114
|
+
* on the issuer deciding the origin, and a free-text URL field is exactly how
|
|
3115
|
+
* that property gets lost.
|
|
3116
|
+
*
|
|
3117
|
+
* `display` feeds both the share metadata (`og:*`) and the boot declaration,
|
|
3118
|
+
* so a link preview and the running app cannot disagree.
|
|
3119
|
+
*/
|
|
3120
|
+
async mintWebEntry(params) {
|
|
3121
|
+
return this.apiClient.dispatch("apps-mintWebEntry", params);
|
|
3122
|
+
}
|
|
3123
|
+
/** Read one entry the caller owns. Requires `domains:read`. */
|
|
3124
|
+
async getWebEntry(host) {
|
|
3125
|
+
return this.apiClient.dispatch("apps-getWebEntry", { host });
|
|
3126
|
+
}
|
|
3127
|
+
/** List the caller's entries, optionally narrowed to one party. Requires `domains:read`. */
|
|
3128
|
+
async listWebEntries(params = {}) {
|
|
3129
|
+
return this.apiClient.dispatch("apps-listWebEntries", params);
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Repoint an entry at a different (still proven) origin, or change how it
|
|
3133
|
+
* appears when shared. The address itself is immutable — a different address
|
|
3134
|
+
* is a different entry.
|
|
3135
|
+
*/
|
|
3136
|
+
async updateWebEntry(params) {
|
|
3137
|
+
return this.apiClient.dispatch("apps-updateWebEntry", params);
|
|
3138
|
+
}
|
|
3139
|
+
/** Retire an entry — the address stops serving immediately. Requires `domains:write`. */
|
|
3140
|
+
async retireWebEntry(host) {
|
|
3141
|
+
return this.apiClient.dispatch("apps-retireWebEntry", { host });
|
|
3142
|
+
}
|
|
3143
|
+
// ============================================
|
|
3144
|
+
// Delegated (Sub-Tenant) Domains
|
|
3145
|
+
//
|
|
3146
|
+
// "Custom domains for your customers" — a tenant registers/verifies/lists/
|
|
3147
|
+
// removes domains on behalf of its own sub-tenants (nested delegation).
|
|
3148
|
+
// These are dispatch ops (no REST path); routed through the gateway RPC
|
|
3149
|
+
// dispatch route under the `apps-` prefix. All four require an API key
|
|
3150
|
+
// with the noted scope.
|
|
3151
|
+
// ============================================
|
|
3152
|
+
/**
|
|
3153
|
+
* Register a custom domain on behalf of one of the caller tenant's
|
|
3154
|
+
* sub-tenants ("custom domains for your customers"). `parentTenantId`,
|
|
3155
|
+
* when provided, must equal the caller's own tenantId — the server never
|
|
3156
|
+
* trusts a client-supplied parent tenant for cross-tenant registration.
|
|
3157
|
+
* Requires API key scope `domains:write`.
|
|
3158
|
+
*
|
|
3159
|
+
* Requires domain-service activation. The control-plane call is live, but
|
|
3160
|
+
* a registered domain/target does not serve traffic until the
|
|
3161
|
+
* domain-serving edge is provisioned (Phase 2). Custom and nested
|
|
3162
|
+
* (sub-tenant) domains additionally require an active domain entitlement
|
|
3163
|
+
* (paid SKU); pricing is not yet set. Subdomains are free.
|
|
3164
|
+
*/
|
|
3165
|
+
async registerDomainForSubTenant(params) {
|
|
3166
|
+
return this.apiClient.dispatch("apps-registerDomainForSubTenant", params);
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Verify a delegated sub-tenant domain's DNS configuration.
|
|
3170
|
+
* Requires API key scope `domains:write`.
|
|
3171
|
+
*
|
|
3172
|
+
* Requires domain-service activation. The control-plane call is live, but
|
|
3173
|
+
* a registered domain/target does not serve traffic until the
|
|
3174
|
+
* domain-serving edge is provisioned (Phase 2). Custom and nested
|
|
3175
|
+
* (sub-tenant) domains additionally require an active domain entitlement
|
|
3176
|
+
* (paid SKU); pricing is not yet set. Subdomains are free.
|
|
3177
|
+
*/
|
|
3178
|
+
async verifyDomainForSubTenant(domain) {
|
|
3179
|
+
return this.apiClient.dispatch("apps-verifyDomainForSubTenant", { domain });
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* List the caller tenant's delegated sub-tenant domains, optionally
|
|
3183
|
+
* filtered to one `subTenantId`. Requires API key scope `domains:read`.
|
|
3184
|
+
*
|
|
3185
|
+
* Requires domain-service activation. The control-plane call is live, but
|
|
3186
|
+
* a registered domain/target does not serve traffic until the
|
|
3187
|
+
* domain-serving edge is provisioned (Phase 2). Custom and nested
|
|
3188
|
+
* (sub-tenant) domains additionally require an active domain entitlement
|
|
3189
|
+
* (paid SKU); pricing is not yet set. Subdomains are free.
|
|
3190
|
+
*/
|
|
3191
|
+
async listSubTenantDomains(subTenantId) {
|
|
3192
|
+
return this.apiClient.dispatch(
|
|
3193
|
+
"apps-listSubTenantDomains",
|
|
3194
|
+
{ subTenantId }
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
/**
|
|
3198
|
+
* Remove a delegated sub-tenant domain. Requires API key scope
|
|
3199
|
+
* `domains:write`.
|
|
3200
|
+
*
|
|
3201
|
+
* Requires domain-service activation. The control-plane call is live, but
|
|
3202
|
+
* a registered domain/target does not serve traffic until the
|
|
3203
|
+
* domain-serving edge is provisioned (Phase 2). Custom and nested
|
|
3204
|
+
* (sub-tenant) domains additionally require an active domain entitlement
|
|
3205
|
+
* (paid SKU); pricing is not yet set. Subdomains are free.
|
|
3206
|
+
*/
|
|
3207
|
+
async removeSubTenantDomain(domain) {
|
|
3208
|
+
return this.apiClient.dispatch("apps-removeSubTenantDomain", {
|
|
3209
|
+
domain
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
// ============================================
|
|
3213
|
+
// Analytics
|
|
3214
|
+
// ============================================
|
|
3215
|
+
async getAnalytics(appId, params) {
|
|
3216
|
+
const query = new URLSearchParams();
|
|
3217
|
+
if (params?.startDate) query.append("startDate", params.startDate);
|
|
3218
|
+
if (params?.endDate) query.append("endDate", params.endDate);
|
|
3219
|
+
const qs = query.toString();
|
|
3220
|
+
const response = await this.apiClient.get(
|
|
3221
|
+
`/apps/${appId}/analytics${qs ? `?${qs}` : ""}`
|
|
3222
|
+
);
|
|
3223
|
+
return response.analytics;
|
|
3224
|
+
}
|
|
3225
|
+
// ============================================
|
|
3226
|
+
// AI Features
|
|
3227
|
+
// ============================================
|
|
3228
|
+
async generateUI(prompt, context, appId) {
|
|
3229
|
+
const response = await this.apiClient.post(
|
|
3230
|
+
"/apps/ai/generate-ui",
|
|
3231
|
+
{ prompt, context, appId }
|
|
3232
|
+
);
|
|
3233
|
+
return { components: response.components };
|
|
3234
|
+
}
|
|
3235
|
+
async suggestActions(appId, componentType, context) {
|
|
3236
|
+
const response = await this.apiClient.post(`/apps/${appId}/ai/suggest-actions`, { componentType, context });
|
|
3237
|
+
return { actions: response.suggestions };
|
|
3238
|
+
}
|
|
3239
|
+
// ============================================
|
|
3240
|
+
// Collaboration
|
|
3241
|
+
// ============================================
|
|
3242
|
+
async startSession(appId, pageId) {
|
|
3243
|
+
const response = await this.apiClient.post(`/apps/${appId}/pages/${pageId}/sessions`, {});
|
|
3244
|
+
return { sessionToken: response.sessionToken, wsEndpoint: response.wsEndpoint };
|
|
3245
|
+
}
|
|
3246
|
+
async addComment(appId, pageId, componentId, content, threadId) {
|
|
3247
|
+
const response = await this.apiClient.post(
|
|
3248
|
+
`/apps/${appId}/pages/${pageId}/comments`,
|
|
3249
|
+
{ componentId, content, threadId }
|
|
3250
|
+
);
|
|
3251
|
+
return response;
|
|
3252
|
+
}
|
|
3253
|
+
async resolveComment(appId, pageId, commentId) {
|
|
3254
|
+
await this.apiClient.post(
|
|
3255
|
+
`/apps/${appId}/pages/${pageId}/comments/${commentId}/resolve`,
|
|
3256
|
+
{}
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
};
|
|
3260
|
+
|
|
3261
|
+
// src/services/marketplace.ts
|
|
3262
|
+
var MAKEMINDMarketplace = class {
|
|
3263
|
+
constructor(config) {
|
|
3264
|
+
this.apiClient = config.apiClient;
|
|
3265
|
+
this.getTenantId = config.tenantId;
|
|
3266
|
+
}
|
|
3267
|
+
// ============================================
|
|
3268
|
+
// Discovery
|
|
3269
|
+
// ============================================
|
|
3270
|
+
async search(options = {}) {
|
|
3271
|
+
const params = new URLSearchParams();
|
|
3272
|
+
if (options.query) params.set("query", options.query);
|
|
3273
|
+
if (options.category) params.set("category", options.category);
|
|
3274
|
+
if (options.tags) params.set("tags", options.tags.join(","));
|
|
3275
|
+
if (options.pricingModel) params.set("pricingModel", options.pricingModel.join(","));
|
|
3276
|
+
if (options.sortBy) params.set("sortBy", options.sortBy);
|
|
3277
|
+
if (options.page) params.set("page", String(options.page));
|
|
3278
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
3279
|
+
const qs = params.toString();
|
|
3280
|
+
return this.apiClient.get(`/marketplace/listings${qs ? `?${qs}` : ""}`);
|
|
3281
|
+
}
|
|
3282
|
+
async getDetail(listingId) {
|
|
3283
|
+
return this.apiClient.get(`/marketplace/listings/${listingId}`);
|
|
3284
|
+
}
|
|
3285
|
+
async browseCategory(categorySlug, options) {
|
|
3286
|
+
const params = new URLSearchParams();
|
|
3287
|
+
if (options?.sortBy) params.set("sortBy", options.sortBy);
|
|
3288
|
+
if (options?.page) params.set("page", String(options.page));
|
|
3289
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
3290
|
+
const qs = params.toString();
|
|
3291
|
+
return this.apiClient.get(`/marketplace/categories/${categorySlug}/listings${qs ? `?${qs}` : ""}`);
|
|
3292
|
+
}
|
|
3293
|
+
async getFeatured() {
|
|
3294
|
+
return this.apiClient.get("/marketplace/featured");
|
|
3295
|
+
}
|
|
3296
|
+
async getTrending() {
|
|
3297
|
+
return this.apiClient.get("/marketplace/trending");
|
|
3298
|
+
}
|
|
3299
|
+
async getRecommendations() {
|
|
3300
|
+
return this.apiClient.get("/marketplace/recommendations");
|
|
3301
|
+
}
|
|
3302
|
+
async listCategories() {
|
|
3303
|
+
return this.apiClient.get("/marketplace/categories");
|
|
3304
|
+
}
|
|
3305
|
+
// ============================================
|
|
3306
|
+
// Installation
|
|
3307
|
+
// ============================================
|
|
3308
|
+
async install(listingId) {
|
|
3309
|
+
return this.apiClient.post("/marketplace/install", { listingId });
|
|
3310
|
+
}
|
|
3311
|
+
async uninstall(listingId, cleanupData = false) {
|
|
3312
|
+
return this.apiClient.post("/marketplace/uninstall", { listingId, cleanupData });
|
|
3313
|
+
}
|
|
3314
|
+
async listInstalled(options) {
|
|
3315
|
+
const params = new URLSearchParams();
|
|
3316
|
+
if (options?.status) params.set("status", options.status);
|
|
3317
|
+
if (options?.page) params.set("page", String(options.page));
|
|
3318
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
3319
|
+
const qs = params.toString();
|
|
3320
|
+
return this.apiClient.get(`/marketplace/installed${qs ? `?${qs}` : ""}`);
|
|
3321
|
+
}
|
|
3322
|
+
async updateApp(listingId) {
|
|
3323
|
+
return this.apiClient.post(`/marketplace/installed/${listingId}/update`);
|
|
3324
|
+
}
|
|
3325
|
+
// ============================================
|
|
3326
|
+
// App Runtime (proxy, config, credentials)
|
|
3327
|
+
// ============================================
|
|
3328
|
+
async proxy(bindingId, options) {
|
|
3329
|
+
const queryStr = options.query ? `?${new URLSearchParams(options.query).toString()}` : "";
|
|
3330
|
+
const path = `/marketplace/apps/${bindingId}/proxy/${options.path.replace(/^\//, "")}${queryStr}`;
|
|
3331
|
+
return this.apiClient.request(path, { method: options.method, body: options.body });
|
|
3332
|
+
}
|
|
3333
|
+
async getApiSpec(bindingId) {
|
|
3334
|
+
return this.apiClient.get(`/marketplace/apps/${bindingId}/spec`);
|
|
3335
|
+
}
|
|
3336
|
+
async getConfig(bindingId) {
|
|
3337
|
+
return this.apiClient.get(`/marketplace/apps/${bindingId}/config`);
|
|
3338
|
+
}
|
|
3339
|
+
async updateConfig(bindingId, updates) {
|
|
3340
|
+
return this.apiClient.put(`/marketplace/apps/${bindingId}/config`, updates);
|
|
3341
|
+
}
|
|
3342
|
+
async issueCredentials(bindingId) {
|
|
3343
|
+
return this.apiClient.post(`/marketplace/apps/${bindingId}/credentials`);
|
|
3344
|
+
}
|
|
3345
|
+
async revokeCredentials(bindingId) {
|
|
3346
|
+
return this.apiClient.delete(`/marketplace/apps/${bindingId}/credentials`);
|
|
3347
|
+
}
|
|
3348
|
+
// ============================================
|
|
3349
|
+
// Reviews
|
|
3350
|
+
// ============================================
|
|
3351
|
+
async createReview(input) {
|
|
3352
|
+
return this.apiClient.post("/marketplace/reviews", input);
|
|
3353
|
+
}
|
|
3354
|
+
async updateReview(reviewId, listingId, updates) {
|
|
3355
|
+
return this.apiClient.put(`/marketplace/reviews/${reviewId}`, { listingId, ...updates });
|
|
3356
|
+
}
|
|
3357
|
+
async deleteReview(reviewId, listingId) {
|
|
3358
|
+
return this.apiClient.delete(`/marketplace/reviews/${reviewId}`, { body: { listingId } });
|
|
3359
|
+
}
|
|
3360
|
+
async listReviews(listingId, options) {
|
|
3361
|
+
const params = new URLSearchParams();
|
|
3362
|
+
if (options?.sortBy) params.set("sortBy", options.sortBy);
|
|
3363
|
+
if (options?.page) params.set("page", String(options.page));
|
|
3364
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
3365
|
+
const qs = params.toString();
|
|
3366
|
+
return this.apiClient.get(`/marketplace/listings/${listingId}/reviews${qs ? `?${qs}` : ""}`);
|
|
3367
|
+
}
|
|
3368
|
+
async replyToReview(reviewId, listingId, body) {
|
|
3369
|
+
return this.apiClient.post(`/marketplace/reviews/${reviewId}/reply`, { listingId, body });
|
|
3370
|
+
}
|
|
3371
|
+
async reportReview(reviewId, listingId, reason) {
|
|
3372
|
+
return this.apiClient.post(`/marketplace/reviews/${reviewId}/report`, { listingId, reason });
|
|
3373
|
+
}
|
|
3374
|
+
async voteHelpful(reviewId, listingId) {
|
|
3375
|
+
return this.apiClient.post(`/marketplace/reviews/${reviewId}/helpful`, { listingId });
|
|
3376
|
+
}
|
|
3377
|
+
// ============================================
|
|
3378
|
+
// Publishing
|
|
3379
|
+
// ============================================
|
|
3380
|
+
async createPublisherProfile(input) {
|
|
3381
|
+
return this.apiClient.post("/marketplace/publisher", input);
|
|
3382
|
+
}
|
|
3383
|
+
async updatePublisherProfile(updates) {
|
|
3384
|
+
return this.apiClient.put("/marketplace/publisher", updates);
|
|
3385
|
+
}
|
|
3386
|
+
async getDashboard() {
|
|
3387
|
+
return this.apiClient.get("/marketplace/publisher/dashboard");
|
|
3388
|
+
}
|
|
3389
|
+
async createListing(input) {
|
|
3390
|
+
return this.apiClient.post("/marketplace/publisher/listings", input);
|
|
3391
|
+
}
|
|
3392
|
+
async updateListing(listingId, updates) {
|
|
3393
|
+
return this.apiClient.put(`/marketplace/publisher/listings/${listingId}`, updates);
|
|
3394
|
+
}
|
|
3395
|
+
async submitForReview(listingId) {
|
|
3396
|
+
return this.apiClient.post(`/marketplace/publisher/listings/${listingId}/submit`);
|
|
3397
|
+
}
|
|
3398
|
+
async publishListing(listingId) {
|
|
3399
|
+
return this.apiClient.post(`/marketplace/publisher/listings/${listingId}/publish`);
|
|
3400
|
+
}
|
|
3401
|
+
async archiveListing(listingId) {
|
|
3402
|
+
return this.apiClient.post(`/marketplace/publisher/listings/${listingId}/archive`);
|
|
3403
|
+
}
|
|
3404
|
+
async reactivateListing(listingId) {
|
|
3405
|
+
return this.apiClient.post(`/marketplace/publisher/listings/${listingId}/reactivate`);
|
|
3406
|
+
}
|
|
3407
|
+
async createVersion(listingId, version, changelog, appVersionId, breakingChanges = false) {
|
|
3408
|
+
return this.apiClient.post(`/marketplace/publisher/listings/${listingId}/versions`, { version, changelog, appVersionId, breakingChanges });
|
|
3409
|
+
}
|
|
3410
|
+
// ============================================
|
|
3411
|
+
// Admin (Platform Admin only)
|
|
3412
|
+
// ============================================
|
|
3413
|
+
async approveListing(listingId) {
|
|
3414
|
+
return this.apiClient.post(`/marketplace/admin/listings/${listingId}/approve`);
|
|
3415
|
+
}
|
|
3416
|
+
async rejectListing(listingId, reason) {
|
|
3417
|
+
return this.apiClient.post(`/marketplace/admin/listings/${listingId}/reject`, { reason });
|
|
3418
|
+
}
|
|
3419
|
+
async suspendListing(listingId, reason) {
|
|
3420
|
+
return this.apiClient.post(`/marketplace/admin/listings/${listingId}/suspend`, { reason });
|
|
3421
|
+
}
|
|
3422
|
+
async approveVersion(listingId, versionId) {
|
|
3423
|
+
return this.apiClient.post(`/marketplace/admin/listings/${listingId}/versions/${versionId}/approve`);
|
|
3424
|
+
}
|
|
3425
|
+
async verifyPublisher(tenantId) {
|
|
3426
|
+
return this.apiClient.post(`/marketplace/admin/publishers/${tenantId}/verify`);
|
|
3427
|
+
}
|
|
3428
|
+
async suspendPublisher(tenantId, reason) {
|
|
3429
|
+
return this.apiClient.post(`/marketplace/admin/publishers/${tenantId}/suspend`, { reason });
|
|
3430
|
+
}
|
|
3431
|
+
async reactivatePublisher(tenantId) {
|
|
3432
|
+
return this.apiClient.post(`/marketplace/admin/publishers/${tenantId}/reactivate`);
|
|
3433
|
+
}
|
|
3434
|
+
async adminCreateCategory(data) {
|
|
3435
|
+
return this.apiClient.post("/marketplace/admin/categories", data);
|
|
3436
|
+
}
|
|
3437
|
+
async adminUpdateCategory(categoryId, data) {
|
|
3438
|
+
return this.apiClient.put(`/marketplace/admin/categories/${categoryId}`, data);
|
|
3439
|
+
}
|
|
3440
|
+
async adminDeleteCategory(categoryId) {
|
|
3441
|
+
return this.apiClient.delete(`/marketplace/admin/categories/${categoryId}`);
|
|
3442
|
+
}
|
|
3443
|
+
async getAnalytics(params) {
|
|
3444
|
+
const qs = new URLSearchParams();
|
|
3445
|
+
if (params?.startDate) qs.set("startDate", params.startDate);
|
|
3446
|
+
if (params?.endDate) qs.set("endDate", params.endDate);
|
|
3447
|
+
if (params?.granularity) qs.set("granularity", params.granularity);
|
|
3448
|
+
const queryStr = qs.toString();
|
|
3449
|
+
return this.apiClient.get(`/marketplace/admin/analytics${queryStr ? `?${queryStr}` : ""}`);
|
|
3450
|
+
}
|
|
3451
|
+
async getPendingListings(params) {
|
|
3452
|
+
const qs = new URLSearchParams();
|
|
3453
|
+
if (params?.status) qs.set("status", params.status);
|
|
3454
|
+
if (params?.page) qs.set("page", String(params.page));
|
|
3455
|
+
if (params?.limit) qs.set("limit", String(params.limit));
|
|
3456
|
+
const queryStr = qs.toString();
|
|
3457
|
+
return this.apiClient.get(`/marketplace/admin/pending-listings${queryStr ? `?${queryStr}` : ""}`);
|
|
3458
|
+
}
|
|
3459
|
+
async listPublishers(params) {
|
|
3460
|
+
const qs = new URLSearchParams();
|
|
3461
|
+
if (params?.status) qs.set("status", params.status);
|
|
3462
|
+
if (params?.page) qs.set("page", String(params.page));
|
|
3463
|
+
if (params?.limit) qs.set("limit", String(params.limit));
|
|
3464
|
+
const queryStr = qs.toString();
|
|
3465
|
+
return this.apiClient.get(`/marketplace/admin/publishers${queryStr ? `?${queryStr}` : ""}`);
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
|
|
3469
|
+
// src/utils/api-client.ts
|
|
3470
|
+
var ApiClient = class {
|
|
3471
|
+
constructor(config) {
|
|
3472
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "").replace(/\/api\/v\d+$/, "");
|
|
3473
|
+
this.apiVersion = config.apiVersion || "v1";
|
|
3474
|
+
this.apiKey = config.apiKey;
|
|
3475
|
+
this.applicationId = config.applicationId;
|
|
3476
|
+
this.getAuthToken = config.getAuthToken;
|
|
3477
|
+
this.getTenantId = config.getTenantId;
|
|
3478
|
+
this.getAppCheckToken = config.getAppCheckToken;
|
|
3479
|
+
}
|
|
3480
|
+
async request(endpoint, options = {}) {
|
|
3481
|
+
const { method = "GET", body, headers = {}, skipAuth = false } = options;
|
|
3482
|
+
const url = `${this.baseUrl}/api/${this.apiVersion}${endpoint}`;
|
|
3483
|
+
const requestHeaders = {
|
|
3484
|
+
"Content-Type": "application/json",
|
|
3485
|
+
"X-API-Key": this.apiKey,
|
|
3486
|
+
"X-Application-ID": this.applicationId,
|
|
3487
|
+
...headers
|
|
3488
|
+
};
|
|
3489
|
+
const authToken = this.getAuthToken();
|
|
3490
|
+
if (authToken && !skipAuth) {
|
|
3491
|
+
requestHeaders["Authorization"] = `Bearer ${authToken}`;
|
|
3492
|
+
}
|
|
3493
|
+
const tenantId = this.getTenantId();
|
|
3494
|
+
if (tenantId) {
|
|
3495
|
+
requestHeaders["X-Tenant-ID"] = tenantId;
|
|
3496
|
+
}
|
|
3497
|
+
if (this.getAppCheckToken) {
|
|
3498
|
+
const appCheckToken = await this.getAppCheckToken();
|
|
3499
|
+
if (appCheckToken) {
|
|
3500
|
+
requestHeaders["X-Firebase-AppCheck"] = appCheckToken;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
const csrf = readCsrfToken();
|
|
3504
|
+
if (csrf) requestHeaders["X-MM-CSRF"] = csrf;
|
|
3505
|
+
const response = await fetch(url, {
|
|
3506
|
+
method,
|
|
3507
|
+
headers: requestHeaders,
|
|
3508
|
+
credentials: "include",
|
|
3509
|
+
body: body ? JSON.stringify(body) : void 0
|
|
3510
|
+
});
|
|
3511
|
+
if (!response.ok) {
|
|
3512
|
+
await this.handleErrorResponse(response);
|
|
3513
|
+
}
|
|
3514
|
+
const contentType = response.headers.get("content-type");
|
|
3515
|
+
if (contentType?.includes("application/json")) {
|
|
3516
|
+
const json = await response.json();
|
|
3517
|
+
return json.data !== void 0 ? json.data : json;
|
|
3518
|
+
}
|
|
3519
|
+
return {};
|
|
3520
|
+
}
|
|
3521
|
+
async handleErrorResponse(response) {
|
|
3522
|
+
let body = {};
|
|
3523
|
+
try {
|
|
3524
|
+
body = await response.json();
|
|
3525
|
+
} catch {
|
|
3526
|
+
}
|
|
3527
|
+
const errorData = body.error ?? body;
|
|
3528
|
+
const code = errorData.code || this.mapStatusToCode(response.status);
|
|
3529
|
+
const message = errorData.message || response.statusText || `Request failed (HTTP ${response.status})`;
|
|
3530
|
+
const details = errorData.requestId ? { ...errorData.details ?? {}, requestId: errorData.requestId } : errorData.details;
|
|
3531
|
+
throw new MAKEMINDError(code, message, details);
|
|
3532
|
+
}
|
|
3533
|
+
mapStatusToCode(status) {
|
|
3534
|
+
switch (status) {
|
|
3535
|
+
case 400:
|
|
3536
|
+
return ErrorCodes.VALIDATION_ERROR;
|
|
3537
|
+
case 401:
|
|
3538
|
+
return ErrorCodes.UNAUTHENTICATED;
|
|
3539
|
+
case 403:
|
|
3540
|
+
return ErrorCodes.PERMISSION_DENIED;
|
|
3541
|
+
case 404:
|
|
3542
|
+
return ErrorCodes.NOT_FOUND;
|
|
3543
|
+
case 409:
|
|
3544
|
+
return ErrorCodes.ALREADY_EXISTS;
|
|
3545
|
+
case 429:
|
|
3546
|
+
return ErrorCodes.QUOTA_EXCEEDED;
|
|
3547
|
+
case 500:
|
|
3548
|
+
return ErrorCodes.INTERNAL_ERROR;
|
|
3549
|
+
case 503:
|
|
3550
|
+
return ErrorCodes.SERVICE_UNAVAILABLE;
|
|
3551
|
+
default:
|
|
3552
|
+
return ErrorCodes.UNKNOWN_ERROR;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
// Convenience methods
|
|
3556
|
+
async get(endpoint, options) {
|
|
3557
|
+
return this.request(endpoint, { ...options, method: "GET" });
|
|
3558
|
+
}
|
|
3559
|
+
async post(endpoint, body, options) {
|
|
3560
|
+
return this.request(endpoint, { ...options, method: "POST", body });
|
|
3561
|
+
}
|
|
3562
|
+
async put(endpoint, body, options) {
|
|
3563
|
+
return this.request(endpoint, { ...options, method: "PUT", body });
|
|
3564
|
+
}
|
|
3565
|
+
async patch(endpoint, body, options) {
|
|
3566
|
+
return this.request(endpoint, { ...options, method: "PATCH", body });
|
|
3567
|
+
}
|
|
3568
|
+
async delete(endpoint, options) {
|
|
3569
|
+
return this.request(endpoint, { ...options, method: "DELETE" });
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Execute a tenant-defined logic function by name via the logic service.
|
|
3573
|
+
* This is NOT the gateway RPC dispatch — use `dispatch()` for Callable operations.
|
|
3574
|
+
*/
|
|
3575
|
+
async call(functionName, data) {
|
|
3576
|
+
return this.post("/logic/call", { functionName, input: data });
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* Invoke a platform Callable operation through the gateway RPC dispatch route
|
|
3580
|
+
* (`POST /api/v<version>/call/:operation`). The operation key is the deployed
|
|
3581
|
+
* Callable name (`<domain>-<name>` for namespaced groups, bare name for flat
|
|
3582
|
+
* exports). The gateway expects the payload under a `data` envelope.
|
|
3583
|
+
*/
|
|
3584
|
+
async dispatch(operation, payload) {
|
|
3585
|
+
return this.post(`/call/${operation}`, { data: payload ?? {} });
|
|
3586
|
+
}
|
|
3587
|
+
/**
|
|
3588
|
+
* Get the version-qualified base URL for direct API calls (e.g. streaming),
|
|
3589
|
+
* including the `/api/<version>` segment.
|
|
3590
|
+
*/
|
|
3591
|
+
getBaseUrl() {
|
|
3592
|
+
return `${this.baseUrl}/api/${this.apiVersion}`;
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Get auth headers for direct API calls (e.g., streaming)
|
|
3596
|
+
*/
|
|
3597
|
+
getAuthHeaders() {
|
|
3598
|
+
const headers = {
|
|
3599
|
+
"X-API-Key": this.apiKey,
|
|
3600
|
+
"X-Application-ID": this.applicationId
|
|
3601
|
+
};
|
|
3602
|
+
const authToken = this.getAuthToken();
|
|
3603
|
+
if (authToken) {
|
|
3604
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
3605
|
+
}
|
|
3606
|
+
const tenantId = this.getTenantId();
|
|
3607
|
+
if (tenantId) {
|
|
3608
|
+
headers["X-Tenant-ID"] = tenantId;
|
|
3609
|
+
}
|
|
3610
|
+
return headers;
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
function readCsrfToken() {
|
|
3614
|
+
if (typeof document === "undefined") return void 0;
|
|
3615
|
+
for (const part of document.cookie.split(";")) {
|
|
3616
|
+
const eq = part.indexOf("=");
|
|
3617
|
+
if (eq < 0) continue;
|
|
3618
|
+
if (part.slice(0, eq).trim() === "mm_csrf") {
|
|
3619
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
return void 0;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
// src/client.ts
|
|
3626
|
+
var MAKEMINDClient = class {
|
|
3627
|
+
constructor(config) {
|
|
3628
|
+
this._auth = null;
|
|
3629
|
+
this._data = null;
|
|
3630
|
+
this._logic = null;
|
|
3631
|
+
this._tenant = null;
|
|
3632
|
+
this._messaging = null;
|
|
3633
|
+
this._settlement = null;
|
|
3634
|
+
this._partners = null;
|
|
3635
|
+
this._storage = null;
|
|
3636
|
+
this._ai = null;
|
|
3637
|
+
this._search = null;
|
|
3638
|
+
this._apps = null;
|
|
3639
|
+
this._marketplace = null;
|
|
3640
|
+
this.currentTenantId = null;
|
|
3641
|
+
this.currentUserId = null;
|
|
3642
|
+
this.authToken = null;
|
|
3643
|
+
this.applicationId = config.applicationId;
|
|
3644
|
+
this.environment = config.environment || "production";
|
|
3645
|
+
this.apiClient = new ApiClient({
|
|
3646
|
+
baseUrl: config.baseUrl || "https://api.makemind.app",
|
|
3647
|
+
apiVersion: config.apiVersion,
|
|
3648
|
+
apiKey: config.apiKey,
|
|
3649
|
+
applicationId: config.applicationId,
|
|
3650
|
+
getAuthToken: () => this.authToken,
|
|
3651
|
+
getTenantId: () => this.currentTenantId,
|
|
3652
|
+
getAppCheckToken: config.getAppCheckToken
|
|
3653
|
+
});
|
|
3654
|
+
this._auth = new MAKEMINDAuth({
|
|
3655
|
+
apiClient: this.apiClient,
|
|
3656
|
+
applicationId: this.applicationId,
|
|
3657
|
+
onAuthStateChange: (user) => {
|
|
3658
|
+
if (user) {
|
|
3659
|
+
this.currentUserId = user.uid;
|
|
3660
|
+
this.currentTenantId = user.tenantId;
|
|
3661
|
+
} else {
|
|
3662
|
+
this.currentUserId = null;
|
|
3663
|
+
this.currentTenantId = null;
|
|
3664
|
+
this.authToken = null;
|
|
3665
|
+
}
|
|
3666
|
+
},
|
|
3667
|
+
setAuthToken: (token) => {
|
|
3668
|
+
this.authToken = token;
|
|
3669
|
+
}
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
// Internal: Set auth token after login
|
|
3673
|
+
setAuthToken(token) {
|
|
3674
|
+
this.authToken = token;
|
|
3675
|
+
}
|
|
3676
|
+
// Internal: Set user context after login
|
|
3677
|
+
setUserContext(userId, tenantId) {
|
|
3678
|
+
this.currentUserId = userId;
|
|
3679
|
+
this.currentTenantId = tenantId;
|
|
3680
|
+
}
|
|
3681
|
+
// Getters for context
|
|
3682
|
+
get tenantId() {
|
|
3683
|
+
return this.currentTenantId;
|
|
3684
|
+
}
|
|
3685
|
+
get userId() {
|
|
3686
|
+
return this.currentUserId;
|
|
3687
|
+
}
|
|
3688
|
+
// Set tenant context manually (for switching tenants)
|
|
3689
|
+
setTenantContext(tenantId) {
|
|
3690
|
+
this.currentTenantId = tenantId;
|
|
3691
|
+
}
|
|
3692
|
+
// Get API client for direct access
|
|
3693
|
+
getApiClient() {
|
|
3694
|
+
return this.apiClient;
|
|
3695
|
+
}
|
|
3696
|
+
/**
|
|
3697
|
+
* Invoke a platform Callable operation through the gateway RPC dispatch
|
|
3698
|
+
* (`POST /api/v1/call/:operation`). The operation name matches the deployed
|
|
3699
|
+
* Callable key (namespaced groups use `<prefix>-<name>`, e.g.
|
|
3700
|
+
* `settlement-getMRR`; flat exports use the bare name, e.g. `createTenant`).
|
|
3701
|
+
*
|
|
3702
|
+
* This is the platform-operation channel, distinct from `logic.call()` which
|
|
3703
|
+
* runs a tenant-defined logic function. Each operation enforces its own
|
|
3704
|
+
* authorization server-side (platform roles / tenant scopes), so exposing
|
|
3705
|
+
* this method does not bypass access control. Returns the operation's result
|
|
3706
|
+
* value directly (the `{ data }` envelope is already unwrapped).
|
|
3707
|
+
*/
|
|
3708
|
+
call(operation, input) {
|
|
3709
|
+
return this.apiClient.dispatch(operation, input);
|
|
3710
|
+
}
|
|
3711
|
+
// Service accessors
|
|
3712
|
+
get auth() {
|
|
3713
|
+
return this._auth;
|
|
3714
|
+
}
|
|
3715
|
+
get data() {
|
|
3716
|
+
if (!this._data) {
|
|
3717
|
+
this._data = new MAKEMINDData({
|
|
3718
|
+
apiClient: this.apiClient,
|
|
3719
|
+
applicationId: this.applicationId,
|
|
3720
|
+
tenantId: () => this.currentTenantId
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
return this._data;
|
|
3724
|
+
}
|
|
3725
|
+
get logic() {
|
|
3726
|
+
if (!this._logic) {
|
|
3727
|
+
this._logic = new MAKEMINDLogic({
|
|
3728
|
+
apiClient: this.apiClient,
|
|
3729
|
+
applicationId: this.applicationId,
|
|
3730
|
+
tenantId: () => this.currentTenantId
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
return this._logic;
|
|
3734
|
+
}
|
|
3735
|
+
get tenant() {
|
|
3736
|
+
if (!this._tenant) {
|
|
3737
|
+
this._tenant = new MAKEMINDTenant({
|
|
3738
|
+
apiClient: this.apiClient,
|
|
3739
|
+
applicationId: this.applicationId,
|
|
3740
|
+
tenantId: () => this.currentTenantId,
|
|
3741
|
+
onTenantCreated: async (tenantId) => {
|
|
3742
|
+
this.currentTenantId = tenantId;
|
|
3743
|
+
try {
|
|
3744
|
+
await this.auth.refreshAuthToken();
|
|
3745
|
+
} catch {
|
|
3746
|
+
}
|
|
3747
|
+
this.currentTenantId = tenantId;
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
return this._tenant;
|
|
3752
|
+
}
|
|
3753
|
+
get messaging() {
|
|
3754
|
+
if (!this._messaging) {
|
|
3755
|
+
this._messaging = new MAKEMINDMessaging({
|
|
3756
|
+
apiClient: this.apiClient,
|
|
3757
|
+
applicationId: this.applicationId,
|
|
3758
|
+
tenantId: () => this.currentTenantId,
|
|
3759
|
+
userId: () => this.currentUserId
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
return this._messaging;
|
|
3763
|
+
}
|
|
3764
|
+
get settlement() {
|
|
3765
|
+
if (!this._settlement) {
|
|
3766
|
+
this._settlement = new MAKEMINDSettlement({
|
|
3767
|
+
apiClient: this.apiClient,
|
|
3768
|
+
applicationId: this.applicationId,
|
|
3769
|
+
tenantId: () => this.currentTenantId
|
|
3770
|
+
});
|
|
3771
|
+
}
|
|
3772
|
+
return this._settlement;
|
|
3773
|
+
}
|
|
3774
|
+
get partners() {
|
|
3775
|
+
if (!this._partners) {
|
|
3776
|
+
this._partners = new MAKEMINDPartners({
|
|
3777
|
+
apiClient: this.apiClient,
|
|
3778
|
+
applicationId: this.applicationId,
|
|
3779
|
+
tenantId: () => this.currentTenantId
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
return this._partners;
|
|
3783
|
+
}
|
|
3784
|
+
get storage() {
|
|
3785
|
+
if (!this._storage) {
|
|
3786
|
+
this._storage = new MAKEMINDStorage({
|
|
3787
|
+
apiClient: this.apiClient,
|
|
3788
|
+
applicationId: this.applicationId,
|
|
3789
|
+
tenantId: () => this.currentTenantId
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
return this._storage;
|
|
3793
|
+
}
|
|
3794
|
+
get ai() {
|
|
3795
|
+
if (!this._ai) {
|
|
3796
|
+
this._ai = new MAKEMINDAI({
|
|
3797
|
+
apiClient: this.apiClient,
|
|
3798
|
+
applicationId: this.applicationId,
|
|
3799
|
+
tenantId: () => this.currentTenantId
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
return this._ai;
|
|
3803
|
+
}
|
|
3804
|
+
get search() {
|
|
3805
|
+
if (!this._search) {
|
|
3806
|
+
this._search = new MAKEMINDSearch({
|
|
3807
|
+
apiClient: this.apiClient,
|
|
3808
|
+
applicationId: this.applicationId,
|
|
3809
|
+
tenantId: () => this.currentTenantId
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
return this._search;
|
|
3813
|
+
}
|
|
3814
|
+
get apps() {
|
|
3815
|
+
if (!this._apps) {
|
|
3816
|
+
this._apps = new MAKEMINDApps({
|
|
3817
|
+
apiClient: this.apiClient,
|
|
3818
|
+
applicationId: this.applicationId,
|
|
3819
|
+
tenantId: () => this.currentTenantId
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
return this._apps;
|
|
3823
|
+
}
|
|
3824
|
+
get marketplace() {
|
|
3825
|
+
if (!this._marketplace) {
|
|
3826
|
+
this._marketplace = new MAKEMINDMarketplace({
|
|
3827
|
+
apiClient: this.apiClient,
|
|
3828
|
+
applicationId: this.applicationId,
|
|
3829
|
+
tenantId: () => this.currentTenantId
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
return this._marketplace;
|
|
3833
|
+
}
|
|
3834
|
+
};
|
|
3835
|
+
var defaultClient = null;
|
|
3836
|
+
function initializeMAKEMIND(config) {
|
|
3837
|
+
defaultClient = new MAKEMINDClient(config);
|
|
3838
|
+
return defaultClient;
|
|
3839
|
+
}
|
|
3840
|
+
function getMAKEMIND() {
|
|
3841
|
+
if (!defaultClient) {
|
|
3842
|
+
throw new Error("MAKEMIND SDK not initialized. Call initializeMAKEMIND() first.");
|
|
3843
|
+
}
|
|
3844
|
+
return defaultClient;
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
// src/services/experts.ts
|
|
3848
|
+
var ExpertDoorError = class extends Error {
|
|
3849
|
+
constructor(code, message, status) {
|
|
3850
|
+
super(message);
|
|
3851
|
+
this.name = "ExpertDoorError";
|
|
3852
|
+
this.code = code;
|
|
3853
|
+
this.status = status;
|
|
3854
|
+
}
|
|
3855
|
+
};
|
|
3856
|
+
var MAKEMINDExperts = class {
|
|
3857
|
+
constructor(config) {
|
|
3858
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
3859
|
+
this.tenantId = config.tenantId;
|
|
3860
|
+
this.appCheck = config.appCheck;
|
|
3861
|
+
this.fetchImpl = config.fetchImpl ?? ((...a) => fetch(...a));
|
|
3862
|
+
}
|
|
3863
|
+
async post(path, body) {
|
|
3864
|
+
const headers = { "Content-Type": "application/json" };
|
|
3865
|
+
if (this.appCheck) {
|
|
3866
|
+
const token = await this.appCheck();
|
|
3867
|
+
if (token) headers["X-Firebase-AppCheck"] = token;
|
|
3868
|
+
}
|
|
3869
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/v1/public/experts/${path}`, {
|
|
3870
|
+
method: "POST",
|
|
3871
|
+
headers,
|
|
3872
|
+
body: JSON.stringify({ tenantId: this.tenantId, ...body })
|
|
3873
|
+
});
|
|
3874
|
+
const payload = await res.json().catch(() => ({}));
|
|
3875
|
+
if (!res.ok) {
|
|
3876
|
+
const err = payload.error;
|
|
3877
|
+
throw new ExpertDoorError(err?.code ?? "UNKNOWN", err?.message ?? `HTTP ${res.status}`, res.status);
|
|
3878
|
+
}
|
|
3879
|
+
return payload.data;
|
|
3880
|
+
}
|
|
3881
|
+
/** One question, one answer, and the capability to read it back. */
|
|
3882
|
+
async consult(options) {
|
|
3883
|
+
return this.post("consult", {
|
|
3884
|
+
expertId: options.expertId,
|
|
3885
|
+
question: options.question,
|
|
3886
|
+
...options.facts !== void 0 ? { facts: options.facts } : {}
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* Open a conversation, or resume one whose capability the caller kept.
|
|
3891
|
+
* @param {Object} options The expert to open, or a thread to resume.
|
|
3892
|
+
* @return {ExpertChat} A thread that carries its own capability.
|
|
3893
|
+
*/
|
|
3894
|
+
chat(options) {
|
|
3895
|
+
return new ExpertChat(this, options);
|
|
3896
|
+
}
|
|
3897
|
+
/** @internal */
|
|
3898
|
+
async converse(body) {
|
|
3899
|
+
return this.post("converse", body);
|
|
3900
|
+
}
|
|
3901
|
+
async getConsultation(options) {
|
|
3902
|
+
return this.post("consultation", options);
|
|
3903
|
+
}
|
|
3904
|
+
async getSession(options) {
|
|
3905
|
+
return this.post("session", options);
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3908
|
+
var ExpertChat = class {
|
|
3909
|
+
constructor(api, options) {
|
|
3910
|
+
this.turns = [];
|
|
3911
|
+
this.api = api;
|
|
3912
|
+
this.expertId = options.expertId;
|
|
3913
|
+
this.session = options.sessionId;
|
|
3914
|
+
this.token = options.accessToken;
|
|
3915
|
+
}
|
|
3916
|
+
get sessionId() {
|
|
3917
|
+
return this.session;
|
|
3918
|
+
}
|
|
3919
|
+
get accessToken() {
|
|
3920
|
+
return this.token;
|
|
3921
|
+
}
|
|
3922
|
+
get history() {
|
|
3923
|
+
return [...this.turns];
|
|
3924
|
+
}
|
|
3925
|
+
/**
|
|
3926
|
+
* Ask, and keep whatever the thread needs to be continued.
|
|
3927
|
+
* @param {string} question What the visitor asked.
|
|
3928
|
+
* @param {Object} [facts] Structured facts the rules read.
|
|
3929
|
+
* @return {Promise<ExpertConsultation>} The turn's record. A refusal comes
|
|
3930
|
+
* back as an answer with `refusal` set; only a closed door throws.
|
|
3931
|
+
*/
|
|
3932
|
+
async ask(question, facts) {
|
|
3933
|
+
const result = await this.api.converse({
|
|
3934
|
+
// A thread names its performer once. Sending the expert again on a later
|
|
3935
|
+
// turn is refused by the platform as renaming it.
|
|
3936
|
+
...this.session ? { sessionId: this.session } : { expertId: this.expertId },
|
|
3937
|
+
...this.token ? { accessToken: this.token } : {},
|
|
3938
|
+
question,
|
|
3939
|
+
...facts !== void 0 ? { facts } : {}
|
|
3940
|
+
});
|
|
3941
|
+
this.session = result.sessionId;
|
|
3942
|
+
if (result.access?.token) this.token = result.access.token;
|
|
3943
|
+
this.turns.push({
|
|
3944
|
+
question,
|
|
3945
|
+
consultationId: result.consultation.id,
|
|
3946
|
+
...result.consultation.synthesis?.answer !== void 0 ? { answer: result.consultation.synthesis.answer } : {}
|
|
3947
|
+
});
|
|
3948
|
+
return result.consultation;
|
|
3949
|
+
}
|
|
3950
|
+
};
|
|
3951
|
+
|
|
3952
|
+
// src/services/expert-chat-widget.ts
|
|
3953
|
+
var DEFAULT_NOTICE = "This conversation lives in this browser only. Lose the link and the thread is gone \u2014 there is no account to recover it into.";
|
|
3954
|
+
function defaultStorage() {
|
|
3955
|
+
try {
|
|
3956
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
3957
|
+
} catch {
|
|
3958
|
+
return null;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
function mountExpertChat(el, options) {
|
|
3962
|
+
const doc = el.ownerDocument;
|
|
3963
|
+
const api = new MAKEMINDExperts(options);
|
|
3964
|
+
const store = options.storage === void 0 ? defaultStorage() : options.storage;
|
|
3965
|
+
const key = `makemind.expert.${options.tenantId}.${options.expertId}`;
|
|
3966
|
+
let saved = {};
|
|
3967
|
+
try {
|
|
3968
|
+
const raw = store?.getItem(key);
|
|
3969
|
+
if (raw) saved = JSON.parse(raw);
|
|
3970
|
+
} catch {
|
|
3971
|
+
saved = {};
|
|
3972
|
+
}
|
|
3973
|
+
const chat = api.chat({ expertId: options.expertId, ...saved });
|
|
3974
|
+
const root = doc.createElement("div");
|
|
3975
|
+
root.className = "mm-expert-chat";
|
|
3976
|
+
const heading = doc.createElement("div");
|
|
3977
|
+
heading.className = "mm-expert-chat__title";
|
|
3978
|
+
heading.textContent = options.title ?? "Ask the expert";
|
|
3979
|
+
const thread = doc.createElement("div");
|
|
3980
|
+
thread.className = "mm-expert-chat__thread";
|
|
3981
|
+
const form = doc.createElement("form");
|
|
3982
|
+
form.className = "mm-expert-chat__form";
|
|
3983
|
+
const input = doc.createElement("input");
|
|
3984
|
+
input.type = "text";
|
|
3985
|
+
input.placeholder = options.placeholder ?? "Type your question";
|
|
3986
|
+
const button = doc.createElement("button");
|
|
3987
|
+
button.type = "submit";
|
|
3988
|
+
button.textContent = "Ask";
|
|
3989
|
+
const notice = doc.createElement("p");
|
|
3990
|
+
notice.className = "mm-expert-chat__notice";
|
|
3991
|
+
notice.textContent = options.bearerNotice ?? DEFAULT_NOTICE;
|
|
3992
|
+
form.append(input, button);
|
|
3993
|
+
root.append(heading, thread, form, notice);
|
|
3994
|
+
el.textContent = "";
|
|
3995
|
+
el.append(root);
|
|
3996
|
+
function line(role, text) {
|
|
3997
|
+
const p = doc.createElement("p");
|
|
3998
|
+
p.className = `mm-expert-chat__${role}`;
|
|
3999
|
+
p.textContent = text;
|
|
4000
|
+
thread.append(p);
|
|
4001
|
+
return p;
|
|
4002
|
+
}
|
|
4003
|
+
for (const turn of chat.history) {
|
|
4004
|
+
line("question", turn.question);
|
|
4005
|
+
if (turn.answer) line("answer", turn.answer);
|
|
4006
|
+
}
|
|
4007
|
+
function remember() {
|
|
4008
|
+
try {
|
|
4009
|
+
store?.setItem(key, JSON.stringify({ sessionId: chat.sessionId, accessToken: chat.accessToken }));
|
|
4010
|
+
} catch {
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
function render(turn) {
|
|
4014
|
+
if (turn.refusal) line("refusal", turn.synthesis?.answer || turn.refusal);
|
|
4015
|
+
else line("answer", turn.synthesis?.answer ?? "");
|
|
4016
|
+
}
|
|
4017
|
+
let asking = false;
|
|
4018
|
+
async function ask(question) {
|
|
4019
|
+
const text = question.trim();
|
|
4020
|
+
if (!text || asking) return;
|
|
4021
|
+
asking = true;
|
|
4022
|
+
button.disabled = true;
|
|
4023
|
+
line("question", text);
|
|
4024
|
+
try {
|
|
4025
|
+
render(await chat.ask(text));
|
|
4026
|
+
remember();
|
|
4027
|
+
} catch (e) {
|
|
4028
|
+
line("error", e instanceof ExpertDoorError ? e.message : "The expert could not be reached.");
|
|
4029
|
+
} finally {
|
|
4030
|
+
asking = false;
|
|
4031
|
+
button.disabled = false;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
const onSubmit = (event) => {
|
|
4035
|
+
event.preventDefault();
|
|
4036
|
+
const question = input.value;
|
|
4037
|
+
input.value = "";
|
|
4038
|
+
void ask(question);
|
|
4039
|
+
};
|
|
4040
|
+
form.addEventListener("submit", onSubmit);
|
|
4041
|
+
return {
|
|
4042
|
+
ask,
|
|
4043
|
+
destroy() {
|
|
4044
|
+
form.removeEventListener("submit", onSubmit);
|
|
4045
|
+
root.remove();
|
|
4046
|
+
},
|
|
4047
|
+
get thread() {
|
|
4048
|
+
return { sessionId: chat.sessionId, accessToken: chat.accessToken };
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
export {
|
|
4053
|
+
ApiClient,
|
|
4054
|
+
ErrorCodes,
|
|
4055
|
+
ExpertChat,
|
|
4056
|
+
ExpertDoorError,
|
|
4057
|
+
MAKEMINDAI,
|
|
4058
|
+
MAKEMINDApps,
|
|
4059
|
+
MAKEMINDAuth,
|
|
4060
|
+
MAKEMINDClient,
|
|
4061
|
+
MAKEMINDData,
|
|
4062
|
+
MAKEMINDError,
|
|
4063
|
+
MAKEMINDExperts,
|
|
4064
|
+
MAKEMINDLogic,
|
|
4065
|
+
MAKEMINDMarketplace,
|
|
4066
|
+
MAKEMINDMessaging,
|
|
4067
|
+
MAKEMINDSearch,
|
|
4068
|
+
MAKEMINDSettlement,
|
|
4069
|
+
MAKEMINDStorage,
|
|
4070
|
+
MAKEMINDTenant,
|
|
4071
|
+
getMAKEMIND,
|
|
4072
|
+
initializeMAKEMIND,
|
|
4073
|
+
mountExpertChat
|
|
4074
|
+
};
|