@seeka-labs/cli-apps 1.1.24 → 1.1.25
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 -19
- package/dist/index.js +10 -10
- package/dist/index.js.map +4 -4
- package/dist/init-templates/aws-lambda/.example.gitignore +48 -48
- package/dist/init-templates/aws-lambda/.nvmrc +1 -1
- package/dist/init-templates/aws-lambda/README.md +76 -76
- package/dist/init-templates/aws-lambda/jest.config.js +4 -4
- package/dist/init-templates/aws-lambda/package.json +51 -54
- package/dist/init-templates/aws-lambda/src/index.test.ts +6 -6
- package/dist/init-templates/aws-lambda/src/lib/logging/index.ts +87 -87
- package/dist/init-templates/aws-lambda/src/lib/state/redis/index.ts +64 -64
- package/dist/init-templates/aws-lambda/src/lib/state/seeka/installations.ts +66 -66
- package/dist/init-templates/aws-lambda/src/routes/seekaAppWebhook.ts +193 -193
- package/dist/init-templates/azure-function/.example.gitignore +47 -47
- package/dist/init-templates/azure-function/README.md +107 -107
- package/dist/init-templates/azure-function/jest.config.js +4 -4
- package/dist/init-templates/azure-function/package.json +45 -48
- package/dist/init-templates/azure-function/scripts/dev-queue-setup.js +29 -29
- package/dist/init-templates/azure-function/src/functions/healthCheck.ts +13 -13
- package/dist/init-templates/azure-function/src/functions/pollingExample.ts +39 -39
- package/dist/init-templates/azure-function/src/functions/queueExample.ts +66 -66
- package/dist/init-templates/azure-function/src/functions/seekaAppWebhook.ts +236 -236
- package/dist/init-templates/azure-function/src/index.test.ts +6 -6
- package/dist/init-templates/azure-function/src/lib/browser/index.ts +54 -54
- package/dist/init-templates/azure-function/src/lib/browser/models/index.ts +6 -6
- package/dist/init-templates/azure-function/src/lib/jobs/index.ts +95 -95
- package/dist/init-templates/azure-function/src/lib/logging/index.ts +92 -92
- package/dist/init-templates/azure-function/src/lib/state/redis/index.ts +64 -64
- package/dist/init-templates/azure-function/src/lib/state/seeka/installations.ts +66 -66
- package/dist/init-templates/browser/.editorconfig +14 -14
- package/dist/init-templates/browser/.eslintrc.cjs +1 -1
- package/dist/init-templates/browser/.yarnrc +1 -1
- package/dist/init-templates/browser/jest.config.js +11 -11
- package/dist/init-templates/browser/package.json +3 -3
- package/dist/init-templates/browser/scripts/esbuild/build-browser-plugin.mjs +110 -110
- package/dist/init-templates/browser/scripts/esbuild/plugins/importAsGlobals.mjs +38 -38
- package/dist/init-templates/browser/src/browser.ts +12 -12
- package/dist/init-templates/browser/src/plugin/index.test.ts +6 -6
- package/dist/init-templates/browser/src/plugin/index.ts +49 -49
- package/dist/init-templates/browser/tsconfig.json +34 -34
- package/dist/init-templates/netlify-function/.env.example +17 -17
- package/dist/init-templates/netlify-function/.example.gitignore +36 -36
- package/dist/init-templates/netlify-function/.nvmrc +1 -1
- package/dist/init-templates/netlify-function/.vscode/launch.json +44 -44
- package/dist/init-templates/netlify-function/README.md +61 -61
- package/dist/init-templates/netlify-function/jest.config.js +4 -4
- package/dist/init-templates/netlify-function/netlify.toml +6 -6
- package/dist/init-templates/netlify-function/package.json +11 -14
- package/dist/init-templates/netlify-function/src/api/example-job-background/index.ts +51 -51
- package/dist/init-templates/netlify-function/src/api/polling-example-job-scheduled/index.ts +45 -45
- package/dist/init-templates/netlify-function/src/api/seeka-app-webhook/index.ts +216 -216
- package/dist/init-templates/netlify-function/src/index.test.ts +6 -6
- package/dist/init-templates/netlify-function/src/lib/jobs/index.ts +67 -67
- package/dist/init-templates/netlify-function/src/lib/logging/index.ts +90 -90
- package/dist/init-templates/netlify-function/src/lib/state/redis/index.ts +64 -64
- package/dist/init-templates/netlify-function/src/lib/state/seeka/installations.ts +66 -66
- package/package.json +7 -7
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { createClient } from 'redis';
|
|
2
|
-
|
|
3
|
-
import { getLogger } from '../../logging';
|
|
4
|
-
import winston from 'winston';
|
|
5
|
-
|
|
6
|
-
const redisProtocol = process.env.REDIS_CONNECTION_TLS === 'true' ? 'rediss://' : 'redis://';
|
|
7
|
-
const redisConn = `${redisProtocol}${process.env.REDIS_CONNECTION_USER}:${process.env.REDIS_CONNECTION_PASSWORD}@${process.env.REDIS_CONNECTION_HOST}:${process.env.REDIS_CONNECTION_PORT}`;
|
|
8
|
-
|
|
9
|
-
const redisClient = createClient({
|
|
10
|
-
url: redisConn
|
|
11
|
-
})
|
|
12
|
-
.on('error', (err: any) => getLogger().error('Redis Client ', { ex: winston.exceptions.getAllInfo(err) }));
|
|
13
|
-
|
|
14
|
-
export const connect = async () => {
|
|
15
|
-
await redisClient.connect();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const isConnected = () => {
|
|
19
|
-
return redisClient.isOpen;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const disconnect = async () => {
|
|
23
|
-
await redisClient.disconnect();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const getKeyPrefix = (stateType: string) => `seeka:app:${process.env.SEEKA_APP_ID}:${stateType}`
|
|
27
|
-
const getKey = (stateType: string, key: string) => `${getKeyPrefix(stateType)}:${key}`
|
|
28
|
-
|
|
29
|
-
export async function getOrCreate<TState>(stateType: string, key: string, toCreate: TState): Promise<TState> {
|
|
30
|
-
const fullKey = getKey(stateType, key);
|
|
31
|
-
const existingStr = await redisClient.get(fullKey);
|
|
32
|
-
if (existingStr) return JSON.parse(existingStr);
|
|
33
|
-
|
|
34
|
-
await redisClient.set(fullKey, JSON.stringify(toCreate));
|
|
35
|
-
|
|
36
|
-
return toCreate;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function tryGet<TState>(stateType: string, key: string): Promise<TState | null> {
|
|
40
|
-
const fullKey = getKey(stateType, key);
|
|
41
|
-
const existingStr = await redisClient.get(fullKey);
|
|
42
|
-
if (existingStr) return JSON.parse(existingStr);
|
|
43
|
-
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function getList<TState>(stateType: string): Promise<TState[]> {
|
|
48
|
-
const prefix = getKeyPrefix(stateType);
|
|
49
|
-
const allKeys = await redisClient.keys(`${prefix}:*`);
|
|
50
|
-
const listStr = await redisClient.mGet(allKeys);
|
|
51
|
-
|
|
52
|
-
if (listStr) return listStr.filter(e => Boolean(e)).map(e => JSON.parse(e as string));
|
|
53
|
-
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function set<TState>(stateType: string, key: string, toCreate: TState): Promise<void> {
|
|
58
|
-
const fullKey = getKey(stateType, key);
|
|
59
|
-
await redisClient.set(fullKey, JSON.stringify(toCreate));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function remove(stateType: string, key: string): Promise<void> {
|
|
63
|
-
const fullKey = getKey(stateType, key);
|
|
64
|
-
await redisClient.del(fullKey);
|
|
1
|
+
import { createClient } from 'redis';
|
|
2
|
+
|
|
3
|
+
import { getLogger } from '../../logging';
|
|
4
|
+
import winston from 'winston';
|
|
5
|
+
|
|
6
|
+
const redisProtocol = process.env.REDIS_CONNECTION_TLS === 'true' ? 'rediss://' : 'redis://';
|
|
7
|
+
const redisConn = `${redisProtocol}${process.env.REDIS_CONNECTION_USER}:${process.env.REDIS_CONNECTION_PASSWORD}@${process.env.REDIS_CONNECTION_HOST}:${process.env.REDIS_CONNECTION_PORT}`;
|
|
8
|
+
|
|
9
|
+
const redisClient = createClient({
|
|
10
|
+
url: redisConn
|
|
11
|
+
})
|
|
12
|
+
.on('error', (err: any) => getLogger().error('Redis Client ', { ex: winston.exceptions.getAllInfo(err) }));
|
|
13
|
+
|
|
14
|
+
export const connect = async () => {
|
|
15
|
+
await redisClient.connect();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const isConnected = () => {
|
|
19
|
+
return redisClient.isOpen;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const disconnect = async () => {
|
|
23
|
+
await redisClient.disconnect();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getKeyPrefix = (stateType: string) => `seeka:app:${process.env.SEEKA_APP_ID}:${stateType}`
|
|
27
|
+
const getKey = (stateType: string, key: string) => `${getKeyPrefix(stateType)}:${key}`
|
|
28
|
+
|
|
29
|
+
export async function getOrCreate<TState>(stateType: string, key: string, toCreate: TState): Promise<TState> {
|
|
30
|
+
const fullKey = getKey(stateType, key);
|
|
31
|
+
const existingStr = await redisClient.get(fullKey);
|
|
32
|
+
if (existingStr) return JSON.parse(existingStr);
|
|
33
|
+
|
|
34
|
+
await redisClient.set(fullKey, JSON.stringify(toCreate));
|
|
35
|
+
|
|
36
|
+
return toCreate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function tryGet<TState>(stateType: string, key: string): Promise<TState | null> {
|
|
40
|
+
const fullKey = getKey(stateType, key);
|
|
41
|
+
const existingStr = await redisClient.get(fullKey);
|
|
42
|
+
if (existingStr) return JSON.parse(existingStr);
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getList<TState>(stateType: string): Promise<TState[]> {
|
|
48
|
+
const prefix = getKeyPrefix(stateType);
|
|
49
|
+
const allKeys = await redisClient.keys(`${prefix}:*`);
|
|
50
|
+
const listStr = await redisClient.mGet(allKeys);
|
|
51
|
+
|
|
52
|
+
if (listStr) return listStr.filter(e => Boolean(e)).map(e => JSON.parse(e as string));
|
|
53
|
+
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function set<TState>(stateType: string, key: string, toCreate: TState): Promise<void> {
|
|
58
|
+
const fullKey = getKey(stateType, key);
|
|
59
|
+
await redisClient.set(fullKey, JSON.stringify(toCreate));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function remove(stateType: string, key: string): Promise<void> {
|
|
63
|
+
const fullKey = getKey(stateType, key);
|
|
64
|
+
await redisClient.del(fullKey);
|
|
65
65
|
}
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import type { Logger } from 'winston';
|
|
2
|
-
|
|
3
|
-
import { getList, remove, set, tryGet } from '../../state/redis';
|
|
4
|
-
|
|
5
|
-
export interface SeekaAppInstallState {
|
|
6
|
-
/** ID of the organisation that installed the app */
|
|
7
|
-
organisationId: string;
|
|
8
|
-
/** ID of the brand that installed the app */
|
|
9
|
-
organisationBrandId: string;
|
|
10
|
-
/** ID of the installation of the app */
|
|
11
|
-
applicationInstallId: string;
|
|
12
|
-
// Installation settings provided by the user installing the app
|
|
13
|
-
installationSettings: SampleAppInstallSettings;
|
|
14
|
-
// State relating to the app and installation of the app
|
|
15
|
-
installationState: SampleAppInstallState;
|
|
16
|
-
|
|
17
|
-
// When the app was installed
|
|
18
|
-
installedAt: string; // new Date().toISOString()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface SampleAppInstallState {
|
|
22
|
-
stateItem1?: string
|
|
23
|
-
stateItem2?: string
|
|
24
|
-
grantedPermissions?: string[]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type SampleAppInstallSettings = { [key: string]: any; } | {
|
|
28
|
-
myAppInstallSetting1: string | number | undefined;
|
|
29
|
-
myAppInstallSetting2: string | number | undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const stateType = 'install'
|
|
33
|
-
|
|
34
|
-
export const tryGetInstallation = async (applicationInstallId: string, throwWhenNotFound: boolean, logger: Logger): Promise<SeekaAppInstallState | null> => {
|
|
35
|
-
const installation = await tryGet<SeekaAppInstallState>(stateType, applicationInstallId);
|
|
36
|
-
if (installation == null && throwWhenNotFound) {
|
|
37
|
-
throw new Error(`Seeka installation ${applicationInstallId} not found`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return installation;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const listInstallations = async (logger: Logger): Promise<SeekaAppInstallState[]> => {
|
|
44
|
-
const installations = await getList<SeekaAppInstallState>(stateType);
|
|
45
|
-
|
|
46
|
-
return installations;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export const createOrUpdateInstallation = async (state: SeekaAppInstallState, logger: Logger): Promise<SeekaAppInstallState> => {
|
|
51
|
-
if (!state.installationState) state.installationState = {};
|
|
52
|
-
if (!state.installedAt) state.installedAt = new Date().toISOString();
|
|
53
|
-
|
|
54
|
-
const creating = (await tryGetInstallation(state.applicationInstallId, false, logger)) === null;
|
|
55
|
-
|
|
56
|
-
await set(stateType, state.applicationInstallId, state);
|
|
57
|
-
|
|
58
|
-
logger.info(creating ? 'Created installation state' : 'Updated installation state', { applicationInstallId: state.applicationInstallId, organisationId: state.organisationId, organisationBrandId: state.organisationBrandId });
|
|
59
|
-
|
|
60
|
-
return state;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const deleteInstallation = async (applicationInstallId: string, logger: Logger): Promise<void> => {
|
|
64
|
-
await remove(stateType, applicationInstallId);
|
|
65
|
-
|
|
66
|
-
logger.info('Deleted installation state', { applicationInstallId });
|
|
1
|
+
import type { Logger } from 'winston';
|
|
2
|
+
|
|
3
|
+
import { getList, remove, set, tryGet } from '../../state/redis';
|
|
4
|
+
|
|
5
|
+
export interface SeekaAppInstallState {
|
|
6
|
+
/** ID of the organisation that installed the app */
|
|
7
|
+
organisationId: string;
|
|
8
|
+
/** ID of the brand that installed the app */
|
|
9
|
+
organisationBrandId: string;
|
|
10
|
+
/** ID of the installation of the app */
|
|
11
|
+
applicationInstallId: string;
|
|
12
|
+
// Installation settings provided by the user installing the app
|
|
13
|
+
installationSettings: SampleAppInstallSettings;
|
|
14
|
+
// State relating to the app and installation of the app
|
|
15
|
+
installationState: SampleAppInstallState;
|
|
16
|
+
|
|
17
|
+
// When the app was installed
|
|
18
|
+
installedAt: string; // new Date().toISOString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SampleAppInstallState {
|
|
22
|
+
stateItem1?: string
|
|
23
|
+
stateItem2?: string
|
|
24
|
+
grantedPermissions?: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SampleAppInstallSettings = { [key: string]: any; } | {
|
|
28
|
+
myAppInstallSetting1: string | number | undefined;
|
|
29
|
+
myAppInstallSetting2: string | number | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stateType = 'install'
|
|
33
|
+
|
|
34
|
+
export const tryGetInstallation = async (applicationInstallId: string, throwWhenNotFound: boolean, logger: Logger): Promise<SeekaAppInstallState | null> => {
|
|
35
|
+
const installation = await tryGet<SeekaAppInstallState>(stateType, applicationInstallId);
|
|
36
|
+
if (installation == null && throwWhenNotFound) {
|
|
37
|
+
throw new Error(`Seeka installation ${applicationInstallId} not found`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return installation;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const listInstallations = async (logger: Logger): Promise<SeekaAppInstallState[]> => {
|
|
44
|
+
const installations = await getList<SeekaAppInstallState>(stateType);
|
|
45
|
+
|
|
46
|
+
return installations;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export const createOrUpdateInstallation = async (state: SeekaAppInstallState, logger: Logger): Promise<SeekaAppInstallState> => {
|
|
51
|
+
if (!state.installationState) state.installationState = {};
|
|
52
|
+
if (!state.installedAt) state.installedAt = new Date().toISOString();
|
|
53
|
+
|
|
54
|
+
const creating = (await tryGetInstallation(state.applicationInstallId, false, logger)) === null;
|
|
55
|
+
|
|
56
|
+
await set(stateType, state.applicationInstallId, state);
|
|
57
|
+
|
|
58
|
+
logger.info(creating ? 'Created installation state' : 'Updated installation state', { applicationInstallId: state.applicationInstallId, organisationId: state.organisationId, organisationBrandId: state.organisationBrandId });
|
|
59
|
+
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const deleteInstallation = async (applicationInstallId: string, logger: Logger): Promise<void> => {
|
|
64
|
+
await remove(stateType, applicationInstallId);
|
|
65
|
+
|
|
66
|
+
logger.info('Deleted installation state', { applicationInstallId });
|
|
67
67
|
}
|
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
import type { Logger } from 'winston';
|
|
2
|
-
|
|
3
|
-
import { Context } from 'aws-lambda';
|
|
4
|
-
import { Request, Response } from 'express';
|
|
5
|
-
import winston from 'winston';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
PersonIdentifiers, SeekaActivityAcceptedWebhookPayload, SeekaAppInstalledWebhookPayload,
|
|
9
|
-
SeekaAppInstallSettingsUpdatedWebhookPayload, SeekaAppUninstalledWebhookPayload,
|
|
10
|
-
SeekaIdentityChangedWebhookPayload, SeekaWebhookCallType, SeekaWebhookPayload,
|
|
11
|
-
throwOnInvalidWebhookSignature
|
|
12
|
-
} from '@seeka-labs/sdk-apps-server';
|
|
13
|
-
|
|
14
|
-
import { webhookLogger } from '../lib/logging';
|
|
15
|
-
import { startServices } from '../lib/services';
|
|
16
|
-
import {
|
|
17
|
-
createOrUpdateInstallation, deleteInstallation, SampleAppInstallSettings, SeekaAppInstallState, tryGetInstallation
|
|
18
|
-
} from '../lib/state/seeka/installations';
|
|
19
|
-
|
|
20
|
-
export async function seekaAppWebhook(req: Request, res: Response, context: Context): Promise<void> {
|
|
21
|
-
const bodyStr = req.body as string;
|
|
22
|
-
const body = JSON.parse(bodyStr) as SeekaWebhookPayload;
|
|
23
|
-
|
|
24
|
-
const logger = webhookLogger(body, context);
|
|
25
|
-
logger.profile('http.seeka.webhook.app')
|
|
26
|
-
logger.verbose('Received webhook from Seeka', { body });
|
|
27
|
-
|
|
28
|
-
// Handle probe
|
|
29
|
-
if (body.type === SeekaWebhookCallType.Probe) {
|
|
30
|
-
res.status(204).send();
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Validate webhook
|
|
35
|
-
try {
|
|
36
|
-
throwOnInvalidWebhookSignature(process.env.SEEKA_APP_SECRET as string, req.headers, bodyStr);
|
|
37
|
-
logger.debug('Webhook signature validated', { body });
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
logger.warn('Webhook signature invalid', { body });
|
|
41
|
-
res.status(401).json({ error: "Webhook call invalid" }).send();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (body.isTest) {
|
|
46
|
-
// This is a test webhook call
|
|
47
|
-
res.status(204).send();
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
await startServices(logger);
|
|
52
|
-
|
|
53
|
-
// Check if the webhook is for an app we have installed
|
|
54
|
-
let installation: SeekaAppInstallState | null = null;
|
|
55
|
-
if (body.type != SeekaWebhookCallType.AppInstalled) {
|
|
56
|
-
installation = await tryGetInstallation((body as SeekaAppInstalledWebhookPayload).context?.applicationInstallId as string, false, logger);
|
|
57
|
-
if (installation == null) {
|
|
58
|
-
logger.warn('Webhook call cannot be processed as the installation ID is not known by this app', { body });
|
|
59
|
-
|
|
60
|
-
res.status(422).json({ error: "App not installed" }).send();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Do something
|
|
66
|
-
try {
|
|
67
|
-
let errorMessage: string | null = null;
|
|
68
|
-
switch (body.type) {
|
|
69
|
-
case SeekaWebhookCallType.AppInstalled:
|
|
70
|
-
{
|
|
71
|
-
errorMessage = await onInstallation(body as SeekaAppInstalledWebhookPayload, logger);
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
case SeekaWebhookCallType.AppInstallSettingsUpdated:
|
|
75
|
-
{
|
|
76
|
-
errorMessage = await onInstallationSettingsUpdate(body as SeekaAppInstallSettingsUpdatedWebhookPayload, logger);
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
case SeekaWebhookCallType.AppUninstalled:
|
|
80
|
-
{
|
|
81
|
-
if (!body.isTest) {
|
|
82
|
-
const payload = body as SeekaAppUninstalledWebhookPayload;
|
|
83
|
-
await deleteInstallation(payload.context?.applicationInstallId as string, logger)
|
|
84
|
-
}
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
case SeekaWebhookCallType.ActivityAccepted:
|
|
88
|
-
{
|
|
89
|
-
const payload = body as SeekaActivityAcceptedWebhookPayload;
|
|
90
|
-
await handleSeekaActivity(payload, logger);
|
|
91
|
-
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
case SeekaWebhookCallType.IdentityChanged:
|
|
95
|
-
{
|
|
96
|
-
const payload = body as SeekaIdentityChangedWebhookPayload;
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (errorMessage) {
|
|
102
|
-
logger.warn('Webhook call failed', { errorMessage });
|
|
103
|
-
res.status(400).json({ error: { message: errorMessage } }).send();
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
res.status(204).send();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
logger.error('Failed to handle webhook', { ex: winston.exceptions.getAllInfo(err) });
|
|
111
|
-
res.status(500).json({ error: "Request failed" }).send();
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
logger.profile('http.seeka.webhook.app')
|
|
115
|
-
logger.verbose('Seeka webhook handled');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const validateInstallationSettings = async (installSettings: SampleAppInstallSettings, logger: Logger): Promise<string | null> => {
|
|
120
|
-
// Returning an error message string here will block the installation request or settings update request by the user installing the app
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const onInstallation = async (payload: SeekaAppInstalledWebhookPayload, logger: Logger): Promise<string | null> => {
|
|
127
|
-
if (payload.isTest) return null;
|
|
128
|
-
|
|
129
|
-
const errorMessage = await validateInstallationSettings(payload.content?.installationSettings || {}, logger);
|
|
130
|
-
if (errorMessage) return errorMessage;
|
|
131
|
-
|
|
132
|
-
const installation = await createOrUpdateInstallation({
|
|
133
|
-
...payload.context,
|
|
134
|
-
installationState: {
|
|
135
|
-
grantedPermissions: payload.content?.grantedPermissions || []
|
|
136
|
-
},
|
|
137
|
-
applicationInstallId: payload.context?.applicationInstallId as string,
|
|
138
|
-
organisationBrandId: payload.context?.organisationBrandId as string,
|
|
139
|
-
organisationId: payload.context?.organisationId as string,
|
|
140
|
-
installedAt: new Date().toISOString(),
|
|
141
|
-
installationSettings: payload.content?.installationSettings || {}
|
|
142
|
-
}, logger)
|
|
143
|
-
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const onInstallationSettingsUpdate = async (payload: SeekaAppInstallSettingsUpdatedWebhookPayload, logger: Logger): Promise<string | null> => {
|
|
148
|
-
if (payload.isTest) return null;
|
|
149
|
-
|
|
150
|
-
const errorMessage = await validateInstallationSettings(payload.content?.installationSettings || {}, logger);
|
|
151
|
-
if (errorMessage) return errorMessage;
|
|
152
|
-
|
|
153
|
-
const existingInstallation = await tryGetInstallation(payload.context?.applicationInstallId as string, true, logger) as SeekaAppInstallState;
|
|
154
|
-
const installation = await createOrUpdateInstallation({
|
|
155
|
-
...existingInstallation,
|
|
156
|
-
installationState: {
|
|
157
|
-
...existingInstallation.installationState,
|
|
158
|
-
grantedPermissions: payload.content?.grantedPermissions || []
|
|
159
|
-
},
|
|
160
|
-
installationSettings: payload.content?.installationSettings || {},
|
|
161
|
-
}, logger)
|
|
162
|
-
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const handleSeekaActivity = async (activity: SeekaActivityAcceptedWebhookPayload, logger: Logger) => {
|
|
167
|
-
// const context = activity.context as SeekaAppWebhookContext;
|
|
168
|
-
// const helper = SeekaAppHelper.create(process.env['SEEKA_APP_SECRET'] as string, {
|
|
169
|
-
// organisationId: context.organisationId as string,
|
|
170
|
-
// applicationInstallId: context.applicationInstallId as string,
|
|
171
|
-
// applicationId: process.env['SEEKA_APP_ID'] as string,
|
|
172
|
-
// }, { name, version }, logger);
|
|
173
|
-
|
|
174
|
-
// // Append a first name to the identity
|
|
175
|
-
// await helper.api.mergeIdentity({
|
|
176
|
-
// seekaPId: activity.content?.personId,
|
|
177
|
-
// firstName: [
|
|
178
|
-
// 'firstname_' + new Date().getTime()
|
|
179
|
-
// ]
|
|
180
|
-
// }, {
|
|
181
|
-
// method: 'toremove',
|
|
182
|
-
// origin: TrackingEventSourceOriginType.Server
|
|
183
|
-
// })
|
|
184
|
-
|
|
185
|
-
// // Fire off a tracking event
|
|
186
|
-
// await helper.api.trackActivity({
|
|
187
|
-
// activityName: TrackingActivityNames.Custom,
|
|
188
|
-
// activityNameCustom: 'seeka-app-activity-accepted',
|
|
189
|
-
// activityId: 'act' + new Date().getTime(),
|
|
190
|
-
// }, activity.content?.personId as string, {
|
|
191
|
-
// method: 'toremove',
|
|
192
|
-
// origin: TrackingEventSourceOriginType.Server
|
|
193
|
-
// })
|
|
1
|
+
import type { Logger } from 'winston';
|
|
2
|
+
|
|
3
|
+
import { Context } from 'aws-lambda';
|
|
4
|
+
import { Request, Response } from 'express';
|
|
5
|
+
import winston from 'winston';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
PersonIdentifiers, SeekaActivityAcceptedWebhookPayload, SeekaAppInstalledWebhookPayload,
|
|
9
|
+
SeekaAppInstallSettingsUpdatedWebhookPayload, SeekaAppUninstalledWebhookPayload,
|
|
10
|
+
SeekaIdentityChangedWebhookPayload, SeekaWebhookCallType, SeekaWebhookPayload,
|
|
11
|
+
throwOnInvalidWebhookSignature
|
|
12
|
+
} from '@seeka-labs/sdk-apps-server';
|
|
13
|
+
|
|
14
|
+
import { webhookLogger } from '../lib/logging';
|
|
15
|
+
import { startServices } from '../lib/services';
|
|
16
|
+
import {
|
|
17
|
+
createOrUpdateInstallation, deleteInstallation, SampleAppInstallSettings, SeekaAppInstallState, tryGetInstallation
|
|
18
|
+
} from '../lib/state/seeka/installations';
|
|
19
|
+
|
|
20
|
+
export async function seekaAppWebhook(req: Request, res: Response, context: Context): Promise<void> {
|
|
21
|
+
const bodyStr = req.body as string;
|
|
22
|
+
const body = JSON.parse(bodyStr) as SeekaWebhookPayload;
|
|
23
|
+
|
|
24
|
+
const logger = webhookLogger(body, context);
|
|
25
|
+
logger.profile('http.seeka.webhook.app')
|
|
26
|
+
logger.verbose('Received webhook from Seeka', { body });
|
|
27
|
+
|
|
28
|
+
// Handle probe
|
|
29
|
+
if (body.type === SeekaWebhookCallType.Probe) {
|
|
30
|
+
res.status(204).send();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate webhook
|
|
35
|
+
try {
|
|
36
|
+
throwOnInvalidWebhookSignature(process.env.SEEKA_APP_SECRET as string, req.headers, bodyStr);
|
|
37
|
+
logger.debug('Webhook signature validated', { body });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
logger.warn('Webhook signature invalid', { body });
|
|
41
|
+
res.status(401).json({ error: "Webhook call invalid" }).send();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (body.isTest) {
|
|
46
|
+
// This is a test webhook call
|
|
47
|
+
res.status(204).send();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await startServices(logger);
|
|
52
|
+
|
|
53
|
+
// Check if the webhook is for an app we have installed
|
|
54
|
+
let installation: SeekaAppInstallState | null = null;
|
|
55
|
+
if (body.type != SeekaWebhookCallType.AppInstalled) {
|
|
56
|
+
installation = await tryGetInstallation((body as SeekaAppInstalledWebhookPayload).context?.applicationInstallId as string, false, logger);
|
|
57
|
+
if (installation == null) {
|
|
58
|
+
logger.warn('Webhook call cannot be processed as the installation ID is not known by this app', { body });
|
|
59
|
+
|
|
60
|
+
res.status(422).json({ error: "App not installed" }).send();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Do something
|
|
66
|
+
try {
|
|
67
|
+
let errorMessage: string | null = null;
|
|
68
|
+
switch (body.type) {
|
|
69
|
+
case SeekaWebhookCallType.AppInstalled:
|
|
70
|
+
{
|
|
71
|
+
errorMessage = await onInstallation(body as SeekaAppInstalledWebhookPayload, logger);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case SeekaWebhookCallType.AppInstallSettingsUpdated:
|
|
75
|
+
{
|
|
76
|
+
errorMessage = await onInstallationSettingsUpdate(body as SeekaAppInstallSettingsUpdatedWebhookPayload, logger);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case SeekaWebhookCallType.AppUninstalled:
|
|
80
|
+
{
|
|
81
|
+
if (!body.isTest) {
|
|
82
|
+
const payload = body as SeekaAppUninstalledWebhookPayload;
|
|
83
|
+
await deleteInstallation(payload.context?.applicationInstallId as string, logger)
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case SeekaWebhookCallType.ActivityAccepted:
|
|
88
|
+
{
|
|
89
|
+
const payload = body as SeekaActivityAcceptedWebhookPayload;
|
|
90
|
+
await handleSeekaActivity(payload, logger);
|
|
91
|
+
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case SeekaWebhookCallType.IdentityChanged:
|
|
95
|
+
{
|
|
96
|
+
const payload = body as SeekaIdentityChangedWebhookPayload;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (errorMessage) {
|
|
102
|
+
logger.warn('Webhook call failed', { errorMessage });
|
|
103
|
+
res.status(400).json({ error: { message: errorMessage } }).send();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
res.status(204).send();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
logger.error('Failed to handle webhook', { ex: winston.exceptions.getAllInfo(err) });
|
|
111
|
+
res.status(500).json({ error: "Request failed" }).send();
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
logger.profile('http.seeka.webhook.app')
|
|
115
|
+
logger.verbose('Seeka webhook handled');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const validateInstallationSettings = async (installSettings: SampleAppInstallSettings, logger: Logger): Promise<string | null> => {
|
|
120
|
+
// Returning an error message string here will block the installation request or settings update request by the user installing the app
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const onInstallation = async (payload: SeekaAppInstalledWebhookPayload, logger: Logger): Promise<string | null> => {
|
|
127
|
+
if (payload.isTest) return null;
|
|
128
|
+
|
|
129
|
+
const errorMessage = await validateInstallationSettings(payload.content?.installationSettings || {}, logger);
|
|
130
|
+
if (errorMessage) return errorMessage;
|
|
131
|
+
|
|
132
|
+
const installation = await createOrUpdateInstallation({
|
|
133
|
+
...payload.context,
|
|
134
|
+
installationState: {
|
|
135
|
+
grantedPermissions: payload.content?.grantedPermissions || []
|
|
136
|
+
},
|
|
137
|
+
applicationInstallId: payload.context?.applicationInstallId as string,
|
|
138
|
+
organisationBrandId: payload.context?.organisationBrandId as string,
|
|
139
|
+
organisationId: payload.context?.organisationId as string,
|
|
140
|
+
installedAt: new Date().toISOString(),
|
|
141
|
+
installationSettings: payload.content?.installationSettings || {}
|
|
142
|
+
}, logger)
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const onInstallationSettingsUpdate = async (payload: SeekaAppInstallSettingsUpdatedWebhookPayload, logger: Logger): Promise<string | null> => {
|
|
148
|
+
if (payload.isTest) return null;
|
|
149
|
+
|
|
150
|
+
const errorMessage = await validateInstallationSettings(payload.content?.installationSettings || {}, logger);
|
|
151
|
+
if (errorMessage) return errorMessage;
|
|
152
|
+
|
|
153
|
+
const existingInstallation = await tryGetInstallation(payload.context?.applicationInstallId as string, true, logger) as SeekaAppInstallState;
|
|
154
|
+
const installation = await createOrUpdateInstallation({
|
|
155
|
+
...existingInstallation,
|
|
156
|
+
installationState: {
|
|
157
|
+
...existingInstallation.installationState,
|
|
158
|
+
grantedPermissions: payload.content?.grantedPermissions || []
|
|
159
|
+
},
|
|
160
|
+
installationSettings: payload.content?.installationSettings || {},
|
|
161
|
+
}, logger)
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const handleSeekaActivity = async (activity: SeekaActivityAcceptedWebhookPayload, logger: Logger) => {
|
|
167
|
+
// const context = activity.context as SeekaAppWebhookContext;
|
|
168
|
+
// const helper = SeekaAppHelper.create(process.env['SEEKA_APP_SECRET'] as string, {
|
|
169
|
+
// organisationId: context.organisationId as string,
|
|
170
|
+
// applicationInstallId: context.applicationInstallId as string,
|
|
171
|
+
// applicationId: process.env['SEEKA_APP_ID'] as string,
|
|
172
|
+
// }, { name, version }, logger);
|
|
173
|
+
|
|
174
|
+
// // Append a first name to the identity
|
|
175
|
+
// await helper.api.mergeIdentity({
|
|
176
|
+
// seekaPId: activity.content?.personId,
|
|
177
|
+
// firstName: [
|
|
178
|
+
// 'firstname_' + new Date().getTime()
|
|
179
|
+
// ]
|
|
180
|
+
// }, {
|
|
181
|
+
// method: 'toremove',
|
|
182
|
+
// origin: TrackingEventSourceOriginType.Server
|
|
183
|
+
// })
|
|
184
|
+
|
|
185
|
+
// // Fire off a tracking event
|
|
186
|
+
// await helper.api.trackActivity({
|
|
187
|
+
// activityName: TrackingActivityNames.Custom,
|
|
188
|
+
// activityNameCustom: 'seeka-app-activity-accepted',
|
|
189
|
+
// activityId: 'act' + new Date().getTime(),
|
|
190
|
+
// }, activity.content?.personId as string, {
|
|
191
|
+
// method: 'toremove',
|
|
192
|
+
// origin: TrackingEventSourceOriginType.Server
|
|
193
|
+
// })
|
|
194
194
|
}
|