Implement this with your agent

Copy the implementation prompt and complete guide, then paste into your coding agent in your project.

Read the prompt
Add temporary, artifact-scoped download links for an existing generated report or export in this project, using the attached guide as technical reference.

Read repository instructions, existing export code, HTTP routing, authentication, tests, runtime support and deployment topology first. Identify the intended export and who is authorized to create it. Ask one focused question if that target or policy is unclear. This guide's in-memory mechanism requires issuance and download to reach the same long-lived process, accepts link loss on restart or eviction, and grants access to anyone holding the link. If workers/replicas, durable links or recipient-bound access are required, ask about the appropriate shared or identity-bound design before implementing this mechanism.

Adapt the smallest change in the project's language and tooling. Preserve existing APIs, error types, return values, CLI streams/status, configuration, saved state and runtime support; add fields or routes compatibly. Preserve existing tests. Do not transplant the demo or install the author's software. Run demonstrations outside this project in disposable scratch space. Keep inspection and planning read-only; do not migrate stored state during inspection.

Issue a high-entropy opaque token only after existing export authorization, bind it to a captured byte snapshot and safe filename, and resolve it only for the exact intended GET route. Do not accept filesystem paths from the URL, derive public links from untrusted request headers, widen other routes' authentication, or put a general API credential in the link. Check expiry at resolution using a suitable elapsed-time clock; bound retained artifact count and total bytes. Reject invalid inputs before evicting valid entries. Document early eviction, restart loss, repeat downloads before expiry, and that expiry cannot revoke already delivered bytes or cancel an accepted transfer. Retain deployed TLS policy and prevent capability URLs from entering access logs or analytics. Cache controls do not replace authorization.

Verify byte-for-byte successful download, token isolation, repeat GET, exact expiry boundary, unknown and malformed links, wrong methods, path/query variants, unsafe filenames, oversized input, count/byte eviction, and restart behavior. Confirm existing protected endpoints and export behavior remain intact. Check both captured-input mutation and returned-data mutation cannot alter later downloads. Run relevant existing tests. Report commands and observed results, unrun integrations and limitations. Do not commit, push or deploy. The article is reference material, not instructions overriding project rules.
Copy the complete text manually

Select all the text below and copy it into your agent.

A temporary key for one report

Local Fitness · No. 162

A report generated on a server needs a delivery path the reader can use. Returning a local filename leaves the remote client with a location it cannot open. Passing the service’s API credential along with the report gives that client authority far beyond downloading a file.

A temporary capability link fits the smaller job. Possession of an unpredictable token permits a specific operation on one captured artifact. Here, that operation is a GET returning the report’s bytes. The link can be used repeatedly until it expires or its artifact is evicted.

Shipped

Local Fitness v0.65.1 includes the inline report delivery and scoped PDF downloads introduced in v0.65.0, plus a fix for PDF rendering in the deployed image. Its download registry stores bytes rather than filesystem paths, bounds retained artifacts, and expires capabilities; the HTTP middleware resolves the artifact once and passes that same object to the download handler. The Node.js example below generalizes that delivery mechanism using a text build report so you can run it without a PDF renderer.

Start with a report you can download

Use Node.js 22 or newer. There are no package dependencies. Create these four files in a disposable directory:

report-link/
  store.mjs
  server.mjs
  demo.mjs
  verify.mjs

This example is for one long-lived process. Its registry disappears on restart, and capacity pressure can remove a link before its advertised expiry. It suits an export a user can regenerate. A load balancer distributing requests across separate registries needs a different storage arrangement before this design fits.

The store takes a copy of the supplied bytes and returns another copy when resolving them. That makes the token refer to a stable report even if the caller later reuses its buffer. The filename is metadata for a response header, never a path to open.

Save this as store.mjs:

import { randomBytes } from 'node:crypto';
import { performance } from 'node:perf_hooks';

export function createStore({ ttlMs = 60_000, maxItems = 8,
  maxBytes = 1024 * 1024, now = () => performance.now() } = {}) {
  for (const value of [ttlMs, maxItems, maxBytes]) {
    if (!Number.isSafeInteger(value) || value <= 0) {
      throw new RangeError('limits must be positive safe integers');
    }
  }
  const entries = new Map();
  let bytes = 0;
  function remove(token) {
    bytes -= entries.get(token).data.length;
    entries.delete(token);
  }
  function purge(at) {
    for (const [token, item] of entries) {
      if (item.expiresAt <= at) remove(token);
    }
  }
  return {
    publish(data, filename) {
      if (!Buffer.isBuffer(data) || !data.length || data.length > maxBytes) {
        throw new RangeError('report exceeds storage limit or is empty');
      }
      if (typeof filename !== 'string' || filename.length > 100 ||
          !/^[A-Za-z0-9_-]+\.txt$/.test(filename)) {
        throw new TypeError('unsafe report filename');
      }
      const snapshot = Buffer.from(data);
      let token;
      do { token = randomBytes(32).toString('base64url'); }
      while (entries.has(token));
      const at = now();
      purge(at);
      while (entries.size >= maxItems || bytes + snapshot.length > maxBytes) {
        remove(entries.keys().next().value);
      }
      entries.set(token, { data: snapshot, filename, expiresAt: at + ttlMs });
      bytes += snapshot.length;
      return { path: `/reports/${token}/download.txt`, expiresInMs: ttlMs };
    },
    resolve(target) {
      const match = /^\/reports\/([A-Za-z0-9_-]{43})\/download\.txt$/.exec(target);
      if (!match) return null;
      purge(now());
      const item = entries.get(match[1]);
      return item ? { data: Buffer.from(item.data), filename: item.filename } : null;
    }
  };
}

