@seeka-labs/cli-apps 1.0.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 +19 -0
- package/README.md +1 -0
- package/dist/index.js +47 -0
- package/dist/init-templates/aws-lambda/.env.example +15 -0
- package/dist/init-templates/aws-lambda/.eslintrc.cjs +10 -0
- package/dist/init-templates/aws-lambda/.example.gitignore +49 -0
- package/dist/init-templates/aws-lambda/.gitlab-ci.yml +37 -0
- package/dist/init-templates/aws-lambda/.vscode/extensions.json +5 -0
- package/dist/init-templates/aws-lambda/.vscode/launch.json +20 -0
- package/dist/init-templates/aws-lambda/.vscode/settings.json +3 -0
- package/dist/init-templates/aws-lambda/.vscode/tasks.json +12 -0
- package/dist/init-templates/aws-lambda/README.md +75 -0
- package/dist/init-templates/aws-lambda/package.json +51 -0
- package/dist/init-templates/aws-lambda/scripts/ngrok.js +28 -0
- package/dist/init-templates/aws-lambda/src/index.ts +33 -0
- package/dist/init-templates/aws-lambda/src/lib/logging/index.ts +88 -0
- package/dist/init-templates/aws-lambda/src/lib/services/index.ts +41 -0
- package/dist/init-templates/aws-lambda/src/lib/state/redis/index.ts +64 -0
- package/dist/init-templates/aws-lambda/src/lib/state/seeka/installations.ts +67 -0
- package/dist/init-templates/aws-lambda/src/routes/seekaAppWebhook.ts +170 -0
- package/dist/init-templates/aws-lambda/tsconfig.json +31 -0
- package/dist/init-templates/azure-function/.eslintrc.cjs +10 -0
- package/dist/init-templates/azure-function/.example.gitignore +48 -0
- package/dist/init-templates/azure-function/.funcignore +17 -0
- package/dist/init-templates/azure-function/.gitlab-ci.yml +33 -0
- package/dist/init-templates/azure-function/.vscode/extensions.json +7 -0
- package/dist/init-templates/azure-function/.vscode/launch.json +13 -0
- package/dist/init-templates/azure-function/.vscode/settings.json +9 -0
- package/dist/init-templates/azure-function/.vscode/tasks.json +39 -0
- package/dist/init-templates/azure-function/README.md +102 -0
- package/dist/init-templates/azure-function/host.json +20 -0
- package/dist/init-templates/azure-function/local.settings.example.json +23 -0
- package/dist/init-templates/azure-function/package.json +44 -0
- package/dist/init-templates/azure-function/scripts/ngrok.js +28 -0
- package/dist/init-templates/azure-function/src/functions/pollingExample.ts +39 -0
- package/dist/init-templates/azure-function/src/functions/queueExample.ts +33 -0
- package/dist/init-templates/azure-function/src/functions/seekaAppWebhook.ts +200 -0
- package/dist/init-templates/azure-function/src/lib/jobs/index.ts +54 -0
- package/dist/init-templates/azure-function/src/lib/logging/index.ts +93 -0
- package/dist/init-templates/azure-function/src/lib/services/index.ts +41 -0
- package/dist/init-templates/azure-function/src/lib/state/redis/index.ts +64 -0
- package/dist/init-templates/azure-function/src/lib/state/seeka/installations.ts +67 -0
- package/dist/init-templates/azure-function/tsconfig.json +22 -0
- package/dist/init-templates/netlify-function/.env.example +18 -0
- package/dist/init-templates/netlify-function/.eslintrc.cjs +7 -0
- package/dist/init-templates/netlify-function/.example.gitignore +36 -0
- package/dist/init-templates/netlify-function/.vscode/launch.json +45 -0
- package/dist/init-templates/netlify-function/README.md +60 -0
- package/dist/init-templates/netlify-function/netlify.toml +7 -0
- package/dist/init-templates/netlify-function/package.json +38 -0
- package/dist/init-templates/netlify-function/src/api/example-job-background/index.ts +52 -0
- package/dist/init-templates/netlify-function/src/api/polling-example-job-scheduled/index.ts +46 -0
- package/dist/init-templates/netlify-function/src/api/seeka-app-webhook/index.ts +185 -0
- package/dist/init-templates/netlify-function/src/lib/jobs/index.ts +68 -0
- package/dist/init-templates/netlify-function/src/lib/logging/index.ts +91 -0
- package/dist/init-templates/netlify-function/src/lib/services/index.ts +41 -0
- package/dist/init-templates/netlify-function/src/lib/state/redis/index.ts +64 -0
- package/dist/init-templates/netlify-function/src/lib/state/seeka/installations.ts +67 -0
- package/dist/init-templates/netlify-function/tsconfig.json +25 -0
- package/package.json +48 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Seeka app - Azure functions
|
|
2
|
+
|
|
3
|
+
## Development
|
|
4
|
+
|
|
5
|
+
- See [Configure your environment](https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-typescript?pivots=nodejs-model-v4#configure-your-environment) for dependencies
|
|
6
|
+
- For emulating Azure storage queues, blobs and tables: `docker run --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 --detach mcr.microsoft.com/azure-storage/azurite`
|
|
7
|
+
- Set `AzureWebJobsStorage` setting in local.settings.json to `UseDevelopmentStorage=true`
|
|
8
|
+
- If not using WSL:
|
|
9
|
+
`UseDevelopmentStorage=true`
|
|
10
|
+
- If using WSL, the following may be required to allow your app to reach Azure storage emulator:
|
|
11
|
+
|
|
12
|
+
Replace `MY-MACHINE-NAME` with your hostname. Below connection string allows running Azurite on your host machine and lets WSL connect to it
|
|
13
|
+
`DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://MY-MACHINE-NAME.local:10000/devstoreaccount1;QueueEndpoint=http://MY-MACHINE-NAME.local:10001/devstoreaccount1;TableEndpoint=http://MY-MACHINE-NAME.local:10002/devstoreaccount1;`
|
|
14
|
+
- Install Azure Storage explorer and create your queues (see `./src/lib/jobs/index.ts` for `queueNames`). Azure Storage explorer can be used to view your queue items.
|
|
15
|
+
- `yarn install`
|
|
16
|
+
- `yarn dev`
|
|
17
|
+
|
|
18
|
+
### Debugging
|
|
19
|
+
Supports VSCode debugging via the debugger and utilisation of breakpoints.
|
|
20
|
+
This is only tested on Linux but may work on Windows.
|
|
21
|
+
If using Windows then use WSL with an Ubuntu distro for support of attaching the VS code debugger
|
|
22
|
+
|
|
23
|
+
### Live urls
|
|
24
|
+
You can expose your app locally to the internet via Ngrok to test your app before deploying.
|
|
25
|
+
|
|
26
|
+
#### Setup
|
|
27
|
+
1. Sign up for a Ngrok account
|
|
28
|
+
2. Get your auth token
|
|
29
|
+
3. `yarn ngrok config add-authtoken [auth token here]` replacing `[auth token]` with the auth token retrieved from your Ngrok dashboard
|
|
30
|
+
|
|
31
|
+
#### Running
|
|
32
|
+
1. `yarn start` OR start debugging in VSCode
|
|
33
|
+
2. In separate terminal window run `yarn tunnel` and observe the log entry that starts with `Live url exposed`.
|
|
34
|
+
3. Input the URL into your Seeka app configuration as the "Webook URL" via the Seeka UI.
|
|
35
|
+
|
|
36
|
+
## Included sample functions
|
|
37
|
+
### Seeka app webhook
|
|
38
|
+
`src/functions/seekaAppWebhook.ts`
|
|
39
|
+
|
|
40
|
+
Handles inbound webhooks sent from Seeka to your app.
|
|
41
|
+
|
|
42
|
+
### Queue example
|
|
43
|
+
`src/functions/queueExample.ts`
|
|
44
|
+
|
|
45
|
+
Handles taking a message and placing it on an Azure storage queue for later processing. This concept can be used to offload long running operations based on data sent to your app from Seeka and can help to ensure Azure Function time execution limits are respected.
|
|
46
|
+
|
|
47
|
+
### Polling example
|
|
48
|
+
`src/functions/pollingExample.ts`
|
|
49
|
+
|
|
50
|
+
Handles scheduled invoking of a function. This concept can be used to "fan out" long running operations based on data sent to your app from Seeka and can help to ensure Azure Function time execution limits are respected by splitting long running operations into smaller, less time consuming parts.
|
|
51
|
+
|
|
52
|
+
## Invoking functions
|
|
53
|
+
### Development environment
|
|
54
|
+
Scheduled / timer function: `curl -i -d '{"input": null}' -H "Content-Type: application/json" -X POST http://localhost:7072/admin/functions/pollingExample`
|
|
55
|
+
|
|
56
|
+
### Azure cloud
|
|
57
|
+
Scheduled / timer function: `curl -i -d '{"input": null}' -H "Content-Type: application/json" -H "x-functions-key: <key from azure portal>" -X POST https://seeka-app-example-name.azurewebsites.net/admin/functions/pollingExample`
|
|
58
|
+
|
|
59
|
+
See [this doc](https://learn.microsoft.com/en-us/azure/azure-functions/functions-manually-run-non-http?tabs=azure-portal#get-the-master-key) on how to get the functions (master) key
|
|
60
|
+
|
|
61
|
+
## Logging
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
[Seq](https://datalust.co/seq) Winston transport is installed in this template (optional).
|
|
65
|
+
|
|
66
|
+
To configure Seq options, see `_SEQ_` environment variables.
|
|
67
|
+
|
|
68
|
+
To install Seq on your development machine
|
|
69
|
+
1. Run `docker run --name seq -d --restart=always -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:2024.2`
|
|
70
|
+
2. Update your `LOGGING_SEQ_SERVERURL` environment variable to `http://localhost:5341` or `http://[YOUR MACHINE NAME].local:5341` if using WSL.
|
|
71
|
+
|
|
72
|
+
## State management
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
### Upstash (optional)
|
|
76
|
+
If you dont want to use Upstash for Redis then the connection strings in `.env` can be swapped with your Redis instance.
|
|
77
|
+
|
|
78
|
+
> If using Upstash then create a database before following the below guide to deploying your AWS lambda function.
|
|
79
|
+
|
|
80
|
+
> When choosing region - use a region that is closest to the Azure region that your function app is hosted in to reduce latency between the function and the Redis database.
|
|
81
|
+
|
|
82
|
+
## Deployment
|
|
83
|
+
This project comes ready to deploy for free to Azure functions with database backed by Redis hosted by Upstash and queues, blobs and tables backed by Azure Storage.
|
|
84
|
+
|
|
85
|
+
1. Create Azure function app via the portal with below settings
|
|
86
|
+
- Function app name: seeka-app-example-name
|
|
87
|
+
- Code or container image: Code
|
|
88
|
+
- Runtime stack: Node.js
|
|
89
|
+
- Version: 18 LTS
|
|
90
|
+
- Region: Closest region to where your upstash "master / write" database is located
|
|
91
|
+
- Operating system: linux
|
|
92
|
+
- Hosting: Consumption
|
|
93
|
+
- Enable public access: On
|
|
94
|
+
2. Set environment variables of the Azure web app - reference local.settings.json "Values"
|
|
95
|
+
3. `yarn deploy`
|
|
96
|
+
|
|
97
|
+
### Continuous delivery
|
|
98
|
+
This template includes a GitLab CD pipeline that can be used to trigger deployments of your app when changes are pushed to your Git repository.
|
|
99
|
+
|
|
100
|
+
## References
|
|
101
|
+
- https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite
|
|
102
|
+
- https://learn.microsoft.com/en-us/azure/storage/queues/storage-quickstart-queues-nodejs?tabs=connection-string%2Croles-azure-portal%2Cenvironment-variable-windows%2Csign-in-azure-cli
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0",
|
|
3
|
+
"logging": {
|
|
4
|
+
"applicationInsights": {
|
|
5
|
+
"samplingSettings": {
|
|
6
|
+
"isEnabled": true,
|
|
7
|
+
"excludedTypes": "Request"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"functionTimeout": "00:10:00",
|
|
12
|
+
"extensionBundle": {
|
|
13
|
+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
|
14
|
+
"version": "[4.*, 5.0.0)"
|
|
15
|
+
},
|
|
16
|
+
"concurrency": {
|
|
17
|
+
"dynamicConcurrencyEnabled": true,
|
|
18
|
+
"snapshotPersistenceEnabled": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"IsEncrypted": false,
|
|
3
|
+
"Values": {
|
|
4
|
+
"FUNCTIONS_WORKER_RUNTIME": "node",
|
|
5
|
+
"FUNCTIONS_EXTENSION_VERSION": "~4",
|
|
6
|
+
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
|
|
7
|
+
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
|
8
|
+
"SEEKA_APP_ID": "",
|
|
9
|
+
"SEEKA_APP_SECRET": "",
|
|
10
|
+
"SEEKA_DEBUG_ENABLED": "true",
|
|
11
|
+
"SEEKA_INGEST_URL": "",
|
|
12
|
+
"SEEKA_ISSUER_URL": "",
|
|
13
|
+
"NODE_TLS_REJECT_UNAUTHORIZED": "1",
|
|
14
|
+
"REDIS_CONNECTION_USER": "default",
|
|
15
|
+
"REDIS_CONNECTION_PASSWORD": "",
|
|
16
|
+
"REDIS_CONNECTION_TLS": "true",
|
|
17
|
+
"REDIS_CONNECTION_HOST": "",
|
|
18
|
+
"REDIS_CONNECTION_PORT": "",
|
|
19
|
+
"LOGGING_SEQ_SERVERURL": "",
|
|
20
|
+
"LOGGING_SEQ_APIKEY": "",
|
|
21
|
+
"LOGGING_LEVEL": "silly"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "seeka-app-example-name",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Seeka example app for hosting on Azure serverless functions",
|
|
5
|
+
"author": "Seeka <platform@seeka.co>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/src/functions/*.js",
|
|
8
|
+
"private": true,
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.14.0"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "eslint",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"watch": "tsc -w",
|
|
16
|
+
"clean": "<packageManagerRunPrefix> rimraf dist",
|
|
17
|
+
"prestart": "<packageManagerRunPrefix> clean && <packageManagerRunPrefix> build",
|
|
18
|
+
"dev": "func start --port 7072",
|
|
19
|
+
"tunnel": "node scripts/ngrok.js seeka-app-example-name-localdev",
|
|
20
|
+
"deploy": "<packageManagerRunPrefix> clean && <packageManagerRunPrefix> build && func azure functionapp publish seeka-app-example-name --no-build --javascript"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@azure/functions": "^4.1.0",
|
|
24
|
+
"@azure/storage-queue": "^12.16.0",
|
|
25
|
+
"@datalust/winston-seq": "^2.0.0",
|
|
26
|
+
"@seeka-labs/sdk-apps-server": "^1.0.1",
|
|
27
|
+
"axios": "^1.6.7",
|
|
28
|
+
"lodash-es": "^4.17.21",
|
|
29
|
+
"openid-client": "^5.6.4",
|
|
30
|
+
"redis": "^4.6.12",
|
|
31
|
+
"winston": "^3.11.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/lodash-es": "^4.17.12",
|
|
35
|
+
"@types/node": "^18",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
|
37
|
+
"@typescript-eslint/parser": "^6.19.1",
|
|
38
|
+
"azure-functions-core-tools": "^4.x",
|
|
39
|
+
"eslint": "^8",
|
|
40
|
+
"ngrok": "^5.0.0-beta.2",
|
|
41
|
+
"rimraf": "^5.0.0",
|
|
42
|
+
"typescript": "^4.1.6"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
3
|
+
const ngrok = require('ngrok');
|
|
4
|
+
(async function () {
|
|
5
|
+
const url = await ngrok.connect({
|
|
6
|
+
proto: 'http',
|
|
7
|
+
web_addr: 'localhost:4040',
|
|
8
|
+
addr: 7072,
|
|
9
|
+
subdomain: process.argv[2],
|
|
10
|
+
onLogEvent: (event) => {
|
|
11
|
+
console.log('Ngrok - ', event);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
console.log('')
|
|
16
|
+
console.log('')
|
|
17
|
+
console.log('------------------------------------------')
|
|
18
|
+
console.log('')
|
|
19
|
+
console.info(`Public URL for Seeka app is exposed by Ngrok`)
|
|
20
|
+
console.log('')
|
|
21
|
+
console.info(`${url}/api/webhook/seeka/app`)
|
|
22
|
+
console.log('')
|
|
23
|
+
console.info(`Use this URL in your Seeka app configuration for testing`)
|
|
24
|
+
console.log('')
|
|
25
|
+
console.log('------------------------------------------')
|
|
26
|
+
console.log('')
|
|
27
|
+
console.log('')
|
|
28
|
+
})();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
import { app, InvocationContext, Timer } from '@azure/functions';
|
|
4
|
+
import { QueueClient } from '@azure/storage-queue';
|
|
5
|
+
|
|
6
|
+
import { jobNames, queueNames, triggerBackgroundJobWithQueue } from '../lib/jobs';
|
|
7
|
+
import { backgroundJobLogger } from '../lib/logging';
|
|
8
|
+
import { startServices } from '../lib/services';
|
|
9
|
+
import { listInstallations } from '../lib/state/seeka/installations';
|
|
10
|
+
|
|
11
|
+
// https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer?tabs=python-v2%2Cisolated-process%2Cnodejs-v4&pivots=programming-language-typescript
|
|
12
|
+
app.timer('pollingExample', {
|
|
13
|
+
schedule: '0 0 * * * *', // every 1 hour
|
|
14
|
+
handler: pollingExample
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function pollingExample(myTimer: Timer, context: InvocationContext): Promise<void> {
|
|
18
|
+
const logger = backgroundJobLogger(jobNames.pollingExample, undefined, context);
|
|
19
|
+
|
|
20
|
+
logger.profile(`job.${jobNames.pollingExample}`)
|
|
21
|
+
logger.info('Received request to trigger scheduled job');
|
|
22
|
+
|
|
23
|
+
await startServices(logger);
|
|
24
|
+
|
|
25
|
+
const allInstallations = await listInstallations(logger);
|
|
26
|
+
logger.verbose(`Triggering background job for ${allInstallations.length} installations`)
|
|
27
|
+
|
|
28
|
+
const queueClient = new QueueClient(process.env.AzureWebJobsStorage as string, queueNames.queueItemExampleQueueName);
|
|
29
|
+
const promises = allInstallations.map(installation => triggerBackgroundJobWithQueue(queueClient, { ...installation, causationId: context.invocationId, correlationId: context.invocationId }, logger))
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await Promise.all(promises);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
logger.error('Error triggering background jobs', { ex: winston.exceptions.getAllInfo(err) })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logger.profile(`job.${jobNames.pollingExample}`)
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
import { app, InvocationContext } from '@azure/functions';
|
|
4
|
+
|
|
5
|
+
import { BackgroundJobRequestContext, deserialiseQueuePayload, queueNames } from '../lib/jobs';
|
|
6
|
+
import { backgroundJobLogger } from '../lib/logging';
|
|
7
|
+
import { startServices } from '../lib/services';
|
|
8
|
+
|
|
9
|
+
app.storageQueue('queueExample', {
|
|
10
|
+
queueName: queueNames.queueItemExampleQueueName,
|
|
11
|
+
connection: 'AzureWebJobsStorage',
|
|
12
|
+
handler: queueExample
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export async function queueExample(queueItem: string, context: InvocationContext): Promise<void> {
|
|
16
|
+
const body = deserialiseQueuePayload<BackgroundJobRequestContext>(queueItem);
|
|
17
|
+
const logger = backgroundJobLogger(queueNames.queueItemExampleQueueName, body, context);
|
|
18
|
+
logger.profile(`queue.${queueNames.queueItemExampleQueueName}`)
|
|
19
|
+
|
|
20
|
+
await startServices(logger);
|
|
21
|
+
|
|
22
|
+
logger.verbose('Received request to trigger background job', { body });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Execute sync
|
|
26
|
+
// await executeLongRunningTask(body, logger);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
logger.error('Error executing background job', { ex: winston.exceptions.getAllInfo(err) })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.profile(`queue.${queueNames.queueItemExampleQueueName}`)
|
|
33
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
4
|
+
import {
|
|
5
|
+
PersonIdentifiers, SeekaActivityAcceptedWebhookPayload, SeekaAppInstalledWebhookPayload,
|
|
6
|
+
SeekaAppInstallSettingsUpdatedWebhookPayload, SeekaAppUninstalledWebhookPayload,
|
|
7
|
+
SeekaIdentityChangedWebhookPayload, SeekaWebhookCallType, SeekaWebhookPayload,
|
|
8
|
+
throwOnInvalidWebhookSignature
|
|
9
|
+
} from '@seeka-labs/sdk-apps-server';
|
|
10
|
+
|
|
11
|
+
import { queueNames, triggerBackgroundJob } from '../lib/jobs';
|
|
12
|
+
import { webhookLogger } from '../lib/logging';
|
|
13
|
+
import { startServices } from '../lib/services';
|
|
14
|
+
import {
|
|
15
|
+
createOrUpdateInstallation, deleteInstallation, SeekaAppInstallState, tryGetInstallation
|
|
16
|
+
} from '../lib/state/seeka/installations';
|
|
17
|
+
|
|
18
|
+
import type { Logger } from 'winston';
|
|
19
|
+
app.http('seekaAppWebhook', {
|
|
20
|
+
methods: ['POST'],
|
|
21
|
+
authLevel: 'anonymous',
|
|
22
|
+
route: 'webhook/seeka/app',
|
|
23
|
+
handler: seekaAppWebhook
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export async function seekaAppWebhook(req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
|
27
|
+
const bodyStr = (await req.text()) as string;
|
|
28
|
+
const body = JSON.parse(bodyStr) as SeekaWebhookPayload;
|
|
29
|
+
|
|
30
|
+
const logger = webhookLogger(body, context);
|
|
31
|
+
logger.profile('http.seeka.webhook.app')
|
|
32
|
+
logger.verbose('Received webhook from Seeka', { body });
|
|
33
|
+
|
|
34
|
+
// Handle probe
|
|
35
|
+
if (body.type === SeekaWebhookCallType.Probe) {
|
|
36
|
+
return {
|
|
37
|
+
status: 204
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate webhook
|
|
42
|
+
try {
|
|
43
|
+
throwOnInvalidWebhookSignature(process.env.SEEKA_APP_SECRET as string, req.headers, bodyStr);
|
|
44
|
+
logger.debug('Webhook signature validated', { body });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
logger.warn('Webhook signature invalid', { body });
|
|
48
|
+
return {
|
|
49
|
+
status: 401,
|
|
50
|
+
jsonBody: { error: "Webhook call invalid" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (body.isTest) {
|
|
55
|
+
// This is a test webhook call
|
|
56
|
+
return {
|
|
57
|
+
status: 204
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await startServices(logger);
|
|
62
|
+
|
|
63
|
+
// Check if the webhook is for an app we have installed
|
|
64
|
+
let installation: SeekaAppInstallState | null = null;
|
|
65
|
+
if (body.type != SeekaWebhookCallType.AppInstalled) {
|
|
66
|
+
installation = await tryGetInstallation((body as SeekaAppInstalledWebhookPayload).context?.applicationInstallId as string, false, logger);
|
|
67
|
+
if (installation == null) {
|
|
68
|
+
logger.warn('Webhook call cannot be processed as the installation ID is not known by this app', { body });
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
status: 422,
|
|
72
|
+
jsonBody: { error: "App not installed" }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Do something
|
|
78
|
+
try {
|
|
79
|
+
switch (body.type) {
|
|
80
|
+
case SeekaWebhookCallType.AppInstalled:
|
|
81
|
+
{
|
|
82
|
+
await onInstallation(body as SeekaAppInstalledWebhookPayload, logger);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case SeekaWebhookCallType.AppInstallSettingsUpdated:
|
|
86
|
+
{
|
|
87
|
+
await onInstallationSettingsUpdate(body as SeekaAppInstallSettingsUpdatedWebhookPayload, logger);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case SeekaWebhookCallType.AppUninstalled:
|
|
91
|
+
{
|
|
92
|
+
if (!body.isTest) {
|
|
93
|
+
const payload = body as SeekaAppUninstalledWebhookPayload;
|
|
94
|
+
await deleteInstallation(payload.context?.applicationInstallId as string, logger)
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case SeekaWebhookCallType.ActivityAccepted:
|
|
99
|
+
{
|
|
100
|
+
const payload = body as SeekaActivityAcceptedWebhookPayload;
|
|
101
|
+
await handleSeekaActivity(payload, logger);
|
|
102
|
+
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case SeekaWebhookCallType.IdentityChanged:
|
|
106
|
+
{
|
|
107
|
+
const payload = body as SeekaIdentityChangedWebhookPayload;
|
|
108
|
+
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
logger.error('Failed to handle webhook', { ex: winston.exceptions.getAllInfo(err) });
|
|
115
|
+
return {
|
|
116
|
+
status: 500,
|
|
117
|
+
jsonBody: { error: "Request failed" }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
logger.profile('http.seeka.webhook.app')
|
|
122
|
+
logger.verbose('Seeka webhook handled');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
status: 204
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const onInstallation = async (payload: SeekaAppInstalledWebhookPayload, logger: Logger) => {
|
|
131
|
+
if (payload.isTest) return;
|
|
132
|
+
|
|
133
|
+
const installation = await createOrUpdateInstallation({
|
|
134
|
+
...payload.context,
|
|
135
|
+
installationState: {
|
|
136
|
+
grantedPermissions: payload.content?.grantedPermissions || []
|
|
137
|
+
},
|
|
138
|
+
applicationInstallId: payload.context?.applicationInstallId as string,
|
|
139
|
+
organisationBrandId: payload.context?.organisationBrandId as string,
|
|
140
|
+
organisationId: payload.context?.organisationId as string,
|
|
141
|
+
installedAt: new Date().toISOString(),
|
|
142
|
+
installationSettings: payload.content?.installationSettings || {}
|
|
143
|
+
}, logger)
|
|
144
|
+
|
|
145
|
+
await triggerBackgroundJob(queueNames.queueItemExampleQueueName, {
|
|
146
|
+
...payload.context,
|
|
147
|
+
causationId: payload.causationId,
|
|
148
|
+
correlationId: payload.requestId
|
|
149
|
+
}, logger)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const onInstallationSettingsUpdate = async (payload: SeekaAppInstallSettingsUpdatedWebhookPayload, logger: Logger) => {
|
|
153
|
+
if (payload.isTest) return;
|
|
154
|
+
|
|
155
|
+
const existingInstallation = await tryGetInstallation(payload.context?.applicationInstallId as string, true, logger) as SeekaAppInstallState;
|
|
156
|
+
const installation = await createOrUpdateInstallation({
|
|
157
|
+
...existingInstallation,
|
|
158
|
+
installationState: {
|
|
159
|
+
...existingInstallation.installationState,
|
|
160
|
+
grantedPermissions: payload.content?.grantedPermissions || []
|
|
161
|
+
},
|
|
162
|
+
installationSettings: payload.content?.installationSettings || {},
|
|
163
|
+
}, logger)
|
|
164
|
+
|
|
165
|
+
await triggerBackgroundJob(queueNames.queueItemExampleQueueName, {
|
|
166
|
+
...payload.context,
|
|
167
|
+
causationId: payload.causationId,
|
|
168
|
+
correlationId: payload.requestId
|
|
169
|
+
}, logger)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const handleSeekaActivity = async (activity: SeekaActivityAcceptedWebhookPayload, logger: Logger) => {
|
|
173
|
+
// const context = activity.context as SeekaAppWebhookContext;
|
|
174
|
+
// const helper = SeekaAppHelper.create(process.env['SEEKA_APP_SECRET'] as string, {
|
|
175
|
+
// organisationId: context.organisationId as string,
|
|
176
|
+
// applicationInstallId: context.applicationInstallId as string,
|
|
177
|
+
// applicationId: process.env['SEEKA_APP_ID'] as string,
|
|
178
|
+
// }, { name, version }, logger);
|
|
179
|
+
|
|
180
|
+
// // Append a first name to the identity
|
|
181
|
+
// await helper.api.mergeIdentity({
|
|
182
|
+
// seekaPId: activity.content?.personId,
|
|
183
|
+
// firstName: [
|
|
184
|
+
// 'firstname_' + new Date().getTime()
|
|
185
|
+
// ]
|
|
186
|
+
// }, {
|
|
187
|
+
// method: 'toremove',
|
|
188
|
+
// origin: TrackingEventSourceOriginType.Server
|
|
189
|
+
// })
|
|
190
|
+
|
|
191
|
+
// // Fire off a tracking event
|
|
192
|
+
// await helper.api.trackActivity({
|
|
193
|
+
// activityName: TrackingActivityNames.Custom,
|
|
194
|
+
// activityNameCustom: 'seeka-app-activity-accepted',
|
|
195
|
+
// activityId: 'act' + new Date().getTime(),
|
|
196
|
+
// }, activity.content?.personId as string, {
|
|
197
|
+
// method: 'toremove',
|
|
198
|
+
// origin: TrackingEventSourceOriginType.Server
|
|
199
|
+
// })
|
|
200
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Logger } from 'winston';
|
|
2
|
+
import { QueueClient } from '@azure/storage-queue';
|
|
3
|
+
|
|
4
|
+
export interface BackgroundJobRequestContext {
|
|
5
|
+
organisationId?: string;
|
|
6
|
+
organisationBrandId?: string;
|
|
7
|
+
applicationInstallId?: string;
|
|
8
|
+
causationId: string
|
|
9
|
+
correlationId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const signatureHeaderName = 'x-signature-hmac';
|
|
13
|
+
export const apiKeyHeaderName = 'x-api-key';
|
|
14
|
+
|
|
15
|
+
export const jobNames = {
|
|
16
|
+
pollingExample: 'polling-example',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const queueNames = {
|
|
20
|
+
queueItemExampleQueueName: 'sample-queue-name'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const triggerBackgroundJob = async (queueName: string, context: BackgroundJobRequestContext, logger: Logger): Promise<void> => {
|
|
24
|
+
const queueClient = new QueueClient(process.env.AzureWebJobsStorage as string, queueName);
|
|
25
|
+
return triggerBackgroundJobWithQueue(queueClient, context, logger);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const serialiseQueuePayload = (payload: unknown): string => {
|
|
29
|
+
const jsonString = JSON.stringify(payload)
|
|
30
|
+
return Buffer.from(jsonString).toString('base64')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const deserialiseQueuePayload = <TPayload>(payload: string): TPayload => {
|
|
34
|
+
const jsonString = Buffer.from(payload, 'base64').toString()
|
|
35
|
+
return JSON.parse(jsonString) as TPayload;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const triggerBackgroundJobWithQueue = async (queueClient: QueueClient, context: BackgroundJobRequestContext, logger: Logger): Promise<void> => {
|
|
39
|
+
const body = {
|
|
40
|
+
...context
|
|
41
|
+
}
|
|
42
|
+
const bodyStr = serialiseQueuePayload(body);
|
|
43
|
+
|
|
44
|
+
const response = await queueClient.sendMessage(bodyStr);
|
|
45
|
+
|
|
46
|
+
if (response.errorCode) {
|
|
47
|
+
const { requestId, date, errorCode } = response;
|
|
48
|
+
logger.error("Failed to trigger background job", { body, requestId, date, errorCode })
|
|
49
|
+
throw new Error(`Failed to trigger background job: ${response.errorCode}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
logger.info("Background job triggered", { body, messageId: response.messageId, context })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as winston from 'winston';
|
|
2
|
+
|
|
3
|
+
import { InvocationContext } from '@azure/functions';
|
|
4
|
+
import { SeqTransport } from '@datalust/winston-seq';
|
|
5
|
+
|
|
6
|
+
import * as packageJson from '../../../package.json';
|
|
7
|
+
import { BackgroundJobRequestContext } from '../jobs';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
SeekaWebhookPayload, SeekaWebhookPayloadOfSeekaAppWebhookContext
|
|
11
|
+
} from '@seeka-labs/sdk-apps-server';
|
|
12
|
+
|
|
13
|
+
const loggerTransports: winston.transport[] = [
|
|
14
|
+
new winston.transports.Console({
|
|
15
|
+
level: process.env.LOGGING_LEVEL,
|
|
16
|
+
format: winston.format.combine(
|
|
17
|
+
winston.format.errors({ stack: true }),
|
|
18
|
+
winston.format.cli(),
|
|
19
|
+
winston.format.splat(),
|
|
20
|
+
),
|
|
21
|
+
handleExceptions: true,
|
|
22
|
+
handleRejections: true,
|
|
23
|
+
}),
|
|
24
|
+
]
|
|
25
|
+
if (process.env.LOGGING_SEQ_SERVERURL) {
|
|
26
|
+
loggerTransports.push(
|
|
27
|
+
new SeqTransport({
|
|
28
|
+
level: process.env.LOGGING_LEVEL,
|
|
29
|
+
serverUrl: process.env.LOGGING_SEQ_SERVERURL,
|
|
30
|
+
apiKey: process.env.LOGGING_SEQ_APIKEY,
|
|
31
|
+
onError: ((e) => { console.error('Failed to configure Seq logging transport', e) }),
|
|
32
|
+
format: winston.format.combine(
|
|
33
|
+
winston.format.errors({ stack: true }),
|
|
34
|
+
winston.format.json(),
|
|
35
|
+
),
|
|
36
|
+
handleExceptions: true,
|
|
37
|
+
handleRejections: true,
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const logger = winston.createLogger({
|
|
43
|
+
level: process.env.LOGGING_LEVEL,
|
|
44
|
+
defaultMeta: {
|
|
45
|
+
seekaAppId: process.env.SEEKA_APP_ID,
|
|
46
|
+
Hosting_Provider: 'azure',
|
|
47
|
+
Release_Version: `v${packageJson.version}`,
|
|
48
|
+
Hosting_Region: process.env.REGION_NAME,
|
|
49
|
+
Service_Instance_Id: process.env.WEBSITE_ROLE_INSTANCE_ID,
|
|
50
|
+
AzureFunctions_FunctionName: process.env.WEBSITE_SITE_NAME,
|
|
51
|
+
Service: `app/${packageJson.name}`,
|
|
52
|
+
},
|
|
53
|
+
transports: loggerTransports,
|
|
54
|
+
exitOnError: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const childLogger = (meta: object) => logger.child(meta);
|
|
58
|
+
|
|
59
|
+
export const webhookLogger = (payload: SeekaWebhookPayload, functionContext: InvocationContext) => {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
const meta: any = {
|
|
62
|
+
seekaWebhookType: payload.type,
|
|
63
|
+
seekaWebhookIsTest: payload.isTest,
|
|
64
|
+
RequestId: payload.requestId,
|
|
65
|
+
AzureFunctions_InvocationId: functionContext.invocationId,
|
|
66
|
+
CausationId: payload.causationId,
|
|
67
|
+
CorrelationId: payload.causationId,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const context = (payload as SeekaWebhookPayloadOfSeekaAppWebhookContext).context;
|
|
71
|
+
if (context) {
|
|
72
|
+
meta.seekaAppInstallId = context.applicationInstallId;
|
|
73
|
+
meta.seekaAppInstallOrganisationBrandId = context.organisationBrandId;
|
|
74
|
+
meta.seekaAppInstallOrganisationId = context.organisationId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return childLogger(meta)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const backgroundJobLogger = (jobName: string, jobContext: BackgroundJobRequestContext | undefined, functionContext: InvocationContext) => {
|
|
81
|
+
const meta: object = {
|
|
82
|
+
jobContext,
|
|
83
|
+
jobName,
|
|
84
|
+
AzureFunctions_InvocationId: functionContext.invocationId,
|
|
85
|
+
CausationId: jobContext?.causationId,
|
|
86
|
+
CorrelationId: jobContext?.causationId,
|
|
87
|
+
seekaAppInstallId: jobContext?.applicationInstallId,
|
|
88
|
+
seekaAppInstallOrganisationBrandId: jobContext?.organisationBrandId,
|
|
89
|
+
seekaAppInstallOrganisationId: jobContext?.organisationId
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return childLogger(meta)
|
|
93
|
+
}
|