@tellescope/sdk 0.0.20 → 0.0.24

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/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "@tellescope/sdk",
3
- "version": "0.0.20",
3
+ "version": "0.0.24",
4
4
  "description": "Code for interacting with the Tellescope API",
5
5
  "main": "./lib/cjs/sdk.js",
6
6
  "module": "./lib/esm/sdk.js",
7
7
  "types": "./lib/esm/sdk.d.ts",
8
8
  "scripts": {
9
9
  "test": "npm run-script build:cjs && npm run-script retest",
10
- "retest": "npm run-script retest-api && npm run-script retest-public-api && npm run-script retest-sockets",
10
+ "retest": "npm run-script retest-api && npm run-script retest-public-api && npm run-script retest-webhooks && npm run-script retest-sockets",
11
11
  "test-api": "npm run-script build:cjs && npm run-script retest-api",
12
12
  "retest-api": "node lib/cjs/tests/tests.js",
13
13
  "test-public-api": "npm run-script build:cjs && npm run-script retest-public-api",
14
14
  "retest-public-api": "node lib/cjs/tests/public_endpoint_tests.js",
15
15
  "test-sockets": "npm run-script build:cjs && npm run-script retest-sockets",
16
16
  "retest-sockets": "node lib/cjs/tests/socket_tests.js",
17
+ "test-webhooks": "npm run-script build:cjs && npm run-script retest-webhooks",
18
+ "retest-webhooks": "node lib/cjs/tests/webhooks_tests.js",
17
19
  "build": "npm run-script build:esm && npm run-script build:cjs",
18
20
  "build:esm": "tsc",
19
21
  "build:cjs": "tsc --module commonjs --outDir lib/cjs",
@@ -30,15 +32,16 @@
30
32
  },
31
33
  "homepage": "https://github.com/tellescope-os/tellescope#readme",
32
34
  "dependencies": {
33
- "@tellescope/constants": "^0.0.20",
34
- "@tellescope/schema": "^0.0.20",
35
- "@tellescope/testing": "^0.0.20",
36
- "@tellescope/types-client": "^0.0.20",
37
- "@tellescope/types-models": "^0.0.20",
38
- "@tellescope/types-utilities": "^0.0.20",
39
- "@tellescope/utilities": "^0.0.20",
40
- "@tellescope/validation": "^0.0.20",
35
+ "@tellescope/constants": "^0.0.24",
36
+ "@tellescope/schema": "^0.0.24",
37
+ "@tellescope/testing": "^0.0.24",
38
+ "@tellescope/types-client": "^0.0.24",
39
+ "@tellescope/types-models": "^0.0.24",
40
+ "@tellescope/types-utilities": "^0.0.24",
41
+ "@tellescope/utilities": "^0.0.24",
42
+ "@tellescope/validation": "^0.0.24",
41
43
  "axios": "^0.21.1",
44
+ "express": "^4.17.1",
42
45
  "form-data": "^4.0.0",
43
46
  "socket.io-client": "^4.2.0",
44
47
  "source-map-support": "^0.5.20"
@@ -53,5 +56,5 @@
53
56
  "publishConfig": {
54
57
  "access": "public"
55
58
  },
56
- "gitHead": "99ef9475f430b7021166b8f47f9863c2e1fed8aa"
59
+ "gitHead": "b43edb136f319f3afac336237c7bcb030bb3c60d"
57
60
  }
package/src/enduser.ts CHANGED
@@ -6,14 +6,16 @@ import { url_safe_path } from "@tellescope/utilities"
6
6
 
7
7
  import { S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
8
8
  import {
9
+ Attendee,
9
10
  AttendeeInfo,
10
- Meeting,
11
11
  } from "@tellescope/types-models"
12
12
  import {
13
13
  ClientModelForName,
14
14
  ClientModelForName_required,
15
15
  Enduser,
16
16
  File,
17
+ Meeting,
18
+ UserDisplayInfo,
17
19
  } from "@tellescope/types-client"
18
20
 
19
21
  export interface EnduserSessionOptions extends SessionOptions {}
@@ -43,14 +45,14 @@ type EnduserQueries = { [K in EnduserAccessibleModels]: APIQuery<K> } & {
43
45
  logout: () => Promise<void>;
44
46
  },
45
47
  users: {
46
- display_info: () => Promise<{ fname?: string, lname?: string, id: string, lastActive?: Date, lastLogout?: Date }[]>
48
+ display_info: () => Promise<UserDisplayInfo[]>
47
49
  },
48
50
  files: {
49
51
  prepare_file_upload: (args: { name: string, size: number, type: string }) => Promise<{ presignedUpload: S3PresignedPost, file: File }>,
50
52
  file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
51
53
  },
52
54
  meetings: {
53
- attendee_info: (args: { id: string }) => Promise<{ attendee: AttendeeInfo, others: UserIdentity[] }>,
55
+ attendee_info: (args: { id: string }) => Promise<{ attendee: Attendee, others: UserIdentity[] }>,
54
56
  my_meetings: () => Promise<Meeting[]>,
55
57
  },
56
58
  }
