backend-manager 5.0.91 → 5.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/CLAUDE.md +14 -6
  3. package/README.md +6 -6
  4. package/TODO-MARKETING.md +3 -0
  5. package/TODO-PAYMENT-v2.md +71 -0
  6. package/TODO.md +7 -0
  7. package/package.json +3 -3
  8. package/src/cli/commands/{emulators.js → emulator.js} +15 -15
  9. package/src/cli/commands/index.js +1 -1
  10. package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
  11. package/src/cli/commands/setup-tests/index.js +2 -2
  12. package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
  13. package/src/cli/commands/test.js +16 -16
  14. package/src/cli/index.js +4 -4
  15. package/src/manager/events/auth/on-create.js +5 -158
  16. package/src/manager/events/firestore/payments-webhooks/analytics.js +4 -3
  17. package/src/manager/events/firestore/payments-webhooks/on-write.js +56 -6
  18. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +3 -3
  19. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +1 -1
  20. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +32 -28
  21. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +3 -3
  22. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +3 -3
  23. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +3 -3
  24. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +3 -3
  25. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +3 -3
  26. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +3 -3
  27. package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
  28. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
  29. package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +2 -2
  30. package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
  31. package/src/manager/index.js +12 -0
  32. package/src/manager/libraries/email.js +523 -0
  33. package/src/manager/libraries/infer-contact.js +140 -0
  34. package/src/manager/libraries/prompts/infer-contact.md +43 -0
  35. package/src/manager/routes/admin/backup/post.js +4 -3
  36. package/src/manager/routes/admin/email/post.js +11 -428
  37. package/src/manager/routes/admin/hook/post.js +3 -2
  38. package/src/manager/routes/admin/notification/post.js +14 -12
  39. package/src/manager/routes/admin/post/post.js +5 -6
  40. package/src/manager/routes/admin/post/put.js +3 -2
  41. package/src/manager/routes/admin/stats/get.js +19 -10
  42. package/src/manager/routes/general/email/post.js +8 -21
  43. package/src/manager/routes/general/email/templates/download-app-link.js +2 -2
  44. package/src/manager/routes/marketing/contact/post.js +2 -100
  45. package/src/manager/routes/payments/intent/post.js +0 -2
  46. package/src/manager/routes/payments/intent/processors/test.js +9 -10
  47. package/src/manager/routes/user/oauth2/_helpers.js +3 -2
  48. package/src/manager/routes/user/oauth2/delete.js +3 -3
  49. package/src/manager/routes/user/oauth2/get.js +2 -2
  50. package/src/manager/routes/user/oauth2/post.js +9 -9
  51. package/src/manager/routes/user/sessions/delete.js +4 -3
  52. package/src/manager/routes/user/signup/post.js +254 -54
  53. package/src/manager/schemas/admin/email/post.js +13 -8
  54. package/src/test/run-tests.js +1 -1
  55. package/test/functions/admin/send-email.js +1 -88
  56. package/test/helpers/email.js +421 -0
  57. package/test/helpers/infer-contact.js +299 -0
  58. package/test/routes/admin/email.js +41 -90
  59. package/REFACTOR-BEM-API.md +0 -76
  60. package/REFACTOR-MIDDLEWARE.md +0 -62
  61. package/REFACTOR-PAYMENT.md +0 -66
@@ -39,7 +39,7 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
39
39
 
40
40
  // Update stats if requested
