Skip to content

Commit 488e76a

Browse files
box-sdk-buildbox-sdk-build
andauthored
feat: Support sensitive data sanitization in errors (box/box-codegen#695) (#573)
Co-authored-by: box-sdk-build <[email protected]>
1 parent 87c8554 commit 488e76a

File tree

9 files changed

+249
-29
lines changed

9 files changed

+249
-29
lines changed

.codegen.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "engineHash": "abf4f7d", "specHash": "c303afc", "version": "1.14.0" }
1+
{ "engineHash": "d69fdc9", "specHash": "c303afc", "version": "1.14.0" }

docs/working-with-nulls.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Handling null values in Box Typescript SDK Gen
2+
3+
While using Box Typescript SDK it's important to understand how null values behave. This document provides a general overview of null value behaviour in Box Typescript SDK to help developers manage data consistently and predictably.
4+
5+
## Understanding null behaviour
6+
7+
The Box Typescript SDK follows a consistent pattern when handling null values in update operations. This behaviour applies to most endpoints that modify resources such as users, files, folders and metadata. The updating field behaves differently depending on weather you omit it, set it to null, or provide a value:
8+
9+
- Omitting the field: The field won't be included in request and the value will remain unchanged
10+
- Setting it to null: Setting a field to null, will cause sending HTTP request with field value set to null, what will result in removing its current value or disassociates it from the resource.
11+
- Providing a value: Providing a non-null value assigns or updates the field to that value.
12+
13+
## Example Usage
14+
15+
The client.files.updateFileById() method demonstrates null handling when modifying the lock field while updating the file:
16+
17+
```ts
18+
async function create_update_file(client) {
19+
fileId = '12345';
20+
21+
// locking the file
22+
const fileWithLock = await client.files.updateFileById(fileId, {
23+
requestBody: { lock: { access: 'lock' } },
24+
queryParams: { fields: ['lock'] },
25+
});
26+
console.log('File with lock ', fileWithLock);
27+
28+
// unlocking the file using lock value as null
29+
const fileWithoutLock = await client.files.updateFileById(fileId, {
30+
requestBody: { lock: null },
31+
queryParams: { fields: ['lock'] },
32+
});
33+
console.log('File without lock ', fileWithoutLock);
34+
}
35+
```
36+
37+
## Summary
38+
39+
To summarize, if you omit the field, the field remains unchanged. If you set it to null, it clears/removes the value. If you provide a value to that field, the field gets updated to that specified value.

package-lock.json

Lines changed: 25 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/box/errors.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { SerializedData } from '../serialization/json.js';
22
import { GeneratedCodeError } from '../internal/errors.js';
3+
import util from 'util';
4+
import { DataSanitizer } from '../internal/logging.generated';
35

