@tellescope/sdk 1.255.16 → 1.255.18
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/lib/cjs/sdk.d.ts +1 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/organization_api_keys.test.js +2 -2
- package/lib/cjs/tests/api_tests/organization_api_keys.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js +414 -0
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +147 -143
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +1 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/organization_api_keys.test.js +2 -2
- package/lib/esm/tests/api_tests/organization_api_keys.test.js.map +1 -1
- package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js +407 -0
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +147 -143
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/sdk.ts +3 -0
- package/src/tests/api_tests/organization_api_keys.test.ts +2 -2
- package/src/tests/api_tests/scoped_api_keys.test.ts +319 -0
- package/src/tests/tests.ts +2 -0
- package/test_generated.pdf +0 -0
|
@@ -12,7 +12,7 @@ import { setup_tests } from "../setup"
|
|
|
12
12
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
13
|
|
|
14
14
|
// fields that the admin org-wide list endpoint is allowed to return
|
|
15
|
-
const ALLOWED_FIELDS = ['id', 'creator', 'businessId', 'updatedAt'].sort()
|
|
15
|
+
const ALLOWED_FIELDS = ['id', 'creator', 'businessId', 'updatedAt', 'scopes'].sort()
|
|
16
16
|
|
|
17
17
|
export const organization_api_keys_tests = async (
|
|
18
18
|
{ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }
|
|
@@ -104,7 +104,7 @@ export const organization_api_keys_tests = async (
|
|
|
104
104
|
assert(
|
|
105
105
|
offending === undefined,
|
|
106
106
|
`a returned record exposed non-allowlisted fields: ${JSON.stringify(offending)}`,
|
|
107
|
-
"returned records contain only allowlisted fields (subset of id, creator, businessId, updatedAt)"
|
|
107
|
+
"returned records contain only allowlisted fields (subset of id, creator, businessId, updatedAt, scopes)"
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
// =============================================
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import axios from "axios"
|
|
4
|
+
import { Session } from "../../sdk"
|
|
5
|
+
import {
|
|
6
|
+
assert,
|
|
7
|
+
async_test,
|
|
8
|
+
log_header,
|
|
9
|
+
wait,
|
|
10
|
+
} from "@tellescope/testing"
|
|
11
|
+
import { setup_tests } from "../setup"
|
|
12
|
+
import { FORM_INGESTION_API_KEY_SCOPE } from "@tellescope/types-models"
|
|
13
|
+
|
|
14
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
15
|
+
|
|
16
|
+
const TEST_EMAIL = "scoped-api-keys@tellescope.com"
|
|
17
|
+
|
|
18
|
+
export const scoped_api_keys_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
19
|
+
log_header("Scoped API Keys (form-ingestion)")
|
|
20
|
+
|
|
21
|
+
const form = await sdk.api.forms.createOne({ title: "Scoped API Key Test Form" })
|
|
22
|
+
|
|
23
|
+
const buildUrl = (pathSegment: string) => {
|
|
24
|
+
const url = new URL(`${host}/v1/webhooks/form-ingestion/${pathSegment}`)
|
|
25
|
+
url.searchParams.set('formId', form.id)
|
|
26
|
+
url.searchParams.set('returnJSON', 'true')
|
|
27
|
+
return url.toString()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bodyFor = (responder_uuid: string) => ({
|
|
31
|
+
answers: [{ key: 'email', value: TEST_EMAIL }],
|
|
32
|
+
responder_uuid,
|
|
33
|
+
finalized: true,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const keyIds: string[] = []
|
|
37
|
+
const formResponseIds: string[] = []
|
|
38
|
+
const fileIds: string[] = []
|
|
39
|
+
let throwawayUserId: string | undefined
|
|
40
|
+
let throwawayRoleId: string | undefined
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// =============================================
|
|
44
|
+
// (a) Admin creates a scoped key
|
|
45
|
+
// =============================================
|
|
46
|
+
const scoped = await sdk.api.api_keys.createOne({ scopes: [FORM_INGESTION_API_KEY_SCOPE] })
|
|
47
|
+
keyIds.push(scoped.id)
|
|
48
|
+
assert(!!scoped.id && !!scoped.key, "scoped key create returned no id/key", "admin creates a form-ingestion-scoped key")
|
|
49
|
+
|
|
50
|
+
// record reflects the scope for its creator (CREATOR_ONLY_ACCESS read)
|
|
51
|
+
await async_test(
|
|
52
|
+
"scoped key record stores scopes",
|
|
53
|
+
() => sdk.api.api_keys.getOne(scoped.id),
|
|
54
|
+
{ onResult: k => JSON.stringify(k.scopes) === JSON.stringify([FORM_INGESTION_API_KEY_SCOPE]) },
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// =============================================
|
|
58
|
+
// (b) Scoped key is rejected on all general /v1 auth
|
|
59
|
+
// =============================================
|
|
60
|
+
await async_test(
|
|
61
|
+
"scoped key rejected on /v1/test-authenticated",
|
|
62
|
+
() => new Session({ host, apiKey: scoped.key }).test_authenticated(),
|
|
63
|
+
// SDK parseError surfaces a raw 401 body as the string 'Unauthenticated'
|
|
64
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.statusCode === 401 || /unauthenticated/i.test(e?.message || '') },
|
|
65
|
+
)
|
|
66
|
+
await async_test(
|
|
67
|
+
"scoped key rejected on GET /v1/endusers",
|
|
68
|
+
() => axios.get(`${host}/v1/endusers`, { headers: { Authorization: `API_KEY ${scoped.key}` } }),
|
|
69
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 },
|
|
70
|
+
)
|
|
71
|
+
await async_test(
|
|
72
|
+
"scoped key rejected on cross-org targeting (x-tellescope-organization)",
|
|
73
|
+
() => axios.get(`${host}/v1/endusers`, {
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `API_KEY ${scoped.key}`,
|
|
76
|
+
'x-tellescope-organization': sdk.userInfo.businessId,
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 },
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// =============================================
|
|
83
|
+
// (c) Scoped key IS accepted by the form-ingestion webhook
|
|
84
|
+
// =============================================
|
|
85
|
+
const headerResult = await axios.post(
|
|
86
|
+
buildUrl('key-in-header'),
|
|
87
|
+
bodyFor("scoped-1"),
|
|
88
|
+
{ headers: { Authorization: `API_KEY ${scoped.key}` } },
|
|
89
|
+
)
|
|
90
|
+
assert(
|
|
91
|
+
headerResult.status === 200 && !!headerResult.data?.formResponseId,
|
|
92
|
+
`scoped key header auth failed: ${headerResult.status} ${JSON.stringify(headerResult.data)}`,
|
|
93
|
+
"scoped key authenticates the form-ingestion webhook via header",
|
|
94
|
+
)
|
|
95
|
+
if (headerResult.data?.formResponseId) formResponseIds.push(headerResult.data.formResponseId)
|
|
96
|
+
|
|
97
|
+
const pathResult = await axios.post(buildUrl(scoped.key), bodyFor("scoped-2"))
|
|
98
|
+
assert(
|
|
99
|
+
pathResult.status === 200 && !!pathResult.data?.formResponseId,
|
|
100
|
+
`scoped key path auth failed: ${pathResult.status} ${JSON.stringify(pathResult.data)}`,
|
|
101
|
+
"scoped key authenticates the form-ingestion webhook via path segment",
|
|
102
|
+
)
|
|
103
|
+
if (pathResult.data?.formResponseId) formResponseIds.push(pathResult.data.formResponseId)
|
|
104
|
+
|
|
105
|
+
// =============================================
|
|
106
|
+
// (c2) Scoped key CAN run the file-upload flow (prepare + confirm) — needed for intake forms with files
|
|
107
|
+
// =============================================
|
|
108
|
+
const scopedSession = new Session({ host, apiKey: scoped.key })
|
|
109
|
+
const { file: uploadedFile } = await scopedSession.api.files.prepare_file_upload({
|
|
110
|
+
name: 'scoped-intake-file.txt', size: 12, type: 'text/plain',
|
|
111
|
+
})
|
|
112
|
+
assert(
|
|
113
|
+
!!uploadedFile?.id,
|
|
114
|
+
"scoped key prepare_file_upload returned no file",
|
|
115
|
+
"scoped key can call POST /prepare-file-upload",
|
|
116
|
+
)
|
|
117
|
+
if (uploadedFile?.id) fileIds.push(uploadedFile.id)
|
|
118
|
+
|
|
119
|
+
await async_test(
|
|
120
|
+
"scoped key can call POST /files/confirm-upload",
|
|
121
|
+
() => scopedSession.api.files.confirm_file_upload({ id: uploadedFile.id }),
|
|
122
|
+
{ onResult: () => true },
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// ...but is blocked from EVERY other endpoint in the files domain — proving the two allowed paths are the
|
|
126
|
+
// ONLY files access, and that the '/files/confirm-upload' allow-prefix does not open the '/files/*' namespace.
|
|
127
|
+
const blockedFileEndpoints: [string, () => Promise<any>][] = [
|
|
128
|
+
['GET /v1/files (readMany)', () => axios.get(`${host}/v1/files`, { headers: { Authorization: `API_KEY ${scoped.key}` } })],
|
|
129
|
+
['GET /v1/file/:id (read one)', () => axios.get(`${host}/v1/file/000000000000000000000000`, { headers: { Authorization: `API_KEY ${scoped.key}` } })],
|
|
130
|
+
['DELETE /v1/file/:id (destructive)', () => axios.delete(`${host}/v1/file/000000000000000000000000`, { headers: { Authorization: `API_KEY ${scoped.key}` } })],
|
|
131
|
+
['POST /v1/files/send-fax (/files/* sibling)', () => axios.post(`${host}/v1/files/send-fax`, {}, { headers: { Authorization: `API_KEY ${scoped.key}` } })],
|
|
132
|
+
['POST /v1/files/ocr (/files/* sibling)', () => axios.post(`${host}/v1/files/ocr`, {}, { headers: { Authorization: `API_KEY ${scoped.key}` } })],
|
|
133
|
+
['POST /v1/files/push (/files/* sibling)', () => axios.post(`${host}/v1/files/push`, {}, { headers: { Authorization: `API_KEY ${scoped.key}` } })],
|
|
134
|
+
]
|
|
135
|
+
for (const [label, call] of blockedFileEndpoints) {
|
|
136
|
+
await async_test(
|
|
137
|
+
`scoped key blocked on ${label}`,
|
|
138
|
+
call,
|
|
139
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 || e.response?.status === 403 },
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================
|
|
144
|
+
// (d) Non-admin cannot create a scoped key (throwaway user, never mutate existing roles)
|
|
145
|
+
// =============================================
|
|
146
|
+
const apiKeysRole = await sdk.api.role_based_access_permissions.createOne({
|
|
147
|
+
role: 'scoped-api-keys-test-role',
|
|
148
|
+
permissions: { api_keys: { create: 'All', read: 'All', update: 'All', delete: 'All' } },
|
|
149
|
+
})
|
|
150
|
+
throwawayRoleId = apiKeysRole.id
|
|
151
|
+
|
|
152
|
+
const throwawayUser = (
|
|
153
|
+
await sdk.api.users.getOne({ email: 'scoped.api.keys.user@tellescope.com' }).catch(() => null)
|
|
154
|
+
) || (
|
|
155
|
+
await sdk.api.users.createOne({ email: 'scoped.api.keys.user@tellescope.com', notificationEmailsDisabled: true, verifiedEmail: true })
|
|
156
|
+
)
|
|
157
|
+
throwawayUserId = throwawayUser.id
|
|
158
|
+
await sdk.api.users.updateOne(throwawayUser.id, { roles: [apiKeysRole.role] }, { replaceObjectFields: true })
|
|
159
|
+
await wait(undefined, 2000) // wait for the role change to propagate before authenticating
|
|
160
|
+
|
|
161
|
+
const otherSdk = new Session({
|
|
162
|
+
host,
|
|
163
|
+
authToken: (await sdk.api.users.generate_auth_token({ id: throwawayUser.id })).authToken,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await async_test(
|
|
167
|
+
"non-admin cannot create a scoped key (401)",
|
|
168
|
+
() => otherSdk.api.api_keys.createOne({ scopes: [FORM_INGESTION_API_KEY_SCOPE] }),
|
|
169
|
+
{ shouldError: true, onError: (e: any) => e === 'Unauthenticated' || e?.statusCode === 401 || /admin/i.test(e?.message || '') },
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// ...but CAN still create an unscoped key; an empty scopes list is rejected by validation (fail closed)
|
|
173
|
+
const nonAdminKey = await otherSdk.api.api_keys.createOne({})
|
|
174
|
+
await async_test(
|
|
175
|
+
"empty scopes list rejected by validation",
|
|
176
|
+
() => otherSdk.api.api_keys.createOne({ scopes: [] }),
|
|
177
|
+
{ shouldError: true, onError: (e: any) => e?.statusCode === 400 || /empty|not valid|invalid/i.test(e?.message || '') },
|
|
178
|
+
)
|
|
179
|
+
// creator-only cleanup for the throwaway user's key before the user is deleted
|
|
180
|
+
await otherSdk.api.api_keys.deleteOne(nonAdminKey.id).catch(() => {})
|
|
181
|
+
|
|
182
|
+
// =============================================
|
|
183
|
+
// (e) Unscoped keys are unchanged (no regression)
|
|
184
|
+
// =============================================
|
|
185
|
+
const unscoped = await sdk.api.api_keys.createOne({})
|
|
186
|
+
const unscopedKeyValue = (unscoped as any).key as string
|
|
187
|
+
keyIds.push(unscoped.id)
|
|
188
|
+
|
|
189
|
+
await async_test(
|
|
190
|
+
"unscoped key still authenticates general /v1",
|
|
191
|
+
() => new Session({ host, apiKey: unscopedKeyValue }).test_authenticated(),
|
|
192
|
+
{ onResult: r => !!r },
|
|
193
|
+
)
|
|
194
|
+
// Frozen pre-existing behavior: unscoped /v1-created keys store a utf8-encoded hashedKey, which the
|
|
195
|
+
// webhook's hex lookup never matches — only legacy plaintext keys and hex-stored scoped keys work there.
|
|
196
|
+
// (Legacy-key webhook auth is covered by formsort_header_auth.test.ts.)
|
|
197
|
+
await async_test(
|
|
198
|
+
"unscoped /v1-created key still does NOT authenticate the webhook (frozen encoding quirk)",
|
|
199
|
+
() => axios.post(
|
|
200
|
+
buildUrl('key-in-header'),
|
|
201
|
+
bodyFor("scoped-3"),
|
|
202
|
+
{ headers: { Authorization: `API_KEY ${unscopedKeyValue}` } },
|
|
203
|
+
),
|
|
204
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 },
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// =============================================
|
|
208
|
+
// (f) Admin org-wide list includes scopes (and still never leaks secrets)
|
|
209
|
+
// =============================================
|
|
210
|
+
const { apiKeys } = await sdk.api.api_keys.get_organization_api_keys()
|
|
211
|
+
assert(
|
|
212
|
+
!!apiKeys.find(k => k.id === scoped.id && JSON.stringify(k.scopes) === JSON.stringify([FORM_INGESTION_API_KEY_SCOPE])),
|
|
213
|
+
"org list did not report scopes on the scoped key",
|
|
214
|
+
"get_organization_api_keys reports scopes on scoped keys",
|
|
215
|
+
)
|
|
216
|
+
assert(
|
|
217
|
+
!!apiKeys.find(k => k.id === unscoped.id && k.scopes === undefined),
|
|
218
|
+
"org list reported scopes on an unscoped key",
|
|
219
|
+
"get_organization_api_keys omits scopes on unscoped keys",
|
|
220
|
+
)
|
|
221
|
+
assert(
|
|
222
|
+
apiKeys.every(k => (k as any).key === undefined && (k as any).hashedKey === undefined),
|
|
223
|
+
"org list leaked key material",
|
|
224
|
+
"get_organization_api_keys still never returns key/hashedKey",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
// =============================================
|
|
228
|
+
// (g) Legacy non-/v1 funnel (app.js requireLoginAndAccess → is_logged_in_API_key with NO requestPath).
|
|
229
|
+
// Proves scoped keys fail closed on every non-/v1 caller (also stands in for the Medplum webhook path),
|
|
230
|
+
// and that unscoped keys still authenticate the legacy funnel after this work.
|
|
231
|
+
// NOTE: the legacy middleware reads the key from ?apiKey= / body only (never the Authorization header).
|
|
232
|
+
// =============================================
|
|
233
|
+
await async_test(
|
|
234
|
+
"unscoped key authenticates a legacy non-/v1 route",
|
|
235
|
+
() => axios.get(`${host}/API-keys-for-user?apiKey=${encodeURIComponent(unscopedKeyValue)}`),
|
|
236
|
+
{ onResult: (r: any) => r.status === 200 },
|
|
237
|
+
)
|
|
238
|
+
await async_test(
|
|
239
|
+
"scoped key rejected on a legacy non-/v1 route (fail closed, no requestPath)",
|
|
240
|
+
() => axios.get(`${host}/API-keys-for-user?apiKey=${encodeURIComponent(scoped.key)}`),
|
|
241
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 },
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// =============================================
|
|
245
|
+
// (h) Data-egress adjacency: the scope's allowed paths must NOT leak to the file DOWNLOAD endpoint.
|
|
246
|
+
// =============================================
|
|
247
|
+
await async_test(
|
|
248
|
+
"scoped key cannot call file_download_URL (no PHI egress)",
|
|
249
|
+
() => axios.get(`${host}/v1/file-download-URL?secureName=nonexistent`, { headers: { Authorization: `API_KEY ${scoped.key}` } }),
|
|
250
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 || e.response?.status === 403 },
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
// =============================================
|
|
254
|
+
// (i) Transport independence: the scope gate applies whether the key is in the header or the ?apiKey= query.
|
|
255
|
+
// =============================================
|
|
256
|
+
await async_test(
|
|
257
|
+
"scoped key via ?apiKey= query param still rejected on a disallowed /v1 endpoint",
|
|
258
|
+
() => axios.get(`${host}/v1/endusers?apiKey=${encodeURIComponent(scoped.key)}`),
|
|
259
|
+
{ shouldError: true, onError: (e: any) => e.response?.status === 401 || e.response?.status === 403 },
|
|
260
|
+
)
|
|
261
|
+
await async_test(
|
|
262
|
+
"unscoped key via ?apiKey= query param still authenticates /v1 (regression)",
|
|
263
|
+
() => axios.get(`${host}/v1/test-authenticated?apiKey=${encodeURIComponent(unscopedKeyValue)}`),
|
|
264
|
+
{ onResult: (r: any) => r.status === 200 },
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
// =============================================
|
|
268
|
+
// Validation: unknown scope values are rejected
|
|
269
|
+
// =============================================
|
|
270
|
+
await async_test(
|
|
271
|
+
"unknown scope value rejected by validation",
|
|
272
|
+
() => sdk.api.api_keys.createOne({ scopes: ['not-a-real-scope'] as any }),
|
|
273
|
+
{ shouldError: true, onError: (e: any) => e?.statusCode === 400 || /must match|not valid|invalid/i.test(e?.message || '') },
|
|
274
|
+
)
|
|
275
|
+
} finally {
|
|
276
|
+
for (const id of keyIds) {
|
|
277
|
+
await sdk.api.api_keys.deleteOne(id).catch(() => {})
|
|
278
|
+
}
|
|
279
|
+
for (const id of formResponseIds) {
|
|
280
|
+
await sdk.api.form_responses.deleteOne(id).catch(() => {})
|
|
281
|
+
}
|
|
282
|
+
for (const id of fileIds) {
|
|
283
|
+
await sdk.api.files.deleteOne(id).catch(() => {})
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const enduser = await sdk.api.endusers.getOne({ email: TEST_EMAIL })
|
|
287
|
+
await sdk.api.endusers.deleteOne(enduser.id)
|
|
288
|
+
} catch (err) { /* may not exist */ }
|
|
289
|
+
await sdk.api.forms.deleteOne(form.id).catch(() => {})
|
|
290
|
+
if (throwawayUserId) {
|
|
291
|
+
await sdk.api.users.deleteOne(throwawayUserId).catch(() => {})
|
|
292
|
+
}
|
|
293
|
+
if (throwawayRoleId) {
|
|
294
|
+
await sdk.api.role_based_access_permissions.deleteOne(throwawayRoleId).catch(() => {})
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Allow running this test file independently
|
|
300
|
+
if (require.main === module) {
|
|
301
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
302
|
+
const sdk = new Session({ host })
|
|
303
|
+
const sdkNonAdmin = new Session({ host })
|
|
304
|
+
|
|
305
|
+
const runTests = async () => {
|
|
306
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
307
|
+
await scoped_api_keys_tests({ sdk, sdkNonAdmin })
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
runTests()
|
|
311
|
+
.then(() => {
|
|
312
|
+
console.log("✅ Scoped API keys test suite completed successfully")
|
|
313
|
+
process.exit(0)
|
|
314
|
+
})
|
|
315
|
+
.catch((error) => {
|
|
316
|
+
console.error("❌ Scoped API keys test suite failed:", error)
|
|
317
|
+
process.exit(1)
|
|
318
|
+
})
|
|
319
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -87,6 +87,7 @@ import { enduser_login_rate_limits_tests } from "./api_tests/enduser_login_rate_
|
|
|
87
87
|
import { eom_procedure_codes_tests } from "./api_tests/eom_procedure_codes.test";
|
|
88
88
|
import { cross_org_api_key_tests } from "./api_tests/cross_org_api_key.test";
|
|
89
89
|
import { organization_api_keys_tests } from "./api_tests/organization_api_keys.test";
|
|
90
|
+
import { scoped_api_keys_tests } from "./api_tests/scoped_api_keys.test";
|
|
90
91
|
import { custom_dashboards_tests } from "./api_tests/custom_dashboards.test";
|
|
91
92
|
import { message_assignment_trigger_tests } from "./api_tests/message_assignment_trigger.test";
|
|
92
93
|
import { outbound_chat_sent_trigger_tests } from "./api_tests/outbound_chat_sent_trigger.test";
|
|
@@ -15084,6 +15085,7 @@ const ip_address_form_tests = async () => {
|
|
|
15084
15085
|
await eom_procedure_codes_tests({ sdk, sdkNonAdmin })
|
|
15085
15086
|
await cross_org_api_key_tests({ sdk, sdkNonAdmin })
|
|
15086
15087
|
await organization_api_keys_tests({ sdk, sdkNonAdmin })
|
|
15088
|
+
await scoped_api_keys_tests({ sdk, sdkNonAdmin })
|
|
15087
15089
|
await organization_settings_duplicates_tests({ sdk, sdkNonAdmin })
|
|
15088
15090
|
await enduser_session_invalidation_tests({ sdk, sdkNonAdmin })
|
|
15089
15091
|
await chats_analytics_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|