Resource Proxies
Signed, serializable refs for non-serializable values — HMAC signing, TTL, purpose binding, sessions.
Signed, serializable refs for non-serializable values — HMAC signing, TTL, purpose binding, sessions.
Some values are neither serializable primitives nor DI-injectable — a file
handle chosen by the producer, for instance. Proxies pass them through a
payload by reference: deconstruct the value into a signed, serializable
ProxyRef on the producer, carry the ref in the payload, and reconstruct the
live value in the handler.
This is explicit — you call deconstruct and reconstruct yourself, and a
ProxyHandler per resource type does the (de)construction:
String secret = Objects.requireNonNull(
System.getenv("FLEXIQ_PROXY_KEY"), "FLEXIQ_PROXY_KEY is not set");
byte[] key = secret.getBytes(StandardCharsets.UTF_8);
Proxies proxies = new Proxies(key).register(new FileProxyHandler());
// Producer: reduce the file to a signed ref and enqueue it
ProxyRef ref = proxies.deconstruct(new File("/data/uploads/report.csv"));
queue.enqueue(PROCESS_FILE, ref);
// Worker: verify and rebuild the file handle
Worker worker = queue.worker()
.handle(PROCESS_FILE, ref2 -> {
File file = proxies.resolve(ref2);
return process(file);
})
.start();Producer and worker must construct Proxies with the same HMAC key and
register the same handler ids.
A ProxyRef is a record of the handler id, the handler's serializable
reference data, and an HMAC-SHA256 signature over the handler id, the
canonicalized reference, the expiry, and the purpose. Verification happens on
every reconstruct — a modified or forged ref throws ProxyException:
ProxyRef tampered = new ProxyRef(ref.handler(), Map.of("path", "/etc/passwd"), ref.signature());
proxies.reconstruct(tampered); // throws ProxyException: signature mismatchThe ref's shape and signature scheme follow the cross-SDK contract, so a ref produced by one SDK verifies in another sharing the key.
Bind a ref to a wall-clock expiry; reconstructing after it lapses throws. The expiry is folded into the signature, so it cannot be extended by tampering:
ProxyRef ref = proxies.deconstruct(file, Duration.ofHours(1));Bind a ref to a label and require it at reconstruction — a ref minted for one flow cannot be replayed into another:
ProxyRef ref = proxies.deconstruct(file, null, "emails");
proxies.resolve(ref, "emails"); // ok
proxies.resolve(ref, "billing"); // throws ProxyException: purpose mismatch
proxies.resolve(ref); // ok — purpose not checked when not requestedFileProxyHandler proxies a java.io.File by its absolute path. Give it an
allowlist of root directories and reconstruction refuses any path outside them
— including paths that only look inside via a symlinked ancestor, since roots
and candidates are resolved to their real filesystem locations first:
Proxies proxies = new Proxies(key)
.register(new FileProxyHandler(List.of(Path.of("/data/uploads"))));An empty allowlist permits any path — always set one in production.
Implement ProxyHandler<T> for your own types: a stable id(), a handles
test, deconstruct to a serializable map, and reconstruct back. Optionally
override cleanup to release what reconstruct opened — it runs when a
session that produced the value closes:
final class SftpHandler implements ProxyHandler<SftpClient> {
public String id() { return "sftp"; }
public boolean handles(Object value) { return value instanceof SftpClient; }
public Map<String, Object> deconstruct(SftpClient client) {
return Map.of("host", client.host(), "port", client.port());
// never put credentials in a reference — the worker uses its own
}
public SftpClient reconstruct(Map<String, Object> reference) {
return SftpClient.connect((String) reference.get("host"), (Integer) reference.get("port"));
}
@Override
public void cleanup(SftpClient client) { client.close(); }
}Handler ids must be unique — registering a duplicate throws, since a producer and worker disagreeing on an id would reconstruct the wrong thing.
A ProxySession wraps a registry for one unit of work — one producer batch or
one task invocation — adding identity dedup and a cleanup lifecycle:
try (ProxySession session = proxies.session()) {
ProxyRef first = session.deconstruct(file);
ProxyRef again = session.deconstruct(file); // same instance → same ref, handler runs once
SftpClient client = session.resolve(sftpRef);
SftpClient same = session.resolve(sftpRef); // memo hit — one live client
} // cleanup runs here, LIFO, once per unique instanceWithin one session:
close() runs each handler's cleanup once per unique reconstructed
instance, in reverse reconstruction order (LIFO). A cleanup failure is logged
and never skips the rest. Closing is idempotent; sessions are
AutoCloseable, so try-with-resources scopes the lifecycle.Sessions are not thread-safe — confine one to the thread that created it.
Direct Proxies.reconstruct calls have no lifecycle and never trigger
cleanup.
A reference should carry identity, never credentials — the worker authenticates with its own ambient configuration. The signature stops tampering, but anything you put in the reference map is stored in plain form with the job payload.