@constructive-io/send-email-link-fn 0.3.1 → 0.4.1

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 (3) hide show
  1. package/esm/index.js +65 -5
  2. package/index.js +65 -5
  3. package/package.json +4 -4
package/esm/index.js CHANGED
@@ -54,16 +54,32 @@ const getRequiredEnv = (name) => {
54
54
  }
55
55
  return value;
56
56
  };
57
- const createGraphQLClient = (url, hostHeaderEnvVar) => {
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
- const hostname = subdomain ? [subdomain, domain].join('.') : domain;
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
- const client = createGraphQLClient(graphqlUrl, 'GRAPHQL_HOST_HEADER');
257
- const meta = createGraphQLClient(metaGraphqlUrl, 'META_GRAPHQL_HOST_HEADER');
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
- const createGraphQLClient = (url, hostHeaderEnvVar) => {
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
- const hostname = subdomain ? [subdomain, domain].join('.') : domain;
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
- const client = createGraphQLClient(graphqlUrl, 'GRAPHQL_HOST_HEADER');
264
- const meta = createGraphQLClient(metaGraphqlUrl, 'META_GRAPHQL_HOST_HEADER');
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.3.1",
3
+ "version": "0.4.1",
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.3.1",
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
40
  "@pgpmjs/env": "^2.10.0",
41
- "@pgpmjs/logger": "^1.4.0",
41
+ "@pgpmjs/logger": "^1.5.0",
42
42
  "graphql-request": "^7.1.2",
43
43
  "graphql-tag": "^2.12.6",
44
44
  "simple-smtp-server": "^0.2.0"
45
45
  },
46
- "gitHead": "3ffd5718e86ea5fa9ca6e0930aeb510cf392f343"
46
+ "gitHead": "eb66bc6e0ee3f0bcfcfeda2b5885b05c153de517"
47
47
  }