@constructive-io/send-email-link-fn 0.3.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.
- package/LICENSE +23 -0
- package/README.md +236 -0
- package/esm/index.js +277 -0
- package/index.d.ts +25 -0
- package/index.js +284 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
Copyright (c) 2025 Constructive <developers@constructive.io>
|
|
5
|
+
Copyright (c) 2020-present, Interweb, Inc.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @constructive-io/send-email-link-fn
|
|
2
|
+
|
|
3
|
+
Knative-compatible email link function used with the Constructive jobs system. It is designed to be invoked by `@constructive-io/knative-job-worker` as an HTTP function named `send-email-link`.
|
|
4
|
+
|
|
5
|
+
The function:
|
|
6
|
+
- Reads metadata about the tenant/site from a GraphQL API
|
|
7
|
+
- Generates a styled HTML email using `@launchql/mjml`
|
|
8
|
+
- Sends the email via `@launchql/postmaster`
|
|
9
|
+
- Supports invite, password reset, and email verification flows
|
|
10
|
+
|
|
11
|
+
## Expected job payload
|
|
12
|
+
|
|
13
|
+
Jobs should use `task_identifier = 'send-email-link'` and a JSON payload like:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"email_type": "invite_email",
|
|
18
|
+
"email": "user@example.com",
|
|
19
|
+
"invite_token": "abc123",
|
|
20
|
+
"sender_id": "00000000-0000-0000-0000-000000000001"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Supported `email_type` values and parameters:
|
|
25
|
+
|
|
26
|
+
- `invite_email`
|
|
27
|
+
- `email` (string, required)
|
|
28
|
+
- `invite_token` (string, required)
|
|
29
|
+
- `sender_id` (UUID string, required)
|
|
30
|
+
- `forgot_password`
|
|
31
|
+
- `email` (string, required)
|
|
32
|
+
- `user_id` (UUID string, required)
|
|
33
|
+
- `reset_token` (string, required)
|
|
34
|
+
- `email_verification`
|
|
35
|
+
- `email` (string, required)
|
|
36
|
+
- `email_id` (UUID string, required)
|
|
37
|
+
- `verification_token` (string, required)
|
|
38
|
+
|
|
39
|
+
If required fields are missing the function returns a small JSON object like:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "missing": "email_type" }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## HTTP contract (with knative-job-worker)
|
|
46
|
+
|
|
47
|
+
The function is wrapped by `@constructive-io/knative-job-fn`, so it expects:
|
|
48
|
+
|
|
49
|
+
- HTTP method: `POST`
|
|
50
|
+
- Body: JSON job payload (see above)
|
|
51
|
+
- Headers (set by `@constructive-io/knative-job-worker`):
|
|
52
|
+
- `X-Worker-Id`
|
|
53
|
+
- `X-Job-Id`
|
|
54
|
+
- `X-Database-Id`
|
|
55
|
+
- `X-Callback-Url`
|
|
56
|
+
|
|
57
|
+
The handler will:
|
|
58
|
+
|
|
59
|
+
1. Resolve the tenant/site by `databaseId` via GraphQL
|
|
60
|
+
2. Generate an email link and HTML via `@launchql/mjml`
|
|
61
|
+
3. Send the email with `@launchql/postmaster`
|
|
62
|
+
4. Respond with HTTP 200 and JSON:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "complete": true }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Errors are propagated through the Express error middleware installed by `@constructive-io/knative-job-fn`, so they can be translated into `X-Job-Error` callbacks by your gateway/callback server.
|
|
69
|
+
|
|
70
|
+
## Environment variables
|
|
71
|
+
|
|
72
|
+
Required:
|
|
73
|
+
|
|
74
|
+
- `GRAPHQL_URL`
|
|
75
|
+
GraphQL endpoint for the tenant database (for `GetUser` and/or per-tenant data).
|
|
76
|
+
|
|
77
|
+
Recommended / optional:
|
|
78
|
+
|
|
79
|
+
- `META_GRAPHQL_URL`
|
|
80
|
+
GraphQL endpoint for meta/database-level schema. Defaults to `GRAPHQL_URL` when not set.
|
|
81
|
+
- `GRAPHQL_AUTH_TOKEN`
|
|
82
|
+
Bearer token to send as `Authorization` header for GraphQL requests.
|
|
83
|
+
- `DEFAULT_DATABASE_ID`
|
|
84
|
+
Used if `X-Database-Id` is not provided by the worker. In normal jobs usage, `X-Database-Id` should always be present.
|
|
85
|
+
- `LOCAL_APP_PORT`
|
|
86
|
+
Optional port suffix for localhost-style hosts (e.g. `3000`). When the resolved hostname is `localhost` / `*.localhost` and `SEND_EMAIL_LINK_DRY_RUN=true`, links are generated as `http://localhost:LOCAL_APP_PORT/...`. Ignored for non-local hostnames and in production.
|
|
87
|
+
|
|
88
|
+
Email delivery (default: `@launchql/postmaster`):
|
|
89
|
+
|
|
90
|
+
- Set `EMAIL_SEND_USE_SMTP=true` to switch to `simple-smtp-server` (SMTP). Otherwise it uses `@launchql/postmaster`.
|
|
91
|
+
|
|
92
|
+
- Mailgun or another provider; consult `@launchql/postmaster` docs. A common pattern is:
|
|
93
|
+
- `MAILGUN_API_KEY`
|
|
94
|
+
- `MAILGUN_DOMAIN`
|
|
95
|
+
- `MAILGUN_FROM`
|
|
96
|
+
|
|
97
|
+
- SMTP variables when `EMAIL_SEND_USE_SMTP=true`:
|
|
98
|
+
- `SMTP_HOST`
|
|
99
|
+
- `SMTP_PORT`
|
|
100
|
+
- `SMTP_USER`
|
|
101
|
+
- `SMTP_PASS`
|
|
102
|
+
- `SMTP_FROM`
|
|
103
|
+
|
|
104
|
+
## Building locally
|
|
105
|
+
|
|
106
|
+
From the repo root:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pnpm --filter="@constructive-io/send-email-link-fn" build
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This compiles TypeScript into `dist/`.
|
|
113
|
+
|
|
114
|
+
## Dockerfile
|
|
115
|
+
|
|
116
|
+
The function is intended to be containerized and run as a Knative Service. A minimal Dockerfile:
|
|
117
|
+
|
|
118
|
+
```dockerfile
|
|
119
|
+
FROM node:18-alpine
|
|
120
|
+
|
|
121
|
+
WORKDIR /usr/src/app
|
|
122
|
+
|
|
123
|
+
# Install production dependencies
|
|
124
|
+
COPY package.json pnpm-lock.yaml ./
|
|
125
|
+
RUN npm install -g pnpm@9 && pnpm install --prod
|
|
126
|
+
|
|
127
|
+
# Copy compiled code
|
|
128
|
+
COPY dist ./dist
|
|
129
|
+
|
|
130
|
+
ENV NODE_ENV=production
|
|
131
|
+
ENV PORT=8080
|
|
132
|
+
|
|
133
|
+
CMD ["node", "dist/index.js"]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Build and push:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pnpm --filter="@constructive-io/send-email-link-fn" build
|
|
140
|
+
docker build -t your-registry/send-email-link-fn:latest functions/send-email-link
|
|
141
|
+
docker push your-registry/send-email-link-fn:latest
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Example Knative Service
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
apiVersion: serving.knative.dev/v1
|
|
148
|
+
kind: Service
|
|
149
|
+
metadata:
|
|
150
|
+
name: send-email-link
|
|
151
|
+
namespace: default
|
|
152
|
+
spec:
|
|
153
|
+
template:
|
|
154
|
+
spec:
|
|
155
|
+
containers:
|
|
156
|
+
- image: your-registry/send-email-link-fn:latest
|
|
157
|
+
env:
|
|
158
|
+
- name: GRAPHQL_URL
|
|
159
|
+
value: "https://api.your-domain.com/graphql"
|
|
160
|
+
- name: META_GRAPHQL_URL
|
|
161
|
+
value: "https://meta-api.your-domain.com/graphql"
|
|
162
|
+
- name: GRAPHQL_AUTH_TOKEN
|
|
163
|
+
valueFrom:
|
|
164
|
+
secretKeyRef:
|
|
165
|
+
name: graphql-auth
|
|
166
|
+
key: token
|
|
167
|
+
# MAILGUN / Postmaster config here...
|
|
168
|
+
- name: MAILGUN_API_KEY
|
|
169
|
+
valueFrom:
|
|
170
|
+
secretKeyRef:
|
|
171
|
+
name: mailgun
|
|
172
|
+
key: api-key
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Once deployed, point `@constructive-io/knative-job-worker` at this service by configuring:
|
|
176
|
+
|
|
177
|
+
- `KNATIVE_SERVICE_URL` to route `/send-email-link` to this function
|
|
178
|
+
- `JOBS_SUPPORTED=send-email-link` (or `JOBS_SUPPORT_ANY=true`)
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Education and Tutorials
|
|
183
|
+
|
|
184
|
+
1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
|
|
185
|
+
Get started with modular databases in minutes. Install prerequisites and deploy your first module.
|
|
186
|
+
|
|
187
|
+
2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
|
|
188
|
+
Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
|
|
189
|
+
|
|
190
|
+
3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
|
|
191
|
+
Master the workflow for adding, organizing, and managing database changes with pgpm.
|
|
192
|
+
|
|
193
|
+
4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
|
|
194
|
+
Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
|
|
195
|
+
|
|
196
|
+
5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
|
|
197
|
+
Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
|
|
198
|
+
|
|
199
|
+
6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
|
|
200
|
+
Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
|
|
201
|
+
|
|
202
|
+
7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
|
|
203
|
+
Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
204
|
+
|
|
205
|
+
## Related Constructive Tooling
|
|
206
|
+
|
|
207
|
+
### 📦 Package Management
|
|
208
|
+
|
|
209
|
+
* [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
|
|
210
|
+
|
|
211
|
+
### 🧪 Testing
|
|
212
|
+
|
|
213
|
+
* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
|
|
214
|
+
* [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
|
|
215
|
+
* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
|
|
216
|
+
* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
|
|
217
|
+
* [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
|
|
218
|
+
|
|
219
|
+
### 🧠 Parsing & AST
|
|
220
|
+
|
|
221
|
+
* [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
|
|
222
|
+
* [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
|
|
223
|
+
* [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
|
|
224
|
+
* [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
|
|
225
|
+
* [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
|
|
226
|
+
* [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
|
|
227
|
+
|
|
228
|
+
## Credits
|
|
229
|
+
|
|
230
|
+
**🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
|
|
231
|
+
|
|
232
|
+
## Disclaimer
|
|
233
|
+
|
|
234
|
+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
|
|
235
|
+
|
|
236
|
+
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { createJobApp } from '@constructive-io/knative-job-fn';
|
|
2
|
+
import { GraphQLClient } from 'graphql-request';
|
|
3
|
+
import gql from 'graphql-tag';
|
|
4
|
+
import { generate } from '@launchql/mjml';
|
|
5
|
+
import { send as sendPostmaster } from '@launchql/postmaster';
|
|
6
|
+
import { send as sendSmtp } from 'simple-smtp-server';
|
|
7
|
+
import { parseEnvBoolean } from '@pgpmjs/env';
|
|
8
|
+
import { createLogger } from '@pgpmjs/logger';
|
|
9
|
+
const isDryRun = parseEnvBoolean(process.env.SEND_EMAIL_LINK_DRY_RUN) ?? false;
|
|
10
|
+
const useSmtp = parseEnvBoolean(process.env.EMAIL_SEND_USE_SMTP) ?? false;
|
|
11
|
+
const logger = createLogger('send-email-link');
|
|
12
|
+
const app = createJobApp();
|
|
13
|
+
const GetUser = gql `
|
|
14
|
+
query GetUser($userId: UUID!) {
|
|
15
|
+
user(id: $userId) {
|
|
16
|
+
username
|
|
17
|
+
displayName
|
|
18
|
+
profilePicture
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
const GetDatabaseInfo = gql `
|
|
23
|
+
query GetDatabaseInfo($databaseId: UUID!) {
|
|
24
|
+
database(id: $databaseId) {
|
|
25
|
+
sites {
|
|
26
|
+
nodes {
|
|
27
|
+
domains {
|
|
28
|
+
nodes {
|
|
29
|
+
subdomain
|
|
30
|
+
domain
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
logo
|
|
34
|
+
title
|
|
35
|
+
siteThemes {
|
|
36
|
+
nodes {
|
|
37
|
+
theme
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
siteModules(condition: { name: "legal_terms_module" }) {
|
|
41
|
+
nodes {
|
|
42
|
+
data
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
const getRequiredEnv = (name) => {
|
|
51
|
+
const value = process.env[name];
|
|
52
|
+
if (!value) {
|
|
53
|
+
throw new Error(`Missing required environment variable ${name}`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
const createGraphQLClient = (url, hostHeaderEnvVar) => {
|
|
58
|
+
const headers = {};
|
|
59
|
+
if (process.env.GRAPHQL_AUTH_TOKEN) {
|
|
60
|
+
headers.Authorization = `Bearer ${process.env.GRAPHQL_AUTH_TOKEN}`;
|
|
61
|
+
}
|
|
62
|
+
const envName = hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER';
|
|
63
|
+
const hostHeader = process.env[envName];
|
|
64
|
+
if (hostHeader) {
|
|
65
|
+
headers.host = hostHeader;
|
|
66
|
+
}
|
|
67
|
+
return new GraphQLClient(url, { headers });
|
|
68
|
+
};
|
|
69
|
+
export const sendEmailLink = async (params, context) => {
|
|
70
|
+
const { client, meta, databaseId } = context;
|
|
71
|
+
const validateForType = () => {
|
|
72
|
+
switch (params.email_type) {
|
|
73
|
+
case 'invite_email':
|
|
74
|
+
if (!params.invite_token || !params.sender_id) {
|
|
75
|
+
return { missing: 'invite_token_or_sender_id' };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
case 'forgot_password':
|
|
79
|
+
if (!params.user_id || !params.reset_token) {
|
|
80
|
+
return { missing: 'user_id_or_reset_token' };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
case 'email_verification':
|
|
84
|
+
if (!params.email_id || !params.verification_token) {
|
|
85
|
+
return { missing: 'email_id_or_verification_token' };
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
default:
|
|
89
|
+
return { missing: 'email_type' };
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
if (!params.email_type) {
|
|
93
|
+
return { missing: 'email_type' };
|
|
94
|
+
}
|
|
95
|
+
if (!params.email) {
|
|
96
|
+
return { missing: 'email' };
|
|
97
|
+
}
|
|
98
|
+
const typeValidation = validateForType();
|
|
99
|
+
if (typeValidation) {
|
|
100
|
+
return typeValidation;
|
|
101
|
+
}
|
|
102
|
+
const databaseInfo = await meta.request(GetDatabaseInfo, {
|
|
103
|
+
databaseId
|
|
104
|
+
});
|
|
105
|
+
const site = databaseInfo?.database?.sites?.nodes?.[0];
|
|
106
|
+
if (!site) {
|
|
107
|
+
throw new Error('Site not found for database');
|
|
108
|
+
}
|
|
109
|
+
const legalTermsModule = site.siteModules?.nodes?.[0];
|
|
110
|
+
const domainNode = site.domains?.nodes?.[0];
|
|
111
|
+
const theme = site.siteThemes?.nodes?.[0]?.theme;
|
|
112
|
+
if (!legalTermsModule || !domainNode || !theme) {
|
|
113
|
+
throw new Error('Missing site configuration for email');
|
|
114
|
+
}
|
|
115
|
+
const subdomain = domainNode.subdomain;
|
|
116
|
+
const domain = domainNode.domain;
|
|
117
|
+
const supportEmail = legalTermsModule.data.emails.support;
|
|
118
|
+
const logo = site.logo?.url;
|
|
119
|
+
const company = legalTermsModule.data.company;
|
|
120
|
+
const website = company.website;
|
|
121
|
+
const nick = company.nick;
|
|
122
|
+
const name = company.name;
|
|
123
|
+
const primary = theme.primary;
|
|
124
|
+
const hostname = subdomain ? [subdomain, domain].join('.') : domain;
|
|
125
|
+
// Treat localhost-style hosts specially so we can generate
|
|
126
|
+
// http://localhost[:port]/... links for local dev without
|
|
127
|
+
// breaking production URLs.
|
|
128
|
+
const isLocalHost = hostname.startsWith('localhost') ||
|
|
129
|
+
hostname.startsWith('0.0.0.0') ||
|
|
130
|
+
hostname.endsWith('.localhost');
|
|
131
|
+
// Optional: LOCAL_APP_PORT lets you attach a port for local dashboards
|
|
132
|
+
// e.g. LOCAL_APP_PORT=3000 -> http://localhost:3000
|
|
133
|
+
// It is ignored for non-local hostnames. Only allow on DRY RUNs
|
|
134
|
+
const localPort = isLocalHost && isDryRun && process.env.LOCAL_APP_PORT
|
|
135
|
+
? `:${process.env.LOCAL_APP_PORT}`
|
|
136
|
+
: '';
|
|
137
|
+
// Use http only for local dry-run to avoid browser TLS warnings
|
|
138
|
+
// in dev; production stays https.
|
|
139
|
+
const protocol = isLocalHost && isDryRun ? 'http' : 'https';
|
|
140
|
+
const url = new URL(`${protocol}://${hostname}${localPort}`);
|
|
141
|
+
let subject;
|
|
142
|
+
let subMessage;
|
|
143
|
+
let linkText;
|
|
144
|
+
let inviterName;
|
|
145
|
+
switch (params.email_type) {
|
|
146
|
+
case 'invite_email': {
|
|
147
|
+
if (!params.invite_token || !params.sender_id) {
|
|
148
|
+
return { missing: 'invite_token_or_sender_id' };
|
|
149
|
+
}
|
|
150
|
+
url.pathname = 'register';
|
|
151
|
+
url.searchParams.append('invite_token', params.invite_token);
|
|
152
|
+
url.searchParams.append('email', params.email);
|
|
153
|
+
const scope = Number(params.invite_type) === 2 ? 'org' : 'app';
|
|
154
|
+
url.searchParams.append('type', scope);
|
|
155
|
+
const inviter = await client.request(GetUser, {
|
|
156
|
+
userId: params.sender_id
|
|
157
|
+
});
|
|
158
|
+
inviterName = inviter?.user?.displayName;
|
|
159
|
+
if (inviterName) {
|
|
160
|
+
subject = `${inviterName} invited you to ${nick}!`;
|
|
161
|
+
subMessage = `You've been invited to ${nick}`;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
subject = `Welcome to ${nick}!`;
|
|
165
|
+
subMessage = `You've been invited to ${nick}`;
|
|
166
|
+
}
|
|
167
|
+
linkText = 'Join Us';
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'forgot_password': {
|
|
171
|
+
if (!params.user_id || !params.reset_token) {
|
|
172
|
+
return { missing: 'user_id_or_reset_token' };
|
|
173
|
+
}
|
|
174
|
+
url.pathname = 'reset-password';
|
|
175
|
+
url.searchParams.append('role_id', params.user_id);
|
|
176
|
+
url.searchParams.append('reset_token', params.reset_token);
|
|
177
|
+
subject = `${nick} Password Reset Request`;
|
|
178
|
+
subMessage = 'Click below to reset your password';
|
|
179
|
+
linkText = 'Reset Password';
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'email_verification': {
|
|
183
|
+
if (!params.email_id || !params.verification_token) {
|
|
184
|
+
return { missing: 'email_id_or_verification_token' };
|
|
185
|
+
}
|
|
186
|
+
url.pathname = 'verify-email';
|
|
187
|
+
url.searchParams.append('email_id', params.email_id);
|
|
188
|
+
url.searchParams.append('verification_token', params.verification_token);
|
|
189
|
+
subject = `${nick} Email Verification`;
|
|
190
|
+
subMessage = 'Please confirm your email address';
|
|
191
|
+
linkText = 'Confirm Email';
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
const link = url.href;
|
|
198
|
+
const html = generate({
|
|
199
|
+
title: subject,
|
|
200
|
+
link,
|
|
201
|
+
linkText,
|
|
202
|
+
message: subject,
|
|
203
|
+
subMessage,
|
|
204
|
+
bodyBgColor: 'white',
|
|
205
|
+
headerBgColor: 'white',
|
|
206
|
+
messageBgColor: 'white',
|
|
207
|
+
messageTextColor: '#414141',
|
|
208
|
+
messageButtonBgColor: primary,
|
|
209
|
+
messageButtonTextColor: 'white',
|
|
210
|
+
companyName: name,
|
|
211
|
+
supportEmail,
|
|
212
|
+
website,
|
|
213
|
+
logo,
|
|
214
|
+
headerImageProps: {
|
|
215
|
+
alt: 'logo',
|
|
216
|
+
align: 'center',
|
|
217
|
+
border: 'none',
|
|
218
|
+
width: '162px',
|
|
219
|
+
paddingLeft: '0px',
|
|
220
|
+
paddingRight: '0px',
|
|
221
|
+
paddingBottom: '0px',
|
|
222
|
+
paddingTop: '0'
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
if (isDryRun) {
|
|
226
|
+
logger.info('DRY RUN email (skipping send)', {
|
|
227
|
+
email_type: params.email_type,
|
|
228
|
+
email: params.email,
|
|
229
|
+
subject,
|
|
230
|
+
link
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const sendEmail = useSmtp ? sendSmtp : sendPostmaster;
|
|
235
|
+
await sendEmail({
|
|
236
|
+
to: params.email,
|
|
237
|
+
subject,
|
|
238
|
+
html
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
complete: true,
|
|
243
|
+
...(isDryRun ? { dryRun: true } : null)
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
// HTTP/Knative entrypoint (used by @constructive-io/knative-job-fn wrapper)
|
|
247
|
+
app.post('/', async (req, res, next) => {
|
|
248
|
+
try {
|
|
249
|
+
const params = (req.body || {});
|
|
250
|
+
const databaseId = req.get('X-Database-Id') || req.get('x-database-id') || process.env.DEFAULT_DATABASE_ID;
|
|
251
|
+
if (!databaseId) {
|
|
252
|
+
return res.status(400).json({ error: 'Missing X-Database-Id header or DEFAULT_DATABASE_ID' });
|
|
253
|
+
}
|
|
254
|
+
const graphqlUrl = getRequiredEnv('GRAPHQL_URL');
|
|
255
|
+
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');
|
|
258
|
+
const result = await sendEmailLink(params, {
|
|
259
|
+
client,
|
|
260
|
+
meta,
|
|
261
|
+
databaseId
|
|
262
|
+
});
|
|
263
|
+
res.status(200).json(result);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
next(err);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
export default app;
|
|
270
|
+
// When executed directly (e.g. via `node dist/index.js`), start an HTTP server.
|
|
271
|
+
if (require.main === module) {
|
|
272
|
+
const port = Number(process.env.PORT ?? 8080);
|
|
273
|
+
// @constructive-io/knative-job-fn exposes a .listen method that delegates to the Express app
|
|
274
|
+
app.listen(port, () => {
|
|
275
|
+
logger.info(`listening on port ${port}`);
|
|
276
|
+
});
|
|
277
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GraphQLClient } from 'graphql-request';
|
|
2
|
+
declare const app: any;
|
|
3
|
+
type SendEmailParams = {
|
|
4
|
+
email_type: 'invite_email' | 'forgot_password' | 'email_verification';
|
|
5
|
+
email: string;
|
|
6
|
+
invite_type?: number | string;
|
|
7
|
+
invite_token?: string;
|
|
8
|
+
sender_id?: string;
|
|
9
|
+
user_id?: string;
|
|
10
|
+
reset_token?: string;
|
|
11
|
+
email_id?: string;
|
|
12
|
+
verification_token?: string;
|
|
13
|
+
};
|
|
14
|
+
type GraphQLContext = {
|
|
15
|
+
client: GraphQLClient;
|
|
16
|
+
meta: GraphQLClient;
|
|
17
|
+
databaseId: string;
|
|
18
|
+
};
|
|
19
|
+
export declare const sendEmailLink: (params: SendEmailParams, context: GraphQLContext) => Promise<false | {
|
|
20
|
+
missing?: string;
|
|
21
|
+
} | {
|
|
22
|
+
dryRun: boolean;
|
|
23
|
+
complete: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
export default app;
|
package/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sendEmailLink = void 0;
|
|
7
|
+
const knative_job_fn_1 = require("@constructive-io/knative-job-fn");
|
|
8
|
+
const graphql_request_1 = require("graphql-request");
|
|
9
|
+
const graphql_tag_1 = __importDefault(require("graphql-tag"));
|
|
10
|
+
const mjml_1 = require("@launchql/mjml");
|
|
11
|
+
const postmaster_1 = require("@launchql/postmaster");
|
|
12
|
+
const simple_smtp_server_1 = require("simple-smtp-server");
|
|
13
|
+
const env_1 = require("@pgpmjs/env");
|
|
14
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
15
|
+
const isDryRun = (0, env_1.parseEnvBoolean)(process.env.SEND_EMAIL_LINK_DRY_RUN) ?? false;
|
|
16
|
+
const useSmtp = (0, env_1.parseEnvBoolean)(process.env.EMAIL_SEND_USE_SMTP) ?? false;
|
|
17
|
+
const logger = (0, logger_1.createLogger)('send-email-link');
|
|
18
|
+
const app = (0, knative_job_fn_1.createJobApp)();
|
|
19
|
+
const GetUser = (0, graphql_tag_1.default) `
|
|
20
|
+
query GetUser($userId: UUID!) {
|
|
21
|
+
user(id: $userId) {
|
|
22
|
+
username
|
|
23
|
+
displayName
|
|
24
|
+
profilePicture
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
const GetDatabaseInfo = (0, graphql_tag_1.default) `
|
|
29
|
+
query GetDatabaseInfo($databaseId: UUID!) {
|
|
30
|
+
database(id: $databaseId) {
|
|
31
|
+
sites {
|
|
32
|
+
nodes {
|
|
33
|
+
domains {
|
|
34
|
+
nodes {
|
|
35
|
+
subdomain
|
|
36
|
+
domain
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
logo
|
|
40
|
+
title
|
|
41
|
+
siteThemes {
|
|
42
|
+
nodes {
|
|
43
|
+
theme
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
siteModules(condition: { name: "legal_terms_module" }) {
|
|
47
|
+
nodes {
|
|
48
|
+
data
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
const getRequiredEnv = (name) => {
|
|
57
|
+
const value = process.env[name];
|
|
58
|
+
if (!value) {
|
|
59
|
+
throw new Error(`Missing required environment variable ${name}`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
const createGraphQLClient = (url, hostHeaderEnvVar) => {
|
|
64
|
+
const headers = {};
|
|
65
|
+
if (process.env.GRAPHQL_AUTH_TOKEN) {
|
|
66
|
+
headers.Authorization = `Bearer ${process.env.GRAPHQL_AUTH_TOKEN}`;
|
|
67
|
+
}
|
|
68
|
+
const envName = hostHeaderEnvVar || 'GRAPHQL_HOST_HEADER';
|
|
69
|
+
const hostHeader = process.env[envName];
|
|
70
|
+
if (hostHeader) {
|
|
71
|
+
headers.host = hostHeader;
|
|
72
|
+
}
|
|
73
|
+
return new graphql_request_1.GraphQLClient(url, { headers });
|
|
74
|
+
};
|
|
75
|
+
const sendEmailLink = async (params, context) => {
|
|
76
|
+
const { client, meta, databaseId } = context;
|
|
77
|
+
const validateForType = () => {
|
|
78
|
+
switch (params.email_type) {
|
|
79
|
+
case 'invite_email':
|
|
80
|
+
if (!params.invite_token || !params.sender_id) {
|
|
81
|
+
return { missing: 'invite_token_or_sender_id' };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
case 'forgot_password':
|
|
85
|
+
if (!params.user_id || !params.reset_token) {
|
|
86
|
+
return { missing: 'user_id_or_reset_token' };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
case 'email_verification':
|
|
90
|
+
if (!params.email_id || !params.verification_token) {
|
|
91
|
+
return { missing: 'email_id_or_verification_token' };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
default:
|
|
95
|
+
return { missing: 'email_type' };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (!params.email_type) {
|
|
99
|
+
return { missing: 'email_type' };
|
|
100
|
+
}
|
|
101
|
+
if (!params.email) {
|
|
102
|
+
return { missing: 'email' };
|
|
103
|
+
}
|
|
104
|
+
const typeValidation = validateForType();
|
|
105
|
+
if (typeValidation) {
|
|
106
|
+
return typeValidation;
|
|
107
|
+
}
|
|
108
|
+
const databaseInfo = await meta.request(GetDatabaseInfo, {
|
|
109
|
+
databaseId
|
|
110
|
+
});
|
|
111
|
+
const site = databaseInfo?.database?.sites?.nodes?.[0];
|
|
112
|
+
if (!site) {
|
|
113
|
+
throw new Error('Site not found for database');
|
|
114
|
+
}
|
|
115
|
+
const legalTermsModule = site.siteModules?.nodes?.[0];
|
|
116
|
+
const domainNode = site.domains?.nodes?.[0];
|
|
117
|
+
const theme = site.siteThemes?.nodes?.[0]?.theme;
|
|
118
|
+
if (!legalTermsModule || !domainNode || !theme) {
|
|
119
|
+
throw new Error('Missing site configuration for email');
|
|
120
|
+
}
|
|
121
|
+
const subdomain = domainNode.subdomain;
|
|
122
|
+
const domain = domainNode.domain;
|
|
123
|
+
const supportEmail = legalTermsModule.data.emails.support;
|
|
124
|
+
const logo = site.logo?.url;
|
|
125
|
+
const company = legalTermsModule.data.company;
|
|
126
|
+
const website = company.website;
|
|
127
|
+
const nick = company.nick;
|
|
128
|
+
const name = company.name;
|
|
129
|
+
const primary = theme.primary;
|
|
130
|
+
const hostname = subdomain ? [subdomain, domain].join('.') : domain;
|
|
131
|
+
// Treat localhost-style hosts specially so we can generate
|
|
132
|
+
// http://localhost[:port]/... links for local dev without
|
|
133
|
+
// breaking production URLs.
|
|
134
|
+
const isLocalHost = hostname.startsWith('localhost') ||
|
|
135
|
+
hostname.startsWith('0.0.0.0') ||
|
|
136
|
+
hostname.endsWith('.localhost');
|
|
137
|
+
// Optional: LOCAL_APP_PORT lets you attach a port for local dashboards
|
|
138
|
+
// e.g. LOCAL_APP_PORT=3000 -> http://localhost:3000
|
|
139
|
+
// It is ignored for non-local hostnames. Only allow on DRY RUNs
|
|
140
|
+
const localPort = isLocalHost && isDryRun && process.env.LOCAL_APP_PORT
|
|
141
|
+
? `:${process.env.LOCAL_APP_PORT}`
|
|
142
|
+
: '';
|
|
143
|
+
// Use http only for local dry-run to avoid browser TLS warnings
|
|
144
|
+
// in dev; production stays https.
|
|
145
|
+
const protocol = isLocalHost && isDryRun ? 'http' : 'https';
|
|
146
|
+
const url = new URL(`${protocol}://${hostname}${localPort}`);
|
|
147
|
+
let subject;
|
|
148
|
+
let subMessage;
|
|
149
|
+
let linkText;
|
|
150
|
+
let inviterName;
|
|
151
|
+
switch (params.email_type) {
|
|
152
|
+
case 'invite_email': {
|
|
153
|
+
if (!params.invite_token || !params.sender_id) {
|
|
154
|
+
return { missing: 'invite_token_or_sender_id' };
|
|
155
|
+
}
|
|
156
|
+
url.pathname = 'register';
|
|
157
|
+
url.searchParams.append('invite_token', params.invite_token);
|
|
158
|
+
url.searchParams.append('email', params.email);
|
|
159
|
+
const scope = Number(params.invite_type) === 2 ? 'org' : 'app';
|
|
160
|
+
url.searchParams.append('type', scope);
|
|
161
|
+
const inviter = await client.request(GetUser, {
|
|
162
|
+
userId: params.sender_id
|
|
163
|
+
});
|
|
164
|
+
inviterName = inviter?.user?.displayName;
|
|
165
|
+
if (inviterName) {
|
|
166
|
+
subject = `${inviterName} invited you to ${nick}!`;
|
|
167
|
+
subMessage = `You've been invited to ${nick}`;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
subject = `Welcome to ${nick}!`;
|
|
171
|
+
subMessage = `You've been invited to ${nick}`;
|
|
172
|
+
}
|
|
173
|
+
linkText = 'Join Us';
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'forgot_password': {
|
|
177
|
+
if (!params.user_id || !params.reset_token) {
|
|
178
|
+
return { missing: 'user_id_or_reset_token' };
|
|
179
|
+
}
|
|
180
|
+
url.pathname = 'reset-password';
|
|
181
|
+
url.searchParams.append('role_id', params.user_id);
|
|
182
|
+
url.searchParams.append('reset_token', params.reset_token);
|
|
183
|
+
subject = `${nick} Password Reset Request`;
|
|
184
|
+
subMessage = 'Click below to reset your password';
|
|
185
|
+
linkText = 'Reset Password';
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case 'email_verification': {
|
|
189
|
+
if (!params.email_id || !params.verification_token) {
|
|
190
|
+
return { missing: 'email_id_or_verification_token' };
|
|
191
|
+
}
|
|
192
|
+
url.pathname = 'verify-email';
|
|
193
|
+
url.searchParams.append('email_id', params.email_id);
|
|
194
|
+
url.searchParams.append('verification_token', params.verification_token);
|
|
195
|
+
subject = `${nick} Email Verification`;
|
|
196
|
+
subMessage = 'Please confirm your email address';
|
|
197
|
+
linkText = 'Confirm Email';
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const link = url.href;
|
|
204
|
+
const html = (0, mjml_1.generate)({
|
|
205
|
+
title: subject,
|
|
206
|
+
link,
|
|
207
|
+
linkText,
|
|
208
|
+
message: subject,
|
|
209
|
+
subMessage,
|
|
210
|
+
bodyBgColor: 'white',
|
|
211
|
+
headerBgColor: 'white',
|
|
212
|
+
messageBgColor: 'white',
|
|
213
|
+
messageTextColor: '#414141',
|
|
214
|
+
messageButtonBgColor: primary,
|
|
215
|
+
messageButtonTextColor: 'white',
|
|
216
|
+
companyName: name,
|
|
217
|
+
supportEmail,
|
|
218
|
+
website,
|
|
219
|
+
logo,
|
|
220
|
+
headerImageProps: {
|
|
221
|
+
alt: 'logo',
|
|
222
|
+
align: 'center',
|
|
223
|
+
border: 'none',
|
|
224
|
+
width: '162px',
|
|
225
|
+
paddingLeft: '0px',
|
|
226
|
+
paddingRight: '0px',
|
|
227
|
+
paddingBottom: '0px',
|
|
228
|
+
paddingTop: '0'
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
if (isDryRun) {
|
|
232
|
+
logger.info('DRY RUN email (skipping send)', {
|
|
233
|
+
email_type: params.email_type,
|
|
234
|
+
email: params.email,
|
|
235
|
+
subject,
|
|
236
|
+
link
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const sendEmail = useSmtp ? simple_smtp_server_1.send : postmaster_1.send;
|
|
241
|
+
await sendEmail({
|
|
242
|
+
to: params.email,
|
|
243
|
+
subject,
|
|
244
|
+
html
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
complete: true,
|
|
249
|
+
...(isDryRun ? { dryRun: true } : null)
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
exports.sendEmailLink = sendEmailLink;
|
|
253
|
+
// HTTP/Knative entrypoint (used by @constructive-io/knative-job-fn wrapper)
|
|
254
|
+
app.post('/', async (req, res, next) => {
|
|
255
|
+
try {
|
|
256
|
+
const params = (req.body || {});
|
|
257
|
+
const databaseId = req.get('X-Database-Id') || req.get('x-database-id') || process.env.DEFAULT_DATABASE_ID;
|
|
258
|
+
if (!databaseId) {
|
|
259
|
+
return res.status(400).json({ error: 'Missing X-Database-Id header or DEFAULT_DATABASE_ID' });
|
|
260
|
+
}
|
|
261
|
+
const graphqlUrl = getRequiredEnv('GRAPHQL_URL');
|
|
262
|
+
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');
|
|
265
|
+
const result = await (0, exports.sendEmailLink)(params, {
|
|
266
|
+
client,
|
|
267
|
+
meta,
|
|
268
|
+
databaseId
|
|
269
|
+
});
|
|
270
|
+
res.status(200).json(result);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
next(err);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
exports.default = app;
|
|
277
|
+
// When executed directly (e.g. via `node dist/index.js`), start an HTTP server.
|
|
278
|
+
if (require.main === module) {
|
|
279
|
+
const port = Number(process.env.PORT ?? 8080);
|
|
280
|
+
// @constructive-io/knative-job-fn exposes a .listen method that delegates to the Express app
|
|
281
|
+
app.listen(port, () => {
|
|
282
|
+
logger.info(`listening on port ${port}`);
|
|
283
|
+
});
|
|
284
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@constructive-io/send-email-link-fn",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Knative function to send email links (invite, password reset, email verification) using Constructive jobs",
|
|
5
|
+
"author": "Constructive <developers@constructive.io>",
|
|
6
|
+
"homepage": "https://github.com/constructive-io/constructive",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"module": "esm/index.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"directory": "dist"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/constructive-io/constructive"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/constructive-io/constructive/issues"
|
|
21
|
+
},
|
|
22
|
+
"directories": {
|
|
23
|
+
"lib": "src",
|
|
24
|
+
"test": "__tests__"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "makage clean",
|
|
28
|
+
"prepack": "npm run build",
|
|
29
|
+
"build": "makage build",
|
|
30
|
+
"build:dev": "makage build --dev"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"makage": "^0.1.10"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@constructive-io/knative-job-fn": "^0.3.1",
|
|
37
|
+
"@launchql/mjml": "0.1.1",
|
|
38
|
+
"@launchql/postmaster": "0.1.4",
|
|
39
|
+
"@launchql/styled-email": "0.1.0",
|
|
40
|
+
"@pgpmjs/env": "^2.10.0",
|
|
41
|
+
"@pgpmjs/logger": "^1.4.0",
|
|
42
|
+
"graphql-request": "^7.1.2",
|
|
43
|
+
"graphql-tag": "^2.12.6",
|
|
44
|
+
"simple-smtp-server": "^0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"gitHead": "3ffd5718e86ea5fa9ca6e0930aeb510cf392f343"
|
|
47
|
+
}
|