@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/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
+ };