@transloadit/node 4.2.0 → 4.3.1

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 (86) hide show
  1. package/README.md +116 -4
  2. package/dist/Transloadit.d.ts +45 -4
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +104 -21
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/assembly-linter.d.ts +123 -0
  7. package/dist/alphalib/assembly-linter.d.ts.map +1 -0
  8. package/dist/alphalib/assembly-linter.js +1142 -0
  9. package/dist/alphalib/assembly-linter.js.map +1 -0
  10. package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
  11. package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
  12. package/dist/alphalib/assembly-linter.lang.en.js +326 -0
  13. package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
  14. package/dist/alphalib/goldenTemplates.d.ts +52 -0
  15. package/dist/alphalib/goldenTemplates.d.ts.map +1 -0
  16. package/dist/alphalib/goldenTemplates.js +46 -0
  17. package/dist/alphalib/goldenTemplates.js.map +1 -0
  18. package/dist/alphalib/object.d.ts +20 -0
  19. package/dist/alphalib/object.d.ts.map +1 -0
  20. package/dist/alphalib/object.js +23 -0
  21. package/dist/alphalib/object.js.map +1 -0
  22. package/dist/alphalib/stepParsing.d.ts +93 -0
  23. package/dist/alphalib/stepParsing.d.ts.map +1 -0
  24. package/dist/alphalib/stepParsing.js +1154 -0
  25. package/dist/alphalib/stepParsing.js.map +1 -0
  26. package/dist/alphalib/templateMerge.d.ts +4 -0
  27. package/dist/alphalib/templateMerge.d.ts.map +1 -0
  28. package/dist/alphalib/templateMerge.js +22 -0
  29. package/dist/alphalib/templateMerge.js.map +1 -0
  30. package/dist/cli/commands/assemblies.d.ts +20 -1
  31. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  32. package/dist/cli/commands/assemblies.js +137 -2
  33. package/dist/cli/commands/assemblies.js.map +1 -1
  34. package/dist/cli/commands/auth.d.ts.map +1 -1
  35. package/dist/cli/commands/auth.js +19 -19
  36. package/dist/cli/commands/auth.js.map +1 -1
  37. package/dist/cli/commands/index.d.ts.map +1 -1
  38. package/dist/cli/commands/index.js +2 -1
  39. package/dist/cli/commands/index.js.map +1 -1
  40. package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
  41. package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
  42. package/dist/cli/docs/assemblyLintingExamples.js +10 -0
  43. package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
  44. package/dist/cli/helpers.d.ts +11 -0
  45. package/dist/cli/helpers.d.ts.map +1 -1
  46. package/dist/cli/helpers.js +29 -0
  47. package/dist/cli/helpers.js.map +1 -1
  48. package/dist/inputFiles.d.ts +41 -0
  49. package/dist/inputFiles.d.ts.map +1 -0
  50. package/dist/inputFiles.js +214 -0
  51. package/dist/inputFiles.js.map +1 -0
  52. package/dist/lintAssemblyInput.d.ts +10 -0
  53. package/dist/lintAssemblyInput.d.ts.map +1 -0
  54. package/dist/lintAssemblyInput.js +73 -0
  55. package/dist/lintAssemblyInput.js.map +1 -0
  56. package/dist/lintAssemblyInstructions.d.ts +29 -0
  57. package/dist/lintAssemblyInstructions.d.ts.map +1 -0
  58. package/dist/lintAssemblyInstructions.js +33 -0
  59. package/dist/lintAssemblyInstructions.js.map +1 -0
  60. package/dist/robots.d.ts +38 -0
  61. package/dist/robots.d.ts.map +1 -0
  62. package/dist/robots.js +230 -0
  63. package/dist/robots.js.map +1 -0
  64. package/dist/tus.d.ts +5 -1
  65. package/dist/tus.d.ts.map +1 -1
  66. package/dist/tus.js +80 -6
  67. package/dist/tus.js.map +1 -1
  68. package/package.json +5 -2
  69. package/src/Transloadit.ts +170 -26
  70. package/src/alphalib/assembly-linter.lang.en.ts +393 -0
  71. package/src/alphalib/assembly-linter.ts +1475 -0
  72. package/src/alphalib/goldenTemplates.ts +53 -0
  73. package/src/alphalib/object.ts +27 -0
  74. package/src/alphalib/stepParsing.ts +1465 -0
  75. package/src/alphalib/templateMerge.ts +32 -0
  76. package/src/alphalib/typings/json-to-ast.d.ts +34 -0
  77. package/src/cli/commands/assemblies.ts +161 -2
  78. package/src/cli/commands/auth.ts +19 -22
  79. package/src/cli/commands/index.ts +2 -0
  80. package/src/cli/docs/assemblyLintingExamples.ts +9 -0
  81. package/src/cli/helpers.ts +50 -0
  82. package/src/inputFiles.ts +278 -0
  83. package/src/lintAssemblyInput.ts +89 -0
  84. package/src/lintAssemblyInstructions.ts +72 -0
  85. package/src/robots.ts +317 -0
  86. package/src/tus.ts +91 -5
