@learnpack/learnpack 5.0.353 → 5.0.355

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.
Files changed (35) hide show
  1. package/lib/commands/serve.js +1 -1
  2. package/lib/scripts/descriptionsS3Backfill.js +142 -22
  3. package/lib/utils/awsCredentials.d.ts +20 -0
  4. package/lib/utils/awsCredentials.js +43 -0
  5. package/lib/utils/descriptions/backfillEvents.d.ts +60 -0
  6. package/lib/utils/descriptions/backfillEvents.js +107 -0
  7. package/lib/utils/descriptions/generateCourseDescriptions.d.ts +7 -0
  8. package/lib/utils/descriptions/generateCourseDescriptions.js +3 -0
  9. package/lib/utils/descriptions/publishStage.js +14 -3
  10. package/lib/utils/descriptions/resumePublication.js +17 -2
  11. package/lib/utils/descriptions/s3Storage.js +13 -6
  12. package/lib/utils/packageManifest.d.ts +8 -0
  13. package/lib/utils/packageManifest.js +8 -0
  14. package/lib/utils/repair/legacyPackageRepair.d.ts +131 -0
  15. package/lib/utils/repair/legacyPackageRepair.js +492 -0
  16. package/lib/utils/repair/repairStorage.d.ts +68 -0
  17. package/lib/utils/repair/repairStorage.js +89 -0
  18. package/lib/utils/s3/packageManifestBackfill.js +3 -8
  19. package/lib/utils/s3/packageSourcesAudit.d.ts +75 -0
  20. package/lib/utils/s3/packageSourcesAudit.js +184 -0
  21. package/package.json +3 -1
  22. package/src/commands/serve.ts +1 -1
  23. package/src/scripts/README.md +244 -0
  24. package/src/scripts/descriptionsS3Backfill.ts +188 -20
  25. package/src/utils/awsCredentials.ts +57 -0
  26. package/src/utils/descriptions/backfillEvents.ts +152 -0
  27. package/src/utils/descriptions/generateCourseDescriptions.ts +10 -0
  28. package/src/utils/descriptions/publishStage.ts +394 -382
  29. package/src/utils/descriptions/resumePublication.ts +217 -200
  30. package/src/utils/descriptions/s3Storage.ts +214 -206
  31. package/src/utils/packageManifest.ts +8 -1
  32. package/src/utils/repair/legacyPackageRepair.ts +731 -0
  33. package/src/utils/repair/repairStorage.ts +168 -0
  34. package/src/utils/s3/packageManifestBackfill.ts +771 -776
  35. package/src/utils/s3/packageSourcesAudit.ts +311 -0
