Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/alphalib/types/assemblyStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,24 @@ export function hasError(
return errorExists
}

export function hasSpecificError(
assembly: AssemblyStatus | null | undefined,
errorType: string,
): boolean {
if (!assembly) return false

if (hasError(assembly) && assembly.error === errorType) {
return true
}

if (typeof assembly === 'object' && assembly !== null && errorType in assembly) {
const candidate = Reflect.get(assembly, errorType)
return Boolean(candidate)
}

return false
}

/**
* Type guard to check if an assembly has an ok status
*/
Expand Down
36 changes: 26 additions & 10 deletions src/alphalib/types/robots/_instructions-primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,25 @@ export const robotMetaSchema = z.object({

export type RobotMetaInput = z.input<typeof robotMetaSchema>

export const interpolationSchemaFull = z
.string()
.regex(/^\${.+}$/, 'Must be a full interpolation string')
export const interpolationSchemaPartial = z
.string()
.regex(/\${.+}/, 'Must be a partially interpolatable string')
// These schemas can be reproduced with z.string().regex(). However, this causes some issues.
// We use this in combination with unions. Internally Zod normalizes unions. A string schema and
// enums merged in some way. Both are validated. Normally, if a Zod union has errors, all of them
// are surfaced. However, if the regex isn’t match, and none of the enum values overlap, then the
// regex error is raised instead of the union error. As a result, the best error we could give back
// to the user, is that there’s a problem with the interpolation syntax. But really the other error
// is more useful in pretty much every case. To work around this, we use z.custom() instead, as Zod
// can’t normalize that.
const interpolationRegexFull = /^\${.+}$/
export const interpolationSchemaFull = z.custom<`\${${string}}`>(
(input) => typeof input === 'string' && interpolationRegexFull.test(input),
'Must be a full interpolation string',
)
const interpolationRegexPartial = /\${.+}/
export const interpolationSchemaPartial = z.custom<string>(
(input) => typeof input === 'string' && interpolationRegexPartial.test(input),
'Must be a partially interpolatable string',
)

export const booleanStringSchema = z.enum(['true', 'false'])

type InterpolatableTuple<Schemas extends readonly z.ZodTypeAny[]> = Schemas extends readonly [
Expand Down Expand Up @@ -277,9 +290,12 @@ export function interpolateRecursive<Schema extends z.ZodFirstPartySchemaTypes>(

switch (def.typeName) {
case z.ZodFirstPartyTypeKind.ZodBoolean:
return z
.union([interpolationSchemaFull, schema, booleanStringSchema])
.transform((value) => value === true || value === false) as InterpolatableSchema<Schema>
return z.union([
interpolationSchemaFull,
z
.union([schema, booleanStringSchema])
.transform((value) => value === true || value === false),
]) as InterpolatableSchema<Schema>
case z.ZodFirstPartyTypeKind.ZodArray: {
let replacement = z.array(interpolateRecursive(def.type), def)

Expand Down Expand Up @@ -631,7 +647,7 @@ export const robotFFmpeg = z.object({
shortest: z.boolean().nullish(),
filter_complex: z.union([z.string(), z.record(z.string())]).optional(),
'level:v': z.union([z.string(), z.number()]).optional(),
'profile:v': z.union([z.number(), z.enum(['baseline', 'main', 'high'])]).optional(),
'profile:v': z.union([z.number(), z.enum(['baseline', 'main', 'high', 'main10'])]).optional(),
'qscale:a': z.number().optional(),
'qscale:v': z.number().optional(),
'x264-params': z.string().optional(),
Expand Down
2 changes: 1 addition & 1 deletion src/alphalib/types/robots/document-thumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ If you set this to \`false\`, the robot will not emit files as they become avail

Also, extracted pages will be resized a lot faster as they are sent off to other machines for the resizing. This is especially useful for large documents with many pages to get up to 20 times faster processing.

Turbo mode increases pricing, though, in that the input document's file size is added for every extracted page. There are no performance benefits nor increased charges for single-page documents.
Turbo Mode increases pricing, though, in that the input document's file size is added for every extracted page. There are no performance benefits nor increased charges for single-page documents.
`),
})
.strict()
Expand Down
38 changes: 33 additions & 5 deletions src/alphalib/types/robots/file-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,40 @@ While theoretically possible, you could use [🤖/file/serve](/docs/robots/file-

Also consider configuring caching headers and cache-control directives to control how content is cached and invalidated on the CDN edge servers, balancing between freshness and efficiency.

More information on:
## Smart CDN Security with Signature Authentication

- [Content Delivery](/services/content-delivery/).
- [🤖/file/serve](/docs/robots/file-serve/) pricing.
- [🤖/tlcdn/deliver](/docs/robots/tlcdn-deliver/) pricing.
- [File Preview Feature](/blog/2024/06/file-preview-with-smart-cdn/) blog post.
You can leverage [Signature Authentication](/docs/api/authentication/#smart-cdn) to avoid abuse of our encoding platform. Below is a quick Node.js example using our Node SDK, but there are [examples for other languages and SDKs](/docs/api/authentication/#example-code) as well.

\`\`\`javascript
// yarn add transloadit
// or
// npm install --save transloadit

import { Transloadit } from 'transloadit'

const transloadit = new Transloadit({
authKey: 'YOUR_TRANSLOADIT_KEY',
authSecret: 'YOUR_TRANSLOADIT_SECRET',
})

const url = transloadit.getSignedSmartCDNUrl({
workspace: 'YOUR_WORKSPACE',
template: 'YOUR_TEMPLATE',
input: 'image.png',
urlParams: { height: 100, width: 100 },
})

console.log(url)
\`\`\`

This will generate a signed Smart CDN URL that includes authentication parameters, preventing unauthorized access to your transformation endpoints.

## More information

- [Content Delivery](/services/content-delivery/)
- [🤖/file/serve](/docs/robots/file-serve/) pricing
- [🤖/tlcdn/deliver](/docs/robots/tlcdn-deliver/) pricing
- [File Preview Feature](/blog/2024/06/file-preview-with-smart-cdn/) blog post
`),
headers: z
.record(z.string())
Expand Down
6 changes: 6 additions & 0 deletions src/alphalib/types/robots/s3-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ Set to \`true\` if you use a custom host and run into access denied errors.
.optional()
.describe(`
This parameter provides signed URLs in the result JSON (in the \`signed_url\` and \`signed_ssl_url\` properties). The number that you set this parameter to is the URL expiry time in seconds. If this parameter is not used, no URL signing is done.
`),
session_token: z
.string()
.optional()
.describe(`
The session token to use for the S3 store. This is only used if the credentials are from an IAM user with the \`sts:AssumeRole\` permission.
`),
})
.strict()
Expand Down
24 changes: 12 additions & 12 deletions src/alphalib/zodParseWithContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function zodParseWithContext<T extends z.ZodType>(
else if ('unionErrors' in zodIssue && zodIssue.unionErrors) {
// --- Moved initialization out of the loop ---
const collectedLiterals: Record<string, (string | number | boolean)[]> = {}
const collectedMessages: Record<string, string[]> = {}
const collectedMessages: Record<string, Set<string>> = {}

// Process nested issues within the union
for (const unionError of zodIssue.unionErrors) {
Expand All @@ -123,8 +123,11 @@ export function zodParseWithContext<T extends z.ZodType>(

// Ensure paths exist in collection maps
if (!collectedLiterals[nestedPath]) collectedLiterals[nestedPath] = []
if (!collectedMessages[nestedPath]) collectedMessages[nestedPath] = []
if (!collectedMessages[nestedPath]) collectedMessages[nestedPath] = new Set()

if (issue.code === 'custom' && issue.message.includes('interpolation string')) {
continue
}
if (issue.code === 'invalid_literal') {
const { expected } = issue
if (
Expand All @@ -137,23 +140,23 @@ export function zodParseWithContext<T extends z.ZodType>(
collectedLiterals[nestedPath].push(expected)
}
// Still add the raw message for fallback
collectedMessages[nestedPath].push(issue.message)
collectedMessages[nestedPath].add(issue.message)
}
// Keep existing enum handling if needed, but literal should cover most cases
else if (issue.code === 'invalid_enum_value') {
const { options } = issue
if (options && options.length > 0) {
collectedLiterals[nestedPath].push(...options.map(String)) // Assuming options are compatible
}
collectedMessages[nestedPath].push(issue.message)
collectedMessages[nestedPath].add(issue.message)
}
// Keep existing unrecognized keys handling
else if (issue.code === 'unrecognized_keys') {
const maxKeysToShow = 3
const { keys } = issue
const truncatedKeys = keys.slice(0, maxKeysToShow)
const ellipsis = keys.length > maxKeysToShow ? '...' : ''
collectedMessages[nestedPath].push(
collectedMessages[nestedPath].add(
`has unrecognized keys: ${truncatedKeys.map((k) => `\`${k}\``).join(', ')}${ellipsis}`,
)
}
Expand All @@ -177,13 +180,13 @@ export function zodParseWithContext<T extends z.ZodType>(
}
}

collectedMessages[nestedPath].push(
collectedMessages[nestedPath].add(
`got invalid type: ${received} (value: \`${actualValueStr}\`, expected: ${expectedOutput})`,
)
}
// <-- End added handling -->
else {
collectedMessages[nestedPath].push(issue.message) // Handle other nested codes
collectedMessages[nestedPath].add(issue.message) // Handle other nested codes
}
}
}
Expand All @@ -201,10 +204,10 @@ export function zodParseWithContext<T extends z.ZodType>(
}

// Prioritize more specific messages (like invalid type with details)
const invalidTypeMessages = collectedMessages[nestedPath].filter((m) =>
const invalidTypeMessages = Array.from(collectedMessages[nestedPath]).filter((m) =>
m.startsWith('got invalid type:'),
)
const unrecognizedKeyMessages = collectedMessages[nestedPath].filter((m) =>
const unrecognizedKeyMessages = Array.from(collectedMessages[nestedPath]).filter((m) =>
m.startsWith('has unrecognized keys:'),
)
const literalMessages = collectedLiterals[nestedPath] ?? []
Expand All @@ -221,9 +224,6 @@ export function zodParseWithContext<T extends z.ZodType>(
targetMessages.push(...collectedMessages[nestedPath])
}
}

// Prevent the main `messages` array from being populated further for this union issue
continue // Skip adding messages directly from the top-level union issue itself
}
// Handle other specific error codes (only if not handled above)
else {
Expand Down