@usebruno/filestore 0.9.0 → 0.11.0
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/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/workers/worker-script.js +1 -1
- package/dist/cjs/workers/worker-script.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/workers/worker-script.js +1 -1
- package/dist/esm/workers/worker-script.js.map +1 -1
- package/dist/formats/bru/index.d.ts.map +1 -1
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts +11 -0
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts.map +1 -0
- package/dist/formats/yml/common/app.d.ts +8 -0
- package/dist/formats/yml/common/app.d.ts.map +1 -0
- package/dist/formats/yml/common/auth.d.ts.map +1 -1
- package/dist/formats/yml/common/body.d.ts.map +1 -1
- package/dist/formats/yml/common/datatype.d.ts +14 -0
- package/dist/formats/yml/common/datatype.d.ts.map +1 -0
- package/dist/formats/yml/common/headers.d.ts.map +1 -1
- package/dist/formats/yml/common/variables.d.ts.map +1 -1
- package/dist/formats/yml/items/parseApp.d.ts +13 -0
- package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
- package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
- package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
- package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
- package/dist/formats/yml/parseCollection.d.ts.map +1 -1
- package/dist/formats/yml/parseEnvironment.d.ts +3 -1
- package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
- package/dist/formats/yml/parseFolder.d.ts.map +1 -1
- package/dist/formats/yml/parseItem.d.ts.map +1 -1
- package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
- package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
- package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
- package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
- package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/constants.ts +9 -0
- package/src/formats/bru/index.ts +52 -0
- package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
- package/src/formats/bru/tests/redact-large-text-blocks.spec.js +313 -0
- package/src/formats/bru/utils/redact-large-text-blocks.ts +102 -0
- package/src/formats/yml/common/actions.ts +1 -1
- package/src/formats/yml/common/app.ts +27 -0
- package/src/formats/yml/common/auth.ts +53 -1
- package/src/formats/yml/common/body.spec.ts +62 -0
- package/src/formats/yml/common/body.ts +27 -9
- package/src/formats/yml/common/datatype.spec.ts +110 -0
- package/src/formats/yml/common/datatype.ts +57 -0
- package/src/formats/yml/common/headers.ts +4 -0
- package/src/formats/yml/common/variables.spec.ts +127 -0
- package/src/formats/yml/common/variables.ts +20 -6
- package/src/formats/yml/items/parseApp.ts +37 -0
- package/src/formats/yml/items/parseGraphQLRequest.ts +8 -0
- package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
- package/src/formats/yml/items/parseHttpRequest.ts +13 -0
- package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
- package/src/formats/yml/items/stringifyApp.ts +45 -0
- package/src/formats/yml/items/stringifyGraphQLRequest.ts +3 -0
- package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
- package/src/formats/yml/items/stringifyHttpRequest.ts +10 -0
- package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
- package/src/formats/yml/parseCollection.spec.ts +70 -0
- package/src/formats/yml/parseCollection.ts +8 -3
- package/src/formats/yml/parseEnvironment.spec.ts +99 -0
- package/src/formats/yml/parseEnvironment.ts +69 -5
- package/src/formats/yml/parseFolder.spec.ts +52 -0
- package/src/formats/yml/parseFolder.ts +14 -21
- package/src/formats/yml/parseItem.spec.ts +147 -0
- package/src/formats/yml/parseItem.ts +4 -0
- package/src/formats/yml/stringifyCollection.spec.ts +77 -0
- package/src/formats/yml/stringifyCollection.ts +4 -0
- package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
- package/src/formats/yml/stringifyEnvironment.ts +59 -10
- package/src/formats/yml/stringifyFolder.spec.ts +47 -0
- package/src/formats/yml/stringifyItem.spec.ts +53 -0
- package/src/formats/yml/stringifyItem.ts +4 -0
- package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
- package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
- package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
- package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
- package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
- package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
- package/src/formats/yml/tests/parseCollection.spec.js +63 -0
- package/src/formats/yml/tests/parseEnvironment.spec.js +122 -0
- package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
- package/src/formats/yml/tests/stringifyEnvironment.spec.ts +204 -0
- package/src/formats/yml/tests/variables.spec.js +34 -0
- package/src/index.ts +5 -0
- package/src/types/bruno-lang.d.ts +1 -0
- package/src/types.ts +7 -0
- package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
- package/src/utils/secretsJsonToEnvironments.ts +129 -0
- package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
- package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
- package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
- package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
- package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
- package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
- package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
- package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const { redactLargeBruTextBlocks, restoreRedactedBlocks } = require('../utils/redact-large-text-blocks');
|
|
2
|
+
const { parseBruRequest, parseBruCollection } = require('../index');
|
|
3
|
+
|
|
4
|
+
const parseViaRedaction = (content, parser) => {
|
|
5
|
+
const { skeleton, blocks } = redactLargeBruTextBlocks(content);
|
|
6
|
+
return restoreRedactedBlocks(parser(skeleton), blocks);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const toCRLF = (content) => content.replace(/\n/g, '\r\n');
|
|
10
|
+
|
|
11
|
+
const requestBru = `meta {
|
|
12
|
+
name: Redaction Test
|
|
13
|
+
type: http
|
|
14
|
+
seq: 1
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get {
|
|
18
|
+
url: https://example.com/api
|
|
19
|
+
body: json
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
headers {
|
|
23
|
+
content-type: application/json
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body:json {
|
|
27
|
+
{
|
|
28
|
+
"hello": "world",
|
|
29
|
+
"nested": {
|
|
30
|
+
"a": 1,
|
|
31
|
+
"b": [1, 2, 3]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body:text {
|
|
37
|
+
This is a text body
|
|
38
|
+
spanning multiple lines
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
script:pre-request {
|
|
42
|
+
const x = 1;
|
|
43
|
+
if (x) {
|
|
44
|
+
console.log("hi");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
script:post-response {
|
|
49
|
+
bru.setVar("y", 2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
tests {
|
|
53
|
+
test("ok", function() {
|
|
54
|
+
expect(1).to.equal(1);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
docs {
|
|
59
|
+
# Title
|
|
60
|
+
|
|
61
|
+
Some **markdown** docs with a fenced block:
|
|
62
|
+
|
|
63
|
+
code line stays indented
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const graphqlBru = `meta {
|
|
68
|
+
name: GraphQL Test
|
|
69
|
+
type: graphql
|
|
70
|
+
seq: 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
post {
|
|
74
|
+
url: https://example.com/graphql
|
|
75
|
+
body: graphql
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
body:graphql {
|
|
79
|
+
{
|
|
80
|
+
launchesPast {
|
|
81
|
+
launch_site {
|
|
82
|
+
site_name
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
body:graphql:vars {
|
|
89
|
+
{
|
|
90
|
+
"limit": 10
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const xmlSparqlBru = `meta {
|
|
96
|
+
name: Others
|
|
97
|
+
type: http
|
|
98
|
+
seq: 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
post {
|
|
102
|
+
url: https://example.com
|
|
103
|
+
body: xml
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
body:xml {
|
|
107
|
+
<xml>
|
|
108
|
+
<name>John</name>
|
|
109
|
+
</xml>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
body:sparql {
|
|
113
|
+
SELECT * WHERE {
|
|
114
|
+
?s ?p ?o .
|
|
115
|
+
}
|
|
116
|
+
LIMIT 10
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const collectionBru = `headers {
|
|
121
|
+
x-trace: enabled
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
script:pre-request {
|
|
125
|
+
bru.setEnvVar("base", "https://example.com");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
tests {
|
|
129
|
+
test("collection level", function() {
|
|
130
|
+
expect(true).to.equal(true);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
docs {
|
|
135
|
+
# Collection docs
|
|
136
|
+
|
|
137
|
+
Notes for the whole collection.
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const folderBru = `meta {
|
|
142
|
+
name: My Folder
|
|
143
|
+
seq: 2
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
docs {
|
|
147
|
+
# Folder docs
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const blankLinesBru = `meta {
|
|
152
|
+
name: Blanks
|
|
153
|
+
type: http
|
|
154
|
+
seq: 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
post {
|
|
158
|
+
url: https://example.com
|
|
159
|
+
body: json
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
body:json {
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
"a": 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
describe('redactLargeBruTextBlocks', () => {
|
|
173
|
+
describe('matches a normal parse (ohm oracle)', () => {
|
|
174
|
+
const requestCases = [
|
|
175
|
+
['request with bodies, scripts, tests, docs', requestBru],
|
|
176
|
+
['graphql query + variables', graphqlBru],
|
|
177
|
+
['xml + sparql bodies', xmlSparqlBru],
|
|
178
|
+
['CRLF request', toCRLF(requestBru)],
|
|
179
|
+
['leading + trailing blank lines in body', blankLinesBru],
|
|
180
|
+
['CRLF leading + trailing blank lines', toCRLF(blankLinesBru)]
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
it.each(requestCases)('%s', (_name, content) => {
|
|
184
|
+
expect(parseViaRedaction(content, parseBruRequest)).toEqual(parseBruRequest(content));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const collectionCases = [
|
|
188
|
+
['collection', collectionBru],
|
|
189
|
+
['folder', folderBru],
|
|
190
|
+
['CRLF collection', toCRLF(collectionBru)]
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
it.each(collectionCases)('%s', (_name, content) => {
|
|
194
|
+
expect(parseViaRedaction(content, parseBruCollection)).toEqual(parseBruCollection(content));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('extracts each large text block and shrinks the skeleton', () => {
|
|
199
|
+
const { skeleton, blocks } = redactLargeBruTextBlocks(requestBru);
|
|
200
|
+
expect(blocks.length).toBe(6);
|
|
201
|
+
expect(skeleton.length).toBeLessThan(requestBru.length);
|
|
202
|
+
blocks.forEach((block) => {
|
|
203
|
+
expect(skeleton).toContain(block.token);
|
|
204
|
+
expect(skeleton).not.toContain(block.value);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('keeps the skeleton bounded regardless of block size', () => {
|
|
209
|
+
const blob = 'x'.repeat(4 * 1024 * 1024);
|
|
210
|
+
const bru = `meta {
|
|
211
|
+
name: Huge
|
|
212
|
+
type: http
|
|
213
|
+
seq: 1
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
post {
|
|
217
|
+
url: https://example.com
|
|
218
|
+
body: json
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
body:json {
|
|
222
|
+
{ "blob": "${blob}" }
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
const { skeleton, blocks } = redactLargeBruTextBlocks(bru);
|
|
226
|
+
expect(bru.length).toBeGreaterThan(4 * 1024 * 1024);
|
|
227
|
+
expect(skeleton.length).toBeLessThan(1024);
|
|
228
|
+
expect(blocks[0].value).toContain(blob);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('leaves dictionary body blocks untouched', () => {
|
|
232
|
+
const multipart = `meta {
|
|
233
|
+
name: Multipart
|
|
234
|
+
type: http
|
|
235
|
+
seq: 1
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
post {
|
|
239
|
+
url: https://example.com
|
|
240
|
+
body: multipartForm
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
body:multipart-form {
|
|
244
|
+
field: value
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
const { blocks } = redactLargeBruTextBlocks(multipart);
|
|
248
|
+
expect(blocks.length).toBe(0);
|
|
249
|
+
expect(parseViaRedaction(multipart, parseBruRequest)).toEqual(parseBruRequest(multipart));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('captures nested braces in content without ending the block early', () => {
|
|
253
|
+
const nested = `body:json {
|
|
254
|
+
{
|
|
255
|
+
"a": {
|
|
256
|
+
"b": [ { "c": 1 } ],
|
|
257
|
+
"d": {}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
script:pre-request {
|
|
263
|
+
function outer() {
|
|
264
|
+
if (true) {
|
|
265
|
+
return { ok: 1 };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
`;
|
|
270
|
+
const { blocks } = redactLargeBruTextBlocks(nested);
|
|
271
|
+
expect(blocks.length).toBe(2);
|
|
272
|
+
expect(blocks[0].value).toContain('"c": 1');
|
|
273
|
+
expect(blocks[0].value).toContain('"d": {}');
|
|
274
|
+
expect(blocks[1].value).toContain('return { ok: 1 };');
|
|
275
|
+
expect(parseViaRedaction(nested, parseBruRequest)).toEqual(parseBruRequest(nested));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('only redacts blocks at column 0, never indented tags', () => {
|
|
279
|
+
const indented = `docs {
|
|
280
|
+
Nested pseudo-block should be treated as content:
|
|
281
|
+
body:json {
|
|
282
|
+
{ "a": 1 }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
`;
|
|
286
|
+
const { blocks } = redactLargeBruTextBlocks(indented);
|
|
287
|
+
expect(blocks.length).toBe(1);
|
|
288
|
+
expect(blocks[0].value).toContain('body:json {');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns content unchanged when no large text blocks are present', () => {
|
|
292
|
+
const minimal = `meta {
|
|
293
|
+
name: Bare
|
|
294
|
+
type: http
|
|
295
|
+
seq: 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
get {
|
|
299
|
+
url: https://example.com
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
const { skeleton, blocks } = redactLargeBruTextBlocks(minimal);
|
|
303
|
+
expect(blocks).toEqual([]);
|
|
304
|
+
expect(skeleton).toBe(minimal);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('restoreRedactedBlocks', () => {
|
|
309
|
+
it('returns the parsed object unchanged when there are no blocks', () => {
|
|
310
|
+
const parsed = { request: { body: { json: 'x' } } };
|
|
311
|
+
expect(restoreRedactedBlocks(parsed, [])).toBe(parsed);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { outdentString } from '@usebruno/lang';
|
|
3
|
+
|
|
4
|
+
export interface RedactedBlock {
|
|
5
|
+
token: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RedactionResult {
|
|
10
|
+
skeleton: string;
|
|
11
|
+
blocks: RedactedBlock[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const BLOCK_TAGS = [
|
|
15
|
+
'body:graphql:vars',
|
|
16
|
+
'body:graphql',
|
|
17
|
+
'body:json',
|
|
18
|
+
'body:text',
|
|
19
|
+
'body:xml',
|
|
20
|
+
'body:sparql',
|
|
21
|
+
'script:pre-request',
|
|
22
|
+
'script:post-response',
|
|
23
|
+
'tests',
|
|
24
|
+
'docs',
|
|
25
|
+
'body'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const BLOCK_OPENING = new RegExp(`^(${BLOCK_TAGS.join('|')})[ \\t]*\\{\\r?$`);
|
|
29
|
+
|
|
30
|
+
const isOpening = (line: string): boolean => BLOCK_OPENING.test(line);
|
|
31
|
+
|
|
32
|
+
// A top-level block's closing brace sits at column 0; braces inside the content are indented, so
|
|
33
|
+
// nested braces are correctly treated as content rather than ending the block early.
|
|
34
|
+
const isClosing = (line: string): boolean => line.startsWith('}');
|
|
35
|
+
|
|
36
|
+
const tokenFor = (content: string): string => {
|
|
37
|
+
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
38
|
+
return `__BRU_REDACTED_TEXT_BLOCK_${hash}__`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const blockValue = (content: string[]): string =>
|
|
42
|
+
outdentString(content.join('\n').replace(/^(?:\r?\n)+/, '').replace(/\r$/, ''));
|
|
43
|
+
|
|
44
|
+
export const redactLargeBruTextBlocks = (content: string): RedactionResult => {
|
|
45
|
+
const source = content || '';
|
|
46
|
+
const skeleton: string[] = [];
|
|
47
|
+
const blocks: RedactedBlock[] = [];
|
|
48
|
+
|
|
49
|
+
let openTag: string | null = null;
|
|
50
|
+
let openContent: string[] = [];
|
|
51
|
+
|
|
52
|
+
for (const line of source.split('\n')) {
|
|
53
|
+
if (openTag === null && isOpening(line)) {
|
|
54
|
+
openTag = line;
|
|
55
|
+
openContent = [];
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (openTag !== null && isClosing(line)) {
|
|
60
|
+
const token = tokenFor(openContent.join('\n'));
|
|
61
|
+
blocks.push({ token, value: blockValue(openContent) });
|
|
62
|
+
skeleton.push(openTag, ` ${token}`, line);
|
|
63
|
+
openTag = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
(openTag === null ? skeleton : openContent).push(line);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (openTag !== null) {
|
|
71
|
+
skeleton.push(openTag, ...openContent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (blocks.some((block) => source.includes(block.token))) {
|
|
75
|
+
console.warn('[bruno-filestore] Token collision detected; skipping redaction');
|
|
76
|
+
return { skeleton: source, blocks: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { skeleton: skeleton.join('\n'), blocks };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const restoreRedactedBlocks = <T>(parsed: T, blocks: RedactedBlock[]): T => {
|
|
83
|
+
if (!blocks.length) {
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const valueByToken = new Map(blocks.map((block) => [block.token, block.value]));
|
|
88
|
+
|
|
89
|
+
const walk = (node: any): any => {
|
|
90
|
+
if (typeof node === 'string') {
|
|
91
|
+
return valueByToken.get(node) ?? node;
|
|
92
|
+
}
|
|
93
|
+
if (node && typeof node === 'object') {
|
|
94
|
+
for (const key of Object.keys(node)) {
|
|
95
|
+
node[key] = walk(node[key]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return node;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return walk(parsed);
|
|
102
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { App as BrunoApp } from '@usebruno/schema-types/collection/item';
|
|
2
|
+
|
|
3
|
+
export interface OpenCollectionApp {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
code?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const toOpenCollectionApp = (app: BrunoApp | null | undefined): OpenCollectionApp | undefined => {
|
|
9
|
+
// The app block persists when enabled even before any code is written.
|
|
10
|
+
if (!app || (!app.code && app.enabled !== true)) return undefined;
|
|
11
|
+
const ocApp: OpenCollectionApp = {};
|
|
12
|
+
if (app.enabled === true) {
|
|
13
|
+
ocApp.enabled = true;
|
|
14
|
+
}
|
|
15
|
+
if (app.code) {
|
|
16
|
+
ocApp.code = app.code;
|
|
17
|
+
}
|
|
18
|
+
return ocApp;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const toBrunoApp = (app: OpenCollectionApp | null | undefined): BrunoApp | null => {
|
|
22
|
+
if (!app) return null;
|
|
23
|
+
return {
|
|
24
|
+
enabled: app.enabled === true,
|
|
25
|
+
code: app.code || null
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -13,6 +13,20 @@ import type { Auth as BrunoAuth, AuthOauth1 as BrunoAuthOauth1 } from '@usebruno
|
|
|
13
13
|
import { isString } from '../../../utils';
|
|
14
14
|
import { toOpenCollectionOAuth2, toBrunoOAuth2 } from './auth-oauth2';
|
|
15
15
|
|
|
16
|
+
// EdgeGrid is a Bruno-specific auth mode that is not part of the OpenCollection spec,
|
|
17
|
+
// so its shape is defined locally and serialized using the same field names as Bruno.
|
|
18
|
+
interface AkamaiEdgeGridAuthValues {
|
|
19
|
+
type: 'akamai-edgegrid';
|
|
20
|
+
accessToken?: string;
|
|
21
|
+
clientToken?: string;
|
|
22
|
+
clientSecret?: string;
|
|
23
|
+
baseURL?: string | null;
|
|
24
|
+
nonce?: string | null;
|
|
25
|
+
timestamp?: string | null;
|
|
26
|
+
headersToSign?: string | null;
|
|
27
|
+
maxBodySize?: number | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
const buildAwsV4Auth = (config?: BrunoAuth['awsv4']): AuthAwsV4 => {
|
|
17
31
|
const auth: AuthAwsV4 = { type: 'awsv4' };
|
|
18
32
|
|
|
@@ -116,6 +130,25 @@ const buildApiKeyAuth = (config?: BrunoAuth['apikey']): AuthApiKey => {
|
|
|
116
130
|
return auth;
|
|
117
131
|
};
|
|
118
132
|
|
|
133
|
+
const buildEdgeGridAuth = (config?: BrunoAuth['akamaiEdgegrid']): AkamaiEdgeGridAuthValues => {
|
|
134
|
+
const auth: AkamaiEdgeGridAuthValues = { type: 'akamai-edgegrid' };
|
|
135
|
+
|
|
136
|
+
if (!config) {
|
|
137
|
+
return auth;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isString(config.accessToken)) auth.accessToken = config.accessToken;
|
|
141
|
+
if (isString(config.clientToken)) auth.clientToken = config.clientToken;
|
|
142
|
+
if (isString(config.clientSecret)) auth.clientSecret = config.clientSecret;
|
|
143
|
+
if (isString(config.nonce)) auth.nonce = config.nonce;
|
|
144
|
+
if (isString(config.timestamp)) auth.timestamp = config.timestamp;
|
|
145
|
+
if (isString(config.baseURL)) auth.baseURL = config.baseURL;
|
|
146
|
+
if (isString(config.headersToSign)) auth.headersToSign = config.headersToSign;
|
|
147
|
+
if (typeof config.maxBodySize === 'number') auth.maxBodySize = config.maxBodySize;
|
|
148
|
+
|
|
149
|
+
return auth;
|
|
150
|
+
};
|
|
151
|
+
|
|
119
152
|
const buildOAuth1Auth = (config?: BrunoAuth['oauth1']): AuthOAuth1 => {
|
|
120
153
|
const auth: AuthOAuth1 = { type: 'oauth1' };
|
|
121
154
|
|
|
@@ -173,6 +206,8 @@ export const toOpenCollectionAuth = (auth?: BrunoAuth | null): Auth | undefined
|
|
|
173
206
|
return buildOAuth1Auth(auth.oauth1);
|
|
174
207
|
case 'oauth2':
|
|
175
208
|
return toOpenCollectionOAuth2(auth.oauth2);
|
|
209
|
+
case 'akamai-edgegrid':
|
|
210
|
+
return buildEdgeGridAuth(auth.akamaiEdgegrid) as unknown as Auth;
|
|
176
211
|
default:
|
|
177
212
|
console.warn(`toOpenCollectionAuth failed: Unsupported auth mode "${auth.mode}".`);
|
|
178
213
|
return undefined;
|
|
@@ -189,7 +224,8 @@ export const toBrunoAuth = (auth: Auth | null | undefined): BrunoAuth | null =>
|
|
|
189
224
|
ntlm: null,
|
|
190
225
|
oauth2: null,
|
|
191
226
|
wsse: null,
|
|
192
|
-
apikey: null
|
|
227
|
+
apikey: null,
|
|
228
|
+
akamaiEdgegrid: null
|
|
193
229
|
};
|
|
194
230
|
|
|
195
231
|
if (!auth) {
|
|
@@ -201,6 +237,22 @@ export const toBrunoAuth = (auth: Auth | null | undefined): BrunoAuth | null =>
|
|
|
201
237
|
return brunoAuth;
|
|
202
238
|
}
|
|
203
239
|
|
|
240
|
+
if ((auth as unknown as AkamaiEdgeGridAuthValues).type === 'akamai-edgegrid') {
|
|
241
|
+
const edgegrid = auth as unknown as AkamaiEdgeGridAuthValues;
|
|
242
|
+
brunoAuth.mode = 'akamai-edgegrid';
|
|
243
|
+
brunoAuth.akamaiEdgegrid = {
|
|
244
|
+
accessToken: edgegrid.accessToken || '',
|
|
245
|
+
clientToken: edgegrid.clientToken || '',
|
|
246
|
+
clientSecret: edgegrid.clientSecret || '',
|
|
247
|
+
nonce: edgegrid.nonce || '',
|
|
248
|
+
timestamp: edgegrid.timestamp || '',
|
|
249
|
+
baseURL: edgegrid.baseURL || '',
|
|
250
|
+
headersToSign: edgegrid.headersToSign || '',
|
|
251
|
+
maxBodySize: typeof edgegrid.maxBodySize === 'number' ? edgegrid.maxBodySize : null
|
|
252
|
+
};
|
|
253
|
+
return brunoAuth;
|
|
254
|
+
}
|
|
255
|
+
|
|
204
256
|
switch (auth.type) {
|
|
205
257
|
case 'awsv4':
|
|
206
258
|
brunoAuth.mode = 'awsv4';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toOpenCollectionBody, toBrunoBody } from './body';
|
|
2
|
+
|
|
3
|
+
describe('file body description', () => {
|
|
4
|
+
it('toOpenCollectionBody: preserves description, omits when absent or whitespace-only', () => {
|
|
5
|
+
const out = toOpenCollectionBody({
|
|
6
|
+
mode: 'file',
|
|
7
|
+
file: [
|
|
8
|
+
{ uid: 'f1', filePath: '/tmp/readme.pdf', contentType: 'application/pdf', selected: true, description: 'Upload doc' },
|
|
9
|
+
{ uid: 'f2', filePath: '/tmp/other.bin', contentType: 'application/octet-stream', selected: false },
|
|
10
|
+
{ uid: 'f3', filePath: '/tmp/plain.bin', contentType: 'application/octet-stream', selected: false, description: ' ' }
|
|
11
|
+
]
|
|
12
|
+
} as any);
|
|
13
|
+
|
|
14
|
+
expect(out?.type).toBe('file');
|
|
15
|
+
expect(out?.data).toHaveLength(3);
|
|
16
|
+
expect(out?.data[0]).toMatchObject({ filePath: '/tmp/readme.pdf', description: 'Upload doc' });
|
|
17
|
+
expect(out?.data[1]).not.toHaveProperty('description');
|
|
18
|
+
expect(out?.data[2]).not.toHaveProperty('description');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('toBrunoBody: parses description from file body entries', () => {
|
|
22
|
+
const out = toBrunoBody({
|
|
23
|
+
type: 'file',
|
|
24
|
+
data: [
|
|
25
|
+
{ filePath: '/tmp/readme.pdf', contentType: 'application/pdf', selected: true, description: 'Upload doc' },
|
|
26
|
+
{ filePath: '/tmp/other.bin', contentType: 'application/octet-stream', selected: false }
|
|
27
|
+
]
|
|
28
|
+
} as any);
|
|
29
|
+
|
|
30
|
+
expect(out?.mode).toBe('file');
|
|
31
|
+
expect(out?.file).toHaveLength(2);
|
|
32
|
+
|
|
33
|
+
// already fails on the length check above if no bodies are recieved, the if below is just
|
|
34
|
+
// to satisfy typescript and then validate the content of the files
|
|
35
|
+
if (out?.file) {
|
|
36
|
+
expect(out?.file[0]).toMatchObject({ filePath: '/tmp/readme.pdf', description: 'Upload doc' });
|
|
37
|
+
expect(out?.file[1].description).toBeFalsy();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('round-trips file body descriptions through OC conversion', () => {
|
|
42
|
+
const brunoBody = {
|
|
43
|
+
mode: 'file',
|
|
44
|
+
file: [
|
|
45
|
+
{ uid: 'f1', filePath: '/tmp/readme.pdf', contentType: 'application/pdf', selected: true, description: 'Upload doc' },
|
|
46
|
+
{ uid: 'f2', filePath: '/tmp/other.bin', contentType: 'application/octet-stream', selected: false }
|
|
47
|
+
]
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
const oc = toOpenCollectionBody(brunoBody);
|
|
51
|
+
const back = toBrunoBody(oc);
|
|
52
|
+
|
|
53
|
+
expect(back?.file).toHaveLength(2);
|
|
54
|
+
|
|
55
|
+
// already fails on the length check above if no bodies are recieved, the if below is just
|
|
56
|
+
// to satisfy typescript and then validate the content of the files
|
|
57
|
+
if (back?.file) {
|
|
58
|
+
expect(back?.file[0]).toMatchObject({ filePath: '/tmp/readme.pdf', description: 'Upload doc' });
|
|
59
|
+
expect(back?.file[1].description).toBeFalsy();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -7,8 +7,10 @@ import type {
|
|
|
7
7
|
MultipartFormBody,
|
|
8
8
|
MultipartFormEntry,
|
|
9
9
|
FileBody,
|
|
10
|
-
|
|
10
|
+
FileBodyVariant
|
|
11
11
|
} from '@opencollection/types/requests/http';
|
|
12
|
+
import type { FileEntry as BrunoFileEntry } from '@usebruno/schema-types/common/file';
|
|
13
|
+
|
|
12
14
|
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
|
|
13
15
|
import { uuid, ensureString } from '../../../utils';
|
|
14
16
|
|
|
@@ -103,12 +105,18 @@ export const toOpenCollectionBody = (body: BrunoHttpRequestBody | null | undefin
|
|
|
103
105
|
return multipartBody;
|
|
104
106
|
|
|
105
107
|
case 'file':
|
|
106
|
-
const fileEntries:
|
|
107
|
-
|
|
108
|
+
const fileEntries: FileBodyVariant[] = body.file?.map((file): FileBodyVariant => {
|
|
109
|
+
const fileEntry: FileBodyVariant = {
|
|
108
110
|
filePath: file.filePath || '',
|
|
109
111
|
contentType: file.contentType || '',
|
|
110
112
|
selected: file.selected ?? false
|
|
111
113
|
};
|
|
114
|
+
|
|
115
|
+
if (file?.description?.trim().length) {
|
|
116
|
+
fileEntry.description = file.description;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return fileEntry;
|
|
112
120
|
}) || [];
|
|
113
121
|
|
|
114
122
|
const fileBody: FileBody = {
|
|
@@ -222,12 +230,22 @@ export const toBrunoBody = (body: HttpRequestBody | null | undefined): BrunoHttp
|
|
|
222
230
|
|
|
223
231
|
case 'file':
|
|
224
232
|
brunoBody.mode = 'file';
|
|
225
|
-
brunoBody.file = body.data?.map((file):
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
brunoBody.file = body.data?.map((file): BrunoFileEntry => {
|
|
234
|
+
const fileEntry: BrunoFileEntry = {
|
|
235
|
+
uid: uuid(),
|
|
236
|
+
filePath: file.filePath || '',
|
|
237
|
+
contentType: file.contentType || '',
|
|
238
|
+
selected: file.selected ?? false
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const fileWithDescription = file as FileBodyVariant;
|
|
242
|
+
const desc = typeof fileWithDescription.description === 'string'
|
|
243
|
+
? fileWithDescription.description
|
|
244
|
+
: (fileWithDescription.description as { content?: string } | undefined)?.content;
|
|
245
|
+
if (desc && desc.trim().length) fileEntry.description = desc;
|
|
246
|
+
|
|
247
|
+
return fileEntry;
|
|
248
|
+
}) || [];
|
|
231
249
|
break;
|
|
232
250
|
|
|
233
251
|
default:
|