---
name: applicaster-signed-urls
description: "Signed URL service integration: use when building or hardening the customer-hosted signing endpoint the Applicaster Video Preload plugin calls after Fetch Entry and immediately before playback, to swap the resolved stream URL for a short-lived signed one. Normally a thin adapter in front of a signing system that already exists (CDN token authentication, CDN signed URLs or cookies, a token-issuing service); when no such system exists and a Fetch Entry route is in scope, signing belongs inside that response instead and this service should not be built. Works in any stack. The response is JSON:API — application/vnd.api+json with data.attributes.stream_src — NOT a Pipes2 feed. Validates the request boundary (ctx, src_url, entitlements), enforces no-store caching on per-user signed URLs, and ships a signed-url-e2e tool that checks a running signer against the documented schema."
---

# applicaster-signed-urls

Implement or harden the **Signed URL service** — the customer-hosted endpoint the Applicaster
**Video Preload** plugin calls as a player hook, immediately before playback, to exchange an
unsigned stream URL for a short-lived signed one. The deliverable is a JSON:API endpoint that
satisfies the documented schema, validates its request boundary, refuses to hand signed URLs to
unauthorized users, and is proven against a running service by the skill-local
[signed-url-e2e tool](tools/signed-url-e2e/README.md).

**This is not a Pipes2 feed.** The signer speaks [JSON:API](https://jsonapi.org/):
`Content-Type: application/vnd.api+json` with the playable URL at
`data.attributes.stream_src`. Returning a Pipes2 feed here, or a Pipes2 media type, is the
single most common wrong turn — the two Video Preload actions look adjacent in the plugin
configuration but have entirely different response contracts.

The plugin's other action — **Fetch Entry**, which resolves a whole entry into playable form
and *does* return a Pipes2 feed — is `applicaster-video-preload`. The two run **in order**:
Fetch Entry first, signing second, which is why the signer receives the already-resolved
`content.src` as `src_url`.

> **Establish who owns signing before building anything.** This action normally exists because a
> **separate signing system is already involved** — an existing signing service, CDN token
> service, or issuer that already owns the key — and this endpoint is a thin adapter in front of
> it. That is the case this skill is for. But when no such system exists and Fetch Entry is in
> scope, signing belongs *inside that response*: it already runs immediately before playback and
> already produces `content.src`, so it can return an already-signed URL and leave *Use Signing
> URLs Service* off — one service instead of two. Question 1 settles which situation this is. See
> [signed-url-contract.md](references/signed-url-contract.md#when-this-service-is-the-right-answer-and-when-it-is-not).

## Scope

- Use for a backend that must expose a signing route, most often as a **thin adapter in front of
  a signing system that already exists** — CDN token authentication, CDN signed URLs or cookies,
  or a token-issuing service the customer already runs. A from-scratch signing scheme is the
  less common case, and when a Fetch Entry route is in scope it usually should not be built at
  all (gate 1).
- **Adapt to the target project's stack** — language, framework, router, crypto/signing library
  already in use, validation library, config/secrets handling, test/build commands, logging, and
  deployment conventions. Do not assume JavaScript, and never introduce a hand-rolled signing
  implementation where the CDN vendor ships one.
- The contract is fixed and documented. Confirm live docs; do not invent response keys, media
  types, or status codes.

## Non-Negotiable Gates

1. **Establish who already owns signing before writing code.** This service is the right answer
   when a separate signing system already exists and this endpoint adapts it to the documented
   JSON:API shape. It is the wrong answer when signing would be written from scratch *and* a
   Fetch Entry route is in scope — that route can return an already-signed `content.src`, saving
   a deployment, a key location, and a request on every playback start. Settle question 1 first;
   if the answer is "sign inside Fetch Entry", say so plainly and hand over to
   `applicaster-video-preload` rather than building here.
2. **JSON:API, not Pipes2.** A successful response is `200` with
   `Content-Type: application/vnd.api+json` and a body whose `data` carries a `type`, an `id`,
   and `attributes.stream_src` — a plain-text, absolute, directly playable URL. A failed
   response carries a top-level `errors` array of JSON:API error objects with the same media
   type. See [signed-url-contract.md](references/signed-url-contract.md).
3. **A signing key is a production secret.** It is read from the project's existing secrets
   convention, never committed, never logged, never returned, and never included in an error
   body or diagnostic. If the key is currently hard-coded anywhere in the target project, say so
   plainly and fix it as part of this work.
4. **Sign only what the caller is entitled to.** Validate the request before signing: the
   decoded `ctx`, the credential inside it, and — critically — the stream URL being signed.
   An endpoint that signs whatever `src_url` it is handed is an open signing oracle: any caller
   can mint a valid token for any path the key covers. Constrain it to an allowlisted
   host/prefix, or resolve the URL server-side from an entry id instead of accepting it. This
   is a security gate, not a preference. See [validation-patterns.md](references/validation-patterns.md).
5. **Never cache a signed URL in a shared cache.** Set `Cache-Control: no-store` (or `private`
   with a max-age strictly below the token lifetime, if the project has an established reason).
   A CDN serving one user's signed URL to another is an entitlement bypass. Decide it
   deliberately — do not inherit a framework default.
6. **Token lifetime is a deliberate decision.** Long enough for the player to start and to
   recover from a retry; short enough that a leaked URL expires quickly. Record the chosen
   window and the reasoning in the implementation contract, and confirm the app's behavior when
   a token expires mid-session.
7. Do not claim end-to-end validation without a successful `signed-url-e2e` run against a
   running service, or a concrete named blocker (no runnable URL, no safe test credential, no
   safe signable source URL).
8. Do not report the work complete without a `## Zapp Studio Configuration` section covering the
   Endpoint and its context keys, the signer Feed, the **Video Preload** plugin's
   *Use Signing URLs Service* / *Signer API* parameters, the per-entry
   `extensions.signer_api` override, the **Preload Video Screen**, and the player storyboard hook.

## 1. Establish facts

1. Fetch the current live documentation and reconcile the local references toward it — live docs
   are semantic authority:
   - <https://docs.applicaster.com/integrations/pipes2-signed-urls>
   - <https://docs.applicaster.com/integrations/video-player-signed-url-json-schema>
   - <https://docs.applicaster.com/integrations/video-preload>
   - <https://docs.applicaster.com/using-zapp/player/video-preload-hook>
   - <https://docs.applicaster.com/integrations/pipes2-endpoint-implementation-guide> (context parameters)
   Then read [signed-url-contract.md](references/signed-url-contract.md),
   [signer-plugin-config.md](references/signer-plugin-config.md), and
   [validation-patterns.md](references/validation-patterns.md).
2. Inspect the target project before asking anything. Record, from the code: the stack and
   test/build commands; **which CDN or origin the streams are served from and what signing
   scheme it requires** — CDN token authentication, a CDN signed URL or signed cookie, a custom
   HMAC scheme, or a token-issuing API; whether a signing library or key already exists and how
   secrets are stored; the
   existing feed routes and whether entries already carry `extensions.signer_api`; the
   entitlements source; the validation and logging conventions; and the deployed URL shape.
3. If a signing route already exists, inventory its consumers and tests. Treat any change to its
   response shape, media type, token lifetime, or caching as a compatibility impact, and note
   that shortening a token lifetime can break in-flight sessions.

Completion: the stack, the CDN signing scheme, the key's storage, the entitlements source, and
the live-contract reconciliation are evidenced in a concise sourced fact summary.

## 2. Resolve developer decisions

Present the fact summary, then ask only what the project and docs cannot settle. **One question
at a time**, each carrying the discovered constraint, a recommended answer, and the consequence
of choosing otherwise. If a question exposes a missing fact, go investigate and come back with a
recommendation rather than asking the developer to decide blind.

Ask first, because a "yes" means this skill is the wrong one and no code should be written here:

1. **Who already owns signing — is a separate signing system involved?** This is the question
   that decides whether this skill applies at all.

   - **An existing signing system, CDN token service, or issuer already owns the key** — the
     normal case. This endpoint is a thin adapter in front of it. Continue with this skill, and
     capture what that system needs as input and returns.
   - **Signing would be written from scratch, and a Fetch Entry route is in scope or deployed** —
     recommend signing inside that response and switching *Use Signing URLs Service* off. It
     removes a service, a deployment, a key location, and a request from every playback start.
     Stop here and hand over to `applicaster-video-preload`.
   - **Signing would be written from scratch and there is no Fetch Entry route**, and none is
     planned — a standalone signer is correct. Continue.

   Also continue when the customer names a reason signing cannot live in Fetch Entry even though
   that route exists: the key must not be reachable from the catalogue service; different entries
   need different signers via `extensions.signer_api`; or the token lifetime is short enough that
   minting it at entry-resolution time is too early. Record which reason applies.

Then, once a standalone signer is confirmed:

2. **Which CDN/origin serves the streams, and what signing scheme does it require?** Use the
   vendor's own signing library or documented algorithm — never hand-roll an HMAC scheme that a
   CDN already specifies, and never invent one where the CDN offers signed URLs natively.

Then resolve, only where facts leave real ambiguity:

3. **How does the signer learn which URL to sign** (Gate 4): the plugin's default-signer route
   sends the original stream URL inside `ctx` (conventionally `src_url`) — in which case an
   allowlist of permitted hosts/prefixes is mandatory; or a per-entry `extensions.signer_api`
   URL identifies the asset and the service resolves the stream itself, which is the safer
   default when the catalogue can provide it.
4. **Authorization**: which credential proves entitlement and how it arrives. Default to
   `quick-brick-login-flow.access_token` in `ctx`; name every required key — never invent one,
   consult the live [available context keys](https://docs.applicaster.com/integrations/available-context-keys).
   Confirm the unauthorized status and that the body is a JSON:API `errors` array.
5. **Token lifetime and scope** (Gate 6): the validity window, and whether the token is scoped
   to a path/ACL, a client IP, or a session. Narrower is better; IP scoping breaks on mobile
   network handover, so raise that trade-off explicitly if it is proposed.
6. **Caching** (Gate 5): confirm `no-store`, or the specific established alternative.
7. **Which entries use this signer**: the plugin's single default Signer API feed, per-entry
   `extensions.signer_api` overrides, or both — and if both, which entries override and why.
8. The target project's response-validation convention, and the runnable `signed-url-e2e`
   inputs: signer URL, a safe signable source URL, a safe test credential, and the expected
   error shape.

Completion: every non-discoverable choice affecting the signing scheme, the URL source,
authorization, lifetime, caching, or routing has a recorded developer answer.

## 3. Confirm before edits

Present a short implementation contract and wait for explicit approval:

- The CDN signing scheme and the library or documented algorithm used.
- How the URL to sign is obtained, and the **exact allowlist rule** that stops the endpoint being
  a signing oracle.
- Where the key lives, and how it is loaded.
- Request validation, required `ctx` keys, credential transport, and the unauthorized status and
  JSON:API error body.
- The token lifetime, its scope, and the `Cache-Control` value.
- The response shape: `data.type`, `data.id`, `data.attributes.stream_src`.
- The target-project response validator, the files changing, and the compatibility impact.
- The exact `signed-url-e2e` command and its prerequisites, or the specific blocker.
- The Zapp Studio changes.

Refresh the contract if a material decision changes.

## 4. Implement the contract

1. Validate first, sign second. Decode `ctx` as base64**url**, validate every required key, check
   the credential and entitlement, and validate the URL to sign against the allowlist — all
   before any signing call. Reject with the agreed sanitized error, never with a signed URL.
2. Generate the token with the CDN vendor's library or documented algorithm, loading the key from
   the project's existing secrets convention.
3. Render the JSON:API document: `data.type`, `data.id`, and `attributes.stream_src` as an
   absolute, plain-text, directly playable URL. Validate that document against the schema through
   the project's own validator before returning it; on failure log a sanitized rule identifier and
   return a safe `5xx`, never a partial or malformed body.
4. Set `Content-Type: application/vnd.api+json` and the agreed `Cache-Control`.
5. Return JSON:API `errors` for every failure path, with the same media type and no stream URL,
   token, key material, `ctx` contents, internal URL, or upstream body inside.
6. Add sanitized logging that never emits the key, the token, the signed URL, or `ctx`. Log the
   entitlement decision and the allowlist rejection — those are the two things worth alerting on.
7. Make only the changes a compliant integration needs; report unrelated findings separately.

Completion: the endpoint validates its request boundary, signs only allowlisted URLs for
entitled users, and returns a schema-valid JSON:API document.

## 5. Verify and hand off

1. Run the target project's formatter, typecheck, and its own tests for the changed route —
   including tests that a non-allowlisted `src_url` is refused unsigned, that an unentitled
   request receives the agreed error, that missing/malformed `ctx` and each missing required key
   are rejected, and that a generated token verifies against the CDN's own verification path
   where one is available.
2. Run the skill-local `signed-url-e2e` tool against a running service with a versioned contract
   ([tool README](tools/signed-url-e2e/README.md)). It is black-box and stack-agnostic: it
   asserts the JSON:API media type and schema, an absolute playable `stream_src`, that the
   signed URL is not merely the unsigned source echoed back, the `no-store` caching, the
   unauthorized JSON:API error, the signing-oracle refusal for a foreign `src_url`, and every
   declared `ctx` negative case. Attach the sanitized `--json-report`.
   For local validation, define the server lifecycle in the implementation contract: start the
   service with explicit safe test configuration and a **test signing key**, never the production
   key, on a non-conflicting port; capture its process id; wait with a bounded timeout for
   readiness; run the CLI; and stop that specific process in a cleanup step. A readiness timeout,
   a missing runnable URL, or a missing safe credential is a **blocker and a failing result,
   never a skip**.
3. Report files changed, commands and results, the validator result or precise blocker, required
   config/secrets, compatibility notes, and remaining deployment prerequisites.
4. Include this exact final section.

## Zapp Studio Configuration

Give the customer concrete values and ordered steps.

**1. Endpoint.** In Zapp Studio → Data Sources → Endpoints, add the narrowest HTTPS prefix that
covers the signing route, and attach the context keys it requires. Warn that where prefixes
overlap, **only the longest matching Endpoint supplies context keys**.

| Endpoint | HTTPS base URL | Context keys | Delivery | Reason |
| --- | --- | --- | --- | --- |
| `<name>` | `<url>` | `<key or none>` | `ctx` | `<reason>` |

**2. Feed.** Register the signer as a Feed under that Endpoint — the plugin's *Signer API*
parameter selects a Feed, not a raw URL. Give its Feed Locator and a resolved URL example.

**3. Video Preload plugin.** Add **Video Preload** to every relevant app version and platform,
then set:

| Parameter | Value | Note |
| --- | --- | --- |
| Use Signing URLs Service | on | Required for this integration |
| Signer API | `<signer feed>` | Default signer; the original stream URL arrives inside `ctx` |
| Fetch Entry | `<on/off>` | On only if a Fetch Entry endpoint is also in scope — see `applicaster-video-preload` |

**4. Per-entry override.** Entries that must use a different signer set
`extensions.signer_api` to that signer's URL; it takes precedence over the plugin default.

**5. Screen and hook.** Add the **Preload Video Screen**, then open the player screen, scroll to
SCREEN STORYBOARD, select **Preload Video** from the plugins dropdown, click `+`, and save the
layout.

**6. Rebuild** the app so the plugin takes effect, then play a protected stream end to end and
confirm a fresh signed URL is issued on each attempt.

Finish with explicit blockers: missing plugin on a platform, missing signing key in the
deployment environment, unregistered Endpoint, or absent safe test data.