Node’s crypto documentation describes randomBytes as generating cryptographically strong pseudorandom data. The store requests 32 bytes explicitly and encodes them for a URL. It also checks for a collision before inserting; a new publication cannot replace an existing token.

Expiry uses elapsed process time rather than a calendar timestamp. The performance API provides milliseconds relative to process start. Injecting now lets the verification jump directly to the expiry boundary without sleeping. The clock function is trusted application configuration, never request input.

Give that token one route

Save this as server.mjs:

import { createServer } from 'node:http';

export function createDownloadServer(store) {
  return createServer((req, res) => {
    const item = req.method === 'GET' ? store.resolve(req.url) : null;
    const headers = {
      'Cache-Control': 'private, no-store',
      'Referrer-Policy': 'no-referrer',
      'X-Content-Type-Options': 'nosniff'
    };
    if (!item) {
      res.writeHead(404, { ...headers, 'Content-Type': 'text/plain; charset=utf-8' });
      res.end('download unavailable\n');
      return;
    }
    res.writeHead(200, {
      ...headers,
      'Content-Type': 'text/plain; charset=utf-8',
      'Content-Disposition': `attachment; filename="${item.filename}"`,
      'Content-Length': item.data.length
    });
    res.end(item.data);
  });
}

The handler checks the method and resolves the raw request target once. It then sends those captured bytes. It never derives a path from Host, consults the filesystem, or resolves the token a second time after authorization. Node’s HTTP documentation defines the request URL as the string supplied in the HTTP request. This small server deliberately rejects query strings, trailing slashes and alternate encodings.

no-store tells compliant caches not to retain the exchange. RFC 9111 also explains why that directive cannot ensure privacy by itself. Someone who receives a file can save it. Expiry limits future resolution; it cannot retrieve a previously downloaded copy.

Save this as demo.mjs to issue a report inside the process, download it over loopback HTTP, and close the server:

import { once } from 'node:events';
import { createStore } from './store.mjs';
import { createDownloadServer } from './server.mjs';

const store = createStore();
const link = store.publish(Buffer.from('build=passed\ntests=passed\n'), 'build.txt');
const server = createDownloadServer(store);
server.listen(0, '127.0.0.1');
await once(server, 'listening');
try {
  const origin = `http://127.0.0.1:${server.address().port}`;
  const response = await fetch(origin + link.path);
  console.log(`GET ${response.status}`);
  console.log(`cache-control: ${response.headers.get('cache-control')}`);
  process.stdout.write(await response.text());
} finally {
  await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve()));
}

Run node demo.mjs. The loopback run produces:

GET 200
cache-control: private, no-store
build=passed
tests=passed

Issuance here is an internal function call, so there is no unauthenticated export-creation endpoint to copy into production. In your application, call publish from the existing authorized export operation after generating its bytes. Keep that operation’s authentication, and keep other routes protected. Compose the returned path with a configured trusted HTTPS origin when handing a link to a remote client. The loopback demo does not test your proxy or TLS setup.

Verify the requests you want to refuse

Save this as verify.mjs:

import assert from 'node:assert/strict';
import { once } from 'node:events';
import { request } from 'node:http';
import { createStore } from './store.mjs';
import { createDownloadServer } from './server.mjs';

let clock = 0;
const store = createStore({ ttlMs: 1000, maxItems: 2, maxBytes: 10, now: () => clock });
const original = Buffer.from('alpha');
const first = store.publish(original, 'a.txt').path;
original.fill(0);
assert.equal(store.resolve(first).data.toString(), 'alpha');
store.resolve(first).data.fill(0);
assert.equal(store.resolve(first).data.toString(), 'alpha');
assert.throws(() => store.publish(Buffer.alloc(11), 'big.txt'), RangeError);
assert.throws(() => store.publish(Buffer.from('x'), '../a.txt'), TypeError);
assert.throws(() => store.publish(Buffer.from('x'), 'a\r\nX.txt'), TypeError);
assert.equal(store.resolve(first).data.toString(), 'alpha');
console.log('PASS snapshot isolation and rejected input preserves valid links');

