Lolly
ایپ کھولیں

Outbound delivery

نوٹ: Lolly Work

This guide covers the optional organisation service. Configuration, console and API paths refer to your Work instance. The packaged console keeps its release-matched copy of this guide.

Outbound delivery sends one finished Lolly export to a fixed destination owned by the organization. Brandfolder and Cloudinary adapters cover fixed DAM destinations. Three open adapters cover object storage, sovereign file servers and a small receiver protocol: generic S3-compatible storage (AWS S3, MinIO, Ceph RGW, Garage, UpCloud), WebDAV (including Nextcloud/ownCloud), and HMAC-signed HTTPS.

This is not a writable catalog provider. Catalog providers federate remote assets into Lolly, normally read-only; delivery sends a generated output out. Reusing the same credential would silently upgrade read authority to write authority, so the two contracts, registries and secrets stay separate.

Freedom boundary

lolli.li is the Lolly open-source project's release/model file host, not a user-upload service and not a built-in destination. Its UpCloud bucket is used only by an explicitly gated developer canary to verify S3 interoperability; the canary writes inert bytes under a random technical key and deletes that exact key in finally.

Configure a fixed S3 destination

{
  "delivery": {
    "maxBytes": 67108864,
    "destinations": [
      {
        "id": "campaign-archive",
        "kind": "s3",
        "label": "Campaign archive",
        "credentialRef": "LW_DESTINATION_CAMPAIGN_ARCHIVE",
        "enabled": true,
        "groups": ["marketing", "brand"],
        "formats": ["svg", "png", "jpg", "pdf"],
        "maxBytes": 33554432,
        "options": {
          "bucket": "approved-output",
          "endpoint": "https://minio.example.org",
          "region": "us-east-1",
          "prefix": "campaigns"
        }
      }
    ]
  }
}

Set the referenced secret in the deployment environment:

LW_DESTINATION_CAMPAIGN_ARCHIVE='<accessKeyId>:<secretAccessKey>'

Use a write key dedicated to this destination and restricted to the configured bucket/prefix. Do not reuse the catalog-provider credential, the instance BlobStore credential, or an operator's broad release key. publicBaseUrl is optional and means exactly what it says: when present, Work marks the descriptor public and returns the corresponding object URL in the receipt. An unguessable public URL is not access control.

Configure a fixed WebDAV destination

The collection and any prefix subdirectory must already exist. Delivery neither browses the account nor creates a folder tree; it can write only beneath the fixed collection URL.

{
  "id": "team-files",
  "kind": "webdav",
  "label": "Approved team files",
  "credentialRef": "LW_DESTINATION_TEAM_FILES",
  "enabled": true,
  "groups": ["brand"],
  "formats": ["svg", "png", "pdf"],
  "options": {
    "url": "https://cloud.example/remote.php/dav/files/team/outgoing",
    "prefix": "approved"
  }
}

Set LW_DESTINATION_TEAM_FILES='<username>:<app-password>', or use bearer:<token> for a token-fronted DAV server. For Nextcloud, use a revocable app password, not the account password. A deterministic flat filename makes a retry overwrite the same resource; a successful PUT is followed by authenticated HEAD and bounded GET verification of size and SHA-256 before the receipt claims byte preservation. publicBaseUrl is optional and separate from the credentialed collection URL.

Configure a signed-HTTPS destination

This is the adapter for an organization-owned publishing service without coupling Work to its vendor or API. The target is one exact HTTPS endpoint, not a user-entered webhook:

{
  "id": "press-publisher",
  "kind": "https",
  "label": "Press publisher",
  "credentialRef": "LW_DESTINATION_PRESS_PUBLISHER",
  "enabled": true,
  "groups": ["communications"],
  "formats": ["png", "jpg", "pdf"],
  "options": { "url": "https://publisher.example/lolly-delivery" }
}

Set the referenced variable to a dedicated random HMAC secret. Work sends the raw export body with x-lolly-delivery-id, x-lolly-format, a base64url x-lolly-name, x-lolly-content-sha256, x-lolly-timestamp, and x-lolly-signature: v1=<hex-hmac-sha256>. The signed canonical string is:

