bundle

Last updated on

Create Bundle artifacts to group multiple artifacts together

Create Bundle artifacts to group multiple documents, forms, and checklists into a single distributable package. Bundles support conditional includes and can reference content inline, by file path, or by registry coordinate.

Examples

Creating a bundle

const lease = p.form({
  name: 'lease',
  fields: {
    pets: { type: 'list', item: { type: 'text' } },
    vehicles: { type: 'list', item: { type: 'text' } },
  },
})

const disclosureDoc = p.document({ name: 'disclosure' })

// Object pattern
const bundle = p.bundle({
  name: 'lease-package',
  version: '1.0.0',
  title: 'Residential Lease Package',
  defs: { hasPets: { type: 'boolean', value: 'forms.lease.fields.pets.length > 0' } },
  contents: [
    { type: 'inline', key: 'lease', artifact: lease },
    { type: 'path', key: 'addendum', path: './pet-addendum.json', include: 'hasPets' },
    { type: 'inline', key: 'disclosure', artifact: disclosureDoc }
  ]
})

// Builder pattern
const bundle = p.bundle()
  .name('lease-package')
  .version('1.0.0')
  .title('Residential Lease Package')
  .def('hasPets', 'forms.lease.fields.pets.length > 0')
  .inline('lease', lease)
  .path('addendum', './pet-addendum.json', 'hasPets')
  .inline('disclosure', disclosureDoc)
  .build()

Loading from external data

// Parse and validate unknown input (throws on error)
const bundle = p.bundle.from(jsonData)

// Safe parsing (returns result object)
const result = p.bundle.safeFrom(jsonData)
if (result.success) {
  const bundle = result.data
}

Assembling a bundle

assemble() renders a set of filled content entries into their target layers and returns the output for each. A content entry is either a filled artifact instance (DraftForm, DraftChecklist, or DraftDocument) or raw bytes for content nothing in Paradoc produced, like a scanned certificate.

There's no resolver option: each artifact-instance entry already carries the resolver it was constructed with, so assembly never needs one of its own.

const assembled = await bundle.assemble({
  contents: {
    lease: filledLease,
    disclosure: preparedDocument,
    checklist: filledChecklist,
    insuranceCert: { kind: 'bytes', content: certBytes, mimeType: 'application/pdf' },
  },
})

// Access assembled outputs, keyed by content key
for (const [key, output] of Object.entries(assembled.outputs)) {
  console.log(key, output.mimeType, output.filename)
  // output.content holds the rendered bytes
}

API

Object Pattern

p.bundle(input: BundleInput): BundleInstance

Parameters

name: string
Unique identifier; must follow slug constraints
version?: string
Artifact version (semantic versioning)
title?: string
Human-friendly name presented to end users
description?: string
Long-form description or context
code?: string
The form number as printed by its issuer (e.g. W-9, ACORD 25), or a self-issued form's own number
language?: string
BCP 47 source language tag; defaults to en when omitted
issuer?: string
Organization that issues the form
edition?: ArtifactEdition
Issuer edition: { key, label, date?, effectiveFrom? }
metadata?: Metadata
Custom metadata map (keys must be alphanumeric with hyphens)
instructions?: ContentRef
Domain or compliance reference content
agentInstructions?: ContentRef
LLM/agent prompts for presentation guidance
defs?: DefsSection
Named typed computed values for conditional includes
contents: BundleContentItem[]
Ordered list of bundle content items

Returns

Returns a BundleInstance with the following properties and methods:

kind: 'bundle'
Artifact discriminator
name: string
Bundle name
version: string | undefined
Semantic version
title: string | undefined
Human-readable title
description: string | undefined
Description text
code: string | undefined
The form number as printed by its issuer (e.g. W-9, ACORD 25), or a self-issued form's own number
language: string | undefined
BCP 47 source language tag
issuer: string | undefined
Issuing organization
edition: ArtifactEdition | undefined
Issuer edition
metadata: Metadata | undefined
Custom metadata map
instructions: ContentRef | undefined
Domain or compliance reference content
agentInstructions: ContentRef | undefined
LLM/agent prompts for presentation guidance
defs: DefsSection | undefined
Named typed computed values
contents: BundleContentItem[]
Bundle content items
Methods
validate: (options?: ValidateOptions) => StandardSchemaV1.Result
Validate the bundle definition
isValid: (options?: ValidateOptions) => boolean
Check if bundle is valid
toJSON: (options?: SerializationOptions) => object
Serialize to JSON (includes the current dated $schema by default)
toYAML: (options?: SerializationOptions) => string
Serialize to YAML (includes the current dated $schema by default)
clone: () => BundleInstance
Deep clone the instance
assemble: (options: BundleAssemblyOptions) => Promise<AssembledBundle>
Resolve contents and render runtime instances
prepare: (contents?: RuntimeBundleContents, options?: RuntimeCreationOptions) => DraftBundle
Create runtime bundle in draft phase. Captures the bundle clock: `options.context.asOf`, or the current instant