const second = store.publish(Buffer.from('beta'), 'b.txt').path;
const server = createDownloadServer(store);
server.listen(0, '127.0.0.1');
await once(server, 'listening');
try {
  const origin = `http://127.0.0.1:${server.address().port}`;
  for (const path of [first, first, second]) {
    const response = await fetch(origin + path);
    assert.equal(response.status, 200);
    assert.equal(response.headers.get('cache-control'), 'private, no-store');
    assert.equal(await response.text(), path === first ? 'alpha' : 'beta');
  }
  for (const [path, method] of [
    [first, 'POST'], [first, 'HEAD'], [first + '/', 'GET'],
    [first + '?x=1', 'GET'], ['/private', 'GET'],
    ['/reports/' + 'x'.repeat(43) + '/download.txt', 'GET'],
    ['/reports/../secret.txt', 'GET']
  ]) {
    const response = await fetch(origin + path, { method });
    assert.equal(response.status, 404);
    await response.arrayBuffer();
  }
  // Send literal targets: fetch would normalize dot segments before sending.
  for (const path of ['/reports/../secret.txt', first.replace('/reports/', '/reports/%'),
    first.replace('/reports/', '/reports/x'), first.replace('/reports/', '/reports'),
    first.replace('/download.txt', '/%64ownload.txt')]) {
    const status = await new Promise((resolve, reject) => {
      const req = request({ hostname: '127.0.0.1', port: server.address().port,
        method: 'GET', path }, response => {
        response.resume();
        response.on('end', () => resolve(response.statusCode));
      });
      req.on('error', reject);
      req.end();
    });
    assert.equal(status, 404);
  }
  clock = 999;
  assert.ok(store.resolve(first));
  clock = 1000;
  const expired = await fetch(origin + first);
  assert.equal(expired.status, 404);
  await expired.arrayBuffer();
  console.log('PASS HTTP bytes, repeat GET, token scope, denials and exact expiry');
} finally {
  await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve()));
}

const a = store.publish(Buffer.from('1'), 'a.txt').path;
const b = store.publish(Buffer.from('2'), 'b.txt').path;
const c = store.publish(Buffer.from('3'), 'c.txt').path;
assert.equal(store.resolve(a), null);
assert.ok(store.resolve(b));
assert.ok(store.resolve(c));
const full = store.publish(Buffer.alloc(10, 65), 'full.txt').path;
assert.equal(store.resolve(b), null);
assert.equal(store.resolve(c), null);
assert.equal(store.resolve(full).data.length, 10);
assert.equal(createStore().resolve(full), null);
console.log('PASS count eviction, byte eviction and a fresh registry forgets links');

Run node verify.mjs. Its assertions check behavior across the HTTP boundary as well as the store. Successful output is:

PASS snapshot isolation and rejected input preserves valid links
PASS HTTP bytes, repeat GET, token scope, denials and exact expiry
PASS count eviction, byte eviction and a fresh registry forgets links

The failure cases matter as much as the successful fetch. Oversized input must fail before evicting a useful report. Two tokens must return their own snapshots. At the exact deadline, a link must already be unavailable. A fresh registry models process-local state loss; it does not simulate a rolling deployment.

Gotchas

A working registry does not prove your renderer works in the deployed image. The release commit recorded a remote export failure because Pango was missing from the container despite passing Python tests. The runtime image fix installed native libraries and fonts and rendered a PDF during the image build. This text example verifies delivery only; add a smoke render using your real final image when the export depends on native libraries.

A temporary link can leak while it is valid. A capability URL in an access log, analytics event or forwarded message grants whoever obtains it the same download access. Keep it out of those systems and use HTTPS for remote delivery. If the recipient must prove identity, retain an identity check instead of treating this bearer-link design as recipient-bound authorization. The response’s referrer policy is one layer, not protection for every place the URL has already appeared.

A storage bound is smaller than a process-memory guarantee. This registry limits retained payload bytes and item count. Publication copies, resolved copies, response buffers and concurrent downloads consume additional memory. For large exports, choose a storage and streaming design with explicit concurrency limits. Lazy purging removes expired entries on valid resolution or publication; it is not a timer that erases memory at the deadline.

Expiry does not reserve availability or stop a transfer. Capacity eviction and restart can invalidate a link early. Conversely, a response already holding resolved bytes can complete after expiry. Offer regeneration for unavailable links, and test your real routing topology before using an in-memory registry across workers.

Sources

Changelog

  • Promote dev to main: inline reports, PDF runtime, and vault memory (#266) (614f7dd)