@@ -78,7 +80,7 @@ export class EnduserSession extends Session {
78
80
  logout: () => this._POST('/v1/logout-enduser'),
79
81
  }
80
82
  this.api.users = {
81
- display_info: () => this._GET<{}, { fname: string, lname: string, id: string }[] >(`/v1/user-display-info`),
83
+ display_info: () => this._GET<{}, UserDisplayInfo[] >(`/v1/user-display-info`),
82
84
  }
83
85
  this.api.meetings = {
84
86
  attendee_info: a => this._GET('/v1/attendee-info', a),
package/src/sdk.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  AttendeeInfo,
3
3
  JourneyState,
4
- Meeting,
5
4
  UserSession,
6
5
  MeetingInfo,
7
6
  ReadFilter,
7
+ WebhookSubscriptionsType,
8
+ Attendee,
8
9
  } from "@tellescope/types-models"
9
10
 
10
11
  import {
@@ -15,6 +16,7 @@ import {
15
16
  ChatRoom,
16
17
  Enduser,
17
18
  File,
19
+ Meeting,
18
20
  } from "@tellescope/types-client"
19
21
  import { CustomUpdateOptions, SortOption, S3PresignedPost, UserIdentity } from "@tellescope/types-utilities"
20
22
  import { url_safe_path } from "@tellescope/utilities"
@@ -82,6 +84,7 @@ const loadDefaultQueries = (s: Session): { [K in keyof ClientModelForName] : API
82
84
  tickets: defaultQueries(s, 'tickets'),
83
85
  meetings: defaultQueries(s, 'meetings'),
84
86
  notes: defaultQueries(s, 'notes'),
87
+ webhooks: defaultQueries(s, 'webhooks')
85
88
  })
86
89
 
87
90
  type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
@@ -101,15 +104,19 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
101
104
  file_download_URL: (args: { secureName: string }) => Promise<{ downloadURL: string }>,
102
105
  },
103
106
  meetings: {
104
- start_meeting: () => Promise<{ id: string, meeting: { Meeting: MeetingInfo }, host: { Attendee: AttendeeInfo } }>,
107
+ start_meeting: () => Promise<{ id: string, meeting: { Meeting: MeetingInfo }, host: Attendee }>,
105
108
  end_meeting: (args: { id: string }) => Promise<void>,
106
109
  add_attendees_to_meeting: (args: { id: string, attendees: UserIdentity[] }) => Promise<void>,
107
110
  my_meetings: () => Promise<Meeting[]>,
108
- attendee_info: (args: { id: string }) => Promise<{ attendee: AttendeeInfo, others: UserIdentity[] }>,
111
+ attendee_info: (args: { id: string }) => Promise<{ attendee: Attendee, others: UserIdentity[] }>,
109
112
  },
110
113
  chat_rooms: {
111
114
  join_room: (args: { id: string }) => Promise<{ room: ChatRoom }>,
112
115
  },
116
+ webhooks: {
117
+ configure: (args: { url: string, secret: string, subscriptions?: WebhookSubscriptionsType }) => Promise<void>,
118
+ update: (args: { url?: string, secret?: string, subscriptionUpdates?: WebhookSubscriptionsType }) => Promise<void>
119
+ },
113
120
  }
114
121
 
