Context
When a key-value-store record is too large to inline (over KV_RECORD_MAX_INLINE_BYTES, 256 KB), resources/read links out with the store's signed recordPublicUrl so the client can fetch it without a token.
Problem
That signature never expires.
getRecordDownloadUrl() in src/resources/api_resources.ts calls apifyClient.keyValueStore(id).getRecordPublicUrl(key), which signs via createHmacSignatureAsync(urlSigningSecretKey, key) — a plain HMAC over the record key, no timestamp. The link stays valid until the store's urlSigningSecretKey rotates (effectively never).
Why it matters
The signed link is a bearer credential: anyone holding the URL can read that record, unauthenticated, indefinitely. It is emitted into model-visible resources/read output, so it can leak via conversation logs, shared transcripts, or third-party LLM providers — and the leak does not expire.
Note: signed record URLs already flow through resources/read today via the /keys endpoint (recordPublicUrl per item), so this is not unique to the large-binary path — but the large-binary change makes the server mint them deliberately.
Options
- Short-lived signature.
@apify/utilities createStorageContentSignature({ urlSigningSecretKey, resourceId, expiresInMillis }) supports expiry, but getRecordPublicUrl does not expose it — would mean bypassing the helper. The record vs content signature formats differ, so confirm the platform validates a content-style signature on a record URL first.
- Only sign when there is no usable token (hosted case); fall back to the unsigned, token-gated API URL otherwise.
- Accept it (matches existing
/keys behavior) and document the exposure.
Context
When a key-value-store record is too large to inline (over
KV_RECORD_MAX_INLINE_BYTES, 256 KB),resources/readlinks out with the store's signedrecordPublicUrlso the client can fetch it without a token.Problem
That signature never expires.
getRecordDownloadUrl()insrc/resources/api_resources.tscallsapifyClient.keyValueStore(id).getRecordPublicUrl(key), which signs viacreateHmacSignatureAsync(urlSigningSecretKey, key)— a plain HMAC over the record key, no timestamp. The link stays valid until the store'surlSigningSecretKeyrotates (effectively never).Why it matters
The signed link is a bearer credential: anyone holding the URL can read that record, unauthenticated, indefinitely. It is emitted into model-visible
resources/readoutput, so it can leak via conversation logs, shared transcripts, or third-party LLM providers — and the leak does not expire.Note: signed record URLs already flow through
resources/readtoday via the/keysendpoint (recordPublicUrlper item), so this is not unique to the large-binary path — but the large-binary change makes the server mint them deliberately.Options
@apify/utilitiescreateStorageContentSignature({ urlSigningSecretKey, resourceId, expiresInMillis })supports expiry, butgetRecordPublicUrldoes not expose it — would mean bypassing the helper. The record vs content signature formats differ, so confirm the platform validates a content-style signature on a record URL first./keysbehavior) and document the exposure.