Builder Pattern

Chain methods to build a bundle incrementally:

All methods return BundleBuilder and are chainable.

p.bundle()
name: (value: string) => BundleBuilder
Set bundle name (required)
version: (value: string) => BundleBuilder
Set semantic version
title: (value: string) => BundleBuilder
Set human-readable title
description: (value: string) => BundleBuilder
Set description
code: (value: string) => BundleBuilder
Set external reference code
language: (value: string) => BundleBuilder
Set source language (BCP 47 tag)
issuer: (value: string) => BundleBuilder
Set the issuing organization
edition: ({ key, label, date?, effectiveFrom? }) => BundleBuilder
Set the issuer edition
metadata: (value: Metadata) => BundleBuilder
Set custom metadata
instructions: (value: ContentRef) => BundleBuilder
Set domain/compliance reference content
agentInstructions: (value: ContentRef) => BundleBuilder
Set LLM/agent presentation guidance
defs: (defsDef: DefsSection) => BundleBuilder
Set all typed computed values
def: (name: string, expression: string | Expression) => BundleBuilder
Add a single typed computed value
registry: (key: string, coordinate: string, include?: CondExpr) => BundleBuilder
Add registry reference from a coordinate: @org/repo/name[/edition][@version]
path: (key: string, path: string, include?: CondExpr) => BundleBuilder
Add path reference
inline: (key: string, artifact: Artifact, include?: CondExpr) => BundleBuilder
Add inline artifact
contents: (items: BundleContentItem[]) => BundleBuilder
Set all contents at once
removeContent: (predicate: (item, index) => boolean) => BundleBuilder
Remove matching content
clearContents: () => BundleBuilder
Remove all contents
build: () => BundleInstance
Build and validate

Static Methods

Parse bundles from unknown data sources:

from: (input: unknown) => BundleInstance
Parse unknown input (throws on error)
safeFrom: (input: unknown) => Result<BundleInstance>
Parse unknown input (returns result object)

Bundle Content Types

Bundles can contain content from three sources:

Adding content

Use builder methods to add content:

// Registry reference - published artifacts
.registry('lease', '@acme/forms/residential-lease/2024-03@1.0.0')

// Path reference - local files
.path('addendum', './pet-addendum.yaml', 'hasPets')

// Inline - embedded artifacts
.inline('disclosure', p.document({...}))

Content Item Types

InlineBundleItem

Embed an artifact directly in the bundle:

type: 'inline'
Discriminator for inline bundle item
key: string
Unique key for this item within the bundle
artifact: Document | Form | Checklist | Bundle
The embedded artifact
include?: CondExpr
Conditional expression for including this item

PathBundleItem

Reference an artifact by file path:

type: 'path'
Discriminator for path-based bundle item
key: string
Unique key for this item within the bundle
path: string
File path to the artifact definition
include?: CondExpr
Conditional expression for including this item

RegistryBundleItem

Reference a published artifact by address, with an optional edition and version. The builder takes a coordinate, @org/repo/name[/edition][@version], and stores the three parts separately:

CoordinateResolves to
@org/repo/w-9The current edition, latest version
@org/repo/w-9@1.0.0That version of the current edition. Fails if absent; never falls back to another edition
@org/repo/w-9/2024-03Latest version of that edition
@org/repo/w-9/2024-03@1.0.0Full pin
@org/repo/w-9/current@latestThe same as @org/repo/w-9

A two-segment value such as @acme/disclosure throws.

type: 'registry'
Discriminator for registry-based bundle item
key: string
Unique key for this item within the bundle
slug: string
Registry address of the artifact: @org/repo/name, with no edition or version
edition?: string
Edition key. Omitted means the registry's current edition
version?: string
Exact semantic version. Omitted means the latest version of the edition
include?: CondExpr
Conditional expression for including this item

Conditional Includes

All item types support conditional inclusion using defs. Item keys start with a letter and contain only letters, digits, _, and -:

const bundle = p.bundle()
  .name('lease-package')
  // Define typed computed values
  .def('hasPets', 'forms.lease.fields.pets.length > 0')
  .def('hasVehicles', 'forms.lease.fields.vehicles.length > 0')
  // Always include
  .inline('lease', lease)
  // Include only if hasPets evaluates to true
  .path('pet-addendum', './pet-addendum.yaml', 'hasPets')
  // Include only if hasVehicles evaluates to true
  .path('parking-addendum', './parking.yaml', 'hasVehicles')
  .build()

Assembly Options

The assemble() method accepts BundleAssemblyOptions. contents is required: there is no default set of contents, and no resolver option — each content entry is an artifact instance that carries the resolver bound when it was constructed.

contents: Record<string, BundleAssemblyEntry>
Content to assemble, keyed by bundle content key
renderers?: RendererRegistry
Custom renderers keyed by MIME type; layers with built-in support render automatically
context?: { asOf?: string | AsOf }
The clock include conditions read for `today()` and `now()`. Defaults to the current instant

A BundleAssemblyEntry is a filled artifact instance, bytes supplied directly, or a bundle nested in this one:

DraftForm | DraftChecklist | DraftDocument: artifact instance
Rendered through its target layer
{ kind: 'bytes', content, mimeType, filename? }: AssemblyBytesEntry
Content that already has its final bytes, like an annex the packet includes but Paradoc did not produce
DraftBundle: nested bundle
Contributes every one of its parts, keyed as a folder under its content key

assemble() returns an AssembledBundle: the original bundle plus outputs, a Record<string, { content, mimeType, filename }> keyed by content key. A nested bundle's parts are keyed as a folder under the parent's content key: a bundle nested as nested with contents docA and docB gives nested/docA and nested/docB, with the files nested/docA.pdf and nested/docB.pdf. Content keys cannot contain /, so these names never collide with a sibling key. A runtime bundle's render() names its outputs the same way.

Sealing a packet

sealBundle(bundle, options) turns a bundle into one signable PDF packet. It seals each part that declares signature slots, renders the rest, flattens and merges every part's pages in bundle order, and returns one signature map in packet page space.

import { sealBundle } from '@paradoc/core'

const packet = await sealBundle(bundle, {
  contents: {
    lease: filledLease,
    addendum: filledAddendum,
    insuranceCert: { kind: 'bytes', content: certBytes, mimeType: 'application/pdf' },
  },
  signers: {
    // The same person signs both parts.
    'lease/tenant-signer': 'jane',
    'addendum/tenant-signer': 'jane',
  },
})

packet.pdf               // the merged packet
packet.canonicalPdfHash  // 'sha256:...' — what a signer signs
packet.signatureMap      // every slot, on packet pages
contents: Record<string, AssemblyContentEntry>
One runtime artifact or byte entry per bundle content key
renderers?: RendererRegistry
Renderers keyed by layer MIME type, used by every part's render and seal
adapter?: SealAdapter
Converter for a part whose layer is neither PDF nor drawn by a registered renderer
locate?: SealLocator
Locator for anchor placements
signers?: Record<string, string>
Maps `<part>/<signerId>` to a packet signer id. Without an entry, each part signer is its own packet signer
context?: { asOf?: string | AsOf }
The clock include conditions read for `today()` and `now()`. Defaults to the current instant

The result, a SealedBundle, carries bundle, pdf, canonicalPdfHash, packetHash (a digest over the merged PDF's hash and every part's), signers, signatureMap, parts, and warnings. Each part reports its kind ('sealed', 'rendered', or 'annex'), its page range in the packet, and its own digest.

Part signer ids are scoped as <part>/<signerId>, because two parts that both say signer-1 are not evidence that one person signs both. Map them with signers when they are. A failure throws BundleSealError, an Error with a problems: string[] list and an optional part: string content key. Its cause preserves a lower-level rendering or sealing failure when one exists.

Lifecycle Types

Bundles follow a three-phase lifecycle similar to forms (since bundles can contain forms that require signatures):

TypePhaseDescription
DraftBundleDraftMutable contents, configure signers
SignableBundleSignableFrozen data, capture signatures
ExecutedBundleExecutedFully frozen, ready for archival
// Prepare bundle with runtime contents (DraftBundle)
const draft = bundle.prepare({
  lease: filledLeaseForm,
  disclosure: preparedDocument,
  checklist: filledChecklist,
})