46
export class BoxSdkError extends GeneratedCodeError {
57
readonly timestamp?: string;
@@ -38,14 +40,56 @@ export interface ResponseInfo {
3840
export class BoxApiError extends BoxSdkError {
3941
readonly requestInfo!: RequestInfo;
4042
readonly responseInfo!: ResponseInfo;
43+
readonly dataSanitizer: DataSanitizer = new DataSanitizer({});
4144
constructor(
4245
fields: Pick<
4346
BoxApiError,
44-
'message' | 'timestamp' | 'error' | 'requestInfo' | 'responseInfo'
47+
| 'message'
48+
| 'timestamp'
49+
| 'error'
50+
| 'requestInfo'
51+
| 'responseInfo'
52+
| 'dataSanitizer'
4553
>,
4654
) {
4755
super(fields);
4856
this.name = 'BoxApiError';
57+
if (fields.dataSanitizer) {
58+
this.dataSanitizer = fields.dataSanitizer;
59+
}
4960
Object.setPrototypeOf(this, BoxApiError.prototype);
5061
}
62+
63+
[util.inspect.custom]() {
64+
return this.toString();
65+
}
66+
67+
toString(): string {
68+
return JSON.stringify(this.toJSON(), null, 2);
69+
}
70+
71+
toJSON() {
72+
return {
73+
name: this.name,
74+
message: this.message,
75+
timestamp: this.timestamp,
76+
error: this.error,
77+
requestInfo: {
78+
method: this.requestInfo.method,
79+
url: this.requestInfo.url,
80+
queryParams: this.requestInfo.queryParams,
81+
headers: this.dataSanitizer.sanitizeHeaders(this.requestInfo.headers),
82+
body: this.requestInfo.body,
83+
},
84+
responseInfo: {
85+
statusCode: this.responseInfo.statusCode,
86+
headers: this.dataSanitizer.sanitizeHeaders(this.responseInfo.headers),
87+
body: this.dataSanitizer.sanitizeBody(this.responseInfo.body),
88+
code: this.responseInfo.code,
89+
contextInfo: this.responseInfo.contextInfo,
90+
requestId: this.responseInfo.requestId,
91+
helpUrl: this.responseInfo.helpUrl,
92+
},
93+
};
94+
}
5195
}

src/internal/logging.generated.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { SerializedData } from '../serialization/json.js';
2+
import { sanitizeMap } from './utils.js';
3+
import { sanitizeSerializedData } from '../serialization/json.js';
4+
export class DataSanitizer {
5+
private readonly keysToSanitize: {
6+
readonly [key: string]: string;
7+
} = {
8+
['authorization']: '',
9+
['access_token']: '',
10+
['refresh_token']: '',
11+
['subject_token']: '',
12+
['token']: '',
13+
['client_id']: '',
14+
['client_secret']: '',
15+
['shared_link']: '',
16+
['download_url']: '',
17+
['jwt_private_key']: '',
18+
['jwt_private_key_passphrase']: '',
19+
['password']: '',
20+
};
21+
constructor(
22+
fields: Omit<
23+
DataSanitizer,
24+
'keysToSanitize' | 'sanitizeHeaders' | 'sanitizeBody'
25+
>,
26+
) {}
27+
/**
28+
* @param {{
29+
readonly [key: string]: string;
30+
}} headers
31+
* @returns {{
32+
readonly [key: string]: string;
33+
}}
34+
*/
35+
sanitizeHeaders(headers: { readonly [key: string]: string }): {
36+
readonly [key: string]: string;
37+
} {
38+
return sanitizeMap(headers, this.keysToSanitize);
39+
}
40+
/**
41+
* @param {SerializedData} body
42+
* @returns {SerializedData}
43+
*/
44+
sanitizeBody(body: SerializedData): SerializedData {
45+
return sanitizeSerializedData(body, this.keysToSanitize);
46+
}
47+
}
48+
export interface DataSanitizerInput {}

src/internal/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
FormData,
2222
generateReadableStreamFromFile,
2323
} from './utilsNode';
24+
import { sanitizedValue } from '../serialization/json';
2425

2526
export type HashName = 'sha1';
2627
export type DigestHashType = 'base64';
@@ -239,3 +240,19 @@ export function createCancellationController(): CancellationController {
239240
export function random(min: number, max: number): number {
240241
return Math.random() * (max - min) + min;
241242
}
243+
244+
/**
245+
* Sanitize a map by replacing sensitive values with a placeholder.
246+
* @param dictionary The map to sanitize
247+
* @param keysToSanitize Keys to sanitize
248+
*/
249+
export function sanitizeMap(
250+
dictionary: Record<string, string>,
251+
keysToSanitize: Record<string, string>,
252+
): Record<string, string> {
253+
return Object.fromEntries(
254+
Object.entries(dictionary).map(([k, v]) =>
255+
k.toLowerCase() in keysToSanitize ? [k, sanitizedValue()] : [k, v],
256+
),
257+
);
258+
}

src/networking/boxNetworkClient.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,19 +248,22 @@ export class BoxNetworkClient implements NetworkClient {
248248
return fetchResponse;
249249
}
250250

251-
const [code, contextInfo, requestId, helpUrl] = sdIsMap(fetchResponse.data)
251+
const [code, contextInfo, requestId, helpUrl, message] = sdIsMap(
252+
fetchResponse.data,
253+
)
252254
? [
253255
sdToJson(fetchResponse.data['code']),
254256
sdIsMap(fetchResponse.data['context_info'])
255257
? fetchResponse.data['context_info']
256258
: undefined,
257259
sdToJson(fetchResponse.data['request_id']),
258260
sdToJson(fetchResponse.data['help_url']),
261+
sdToJson(fetchResponse.data['message']),
259262
]
260263
: [];
261264

262265
throw new BoxApiError({
263-
message: `${fetchResponse.status}`,
266+
message: `${fetchResponse.status} ${message}; Request ID: ${requestId}`,
264267
timestamp: `${Date.now()}`,
265268
requestInfo: {
266269
method: requestInit.method!,
@@ -280,6 +283,7 @@ export class BoxNetworkClient implements NetworkClient {
280283
requestId: requestId,
281284
helpUrl: helpUrl,
282285
},
286+
dataSanitizer: networkSession.dataSanitizer,
283287
});
284288
}
285289
}

0 commit comments

Comments
 (0)