@tellescope/sdk 1.255.17 → 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/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js +206 -106
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/esm/enduser.d.ts +1 -0
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +3 -2
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/session.d.ts +1 -0
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js +206 -106
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/tests/api_tests/scoped_api_keys.test.ts +82 -0
- package/test_generated.pdf +0 -0
|
@@ -35,6 +35,7 @@ export const scoped_api_keys_tests = async ({ sdk, sdkNonAdmin } : { sdk: Sessio
|
|
|
35
35
|
|
|
36
36
|
const keyIds: string[] = []
|
|
37
37
|
const formResponseIds: string[] = []
|
|
38
|
+
const fileIds: string[] = []
|
|
38
39
|
let throwawayUserId: string | undefined
|
|
39
40
|
let throwawayRoleId: string | undefined
|
|
40
41
|
|
|
@@ -101,6 +102,44 @@ export const scoped_api_keys_tests = async ({ sdk, sdkNonAdmin } : { sdk: Sessio
|
|
|
101
102
|
)
|
|
102
103
|
if (pathResult.data?.formResponseId) formResponseIds.push(pathResult.data.formResponseId)
|
|
103
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
|
+
|
|
104
143
|
// =============================================
|
|
105
144
|
// (d) Non-admin cannot create a scoped key (throwaway user, never mutate existing roles)
|
|
106
145
|
// =============================================
|
|
@@ -185,6 +224,46 @@ export const scoped_api_keys_tests = async ({ sdk, sdkNonAdmin } : { sdk: Sessio
|
|
|
185
224
|
"get_organization_api_keys still never returns key/hashedKey",
|
|
186
225
|
)
|
|
187
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
|
+
|
|
188
267
|
// =============================================
|
|
189
268
|
// Validation: unknown scope values are rejected
|
|
190
269
|
// =============================================
|
|
@@ -200,6 +279,9 @@ export const scoped_api_keys_tests = async ({ sdk, sdkNonAdmin } : { sdk: Sessio
|
|
|
200
279
|
for (const id of formResponseIds) {
|
|
201
280
|
await sdk.api.form_responses.deleteOne(id).catch(() => {})
|
|
202
281
|
}
|
|
282
|
+
for (const id of fileIds) {
|
|
283
|
+
await sdk.api.files.deleteOne(id).catch(() => {})
|
|
284
|
+
}
|
|
203
285
|
try {
|
|
204
286
|
const enduser = await sdk.api.endusers.getOne({ email: TEST_EMAIL })
|
|
205
287
|
await sdk.api.endusers.deleteOne(enduser.id)
|
package/test_generated.pdf
CHANGED
|
Binary file
|