// Prepare for signing (SignableBundle)
const signable = draft.prepareForSigning()

// Capture a signature on the underlying form, then update the bundle's copy
const leaseForm = signable.getContent('lease') as SignableForm<Form>
const signedLease = leaseForm.captureSignature('tenant', 'tenant-0', 'john', 'sig-1')
const updated = signable.updateContent('lease', signedLease)

// Finalize (ExecutedBundle)
const executed = updated.finalize()

// executed.executedAt === ISO timestamp

Only included members change phase. An excluded member stays a draft, so it never blocks signing and reinclusion keeps its answers. prepareForSigning() and finalize() throw when a condition is unresolved or an included member has no content.

The bundle clock

prepare() captures one clock, and every include condition that reads today() or now() uses it. A member's own clock never decides inclusion, so members filled at different instants agree. Pass { context: { asOf } } as the second argument to fix it. The clock is kept through every phase and in toJSON().

Save and reload

toJSON() stores the bundle clock and every member with its phase, and the completion time of a checklist or document. runtimeBundleFromJSON(json) rebuilds it:

import { runtimeBundleFromJSON, runtimeContentFromJSON } from '@paradoc/sdk'

const reloaded = runtimeBundleFromJSON(JSON.parse(stored))
// Bind a resolver for file-backed layers:
const withFiles = runtimeBundleFromJSON(JSON.parse(stored), (member) => runtimeContentFromJSON(member, { resolver }))

Loading runs the same checks as prepare() and the phase transitions: an undeclared key, a member of the wrong kind, or a member in the wrong phase throws.

DraftBundle

Created by bundle.prepare(). Contains runtime instances of all artifacts.

phase: 'draft'
Phase discriminator
bundle: Bundle
The underlying bundle definition
context: RuntimeContext
The clock captured by `prepare()`, which include conditions read
executedAt: undefined
Execution timestamp (always undefined for draft)
Methods
getContentKeys: () => string[]
Get all content keys
getContent: (key: string) => RuntimeInstance | undefined
Get runtime instance by key
hasContent: (key: string) => boolean
Check if content exists
getAllContents: () => RuntimeBundleContents
Get all contents
setContent: (key: string, instance: RuntimeInstance) => DraftBundle
Set content instance
removeContent: (key: string) => DraftBundle
Remove content by key
updateContents: (contents: RuntimeBundleContents) => DraftBundle
Update multiple contents
prepareForSigning: () => SignableBundle
Transition to signable phase
render: (options?: RuntimeBundleRenderOptions) => Promise<RuntimeBundleRendered>
Render all contents
toJSON: () => RuntimeBundleJSON
Serialize to JSON
clone: () => DraftBundle
Create exact copy

SignableBundle

Created by draft.prepareForSigning(). Contains signable instances.

phase: 'signable'
Phase discriminator
bundle: Bundle
The underlying bundle definition
context: RuntimeContext
The clock captured by `prepare()`, which include conditions read
executedAt: undefined
Execution timestamp (always undefined for signable)
Methods
getContentKeys: () => string[]
Get all content keys
getContent: (key: string) => RuntimeInstance | undefined
Get runtime instance by key
hasContent: (key: string) => boolean
Check if content exists
getAllContents: () => RuntimeBundleContents
Get all contents
updateContent: (key: string, instance: RuntimeInstance) => SignableBundle
Update content (for signature capture)
finalize: () => ExecutedBundle
Transition to executed phase
render: (options?: RuntimeBundleRenderOptions) => Promise<RuntimeBundleRendered>
Render all contents
toJSON: () => RuntimeBundleJSON
Serialize to JSON
clone: () => SignableBundle
Create exact copy

ExecutedBundle

Created by signable.finalize(). Fully frozen.

phase: 'executed'
Phase discriminator
bundle: Bundle
The underlying bundle definition
context: RuntimeContext
The clock captured by `prepare()`, which include conditions read
executedAt: string
ISO timestamp of execution
Methods
getContentKeys: () => string[]
Get all content keys
getContent: (key: string) => RuntimeInstance | undefined
Get runtime instance by key
hasContent: (key: string) => boolean
Check if content exists
getAllContents: () => RuntimeBundleContents
Get all contents
render: (options?: RuntimeBundleRenderOptions) => Promise<RuntimeBundleRendered>
Render all contents
toJSON: () => RuntimeBundleJSON
Serialize to JSON
clone: () => ExecutedBundle
Create exact copy

On this page