lolly-delivery-v1\n<timestamp>\n<delivery-id>\n<format>\n<base64url-name>\n<content-type>\n<size>\n<sha256>

The receiver should reject stale timestamps and duplicate delivery ids. It may return no body, or JSON { "id": "…", "url": "https://…", "sha256": "…" } (maximum 64 KiB). A matching digest yields transformation: "none"; a different digest is recorded as "provider-managed"; no digest stays honestly "unknown". Work does not offer a revoke for this protocol because it has no safe universal delete contract.

Configure Brandfolder writes

Use a separate write credential, even when Brandfolder is already a catalog source. The existing read-only source does not grant delivery permission. Configure one existing Brandfolder and file section per destination:

{
  "id": "brandfolder-campaigns",
  "kind": "brandfolder",
  "label": "Brandfolder campaigns",
  "credentialRef": "LW_DESTINATION_BRANDFOLDER",
  "enabled": true,
  "groups": ["brand"],
  "formats": ["png", "jpg", "pdf"],
  "projectFiles": true,
  "options": {
    "brandfolderId": "your-brandfolder-id",
    "sectionId": "your-section-id",
    "visibility": "private",
    "uploadOrigins": ["https://your-qualified-upload-origin.example"],
    "downloadOrigins": ["https://your-qualified-original-origin.example"]
  }
}

The referenced environment variable holds the Brandfolder API token. Supply exact HTTPS storage origins observed and qualified for your tenant; the example origins are placeholders. API and upload redirects are refused. Original readback follows at most three redirects, checking the exact download origin at every hop without API credentials. Bearer credentials go only to https://brandfolder.com/api/v4, never to storage. visibility declares the existing Brandfolder's access policy; it does not change that policy. Requalify it when administrators change remote access.

The adapter follows Brandfolder Binary Upload: request a temporary upload ticket, PUT the original, then create one asset in the pinned section. Brandfolder's temporary object URL is accessible to its importer and is vendor-managed; its docs state that upload URLs expire after 24 hours and temporary objects after seven days. Work never publishes its own source blob or returns those temporary URLs to the shell.

A private progress record is saved before creation and again with the created asset ID. Processing or verification failures retry that known asset, without uploading or creating it again. If the create response is lost, Brandfolder documents no idempotency key: the delivery stops with an uncertain-outcome message. An operator must reconcile the asset before another create is attempted. Do not create a fresh delivery just to bypass that message. This adapter never updates, replaces, moves or deletes an existing Brandfolder asset and does not assign collection membership.

Configure Cloudinary writes

This first adapter stores authenticated raw originals in an explicitly declared dynamic-folder product environment. It does not generate image/video renditions or publish a public CDN URL.

{
  "id": "cloudinary-originals",
  "kind": "cloudinary",
  "label": "Cloudinary originals",
  "credentialRef": "LW_DESTINATION_CLOUDINARY",
  "enabled": true,
  "groups": ["brand"],
  "formats": ["png", "jpg", "svg", "pdf", "mp4"],
  "maxBytes": 67108864,
  "projectFiles": true,
  "options": {
    "cloudName": "your-cloud",
    "region": "eu",
    "folderMode": "dynamic",
    "assetFolder": "campaigns",
    "publicIdPrefix": "lolly-deliveries"
  }
}

The secret is JSON { "apiKey": "...", "apiSecret": "..." }, used only on the server. Regions are us (default), eu and ap. The folder and public-ID prefix are separately pinned: asset_folder does not imply a public-ID path in dynamic-folder mode. Fixed-folder tenants are not supported by this first writer. Keep the delivery ceiling at or below 64 MiB; chunked uploads are outside this adapter.