@@ -0,0 +1,1142 @@
1
+ import parse from 'json-to-ast';
2
+ import { z } from 'zod';
3
+ import { entries } from "./object.js";
4
+ import { addUseReference, botNeedsInput, doesStepRobotSupportUse, getFirstStepNameThatDoesNotNeedInput, getIndentation, hasRobot, parseSafeTemplate, } from "./stepParsing.js";
5
+ import { robotsMeta } from "./types/robots/_index.js";
6
+ import { stackVersions } from "./types/stackVersions.js";
7
+ import { assemblyInstructionsSchema } from "./types/template.js";
8
+ import { zodParseWithContext } from "./zodParseWithContext.js";
9
+ // Maximum number of steps allowed in a Smart CDN Assembly
10
+ // We set this ~unreasonably high for now as it could already avoid misuse/abuse
11
+ // until we have settled on a discussion about limits:
12
+ // See: https://github.com/transloadit/content/pull/4176
13
+ const MAX_STEPS_PER_URLTRANSFORM_ASSEMBLY = 20;
14
+ const getStepLocation = (steps, stepName) => ({
15
+ row: steps.__line?.[stepName] ?? 0,
16
+ column: steps.__column?.[stepName] ?? 0,
17
+ });
18
+ const fixWrongStackVersionSchema = z.object({
19
+ stepName: z.string(),
20
+ paramName: z.string(),
21
+ recommendedVersion: z.string(),
22
+ });
23
+ const fixMissingUseSchema = z.object({
24
+ stepName: z.string(),
25
+ });
26
+ const fixDuplicateKeyInStepSchema = z.object({
27
+ stepName: z.string(),
28
+ duplicateKeys: z.array(z.string()),
29
+ });
30
+ const fixSmartCdnInputFieldSchema = z.object({
31
+ stepName: z.string(),
32
+ });
33
+ const fixMissingInputSchema = z.object({});
34
+ const fixMissingStepsSchema = z.object({});
35
+ const fixInvalidStepsTypeSchema = z.object({});
36
+ const fixEmptyStepsSchema = z.object({});
37
+ const fixMissingOriginalStorageSchema = z.object({});
38
+ class ParseError extends SyntaxError {
39
+ line;
40
+ column;
41
+ rawMessage;
42
+ source;
43
+ constructor(message, line, column, rawMessage, source) {
44
+ super(message);
45
+ this.line = line;
46
+ this.column = column;
47
+ this.rawMessage = rawMessage;
48
+ this.source = source;
49
+ }
50
+ }
51
+ function isObject(obj) {
52
+ return typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
53
+ }
54
+ function has(object, key) {
55
+ return Object.hasOwn(object, key);
56
+ }
57
+ function isParseError(e) {
58
+ return (e instanceof Error &&
59
+ isObject(e) &&
60
+ 'line' in e &&
61
+ typeof e.line === 'number' &&
62
+ 'column' in e &&
63
+ typeof e.column === 'number' &&
64
+ 'rawMessage' in e &&
65
+ typeof e.rawMessage === 'string');
66
+ }
67
+ // getASTValue traverses through the provided AST and will return
68
+ // the JavaScript value described by it.
69
+ // Objects and arrays will also have the __line and __column
70
+ // properties containing line and column number for their
71
+ // child elements.
72
+ // See https://github.com/vtrushin/json-to-ast#node-types
73
+ function getASTValue(ast) {
74
+ switch (ast.type) {
75
+ case 'Literal':
76
+ return ast.value;
77
+ case 'Array': {
78
+ const value = [];
79
+ const lines = [];
80
+ const columns = [];
81
+ for (const property of ast.children) {
82
+ if (property.loc) {
83
+ // json-to-ast starts the line and column numbers at 1 but the
84
+ // ace editor expects them to start at 0. To make up for that
85
+ // difference we subtract 1.
86
+ value.push(getASTValue(property));
87
+ lines.push(property.loc.start.line - 1);
88
+ columns.push(property.loc.start.column - 1);
89
+ }
90
+ }
91
+ Object.defineProperty(value, '__line', { value: lines });
92
+ Object.defineProperty(value, '__column', { value: columns });
93
+ return value;
94
+ }
95
+ case 'Object': {
96
+ const value = {};
97
+ const lines = {};
98
+ const columns = {};
99
+ for (const property of ast.children) {
100
+ if (property.key && property.value && property.value.loc) {
101
+ // json-to-ast starts the line and column numbers at 1 but the
102
+ // ace editor expects them to start at 0. To make up for that
103
+ // difference we subtract 1.
104
+ value[property.key.value] = getASTValue(property.value);
105
+ lines[property.key.value] = property.value.loc.start.line - 1;
106
+ columns[property.key.value] = property.value.loc.start.column - 1;
107
+ }
108
+ }
109
+ Object.defineProperty(value, '__line', { value: lines });
110
+ Object.defineProperty(value, '__column', { value: columns });
111
+ return value;
112
+ }
113
+ default:
114
+ // Should not happen for valid ValueNode types
115
+ return undefined;
116
+ }
117
+ }
118
+ // getRobotsUsingTool returns an array of the robots names
119
+ // which have a specific tool. This can be used to
120
+ // get all robots supporting the ffmpeg_stack setting, for example.
121
+ function getRobotsUsingTool(tool) {
122
+ return Object.entries(robotsMeta)
123
+ .filter(([, meta]) => !tool || meta.uses_tools?.includes(tool))
124
+ .map(([varName]) => {
125
+ // turn: audioArtworkMeta -> /audio/artwork
126
+ // turn: s3StoreMeta -> /s3/store
127
+ const rName = `/${varName
128
+ .replace(/([a-z0-9])([A-Z])/g, '$1/$2')
129
+ .toLowerCase()
130
+ .replace(/\/meta$/, '')}`;
131
+ return rName;
132
+ });
133
+ }
134
+ const STORE_ROBOT_NAME = /^\/[a-z0-9]+\/store$/i;
135
+ function isStoreRobot(name) {
136
+ return STORE_ROBOT_NAME.test(name);
137
+ }
138
+ const IMPORT_ROBOT_NAME = /^\/[a-z0-9]+\/import$/i;
139
+ function isImportRobot(name) {
140
+ return IMPORT_ROBOT_NAME.test(name);
141
+ }
142
+ const FFMPEG_ROBOT_NAMES = getRobotsUsingTool('ffmpeg');
143
+ function isFfmpegRobot(name) {
144
+ return FFMPEG_ROBOT_NAMES.some((x) => x === name);
145
+ }
146
+ const IMAGICK_ROBOT_NAMES = getRobotsUsingTool('imagemagick');
147
+ function isImagickRobot(name) {
148
+ return IMAGICK_ROBOT_NAMES.some((x) => x === name);
149
+ }
150
+ const ALL_ROBOT_NAMES = getRobotsUsingTool();
151
+ function isRobot(name) {
152
+ return ALL_ROBOT_NAMES.includes(name);
153
+ }
154
+ function isHttpImportRobot(name) {
155
+ return name === '/http/import';
156
+ }
157
+ // lintStackParameter validates whether a given step has a proper
158
+ // ffmpeg_stack or imagemagick_stack paramater with an existing version.
159
+ // Which parameter is expected is controlled by the stackName
160
+ // argument which should either by 'ffmpeg' or 'imagemagick'.
161
+ // If a linting issue is found, the corresponding message is added
162
+ // to the result array.
163
+ function lintStackParameter(step, stepName, steps, stackName, result) {
164
+ const paramName = `${stackName}_stack`;
165
+ // Stack parameters are optional; when omitted, Transloadit defaults apply.
166
+ if (has(step, paramName)) {
167
+ const stackVersionValue = step[paramName];
168
+ if (typeof stackVersionValue === 'string') {
169
+ if (!stackVersions[stackName].test.test(stackVersionValue)) {
170
+ result.push({
171
+ code: `wrong-${stackName}-version`,
172
+ stepName,
173
+ robot: step.robot,
174
+ isAudioRobot: step.robot?.indexOf('/audio/') === 0,
175
+ stackVersion: stackVersionValue,
176
+ type: 'error',
177
+ row: steps.__line[stepName],
178
+ column: steps.__column[stepName],
179
+ fixId: 'fix-wrong-stack-version',
180
+ fixData: {
181
+ stepName,
182
+ paramName,
183
+ recommendedVersion: stackVersions[stackName].recommendedVersion,
184
+ },
185
+ });
186
+ }
187
+ }
188
+ else {
189
+ // Handle cases where the stack parameter is present but not a string (though schema should catch this)
190
+ // Or, decide if this case is impossible due to schema validation and remove this else.
191
+ // For now, let's assume schema validation makes this path unlikely for a 'wrong-version' error,
192
+ // but a general 'schema-violation' might be more appropriate if this state is reached.
193
+ }
194
+ }
195
+ }
196
+ function lintUseArray(use, stepName, stepNames, result, row, column) {
197
+ if (!Array.isArray(use))
198
+ return;
199
+ if (use.length === 0) {
200
+ result.push({
201
+ code: 'empty-use-array',
202
+ stepName,
203
+ type: 'warning',
204
+ row: row ?? 0,
205
+ column: column ?? 0,
206
+ });
207
+ return;
208
+ }
209
+ use.forEach((obj, index) => {
210
+ let name;
211
+ if (typeof obj === 'object' && obj !== null) {
212
+ name = obj.name;
213
+ }
214
+ else if (typeof obj === 'string') {
215
+ name = obj;
216
+ }
217
+ if (name && stepNames.indexOf(name) === -1) {
218
+ result.push({
219
+ code: 'undefined-step',
220
+ stepName,
221
+ wrongStepName: name,
222
+ type: 'error',
223
+ row: typeof obj === 'object' && obj !== null && obj.__line?.[index]
224
+ ? obj.__line[index]
225
+ : (row ?? 0),
226
+ column: typeof obj === 'object' && obj !== null && obj.__column?.[index]
227
+ ? obj.__column[index]
228
+ : (column ?? 0),
229
+ });
230
+ }
231
+ });
232
+ }
233
+ function lintHttpImportUrl(step, stepName, result) {
234
+ if (!has(step, 'url')) {
235
+ return;
236
+ }
237
+ const { url } = step;
238
+ if (typeof url !== 'string') {
239
+ return;
240
+ }
241
+ // Check if the URL contains a field variable without a protocol or domain
242
+ // Only warn when the URL is exactly an interpolation to avoid false positives.
243
+ const fieldVariableRegex = /^\$\{fields\.[^}]+\}$/;
244
+ const protocolDomainRegex = /^(https?:\/\/|\/\/)[^/]+/i;
245
+ if (fieldVariableRegex.test(url) && !protocolDomainRegex.test(url)) {
246
+ result.push({
247
+ code: 'unqualified-http-import-url',
248
+ stepName,
249
+ type: 'warning',
250
+ row: step.__line.url,
251
+ column: step.__column.url,
252
+ message: 'The /http/import url should include a protocol and domain name for security reasons.',
253
+ });
254
+ }
255
+ }
256
+ export function lint(assembly) {
257
+ const result = [];
258
+ if (!isObject(assembly) || !('steps' in assembly)) {
259
+ result.push({
260
+ code: 'missing-steps',
261
+ type: 'error',
262
+ row: 0,
263
+ column: 0,
264
+ message: "The 'steps' property is missing",
265
+ fixId: 'fix-missing-steps',
266
+ fixData: {},
267
+ });
268
+ return result;
269
+ }
270
+ if (!isObject(assembly.steps)) {
271
+ result.push({
272
+ code: 'invalid-steps-type',
273
+ type: 'error',
274
+ row: assembly.__line?.steps ?? 0,
275
+ column: assembly.__column?.steps ?? 0,
276
+ message: "The 'steps' property must be an object",
277
+ fixId: 'fix-invalid-steps-type',
278
+ fixData: {},
279
+ });
280
+ return result;
281
+ }
282
+ if (Object.keys(assembly.steps).length === 0) {
283
+ result.push({
284
+ code: 'empty-steps',
285
+ type: 'warning',
286
+ row: assembly.__line?.steps ?? 0,
287
+ column: assembly.__column?.steps ?? 0,
288
+ message: "The 'steps' object is empty",
289
+ fixId: 'fix-empty-steps',
290
+ fixData: {},
291
+ });
292
+ return result; // Return here to avoid additional checks for empty steps
293
+ }
294
+ if (!isObject(assembly.steps)) {
295
+ return result;
296
+ }
297
+ const steps = assembly.steps;
298
+ const stepNames = Object.keys(steps).filter((key) => key !== '__line' && key !== '__column');
299
+ if (!stepNames.includes(':original')) {
300
+ // The step :original always exists automatically
301
+ stepNames.push(':original');
302
+ }
303
+ let hasFileServe = false;
304
+ let hasFieldsInput = false;
305
+ let importStepName = '';
306
+ // First pass - check for /file/serve and ${fields.input}
307
+ for (const [stepName, step] of Object.entries(steps)) {
308
+ if (stepName === '__line' || stepName === '__column')
309
+ continue;
310
+ if (!isObject(step))
311
+ continue;
312
+ // Ensure 'step' is actually a StepWithMetadata-like object, not just Record<string, number>
313
+ // A simple check for 'robot' or other StepInput fields can make the cast safer.
314
+ // StepInput is { robot?: string; use?: unknown; ... } & Record<string, unknown>
315
+ // StepWithMetadata adds __line and __column to StepInput.
316
+ // A Record<string, number> would not typically have these specific fields like 'robot'.
317
+ if (!('robot' in step || 'use' in step)) {
318
+ // This object doesn't look like a step, skip or handle as an error.
319
+ // For now, let's assume it might be an invalid structure caught by schema validation later
320
+ // or it's a case not expected here if it passed earlier checks.
321
+ continue;
322
+ }
323
+ const typedStep = step;
324
+ if (!typedStep.robot)
325
+ continue;
326
+ // Check if we have a /file/serve robot anywhere
327
+ if (typedStep.robot === '/file/serve') {
328
+ hasFileServe = true;
329
+ }
330
+ // Check if we use ${fields.input} in any import step
331
+ if (isImportRobot(typedStep.robot)) {
332
+ importStepName = stepName;
333
+ const stepStr = JSON.stringify(step);
334
+ if (stepStr.includes('${fields.input}')) {
335
+ hasFieldsInput = true;
336
+ }
337
+ }
338
+ }
339
+ // If we have /file/serve but don't use ${fields.input} in the import step, add warning
340
+ if (hasFileServe && !hasFieldsInput && importStepName) {
341
+ const { row, column } = getStepLocation(steps, importStepName);
342
+ result.push({
343
+ code: 'smart-cdn-input-field-missing',
344
+ type: 'warning',
345
+ row,
346
+ column,
347
+ message: 'Smart CDN path component available as `${fields.input}`',
348
+ stepName: importStepName,
349
+ fixId: 'fix-smart-cdn-input-field',
350
+ fixData: { stepName: importStepName },
351
+ });
352
+ }
353
+ let usesOriginalFiles = false;
354
+ let storesOriginalFiles = false;
355
+ let hasInputStep = false;
356
+ for (const [stepName, step] of Object.entries(steps)) {
357
+ if (stepName === '__line' || stepName === '__column')
358
+ continue;
359
+ const { row, column } = getStepLocation(steps, stepName);
360
+ if (!step || typeof step !== 'object' || Array.isArray(step)) {
361
+ result.push({
362
+ code: 'step-is-not-an-object',
363
+ stepName,
364
+ type: 'error',
365
+ row,
366
+ column,
367
+ });
368
+ continue;
369
+ }
370
+ const stepKeys = Object.keys(step).filter((key) => key !== '__line' && key !== '__column');
371
+ if (!('robot' in step || 'use' in step)) {
372
+ if (stepKeys.length > 0) {
373
+ result.push({
374
+ code: 'missing-robot',
375
+ stepName,
376
+ type: 'error',
377
+ row,
378
+ column,
379
+ });
380
+ }
381
+ continue;
382
+ }
383
+ const typedStep = step;
384
+ if (!typedStep.robot) {
385
+ result.push({
386
+ code: 'missing-robot',
387
+ stepName,
388
+ type: 'error',
389
+ row,
390
+ column,
391
+ });
392
+ continue;
393
+ }
394
+ else if (!isRobot(typedStep.robot)) {
395
+ result.push({
396
+ code: 'undefined-robot',
397
+ stepName,
398
+ robot: typedStep.robot,
399
+ type: 'error',
400
+ row: typedStep.__line.robot,
401
+ column: typedStep.__column.robot,
402
+ });
403
+ }
404
+ else if (typedStep.robot === '/file/serve') {
405
+ hasFileServe = true;
406
+ if ('url' in typedStep && !('use' in typedStep)) {
407
+ const stepStr = JSON.stringify(step);
408
+ if (!stepStr.includes('${fields.input}')) {
409
+ result.push({
410
+ code: 'smart-cdn-input-field-missing',
411
+ type: 'warning',
412
+ row,
413
+ column,
414
+ message: 'Smart CDN path component available as `${fields.input}`',
415
+ stepName,
416
+ });
417
+ }
418
+ }
419
+ }
420
+ else if (isFfmpegRobot(typedStep.robot)) {
421
+ lintStackParameter(typedStep, stepName, steps, 'ffmpeg', result);
422
+ }
423
+ else if (isImagickRobot(typedStep.robot)) {
424
+ lintStackParameter(typedStep, stepName, steps, 'imagemagick', result);
425
+ }
426
+ else if (typedStep.robot === '/upload/handle') {
427
+ if (stepName !== ':original') {
428
+ result.push({
429
+ code: 'wrong-step-name',
430
+ type: 'error',
431
+ row,
432
+ column,
433
+ });
434
+ }
435
+ }
436
+ else if (isHttpImportRobot(typedStep.robot)) {
437
+ lintHttpImportUrl(typedStep, stepName, result);
438
+ }
439
+ if (!has(typedStep, 'use')) {
440
+ if (typedStep.robot === '/html/convert') {
441
+ // The /html/convert robot can either act as a import robot when
442
+ // the `url` parameter is defined. Or it can be a conversion robot
443
+ // if `use` is available. If neither of those parameters is given,
444
+ // we emit a warning.
445
+ if (has(typedStep, 'url')) {
446
+ hasInputStep = true;
447
+ }
448
+ else {
449
+ result.push({
450
+ code: 'missing-url',
451
+ stepName,
452
+ type: 'warning',
453
+ row,
454
+ column,
455
+ });
456
+ }
457
+ }
458
+ else if (
459
+ // Check if this robot doesn't need input (like import robots, /upload/handle,
460
+ // file-generating robots like /image/generate, /text/speak with prompt, etc.)
461
+ !botNeedsInput(typedStep.robot, stepName, typedStep)) {
462
+ hasInputStep = true;
463
+ }
464
+ else {
465
+ // Import robots and /upload/handle do not need a use parameter. For
466
+ // all others we emit a warning.
467
+ result.push({
468
+ code: 'missing-use',
469
+ stepName,
470
+ type: 'warning',
471
+ row,
472
+ column,
473
+ fixId: 'fix-missing-use',
474
+ fixData: { stepName },
475
+ });
476
+ }
477
+ }
478
+ else {
479
+ if (Array.isArray(typedStep.use)) {
480
+ const referencesOriginal = typedStep.use.some((item) => {
481
+ if (typeof item === 'string') {
482
+ return item === ':original';
483
+ }
484
+ return (typeof item === 'object' && item !== null && 'name' in item && item.name === ':original');
485
+ });
486
+ if (referencesOriginal) {
487
+ hasInputStep = true;
488
+ }
489
+ // Situation 1: use parameter is an array, for example
490
+ // "use": [ "hello", { name: ":original" } ]
491
+ lintUseArray(typedStep.use, stepName, stepNames, result, typedStep.__line.use, typedStep.__column.use);
492
+ }
493
+ else if (typeof typedStep.use === 'object' && typedStep.use !== null) {
494
+ // Situation 2: use parameter is an object, for example
495
+ // "use": { steps: [ "hello", "hi" ], "bundle_steps": true }
496
+ // typedStep.use here is inferred as StepUse, which can be StepUseObject
497
+ // StepUseObject has a MANDATORY 'steps' property of type StepUseArrayItemSchema[]
498
+ const useObject = typedStep.use; // No immediate cast
499
+ if ('steps' in useObject) {
500
+ // Access metadata for the 'steps' key within the useObject, if available.
501
+ // The useObject itself, being a product of getASTValue for an object, should have __line/__column.
502
+ const useStepsLine = useObject?.__line?.steps;
503
+ const useStepsColumn = useObject?.__column?.steps;
504
+ if (Array.isArray(useObject.steps)) {
505
+ // Now useObject.steps is known to be an array.
506
+ // We still need to ensure elements match StepUseArrayItemSchema if processing them.
507
+ // The existing lintUseArray function takes 'unknown[]' for its first arg's 'steps' property if it's an object, so this is compatible.
508
+ if (useObject.steps.some((step) => {
509
+ if (typeof step === 'string') {
510
+ return step === ':original';
511
+ }
512
+ return (typeof step === 'object' &&
513
+ step !== null &&
514
+ 'name' in step &&
515
+ step.name === ':original');
516
+ })) {
517
+ hasInputStep = true;
518
+ }
519
+ lintUseArray(useObject.steps, stepName, stepNames, result, useStepsLine ?? typedStep.__line.use, // Fallback to the line of the 'use' key itself
520
+ useStepsColumn ?? typedStep.__column.use);
521
+ }
522
+ else if (typeof useObject.steps === 'string') {
523
+ if (useObject.steps === ':original') {
524
+ hasInputStep = true;
525
+ }
526
+ lintUseArray([useObject.steps], stepName, stepNames, result, useStepsLine ?? typedStep.__line.use, useStepsColumn ?? typedStep.__column.use);
527
+ }
528
+ else if (typeof useObject.steps !== 'string') {
529
+ // If 'steps' is not an array or not present, it's an invalid use object structure.
530
+ result.push({
531
+ code: 'missing-use-steps', // Or a more specific error like 'invalid-use-object-structure'
532
+ stepName,
533
+ type: 'error',
534
+ row: typedStep.__line.use, // Point to the start of the use object
535
+ column: typedStep.__column.use,
536
+ });
537
+ }
538
+ }
539
+ }
540
+ else if (typeof typedStep.use === 'string') {
541
+ if (typedStep.use === ':original') {
542
+ hasInputStep = true;
543
+ }
544
+ // Situation 3: use parameter is a string, for example
545
+ // "use": "import"
546
+ if (stepNames.indexOf(typedStep.use) === -1) {
547
+ result.push({
548
+ code: 'undefined-step',
549
+ stepName,
550
+ wrongStepName: typedStep.use,
551
+ type: 'error',
552
+ row: typedStep.__line.use,
553
+ column: typedStep.__column.use,
554
+ });
555
+ }
556
+ }
557
+ else {
558
+ // Situation 4: use parameter has some other invalid type
559
+ result.push({
560
+ code: 'wrong-use-type',
561
+ stepName,
562
+ type: 'error',
563
+ row: typedStep.__line.use,
564
+ column: typedStep.__column.use,
565
+ });
566
+ }
567
+ const referencesOriginalFiles = JSON.stringify(typedStep.use).includes(':original');
568
+ if (referencesOriginalFiles) {
569
+ if (typedStep.robot && isStoreRobot(typedStep.robot)) {
570
+ storesOriginalFiles = true;
571
+ }
572
+ else {
573
+ usesOriginalFiles = true;
574
+ }
575
+ }
576
+ }
577
+ }
578
+ // When the /file/serve robot is used for the UrlProxy, customers should not use a
579
+ // storage robot, so we should not warn them about it.
580
+ if (!hasFileServe) {
581
+ const hasStorageRobot = hasRobot(JSON.stringify(assembly), /\/store$/, true);
582
+ if (!hasStorageRobot) {
583
+ result.push({
584
+ code: 'no-storage',
585
+ type: 'warning',
586
+ row: assembly.__line?.steps ?? 0,
587
+ column: assembly.__column?.steps ?? 0,
588
+ });
589
+ }
590
+ if (usesOriginalFiles && !storesOriginalFiles && hasStorageRobot) {
591
+ // Keep only the missing-original-storage warning
592
+ result.push({
593
+ code: 'missing-original-storage',
594
+ type: 'warning',
595
+ row: assembly.__line?.steps ?? 0,
596
+ column: assembly.__column?.steps ?? 0,
597
+ fixId: 'fix-missing-original-storage',
598
+ fixData: {},
599
+ });
600
+ }
601
+ }
602
+ if (!hasInputStep) {
603
+ result.push({
604
+ code: 'missing-input',
605
+ type: 'error',
606
+ row: assembly.__line?.steps ?? 0,
607
+ column: assembly.__column?.steps ?? 0,
608
+ fixId: 'fix-missing-input',
609
+ fixData: {}, // Add an empty object as fixData
610
+ });
611
+ }
612
+ // Add schema violations as linting issues, only if we don't have any
613
+ // serious linting issues yet. Otherwise we risk having duplicate
614
+ // issues, for example, for ffmpeg_stack. Both the linter and the schema cover it.
615
+ // @TODO: In the future we should delete Linter issues that are covered by the Schema.
616
+ // It could result in just having only a few Linter issues left.
617
+ const cntErrors = result.filter((r) => r.type === 'error').length;
618
+ // const cntWarnings = result.filter((r) => r.type === 'warning').length
619
+ if (!cntErrors) {
620
+ const parsed = zodParseWithContext(assemblyInstructionsSchema, assembly);
621
+ if (!parsed.success) {
622
+ for (const zodIssue of parsed.errors) {
623
+ // Start with default values at the steps object level
624
+ let row = assembly.__line?.steps ?? 1;
625
+ let column = assembly.__column?.steps ?? 1;
626
+ const { path } = zodIssue;
627
+ // Find the row and column of this path in the JSON string:
628
+ if (path.length > 0) {
629
+ let current = assembly;
630
+ let metadata = assembly;
631
+ // Walk the path to find the deepest available line/column info
632
+ for (const segment of path) {
633
+ if (typeof segment === 'string' && current && typeof current === 'object') {
634
+ // Keep track of both the actual value and its metadata
635
+ current = current[segment];
636
+ // The metadata contains __line and __column info
637
+ if (metadata && '__line' in metadata && '__column' in metadata) {
638
+ const lines = metadata.__line;
639
+ const columns = metadata.__column;
640
+ if (segment in lines && segment in columns) {
641
+ row = lines[segment];
642
+ column = columns[segment];
643
+ }
644
+ }
645
+ // Update metadata pointer for next iteration
646
+ metadata = current;
647
+ }
648
+ }
649
+ }
650
+ result.push({
651
+ code: 'schema-violation',
652
+ type: 'error',
653
+ row,
654
+ column,
655
+ message: zodIssue.humanReadable,
656
+ });
657
+ }
658
+ }
659
+ }
660
+ return result;
661
+ }
662
+ function isInfiniteAssembly(template) {
663
+ if (!template.steps)
664
+ return [false];
665
+ const graph = new Map();
666
+ for (const [stepName, stepValue] of Object.entries(template.steps)) {
667
+ if (stepName === '__line' || stepName === '__column')
668
+ continue;
669
+ if (typeof stepValue !== 'object' ||
670
+ stepValue === null ||
671
+ !('use' in stepValue) ||
672
+ !stepValue.use) {
673
+ continue;
674
+ }
675
+ const stepUseValue = stepValue.use;
676
+ if (typeof stepUseValue === 'string') {
677
+ graph.set(stepName, [stepUseValue]);
678
+ continue;
679
+ }
680
+ if (Array.isArray(stepUseValue)) {
681
+ // Filter out non-string/non-object-with-name items to satisfy .every checks
682
+ const filteredUseArray = stepUseValue.filter((u) => typeof u === 'string' ||
683
+ (typeof u === 'object' && u !== null && 'name' in u && typeof u.name === 'string'));
684
+ if (filteredUseArray.every((u) => typeof u === 'string')) {
685
+ graph.set(stepName, filteredUseArray);
686
+ continue;
687
+ }
688
+ if (filteredUseArray.every((u) => typeof u === 'object' && u !== null && 'name' in u)) {
689
+ graph.set(stepName, filteredUseArray.map((u) => u.name));
690
+ continue;
691
+ }
692
+ }
693
+ if (typeof stepUseValue === 'object' &&
694
+ stepUseValue !== null &&
695
+ 'steps' in stepUseValue &&
696
+ Array.isArray(stepUseValue.steps)) {
697
+ const useSteps = stepUseValue.steps;
698
+ // Similar filtering as above for useSteps elements
699
+ const filteredUseSteps = useSteps.filter((s) => typeof s === 'string' ||
700
+ (typeof s === 'object' && s !== null && 'name' in s && typeof s.name === 'string'));
701
+ if (filteredUseSteps.every((s) => typeof s === 'string')) {
702
+ graph.set(stepName, filteredUseSteps);
703
+ }
704
+ else if (filteredUseSteps.every((s) => typeof s === 'object' && s !== null && 'name' in s)) {
705
+ graph.set(stepName, filteredUseSteps.map((s) => s.name));
706
+ }
707
+ }
708
+ }
709
+ const visited = new Set();
710
+ const recursionStack = new Set();
711
+ function dfs(node) {
712
+ if (recursionStack.has(node))
713
+ return true; // Cycle detected
714
+ if (visited.has(node))
715
+ return false; // Already visited and no cycle from this node
716
+ visited.add(node);
717
+ recursionStack.add(node);
718
+ const neighbors = graph.get(node) || [];
719
+ for (const neighbor of neighbors) {
720
+ // One of the pitfalls of our normalization is that an :original step
721
+ // references itself in its own use property after normalization.
722
+ // This is an "accepted" circular dependency.
723
+ if (node === ':original' && neighbor !== ':original') {
724
+ continue;
725
+ }
726
+ if (dfs(neighbor)) {
727
+ return true; // Cycle detected in recursion
728
+ }
729
+ }
730
+ recursionStack.delete(node);
731
+ return false;
732
+ }
733
+ for (const [stepName, stepValue] of Object.entries(template.steps)) {
734
+ if (stepName === '__line' || stepName === '__column')
735
+ continue;
736
+ if (!graph.has(stepName))
737
+ continue;
738
+ if (!visited.has(stepName) && dfs(stepName)) {
739
+ const offendingStep = stepValue; // Cast for __line/__column access
740
+ return [
741
+ true,
742
+ {
743
+ stepName,
744
+ line: offendingStep.__line?.use ?? 0, // Assumes 'use' is a key in __line for the property itself
745
+ column: offendingStep.__column?.use ?? 0,
746
+ },
747
+ ]; // Circular dependency found
748
+ }
749
+ }
750
+ return [false]; // No circular dependencies detected
751
+ }
752
+ function findDuplicateKeysInAST(node, path = '', annotations = []) {
753
+ if (node.type === 'Object') {
754
+ const keysSeen = new Map();
755
+ for (const property of node.children) {
756
+ const key = property.key.value;
757
+ const keyLocation = property.key.loc;
758
+ const fullPath = path ? `${path}.${key}` : key;
759
+ if (keysSeen.has(key) && keyLocation) {
760
+ const stepName = path.includes('steps.') ? path.split('steps.')[1] : undefined;
761
+ // Duplicate key found
762
+ annotations.push({
763
+ code: 'duplicate-key-in-step',
764
+ type: 'warning',
765
+ row: keyLocation.start.line - 1,
766
+ column: keyLocation.start.column - 1,
767
+ message: `Duplicate key '${key}' found`,
768
+ stepName,
769
+ duplicateKeys: [key],
770
+ fixId: 'fix-duplicate-key-in-step',
771
+ fixData: {
772
+ stepName: stepName ?? '',
773
+ duplicateKeys: [key],
774
+ },
775
+ });
776
+ }
777
+ else {
778
+ keysSeen.set(key, property.value);
779
+ }
780
+ // Recurse into the property value
781
+ findDuplicateKeysInAST(property.value, fullPath, annotations);
782
+ }
783
+ }
784
+ else if (node.type === 'Array') {
785
+ for (const item of node.children) {
786
+ findDuplicateKeysInAST(item, path, annotations);
787
+ }
788
+ }
789
+ }
790
+ /**
791
+ * Checks if an assembly is a Smart CDN Assembly by looking for the `/file/serve` robot
792
+ */
793
+ export function isSmartCdnAssembly(assembly) {
794
+ if (!isObject(assembly) || !isObject(assembly.steps)) {
795
+ return false;
796
+ }
797
+ for (const [stepName, step] of Object.entries(assembly.steps)) {
798
+ if (stepName === '__line' || stepName === '__column')
799
+ continue;
800
+ if (!isObject(step))
801
+ continue;
802
+ const typedStep = step;
803
+ if (typedStep.robot === '/file/serve') {
804
+ return true;
805
+ }
806
+ }
807
+ return false;
808
+ }
809
+ // This function counts the steps in an assembly
810
+ function countSteps(steps) {
811
+ // Filter out metadata properties
812
+ return Object.keys(steps).filter((key) => key !== '__line' && key !== '__column').length;
813
+ }
814
+ // Checks if a robot is allowed for Smart CDN
815
+ function isRobotAllowedForSmartCdn(robotName) {
816
+ if (!robotName || typeof robotName !== 'string') {
817
+ return false;
818
+ }
819
+ // Convert robotName like /http/import to httpImportMeta
820
+ const parts = robotName.substring(1).split('/');
821
+ const keyBase = parts
822
+ .map((part, index) => {
823
+ if (index === 0)
824
+ return part;
825
+ return part.charAt(0).toUpperCase() + part.slice(1);
826
+ })
827
+ .join('');
828
+ const robotMetaKey = `${keyBase}Meta`;
829
+ const meta = robotsMeta[robotMetaKey];
830
+ // Check if this robot exists and is allowed for Smart CDN
831
+ return meta?.allowed_for_url_transform === true;
832
+ }
833
+ // This function lints Smart CDN Assemblies
834
+ function lintSmartCdn(assembly) {
835
+ const results = [];
836
+ if (!assembly.steps || typeof assembly.steps !== 'object') {
837
+ return results;
838
+ }
839
+ const steps = assembly.steps;
840
+ // Check step count against limit
841
+ const stepCount = countSteps(steps);
842
+ if (stepCount > MAX_STEPS_PER_URLTRANSFORM_ASSEMBLY) {
843
+ results.push({
844
+ code: 'smart-cdn-max-steps-exceeded',
845
+ type: 'error',
846
+ row: assembly.__line?.steps ?? 0,
847
+ column: assembly.__column?.steps ?? 0,
848
+ message: `Smart CDN Assemblies are limited to ${MAX_STEPS_PER_URLTRANSFORM_ASSEMBLY} steps, but found ${stepCount} steps`,
849
+ maxStepCount: MAX_STEPS_PER_URLTRANSFORM_ASSEMBLY,
850
+ stepCount,
851
+ });
852
+ }
853
+ // Check for disallowed robots
854
+ for (const [stepName, step] of Object.entries(steps)) {
855
+ if (stepName === '__line' || stepName === '__column' || typeof step !== 'object' || !step) {
856
+ continue;
857
+ }
858
+ const typedStep = step;
859
+ const robotNameValue = typedStep.robot;
860
+ if (robotNameValue && !isRobotAllowedForSmartCdn(robotNameValue)) {
861
+ const { row, column } = getStepLocation(steps, stepName);
862
+ results.push({
863
+ code: 'smart-cdn-robot-not-allowed',
864
+ type: 'error',
865
+ row,
866
+ column,
867
+ message: `Robot "${robotNameValue}" is not allowed in Smart CDN Assemblies`,
868
+ stepName,
869
+ robot: robotNameValue,
870
+ });
871
+ }
872
+ }
873
+ return results;
874
+ }
875
+ export async function parseAndLint(json) {
876
+ let ast;
877
+ try {
878
+ ast = parse(json, { loc: true });
879
+ }
880
+ catch (e) {
881
+ if (!(e instanceof Error)) {
882
+ throw e;
883
+ }
884
+ if (e.name !== 'SyntaxError') {
885
+ throw e;
886
+ }
887
+ if (!isParseError(e)) {
888
+ throw e;
889
+ }
890
+ return [
891
+ {
892
+ code: 'invalid-json',
893
+ type: 'error',
894
+ row: e.line - 1,
895
+ column: e.column - 1,
896
+ message: e.rawMessage,
897
+ },
898
+ ];
899
+ }
900
+ const obj = getASTValue(ast);
901
+ const templateMeta = obj;
902
+ const annotations = lint(templateMeta);
903
+ // Additional checks for Smart CDN assemblies
904
+ if (isSmartCdnAssembly(templateMeta)) {
905
+ annotations.push(...lintSmartCdn(templateMeta));
906
+ }
907
+ findDuplicateKeysInAST(ast, undefined, annotations);
908
+ const [isInfinite, positionalInfo] = isInfiniteAssembly(templateMeta);
909
+ if (isInfinite && positionalInfo) {
910
+ annotations.push({
911
+ code: 'infinite-assembly',
912
+ type: 'error',
913
+ row: positionalInfo.line,
914
+ column: positionalInfo.column,
915
+ stepName: positionalInfo.stepName,
916
+ });
917
+ }
918
+ // Sort the annotations by row numbers descending
919
+ annotations.sort((a, b) => a.row - b.row);
920
+ return annotations;
921
+ }
922
+ function fixWrongStackVersion(content, fixData) {
923
+ // A wrong stack version is a violation of our schema so we cannot use parseSafeTemplate
924
+ // here.
925
+ let parsed;
926
+ let indent = ' ';
927
+ try {
928
+ parsed = JSON.parse(content);
929
+ indent = getIndentation(content);
930
+ }
931
+ catch (_e) {
932
+ return content;
933
+ }
934
+ if (!isObject(parsed)) {
935
+ return content;
936
+ }
937
+ const parsedRecord = parsed;
938
+ const stepsValue = parsedRecord.steps;
939
+ if (!isObject(stepsValue)) {
940
+ return content;
941
+ }
942
+ const stepsRecord = stepsValue;
943
+ const newStepsEntries = [];
944
+ for (const [stepName2, step2] of Object.entries(stepsRecord)) {
945
+ if (typeof step2 !== 'object' || step2 === null) {
946
+ newStepsEntries.push([stepName2, step2]);
947
+ continue;
948
+ }
949
+ let newStep = { ...step2 };
950
+ if (fixData.stepName === stepName2) {
951
+ newStep = { ...step2, [fixData.paramName]: fixData.recommendedVersion };
952
+ }
953
+ newStepsEntries.push([stepName2, newStep]);
954
+ }
955
+ return JSON.stringify({ ...parsedRecord, steps: Object.fromEntries(newStepsEntries) }, null, indent);
956
+ }
957
+ function fixMissingUse(content, fixData) {
958
+ // A missing use is a violation of our schema so we cannot use parseSafeTemplate
959
+ // here.
960
+ let parsed;
961
+ let indent = ' ';
962
+ try {
963
+ parsed = JSON.parse(content);
964
+ indent = getIndentation(content);
965
+ }
966
+ catch (_e) {
967
+ return content;
968
+ }
969
+ if (!isObject(parsed)) {
970
+ return content;
971
+ }
972
+ const parsedRecord = parsed;
973
+ const stepsValue = parsedRecord.steps;
974
+ if (!isObject(stepsValue)) {
975
+ return content;
976
+ }
977
+ const stepsRecord = stepsValue;
978
+ // Get the step that needs fixing
979
+ const stepValue = stepsRecord[fixData.stepName];
980
+ if (!isObject(stepValue) || !('robot' in stepValue)) {
981
+ return content;
982
+ }
983
+ const step = stepValue;
984
+ // Get the first upload or import step:
985
+ const firstInputStepName = getFirstStepNameThatDoesNotNeedInput(content);
986
+ if (!firstInputStepName) {
987
+ return content;
988
+ }
989
+ // Add the use parameter pointing to :original only if the robot supports it:
990
+ if (doesStepRobotSupportUse(step)) {
991
+ step.use = firstInputStepName;
992
+ }
993
+ parsedRecord.steps = stepsRecord;
994
+ return JSON.stringify(parsedRecord, null, indent);
995
+ }
996
+ function fixDuplicateKeyInStep(content, _fixData) {
997
+ const [templateError, template, indent] = parseSafeTemplate(content);
998
+ if (templateError) {
999
+ // If parsing fails, return the original content
1000
+ return content;
1001
+ }
1002
+ return JSON.stringify(template, null, indent);
1003
+ }
1004
+ function fixMissingSteps(content) {
1005
+ const [templateError, template, indent] = parseSafeTemplate(content);
1006
+ if (templateError) {
1007
+ return JSON.stringify({ steps: {} }, null, ' ');
1008
+ }
1009
+ return JSON.stringify({ ...template, steps: {} }, null, indent);
1010
+ }
1011
+ function fixMissingInput(content) {
1012
+ // A missing input is a violation of our schema so we cannot use parseSafeTemplate
1013
+ // here.
1014
+ let parsed;
1015
+ let indent = ' ';
1016
+ try {
1017
+ parsed = JSON.parse(content);
1018
+ indent = getIndentation(content);
1019
+ }
1020
+ catch (_e) {
1021
+ return content;
1022
+ }
1023
+ if (!isObject(parsed)) {
1024
+ return content;
1025
+ }
1026
+ const parsedRecord = parsed;
1027
+ const stepsValue = parsedRecord.steps;
1028
+ if (!isObject(stepsValue)) {
1029
+ return content;
1030
+ }
1031
+ const stepsRecord = stepsValue;
1032
+ // Add the :original step with /upload/handle robot
1033
+ stepsRecord[':original'] = {
1034
+ robot: '/upload/handle',
1035
+ };
1036
+ // Update other steps to use :original if they don't have a 'use' property
1037
+ for (const [stepName, step] of Object.entries(stepsRecord)) {
1038
+ if (stepName !== ':original' && isObject(step) && !('use' in step) && 'robot' in step) {
1039
+ // Use addUseReference instead of direct assignment
1040
+ // @ts-expect-error: robot should be good here
1041
+ const updatedStep = addUseReference(step, ':original');
1042
+ stepsRecord[stepName] = updatedStep;
1043
+ }
1044
+ }
1045
+ parsedRecord.steps = stepsRecord;
1046
+ return JSON.stringify(parsedRecord, null, indent);
1047
+ }
1048
+ function fixInvalidStepsType(content) {
1049
+ let parsed;
1050
+ let indent = ' ';
1051
+ try {
1052
+ parsed = JSON.parse(content);
1053
+ indent = getIndentation(content);
1054
+ }
1055
+ catch (_err) {
1056
+ return content;
1057
+ }
1058
+ if (!isObject(parsed)) {
1059
+ return content;
1060
+ }
1061
+ const parsedRecord = parsed;
1062
+ if (!isObject(parsedRecord.steps)) {
1063
+ parsedRecord.steps = {};
1064
+ }
1065
+ return JSON.stringify(parsedRecord, null, indent);
1066
+ }
1067
+ function fixEmptySteps(content) {
1068
+ const [templateError, template, indent] = parseSafeTemplate(content);
1069
+ if (templateError) {
1070
+ return content;
1071
+ }
1072
+ if (Object.keys(template.steps ?? {}).length === 0) {
1073
+ template.steps = {
1074
+ ':original': {
1075
+ robot: '/upload/handle',
1076
+ },
1077
+ };
1078
+ }
1079
+ return JSON.stringify(template, null, indent);
1080
+ }
1081
+ function fixMissingOriginalStorage(content) {
1082
+ const [templateError, template, indent] = parseSafeTemplate(content);
1083
+ if (templateError) {
1084
+ return content;
1085
+ }
1086
+ // Find the storage step
1087
+ for (const [, step] of entries(template.steps)) {
1088
+ if (step.robot.endsWith('/store')) {
1089
+ // Add :original to the use array if it's not already there
1090
+ const updatedStep = addUseReference(step, ':original', { leading: true });
1091
+ Object.assign(step, updatedStep);
1092
+ }
1093
+ }
1094
+ return JSON.stringify(template, null, indent);
1095
+ }
1096
+ // Add new fix function
1097
+ function fixSmartCdnInputField(content, fixData) {
1098
+ const [templateError, template, indent] = parseSafeTemplate(content);
1099
+ if (templateError) {
1100
+ return content;
1101
+ }
1102
+ const step = template.steps?.[fixData.stepName];
1103
+ if (!step || step.robot !== '/http/import') {
1104
+ return content;
1105
+ }
1106
+ // Type assertion since we know this is an http-import step
1107
+ const httpImportStep = step;
1108
+ // Only modify the url field in the specified step
1109
+ httpImportStep.url = 'https://demos.transloadit.com/${fields.input}';
1110
+ // Stringify back with the same indentation
1111
+ return JSON.stringify(template, null, indent);
1112
+ }
1113
+ export function applyFix(content, fixId, fixData) {
1114
+ switch (fixId) {
1115
+ case 'fix-wrong-stack-version':
1116
+ return fixWrongStackVersion(content, fixWrongStackVersionSchema.parse(fixData));
1117
+ case 'fix-missing-use':
1118
+ return fixMissingUse(content, fixMissingUseSchema.parse(fixData));
1119
+ case 'fix-duplicate-key-in-step':
1120
+ return fixDuplicateKeyInStep(content, fixDuplicateKeyInStepSchema.parse(fixData));
1121
+ case 'fix-missing-input':
1122
+ fixMissingInputSchema.parse(fixData);
1123
+ return fixMissingInput(content);
1124
+ case 'fix-missing-steps':
1125
+ fixMissingStepsSchema.parse(fixData);
1126
+ return fixMissingSteps(content);
1127
+ case 'fix-invalid-steps-type':
1128
+ fixInvalidStepsTypeSchema.parse(fixData);
1129
+ return fixInvalidStepsType(content);
1130
+ case 'fix-empty-steps':
1131
+ fixEmptyStepsSchema.parse(fixData);
1132
+ return fixEmptySteps(content);
1133
+ case 'fix-missing-original-storage':
1134
+ fixMissingOriginalStorageSchema.parse(fixData);
1135
+ return fixMissingOriginalStorage(content);
1136
+ case 'fix-smart-cdn-input-field':
1137
+ return fixSmartCdnInputField(content, fixSmartCdnInputFieldSchema.parse(fixData));
1138
+ default:
1139
+ throw new Error(`Unknown fixId: ${fixId}`);
1140
+ }
1141
+ }
1142
+ //# sourceMappingURL=assembly-linter.js.map