@seeka-labs/cli-apps 1.1.39 → 2.0.7-rc.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/dist/index.js +31 -36
- package/dist/index.js.map +4 -4
- package/dist/init-template/.gitlab-ci.yml +47 -0
- package/dist/init-template/.yarnrc.yml +8 -0
- package/dist/init-template/README.md +7 -0
- package/dist/{init-templates → init-template/app}/browser/jest.config.js +11 -11
- package/dist/{init-templates → init-template/app}/browser/package.json +16 -12
- package/dist/{init-templates → init-template/app}/browser/scripts/esbuild/build-browser-plugin.mjs +110 -110
- package/dist/{init-templates → init-template/app}/browser/scripts/esbuild/plugins/importAsGlobals.mjs +38 -38
- package/dist/{init-templates → init-template/app}/browser/src/browser.ts +12 -12
- package/dist/{init-templates → init-template/app}/browser/src/plugin/index.test.ts +6 -6
- package/dist/{init-templates → init-template/app}/browser/src/plugin/index.ts +47 -49
- package/dist/{init-templates → init-template/app}/browser/tsconfig.json +34 -34
- package/dist/{init-templates/netlify-function → init-template/app/server-azure-function}/.eslintrc.cjs +2 -10
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/.funcignore +12 -7
- package/dist/init-template/app/server-azure-function/.nvmrc +1 -0
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/README.md +104 -107
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/host.json +1 -10
- package/dist/init-template/app/server-azure-function/package.json +52 -0
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/functions/seekaAppWebhook.ts +235 -236
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/lib/browser/index.ts +54 -54
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/lib/jobs/index.ts +1 -1
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/lib/logging/index.ts +92 -92
- package/dist/{init-templates/azure-function/src/lib/browser → init-template/app/server-azure-function/src/lib}/models/index.ts +6 -6
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/lib/state/redis/index.ts +96 -65
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/lib/state/seeka/installations.ts +64 -66
- package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/tsconfig.json +1 -1
- package/dist/init-template/package.json +28 -0
- package/dist/init-template/tsconfig.json +25 -0
- package/package.json +5 -4
- package/dist/init-templates/aws-lambda/.env.example +0 -15
- package/dist/init-templates/aws-lambda/.eslintrc.cjs +0 -19
- package/dist/init-templates/aws-lambda/.example.gitignore +0 -49
- package/dist/init-templates/aws-lambda/.gitlab-ci.yml +0 -39
- package/dist/init-templates/aws-lambda/.nvmrc +0 -1
- package/dist/init-templates/aws-lambda/.vscode/extensions.json +0 -5
- package/dist/init-templates/aws-lambda/.vscode/launch.json +0 -20
- package/dist/init-templates/aws-lambda/.vscode/settings.json +0 -3
- package/dist/init-templates/aws-lambda/.vscode/tasks.json +0 -12
- package/dist/init-templates/aws-lambda/README.md +0 -77
- package/dist/init-templates/aws-lambda/jest.config.js +0 -5
- package/dist/init-templates/aws-lambda/package.json +0 -53
- package/dist/init-templates/aws-lambda/scripts/ngrok.js +0 -28
- package/dist/init-templates/aws-lambda/src/index.test.ts +0 -7
- package/dist/init-templates/aws-lambda/src/index.ts +0 -33
- package/dist/init-templates/aws-lambda/src/lib/logging/index.ts +0 -88
- package/dist/init-templates/aws-lambda/src/lib/services/index.ts +0 -41
- package/dist/init-templates/aws-lambda/src/lib/state/redis/index.ts +0 -65
- package/dist/init-templates/aws-lambda/src/lib/state/seeka/installations.ts +0 -67
- package/dist/init-templates/aws-lambda/src/routes/seekaAppWebhook.ts +0 -194
- package/dist/init-templates/aws-lambda/tsconfig.json +0 -31
- package/dist/init-templates/aws-lambda/yarn.lock +0 -4392
- package/dist/init-templates/azure-function/.eslintrc.cjs +0 -19
- package/dist/init-templates/azure-function/.example.gitignore +0 -48
- package/dist/init-templates/azure-function/.gitlab-ci.yml +0 -48
- package/dist/init-templates/azure-function/.nvmrc +0 -1
- package/dist/init-templates/azure-function/.vscode/extensions.json +0 -7
- package/dist/init-templates/azure-function/.vscode/launch.json +0 -13
- package/dist/init-templates/azure-function/.vscode/settings.json +0 -9
- package/dist/init-templates/azure-function/.vscode/tasks.json +0 -39
- package/dist/init-templates/azure-function/jest.config.js +0 -5
- package/dist/init-templates/azure-function/package.json +0 -47
- package/dist/init-templates/azure-function/scripts/dev-queue-setup.js +0 -30
- package/dist/init-templates/azure-function/src/index.test.ts +0 -7
- package/dist/init-templates/azure-function/yarn.lock +0 -3772
- package/dist/init-templates/browser/.editorconfig +0 -14
- package/dist/init-templates/browser/.eslintrc.cjs +0 -1
- package/dist/init-templates/browser/.yarnrc +0 -1
- package/dist/init-templates/browser/yarn.lock +0 -3045
- package/dist/init-templates/netlify-function/.env.example +0 -18
- package/dist/init-templates/netlify-function/.example.gitignore +0 -36
- package/dist/init-templates/netlify-function/.nvmrc +0 -1
- package/dist/init-templates/netlify-function/.vscode/launch.json +0 -45
- package/dist/init-templates/netlify-function/README.md +0 -62
- package/dist/init-templates/netlify-function/jest.config.js +0 -5
- package/dist/init-templates/netlify-function/netlify.toml +0 -7
- package/dist/init-templates/netlify-function/package.json +0 -39
- package/dist/init-templates/netlify-function/src/api/example-job-background/index.ts +0 -52
- package/dist/init-templates/netlify-function/src/api/polling-example-job-scheduled/index.ts +0 -46
- package/dist/init-templates/netlify-function/src/api/seeka-app-webhook/index.ts +0 -217
- package/dist/init-templates/netlify-function/src/index.test.ts +0 -7
- package/dist/init-templates/netlify-function/src/lib/jobs/index.ts +0 -68
- package/dist/init-templates/netlify-function/src/lib/logging/index.ts +0 -91
- package/dist/init-templates/netlify-function/src/lib/services/index.ts +0 -41
- package/dist/init-templates/netlify-function/src/lib/state/redis/index.ts +0 -65
- package/dist/init-templates/netlify-function/src/lib/state/seeka/installations.ts +0 -67
- package/dist/init-templates/netlify-function/tsconfig.json +0 -25
- package/dist/init-templates/netlify-function/yarn.lock +0 -9337
- /package/dist/{init-templates → init-template/app}/browser/README.md +0 -0
- /package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/local.settings.example.json +0 -0
- /package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/scripts/ngrok.js +0 -0
- /package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/functions/healthCheck.ts +0 -0
- /package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/functions/pollingExample.ts +0 -0
- /package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/functions/queueExample.ts +0 -0
- /package/dist/{init-templates/azure-function → init-template/app/server-azure-function}/src/lib/services/index.ts +0 -0
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
SEEKA_APP_ID=
|
|
2
|
-
SEEKA_APP_SECRET=
|
|
3
|
-
SEEKA_DEBUG_ENABLED=true
|
|
4
|
-
SEEKA_INGEST_URL=
|
|
5
|
-
SEEKA_ISSUER_URL=
|
|
6
|
-
NODE_TLS_REJECT_UNAUTHORIZED=1
|
|
7
|
-
NETLIFY_ENABLED=false
|
|
8
|
-
NETLIFY_SITE_NAME=
|
|
9
|
-
REDIS_CONNECTION_USER=default
|
|
10
|
-
REDIS_CONNECTION_PASSWORD=
|
|
11
|
-
REDIS_CONNECTION_TLS=true
|
|
12
|
-
REDIS_CONNECTION_HOST=
|
|
13
|
-
REDIS_CONNECTION_PORT=
|
|
14
|
-
LOGGING_SEQ_SERVERURL=
|
|
15
|
-
LOGGING_SEQ_APIKEY=
|
|
16
|
-
LOGGING_LEVEL=silly
|
|
17
|
-
ENCRYPTION_SECRET=
|
|
18
|
-
INBOUND_HTTP_CALL_API_KEY=
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Local Netlify folder
|
|
2
|
-
.netlify
|
|
3
|
-
.env
|
|
4
|
-
|
|
5
|
-
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
6
|
-
|
|
7
|
-
# dependencies
|
|
8
|
-
/node_modules
|
|
9
|
-
/.pnp
|
|
10
|
-
.pnp.js
|
|
11
|
-
.yarn/install-state.gz
|
|
12
|
-
|
|
13
|
-
# testing
|
|
14
|
-
/coverage
|
|
15
|
-
|
|
16
|
-
# production
|
|
17
|
-
/build
|
|
18
|
-
/dist
|
|
19
|
-
|
|
20
|
-
# misc
|
|
21
|
-
.DS_Store
|
|
22
|
-
*.pem
|
|
23
|
-
|
|
24
|
-
# debug
|
|
25
|
-
npm-debug.log*
|
|
26
|
-
yarn-debug.log*
|
|
27
|
-
yarn-error.log*
|
|
28
|
-
|
|
29
|
-
# local env files
|
|
30
|
-
.env*.local
|
|
31
|
-
|
|
32
|
-
# vercel
|
|
33
|
-
.vercel
|
|
34
|
-
|
|
35
|
-
# typescript
|
|
36
|
-
*.tsbuildinfo
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v20
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "0.2.0",
|
|
3
|
-
"configurations": [
|
|
4
|
-
{
|
|
5
|
-
"name": "netlify dev",
|
|
6
|
-
"type": "node",
|
|
7
|
-
"request": "launch",
|
|
8
|
-
"skipFiles": [
|
|
9
|
-
"<node_internals>/**"
|
|
10
|
-
],
|
|
11
|
-
"outFiles": [
|
|
12
|
-
"${workspaceFolder}/.netlify/functions-serve/**/*.js"
|
|
13
|
-
],
|
|
14
|
-
"program": "${workspaceFolder}/node_modules/.bin/netlify",
|
|
15
|
-
"args": [
|
|
16
|
-
"dev"
|
|
17
|
-
],
|
|
18
|
-
"console": "integratedTerminal",
|
|
19
|
-
"env": {
|
|
20
|
-
"BROWSER": "none"
|
|
21
|
-
},
|
|
22
|
-
"serverReadyAction": {
|
|
23
|
-
"pattern": "Server now ready on (https?://[\\w:.-]+)",
|
|
24
|
-
"uriFormat": "%s",
|
|
25
|
-
"action": "debugWithChrome"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"name": "netlify functions:serve",
|
|
30
|
-
"type": "node",
|
|
31
|
-
"request": "launch",
|
|
32
|
-
"skipFiles": [
|
|
33
|
-
"<node_internals>/**"
|
|
34
|
-
],
|
|
35
|
-
"outFiles": [
|
|
36
|
-
"${workspaceFolder}/.netlify/functions-serve/**/*.js"
|
|
37
|
-
],
|
|
38
|
-
"program": "${workspaceFolder}/node_modules/.bin/netlify",
|
|
39
|
-
"args": [
|
|
40
|
-
"functions:serve"
|
|
41
|
-
],
|
|
42
|
-
"console": "integratedTerminal"
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# Seeka app - Netlify functions
|
|
2
|
-
|
|
3
|
-
## Development
|
|
4
|
-
|
|
5
|
-
- `yarn install`
|
|
6
|
-
- `yarn dev`
|
|
7
|
-
|
|
8
|
-
### Dependencies
|
|
9
|
-
- Python >=3.6.0 - https://www.python.org/downloads/ or `choco install python`
|
|
10
|
-
|
|
11
|
-
### Invoking functions
|
|
12
|
-
Run `yarn dev:netlify:invoke:polling-example-job-scheduled` to trigger scheduled job which triggers multiple background jobs, 1 for each installation of the app
|
|
13
|
-
|
|
14
|
-
### Debugging / local development before deployment
|
|
15
|
-
- This template uses [Netlify's live url debugging](https://answers.netlify.com/t/netlify-dev-with-support-for-custom-live-urls/101261)
|
|
16
|
-
- Supports VSCode debugging - See `.vscode/launch.json`. This is only supported on Linux. If using Windows then use WSL with an Ubuntu distro for support of attaching the VS code debugger
|
|
17
|
-
|
|
18
|
-
## Logging
|
|
19
|
-
Centralised logging handled by [Winston](https://www.npmjs.com/package/winston). Winston is installed in this template and is also used in the Seeka SDK NPM packages.
|
|
20
|
-
|
|
21
|
-
[Seq](https://datalust.co/seq) Winston transport is installed in this template (optional).
|
|
22
|
-
|
|
23
|
-
To configure Seq options, see `_SEQ_` environment variables.
|
|
24
|
-
|
|
25
|
-
To install Seq on your development machine
|
|
26
|
-
1. Run `docker run --name seq -d --restart=always -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:2024.2`
|
|
27
|
-
2. Update your `LOGGING_SEQ_SERVERURL` environment variable to `http://localhost:5341` or `http://[YOUR MACHINE NAME].local:5341` if using WSL.
|
|
28
|
-
|
|
29
|
-
If creating an API key for Seq, only permission that is required is `Ingest`.
|
|
30
|
-
|
|
31
|
-
## State management
|
|
32
|
-
Installations of your app and other state required for your app to function is stored in Redis. Another state provider can be swapped out for Redis, see `src/lib/state/seeka/installations.ts` for the file that manages the state of the installations.
|
|
33
|
-
|
|
34
|
-
### Upstash (optional)
|
|
35
|
-
If you dont want to use Upstash for Redis then the connection strings in `.env` can be swapped with your Redis instance.
|
|
36
|
-
|
|
37
|
-
> If using Upstash then create a database before following the below guide to deploying your Netlify function.
|
|
38
|
-
|
|
39
|
-
> When choosing region - use US East (Ohio) - us-east-* as this is the same AWS region that the free Netlify functions run from. If not on the Netlify free plan, select the closest Upstash region that your Netlify function is hosted in to reduce latency between the function and the Redis database.
|
|
40
|
-
|
|
41
|
-
> Considerations - max connection limit (1000) of Upstash connections in relation to concurrent execution count of the Netlify functions
|
|
42
|
-
|
|
43
|
-
## Deployment
|
|
44
|
-
This project comes ready to deploy for free to Netlify with a database and queues backed by Redis hosted by Upstash.
|
|
45
|
-
|
|
46
|
-
At the point of writing, Netlify and Upstash can be used to deploy your app for free.
|
|
47
|
-
- https://upstash.com/pricing
|
|
48
|
-
- https://www.netlify.com/pricing/
|
|
49
|
-
|
|
50
|
-
### Deployment to Netlify
|
|
51
|
-
#### From your local machine
|
|
52
|
-
- `yarn deploy`
|
|
53
|
-
|
|
54
|
-
#### Using Netlify continuous delivery
|
|
55
|
-
See the [Netlify docs](https://docs.netlify.com/functions/deploy/?fn-language=ts)
|
|
56
|
-
|
|
57
|
-
## Considerations when using this template
|
|
58
|
-
Each call to the netlify function will create a new connection to the database/Redis as calls to the Netlify function in quick succession will not share a single "warm" function host which is supported by Azure Functions and AWS Lambda.
|
|
59
|
-
|
|
60
|
-
## References
|
|
61
|
-
- https://docs.netlify.com/cli/get-started/
|
|
62
|
-
- https://docs.netlify.com/functions/get-started/?fn-language=ts
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "seeka-app-example-name",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"description": "Seeka example app for hosting on Netlify functions",
|
|
5
|
-
"author": "Seeka <platform@seeka.co>",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"private": true,
|
|
9
|
-
"engines": {
|
|
10
|
-
"node": ">=20"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"dev:invoke:example-scheduled-job": "netlify functions:invoke --name polling-example-job-scheduled --port 8888 --identity",
|
|
14
|
-
"lint": "eslint",
|
|
15
|
-
"test": "yarn jest",
|
|
16
|
-
"dev": "netlify dev --live=localdev --port 8888 --no-open",
|
|
17
|
-
"build": "netlify functions:build --functions=dist --src=src/api",
|
|
18
|
-
"deploy": "netlify deploy --build --skip-functions-cache --prod"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@datalust/winston-seq": "^2.0.0",
|
|
22
|
-
"@netlify/functions": "^3.0.0",
|
|
23
|
-
"@seeka-labs/sdk-apps-server": "^1.1.26",
|
|
24
|
-
"lodash-es": "^4.17.21",
|
|
25
|
-
"redis": "^4.7.0"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@jest/globals": "^29.7.0",
|
|
29
|
-
"@types/lodash-es": "^4.17.12",
|
|
30
|
-
"@types/node": "^20",
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
|
32
|
-
"@typescript-eslint/parser": "^8.22.0",
|
|
33
|
-
"eslint": "^9",
|
|
34
|
-
"jest": "^29.7.0",
|
|
35
|
-
"ts-jest": "^29.2.5",
|
|
36
|
-
"netlify-cli": "^18.0.3",
|
|
37
|
-
"typescript": "^5.7.3"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import winston, { Logger } from 'winston';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
apiKeyHeaderName, BackgroundJobRequestContext, jobNames, signatureHeaderName, validateApiKey,
|
|
5
|
-
validateSignature
|
|
6
|
-
} from '@/lib/jobs';
|
|
7
|
-
import { backgroundJobLogger } from '@/lib/logging';
|
|
8
|
-
import { startServices, stopServices } from '@/lib/services';
|
|
9
|
-
|
|
10
|
-
import type { Config, Context } from "@netlify/functions"
|
|
11
|
-
export default async (req: Request, context: Context) => {
|
|
12
|
-
if (req.method != "POST") {
|
|
13
|
-
return new Response("Method not allowed", { status: 405 });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const bodyStr = await req.text();
|
|
17
|
-
const body = JSON.parse(bodyStr) as BackgroundJobRequestContext;
|
|
18
|
-
const logger = backgroundJobLogger(jobNames.exampleBackgroundJob, body, context);
|
|
19
|
-
logger.profile(`job.${jobNames.exampleBackgroundJob}`)
|
|
20
|
-
|
|
21
|
-
await startServices(logger);
|
|
22
|
-
|
|
23
|
-
if (process.env.SEEKA_DEBUG_ENABLED === 'true') {
|
|
24
|
-
logger.debug('Received request to trigger background job', { body, headers: req.headers });
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
logger.verbose('Received request to trigger background job', { body });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// Verify request
|
|
32
|
-
validateApiKey(req.headers.get(apiKeyHeaderName) as string, logger);
|
|
33
|
-
validateSignature(req.headers.get(signatureHeaderName) as string, bodyStr, logger);
|
|
34
|
-
|
|
35
|
-
// Execute
|
|
36
|
-
await doWork(body, logger);
|
|
37
|
-
|
|
38
|
-
// Close db connections
|
|
39
|
-
await stopServices(logger);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
logger.error('Error executing background job', { ex: winston.exceptions.getAllInfo(err) })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
logger.profile(`job.${jobNames.exampleBackgroundJob}`)
|
|
46
|
-
|
|
47
|
-
return new Response(undefined, { status: 202 })
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const doWork = async (context: BackgroundJobRequestContext, logger: Logger) => {
|
|
51
|
-
logger.debug('Ran background job')
|
|
52
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import winston from 'winston';
|
|
2
|
-
|
|
3
|
-
import { jobNames, triggerBackgroundJob } from '@/lib/jobs';
|
|
4
|
-
import { backgroundJobLogger } from '@/lib/logging';
|
|
5
|
-
import { startServices, stopServices } from '@/lib/services';
|
|
6
|
-
import { listInstallations } from '@/lib/state/seeka/installations';
|
|
7
|
-
|
|
8
|
-
import type { Config, Context } from "@netlify/functions"
|
|
9
|
-
export const config: Config = {
|
|
10
|
-
schedule: "@hourly"
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default async (req: Request, context: Context) => {
|
|
14
|
-
const { next_run } = (await req.json()) as { next_run: string }
|
|
15
|
-
|
|
16
|
-
const logger = backgroundJobLogger(jobNames.examplePollingScheduledJob, undefined, context);
|
|
17
|
-
|
|
18
|
-
logger.profile(`job.${jobNames.examplePollingScheduledJob}`)
|
|
19
|
-
if (process.env.SEEKA_DEBUG_ENABLED === 'true') {
|
|
20
|
-
logger.debug('Received request to trigger scheduled job', { headers: req.headers, next_run });
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
logger.verbose('Received request to trigger scheduled job', { next_run });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
await startServices(logger);
|
|
27
|
-
|
|
28
|
-
const allInstallations = await listInstallations(logger);
|
|
29
|
-
logger.verbose(`Triggering background job for ${allInstallations.length} installations`, { allInstallations })
|
|
30
|
-
|
|
31
|
-
// Close db connections
|
|
32
|
-
await stopServices(logger);
|
|
33
|
-
|
|
34
|
-
const promises = allInstallations.map(installation => triggerBackgroundJob(jobNames.exampleBackgroundJob, installation, logger))
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
await Promise.all(promises);
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
logger.error('Error triggering background jobs', { ex: winston.exceptions.getAllInfo(err) })
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
logger.profile(`job.${jobNames.examplePollingScheduledJob}`)
|
|
44
|
-
|
|
45
|
-
return new Response(undefined, { status: 202 })
|
|
46
|
-
}
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import type { Logger } from 'winston';
|
|
3
|
-
|
|
4
|
-
import winston from 'winston';
|
|
5
|
-
|
|
6
|
-
import { jobNames, triggerBackgroundJob } from '@/lib/jobs';
|
|
7
|
-
import { webhookLogger } from '@/lib/logging';
|
|
8
|
-
import { startServices, stopServices } from '@/lib/services';
|
|
9
|
-
import {
|
|
10
|
-
createOrUpdateInstallation, deleteInstallation, SampleAppInstallSettings, SeekaAppInstallState, tryGetInstallation
|
|
11
|
-
} from '@/lib/state/seeka/installations';
|
|
12
|
-
import {
|
|
13
|
-
PersonIdentifiers, SeekaActivityAcceptedWebhookPayload, SeekaAppInstalledWebhookPayload,
|
|
14
|
-
SeekaAppInstallSettingsUpdatedWebhookPayload, SeekaAppUninstalledWebhookPayload,
|
|
15
|
-
SeekaIdentityChangedWebhookPayload, SeekaWebhookCallType, SeekaWebhookPayload,
|
|
16
|
-
throwOnInvalidWebhookSignature
|
|
17
|
-
} from '@seeka-labs/sdk-apps-server';
|
|
18
|
-
|
|
19
|
-
import type { Config, Context } from "@netlify/functions"
|
|
20
|
-
export const config: Config = {
|
|
21
|
-
path: "/api/webhook/seeka/app",
|
|
22
|
-
method: "POST"
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export default async (req: Request, context: Context) => {
|
|
26
|
-
const bodyStr = (await req.text()) as string;
|
|
27
|
-
const body = JSON.parse(bodyStr) as SeekaWebhookPayload;
|
|
28
|
-
|
|
29
|
-
const logger = webhookLogger(body, context);
|
|
30
|
-
logger.profile('http.seeka.webhook.app')
|
|
31
|
-
logger.verbose('Received webhook from Seeka', { body });
|
|
32
|
-
|
|
33
|
-
// Handle probe
|
|
34
|
-
if (body.type === SeekaWebhookCallType.Probe) {
|
|
35
|
-
return new Response(undefined, { status: 204 })
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Validate webhook
|
|
39
|
-
try {
|
|
40
|
-
throwOnInvalidWebhookSignature(process.env.SEEKA_APP_SECRET as string, req.headers, bodyStr);
|
|
41
|
-
logger.debug('Webhook signature validated', { body });
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
logger.warn('Webhook signature invalid', { body });
|
|
45
|
-
return new Response(JSON.stringify({ error: "Webhook call invalid" }), { status: 401 })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (body.isTest) {
|
|
49
|
-
// This is a test webhook call
|
|
50
|
-
return new Response(undefined, { status: 204 })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
await startServices(logger);
|
|
54
|
-
|
|
55
|
-
// Check if the webhook is for an app we have installed
|
|
56
|
-
let installation: SeekaAppInstallState | null = null;
|
|
57
|
-
if (body.type != SeekaWebhookCallType.AppInstalled) {
|
|
58
|
-
installation = await tryGetInstallation((body as SeekaAppInstalledWebhookPayload).context?.applicationInstallId as string, false, logger);
|
|
59
|
-
if (installation == null) {
|
|
60
|
-
logger.warn('Webhook call cannot be processed as the installation ID is not known by this app', { body });
|
|
61
|
-
|
|
62
|
-
// Close db connections
|
|
63
|
-
await stopServices(logger);
|
|
64
|
-
|
|
65
|
-
return new Response(JSON.stringify({ error: "App not installed" }), { status: 422 })
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Do something
|
|
70
|
-
let errorMessage: string | null = null;
|
|
71
|
-
try {
|
|
72
|
-
switch (body.type) {
|
|
73
|
-
case SeekaWebhookCallType.AppInstalled:
|
|
74
|
-
{
|
|
75
|
-
errorMessage = await onInstallation(body as SeekaAppInstalledWebhookPayload, logger);
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
case SeekaWebhookCallType.AppInstallSettingsUpdated:
|
|
79
|
-
{
|
|
80
|
-
errorMessage = await onInstallationSettingsUpdate(body as SeekaAppInstallSettingsUpdatedWebhookPayload, logger);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
case SeekaWebhookCallType.AppUninstalled:
|
|
84
|
-
{
|
|
85
|
-
if (!body.isTest) {
|
|
86
|
-
const payload = body as SeekaAppUninstalledWebhookPayload;
|
|
87
|
-
await deleteInstallation(payload.context?.applicationInstallId as string, logger) // TODO: remove cast
|
|
88
|
-
}
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
case SeekaWebhookCallType.ActivityAccepted:
|
|
92
|
-
{
|
|
93
|
-
const payload = body as SeekaActivityAcceptedWebhookPayload;
|
|
94
|
-
await handleSeekaActivity(payload, logger);
|
|
95
|
-
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case SeekaWebhookCallType.IdentityChanged:
|
|
99
|
-
{
|
|
100
|
-
const payload = body as SeekaIdentityChangedWebhookPayload;
|
|
101
|
-
logger.debug('Identity changed', { payload });
|
|
102
|
-
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
logger.error('Failed to handle webhook', { ex: winston.exceptions.getAllInfo(err) });
|
|
109
|
-
return new Response(JSON.stringify({ error: "Request failed" }), { status: 500 })
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
// Close db connections
|
|
113
|
-
await stopServices(logger);
|
|
114
|
-
logger.profile('http.seeka.webhook.app')
|
|
115
|
-
logger.verbose('Seeka webhook handled');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (errorMessage) {
|
|
119
|
-
logger.warn('Webhook call failed', { errorMessage });
|
|
120
|
-
return Response.json({ error: { message: errorMessage } }, { status: 400 });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return new Response(undefined, { status: 204 })
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const validateInstallationSettings = async (installSettings: SampleAppInstallSettings, logger: Logger): Promise<string | null> => {
|
|
127
|
-
// Returning an error message string here will block the installation request or settings update request by the user installing the app
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const onInstallation = async (payload: SeekaAppInstalledWebhookPayload, logger: Logger): Promise<string | null> => {
|
|
134
|
-
if (payload.isTest) return null;
|
|
135
|
-
|
|
136
|
-
const errorMessage = await validateInstallationSettings(payload.content?.installationSettings || {}, logger);
|
|
137
|
-
if (errorMessage) return errorMessage;
|
|
138
|
-
|
|
139
|
-
const installation = await createOrUpdateInstallation({
|
|
140
|
-
...payload.context,
|
|
141
|
-
installationState: {
|
|
142
|
-
grantedPermissions: payload.content?.grantedPermissions || []
|
|
143
|
-
},
|
|
144
|
-
applicationInstallId: payload.context?.applicationInstallId as string,
|
|
145
|
-
organisationBrandId: payload.context?.organisationBrandId as string,
|
|
146
|
-
organisationId: payload.context?.organisationId as string,
|
|
147
|
-
installedAt: new Date().toISOString(),
|
|
148
|
-
installationSettings: payload.content?.installationSettings || {}
|
|
149
|
-
}, logger)
|
|
150
|
-
|
|
151
|
-
// Trigger a sync for the installation
|
|
152
|
-
try {
|
|
153
|
-
await triggerBackgroundJob(jobNames.exampleBackgroundJob, {
|
|
154
|
-
...payload.context
|
|
155
|
-
}, logger)
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
await deleteInstallation(installation.applicationInstallId, logger)
|
|
159
|
-
return 'Failed to complete install';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const onInstallationSettingsUpdate = async (payload: SeekaAppInstallSettingsUpdatedWebhookPayload, logger: Logger): Promise<string | null> => {
|
|
166
|
-
if (payload.isTest) return null;
|
|
167
|
-
|
|
168
|
-
const errorMessage = await validateInstallationSettings(payload.content?.installationSettings || {}, logger);
|
|
169
|
-
if (errorMessage) return errorMessage;
|
|
170
|
-
|
|
171
|
-
const existingInstallation = await tryGetInstallation(payload.context?.applicationInstallId as string, true, logger) as SeekaAppInstallState;
|
|
172
|
-
|
|
173
|
-
// Update settings
|
|
174
|
-
const installation = await createOrUpdateInstallation({
|
|
175
|
-
...payload.context,
|
|
176
|
-
...existingInstallation,
|
|
177
|
-
installationState: {
|
|
178
|
-
...existingInstallation.installationState,
|
|
179
|
-
grantedPermissions: payload.content?.grantedPermissions || []
|
|
180
|
-
},
|
|
181
|
-
installationSettings: payload.content?.installationSettings || {}
|
|
182
|
-
} as any, logger) // TODO: remove any
|
|
183
|
-
|
|
184
|
-
logger.info('Settings updated')
|
|
185
|
-
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const handleSeekaActivity = async (activity: SeekaActivityAcceptedWebhookPayload, logger: Logger) => {
|
|
190
|
-
// const context = activity.context as SeekaAppWebhookContext;
|
|
191
|
-
// const helper = SeekaAppHelper.create(process.env['SEEKA_APP_SECRET'] as string, {
|
|
192
|
-
// organisationId: context.organisationId as string,
|
|
193
|
-
// applicationInstallId: context.applicationInstallId as string,
|
|
194
|
-
// applicationId: process.env['SEEKA_APP_ID'] as string,
|
|
195
|
-
// }, { name, version }, logger);
|
|
196
|
-
|
|
197
|
-
// // Append a first name to the identity
|
|
198
|
-
// await helper.api.mergeIdentity({
|
|
199
|
-
// seekaPId: activity.content?.personId,
|
|
200
|
-
// firstName: [
|
|
201
|
-
// 'firstname_' + new Date().getTime()
|
|
202
|
-
// ]
|
|
203
|
-
// }, {
|
|
204
|
-
// method: 'toremove',
|
|
205
|
-
// origin: TrackingEventSourceOriginType.Server
|
|
206
|
-
// })
|
|
207
|
-
|
|
208
|
-
// // Fire off a tracking event
|
|
209
|
-
// await helper.api.trackActivity({
|
|
210
|
-
// activityName: TrackingActivityNames.Custom,
|
|
211
|
-
// activityNameCustom: 'seeka-app-activity-accepted',
|
|
212
|
-
// activityId: 'act' + new Date().getTime(),
|
|
213
|
-
// }, activity.content?.personId as string, {
|
|
214
|
-
// method: 'toremove',
|
|
215
|
-
// origin: TrackingEventSourceOriginType.Server
|
|
216
|
-
// })
|
|
217
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
|
-
import { trimEnd } from 'lodash-es';
|
|
3
|
-
|
|
4
|
-
import type { Logger } from 'winston';
|
|
5
|
-
export interface BackgroundJobRequestContext {
|
|
6
|
-
organisationId?: string;
|
|
7
|
-
organisationBrandId?: string;
|
|
8
|
-
applicationInstallId?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const signatureHeaderName = 'x-signature-hmac';
|
|
12
|
-
export const apiKeyHeaderName = 'x-api-key';
|
|
13
|
-
|
|
14
|
-
export const jobNames = {
|
|
15
|
-
exampleBackgroundJob: 'example-job-background',
|
|
16
|
-
examplePollingScheduledJob: 'polling-example-job-scheduled',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const validateApiKey = (apiKey: string, logger: Logger) => {
|
|
20
|
-
if (!apiKey || apiKey != process.env.INBOUND_HTTP_CALL_API_KEY) {
|
|
21
|
-
logger.error('Failed to validate API key');
|
|
22
|
-
throw new Error('Invalid API key');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
logger.verbose('Validated API key')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const validateSignature = (signature: string, payload: string, logger: Logger) => {
|
|
29
|
-
const digest = createSignature(payload)
|
|
30
|
-
|
|
31
|
-
if (!signature || signature != digest) {
|
|
32
|
-
logger.error('Invalid webhook signature');
|
|
33
|
-
throw new Error('Invalid signature');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
logger.verbose('Validated signature');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const triggerBackgroundJob = async (jobName: string, context: BackgroundJobRequestContext, logger: Logger): Promise<void> => {
|
|
40
|
-
const body = {
|
|
41
|
-
...context
|
|
42
|
-
}
|
|
43
|
-
const bodyStr = JSON.stringify(body);
|
|
44
|
-
|
|
45
|
-
const response = await fetch(`${trimEnd(process.env.URL, '/')}/.netlify/functions/${jobName}`, {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
body: bodyStr,
|
|
48
|
-
headers: {
|
|
49
|
-
[apiKeyHeaderName]: process.env.INBOUND_HTTP_CALL_API_KEY as string,
|
|
50
|
-
[signatureHeaderName]: createSignature(bodyStr),
|
|
51
|
-
'Content-Type': 'application/json',
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
const { status, statusText, url, type, headers } = response;
|
|
57
|
-
logger.error("Failed to trigger background job", { status, statusText, body: await response.text(), url, type, headers })
|
|
58
|
-
throw new Error(`Failed to trigger background job: ${response.statusText}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const createSignature = (payload: string) => {
|
|
63
|
-
const hmac = crypto.createHmac('sha256', process.env.ENCRYPTION_SECRET as string);
|
|
64
|
-
hmac.update(payload);
|
|
65
|
-
const digest = hmac.digest('hex');
|
|
66
|
-
|
|
67
|
-
return digest;
|
|
68
|
-
}
|