Uploads use an operation-specific public ID, type=authenticated and overwrite=false. A retry first looks up that ID through the Admin API. Existing bytes are accepted only when destination, operation markers and the original download digest all match; an ID collision is never replaced. Only the delivery ID and SHA-256 are added as reconciliation context. No source tags, approval, private notes, prompts or other organization fields are copied. The adapter sends no upload preset, eager/incoming transformation, moderation, AI/add-on or URL-import parameters. Qualify tenant-wide processing settings separately. See the Upload API and folder modes.

Both DAM adapters read the stored original back, with a byte ceiling, and compare SHA-256 before issuing a delivered receipt. A successful fixture run is not live vendor qualification: before enabling a destination, use a dedicated test section/folder to qualify credentials, tenant settings, readback, access, processing latency and timeout recovery.

Send a shared original

With projectFiles: true on a DAM destination, the Assets view offers that destination when the selected asset is a shared-project original. The shell POSTs an empty body to /api/v1/destinations/:id/deliveries?projectId=...&fileId=...&checksum=.... No name or format override is accepted. The server reads the ready file, verifies every stored part and its whole digest, and derives the format from bytes with the stored filename as fallback. The destination allowlist and byte ceiling apply. No browser download/upload round trip is needed.

The original stays in its project. Work stages an immutable private copy for retry, retaining the source selector only on the server. The configured submit scan hook runs before provider egress, using the same reject/error policy as catalog admission. No hook means no scan was performed. The source person's current project access, account status, destination grants and configuration are checked again before egress, after scanning, on retry and after any human approval. Service tokens cannot select project originals. The destination's access rules apply separately from the source project; sending an original does not copy its project permissions or approval state.

Raw-body and automation-output routes continue to require genuine signed Lolly exports. This capability does not make arbitrary catalog or device bytes eligible for governed publishing.

Delivery lifecycle

The raw body is accepted only when it carries Lolly's C2PA export assertion. Before provider egress, Work records its SHA-256 and stages the exact bytes in the configured BlobStore:

awaiting-approval --approve--> queued -> delivering -> delivered
        |                               \-> failed -> retry -> delivering
        \--reject/withdraw--> rejected/cancelled

The record binds the destination version, name, format, MIME, size and SHA-256. Those facts are immutable in both memory and Postgres stores. Attempts are claimed atomically across replicas, so concurrent retries cannot both execute. A process interrupted while delivering remains held for operator reconciliation; it is not automatically taken over by another worker. For S3, a retry uses the staged bytes and the same key:

<prefix>/<delivery-id>/<sha256-prefix>-<safe-filename>.<format>

If the destination's endpoint, bucket, prefix, formats, exposure or other semantics change, the version changes and retry returns 409 DESTINATION_CHANGED; the caller creates a new delivery deliberately. Credential rotation does not move the version.

Delivering an automation output

POST /api/v1/jobs/:id/deliveries consumes a completed render job owned by the same member or service principal. The JSON body is { "destinationId": "campaign-archive", "name": "Launch poster" }; an optional format must equal the immutable render request rather than overriding the render request. When the job completes, Work records SHA-256 over the actual output bytes independently of the blob provider (an S3 ETag is not a portable content digest). Delivery reads the retained result, verifies it against that digest, re-verifies its Lolly Content Credential, then points the delivery's private sourceRef at that existing blob. There is no download/upload round trip and no second stored copy. Jobs completed before digest storage was introduced must be rendered again before this route can deliver them.

The delivery receipt exposes sourceJobId for correlation, never the private blob reference. The source job cannot be deleted while any delivery retains its output (409 JOB_OUTPUT_IN_USE), including after successful egress, so retry and audit cannot be made false by deleting their evidence. A later retention policy will release both deliberately. A service token still cannot enter a destination's human approval chain; that boundary is identical for raw-body and job-output creation.

Optional human approval

Set approvalChain on a destination to an existing chain id. Creation then returns 202 with state: "awaiting-approval" and an approvalId; no provider request has happened. The approval subject is the delivery id, whose immutable record already binds destination version, name, format, size and digest. A terminal approval runs that exact staged record. Rejection becomes the terminal delivery state rejected, withdrawal becomes cancelled, and neither can be relabelled as a transport failure or bypassed through the retry route. A configured chain that does not exist fails closed before the request body is accepted. Service tokens cannot create a human-review request; later workflow automation must enter through its own explicit approval step rather than laundering a token as a person.

