@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.
Files changed (60) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +1 -0
  3. package/dist/index.js +47 -0
  4. package/dist/init-templates/aws-lambda/.env.example +15 -0
  5. package/dist/init-templates/aws-lambda/.eslintrc.cjs +10 -0
  6. package/dist/init-templates/aws-lambda/.example.gitignore +49 -0
  7. package/dist/init-templates/aws-lambda/.gitlab-ci.yml +37 -0
  8. package/dist/init-templates/aws-lambda/.vscode/extensions.json +5 -0
  9. package/dist/init-templates/aws-lambda/.vscode/launch.json +20 -0
  10. package/dist/init-templates/aws-lambda/.vscode/settings.json +3 -0
  11. package/dist/init-templates/aws-lambda/.vscode/tasks.json +12 -0
  12. package/dist/init-templates/aws-lambda/README.md +75 -0
  13. package/dist/init-templates/aws-lambda/package.json +51 -0
  14. package/dist/init-templates/aws-lambda/scripts/ngrok.js +28 -0
  15. package/dist/init-templates/aws-lambda/src/index.ts +33 -0
  16. package/dist/init-templates/aws-lambda/src/lib/logging/index.ts +88 -0
  17. package/dist/init-templates/aws-lambda/src/lib/services/index.ts +41 -0
  18. package/dist/init-templates/aws-lambda/src/lib/state/redis/index.ts +64 -0
  19. package/dist/init-templates/aws-lambda/src/lib/state/seeka/installations.ts +67 -0
  20. package/dist/init-templates/aws-lambda/src/routes/seekaAppWebhook.ts +170 -0
  21. package/dist/init-templates/aws-lambda/tsconfig.json +31 -0
  22. package/dist/init-templates/azure-function/.eslintrc.cjs +10 -0
  23. package/dist/init-templates/azure-function/.example.gitignore +48 -0
  24. package/dist/init-templates/azure-function/.funcignore +17 -0
  25. package/dist/init-templates/azure-function/.gitlab-ci.yml +33 -0
  26. package/dist/init-templates/azure-function/.vscode/extensions.json +7 -0
  27. package/dist/init-templates/azure-function/.vscode/launch.json +13 -0
  28. package/dist/init-templates/azure-function/.vscode/settings.json +9 -0
  29. package/dist/init-templates/azure-function/.vscode/tasks.json +39 -0
  30. package/dist/init-templates/azure-function/README.md +102 -0
  31. package/dist/init-templates/azure-function/host.json +20 -0
  32. package/dist/init-templates/azure-function/local.settings.example.json +23 -0
  33. package/dist/init-templates/azure-function/package.json +44 -0
  34. package/dist/init-templates/azure-function/scripts/ngrok.js +28 -0
  35. package/dist/init-templates/azure-function/src/functions/pollingExample.ts +39 -0
  36. package/dist/init-templates/azure-function/src/functions/queueExample.ts +33 -0
  37. package/dist/init-templates/azure-function/src/functions/seekaAppWebhook.ts +200 -0
  38. package/dist/init-templates/azure-function/src/lib/jobs/index.ts +54 -0
  39. package/dist/init-templates/azure-function/src/lib/logging/index.ts +93 -0
  40. package/dist/init-templates/azure-function/src/lib/services/index.ts +41 -0
  41. package/dist/init-templates/azure-function/src/lib/state/redis/index.ts +64 -0
  42. package/dist/init-templates/azure-function/src/lib/state/seeka/installations.ts +67 -0
  43. package/dist/init-templates/azure-function/tsconfig.json +22 -0
  44. package/dist/init-templates/netlify-function/.env.example +18 -0
  45. package/dist/init-templates/netlify-function/.eslintrc.cjs +7 -0
  46. package/dist/init-templates/netlify-function/.example.gitignore +36 -0
  47. package/dist/init-templates/netlify-function/.vscode/launch.json +45 -0
  48. package/dist/init-templates/netlify-function/README.md +60 -0
  49. package/dist/init-templates/netlify-function/netlify.toml +7 -0
  50. package/dist/init-templates/netlify-function/package.json +38 -0
  51. package/dist/init-templates/netlify-function/src/api/example-job-background/index.ts +52 -0
  52. package/dist/init-templates/netlify-function/src/api/polling-example-job-scheduled/index.ts +46 -0
  53. package/dist/init-templates/netlify-function/src/api/seeka-app-webhook/index.ts +185 -0
  54. package/dist/init-templates/netlify-function/src/lib/jobs/index.ts +68 -0
  55. package/dist/init-templates/netlify-function/src/lib/logging/index.ts +91 -0
  56. package/dist/init-templates/netlify-function/src/lib/services/index.ts +41 -0
  57. package/dist/init-templates/netlify-function/src/lib/state/redis/index.ts +64 -0
  58. package/dist/init-templates/netlify-function/src/lib/state/seeka/installations.ts +67 -0
  59. package/dist/init-templates/netlify-function/tsconfig.json +25 -0
  60. package/package.json +48 -0