@@ -0,0 +1,68 @@
1
+ import { Bucket } from "@google-cloud/storage";
2
+ import { AwsClient } from "../s3/packageManifestBackfill";
3
+ /**
4
+ * The two buckets a package lives in, behind one interface.
5
+ *
6
+ * A legacy package is repaired **twice, independently** — once against the
7
+ * published snapshot in S3 and once against the draft in GCS — never by copying
8
+ * one into the other. The two hold different content (the draft keeps moving
9
+ * after a publication) and the synthesized structure is a projection of that
10
+ * content: a syllabus derived from the snapshot could name steps the draft does
11
+ * not have. This is the same rule `mirrorDescriptions` enforces with its content
12
+ * hash; deriving from each bucket needs no guard at all.
13
+ *
14
+ * What crosses between them is only the descriptions, through
15
+ * `descriptionsS3Backfill --mirror-draft`, which is hash-guarded per step.
16
+ */
17
+ export type RepairTarget = "s3" | "gcs";
18
+ export declare const ALL_REPAIR_TARGETS: RepairTarget[];
19
+ /**
20
+ * Where each input lives. The two buckets do not agree on this, and the
21
+ * disagreement is load-bearing:
22
+ *
23
+ * - S3 reads `{slug}/config.json` first (`fetchPackageSources`), which is where
24
+ * the publish writes it into the zip.
25
+ * - GCS reads `courses/{slug}/.learn/config.json` — the publish route downloads
26
+ * that one before anything else, so a config written anywhere else in the
27
+ * draft would not make the package publishable.
28
+ */
29
+ export type PackageLayout = {
30
+ learnJsonKey(slug: string): string;
31
+ /** Config locations in resolution order; the first is the write target. */
32
+ configKeys(slug: string): string[];
33
+ syllabusKey(slug: string): string;
34
+ /** Sidebar locations in resolution order; the first is the write target. */
35
+ sidebarKeys(slug: string): string[];
36
+ exercisesPrefix(slug: string): string;
37
+ /**
38
+ * Whether `config.json` is the authority on the exercise list, or the folder
39
+ * listing is.
40
+ *
41
+ * This decides the **language keys** of the synthesized syllabus, and the two
42
+ * buckets genuinely disagree:
43
+ *
44
+ * - S3 reads the published `config.json` (`fetchPackageSources`), where these
45
+ * legacy packages carry `us` for the unsuffixed README.
46
+ * - GCS has no such reader. Both the publish-time manifest and the
47
+ * descriptions pipeline go through `configBuilder.buildConfig`, which
48
+ * derives exercises from the listing and maps an unsuffixed `README.md` to
49
+ * `en` — regardless of what the draft's `.learn/config.json` says.
50
+ *
51
+ * A syllabus keyed the other way is not read at all: the manifest would look
52
+ * up `translations["en"]`, find nothing, and publish `description: null` for
53
+ * every step. So each side is built the way its own reader will read it.
54
+ */
55
+ exercisesFromConfig: boolean;
56
+ };
57
+ export type RepairStorage = {
58
+ target: RepairTarget;
59
+ layout: PackageLayout;
60
+ readJson<T>(key: string): Promise<T | null>;
61
+ readText(key: string): Promise<string | null>;
62
+ listKeys(prefix: string): Promise<string[]>;
63
+ writeJson(key: string, value: unknown): Promise<void>;
64
+ };
65
+ export declare const s3Layout: PackageLayout;
66
+ export declare const gcsLayout: PackageLayout;
67
+ export declare function createS3RepairStorage(s3: AwsClient, bucket: string): RepairStorage;
68
+ export declare function createGcsRepairStorage(bucket: Bucket): RepairStorage;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gcsLayout = exports.s3Layout = exports.ALL_REPAIR_TARGETS = void 0;
4
+ exports.createS3RepairStorage = createS3RepairStorage;
5
+ exports.createGcsRepairStorage = createGcsRepairStorage;
6
+ const client_s3_1 = require("@aws-sdk/client-s3");
7
+ const packageManifestBackfill_1 = require("../s3/packageManifestBackfill");
8
+ exports.ALL_REPAIR_TARGETS = ["s3", "gcs"];
9
+ exports.s3Layout = {
10
+ learnJsonKey: slug => `${slug}/learn.json`,
11
+ configKeys: slug => [`${slug}/config.json`, `${slug}/.learn/config.json`],
12
+ syllabusKey: slug => `${slug}/.learn/initialSyllabus.json`,
13
+ // 25 of the published packages keep their sidebar at the root. It has to be
14
+ // resolved before writing: `fetchPackageSources` prefers the `.learn/` copy,
15
+ // so a synthesized one would shadow the published titles.
16
+ sidebarKeys: slug => [
17
+ `${slug}/.learn/sidebar.json`,
18
+ `${slug}/sidebar.json`,
19
+ ],
20
+ exercisesPrefix: slug => `${slug}/exercises/`,
21
+ exercisesFromConfig: true,
22
+ };
23
+ exports.gcsLayout = {
24
+ learnJsonKey: slug => `courses/${slug}/learn.json`,
25
+ configKeys: slug => [
26
+ `courses/${slug}/.learn/config.json`,
27
+ `courses/${slug}/config.json`,
28
+ ],
29
+ syllabusKey: slug => `courses/${slug}/.learn/initialSyllabus.json`,
30
+ sidebarKeys: slug => [`courses/${slug}/.learn/sidebar.json`],
31
+ exercisesPrefix: slug => `courses/${slug}/exercises/`,
32
+ exercisesFromConfig: false,
33
+ };
34
+ function createS3RepairStorage(s3, bucket) {
35
+ return {
36
+ target: "s3",
37
+ layout: exports.s3Layout,
38
+ readJson: key => (0, packageManifestBackfill_1.fetchJsonObject)(s3, bucket, key),
39
+ readText: key => (0, packageManifestBackfill_1.fetchTextObject)(s3, bucket, key),
40
+ listKeys: prefix => (0, packageManifestBackfill_1.listObjectKeys)(s3, bucket, prefix),
41
+ async writeJson(key, value) {
42
+ await (0, packageManifestBackfill_1.withRetry)(() => s3.send(new client_s3_1.PutObjectCommand({
43
+ Bucket: bucket,
44
+ Key: key,
45
+ Body: JSON.stringify(value, null, 2),
46
+ ContentType: "application/json",
47
+ })));
48
+ },
49
+ };
50
+ }
51
+ function createGcsRepairStorage(bucket) {
52
+ const download = async (key) => {
53
+ try {
54
+ const [buf] = await bucket.file(key).download();
55
+ return buf.toString();
56
+ }
57
+ catch (_a) {
58
+ return null;
59
+ }
60
+ };
61
+ return {
62
+ target: "gcs",
63
+ layout: exports.gcsLayout,
64
+ async readJson(key) {
65
+ const content = await download(key);
66
+ if (content === null) {
67
+ return null;
68
+ }
69
+ try {
70
+ return JSON.parse(content);
71
+ }
72
+ catch (_a) {
73
+ return null;
74
+ }
75
+ },
76
+ readText: download,
77
+ async listKeys(prefix) {
78
+ const [files] = await bucket.getFiles({ prefix });
79
+ return files.map(file => file.name);
80
+ },
81
+ async writeJson(key, value) {
82
+ await bucket
83
+ .file(key)
84
+ .save(Buffer.from(JSON.stringify(value, null, 2), "utf8"), {
85
+ contentType: "application/json",
86
+ });
87
+ },
88
+ };
89
+ }
@@ -16,6 +16,7 @@ exports.getBatchExitCode = getBatchExitCode;
16
16
  const tslib_1 = require("tslib");
