@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.14
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/CHANGELOG.md +76 -0
- package/README.md +3 -1
- package/docs/configuration.md +40 -5
- package/docs/getting-started.md +28 -0
- package/docs/production-source.md +43 -7
- package/docs/releasing.md +5 -3
- package/docs/roadmap.md +3 -2
- package/docs/sanitization.md +415 -14
- package/docs/standalone-workflow.md +12 -6
- package/docs/troubleshooting.md +5 -1
- package/docs/tutorial.md +5 -0
- package/package.json +1 -1
- package/scripts/install/create_root_config.mjs +5 -22
- package/src/baseline/privacy_engine.mjs +660 -213
- package/src/baseline/privacy_structured_json.mjs +554 -0
- package/src/cli/rehearsal.mjs +3 -1
- package/src/cli/renderers.mjs +12 -0
- package/src/cli/source_commands.mjs +11 -3
- package/src/identity/claim.mjs +23 -21
- package/src/project/install_scaffold.mjs +35 -1
- package/src/project/setup.mjs +9 -1
- package/src/source/access.mjs +50 -5
- package/src/source/asset_transfer.mjs +139 -87
- package/src/source/external_reader.mjs +136 -23
- package/src/source/postgresql_access.mjs +80 -23
|
@@ -138,6 +138,31 @@ const uniqueProjects = (projects) => [
|
|
|
138
138
|
|
|
139
139
|
const skip = (reason) => ({ status: "skipped", reason });
|
|
140
140
|
|
|
141
|
+
export const renderInstalledRehearsalScaffold = (result) => {
|
|
142
|
+
if (result.status !== "ready") {
|
|
143
|
+
return `Rehearsal did not create project files: ${result.reason}.\nNext: run npx rehearsal from the application directory.`;
|
|
144
|
+
}
|
|
145
|
+
const changed = result.files.filter((file) => file.action !== "unchanged");
|
|
146
|
+
return [
|
|
147
|
+
"Rehearsal is ready in this project.",
|
|
148
|
+
`Configuration: ${result.configurationPath} (${result.configurationAction === "create" ? "created" : "existing"})`,
|
|
149
|
+
...(result.rootConfigGenerationSkipped
|
|
150
|
+
? [
|
|
151
|
+
` Existing supported configuration detected at ${result.configurationPath}.`,
|
|
152
|
+
" rehearsal.config.mjs was not created because Rehearsal preserves one active configuration and never creates a competing file.",
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
...(changed.length
|
|
156
|
+
? [
|
|
157
|
+
...changed.map(
|
|
158
|
+
(file) => ` ${file.action === "create" ? "+" : "~"} ${file.path}`,
|
|
159
|
+
),
|
|
160
|
+
]
|
|
161
|
+
: [" Existing Rehearsal files preserved."]),
|
|
162
|
+
`Next: review ${result.configurationPath}, then run npx rehearsal.`,
|
|
163
|
+
].join("\n");
|
|
164
|
+
};
|
|
165
|
+
|
|
141
166
|
export const resolveRehearsalConsumerRoot = async ({
|
|
142
167
|
packageRoot,
|
|
143
168
|
initCwd,
|
|
@@ -281,11 +306,20 @@ export const createInstalledRehearsalScaffold = async ({
|
|
|
281
306
|
isPortAvailable,
|
|
282
307
|
});
|
|
283
308
|
const result = await applyRehearsalSetup(plan);
|
|
284
|
-
await loadRehearsalConfig({
|
|
309
|
+
const loaded = await loadRehearsalConfig({
|
|
310
|
+
projectRoot: resolution.projectRoot,
|
|
311
|
+
});
|
|
312
|
+
const configurationPath = relative(resolution.projectRoot, loaded.configPath);
|
|
313
|
+
const configurationAction =
|
|
314
|
+
result.files.find(({ path }) => path === configurationPath)?.action ??
|
|
315
|
+
"unchanged";
|
|
285
316
|
return {
|
|
286
317
|
status: "ready",
|
|
287
318
|
projectRoot: resolution.projectRoot,
|
|
288
319
|
target: plan.target,
|
|
289
320
|
files: result.files,
|
|
321
|
+
configurationPath,
|
|
322
|
+
configurationAction,
|
|
323
|
+
rootConfigGenerationSkipped: configurationPath !== "rehearsal.config.mjs",
|
|
290
324
|
};
|
|
291
325
|
};
|
package/src/project/setup.mjs
CHANGED
|
@@ -374,6 +374,9 @@ export const planRehearsalSetup = async ({
|
|
|
374
374
|
];
|
|
375
375
|
return {
|
|
376
376
|
projectRoot: root,
|
|
377
|
+
configurationPath: existingConfigPath
|
|
378
|
+
? relative(root, existingConfigPath)
|
|
379
|
+
: CONFIG_PATH,
|
|
377
380
|
project: detected.projectName,
|
|
378
381
|
projectId,
|
|
379
382
|
target: runtimeTarget,
|
|
@@ -444,6 +447,11 @@ export const applyRehearsalSetup = async (plan) => {
|
|
|
444
447
|
|
|
445
448
|
export const summarizeRehearsalSetup = (plan, { mode }) => ({
|
|
446
449
|
mode,
|
|
450
|
+
configurationPath: plan.configurationPath,
|
|
451
|
+
configurationAction:
|
|
452
|
+
plan.files.find((file) => file.path === plan.configurationPath)?.action ??
|
|
453
|
+
"unchanged",
|
|
454
|
+
rootConfigGenerationSkipped: plan.configurationPath !== CONFIG_PATH,
|
|
447
455
|
project: plan.project,
|
|
448
456
|
projectId: plan.projectId,
|
|
449
457
|
target: plan.target,
|
|
@@ -461,6 +469,6 @@ export const summarizeRehearsalSetup = (plan, { mode }) => ({
|
|
|
461
469
|
safety: plan.safety,
|
|
462
470
|
nextAction:
|
|
463
471
|
mode === "written"
|
|
464
|
-
?
|
|
472
|
+
? `Review ${plan.configurationPath}, including its commands and sanitization policy path, then run rehearsal doctor.`
|
|
465
473
|
: "Review this plan, then rerun rehearsal setup --write to create it.",
|
|
466
474
|
});
|
package/src/source/access.mjs
CHANGED
|
@@ -115,6 +115,7 @@ export const validateSourceAccessPolicy = (policy) => {
|
|
|
115
115
|
: [
|
|
116
116
|
"mode",
|
|
117
117
|
"role",
|
|
118
|
+
"allowedMemberships",
|
|
118
119
|
"connectionEnvironmentVariable",
|
|
119
120
|
"credentialFile",
|
|
120
121
|
"maximumValidForMinutes",
|
|
@@ -137,6 +138,7 @@ export const validateSourceAccessPolicy = (policy) => {
|
|
|
137
138
|
let validForMinutes;
|
|
138
139
|
let connectionEnvironmentVariable;
|
|
139
140
|
let maximumValidForMinutes;
|
|
141
|
+
let allowedMemberships = Object.freeze([]);
|
|
140
142
|
if (readerMode === "managed") {
|
|
141
143
|
ownerRole = identifier(
|
|
142
144
|
policy.reader.ownerRole,
|
|
@@ -155,6 +157,33 @@ export const validateSourceAccessPolicy = (policy) => {
|
|
|
155
157
|
}
|
|
156
158
|
validForMinutes = policy.reader.validForMinutes;
|
|
157
159
|
} else {
|
|
160
|
+
if (
|
|
161
|
+
policy.reader.allowedMemberships !== undefined &&
|
|
162
|
+
(!Array.isArray(policy.reader.allowedMemberships) ||
|
|
163
|
+
policy.reader.allowedMemberships.length > 8)
|
|
164
|
+
) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
"sourcePolicy.reader.allowedMemberships must contain at most 8 role names.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
allowedMemberships = Object.freeze(
|
|
170
|
+
(policy.reader.allowedMemberships ?? []).map((membership, index) =>
|
|
171
|
+
identifier(
|
|
172
|
+
membership,
|
|
173
|
+
`sourcePolicy.reader.allowedMemberships[${index}]`,
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
if (new Set(allowedMemberships).size !== allowedMemberships.length) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
"sourcePolicy.reader.allowedMemberships must not contain duplicates.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (allowedMemberships.includes(role)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"sourcePolicy.reader.allowedMemberships must not include the login role.",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
158
187
|
connectionEnvironmentVariable = envKey(
|
|
159
188
|
policy.reader.connectionEnvironmentVariable,
|
|
160
189
|
"sourcePolicy.reader.connectionEnvironmentVariable",
|
|
@@ -170,13 +199,22 @@ export const validateSourceAccessPolicy = (policy) => {
|
|
|
170
199
|
}
|
|
171
200
|
maximumValidForMinutes = policy.reader.maximumValidForMinutes;
|
|
172
201
|
}
|
|
202
|
+
const credentialParts =
|
|
203
|
+
typeof policy.reader.credentialFile === "string"
|
|
204
|
+
? policy.reader.credentialFile.split(/[\\/]/u)
|
|
205
|
+
: [];
|
|
206
|
+
const rehearsalIndex = credentialParts.lastIndexOf(".rehearsal");
|
|
173
207
|
if (
|
|
174
208
|
typeof policy.reader.credentialFile !== "string" ||
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
policy.reader.credentialFile.startsWith("/") ||
|
|
210
|
+
/^[A-Za-z]:[\\/]/u.test(policy.reader.credentialFile) ||
|
|
211
|
+
credentialParts.some((part) => !part || part === "." || part === "..") ||
|
|
212
|
+
rehearsalIndex < 0 ||
|
|
213
|
+
credentialParts[rehearsalIndex + 1] !== "secrets" ||
|
|
214
|
+
rehearsalIndex + 2 >= credentialParts.length
|
|
177
215
|
) {
|
|
178
216
|
throw new Error(
|
|
179
|
-
"sourcePolicy.reader.credentialFile must
|
|
217
|
+
"sourcePolicy.reader.credentialFile must be a project-relative file under the configured .rehearsal/secrets directory.",
|
|
180
218
|
);
|
|
181
219
|
}
|
|
182
220
|
const exportSchema = identifier(
|
|
@@ -488,7 +526,11 @@ export const validateSourceAccessPolicy = (policy) => {
|
|
|
488
526
|
credentialFile: policy.reader.credentialFile,
|
|
489
527
|
...(readerMode === "managed"
|
|
490
528
|
? { ownerRole, validForMinutes }
|
|
491
|
-
: {
|
|
529
|
+
: {
|
|
530
|
+
allowedMemberships,
|
|
531
|
+
connectionEnvironmentVariable,
|
|
532
|
+
maximumValidForMinutes,
|
|
533
|
+
}),
|
|
492
534
|
}),
|
|
493
535
|
exportSchema,
|
|
494
536
|
migrationLedger,
|
|
@@ -580,6 +622,7 @@ export const createSourceAccessPlan = ({
|
|
|
580
622
|
: {
|
|
581
623
|
connectionEnvironmentVariable:
|
|
582
624
|
normalized.reader.connectionEnvironmentVariable,
|
|
625
|
+
allowedMemberships: normalized.reader.allowedMemberships,
|
|
583
626
|
maximumValidForMinutes: normalized.reader.maximumValidForMinutes,
|
|
584
627
|
sourceChanges: false,
|
|
585
628
|
}),
|
|
@@ -632,7 +675,9 @@ export const createSourceAccessPlan = ({
|
|
|
632
675
|
: [
|
|
633
676
|
"all non-system readable columns match the approved views and migration ledger",
|
|
634
677
|
"no relation, column, schema, or database writes",
|
|
635
|
-
"
|
|
678
|
+
"role memberships exactly match the reviewed allowlist",
|
|
679
|
+
"allowed groups cannot log in, escalate, switch roles, delegate membership, or inherit other roles",
|
|
680
|
+
"no privileged role attributes",
|
|
636
681
|
"no executable non-system security-definer functions",
|
|
637
682
|
"database-enforced credential expiration",
|
|
638
683
|
"unrelated private assets",
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const BUCKET = /^[a-z0-9][a-z0-9.-]{0,99}$/u;
|
|
4
4
|
const LOOPBACK = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
5
|
+
const MAXIMUM_STORAGE_DEPTH = 64;
|
|
6
|
+
const MAXIMUM_STORAGE_PATH_BYTES = 4096;
|
|
5
7
|
|
|
6
8
|
const safeBaseUrl = (value) => {
|
|
7
9
|
let url;
|
|
@@ -34,9 +36,20 @@ const safePath = (value, label) => {
|
|
|
34
36
|
) {
|
|
35
37
|
throw new Error(`${label} is unsafe.`);
|
|
36
38
|
}
|
|
39
|
+
if (Buffer.byteLength(value) > MAXIMUM_STORAGE_PATH_BYTES) {
|
|
40
|
+
throw new Error(`${label} is too long.`);
|
|
41
|
+
}
|
|
37
42
|
return value;
|
|
38
43
|
};
|
|
39
44
|
|
|
45
|
+
const safeInventoryName = (value) => {
|
|
46
|
+
const name = safePath(value, "Storage inventory name");
|
|
47
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
48
|
+
throw new Error("Storage inventory name is not a single path segment.");
|
|
49
|
+
}
|
|
50
|
+
return name;
|
|
51
|
+
};
|
|
52
|
+
|
|
40
53
|
const readError = (label, response) =>
|
|
41
54
|
new Error(
|
|
42
55
|
`${label} failed with HTTP ${response.status}; response content was withheld.`,
|
|
@@ -93,115 +106,154 @@ export const streamApprovedSupabaseAssets = async function* ({
|
|
|
93
106
|
if (!Array.isArray(declarations) || declarations.length === 0) return;
|
|
94
107
|
let objectCount = 0;
|
|
95
108
|
let totalBytes = 0;
|
|
109
|
+
const inventoryPaths = new Set();
|
|
96
110
|
for (const declaration of declarations) {
|
|
97
111
|
if (!BUCKET.test(declaration.bucket ?? "")) {
|
|
98
112
|
throw new Error("Approved Storage bucket is invalid.");
|
|
99
113
|
}
|
|
100
|
-
const
|
|
114
|
+
const approvedPrefix = safePath(
|
|
101
115
|
declaration.prefix.replace(/\/$/u, ""),
|
|
102
116
|
"Approved Storage prefix",
|
|
103
117
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"content-type": "application/json",
|
|
113
|
-
},
|
|
114
|
-
body: JSON.stringify({
|
|
115
|
-
prefix,
|
|
116
|
-
limit: 100,
|
|
117
|
-
offset,
|
|
118
|
-
sortBy: { column: "name", order: "asc" },
|
|
119
|
-
}),
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
if (!response.ok) throw readError("Storage inventory", response);
|
|
123
|
-
const inventory = await response.json();
|
|
124
|
-
if (!Array.isArray(inventory))
|
|
125
|
-
throw new Error("Storage inventory response is invalid.");
|
|
126
|
-
for (const object of inventory) {
|
|
127
|
-
const name = safePath(
|
|
128
|
-
`${prefix}/${object.name}`,
|
|
129
|
-
"Storage object path",
|
|
130
|
-
);
|
|
131
|
-
if (!name.startsWith(`${prefix}/`))
|
|
132
|
-
throw new Error("Storage inventory escaped its approved prefix.");
|
|
133
|
-
const expectedBytes = Number(
|
|
134
|
-
object.metadata?.size ?? object.metadata?.contentLength,
|
|
135
|
-
);
|
|
136
|
-
if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0) {
|
|
137
|
-
throw new Error("Storage inventory omitted an exact object size.");
|
|
138
|
-
}
|
|
139
|
-
if (expectedBytes > maximumObjectBytes) {
|
|
140
|
-
throw new Error("Storage object exceeds its reviewed size limit.");
|
|
141
|
-
}
|
|
142
|
-
objectCount += 1;
|
|
143
|
-
totalBytes += expectedBytes;
|
|
144
|
-
if (objectCount > maximumObjects || totalBytes > maximumTotalBytes) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
"Storage inventory exceeds its reviewed transfer boundary.",
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
const encodedPath = name.split("/").map(encodeURIComponent).join("/");
|
|
150
|
-
const download = await fetchImplementation(
|
|
151
|
-
`${endpoint}/storage/v1/object/authenticated/${encodeURIComponent(declaration.bucket)}/${encodedPath}`,
|
|
118
|
+
const prefixes = [{ path: approvedPrefix, depth: 0 }];
|
|
119
|
+
const scheduledPrefixes = new Set([approvedPrefix]);
|
|
120
|
+
for (let prefixIndex = 0; prefixIndex < prefixes.length; prefixIndex += 1) {
|
|
121
|
+
const prefix = prefixes[prefixIndex];
|
|
122
|
+
let offset = 0;
|
|
123
|
+
while (true) {
|
|
124
|
+
const response = await fetchImplementation(
|
|
125
|
+
`${endpoint}/storage/v1/object/list/${encodeURIComponent(declaration.bucket)}`,
|
|
152
126
|
{
|
|
127
|
+
method: "POST",
|
|
153
128
|
headers: {
|
|
154
129
|
authorization: `Bearer ${token}`,
|
|
155
|
-
|
|
156
|
-
? { "if-match": object.metadata.eTag }
|
|
157
|
-
: {}),
|
|
130
|
+
"content-type": "application/json",
|
|
158
131
|
},
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
prefix: prefix.path,
|
|
134
|
+
limit: 100,
|
|
135
|
+
offset,
|
|
136
|
+
sortBy: { column: "name", order: "asc" },
|
|
137
|
+
}),
|
|
159
138
|
},
|
|
160
139
|
);
|
|
161
|
-
if (!
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
|
|
140
|
+
if (!response.ok) throw readError("Storage inventory", response);
|
|
141
|
+
const inventory = await response.json();
|
|
142
|
+
if (!Array.isArray(inventory))
|
|
143
|
+
throw new Error("Storage inventory response is invalid.");
|
|
144
|
+
for (const object of inventory) {
|
|
145
|
+
if (
|
|
146
|
+
object === null ||
|
|
147
|
+
typeof object !== "object" ||
|
|
148
|
+
Array.isArray(object)
|
|
149
|
+
) {
|
|
150
|
+
throw new Error("Storage inventory entry is invalid.");
|
|
151
|
+
}
|
|
152
|
+
const entryName = safeInventoryName(object.name);
|
|
153
|
+
const name = safePath(
|
|
154
|
+
`${prefix.path}/${entryName}`,
|
|
155
|
+
"Storage object path",
|
|
156
|
+
);
|
|
157
|
+
if (!name.startsWith(`${approvedPrefix}/`))
|
|
158
|
+
throw new Error("Storage inventory escaped its approved prefix.");
|
|
159
|
+
const inventoryKey = `${declaration.bucket}\0${name}`;
|
|
160
|
+
if (inventoryPaths.has(inventoryKey)) {
|
|
161
|
+
throw new Error("Storage inventory returned a duplicate path.");
|
|
162
|
+
}
|
|
163
|
+
inventoryPaths.add(inventoryKey);
|
|
164
|
+
objectCount += 1;
|
|
165
|
+
if (objectCount > maximumObjects) {
|
|
165
166
|
throw new Error(
|
|
166
|
-
"Storage
|
|
167
|
+
"Storage inventory exceeds its reviewed transfer boundary.",
|
|
167
168
|
);
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
+
const directory = object.id === null && object.metadata === null;
|
|
171
|
+
if (directory) {
|
|
172
|
+
if (prefix.depth >= MAXIMUM_STORAGE_DEPTH) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"Storage inventory exceeds its safe folder depth.",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (scheduledPrefixes.has(name)) {
|
|
178
|
+
throw new Error("Storage inventory returned a duplicate folder.");
|
|
179
|
+
}
|
|
180
|
+
scheduledPrefixes.add(name);
|
|
181
|
+
prefixes.push({ path: name, depth: prefix.depth + 1 });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const expectedBytes = Number(
|
|
185
|
+
object.metadata?.size ?? object.metadata?.contentLength,
|
|
186
|
+
);
|
|
187
|
+
if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0) {
|
|
188
|
+
throw new Error("Storage inventory omitted an exact object size.");
|
|
189
|
+
}
|
|
190
|
+
if (expectedBytes > maximumObjectBytes) {
|
|
191
|
+
throw new Error("Storage object exceeds its reviewed size limit.");
|
|
192
|
+
}
|
|
193
|
+
totalBytes += expectedBytes;
|
|
194
|
+
if (totalBytes > maximumTotalBytes) {
|
|
170
195
|
throw new Error(
|
|
171
|
-
"Storage
|
|
196
|
+
"Storage inventory exceeds its reviewed transfer boundary.",
|
|
172
197
|
);
|
|
173
198
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
const encodedPath = name.split("/").map(encodeURIComponent).join("/");
|
|
200
|
+
const download = await fetchImplementation(
|
|
201
|
+
`${endpoint}/storage/v1/object/authenticated/${encodeURIComponent(declaration.bucket)}/${encodedPath}`,
|
|
202
|
+
{
|
|
203
|
+
headers: {
|
|
204
|
+
authorization: `Bearer ${token}`,
|
|
205
|
+
...(object.metadata?.eTag
|
|
206
|
+
? { "if-match": object.metadata.eTag }
|
|
207
|
+
: {}),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
178
210
|
);
|
|
211
|
+
if (!download.ok)
|
|
212
|
+
throw readError("Storage object transfer", download);
|
|
213
|
+
const responseEtag = download.headers.get("etag");
|
|
214
|
+
if (object.metadata?.eTag) {
|
|
215
|
+
if (!responseEtag) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"Storage transfer omitted the reviewed object version.",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (responseEtag !== object.metadata.eTag) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Storage object changed between inventory and transfer.",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (declaration.pathMapping && typeof pathMapper !== "function") {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"Storage path mapping requires the package-owned privacy mapper.",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const objectPath = declaration.pathMapping
|
|
232
|
+
? safePath(
|
|
233
|
+
pathMapper({
|
|
234
|
+
mapping: declaration.pathMapping,
|
|
235
|
+
value: name,
|
|
236
|
+
}),
|
|
237
|
+
"Mapped Storage object path",
|
|
238
|
+
)
|
|
239
|
+
: name;
|
|
240
|
+
yield {
|
|
241
|
+
bucket: declaration.bucket,
|
|
242
|
+
objectPath,
|
|
243
|
+
contentType: canonicalContentType(
|
|
244
|
+
object.metadata?.mimetype,
|
|
245
|
+
download.headers.get("content-type"),
|
|
246
|
+
),
|
|
247
|
+
content: responseBody({
|
|
248
|
+
response: download,
|
|
249
|
+
expectedBytes,
|
|
250
|
+
maximumBytes: maximumObjectBytes,
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
179
253
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
pathMapper({
|
|
183
|
-
mapping: declaration.pathMapping,
|
|
184
|
-
value: name,
|
|
185
|
-
}),
|
|
186
|
-
"Mapped Storage object path",
|
|
187
|
-
)
|
|
188
|
-
: name;
|
|
189
|
-
yield {
|
|
190
|
-
bucket: declaration.bucket,
|
|
191
|
-
objectPath,
|
|
192
|
-
contentType: canonicalContentType(
|
|
193
|
-
object.metadata?.mimetype,
|
|
194
|
-
download.headers.get("content-type"),
|
|
195
|
-
),
|
|
196
|
-
content: responseBody({
|
|
197
|
-
response: download,
|
|
198
|
-
expectedBytes,
|
|
199
|
-
maximumBytes: maximumObjectBytes,
|
|
200
|
-
}),
|
|
201
|
-
};
|
|
254
|
+
if (inventory.length < 100) break;
|
|
255
|
+
offset += inventory.length;
|
|
202
256
|
}
|
|
203
|
-
if (inventory.length < 100) break;
|
|
204
|
-
offset += inventory.length;
|
|
205
257
|
}
|
|
206
258
|
}
|
|
207
259
|
};
|