S3 PUT is followed by a signed HEAD size check; WebDAV performs authenticated PUT/HEAD/bounded-GET digest verification. Their receipts record transformation: "none" and the delivered digest. Signed HTTPS derives that status from the receiver's digest instead of pretending every publisher is byte-preserving. Redirects are refused by all three adapters. Delivery creation and retry share the existing automation rate-limit bucket because each may spend an organization-owned credential; read-only discovery and receipt history do not.

Delivery history is principal-scoped. Stored records and audit events contain destination ids and output facts, never credentials. Removing a destination stops new sends and retries but does not delete remote objects or erase historical receipts.

API example

curl -X POST \
  -H 'Cookie: lw_session=…' \
  -H 'Content-Type: image/png' \
  -H 'Idempotency-Key: campaign-42-poster-v1' \
  --data-binary @poster.png \
  'https://work.example/api/v1/destinations/campaign-archive/deliveries?name=Launch%20poster&format=png'

Discovery and history routes are listed in the API reference.

Interoperability canaries

Three gated tests check each adapter against a real endpoint. Each one is skipped unless its switch is set to 1, writes inert text under a random canary-<uuid> delivery id, and removes that exact object afterwards, or fails and lists each id whose removal it could not confirm. None of them lists or browses the remote side or prints a credential.

TestSwitchSettings (each prefixed with the switch name)
tests/s3-upcloud-canary.test.tsLW_S3_CANARY_ENDPOINT, _REGION, _BUCKET, _ACCESS_KEY, _SECRET_KEY
tests/webdav-delivery-canary.test.tsLW_WEBDAV_CANARY_URL, _CREDENTIAL, optional _PREFIX
tests/https-delivery-canary.test.tsLW_HTTPS_CANARY_URL, _SECRET, _CLEANUP_URL, optional _CLEANUP_CREDENTIAL

Point the WebDAV canary at an existing collection set aside for the test. It delivers twice to confirm that a retry overwrites the same resource, then reads the bytes back with its own request. The signed-HTTPS protocol has no delete contract, so that canary deletes through the receiver's own management API: LW_HTTPS_CANARY_CLEANUP_URL is an HTTPS URL with one {id} placeholder, filled with the id from each receipt, and a GET of the same URL must then answer 404 or 410. When a receipt is missing, unusable or has no id, the canary uses the delivery id instead and counts the item as removed only when the DELETE succeeds. The management API should resolve delivery ids; if it cannot, the canary reports the cleanup as unconfirmed and the operator must check the receiver by hand. When the receipt carries a digest, it must match the bytes sent. A second HTTPS test sends three requests the receiver must refuse with a 4xx status: a signature from another secret, a timestamp a day old, and a body that does not match its signed digest.

Current edge of the first slice

Built now: shell presentation separated from personal connections; optional approval-chain binding over immutable staged bytes; completed automation-render output consumed by immutable reference with principal isolation and delete protection; fixed config-managed S3, WebDAV and signed-HTTPS targets; group/RBAC projection; C2PA and byte caps; durable records in both stores; idempotency; exact-byte retry; provider receipts; and audit events. The S3 wire is live-validated against the project's UpCloud host. WebDAV and HTTPS have injected-wire tests and the gated canaries above, which have not yet run against a real tenant or receiver, so neither adapter is called live-verified.

Future asset-system destinations must also declare create/new-version/replace and conflict policy before writing. Transfer acceptance, processing, original readiness, review and public activation need separate observations. Reconcile uncertain completion using the remote asset/version identity and committed effects before another write. Source approval must retain its purpose, activation target and human/automated origin; it cannot stand in for approval of the staged final digest. These are qualification requirements, not capabilities added to the current S3, WebDAV or HTTPS adapters.

Still deliberately pending:

Those additions extend this record and adapter seam; they do not widen catalog providers or the personal connected-services contract.