17
17
  const client_cloudfront_1 = require("@aws-sdk/client-cloudfront");
18
18
  const client_s3_1 = require("@aws-sdk/client-s3");
19
+ const awsCredentials_1 = require("../awsCredentials");
19
20
  const packageManifest_1 = require("../packageManifest");
20
21
  const README_FILENAME_PATTERN = /^readme(?:\.[a-z]{2})?\.md$/i;
21
22
  const INDEX_HTML_PATTERN = /^[^/]+\/index\.html$/;
@@ -389,18 +390,12 @@ async function runBatch(options, clients) {
389
390
  const s3 = (clients === null || clients === void 0 ? void 0 : clients.s3) ||
390
391
  new client_s3_1.S3Client({
391
392
  region: options.region,
392
- credentials: {
393
- accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
394
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
395
- },
393
+ credentials: (0, awsCredentials_1.requireAwsCredentials)(),
396
394
  });
397
395
  const cf = (clients === null || clients === void 0 ? void 0 : clients.cf) ||
398
396
  new client_cloudfront_1.CloudFrontClient({
399
397
  region: "us-east-1",
400
- credentials: {
401
- accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
402
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
403
- },
398
+ credentials: (0, awsCredentials_1.requireAwsCredentials)(),
404
399
  });
405
400
  let slugs = options.slug ?
406
401
  [options.slug] :
@@ -0,0 +1,75 @@
1
+ import { AwsClient } from "./packageManifestBackfill";
2
+ /**
3
+ * Inventory of the files a published package needs for its
4
+ * `package-manifest.json` to be generated at full fidelity.
5
+ *
6
+ * This reads **key names only** — one paginated `ListObjectsV2` over the bucket,
7
+ * zero `GetObject`. Cheap enough to run over the whole catalogue on demand, and
8
+ * safe at any time: it never writes.
9
+ *
10
+ * What it answers: which packages would *fail* the manifest backfill (no
11
+ * `config.json`, no exercises), and which ones only produce a *degraded*
12
+ * manifest (no `initialSyllabus.json` → every lesson typed `READ`, ids derived
13
+ * from the folder name, and descriptions permanently `null`, because the
14
+ * descriptions pipeline builds its work list from the syllabus keys and cannot
15
+ * see a course that has none).
16
+ *
17
+ * Companion of `legacyPackageRepair.ts`, which writes what this reports missing.
18
+ */
19
+ export type PackageSourcesAuditEntry = {
20
+ slug: string;
21
+ /** A published package is a prefix holding an `index.html` at its root. */
22
+ hasIndexHtml: boolean;
23
+ /** Required: without it the manifest backfill reports `failed`. */
24
+ hasConfigJson: boolean;
25
+ configJsonKey: string | null;
26
+ /**
27
+ * Optional *as a key*: `config.json` may carry the same metadata under
28
+ * `config`. Only a `GetObject` could tell, so `false` here means "resolves
29
+ * through the fallback, or not at all" — not "broken".
30
+ */
31
+ hasLearnJson: boolean;
32
+ hasSyllabus: boolean;
33
+ hasSidebar: boolean;
34
+ sidebarKey: string | null;
35
+ hasManifest: boolean;
36
+ /** Folders directly under `exercises/` holding at least one object. */
37
+ exerciseFolders: number;
38
+ /** `README.md` / `README.xx.md` files found in those folders. */
39
+ readmeCount: number;
40
+ /** Missing inputs, named as in the docs, for quick reporting. */
41
+ missing: string[];
42
+ /** True when the manifest backfill cannot produce a manifest at all. */
43
+ blocking: boolean;
44
+ };
45
+ export type PackageSourcesAuditSummary = {
46
+ missingConfigJson: number;
47
+ missingLearnJson: number;
48
+ missingSyllabus: number;
49
+ missingSidebar: number;
50
+ missingManifest: number;
51
+ noExerciseFolders: number;
52
+ blocking: number;
53
+ };
54
+ export type PackageSourcesAuditReport = {
55
+ generatedAt: string;
56
+ bucket: string;
57
+ scannedKeys: number;
58
+ totalPackages: number;
59
+ summary: PackageSourcesAuditSummary;
60
+ packages: PackageSourcesAuditEntry[];
61
+ };
62
+ export type AuditOptions = {
63
+ /** Restrict the scan to a single package (uses a prefixed listing). */
64
+ slug?: string;
65
+ /** Keep prefixes with no `index.html` (drafts, leftovers). Default false. */
66
+ includeUnpublished?: boolean;
67
+ };
68
+ export declare function summarize(packages: PackageSourcesAuditEntry[]): PackageSourcesAuditSummary;
69
+ /**
70
+ * One paginated pass over the bucket, folding each page into the accumulator.
71
+ *
72
+ * Deliberately not built on `listObjectKeys`: that one materializes every key in
73
+ * the catalogue before the caller sees the first one.
74
+ */
75
+ export declare function auditPackageSources(s3: AwsClient, bucket: string, options?: AuditOptions): Promise<PackageSourcesAuditReport>;
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.summarize = summarize;
4
+ exports.auditPackageSources = auditPackageSources;
5
+ const client_s3_1 = require("@aws-sdk/client-s3");
6
+ const packageManifestBackfill_1 = require("./packageManifestBackfill");
7
+ const README_FILENAME_PATTERN = /^readme(?:\.[a-z]{2})?\.md$/i;
8
+ const ROOT_FILES = new Set([
9
+ "index.html",
10
+ "learn.json",
11
+ "config.json",
12
+ ".learn/config.json",
13
+ ".learn/initialSyllabus.json",
14
+ ".learn/sidebar.json",
15
+ "sidebar.json",
16
+ ".learn/package-manifest.json",
17
+ ]);
18
+ function emptyAccumulator(slug) {
19
+ return {
20
+ slug,
21
+ hasIndexHtml: false,
22
+ configJsonKey: null,
23
+ hasLearnJson: false,
24
+ hasSyllabus: false,
25
+ sidebarKey: null,
26
+ hasManifest: false,
27
+ exerciseFolders: new Set(),
28
+ readmeCount: 0,
29
+ };
30
+ }
31
+ /** Fold one object key into the accumulator map. */
32
+ function classifyKey(entries, key) {
33
+ const separatorIndex = key.indexOf("/");
34
+ if (separatorIndex <= 0) {
35
+ // Object at the bucket root: not part of any package.
36
+ return;
37
+ }
38
+ const slug = key.slice(0, separatorIndex);
39
+ const rest = key.slice(separatorIndex + 1);
40
+ if (!rest) {
41
+ return;
42
+ }
43
+ const exerciseParts = rest.startsWith("exercises/") ? rest.split("/") : [];
44
+ // `exercises/<slug>/<file>`: shorter paths are the folder marker itself.
45
+ const exerciseSlug = exerciseParts.length >= 3 && exerciseParts[1] ? exerciseParts[1] : null;
46
+ if (!ROOT_FILES.has(rest) && !exerciseSlug) {
47
+ return;
48
+ }
49
+ let entry = entries.get(slug);
50
+ if (!entry) {
51
+ entry = emptyAccumulator(slug);
52
+ entries.set(slug, entry);
53
+ }
54
+ if (exerciseSlug) {
55
+ entry.exerciseFolders.add(exerciseSlug);
56
+ const filename = exerciseParts[exerciseParts.length - 1];
57
+ if (exerciseParts.length === 3 && README_FILENAME_PATTERN.test(filename)) {
58
+ entry.readmeCount += 1;
59
+ }
60
+ return;
61
+ }
62
+ switch (rest) {
63
+ case "index.html":
64
+ entry.hasIndexHtml = true;
65
+ break;
66
+ case "learn.json":
67
+ entry.hasLearnJson = true;
68
+ break;
69
+ // The root copy wins over `.learn/config.json`, matching the order
70
+ // `fetchPackageSources` resolves them in. Same for the sidebar, inverted.
71
+ case "config.json":
72
+ entry.configJsonKey = key;
73
+ break;
74
+ case ".learn/config.json":
75
+ entry.configJsonKey = entry.configJsonKey || key;
76
+ break;
77
+ case ".learn/sidebar.json":
78
+ entry.sidebarKey = key;
79
+ break;
80
+ case "sidebar.json":
81
+ entry.sidebarKey = entry.sidebarKey || key;
82
+ break;
83
+ case ".learn/initialSyllabus.json":
84
+ entry.hasSyllabus = true;
85
+ break;
86
+ case ".learn/package-manifest.json":
87
+ entry.hasManifest = true;
88
+ break;
89
+ default:
90
+ break;
91
+ }
92
+ }
93
+ function finalizeEntry(entry) {
94
+ const missing = [];
95
+ if (!entry.configJsonKey) {
96
+ missing.push("config.json");
97
+ }
98
+ if (!entry.hasLearnJson) {
99
+ missing.push("learn.json");
100
+ }
101
+ if (!entry.hasSyllabus) {
102
+ missing.push("initialSyllabus.json");
103
+ }
104
+ if (!entry.sidebarKey) {
105
+ missing.push("sidebar.json");
106
+ }
107
+ if (entry.exerciseFolders.size === 0) {
108
+ missing.push("exercises/");
109
+ }
110
+ if (!entry.hasManifest) {
111
+ missing.push("package-manifest.json");
112
+ }
113
+ return {
114
+ slug: entry.slug,
115
+ hasIndexHtml: entry.hasIndexHtml,
116
+ hasConfigJson: Boolean(entry.configJsonKey),
117
+ configJsonKey: entry.configJsonKey,
118
+ hasLearnJson: entry.hasLearnJson,
119
+ hasSyllabus: entry.hasSyllabus,
120
+ hasSidebar: Boolean(entry.sidebarKey),
121
+ sidebarKey: entry.sidebarKey,
122
+ hasManifest: entry.hasManifest,
123
+ exerciseFolders: entry.exerciseFolders.size,
124
+ readmeCount: entry.readmeCount,
125
+ missing,
126
+ // A missing `learn.json` is not blocking on its own: `config.config` can
127
+ // stand in for it, and only a download would tell.
128
+ blocking: !entry.configJsonKey || entry.exerciseFolders.size === 0,
129
+ };
130
+ }
131
+ function count(packages, predicate) {
132
+ return packages.filter(entry => predicate(entry)).length;
133
+ }
134
+ function summarize(packages) {
135
+ return {
136
+ missingConfigJson: count(packages, entry => !entry.hasConfigJson),
137
+ missingLearnJson: count(packages, entry => !entry.hasLearnJson),
138
+ missingSyllabus: count(packages, entry => !entry.hasSyllabus),
139
+ missingSidebar: count(packages, entry => !entry.hasSidebar),
140
+ missingManifest: count(packages, entry => !entry.hasManifest),
141
+ noExerciseFolders: count(packages, entry => entry.exerciseFolders === 0),
142
+ blocking: count(packages, entry => entry.blocking),
143
+ };
144
+ }
145
+ /**
146
+ * One paginated pass over the bucket, folding each page into the accumulator.
147
+ *
148
+ * Deliberately not built on `listObjectKeys`: that one materializes every key in
149
+ * the catalogue before the caller sees the first one.
150
+ */
151
+ async function auditPackageSources(s3, bucket, options = {}) {
152
+ const entries = new Map();
153
+ const prefix = options.slug ? `${options.slug}/` : undefined;
154
+ let continuationToken;
155
+ let scannedKeys = 0;
156
+ do {
157
+ // eslint-disable-next-line no-await-in-loop -- S3 pagination is sequential
158
+ const response = (await (0, packageManifestBackfill_1.withRetry)(() => s3.send(new client_s3_1.ListObjectsV2Command({
159
+ Bucket: bucket,
160
+ Prefix: prefix,
161
+ ContinuationToken: continuationToken,
162
+ }))));
163
+ for (const item of response.Contents || []) {
164
+ if (!item.Key) {
165
+ continue;
166
+ }
167
+ scannedKeys += 1;
168
+ classifyKey(entries, item.Key);
169
+ }
170
+ continuationToken = response.NextContinuationToken;
171
+ } while (continuationToken);
172
+ const packages = [...entries.values()]
173
+ .filter(entry => options.includeUnpublished || entry.hasIndexHtml)
174
+ .map(entry => finalizeEntry(entry))
175
+ .sort((a, b) => a.slug.localeCompare(b.slug));
176
+ return {
177
+ generatedAt: new Date().toISOString(),
178
+ bucket,
179
+ scannedKeys,
180
+ totalPackages: packages.length,
181
+ summary: summarize(packages),
182
+ packages,
183
+ };
184
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.353",
4
+ "version": "5.0.355",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -163,7 +163,9 @@
163
163
  ]
164
164
  },
165
165
  "scripts": {
166
+ "audit:package-sources": "ts-node --project ./tsconfig.json ./scripts/audit-package-sources.ts",
166
167
  "backfill:package-manifest": "ts-node --project ./tsconfig.json ./scripts/backfill-package-manifest-s3.ts",
168
+ "repair:legacy-packages": "ts-node --project ./tsconfig.json ./scripts/repair-legacy-packages.ts",
167
169
  "copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose && npx cpy src/utils/templates/**/* lib/utils/templates --parents --verbose && npx cpy src/lua/**/* lib/lua --parents --verbose",
168
170
  "tsc": "tsc -b",
169
171
  "postpack": "rm -f oclif.manifest.json && eslint . --ext .ts --config .eslintrc",
@@ -6731,7 +6731,7 @@ class ServeCommand extends SessionCommand {
6731
6731
 
6732
6732
  const buffer = Buffer.from(content, "base64")
6733
6733
 
6734
- if (isImageFile(item.relativePath)) {
6734
+ if (item.isAsset || isImageFile(item.relativePath)) {
6735
6735
  // eslint-disable-next-line no-await-in-loop -- Sequential processing to avoid rate limits
6736
6736
  await uploadBinaryToBucket(
6737
6737
  bucket,