Skip to content

Static Asset Component Reference

Every binding, endpoint, type, and internal mechanism of StaticAssetComponent. For task-oriented walkthroughs, see Usage & Examples.

Files:

Quick reference

ItemValue
Package@venizia/ignis (core component) + @venizia/ignis-helpers (storage helpers)
Component classStaticAssetComponent
Import subpath@venizia/ignis/static-asset - not on the root barrel
Storage helpersDiskHelper, MinioHelper (@venizia/ignis-helpers/minio), BunS3Helper (@venizia/ignis-helpers/bun-s3)
RuntimesBoth - BunS3Helper specifically requires Bun (imports Bun's native S3Client)
Optional featureMetaLink - Postgres-backed upload tracking via BaseMetaLinkModel/BaseMetaLinkRepository

Import paths

typescript
// Core - subpath import only
import {
  StaticAssetComponent,
  StaticAssetComponentBindingKeys,
  StaticAssetStorageTypes,
  AssetControllerFactory,
  BaseMetaLinkModel,
  BaseMetaLinkRepository,
} from '@venizia/ignis/static-asset';

import type {
  TStaticAssetsComponentOptions,
  TStaticAssetExtraOptions,
  TMetaLinkConfig,
  TStaticAssetStorageType,
  IAssetControllerOptions,
} from '@venizia/ignis/static-asset';

// Helpers - main entry + storage-backend subpaths
import { DiskHelper } from '@venizia/ignis-helpers';
import { MinioHelper } from '@venizia/ignis-helpers/minio';
import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';

Binding keys

KeyConstantTypeRequiredDefault
@app/static-asset-component/optionsStaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONSTStaticAssetsComponentOptionsYes{}

NOTE

StaticAssetComponent's constructor binds an empty {} default for this key. binding() iterates Object.entries(componentOptions) - an empty object produces zero controllers, no error. Bind your configuration in preConfigure() before this.component(StaticAssetComponent).

TStaticAssetsComponentOptions

typescript
type TStaticAssetsComponentOptions = {
  [key: string]: {
    controller: {
      name: string;
      basePath: string;
      isStrict?: boolean;
      routes?: {
        getBuckets?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        getBucketByName?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        createBucket?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        deleteBucket?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        upload?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        listObjects?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        deleteObject?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        getObjectByName?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        downloadObjectByName?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
        recreateMetaLink?: Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>>;
      };
    };
    extra?: TStaticAssetExtraOptions;
    resolveObjectName?: TResolveObjectName;
    defineExtraRoutes?: TDefineExtraRoutes;
  } & (
    | { storage: typeof StaticAssetStorageTypes.BUN_S3; helper: BunS3Helper }
    | { storage: typeof StaticAssetStorageTypes.DISK; helper: DiskHelper }
    | { storage: typeof StaticAssetStorageTypes.MINIO; helper: MinioHelper }
  ) &
    ({ useMetaLink?: false | undefined } | { useMetaLink: true; metaLink: TMetaLinkConfig });
};
FieldTypeDefaultDescription
controller.namestring-Class name given to the generated controller (via Object.defineProperty)
controller.basePathstring-Mount path, for example '/assets'
controller.isStrictbooleantruePassed through to BaseRestController's strict routing mode
controller.routesobjectundefinedPer-route overrides - see Per-route overrides
storage'disk' | 'minio' | 'bun-s3'-Selects which helper type is required (discriminated union)
helperDiskHelper | MinioHelper | BunS3Helper-Storage backend instance matching storage
extraTStaticAssetExtraOptionsundefinedMultipart parsing mode, name/link normalization, max folder depth
useMetaLinkbooleanfalseEnables the PUT .../meta-links/:objectName route and DB tracking on upload/delete
metaLinkTMetaLinkConfig-Required when useMetaLink: true; ignored otherwise
resolveObjectNameTResolveObjectNameundefinedDecides the stored object name - see resolveObjectName
defineExtraRoutesTDefineExtraRoutesundefinedAdds your own routes to the generated controller - see defineExtraRoutes

Per-route overrides

Each key accepts a Partial<Omit<IAuthRouteConfig, 'method' | 'request' | 'responses'>> - typically authenticate, authorize, path, and middleware. It is shallow-merged onto the base definition: { ...StaticAssetDefinitions.UPLOAD, ...routes?.upload }.

Route keyHTTP MethodBase Path
getBucketsGET/buckets
getBucketByNameGET/buckets/{bucketName}
createBucketPOST/buckets/{bucketName}
deleteBucketDELETE/buckets/{bucketName}
uploadPOST/buckets/{bucketName}/upload
listObjectsGET/buckets/{bucketName}/objects
getObjectByNameGET/buckets/{bucketName}/objects/{objectName}
downloadObjectByNameGET/buckets/{bucketName}/download/{objectName}
deleteObjectDELETE/buckets/{bucketName}/objects/{objectName}
recreateMetaLinkPUT/buckets/{bucketName}/meta-links/{objectName} - only registered when useMetaLink: true

TStaticAssetExtraOptions

typescript
type TStaticAssetExtraOptions = {
  parseMultipartBody?: {
    storage?: 'memory' | 'disk';
    uploadDir?: string;
  };
  normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
  normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
  /** Maximum folder nesting depth allowed in object paths. Default: 2 */
  maxFolderDepth?: number;
  [key: string]: any;
};

NOTE

normalizeNameFn receives both originalName and folderPath. The second argument lets a custom implementation decide how to fold the target folder into the stored name. Omit folderPath handling and nested uploads flatten into the bucket root.

FieldDefaultNotes
parseMultipartBody.storage'memory''disk' spools to uploadDir, then the controller reads the file back with readFileSync before handing it to the storage helper
parseMultipartBody.uploadDir'./uploads'Created with fs.mkdirSync({ recursive: true }) if missing
normalizeNameFnBaseStorageHelper's internal lowercase + _-for-space normalizerRuns before the file is written; its output is re-validated with isValidPath()
normalizeLinkFnComponent-generated - see belowRuns after the write to build the returned link
maxFolderDepthBaseStorageHelper.DEFAULT_MAX_FOLDER_DEPTH (2)Folder segments only - the filename itself does not count against this limit

Default normalizeLinkFn

StaticAssetComponent.binding() always supplies a normalizeLinkFn to the factory - your own extra.normalizeLinkFn if set, otherwise this default:

typescript
(opts: { bucketName: string; normalizeName: string }) => {
  const encodedPath = encodeURIComponent(opts.normalizeName);
  return `${controller.basePath}/buckets/${opts.bucketName}/objects/${encodedPath}`;
};

This is why every generated link points back at the objects/{objectName} stream route by default, regardless of storage backend.

BaseStorageHelper has its own backend-specific normalizeObjectLink(). It only runs when you call a helper's upload() directly, outside the component. Through StaticAssetComponent, the component's default normalizeLinkFn always takes priority instead.

Storage types

typescript
class StaticAssetStorageTypes {
  static readonly DISK = 'disk';
  static readonly MINIO = 'minio';
  static readonly BUN_S3 = 'bun-s3';

  static readonly SCHEME_SET = new Set([this.DISK, this.MINIO, this.BUN_S3]);

  static isValid(orgType: string): boolean {
    return this.SCHEME_SET.has(orgType);
  }
}

type TStaticAssetStorageType = TConstValue<typeof StaticAssetStorageTypes>;
// 'disk' | 'minio' | 'bun-s3'
TypeConstantHelperRequires
'disk'StaticAssetStorageTypes.DISKDiskHelperLocal filesystem write access
'minio'StaticAssetStorageTypes.MINIOMinioHelperA MinIO or S3-compatible endpoint
'bun-s3'StaticAssetStorageTypes.BUN_S3BunS3HelperBun runtime (imports Bun's native S3Client)

Storage helpers

IStorageHelper interface

Every backend implements this contract; BaseStorageHelper (abstract) implements the shared parts (isValidName, isValidPath, upload, getMimeType, getFileType) and leaves the rest abstract.

typescript
interface IStorageHelper {
  isValidName(name: string): boolean;
  isValidPath(pathStr: string, opts?: { maxDepth?: number }): boolean;

  isBucketExists(opts: { name: string }): Promise<boolean>;
  getBuckets(): Promise<IBucketInfo[]>;
  getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
  createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
  removeBucket(opts: { name: string }): Promise<boolean>;

  upload(opts: {
    bucket: string;
    files: IUploadFile[];
    normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
    normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
    maxFolderDepth?: number;
  }): Promise<IUploadResult[]>;

  getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
  getStat(opts: { bucket: string; name: string }): Promise<IFileStat>;
  removeObject(opts: { bucket: string; name: string }): Promise<void>;
  removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
  listObjects(opts: IListObjectsOptions): Promise<IObjectInfo[]>;

  getFileType(opts: { mimeType: string }): string;
}
IStorageHelper (interface)
    |
BaseStorageHelper (abstract - implements isValidName/isValidPath/upload/getMimeType/getFileType)
    |
    +-- DiskHelper    (local filesystem)
    +-- MinioHelper   (S3-compatible)
    +-- BunS3Helper   (Bun-native S3, Bun only)

Supporting types

typescript
interface IUploadFile {
  originalName: string;
  mimetype: string;
  buffer: Buffer;
  size: number;
  encoding?: string;
  folderPath?: string;
  [key: string | symbol]: any;
}

interface IUploadResult {
  bucketName: string;
  objectName: string;
  link: string;
  metaLink?: any;
  metaLinkError?: any;
}

interface IFileStat {
  size: number;
  metadata: Record<string, any>;
  lastModified?: Date;
  etag?: string;
  versionId?: string;
}

interface IBucketInfo {
  name: string;
  creationDate: Date;
}

interface IObjectInfo {
  name?: string;
  size?: number;
  lastModified?: Date;
  etag?: string;
  prefix?: string;
}

interface IListObjectsOptions {
  bucket: string;
  prefix?: string;
  useRecursive?: boolean;
  maxKeys?: number;
}

Name and path validation (BaseStorageHelper)

isValidName(name) rejects any of the following, in order:

CheckRejects
TypeNon-string input
EmptyEmpty string
Path traversal.., /, or \
Hidden fileA leading .
Shell metacharacters;, |, &, $, `, <, >, {, }, [, ], !, #
Control characters\n, \r, \0
LengthOver 255 characters
Whitespace-onlyEmpty after trimming

isValidPath(pathStr, { maxDepth }) runs these checks, in order:

StepBehavior
NormalizeTrims leading/trailing slashes
Empty checkRejects if the result is empty
Double slashesRejects empty segments, for example a//b
Folder depthfolderDepth = segments.length - 1; rejects if it exceeds maxDepth (default BaseStorageHelper.DEFAULT_MAX_FOLDER_DEPTH = 2)
Segment namesEvery segment must pass isValidName()
Path lengthRejects a normalized length over 1024 characters

DiskHelper

typescript
interface IDiskHelperOptions {
  basePath: string;    // Base directory for storage
  scope?: string;      // Logger scope, default: 'DiskHelper'
  identifier?: string; // Helper identifier, default: 'DiskHelper'
}

Creates basePath with fs.mkdirSync({ recursive: true }) in the constructor if it does not already exist. Buckets map to subdirectories; objects map to files inside them.

typescript
const diskHelper = new DiskHelper({ basePath: './app_data/storage' });

MinioHelper

typescript
interface IMinioHelperOptions extends ClientOptions { // minio's own SDK options, plus:
  scope?: string;
  identifier?: string;
}

ClientOptions comes straight from the minio package - endPoint, port, useSSL, accessKey, secretKey, and the rest of the MinIO client's own configuration surface.

typescript
const minioHelper = new MinioHelper({
  endPoint: 'minio.example.com',
  port: 9000,
  useSSL: true,
  accessKey: process.env.MINIO_ACCESS_KEY,
  secretKey: process.env.MINIO_SECRET_KEY,
});

BunS3Helper

typescript
interface IBunS3HelperOptions {
  accessKey: string;
  secretKey: string;
  endpoint: string;
  region?: string;       // Default: 'us-east-1'
  sessionToken?: string;
  scope?: string;
  identifier?: string;
}

Wraps Bun's native S3Client, imported from the bun builtin module. That module only resolves under the Bun runtime. This is why BunS3Helper is exported from the separate @venizia/ignis-helpers/bun-s3 subpath, not the main entry point.

typescript
import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';

const bunS3Helper = new BunS3Helper({
  accessKey: process.env.S3_ACCESS_KEY,
  secretKey: process.env.S3_SECRET_KEY,
  endpoint: 'https://s3.us-east-1.amazonaws.com',
});

Controller factory

AssetControllerFactory.defineAssetController(opts: IAssetControllerOptions) builds one controller class per call:

typescript
interface IAssetControllerOptions {
  controller: TStaticAssetsComponentOptions[string]['controller'];
  storage: TStaticAssetStorageType;
  helper: IStorageHelper;
  useMetaLink?: boolean;
  metaLink?: TMetaLinkConfig;
  options?: TStaticAssetExtraOptions;
  resolveObjectName?: TResolveObjectName;
  defineExtraRoutes?: TDefineExtraRoutes;
}
  1. Creates a class extending BaseRestController, decorated @controller({ path: basePath }).
  2. Renames it via Object.defineProperty(GeneratedStaticAssetController, 'name', { value: name, configurable: true }) so logs and DI bindings show your configured controller.name, not a generic factory name.
  3. Binds every route in binding() with this.bindRoute({ configs }).to({ handler }), spread-merging each base definition with its routes?.<key> override.
  4. Registers recreateMetaLink only when useMetaLink && metaLink are both set.
  5. Calls defineExtraRoutes last, after every built-in route.
  6. StaticAssetComponent.binding() registers the resulting class with this.application.controller(...).
StaticAssetComponent.binding()
    | iterates componentOptions
AssetControllerFactory.defineAssetController({ controller, storage, helper, ... })
    | creates
@controller({ path: basePath })
class GeneratedStaticAssetController extends BaseRestController { ... }
    | registered via
this.application.controller(GeneratedStaticAssetController)

resolveObjectName

Decides the object name one uploaded file is stored under. Set it instead of copying the factory to rename a single upload.

typescript
type TResolveObjectName = (opts: {
  originalName: string;
  defaultName: string;
  bucket: string;
}) => string;

defaultName is the name IGNIS would have written without the hook. That is your extra.normalizeNameFn output when you configured one. Otherwise it is the storage helper's lowercase, _-for-space name. Return defaultName and nothing changes.

typescript
resolveObjectName: ({ originalName, defaultName }) => {
  return originalName.startsWith('invoice-') ? originalName : defaultName;
};

The returned name is still validated with isValidPath() before the write, so a traversal cannot leave the bucket.

defineExtraRoutes

Adds routes of your own to the generated controller.

typescript
type TDefineExtraRoutes = (opts: {
  controller: BaseRestController;
  helper: IStorageHelper;
  basePath: string;
}) => void;
typescript
defineExtraRoutes: ({ controller, helper }) => {
  controller.defineRoute({
    configs: {
      method: 'get',
      path: '/health',
      responses: jsonResponse({ schema: z.object({ buckets: z.number() }) }),
    },
    handler: async context => {
      const buckets = await helper.getBuckets();
      return context.json({ buckets: buckets.length }, HTTP.ResultCodes.RS_2.Ok);
    },
  });
};

The hook runs after every built-in route, so a built-in route wins a path collision with yours. basePath is the mount path with exactly one leading slash, the same value the default normalizeLinkFn builds links from.

MultipartBodySchema

The Zod schema validating the upload request body:

typescript
const MultipartBodySchema = z.object({
  files: z.union([z.instanceof(File), z.array(z.instanceof(File))]).openapi({
    type: 'array',
    items: { type: 'string', format: 'binary' },
  }),
});

Endpoint reference

MethodPathNotes
GET/bucketsNo params. Returns IBucketInfo[]
GET/buckets/{bucketName}Returns IBucketInfo | null
POST/buckets/{bucketName}Returns the created IBucketInfo. Throws if the bucket already exists or the name is invalid - see Error Reference
DELETE/buckets/{bucketName}Returns { isDeleted: boolean }
POST/buckets/{bucketName}/uploadmultipart/form-data body; query: principalType?, principalId?, variant?, folderPath?. Returns IUploadResult[]
GET/buckets/{bucketName}/objectsQuery: prefix?, recursive? ('true' string only), maxKeys? (positive integer string). Returns IObjectInfo[]
GET/buckets/{bucketName}/objects/{objectName}Streams the file inline. objectName is a single percent-encoded segment
GET/buckets/{bucketName}/download/{objectName}Streams the file with Content-Disposition: attachment
DELETE/buckets/{bucketName}/objects/{objectName}Returns { success: boolean }
PUT/buckets/{bucketName}/meta-links/{objectName}Only registered when useMetaLink: true. Returns { success: boolean, metaLink }

Upload validation order

  1. bucketName validated with isValidName() - 400 "Invalid bucket name" on failure.

  2. If folderPath is present, three checks run in order:

    CheckFailure
    Trim leading/trailing slashes400 "Invalid folder path" if empty after trimming
    Segment count vs. maxFolderDepth400 "Folder path exceeds max depth of {n}" if over
    Each segment via isValidName()400 "Invalid folder path segment: {segment}" if any fails
  3. multipart/form-data parsed via parseMultipartBody().

  4. Each file's effective buffer is checked non-empty - direct buffer, or readFileSync(file.path) when storage: 'disk' was used. Empty content returns 400 "Empty file content | name: {originalName}".

  5. helper.upload() runs the storage-helper-level checks below.

  6. Spool files written by storage: 'disk' parsing are removed in a finally block via rmSync({ force: true }) - regardless of success or failure. Removal errors are logged, never thrown.

Storage-helper-level upload checks (BaseStorageHelper.upload)

These run inside helper.upload(), separate from the controller checks above. They are reachable even when a caller uses the storage helper directly:

CheckError messageDefault status
Bucket does not exist (isBucketExists() false)[upload] Bucket does not exist | name: {bucket}400
originalName fails isValidName()[upload] Invalid original file name400
folderPath segment count exceeds maxFolderDepth[upload] Invalid folder path | depth: {n} | max: {m}400
folderPath fails isValidPath() for any other reason[upload] Invalid folder path400
size is undefined, null, or negative[upload] Invalid file size | size: {size}400
Normalized name (post normalizeNameFn) fails isValidPath()[upload] Invalid normalized object name | name: {name}400

getError() defaults statusCode to 400 when the caller does not pass one explicitly. Every message above is thrown without an explicit status, so all resolve to 400.

Header sanitization

typescript
const WHITELIST_HEADERS = [
  'content-type',
  'content-encoding',
  'cache-control',
  'etag',
  'last-modified',
] as const;

These correspond to HTTP.Headers.CONTENT_TYPE, CONTENT_ENCODING, CACHE_CONTROL, ETAG, and LAST_MODIFIED from @venizia/ignis-helpers.

When streaming a file - both objects/{objectName} and download/{objectName} - the controller copies only these keys from the storage metadata onto the response. Every other metadata header is dropped. Each forwarded value is sanitized with String(value).replace(/[\r\n]/g, '') before being set, to prevent HTTP header injection.

All streaming responses also set:

http
X-Content-Type-Options: nosniff
Content-Type: <from metadata, or application/octet-stream as fallback>
Content-Length: <file size in bytes>
Content-Disposition: attachment; filename="..."   (download endpoint only, via createContentDispositionHeader())

Object name decoding

Hono percent-decodes a path param before the handler reads it. The controller's readObjectName() is therefore a deliberate no-op - it does not run a second decodeURIComponent():

  • report_100%.pdf is a legal object name. Its link is .../objects/report_100%25.pdf. Hono hands the handler back report_100%.pdf. A second decode would hit the invalid escape %.p and throw - the object would become permanently unfetchable and undeletable.
  • An object named a%2Fb.png would decode twice into a/b.png - a different object than the one requested.

isValidName()/isValidPath() still run on the singly-decoded value, so a traversal payload is rejected exactly as before.

TMetaLinkConfig

typescript
type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
  model: typeof BaseRelationalEntity<Schema>;
  repository: DefaultRelationalRepository<Schema>;
  createMetaLink?: (opts: {
    uploadResult: IUploadResult;
    fileStat: IFileStat;
    query: TUploadQuery;
  }) => ValueOrPromise<{ count: number; data: Schema }>;
};
Canonical nameAlias
BaseRelationalEntityBasePostgresEntity
DefaultRelationalRepositoryDefaultCRUDRepository

Both canonical classes live in packages/core-server/src/connectors/postgres/. Each alias re-exports the same class under a different name.

Table: MetaLink

FieldTypeNullableDefaultDescription
idTEXTNo-Primary key
created_atTIMESTAMPTZNoNOW()Row creation time
modified_atTIMESTAMPTZNoNOW()Row last-update time
bucket_nameTEXTNo-Storage bucket name
object_nameTEXTNo-File object name (may include folder segments)
linkTEXTNo-Access URL to the file
mimetypeTEXTNo-File MIME type
sizeINTEGERNo-File size in bytes
etagTEXTYes-Entity tag for versioning
metadataJSONBYes-Additional file metadata
storage_typeTEXTNo-'disk', 'minio', or 'bun-s3'
is_syncedBOOLEANNofalseSet true on every upload and every meta-links sync
variantTEXTYes-Upload variant tag (for example 'thumbnail', 'original')
principal_typeTEXTYes-Associated principal type
principal_idTEXTYes-Associated principal ID, always stored as a string

Indexes: bucket_name, object_name, storage_type, is_synced.

@model({ type: 'entity', skipMigrate: true }) on BaseMetaLinkModel means IGNIS's schema migration skips this table. Create it manually, once, per database.

  • On upload:

    • Creates one MetaLink row per uploaded file, after fetching fresh stats via helper.getStat().
    • Uses metaLink.createMetaLink() when provided, otherwise a default insert that covers every standard field.
    • principalType, principalId, and variant come from the upload's query parameters.
    • If the insert throws, the upload still succeeds. The file's response entry gets metaLink: null plus a metaLinkError string, and the error is logged.
  • On delete:

    • The storage delete happens first and is awaited.
    • The MetaLink row delete (deleteAll({ where: { bucketName, objectName } })) fires without being awaited.
    • The HTTP response returns as soon as the storage delete resolves - the database delete may still be in flight.
    • Errors there are logged, never surfaced to the client.
  • On sync (PUT meta-links/:objectName): looks up an existing row by bucketName + objectName.

    Row found?Action
    YesupdateById(), then re-fetches with findById()
    Nocreate()

    Either path sets isSynced: true and returns { success: true, metaLink }.

Component lifecycle

  1. binding() reads STATIC_ASSET_COMPONENT_OPTIONS from the DI container.
  2. Iterates each key in the options object.
  3. For each entry, builds a normalizeLinkFn default if the caller did not supply one (see Default normalizeLinkFn).
  4. Calls AssetControllerFactory.defineAssetController() and registers the result with this.application.controller().
  5. Logs the storage key, storage type, and whether MetaLink is enabled for each registered backend.

StaticAssetComponent itself performs no eager configuration validation beyond the options type. A missing metaLink when useMetaLink: true is caught at compile time by the discriminated union, not at binding() runtime.

See also

  • Overview - quick start, imports, and common configuration tasks
  • Usage & Examples - task-oriented walkthroughs for every endpoint and MetaLink setup
  • Error Reference - name validation rules and troubleshooting