41
41
  if (settings.update) {
42
- const error = await updateStats(admin, assistant, Manager, data, settings.update);
42
+ const error = await updateStats(assistant, data, settings.update);
43
43
 
44
44
  if (error) {
45
45
  return assistant.respond(error.message, { code: 500 });
@@ -58,7 +58,9 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
58
58
  return assistant.respond(data);
59
59
  };
60
60
 
61
- async function updateStats(admin, assistant, Manager, existingData, update) {
61
+ async function updateStats(assistant, existingData, update) {
62
+ const Manager = assistant.Manager;
63
+ const { admin } = Manager.libraries;
62
64
  const stats = admin.firestore().doc('meta/stats');
63
65
  const newData = {
64
66
  app: Manager.config?.app?.id || null,
@@ -70,7 +72,7 @@ async function updateStats(admin, assistant, Manager, existingData, update) {
70
72
 
71
73
  // Update notification stats
72
74
  if (update === true || update?.notifications) {
73
- const count = await getAllNotifications(admin, assistant).catch((e) => e);
75
+ const count = await getAllNotifications(assistant).catch((e) => e);
74
76
 
75
77
  if (count instanceof Error) {
76
78
  error = new Error(`Failed getting notifications: ${count.message}`);
@@ -81,7 +83,7 @@ async function updateStats(admin, assistant, Manager, existingData, update) {
81
83
 
82
84
  // Update subscription stats
83
85
  if (!error && (update === true || update?.subscriptions)) {
84
- const subscriptions = await getAllSubscriptions(admin, assistant).catch((e) => e);
86
+ const subscriptions = await getAllSubscriptions(assistant).catch((e) => e);
85
87
 
86
88
  if (subscriptions instanceof Error) {
87
89
  error = new Error(`Failed getting subscriptions: ${subscriptions.message}`);
@@ -92,7 +94,7 @@ async function updateStats(admin, assistant, Manager, existingData, update) {
92
94
 
93
95
  // Update user stats
94
96
  if (!error && (!existingData?.users?.total || update === true || update?.users)) {
95
- const users = await getAllUsers(admin, assistant).catch((e) => e);
97
+ const users = await getAllUsers(assistant).catch((e) => e);
96
98
 
97
99
  if (users instanceof Error) {
98
100
  error = new Error(`Failed getting users: ${users.message}`);
@@ -103,7 +105,7 @@ async function updateStats(admin, assistant, Manager, existingData, update) {
103
105
 
104
106
  // Update online users
105
107
  if (!error && (update === true || update?.online)) {
106
- const online = await countOnlineUsers(admin, assistant);
108
+ const online = await countOnlineUsers(assistant);
107
109
 
108
110
  _.set(newData, 'users.online', online);
109
111
  }
@@ -125,7 +127,9 @@ async function updateStats(admin, assistant, Manager, existingData, update) {
125
127
  return error;
126
128
  }
127
129
 
128
- async function getAllUsers(admin, assistant) {
130
+ async function getAllUsers(assistant) {
131
+ const { admin } = assistant.Manager.libraries;
132
+
129
133
  assistant.log('getAllUsers(): Starting...');
130
134
 
131
135
  const users = [];
@@ -142,7 +146,9 @@ async function getAllUsers(admin, assistant) {
142
146
  return users;
143
147
  }
144
148
 
145
- async function getAllNotifications(admin, assistant) {
149
+ async function getAllNotifications(assistant) {
150
+ const { admin } = assistant.Manager.libraries;
151
+
146
152
  assistant.log('getAllNotifications(): Starting...');
147
153
 
148
154
  const snap = await admin.firestore().collection('notifications').count().get();
@@ -153,7 +159,9 @@ async function getAllNotifications(admin, assistant) {
153
159
  return count;
154
160
  }
155
161
 
156
- async function getAllSubscriptions(admin, assistant) {
162
+ async function getAllSubscriptions(assistant) {
163
+ const { admin } = assistant.Manager.libraries;
164
+
157
165
  assistant.log('getAllSubscriptions(): Starting...');
158
166
 
159
167
  const snapshot = await admin.firestore().collection('users')
@@ -195,7 +203,8 @@ async function getAllSubscriptions(admin, assistant) {
195
203
  return stats;
196
204
  }
197
205
 
198
- async function countOnlineUsers(admin, assistant) {
206
+ async function countOnlineUsers(assistant) {
207
+ const { admin } = assistant.Manager.libraries;
199
208
  let online = 0;
200
209
 
201
210
  const paths = ['gatherings/online', 'sessions/app', 'sessions/online'];
@@ -4,10 +4,7 @@
4
4
  */
5
5
  const path = require('path');
6
6
  const { merge } = require('lodash');
7
-
8
- module.exports = async ({ assistant, Manager, settings, analytics }) => {
9
- const fetch = Manager.require('wonderful-fetch');
10
-
7
+ module.exports = async ({ assistant, Manager, settings }) => {
11
8
  // Validate required parameters
12
9
  if (!settings.id) {
13
10
  return assistant.respond('Parameter {id} is required.', { code: 400 });
@@ -22,10 +19,7 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
22
19
  email: 3,
23
20
  },
24
21
  delay: 1,
25
- payload: {
26
- backendManagerKey: process.env.BACKEND_MANAGER_KEY,
27
- app: Manager.config.app.id,
28
- },
22
+ payload: {},
29
23
  };
30
24
 
31
25
  // Load email template
@@ -75,25 +69,18 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
75
69
 
76
70
  assistant.log('Email payload:', emailPayload);
77
71
 
78
- // Send the email via NEW admin/email API
79
- const result = await fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
80
- method: 'post',
81
- response: 'json',
82
- log: true,
83
- headers: {
84
- 'Authorization': `Bearer ${process.env.BACKEND_MANAGER_KEY}`,
85
- },
86
- body: emailPayload.payload,
87
- }).catch(e => e);
72
+ // Send email directly via library
73
+ const email = Manager.Email(assistant);
74
+ const result = await email.send(emailPayload.payload).catch(e => e);
88
75
 
89
76
  if (result instanceof Error) {
90
- return assistant.respond(`Error sending email: ${result}`, { code: 500, sentry: true });
77
+ return assistant.respond(result.message, { code: result.code || 500, sentry: result.code !== 400 });
91
78
  }
92
79
 
93
- assistant.log('Response:', result);
80
+ assistant.log('Response:', result.status);
94
81
 
95
82
  // Track analytics
96
- analytics.event('general/email', { id: settings.id });
83
+ assistant.analytics.event('general/email', { id: settings.id });
97
84
 
98
85
  return assistant.respond({ success: true });
99
86
  };
@@ -16,8 +16,8 @@ module.exports = function (payload, config) {
16
16
  },
17
17
  categories: ['download'],
18
18
  subject: `Free ${config.brand.name} download link for ${payload.name || 'you'}!`,
19
- template: 'd-1d730ac8cc544b7cbccc8fa4a4b3f9ce',
20
- group: 25927,
19
+ template: 'main/misc/app-download-link',
20
+ group: 'marketing',
21
21
  copy: false,
22
22
  data: {},
23
23
  }
@@ -9,9 +9,7 @@ const dns = require('dns').promises;
9
9
  // Load disposable domains list
10
10
  const DISPOSABLE_DOMAINS = require(path.join(__dirname, '..', '..', '..', 'libraries', 'disposable-domains.json'));
11
11
  const DISPOSABLE_SET = new Set(DISPOSABLE_DOMAINS.map(d => d.toLowerCase()));
12
-
13
- // Load OpenAI library
14
- const OpenAI = require(path.join(__dirname, '..', '..', '..', 'libraries', 'openai'));
12
+ const { inferContact } = require(path.join(__dirname, '..', '..', '..', 'libraries', 'infer-contact.js'));
15
13
 
16
14
  module.exports = async ({ assistant, Manager, settings, analytics }) => {
17
15
 
@@ -100,7 +98,7 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
100
98
  // Infer name if not provided
101
99
  let nameInferred = null;
102
100
  if (!firstName && !lastName) {
103
- nameInferred = await inferName(email, assistant);
101
+ nameInferred = await inferContact(email, assistant);
104
102
  firstName = nameInferred.firstName;
105
103
  lastName = nameInferred.lastName;
106
104
  }
@@ -212,102 +210,6 @@ async function validateWithZeroBounce(email) {
212
210
  }
213
211
  }
214
212
 
215
- // Helper: Infer name from email
216
- async function inferName(email, assistant) {
217
- if (process.env.OPENAI_API_KEY) {
218
- const aiResult = await inferNameWithAI(email, assistant);
219
- if (aiResult && (aiResult.firstName || aiResult.lastName)) {
220
- return aiResult;
221
- }
222
- }
223
-
224
- return inferNameFromEmail(email);
225
- }
226
-
227
- // Helper: Use AI to infer name
228
- async function inferNameWithAI(email, assistant) {
229
- try {
230
- const ai = new OpenAI(assistant);
231
- const result = await ai.request({
232
- model: 'gpt-5-mini',
233
- timeout: 30000,
234
- maxTokens: 1024,
235
- moderate: false,
236
- response: 'json',
237
- prompt: {
238
- content: `
239
- <identity>
240
- You extract names and company from email addresses.
241
- </identity>
242
-
243
- <format>
244
- Return ONLY valid JSON like so:
245
- {
246
- "firstName": "...",
247
- "lastName": "...",
248
- "company": "...",
249
- "confidence": "..."
250
- }
251
-
252
- If you cannot determine a name, use empty strings.
253
- </format>
254
- `,
255
- },
256
- message: {
257
- content: `Email: ${email}`,
258
- },
259
- });
260
-
261
- if (result?.firstName !== undefined) {
262
- return {
263
- firstName: capitalize(result.firstName || ''),
264
- lastName: capitalize(result.lastName || ''),
265
- company: capitalize(result.company || ''),
266
- confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
267
- method: 'ai',
268
- };
269
- }
270
- } catch (e) {
271
- console.error('AI name inference error:', e);
272
- }
273
-
274
- return null;
275
- }
276
-
277
- // Helper: Regex-based name inference
278
- function inferNameFromEmail(email) {
279
- const local = email.split('@')[0];
280
- const cleaned = local.replace(/[0-9]+$/, '');
281
- const parts = cleaned.split(/[._-]/);
282
-
283
- if (parts.length >= 2) {
284
- return {
285
- firstName: capitalize(parts[0]),
286
- lastName: capitalize(parts.slice(1).join(' ')),
287
- confidence: 0.5,
288
- method: 'regex',
289
- };
290
- }
291
-
292
- return {
293
- firstName: capitalize(cleaned),
294
- lastName: '',
295
- confidence: 0.25,
296
- method: 'regex',
297
- };
298
- }
299
-
300
- // Helper: Capitalize string
301
- function capitalize(str) {
302
- if (!str) {
303
- return '';
304
- }
305
- return str
306
- .split(' ')
307
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
308
- .join(' ');
309
- }
310
-
311
213
  // Helper: Add contact to SendGrid
312
214
  async function addToSendGrid({ email, firstName, lastName, source, appId, brandName }) {
313
215
  try {
@@ -95,8 +95,6 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
95
95
  trial,
96
96
  confirmationUrl,
97
97
  cancelUrl,
98
- config: Manager.config,
99
- Manager,
100
98
  assistant,
101
99
  });
102
100
  } catch (e) {
@@ -19,11 +19,10 @@ module.exports = {
19
19
  * @param {boolean} options.trial - Whether to include a trial period (subscriptions only)
20
20
  * @param {string} options.confirmationUrl - Success redirect URL
21
21
  * @param {string} options.cancelUrl - Cancel redirect URL
22
- * @param {object} options.Manager - Manager instance
23
22
  * @param {object} options.assistant - Assistant instance
24
23
  * @returns {object} { id, url, raw }
25
24
  */
26
- async createIntent({ uid, orderId, product, productId, frequency, trial, confirmationUrl, Manager, assistant }) {
25
+ async createIntent({ uid, orderId, product, productId, frequency, trial, confirmationUrl, assistant }) {
27
26
  // Guard: test processor is not available in production
28
27
  if (assistant.isProduction()) {
29
28
  throw new Error('Test processor is not available in production');
@@ -32,10 +31,10 @@ module.exports = {
32
31
  const productType = product.type || 'subscription';
33
32
 
34
33
  if (productType === 'subscription') {
35
- return createSubscriptionIntent({ uid, orderId, product, frequency, trial, confirmationUrl, Manager, assistant });
34
+ return createSubscriptionIntent({ uid, orderId, product, frequency, trial, confirmationUrl, assistant });
36
35
  }
37
36
 
38
- return createOneTimeIntent({ uid, orderId, product, productId, confirmationUrl, Manager, assistant });
37
+ return createOneTimeIntent({ uid, orderId, product, productId, confirmationUrl, assistant });
39
38
  },
40
39
  };
41
40
 
@@ -43,7 +42,7 @@ module.exports = {
43
42
  * Create a test subscription intent
44
43
  * Generates Stripe-shaped subscription + customer.subscription.created event
45
44
  */
46
- async function createSubscriptionIntent({ uid, orderId, product, frequency, trial, confirmationUrl, Manager, assistant }) {
45
+ async function createSubscriptionIntent({ uid, orderId, product, frequency, trial, confirmationUrl, assistant }) {
47
46
  // Get the price ID for the requested frequency
48
47
  const priceId = resolvePriceId(product, 'subscription', frequency);
49
48
 
@@ -96,7 +95,7 @@ async function createSubscriptionIntent({ uid, orderId, product, frequency, tria
96
95
  assistant.log(`Test subscription intent: sessionId=${sessionId}, subscriptionId=${subscriptionId}, eventId=${eventId}, trial=${!!subscription.trial_start}`);
97
96
 
98
97
  // Auto-fire webhook
99
- fireWebhook({ event, Manager, assistant });
98
+ fireWebhook({ event, assistant });
100
99
 
101
100
  return {
102
101
  id: sessionId,
@@ -109,7 +108,7 @@ async function createSubscriptionIntent({ uid, orderId, product, frequency, tria
109
108
  * Create a test one-time payment intent
110
109
  * Generates Stripe-shaped checkout session + checkout.session.completed event
111
110
  */
112
- async function createOneTimeIntent({ uid, orderId, product, productId, confirmationUrl, Manager, assistant }) {
111
+ async function createOneTimeIntent({ uid, orderId, product, productId, confirmationUrl, assistant }) {
113
112
  // Validate that a price exists (resolvePriceId throws if not found)
114
113
  resolvePriceId(product, 'one-time', null);
115
114
 
@@ -140,7 +139,7 @@ async function createOneTimeIntent({ uid, orderId, product, productId, confirmat
140
139
  assistant.log(`Test one-time intent: sessionId=${sessionId}, eventId=${eventId}, productId=${productId}`);
141
140
 
142
141
  // Auto-fire webhook
143
- fireWebhook({ event, Manager, assistant });
142
+ fireWebhook({ event, assistant });
144
143
 
145
144
  return {
146
145
  id: sessionId,
@@ -152,8 +151,8 @@ async function createOneTimeIntent({ uid, orderId, product, productId, confirmat
152
151
  /**
153
152
  * Fire-and-forget webhook to trigger the full pipeline
154
153
  */
155
- function fireWebhook({ event, Manager, assistant }) {
156
- const webhookUrl = `${Manager.project.apiUrl}/backend-manager/payments/webhook?processor=test&key=${process.env.BACKEND_MANAGER_KEY}`;
154
+ function fireWebhook({ event, assistant }) {
155
+ const webhookUrl = `${assistant.Manager.project.apiUrl}/backend-manager/payments/webhook?processor=test&key=${process.env.BACKEND_MANAGER_KEY}`;
157
156
  fetch(webhookUrl, {
158
157
  method: 'POST',
159
158
  response: 'json',
@@ -14,8 +14,9 @@ const STATE_KEY = process.env.BACKEND_MANAGER_KEY
14
14
  * Build context object with common OAuth2 data
15
15
  * Used by GET, POST, DELETE handlers
16
16
  */
17
- async function buildContext({ assistant, Manager, user, settings, libraries, requireProvider = true }) {
18
- const { admin } = libraries;
17
+ async function buildContext({ assistant, user, settings, requireProvider = true }) {
18
+ const Manager = assistant.Manager;
19
+ const { admin } = Manager.libraries;
19
20
 
20
21
  // Require authentication
21
22
  if (!user.authenticated) {
@@ -8,14 +8,14 @@ const {
8
8
  *
9
9
  * Revokes tokens with the provider (best effort) and removes the connection.
10
10
  */
11
- module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
12
- const context = await buildContext({ assistant, Manager, user, settings, libraries });
11
+ module.exports = async ({ assistant, user, settings }) => {
12
+ const context = await buildContext({ assistant, user, settings });
13
13
 
14
14
  if (context.error) {
15
15
  return assistant.respond(context.error.message, { code: context.error.code });
16
16
  }
17
17
 
18
- const { admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret } = context;
18
+ const { Manager, admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret } = context;
19
19
 
20
20
  assistant.log('OAuth2 DELETE request', { provider: settings.provider });
21
21
 
@@ -12,8 +12,8 @@ const {
12
12
  * - authorize (default): Get authorization URL
13
13
  * - status: Check connection status
14
14
  */
15
- module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
16
- const context = await buildContext({ assistant, Manager, user, settings, libraries });
15
+ module.exports = async ({ assistant, user, settings }) => {
16
+ const context = await buildContext({ assistant, user, settings });
17
17
 
18
18
  if (context.error) {
19
19
  return assistant.respond(context.error.message, { code: context.error.code });
@@ -14,18 +14,16 @@ const {
14
14
  * - tokenize (default): Exchange authorization code for tokens
15
15
  * - refresh: Refresh access token
16
16
  */
17
- module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
18
- const { admin } = libraries;
19
-
17
+ module.exports = async ({ assistant, user, settings }) => {
20
18
  assistant.log('OAuth2 POST request', { action: settings.action });
21
19
 
22
20
  switch (settings.action) {
23
21
  case 'refresh':
24
- return processRefresh({ assistant, Manager, user, settings, libraries });
22
+ return processRefresh({ assistant, user, settings });
25
23
 
26
24
  case 'tokenize':
27
25
  default:
28
- return processTokenize({ assistant, Manager, admin, settings });
26
+ return processTokenize({ assistant, settings });
29
27
  }
30
28
  };
31
29
 
@@ -33,7 +31,9 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
33
31
  // Handlers
34
32
  // ============================================================================
35
33
 
36
- async function processTokenize({ assistant, Manager, admin, settings }) {
34
+ async function processTokenize({ assistant, settings }) {
35
+ const Manager = assistant.Manager;
36
+ const { admin } = Manager.libraries;
37
37
  assistant.log('processTokenize settings', {
38
38
  hasCode: !!settings.code,
39
39
  codeType: typeof settings.code,
@@ -167,14 +167,14 @@ async function processTokenize({ assistant, Manager, admin, settings }) {
167
167
  return assistant.respond({ success: true });
168
168
  }
169
169
 
170
- async function processRefresh({ assistant, Manager, user, settings, libraries }) {
171
- const context = await buildContext({ assistant, Manager, user, settings, libraries });
170
+ async function processRefresh({ assistant, user, settings }) {
171
+ const context = await buildContext({ assistant, user, settings });
172
172
 
173
173
  if (context.error) {
174
174
  return assistant.respond(context.error.message, { code: context.error.code });
175
175
  }
176
176
 
177
- const { admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret } = context;
177
+ const { Manager, admin, oauth2Provider, targetUid, targetUser, clientId, clientSecret } = context;
178
178
 
179
179
  const refreshToken = targetUser?.oauth2?.[settings.provider]?.token?.refresh_token;
180
180
 
@@ -26,10 +26,10 @@ module.exports = async ({ assistant, user, settings, libraries }) => {
26
26
  let count = 0;
27
27
 
28
28
  // Sign out of main session
29
- count += await signOutOfSession(admin, assistant, uid, sessionPath);
29
+ count += await signOutOfSession(assistant, uid, sessionPath);
30
30
 
31
31
  // Legacy for somiibo and old electron-manager
32
- count += await signOutOfSession(admin, assistant, uid, 'gatherings/online');
32
+ count += await signOutOfSession(assistant, uid, 'gatherings/online');
33
33
 
34
34
  // Revoke Firebase refresh tokens
35
35
  try {
@@ -47,7 +47,8 @@ module.exports = async ({ assistant, user, settings, libraries }) => {
47
47
  /**
48
48
  * Sign out of a specific session path
49
49
  */
50
- async function signOutOfSession(admin, assistant, uid, sessionPath) {
50
+ async function signOutOfSession(assistant, uid, sessionPath) {
51
+ const { admin } = assistant.Manager.libraries;
51
52
  let count = 0;
52
53
 
53
54
  const snapshot = await admin.database().ref(sessionPath)