@constructive-io/send-email-link-fn 0.4.0 → 0.4.2
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/esm/index.js +65 -5
- package/index.js +65 -5
- package/package.json +5 -5
package/esm/index.js
CHANGED
|
@@ -54,16 +54,32 @@ const getRequiredEnv = (name) => {
|
|
|
54
54
|
}
|
|
55
55
|
return value;
|
|
56
56
|
};
|
|
57
|
-
|
|
57
|
+
// TODO: Consider moving this to @constructive-io/knative-job-fn as a shared
|
|
58
|
+
// utility so all job functions can create GraphQL clients with consistent
|
|
59
|
+
// header-based routing without duplicating this logic.
|
|
60
|
+
const createGraphQLClient = (url, options = {}) => {
|
|
58
61
|
const headers = {};
|
|
59
62
|
if (process.env.GRAPHQL_AUTH_TOKEN) {
|
|
60
63
|
headers.Authorization = `Bearer ${process.env.GRAPHQL_AUTH_TOKEN}`;
|
|
61
64
|
}
|
|
62
|
-
const envName = hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER';
|
|
65
|
+
const envName = options.hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER';
|
|
63
66
|
const hostHeader = process.env[envName];
|
|
64
67
|
if (hostHeader) {
|
|
65
68
|
headers.host = hostHeader;
|
|
66
69
|
}
|
|
70
|
+
// Header-based routing for internal cluster services (API_IS_PUBLIC=false)
|
|
71
|
+
if (options.databaseId) {
|
|
72
|
+
headers['X-Database-Id'] = options.databaseId;
|
|
73
|
+
}
|
|
74
|
+
if (options.useMetaSchema) {
|
|
75
|
+
headers['X-Meta-Schema'] = 'true';
|
|
76
|
+
}
|
|
77
|
+
if (options.apiName) {
|
|
78
|
+
headers['X-Api-Name'] = options.apiName;
|
|
79
|
+
}
|
|
80
|
+
if (options.schemata) {
|
|
81
|
+
headers['X-Schemata'] = options.schemata;
|
|
82
|
+
}
|
|
67
83
|
return new GraphQLClient(url, { headers });
|
|
68
84
|
};
|
|
69
85
|
export const sendEmailLink = async (params, context) => {
|
|
@@ -121,7 +137,17 @@ export const sendEmailLink = async (params, context) => {
|
|
|
121
137
|
const nick = company.nick;
|
|
122
138
|
const name = company.name;
|
|
123
139
|
const primary = theme.primary;
|
|
124
|
-
|
|
140
|
+
// Check if this is a localhost-style domain before building hostname
|
|
141
|
+
// TODO: Security consideration - this only affects localhost domains which
|
|
142
|
+
// should not exist in production. The isLocalHost check combined with isDryRun
|
|
143
|
+
// ensures special behavior (http, custom port) only applies in dev environments.
|
|
144
|
+
const isLocalDomain = domain === 'localhost' ||
|
|
145
|
+
domain.startsWith('localhost') ||
|
|
146
|
+
domain === '0.0.0.0';
|
|
147
|
+
// For localhost, skip subdomain to generate cleaner URLs (http://localhost:3000)
|
|
148
|
+
const hostname = subdomain && !isLocalDomain
|
|
149
|
+
? [subdomain, domain].join('.')
|
|
150
|
+
: domain;
|
|
125
151
|
// Treat localhost-style hosts specially so we can generate
|
|
126
152
|
// http://localhost[:port]/... links for local dev without
|
|
127
153
|
// breaking production URLs.
|
|
@@ -253,13 +279,33 @@ app.post('/', async (req, res, next) => {
|
|
|
253
279
|
}
|
|
254
280
|
const graphqlUrl = getRequiredEnv('GRAPHQL_URL');
|
|
255
281
|
const metaGraphqlUrl = process.env.META_GRAPHQL_URL || graphqlUrl;
|
|
256
|
-
|
|
257
|
-
const
|
|
282
|
+
// Get API name or schemata from env (for tenant queries like GetUser)
|
|
283
|
+
const apiName = process.env.GRAPHQL_API_NAME;
|
|
284
|
+
const schemata = process.env.GRAPHQL_SCHEMATA;
|
|
285
|
+
// For GetUser query - needs tenant API access via X-Api-Name or X-Schemata
|
|
286
|
+
const client = createGraphQLClient(graphqlUrl, {
|
|
287
|
+
hostHeaderEnvVar: 'GRAPHQL_HOST_HEADER',
|
|
288
|
+
databaseId,
|
|
289
|
+
...(apiName && { apiName }),
|
|
290
|
+
...(schemata && { schemata }),
|
|
291
|
+
});
|
|
292
|
+
// For GetDatabaseInfo query - uses same API routing as client
|
|
293
|
+
// The private API exposes both user and database queries
|
|
294
|
+
const meta = createGraphQLClient(metaGraphqlUrl, {
|
|
295
|
+
hostHeaderEnvVar: 'META_GRAPHQL_HOST_HEADER',
|
|
296
|
+
databaseId,
|
|
297
|
+
...(apiName && { apiName }),
|
|
298
|
+
...(schemata && { schemata }),
|
|
299
|
+
});
|
|
258
300
|
const result = await sendEmailLink(params, {
|
|
259
301
|
client,
|
|
260
302
|
meta,
|
|
261
303
|
databaseId
|
|
262
304
|
});
|
|
305
|
+
// Validation failures return { missing: '...' } - treat as client error
|
|
306
|
+
if (result && typeof result === 'object' && 'missing' in result) {
|
|
307
|
+
return res.status(400).json({ error: `Missing required field: ${result.missing}` });
|
|
308
|
+
}
|
|
263
309
|
res.status(200).json(result);
|
|
264
310
|
}
|
|
265
311
|
catch (err) {
|
|
@@ -270,6 +316,20 @@ export default app;
|
|
|
270
316
|
// When executed directly (e.g. via `node dist/index.js`), start an HTTP server.
|
|
271
317
|
if (require.main === module) {
|
|
272
318
|
const port = Number(process.env.PORT ?? 8080);
|
|
319
|
+
// Log startup configuration (non-sensitive values only - no API keys or tokens)
|
|
320
|
+
logger.info('[send-email-link] Starting with config:', {
|
|
321
|
+
port,
|
|
322
|
+
graphqlUrl: process.env.GRAPHQL_URL || 'not set',
|
|
323
|
+
metaGraphqlUrl: process.env.META_GRAPHQL_URL || process.env.GRAPHQL_URL || 'not set',
|
|
324
|
+
apiName: process.env.GRAPHQL_API_NAME || 'not set',
|
|
325
|
+
defaultDatabaseId: process.env.DEFAULT_DATABASE_ID || 'not set',
|
|
326
|
+
dryRun: isDryRun,
|
|
327
|
+
useSmtp,
|
|
328
|
+
mailgunDomain: process.env.MAILGUN_DOMAIN || 'not set',
|
|
329
|
+
mailgunFrom: process.env.MAILGUN_FROM || 'not set',
|
|
330
|
+
localAppPort: process.env.LOCAL_APP_PORT || 'not set',
|
|
331
|
+
hasAuthToken: !!process.env.GRAPHQL_AUTH_TOKEN
|
|
332
|
+
});
|
|
273
333
|
// @constructive-io/knative-job-fn exposes a .listen method that delegates to the Express app
|
|
274
334
|
app.listen(port, () => {
|
|
275
335
|
logger.info(`listening on port ${port}`);
|
package/index.js
CHANGED
|
@@ -60,16 +60,32 @@ const getRequiredEnv = (name) => {
|
|
|
60
60
|
}
|
|
61
61
|
return value;
|
|
62
62
|
};
|
|
63
|
-
|
|
63
|
+
// TODO: Consider moving this to @constructive-io/knative-job-fn as a shared
|
|
64
|
+
// utility so all job functions can create GraphQL clients with consistent
|
|
65
|
+
// header-based routing without duplicating this logic.
|
|
66
|
+
const createGraphQLClient = (url, options = {}) => {
|
|
64
67
|
const headers = {};
|
|
65
68
|
if (process.env.GRAPHQL_AUTH_TOKEN) {
|
|
66
69
|
headers.Authorization = `Bearer ${process.env.GRAPHQL_AUTH_TOKEN}`;
|
|
67
70
|
}
|
|
68
|
-
const envName = hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER';
|
|
71
|
+
const envName = options.hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER';
|
|
69
72
|
const hostHeader = process.env[envName];
|
|
70
73
|
if (hostHeader) {
|
|
71
74
|
headers.host = hostHeader;
|
|
72
75
|
}
|
|
76
|
+
// Header-based routing for internal cluster services (API_IS_PUBLIC=false)
|
|
77
|
+
if (options.databaseId) {
|
|
78
|
+
headers['X-Database-Id'] = options.databaseId;
|
|
79
|
+
}
|
|
80
|
+
if (options.useMetaSchema) {
|
|
81
|
+
headers['X-Meta-Schema'] = 'true';
|
|
82
|
+
}
|
|
83
|
+
if (options.apiName) {
|
|
84
|
+
headers['X-Api-Name'] = options.apiName;
|
|
85
|
+
}
|
|
86
|
+
if (options.schemata) {
|
|
87
|
+
headers['X-Schemata'] = options.schemata;
|
|
88
|
+
}
|
|
73
89
|
return new graphql_request_1.GraphQLClient(url, { headers });
|
|
74
90
|
};
|
|
75
91
|
const sendEmailLink = async (params, context) => {
|
|
@@ -127,7 +143,17 @@ const sendEmailLink = async (params, context) => {
|
|
|
127
143
|
const nick = company.nick;
|
|
128
144
|
const name = company.name;
|
|
129
145
|
const primary = theme.primary;
|
|
130
|
-
|
|
146
|
+
// Check if this is a localhost-style domain before building hostname
|
|
147
|
+
// TODO: Security consideration - this only affects localhost domains which
|
|
148
|
+
// should not exist in production. The isLocalHost check combined with isDryRun
|
|
149
|
+
// ensures special behavior (http, custom port) only applies in dev environments.
|
|
150
|
+
const isLocalDomain = domain === 'localhost' ||
|
|
151
|
+
domain.startsWith('localhost') ||
|
|
152
|
+
domain === '0.0.0.0';
|
|
153
|
+
// For localhost, skip subdomain to generate cleaner URLs (http://localhost:3000)
|
|
154
|
+
const hostname = subdomain && !isLocalDomain
|
|
155
|
+
? [subdomain, domain].join('.')
|
|
156
|
+
: domain;
|
|
131
157
|
// Treat localhost-style hosts specially so we can generate
|
|
132
158
|
// http://localhost[:port]/... links for local dev without
|
|
133
159
|
// breaking production URLs.
|
|
@@ -260,13 +286,33 @@ app.post('/', async (req, res, next) => {
|
|
|
260
286
|
}
|
|
261
287
|
const graphqlUrl = getRequiredEnv('GRAPHQL_URL');
|
|
262
288
|
const metaGraphqlUrl = process.env.META_GRAPHQL_URL || graphqlUrl;
|
|
263
|
-
|
|
264
|
-
const
|
|
289
|
+
// Get API name or schemata from env (for tenant queries like GetUser)
|
|
290
|
+
const apiName = process.env.GRAPHQL_API_NAME;
|
|
291
|
+
const schemata = process.env.GRAPHQL_SCHEMATA;
|
|
292
|
+
// For GetUser query - needs tenant API access via X-Api-Name or X-Schemata
|
|
293
|
+
const client = createGraphQLClient(graphqlUrl, {
|
|
294
|
+
hostHeaderEnvVar: 'GRAPHQL_HOST_HEADER',
|
|
295
|
+
databaseId,
|
|
296
|
+
...(apiName && { apiName }),
|
|
297
|
+
...(schemata && { schemata }),
|
|
298
|
+
});
|
|
299
|
+
// For GetDatabaseInfo query - uses same API routing as client
|
|
300
|
+
// The private API exposes both user and database queries
|
|
301
|
+
const meta = createGraphQLClient(metaGraphqlUrl, {
|
|
302
|
+
hostHeaderEnvVar: 'META_GRAPHQL_HOST_HEADER',
|
|
303
|
+
databaseId,
|
|
304
|
+
...(apiName && { apiName }),
|
|
305
|
+
...(schemata && { schemata }),
|
|
306
|
+
});
|
|
265
307
|
const result = await (0, exports.sendEmailLink)(params, {
|
|
266
308
|
client,
|
|
267
309
|
meta,
|
|
268
310
|
databaseId
|
|
269
311
|
});
|
|
312
|
+
// Validation failures return { missing: '...' } - treat as client error
|
|
313
|
+
if (result && typeof result === 'object' && 'missing' in result) {
|
|
314
|
+
return res.status(400).json({ error: `Missing required field: ${result.missing}` });
|
|
315
|
+
}
|
|
270
316
|
res.status(200).json(result);
|
|
271
317
|
}
|
|
272
318
|
catch (err) {
|
|
@@ -277,6 +323,20 @@ exports.default = app;
|
|
|
277
323
|
// When executed directly (e.g. via `node dist/index.js`), start an HTTP server.
|
|
278
324
|
if (require.main === module) {
|
|
279
325
|
const port = Number(process.env.PORT ?? 8080);
|
|
326
|
+
// Log startup configuration (non-sensitive values only - no API keys or tokens)
|
|
327
|
+
logger.info('[send-email-link] Starting with config:', {
|
|
328
|
+
port,
|
|
329
|
+
graphqlUrl: process.env.GRAPHQL_URL || 'not set',
|
|
330
|
+
metaGraphqlUrl: process.env.META_GRAPHQL_URL || process.env.GRAPHQL_URL || 'not set',
|
|
331
|
+
apiName: process.env.GRAPHQL_API_NAME || 'not set',
|
|
332
|
+
defaultDatabaseId: process.env.DEFAULT_DATABASE_ID || 'not set',
|
|
333
|
+
dryRun: isDryRun,
|
|
334
|
+
useSmtp,
|
|
335
|
+
mailgunDomain: process.env.MAILGUN_DOMAIN || 'not set',
|
|
336
|
+
mailgunFrom: process.env.MAILGUN_FROM || 'not set',
|
|
337
|
+
localAppPort: process.env.LOCAL_APP_PORT || 'not set',
|
|
338
|
+
hasAuthToken: !!process.env.GRAPHQL_AUTH_TOKEN
|
|
339
|
+
});
|
|
280
340
|
// @constructive-io/knative-job-fn exposes a .listen method that delegates to the Express app
|
|
281
341
|
app.listen(port, () => {
|
|
282
342
|
logger.info(`listening on port ${port}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/send-email-link-fn",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Knative function to send email links (invite, password reset, email verification) using Constructive jobs",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"makage": "^0.1.10"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@constructive-io/knative-job-fn": "^0.4.
|
|
36
|
+
"@constructive-io/knative-job-fn": "^0.4.1",
|
|
37
37
|
"@launchql/mjml": "0.1.1",
|
|
38
38
|
"@launchql/postmaster": "0.1.4",
|
|
39
39
|
"@launchql/styled-email": "0.1.0",
|
|
40
|
-
"@pgpmjs/env": "^2.10.
|
|
40
|
+
"@pgpmjs/env": "^2.10.1",
|
|
41
41
|
"@pgpmjs/logger": "^1.5.0",
|
|
42
42
|
"graphql-request": "^7.1.2",
|
|
43
43
|
"graphql-tag": "^2.12.6",
|
|
44
|
-
"simple-smtp-server": "^0.2.
|
|
44
|
+
"simple-smtp-server": "^0.2.1"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "39b5c59b01b8ce391dc14daf5a9430ca0ff67574"
|
|
47
47
|
}
|