115
122
  export class Session extends SessionManager {
@@ -137,6 +144,9 @@ export class Session extends SessionManager {
137
144
  queries.meetings.attendee_info = a => this._GET('/v1/attendee-info', a)
138
145
  queries.meetings.my_meetings = () => this._GET('/v1/my-meetings')
139
146
 
147
+ queries.webhooks.configure = a => this._POST('/v1/configure-webhooks', a)
148
+ queries.webhooks.update = a => this._PATCH('/v1/update-webhooks', a)
149
+
140
150
  this.api = queries
141
151
 
142
152
  // if (this.userInfo) this.refresh_session()
@@ -973,47 +973,48 @@ const enduserAccessTests = async () => {
973
973
  for (const n in schema) {
974
974
  const endpoint = url_safe_path(n)
975
975
  const model = schema[n as keyof typeof schema]
976
+ if (n === 'webhooks') continue // no default endpoints implemented
976
977
 
977
978
  if (!model?.enduserActions?.read && (model.defaultActions.read || model.customActions.read)) {
978
979
  await async_test(
979
980
  `no-enduser-access getOne (${endpoint})`,
980
981
  () => enduserSDK.GET(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
981
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
982
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
982
983
  )
983
984
  }
984
985
  if (!model.enduserActions?.readMany && (model.defaultActions.readMany || model.customActions.readMany)) {
985
986
  await async_test(
986
987
  `no-enduser-access getSome (${endpoint})`,
987
988
  () => enduserSDK.GET(`/v1/${endpoint}`),
988
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
989
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
989
990
  )
990
991
  }
991
992
  if (!model.enduserActions?.create && (model.defaultActions.create || model.customActions.create)) {
992
993
  await async_test(
993
994
  `no-enduser-access createOne (${endpoint})`,
994
995
  () => enduserSDK.POST(`/v1/${endpoint.substring(0, endpoint.length - 1)}`),
995
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
996
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
996
997
  )
997
998
  }
998
999
  if (!model.enduserActions?.createMany && (model.defaultActions.createMany || model.customActions.createMany)) {
999
1000
  await async_test(
1000
1001
  `no-enduser-access createMany (${endpoint})`,
1001
1002
  () => enduserSDK.POST(`/v1/${endpoint}`),
1002
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
1003
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
1003
1004
  )
1004
1005
  }
1005
1006
  if (!model.enduserActions?.update && (model.defaultActions.update || model.customActions.update)) {
1006
1007
  await async_test(
1007
1008
  `no-enduser-access update (${endpoint})`,
1008
1009
  () => enduserSDK.PATCH(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
1009
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
1010
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
1010
1011
  )
1011
1012
  }
1012
1013
  if (!model.enduserActions?.delete && (model.defaultActions.delete || model.customActions.delete)) {
1013
1014
  await async_test(
1014
1015
  `no-enduser-access delete (${endpoint})`,
1015
1016
  () => enduserSDK.DELETE(`/v1/${endpoint.substring(0, endpoint.length - 1)}/:id`),
1016
- { shouldError: true, onError: (e: string) => e === 'Unauthenticated' }
1017
+ { shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.message === 'This action is not allowed' }
1017
1018
  )
1018
1019
  }
1019
1020
  }
@@ -1060,6 +1061,20 @@ const files_tests = async () => {
1060
1061
  assert(downloaded === buff.toString(), 'downloaded file does not match uploaded file', 'upload, download comparison')
1061
1062
  }
1062
1063
 
1064
+ const enduser_session_tests = async () => {
1065
+ const email = 'enduser@tellescope.com'
1066
+ const password = 'testpassword'
1067
+
1068
+ const enduser = await sdk.api.endusers.createOne({ email })
1069
+ await sdk.api.endusers.set_password({ id: enduser.id, password }).catch(console.error)
1070
+ await enduserSDK.authenticate(email, password).catch(console.error)
1071
+
1072
+ const users = await enduserSDK.api.users.display_info()
1073
+ assert(users && users.length > 0, 'No users returned', 'Get user display info for enduser')
1074
+
1075
+ await sdk.api.endusers.deleteOne(enduser.id)
1076
+ }
1077
+
1063
1078
  const tests: { [K in keyof ClientModelForName]: () => void } = {
1064
1079
  chats: chat_tests,
1065
1080
  endusers: enduser_tests,
@@ -1076,6 +1091,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1076
1091
  tickets: () => {},
1077
1092
  meetings: () => {},
1078
1093
  notes: () => {},
1094
+ webhooks: () => {},
1079
1095
  };
1080
1096
 
1081
1097
  (async () => {
@@ -1091,6 +1107,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1091
1107
  await threadKeyTests()
1092
1108
  await enduserAccessTests()
1093
1109
  await generateEnduserAuthTests()
1110
+ await enduser_session_tests()
1094
1111
  } catch(err) {
1095
1112
  console.error("Failed during custom test")
1096
1113
  console.error(err)
@@ -1107,7 +1124,7 @@ const tests: { [K in keyof ClientModelForName]: () => void } = {
1107
1124
  model: schema[n] as any,
1108
1125
  name: n,
1109
1126
  returns: {
1110
- create: returnValidation as ModelFields<ClientModel>,
1127
+ create: returnValidation as any// ModelFields<ClientModel>,
1111
1128
  }
1112
1129
  })
1113
1130
  }
@@ -0,0 +1,207 @@
1
+ import express from "express"
2
+ import bodyParser from 'body-parser'
3
+ import crypto from "crypto"
4
+
5
+ import {
6
+ assert,
7
+ async_test,
8
+ log_header,
9
+ wait,
10
+ } from "@tellescope/testing"
11
+ import {
12
+ objects_equivalent
13
+ } from "@tellescope/utilities"
14
+
15
+ import {
16
+ WEBHOOK_MODELS,
17
+ WebhookSupportedModel,
18
+ WebhookRecord,
19
+ WebhookCall,
20
+ CUDSubscription,
21
+ } from "@tellescope/types-models"
22
+
23
+ import { Session } from "../sdk"
24
+ import { ChatMessage } from "@tellescope/types-client"
25
+
26
+ const [email, password] = [process.env.TEST_EMAIL, process.env.TEST_PASSWORD]
27
+ const [email2, password2] = [process.env.TEST_EMAIL_2, process.env.TEST_PASSWORD_2]
28
+ const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
29
+ if (!(email && password && email2 && password2 && nonAdminEmail && nonAdminPassword)) {
30
+ console.error("Set TEST_EMAIL and TEST_PASSWORD")
31
+ process.exit()
32
+ }
33
+
34
+
35
+ const sdk = new Session({ host: 'http://localhost:8080' })
36
+ const nonAdminSdk = new Session({ host: 'http://localhost:8080' })
37
+
38
+ const app = express()
39
+ app.use(bodyParser.urlencoded({ extended: true, limit: '25mb' }))
40
+ app.use(bodyParser.json({ limit: "25mb" }))
41
+
42
+ const PORT = 4000
43
+ const TEST_SECRET = "this is a test secret for verifying integrity of web hooks"
44
+ const webhookEndpoint = '/handle-webhook'
45
+ const webhookURL = `http://127.0.0.1:${PORT}${webhookEndpoint}`
46
+
47
+ const sha256 = (s: string) => crypto.createHash('sha256').update(s).digest('hex')
48
+
49
+ const verify_integrity = (records: WebhookRecord[], timestamp: string, integrity: string,) => (
50
+ sha256(records.map(r => r.id).join('') + timestamp + TEST_SECRET) === integrity
51
+ )
52
+
53
+ const handledEvents: WebhookCall[] = []
54
+ app.post(webhookEndpoint, (req, res) => {
55
+ const body = req.body as WebhookCall
56
+ // console.log('got hook', body.records, body.timestamp, body.integrity)
57
+
58
+ if (!verify_integrity(body.records, body.timestamp, body.integrity)) {
59
+ console.error("Integrity check failed for request", JSON.stringify(body, null, 2))
60
+ process.exit()
61
+ }
62
+
63
+ handledEvents.push(req.body)
64
+ res.status(204).end()
65
+ })
66
+
67
+ const fullSubscription = {} as { [K in WebhookSupportedModel]: CUDSubscription }
68
+ const emptySubscription = {} as { [K in WebhookSupportedModel]: CUDSubscription }
69
+ for (const model in WEBHOOK_MODELS) {
70
+ fullSubscription[model as WebhookSupportedModel] = { create: true, update: true, delete: true }
71
+ emptySubscription[model as WebhookSupportedModel] = { create: false, update: false, delete: false }
72
+ }
73
+
74
+ let webhookIndex = 0
75
+ const check_next_webhook = async (evaluate: (hook: WebhookCall) => boolean, name: string, error: string, isSubscribed: boolean) => {
76
+ if (isSubscribed === false) return
77
+
78
+ await wait(undefined, 25) // wait for hook to post
79
+
80
+ const event = handledEvents[webhookIndex]
81
+ assert(!!event, 'did not get hook', 'got hook')
82
+ if (!event) return // ensure webhookIndex not incremented
83
+
84
+ const success = evaluate(event)
85
+ assert(success, error, name)
86
+ if (!success) { console.error('Got', event) }
87
+
88
+ webhookIndex++
89
+ }
90
+
91
+ const chats_tests = async (isSubscribed: boolean) => {
92
+ log_header(`Chats Tests, isSubscribed=${isSubscribed}`)
93
+ const room = await sdk.api.chat_rooms.createOne({ userIds: [sdk.userInfo.id] })
94
+
95
+ const chat = await sdk.api.chats.createOne({ roomId: room.id, message: "Hello hello hi hello" })
96
+ await check_next_webhook(
97
+ ({ records, relatedRecords }) => {
98
+ const record = records[0] as ChatMessage
99
+
100
+ return (
101
+ objects_equivalent(record, chat) &&
102
+ relatedRecords[record.roomId] !== undefined &&
103
+ relatedRecords[record.senderId as string] !== undefined &&
104
+ relatedRecords[record.roomId]?.id === room.id &&
105
+ relatedRecords[record.senderId as string]?.id === room.userIds?.[0]
106
+ )
107
+ },
108
+ 'Create chat error', 'Create chat webhook', isSubscribed
109
+ )
110
+
111
+ // cleanup
112
+ await sdk.api.chat_rooms.deleteOne(room.id) // also cleans up messages
113
+
114
+ // when chatroom support added for webhooks, check deletion here
115
+ // await check_next_webhook(a => objects_equivalent(a.records, [chat_room]), 'Delete chat room error', 'Delete chat room webhook', isSubscribed)
116
+ }
117
+
118
+ const meetings_tests = async (isSubscribed: boolean) => {
119
+ log_header(`Meetings Tests, isSubscribed=${isSubscribed}`)
120
+ const meeting = await sdk.api.meetings.start_meeting()
121
+
122
+ await check_next_webhook(a => objects_equivalent(a.records, [meeting]), 'Create meeting error', 'Create meeting webhook', isSubscribed)
123
+
124
+ // cleanup
125
+ await sdk.api.meetings.end_meeting({ id: meeting.id }) // also cleans up messages
126
+ }
127
+
128
+ const tests: { [K in WebhookSupportedModel]: (isSubscribed: boolean) => Promise<void> } = {
129
+ chats: chats_tests,
130
+ meetings: meetings_tests,
131
+ }
132
+
133
+ const run_tests = async () => {
134
+ log_header("Webhooks Tests")
135
+ await sdk.authenticate(email, password)
136
+ await sdk.reset_db()
137
+ await nonAdminSdk.authenticate(nonAdminEmail, nonAdminPassword)
138
+
139
+ await async_test(
140
+ 'configure webhook is admin only',
141
+ () => nonAdminSdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
142
+ { shouldError: true, onError: e => e.message === "Inaccessible" || e.message === "Admin access only"}
143
+ )
144
+ await async_test(
145
+ 'update webhook is admin only',
146
+ () => nonAdminSdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
147
+ { shouldError: true, onError: e => e.message === "Inaccessible" || e.message === "Admin access only"}
148
+ )
149
+
150
+ await async_test(
151
+ 'configure webhook',
152
+ () => sdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
153
+ { onResult: _ => true }
154
+ )
155
+ await async_test(
156
+ 'configure webhook (only callable once)',
157
+ () => sdk.api.webhooks.configure({ url: webhookURL, secret: TEST_SECRET }),
158
+ { shouldError: true, onError: e => e.message === "Only one webhook configuration is supported per organization. Use /update-webooks to update your configuration." }
159
+ )
160
+ await async_test(
161
+ 'update webhook (set empty subscription)',
162
+ () => sdk.api.webhooks.update({ subscriptionUpdates: {} }),
163
+ { onResult: _ => true }
164
+ )
165
+ await async_test(
166
+ 'update webhook (set partial subscription)',
167
+ () => sdk.api.webhooks.update({ subscriptionUpdates: { chats: { create: true }} }),
168
+ { onResult: _ => true }
169
+ )
170
+ await async_test(
171
+ 'update webhook (set subscriptions)',
172
+ () => sdk.api.webhooks.update({ subscriptionUpdates: fullSubscription }),
173
+ { onResult: _ => true }
174
+ )
175
+ await async_test(
176
+ 'update webhook invalid model',
177
+ () => sdk.api.webhooks.update({ subscriptionUpdates: { notAModel: { create: false } } as any }),
178
+ { shouldError: true, onError: e => e.message === "Error parsing field subscriptionUpdates: Got unexpected field(s) [notAModel]" }
179
+ )
180
+
181
+ log_header("Webhooks Tests with Subscriptions")
182
+ for (const t in tests) {
183
+ await tests[t as keyof typeof tests](true)
184
+ }
185
+ const finalLength = handledEvents.length
186
+
187
+ await async_test(
188
+ 'update webhook (set subscriptions empty)',
189
+ () => sdk.api.webhooks.update({ subscriptionUpdates: emptySubscription }),
190
+ { onResult: _ => true }
191
+ )
192
+
193
+ log_header("Webhooks Tests without Subscriptions")
194
+ for (const t in tests) {
195
+ await tests[t as keyof typeof tests](false)
196
+ }
197
+ assert(finalLength === handledEvents.length, 'length changed after subscriptions', 'No webhooks posted when no subscription')
198
+
199
+ }
200
+
201
+ app.listen(PORT, async () => {
202
+ try {
203
+ await run_tests()
204
+ } catch(err) { console.error(err) }
205
+
206
+ process.exit()
207
+ })