@@ -0,0 +1,185 @@
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, 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
+ try {
71
+ switch (body.type) {
72
+ case SeekaWebhookCallType.AppInstalled:
73
+ {
74
+ await onInstallation(body as SeekaAppInstalledWebhookPayload, logger);
75
+ break;
76
+ }
77
+ case SeekaWebhookCallType.AppInstallSettingsUpdated:
78
+ {
79
+ await onInstallationSettingsUpdate(body as SeekaAppInstallSettingsUpdatedWebhookPayload, logger);
80
+ break;
81
+ }
82
+ case SeekaWebhookCallType.AppUninstalled:
83
+ {
84
+ if (!body.isTest) {
85
+ const payload = body as SeekaAppUninstalledWebhookPayload;
86
+ await deleteInstallation(payload.context?.applicationInstallId as string, logger) // TODO: remove cast
87
+ }
88
+ break;
89
+ }
90
+ case SeekaWebhookCallType.ActivityAccepted:
91
+ {
92
+ const payload = body as SeekaActivityAcceptedWebhookPayload;
93
+ await handleSeekaActivity(payload, logger);
94
+
95
+ break;
96
+ }
97
+ case SeekaWebhookCallType.IdentityChanged:
98
+ {
99
+ const payload = body as SeekaIdentityChangedWebhookPayload;
100
+ logger.debug('Identity changed', { payload });
101
+
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ catch (err) {
107
+ logger.error('Failed to handle webhook', { ex: winston.exceptions.getAllInfo(err) });
108
+ return new Response(JSON.stringify({ error: "Request failed" }), { status: 500 })
109
+ }
110
+ finally {
111
+ // Close db connections
112
+ await stopServices(logger);
113
+ logger.profile('http.seeka.webhook.app')
114
+ logger.verbose('Seeka webhook handled');
115
+ }
116
+
117
+ return new Response(undefined, { status: 204 })
118
+ }
119
+
120
+
121
+ const onInstallation = async (payload: SeekaAppInstalledWebhookPayload, logger: Logger) => {
122
+ if (payload.isTest) return;
123
+
124
+ const installation = await createOrUpdateInstallation({
125
+ ...payload.context,
126
+ installationState: {
127
+ grantedPermissions: payload.content?.grantedPermissions || []
128
+ },
129
+ installationSettings: payload.content?.installationSettings || {}
130
+ } as any, logger) // TODO: remove any
131
+
132
+ // Trigger a sync for the installation
133
+ await triggerBackgroundJob(jobNames.exampleBackgroundJob, {
134
+ ...payload.context
135
+ }, logger)
136
+ }
137
+
138
+ const onInstallationSettingsUpdate = async (payload: SeekaAppInstallSettingsUpdatedWebhookPayload, logger: Logger) => {
139
+ if (payload.isTest) return;
140
+
141
+ const existingInstallation = await tryGetInstallation(payload.context?.applicationInstallId as string, true, logger) as SeekaAppInstallState;
142
+
143
+ // Update settings
144
+ const installation = await createOrUpdateInstallation({
145
+ ...payload.context,
146
+ ...existingInstallation,
147
+ installationState: {
148
+ ...existingInstallation.installationState,
149
+ grantedPermissions: payload.content?.grantedPermissions || []
150
+ },
151
+ installationSettings: payload.content?.installationSettings || {}
152
+ } as any, logger) // TODO: remove any
153
+
154
+ logger.info('Settings updated')
155
+ }
156
+
157
+ const handleSeekaActivity = async (activity: SeekaActivityAcceptedWebhookPayload, logger: Logger) => {
158
+ // const context = activity.context as SeekaAppWebhookContext;
159
+ // const helper = SeekaAppHelper.create(process.env['SEEKA_APP_SECRET'] as string, {
160
+ // organisationId: context.organisationId as string,
161
+ // applicationInstallId: context.applicationInstallId as string,
162
+ // applicationId: process.env['SEEKA_APP_ID'] as string,
163
+ // }, { name, version }, logger);
164
+
165
+ // // Append a first name to the identity
166
+ // await helper.api.mergeIdentity({
167
+ // seekaPId: activity.content?.personId,
168
+ // firstName: [
169
+ // 'firstname_' + new Date().getTime()
170
+ // ]
171
+ // }, {
172
+ // method: 'toremove',
173
+ // origin: TrackingEventSourceOriginType.Server
174
+ // })
175
+
176
+ // // Fire off a tracking event
177
+ // await helper.api.trackActivity({
178
+ // activityName: TrackingActivityNames.Custom,
179
+ // activityNameCustom: 'seeka-app-activity-accepted',
180
+ // activityId: 'act' + new Date().getTime(),
181
+ // }, activity.content?.personId as string, {
182
+ // method: 'toremove',
183
+ // origin: TrackingEventSourceOriginType.Server
184
+ // })
185
+ }
@@ -0,0 +1,68 @@
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
+ }
@@ -0,0 +1,91 @@
1
+ import * as winston from 'winston';
2
+
3
+ import { BackgroundJobRequestContext } from '@/lib/jobs';
4
+ import { SeqTransport } from '@datalust/winston-seq';
5
+ import { Context } from '@netlify/functions';
6
+
7
+ import packageJson from '../../../package.json';
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.cli(),
18
+ winston.format.errors({ stack: true }),
19
+ winston.format.splat(),
20
+ winston.format.cli()
21
+ ),
22
+ handleExceptions: true,
23
+ handleRejections: true,
24
+ }),
25
+ ]
26
+ if (process.env.LOGGING_SEQ_SERVERURL) {
27
+ loggerTransports.push(
28
+ new SeqTransport({
29
+ level: process.env.LOGGING_LEVEL,
30
+ serverUrl: process.env.LOGGING_SEQ_SERVERURL,
31
+ apiKey: process.env.LOGGING_SEQ_APIKEY,
32
+ onError: ((e: any) => { console.error('Failed to configure Seq logging transport', e) }),
33
+ format: winston.format.combine(
34
+ winston.format.errors({ stack: true }),
35
+ winston.format.json(),
36
+ ),
37
+ handleExceptions: true,
38
+ handleRejections: true,
39
+ })
40
+ )
41
+ }
42
+
43
+ export const logger = winston.createLogger({
44
+ level: process.env.LOGGING_LEVEL,
45
+ defaultMeta: {
46
+ seekaAppId: process.env.SEEKA_APP_ID,
47
+ seekaAppPackageName: packageJson.name,
48
+ seekaAppPackageVersion: packageJson.version,
49
+ Hosting_Provider: process.env.NETLIFY === 'true' ? 'netlify-aws' : undefined,
50
+ Release_Version: process.env.COMMIT_REF ? `sha-${process.env.COMMIT_REF}` : undefined,
51
+ Hosting_Region: process.env.AWS_REGION || undefined
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: Context) => {
60
+ const meta: any = {
61
+ seekaWebhookType: payload.type,
62
+ seekaWebhookIsTest: payload.isTest,
63
+ seekaWebhookRequestId: payload.requestId,
64
+ seekaWebhookCausationId: payload.causationId,
65
+ netlifyRequestId: functionContext.requestId,
66
+ netlifyDeployId: functionContext.deploy.id
67
+ }
68
+
69
+ const context = (payload as SeekaWebhookPayloadOfSeekaAppWebhookContext).context;
70
+ if (context) {
71
+ meta.seekaAppInstallId = context.applicationInstallId;
72
+ meta.seekaAppInstallOrganisationBrandId = context.organisationBrandId;
73
+ meta.seekaAppInstallOrganisationId = context.organisationId;
74
+ }
75
+
76
+ return childLogger(meta)
77
+ }
78
+
79
+ export const backgroundJobLogger = (jobName: string, jobContext: BackgroundJobRequestContext | undefined, functionContext: Context) => {
80
+ const meta: any = {
81
+ jobContext,
82
+ jobName,
83
+ netlifyRequestId: functionContext.requestId,
84
+ netlifyDeployId: functionContext.deploy.id,
85
+ seekaAppInstallId: jobContext?.applicationInstallId,
86
+ seekaAppInstallOrganisationBrandId: jobContext?.organisationBrandId,
87
+ seekaAppInstallOrganisationId: jobContext?.organisationId
88
+ }
89
+
90
+ return childLogger(meta)
91
+ }
@@ -0,0 +1,41 @@
1
+ import winston, { Logger } from 'winston';
2
+
3
+ import { connect, disconnect, isConnected } from '@/lib/state/redis';
4
+
5
+ export const startServices = async (logger: Logger) => {
6
+ logger.debug(`Trying to connect to Redis - ${process.env.REDIS_CONNECTION_HOST}`)
7
+ try {
8
+ if (isConnected()) {
9
+ logger.verbose(`Redis already connected - ${process.env.REDIS_CONNECTION_HOST}`)
10
+ }
11
+ else {
12
+ logger.profile('service.redis.connect')
13
+ await connect();
14
+ logger.profile('service.redis.connect')
15
+ logger.debug(`Redis connected - ${process.env.REDIS_CONNECTION_HOST}`)
16
+ }
17
+ }
18
+ catch (err) {
19
+ logger.error(`Failed to connect to Redis - ${process.env.REDIS_CONNECTION_HOST}`, { ex: winston.exceptions.getAllInfo(err) })
20
+ throw err;
21
+ }
22
+ }
23
+
24
+ export const stopServices = async (logger: Logger) => {
25
+ logger.debug(`Trying to disconnect from Redis - ${process.env.REDIS_CONNECTION_HOST}`)
26
+ try {
27
+ if (isConnected() === false) {
28
+ logger.verbose(`Redis already disconnected - ${process.env.REDIS_CONNECTION_HOST}`)
29
+ }
30
+ else {
31
+ logger.profile('service.redis.disconnect')
32
+ await disconnect();
33
+ logger.profile('service.redis.disconnect')
34
+ logger.verbose(`Redis disconnected - ${process.env.REDIS_CONNECTION_HOST}`)
35
+ }
36
+ }
37
+ catch (err) {
38
+ logger.error(`Failed to disconnect from Redis - ${process.env.REDIS_CONNECTION_HOST}`, { ex: winston.exceptions.getAllInfo(err) })
39
+ throw err;
40
+ }
41
+ }
@@ -0,0 +1,64 @@
1
+ import { createClient } from 'redis';
2
+
3
+ import { logger } from '@/lib/logging';
4
+
5
+ const redisProtocol = process.env.REDIS_CONNECTION_TLS === 'true' ? 'rediss://' : 'redis://';
6
+ const redisConn = `${redisProtocol}${process.env.REDIS_CONNECTION_USER}:${process.env.REDIS_CONNECTION_PASSWORD}@${process.env.REDIS_CONNECTION_HOST}:${process.env.REDIS_CONNECTION_PORT}`;
7
+
8
+ const redisClient = createClient({
9
+ url: redisConn
10
+ })
11
+ .on('error', err => logger.error('Redis Client ', { error: err }));
12
+
13
+ export const connect = async () => {
14
+ await redisClient.connect();
15
+ }
16
+
17
+ export const isConnected = () => {
18
+ return redisClient.isOpen;
19
+ }
20
+
21
+ export const disconnect = async () => {
22
+ await redisClient.disconnect();
23
+ }
24
+
25
+ const getKeyPrefix = (stateType: string) => `seeka:app:${process.env.SEEKA_APP_ID}:${stateType}`
26
+ const getKey = (stateType: string, key: string) => `${getKeyPrefix(stateType)}:${key}`
27
+
28
+ export async function getOrCreate<TState>(stateType: string, key: string, toCreate: TState): Promise<TState> {
29
+ const fullKey = getKey(stateType, key);
30
+ const existingStr = await redisClient.get(fullKey);
31
+ if (existingStr) return JSON.parse(existingStr);
32
+
33
+ await redisClient.set(fullKey, JSON.stringify(toCreate));
34
+
35
+ return toCreate;
36
+ }
37
+
38
+ export async function tryGet<TState>(stateType: string, key: string): Promise<TState | null> {
39
+ const fullKey = getKey(stateType, key);
40
+ const existingStr = await redisClient.get(fullKey);
41
+ if (existingStr) return JSON.parse(existingStr);
42
+
43
+ return null;
44
+ }
45
+
46
+ export async function getList<TState>(stateType: string): Promise<TState[]> {
47
+ const prefix = getKeyPrefix(stateType);
48
+ const allKeys = await redisClient.keys(`${prefix}:*`);
49
+ const listStr = await redisClient.mGet(allKeys);
50
+
51
+ if (listStr) return listStr.filter(e => Boolean(e)).map(e => JSON.parse(e as string));
52
+
53
+ return [];
54
+ }
55
+
56
+ export async function set<TState>(stateType: string, key: string, toCreate: TState): Promise<void> {
57
+ const fullKey = getKey(stateType, key);
58
+ await redisClient.set(fullKey, JSON.stringify(toCreate));
59
+ }
60
+
61
+ export async function remove(stateType: string, key: string): Promise<void> {
62
+ const fullKey = getKey(stateType, key);
63
+ await redisClient.del(fullKey);
64
+ }
@@ -0,0 +1,67 @@
1
+ import type { Logger } from 'winston';
2
+
3
+ import { getList, remove, set, tryGet } from '@/lib/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
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "skipLibCheck": true,
5
+ "strict": true,
6
+ "noEmit": true,
7
+ "esModuleInterop": true,
8
+ "module": "ESNext",
9
+ "moduleResolution": "Bundler",
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "paths": {
13
+ "@/*": [
14
+ "./src/*"
15
+ ]
16
+ }
17
+ },
18
+ "include": [
19
+ "**/*.ts",
20
+ "**/*.mts",
21
+ ],
22
+ "exclude": [
23
+ "node_modules"
24
+ ]
25
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@seeka-labs/cli-apps",
3
+ "version": "1.0.1",
4
+ "description": "Seeka - Apps CLI",
5
+ "author": "SEEKA <platform@seeka.co>",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "apps",
9
+ "seeka",
10
+ "node",
11
+ "azure",
12
+ "aws",
13
+ "netlify"
14
+ ],
15
+ "files": [
16
+ "dist/"
17
+ ],
18
+ "bin": "./dist/index.js",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "test": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 jest",
24
+ "build:ci": "yarn clean && tsc --noEmit && yarn build:templates && esbuild src/index.ts --bundle --minify --platform=node --target=node18 --outfile=dist/index.js",
25
+ "build": "yarn build:templates && tsc --noEmit && esbuild src/index.ts --bundle --sourcemap --platform=node --target=node18 --outfile=dist/index.js",
26
+ "build:templates": "node ./scripts/build-templates.js",
27
+ "watch": "tsc -w",
28
+ "clean": "yarn rimraf dist",
29
+ "dev": "yarn run clean && yarn build && node dist/index.js init seeka-app-test1 --template azure-function --email 'dev@seeka.co' --developer Seeka --noDependencies --force --env 'SEEKA_APP_ID=123' 'SEEKA_APP_SECRET=345' --packageManager pnpm"
30
+ },
31
+ "devDependencies": {
32
+ "@jest/globals": "^29.7.0",
33
+ "@types/cross-spawn": "^6.0.6",
34
+ "@types/jest": "^29.5.12",
35
+ "@types/memory-cache": "^0.2.5",
36
+ "@types/node": "^18.14.0",
37
+ "commander": "^12.0.0",
38
+ "cross-env": "^7.0.3",
39
+ "cross-spawn": "^7.0.3",
40
+ "esbuild": "^0.20.0",
41
+ "jest": "^29.7.0",
42
+ "nodemon": "^3.0.3",
43
+ "ts-jest": "^29.1.2",
44
+ "ts-node": "^10.9.2",
45
+ "typescript": "^5.3.3"
46
+ },
47
+ "gitHead": "bd153ede29757d814a114f97953a587b62f0de18"
48
+ }