backend-manager 5.0.188 → 5.0.190
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/package.json +1 -1
- package/src/manager/events/auth/before-create.js +21 -3
- package/src/manager/events/auth/before-signin.js +22 -0
- package/src/manager/events/auth/on-create.js +20 -30
- package/src/manager/events/auth/on-delete.js +20 -30
- package/src/manager/events/auth/utils.js +74 -0
- package/src/manager/functions/core/actions/api/admin/cron.js +1 -1
- package/src/manager/index.js +1 -1
- package/src/manager/libraries/abandoned-cart-config.js +1 -1
- package/src/manager/routes/admin/cron/post.js +1 -1
- /package/src/manager/{cron → events/cron}/daily/data-requests.js +0 -0
- /package/src/manager/{cron → events/cron}/daily/expire-paypal-cancellations.js +0 -0
- /package/src/manager/{cron → events/cron}/daily/ghostii-auto-publisher.js +0 -0
- /package/src/manager/{cron → events/cron}/daily/marketing-newsletter-generate.js +0 -0
- /package/src/manager/{cron → events/cron}/daily/marketing-prune.js +0 -0
- /package/src/manager/{cron → events/cron}/daily/reset-usage.js +0 -0
- /package/src/manager/{cron → events/cron}/daily.js +0 -0
- /package/src/manager/{cron → events/cron}/frequent/abandoned-carts.js +0 -0
- /package/src/manager/{cron → events/cron}/frequent/email-queue.js +0 -0
- /package/src/manager/{cron → events/cron}/frequent/marketing-campaigns.js +0 -0
- /package/src/manager/{cron → events/cron}/frequent.js +0 -0
- /package/src/manager/{cron → events/cron}/runner.js +0 -0
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { isDisposable } = require('../../libraries/email/validation.js');
|
|
2
|
+
const { runAuthHook } = require('./utils.js');
|
|
2
3
|
|
|
3
4
|
const ERROR_TOO_MANY_ATTEMPTS = 'Unable to create account at this time. Please try again later.';
|
|
4
5
|
const ERROR_DISPOSABLE_EMAIL = 'This email domain is not allowed. Please use a different email address.';
|
|
@@ -12,6 +13,23 @@ const MAX_SIGNUPS_PER_DAY = 2;
|
|
|
12
13
|
* Why not create user doc here?
|
|
13
14
|
* - Admin SDK (used for tests) does NOT trigger beforeUserCreated
|
|
14
15
|
* - on-create fires for ALL user creations, making it more reliable
|
|
16
|
+
*
|
|
17
|
+
* Available parameters (1st gen):
|
|
18
|
+
*
|
|
19
|
+
* user (AuthUserRecord):
|
|
20
|
+
* uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled,
|
|
21
|
+
* metadata: { creationTime, lastSignInTime },
|
|
22
|
+
* providerData: [{ uid, displayName, email, photoURL, providerId, phoneNumber }],
|
|
23
|
+
* passwordHash, passwordSalt, customClaims, tenantId, tokensValidAfterTime, multiFactor
|
|
24
|
+
*
|
|
25
|
+
* context (AuthEventContext):
|
|
26
|
+
* ipAddress, userAgent, locale, eventId, eventType, authType, resource, timestamp,
|
|
27
|
+
* additionalUserInfo: { providerId, profile, username, isNewUser, recaptchaScore, email, phoneNumber },
|
|
28
|
+
* credential: { providerId, signInMethod, claims, idToken, accessToken, refreshToken, expirationTime, secret } | null,
|
|
29
|
+
* emailType, smsType, params
|
|
30
|
+
*
|
|
31
|
+
* Note: recaptchaScore requires reCAPTCHA Enterprise (Google Cloud level), NOT the Firebase SMS fraud toggle.
|
|
32
|
+
* Note: credential tokens (idToken, accessToken, refreshToken) require opt-in via BlockingOptions.
|
|
15
33
|
*/
|
|
16
34
|
module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
17
35
|
const startTime = Date.now();
|
|
@@ -54,8 +72,8 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
54
72
|
usage.increment('signups');
|
|
55
73
|
await usage.update();
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
// Run consumer hook (can throw HttpsError to block signup)
|
|
76
|
+
await runAuthHook('before-create', { Manager, assistant, user, context, libraries });
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
// User doc will be created by on-create.js
|
|
78
|
+
assistant.log(`beforeCreate: Completed for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
61
79
|
};
|
|
@@ -6,7 +6,26 @@
|
|
|
6
6
|
*
|
|
7
7
|
* TODO: Add mailer.sync(uid) here with 1x/day rate limit to keep marketing
|
|
8
8
|
* contact data (name, country, subscription fields) fresh between sessions.
|
|
9
|
+
*
|
|
10
|
+
* Available parameters (1st gen):
|
|
11
|
+
*
|
|
12
|
+
* user (AuthUserRecord):
|
|
13
|
+
* uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled,
|
|
14
|
+
* metadata: { creationTime, lastSignInTime },
|
|
15
|
+
* providerData: [{ uid, displayName, email, photoURL, providerId, phoneNumber }],
|
|
16
|
+
* passwordHash, passwordSalt, customClaims, tenantId, tokensValidAfterTime, multiFactor
|
|
17
|
+
*
|
|
18
|
+
* context (AuthEventContext):
|
|
19
|
+
* ipAddress, userAgent, locale, eventId, eventType, authType, resource, timestamp,
|
|
20
|
+
* additionalUserInfo: { providerId, profile, username, isNewUser, recaptchaScore, email, phoneNumber },
|
|
21
|
+
* credential: { providerId, signInMethod, claims, idToken, accessToken, refreshToken, expirationTime, secret } | null,
|
|
22
|
+
* emailType, smsType, params
|
|
23
|
+
*
|
|
24
|
+
* Note: recaptchaScore requires reCAPTCHA Enterprise (Google Cloud level), NOT the Firebase SMS fraud toggle.
|
|
25
|
+
* Note: credential tokens (idToken, accessToken, refreshToken) require opt-in via BlockingOptions.
|
|
9
26
|
*/
|
|
27
|
+
const { runAuthHook } = require('./utils.js');
|
|
28
|
+
|
|
10
29
|
module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
11
30
|
const startTime = Date.now();
|
|
12
31
|
const { admin } = libraries;
|
|
@@ -43,5 +62,8 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
43
62
|
assistant.log(`beforeSignIn: Updated user activity`);
|
|
44
63
|
}
|
|
45
64
|
|
|
65
|
+
// Run consumer hook (can throw HttpsError to block sign-in)
|
|
66
|
+
await runAuthHook('before-signin', { Manager, assistant, user, context, libraries });
|
|
67
|
+
|
|
46
68
|
assistant.log(`beforeSignIn: Completed for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
47
69
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const MAX_RETRIES =
|
|
2
|
-
const RETRY_DELAY_MS = 1000;
|
|
1
|
+
const { retryWrite, runAuthHook, MAX_RETRIES } = require('./utils.js');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* onCreate - Create user doc
|
|
@@ -16,12 +15,23 @@ const RETRY_DELAY_MS = 1000;
|
|
|
16
15
|
*
|
|
17
16
|
* Non-critical work (welcome emails, marketing contact) is handled
|
|
18
17
|
* by the user/signup endpoint, which the frontend calls after account creation.
|
|
18
|
+
*
|
|
19
|
+
* Available parameters (1st gen):
|
|
20
|
+
*
|
|
21
|
+
* user (UserRecord — firebase-admin):
|
|
22
|
+
* uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled,
|
|
23
|
+
* metadata: { creationTime, lastSignInTime, lastRefreshTime },
|
|
24
|
+
* providerData: [{ uid, displayName, email, photoURL, providerId, phoneNumber }],
|
|
25
|
+
* passwordHash, passwordSalt, customClaims, tenantId, tokensValidAfterTime, multiFactor
|
|
26
|
+
*
|
|
27
|
+
* context (EventContext — NOT AuthEventContext, no ipAddress/userAgent/locale):
|
|
28
|
+
* eventId, eventType, timestamp, resource: { service, name }, params
|
|
19
29
|
*/
|
|
20
30
|
module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
21
31
|
const startTime = Date.now();
|
|
22
32
|
const { admin } = libraries;
|
|
23
33
|
|
|
24
|
-
assistant.log(`onCreate: ${user.uid}
|
|
34
|
+
assistant.log(`onCreate: ${user.uid} (${user.email})`, user, context);
|
|
25
35
|
|
|
26
36
|
// Skip anonymous users
|
|
27
37
|
if (user.providerData?.every(p => p.providerId === 'anonymous')) {
|
|
@@ -66,9 +76,9 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
66
76
|
|
|
67
77
|
// Write user doc with retry
|
|
68
78
|
try {
|
|
69
|
-
await retryWrite(assistant, async () => {
|
|
79
|
+
await retryWrite(assistant, 'onCreate', async () => {
|
|
70
80
|
await admin.firestore().doc(`users/${user.uid}`).set(userRecord);
|
|
71
|
-
}
|
|
81
|
+
});
|
|
72
82
|
|
|
73
83
|
assistant.log(`onCreate: Successfully created user doc for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
74
84
|
} catch (error) {
|
|
@@ -77,6 +87,11 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
77
87
|
// Don't reject - the user was already created in Auth
|
|
78
88
|
// The user/signup endpoint will handle creating the doc if it's missing
|
|
79
89
|
}
|
|
90
|
+
|
|
91
|
+
// Run consumer hook (non-blocking — errors logged but don't fail)
|
|
92
|
+
await runAuthHook('on-create', { Manager, assistant, user, context, libraries }).catch(e => {
|
|
93
|
+
assistant.error('onCreate: Consumer hook error:', e);
|
|
94
|
+
});
|
|
80
95
|
};
|
|
81
96
|
|
|
82
97
|
/**
|
|
@@ -102,28 +117,3 @@ function extractProviderName(user) {
|
|
|
102
117
|
last: parts.slice(1).join(' ') || null,
|
|
103
118
|
};
|
|
104
119
|
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Retry a function up to maxRetries times with exponential backoff
|
|
108
|
-
*/
|
|
109
|
-
async function retryWrite(assistant, fn, maxRetries, delayMs) {
|
|
110
|
-
let lastError;
|
|
111
|
-
|
|
112
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
113
|
-
try {
|
|
114
|
-
await fn();
|
|
115
|
-
return; // Success
|
|
116
|
-
} catch (error) {
|
|
117
|
-
lastError = error;
|
|
118
|
-
assistant.error(`onCreate: Write attempt ${attempt}/${maxRetries} failed:`, error);
|
|
119
|
-
|
|
120
|
-
if (attempt < maxRetries) {
|
|
121
|
-
const delay = delayMs * Math.pow(2, attempt - 1); // Exponential backoff: 1s, 2s, 4s
|
|
122
|
-
assistant.log(`onCreate: Retrying in ${delay}ms...`);
|
|
123
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
throw lastError; // All retries failed
|
|
129
|
-
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const MAX_RETRIES =
|
|
2
|
-
const RETRY_DELAY_MS = 1000;
|
|
1
|
+
const { retryWrite, runAuthHook, MAX_RETRIES } = require('./utils.js');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* onDelete - Delete user doc
|
|
@@ -11,12 +10,23 @@ const RETRY_DELAY_MS = 1000;
|
|
|
11
10
|
* - Checks if user doc exists before attempting delete
|
|
12
11
|
* - Retries up to 3 times with exponential backoff on failure
|
|
13
12
|
* - Logs timing for performance monitoring
|
|
13
|
+
*
|
|
14
|
+
* Available parameters (1st gen):
|
|
15
|
+
*
|
|
16
|
+
* user (UserRecord — firebase-admin):
|
|
17
|
+
* uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled,
|
|
18
|
+
* metadata: { creationTime, lastSignInTime, lastRefreshTime },
|
|
19
|
+
* providerData: [{ uid, displayName, email, photoURL, providerId, phoneNumber }],
|
|
20
|
+
* passwordHash, passwordSalt, customClaims, tenantId, tokensValidAfterTime, multiFactor
|
|
21
|
+
*
|
|
22
|
+
* context (EventContext — NOT AuthEventContext, no ipAddress/userAgent/locale):
|
|
23
|
+
* eventId, eventType, timestamp, resource: { service, name }, params
|
|
14
24
|
*/
|
|
15
25
|
module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
16
26
|
const startTime = Date.now();
|
|
17
27
|
const { admin } = libraries;
|
|
18
28
|
|
|
19
|
-
assistant.log(`onDelete: ${user.uid}
|
|
29
|
+
assistant.log(`onDelete: ${user.uid} (${user.email})`, user, context);
|
|
20
30
|
|
|
21
31
|
// Check if user doc exists before attempting delete
|
|
22
32
|
const existingDoc = await admin.firestore().doc(`users/${user.uid}`)
|
|
@@ -33,9 +43,9 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
33
43
|
|
|
34
44
|
// Delete user doc with retry
|
|
35
45
|
try {
|
|
36
|
-
await retryWrite(assistant, async () => {
|
|
46
|
+
await retryWrite(assistant, 'onDelete', async () => {
|
|
37
47
|
await admin.firestore().doc(`users/${user.uid}`).delete();
|
|
38
|
-
}
|
|
48
|
+
});
|
|
39
49
|
|
|
40
50
|
assistant.log(`onDelete: Successfully deleted user doc for ${user.uid}`);
|
|
41
51
|
} catch (error) {
|
|
@@ -60,30 +70,10 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
60
70
|
uuid: user.uid,
|
|
61
71
|
}).event('user_delete', {});
|
|
62
72
|
|
|
73
|
+
// Run consumer hook (non-blocking — errors logged but don't fail)
|
|
74
|
+
await runAuthHook('on-delete', { Manager, assistant, user, context, libraries }).catch(e => {
|
|
75
|
+
assistant.error('onDelete: Consumer hook error:', e);
|
|
76
|
+
});
|
|
77
|
+
|
|
63
78
|
assistant.log(`onDelete: Completed for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
64
79
|
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Retry a function up to maxRetries times with exponential backoff
|
|
68
|
-
*/
|
|
69
|
-
async function retryWrite(assistant, fn, maxRetries, delayMs) {
|
|
70
|
-
let lastError;
|
|
71
|
-
|
|
72
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
73
|
-
try {
|
|
74
|
-
await fn();
|
|
75
|
-
return; // Success
|
|
76
|
-
} catch (error) {
|
|
77
|
-
lastError = error;
|
|
78
|
-
assistant.error(`onDelete: Write attempt ${attempt}/${maxRetries} failed:`, error);
|
|
79
|
-
|
|
80
|
-
if (attempt < maxRetries) {
|
|
81
|
-
const delay = delayMs * Math.pow(2, attempt - 1); // Exponential backoff: 1s, 2s, 4s
|
|
82
|
-
assistant.log(`onDelete: Retrying in ${delay}ms...`);
|
|
83
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
throw lastError; // All retries failed
|
|
89
|
-
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const jetpack = require('fs-jetpack');
|
|
2
|
+
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
const RETRY_DELAY_MS = 1000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Retry a function up to maxRetries times with exponential backoff
|
|
8
|
+
*/
|
|
9
|
+
async function retryWrite(assistant, tag, fn) {
|
|
10
|
+
let lastError;
|
|
11
|
+
|
|
12
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
13
|
+
try {
|
|
14
|
+
await fn();
|
|
15
|
+
return; // Success
|
|
16
|
+
} catch (error) {
|
|
17
|
+
lastError = error;
|
|
18
|
+
assistant.error(`${tag}: Write attempt ${attempt}/${MAX_RETRIES} failed:`, error);
|
|
19
|
+
|
|
20
|
+
if (attempt < MAX_RETRIES) {
|
|
21
|
+
const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoff: 1s, 2s, 4s
|
|
22
|
+
assistant.log(`${tag}: Retrying in ${delay}ms...`);
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw lastError; // All retries failed
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run consumer auth hooks from hooks/auth/{eventName}.js
|
|
33
|
+
*
|
|
34
|
+
* Similar to how cron discovers hooks at hooks/cron/{schedule}/*.js,
|
|
35
|
+
* auth hooks are discovered at hooks/auth/{eventName}.js in the consumer project.
|
|
36
|
+
*
|
|
37
|
+
* For blocking functions (before-create, before-signin):
|
|
38
|
+
* - Hook can throw HttpsError to block the operation
|
|
39
|
+
* - Hook runs AFTER BEM's core checks (disposable email, rate limiting, etc.)
|
|
40
|
+
*
|
|
41
|
+
* For trigger functions (on-create, on-delete):
|
|
42
|
+
* - Hook errors are logged but don't block the operation
|
|
43
|
+
*
|
|
44
|
+
* Hook signature:
|
|
45
|
+
* module.exports = async ({ Manager, assistant, user, context, libraries }) => { ... }
|
|
46
|
+
*
|
|
47
|
+
* Consumer project structure:
|
|
48
|
+
* functions/
|
|
49
|
+
* hooks/
|
|
50
|
+
* auth/
|
|
51
|
+
* before-create.js — runs after BEM checks, can block signup
|
|
52
|
+
* before-signin.js — runs after BEM signin logic, can block signin
|
|
53
|
+
* on-create.js — runs after BEM creates user doc
|
|
54
|
+
* on-delete.js — runs after BEM deletes user doc
|
|
55
|
+
*/
|
|
56
|
+
async function runAuthHook(eventName, args) {
|
|
57
|
+
const { Manager, assistant } = args;
|
|
58
|
+
const hookPath = `${Manager.cwd}/hooks/auth/${eventName}.js`;
|
|
59
|
+
|
|
60
|
+
// Check if hook file exists
|
|
61
|
+
if (!jetpack.exists(hookPath)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
assistant.log(`${eventName}: Running consumer hook @ ${hookPath}`);
|
|
66
|
+
|
|
67
|
+
// Load and execute — passes the same args object the BEM handler received
|
|
68
|
+
const hook = require(hookPath);
|
|
69
|
+
await hook(args);
|
|
70
|
+
|
|
71
|
+
assistant.log(`${eventName}: Consumer hook completed`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { retryWrite, runAuthHook, MAX_RETRIES };
|
|
@@ -22,7 +22,7 @@ Module.prototype.main = function () {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Run the cron job
|
|
25
|
-
Manager._process((new (require(
|
|
25
|
+
Manager._process((new (require(`${Manager.rootDirectory}/events/cron/${payload.data.payload.id}.js`))()).init(Manager, { context: {}, }))
|
|
26
26
|
.then((res) => {
|
|
27
27
|
return resolve({data: res});
|
|
28
28
|
})
|
package/src/manager/index.js
CHANGED
|
@@ -14,8 +14,8 @@ const util = require('util');
|
|
|
14
14
|
const core = './functions/core';
|
|
15
15
|
const wrappers = './functions/wrappers';
|
|
16
16
|
const _legacy = './functions/_legacy';
|
|
17
|
-
const cron = path.resolve(__dirname, './cron');
|
|
18
17
|
const events = path.resolve(__dirname, './events');
|
|
18
|
+
const cron = path.resolve(events, './cron');
|
|
19
19
|
|
|
20
20
|
const BEM_CONFIG_TEMPLATE_PATH = path.resolve(__dirname, '../../templates/backend-manager-config.json');
|
|
21
21
|
const BEM_PACKAGE = require('../../package.json');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Abandoned cart reminder configuration (SSOT)
|
|
3
3
|
*
|
|
4
4
|
* Used by:
|
|
5
|
-
* - cron/frequent/abandoned-carts.js (processing reminders)
|
|
5
|
+
* - events/cron/frequent/abandoned-carts.js (processing reminders)
|
|
6
6
|
* - Client-side checkout page (creating cart doc with first delay)
|
|
7
7
|
*/
|
|
8
8
|
module.exports = {
|
|
@@ -22,7 +22,7 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
22
22
|
assistant.log('Running cron job:', settings.id);
|
|
23
23
|
|
|
24
24
|
// Run the cron job
|
|
25
|
-
const cronPath =
|
|
25
|
+
const cronPath = `${Manager.rootDirectory}/events/cron/${settings.id}.js`;
|
|
26
26
|
const cronHandler = require(cronPath);
|
|
27
27
|
const result = await cronHandler({ Manager, assistant, context: {}, libraries: Manager.libraries }).catch(e => e);
|
|
28
28
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|