@sequenceholdings/managed-functions
v0.2.0
Published
Managed Function context and opt-in cache runtime
Readme
@sequenceholdings/managed-functions
@sequenceholdings/managed-functions is the TypeScript SDK for Managed
Functions. It provides caller-scoped ORM access and opt-in caching. The
/orm import loads only the dependency-free ORM execution client, without
initializing cache, storage, authentication or tracing libraries.
Managed Functions is a workflow-agnostic platform primitive. The runtime owns generic invocation, isolation, secrets, egress, caching, and observability; deployed functions own business logic. Primitive code must not identify a function consumer or branch on a function ID.
ORM as the invoking user
Available in SDK 0.2.0 and later. Install the SDK in the function package;
the separate ORM authoring/compiler package is not needed in the deployed
function. Copy the namespace's generated operations.manifest.json into that
package, and keep it synchronized with the applied namespace version.
import type { Request, Response } from '@google-cloud/functions-framework'
import { createFunctionOrmExecutor, FunctionOrmError } from '@sequenceholdings/managed-functions/orm'
import manifest from './operations.manifest.json'
// Nonsecret configuration: use your deployment's URL, never request input.
const platformOrigin = 'https://your-environment.example.com'
export async function handler(request: Request, response: Response) {
try {
// Create inside every invocation; never keep this executor in module state.
const orm = createFunctionOrmExecutor({
request,
baseUrl: platformOrigin,
namespace: 'directory',
manifest,
})
const profile = await orm.execute('GetMyProfile', {})
response.json(profile)
} catch (error) {
if (error instanceof FunctionOrmError) {
response.status(403).json({ error: error.code })
return
}
// Do not expose arbitrary exceptions or upstream response bodies.
response.status(502).json({ error: 'PROFILE_UNAVAILABLE' })
}
}The manifest declares the account and data scope. Replace these illustrative names with your provisioned account, namespace, relations and mutation names:
service_account: directory-runtime
capabilities:
data:
directory:
tables: [my_profile]
operations: [UpdateMyProfile]
query: falseThe deployer needs permission to bind the service account. The human caller
needs function invocation access plus the namespace runtime grants and operation
capabilities. tables covers read relations, including linked selections and
policy dependencies; operations contains persisted mutation names. These
declarations restrict access; they do not grant the caller access. Apply matching
namespace operations before deploying consumers. Open the Managed Functions guide at /docs/managed-functions/overview on
your administrator-supplied platform URL for provisioning, grants and deployment.
createFunctionOrmExecutor({ request, baseUrl, namespace, manifest, fetchImpl? })
returns an isolated executor with execute(operationName, variables?). It sends
only registered operation hashes and variables. The optional fetchImpl is for
testing or an application-owned transport; it must honor redirect: 'error'.
It never retries requests, follows redirects, reads credentials from a request
body, substitutes a service token, or configures process-wide ORM state. Reuse
within one handler invocation is supported; discard it when the handler returns.
baseUrl must be an explicitly configured HTTPS origin, with no path, query,
fragment or embedded credentials. Loopback HTTP is accepted for a local platform.
There is no automatic environment URL: the helper never trusts a request header
or body to choose where it sends a delegated credential.
Caller identity and trust boundary
This helper supports direct human invocations through the platform gateway. Scheduled/system-runner and service-account invocations fail closed. They do not silently become an attached service account or a recorded human initiator. No Function UUID, profile lookup, signing key, bearer token or claims decoder is required in application code.
The platform must control ingress to the deployed handler. The helper consumes the request delivered by that gateway; it does not authenticate arbitrary HTTP requests or make a publicly reachable Functions Framework server safe. Its local metadata checks only reject an unsupported or expired invocation mode. The ORM server verifies the credential's signature and expiry, checks its declared scope, resolves the caller's current permissions, and enforces row policies on every operation. The helper exposes no decoded caller identity or capabilities for use as a separate authorization decision.
seq-studio functions dev does not mint delegated user identity. A missing
context there is expected; validate pure handler logic with supplied test
executors and verify real caller permissions through a deployed invocation.
Do not copy real invocation headers into fixtures or logs.
Errors and recovery
FunctionOrmError.code is stable and safe to return without including the
exception's stack or request headers:
| Code | Recovery |
| --- | --- |
| INVALID_CONFIGURATION | Correct the trusted origin or fetch configuration. |
| MISSING_CALLER_CONTEXT | Declare the data binding and invoke through the platform as a signed-in user. |
| INVALID_CALLER_CONTEXT | Verify that the request came through the supported platform runtime. |
| UNSUPPORTED_CALLER | Use a direct human invocation; this helper has no service mode. |
| EXPIRED_CALLER_CONTEXT | Begin a new invocation and construct a new executor. |
Invalid manifests, unknown operations and ORM failures propagate OrmClientError
or OrmOperationError, also exported from /orm. An OrmOperationError exposes
status and code; handle those instead of returning its upstream message.
A denied write is still denied; the helper supplies no permission
fallback. A network error may follow a successful write: use application-level
idempotency and inspect persisted results before retrying.
Opt-in cache
Opt-in caching inside a Managed Function. When the manifest declares a cache block, the platform passes a process-reused context as the HTTP handler's third argument. Existing two-argument handlers keep working.
cache:
enabled: true
tier: custom-pico
replicas: 0
default_ttl_seconds: 30Each function/environment gets a dedicated private Valkey instance. The baseline is one pico node with no replicas or persistence. Instances incur charges while idle. Only Sequence organization members can change an enabled cache to a nonbaseline tier or replica count; other authors can retain an already-approved configuration or return to baseline. Supported tiers: custom-pico, custom-micro, custom-mini; replicas: 0–5.
import type { ManagedFunctionContext } from '@sequenceholdings/managed-functions'
import type { Request, Response } from '@google-cloud/functions-framework'
export async function handler(req: Request, res: Response, context: ManagedFunctionContext) {
// Authenticate and authorize EVERY invocation before accessing shared cached data.
// Include tenant, permission scope, arguments, and authoritative data version
// in keys whenever they affect the result.
const result = await context.cache.getOrCompute('public-catalog:v1', { ttlSeconds: 30 }, async () => {
const response = await fetch('https://api.example.com/catalog')
if (!response.ok) throw new Error('Catalog unavailable')
return response.json()
})
res.json(result)
}Contract
All methods are asynchronous. Values are lossless JSON, at most 1 MiB; keys are nonempty strings, at most 512 UTF-8 bytes. TTL is an integer from 1 to 86,400 seconds. CacheOptions is { ttlSeconds?: number }, defaulting to the manifest TTL. Generic reads assert a type; they do not validate your schema.
| Method | Result |
| --- | --- |
| get<T>(key): Promise<T \| undefined> | Missing/expired → undefined; cached null is a hit. Does not extend TTL. |
| set(key, value, options?): Promise<void> | Atomic upsert with a new TTL. |
| setIfAbsent(key, value, options?): Promise<boolean> | Atomic insert+TTL; false leaves the existing value and TTL unchanged. |
| delete(key): Promise<boolean> | true if removed, otherwise false. |
| getOrCompute<T>(key, options, loader): Promise<T> | Hit or computed value; stores successful JSON results. |
Explicit CRUD throws CacheError with DISABLED, UNAVAILABLE, UNKNOWN_OUTCOME, INVALID_ARGUMENT, or INVALID_VALUE. A lost mutation acknowledgement has an unknown outcome and is never replayed automatically. getOrCompute bypasses disabled/unavailable caching and failed cache writes; loader errors and invalid inputs/results propagate. A cache failure never retries the loader. Coalescing is bounded and process-local: duplicate computation is possible. This is not an idempotency or exactly-once primitive.
Authors own authorization, read/write choices, and invalidation. A concurrent loader can repopulate a deleted key with old data; use authoritative versioned keys or bypass the cache when freshness is required. Every activation gets a fresh key namespace; old entries expire naturally.
Local development
Run seq-studio functions dev --dir . --port 8080 after installing dependencies. Enabled caching without a Valkey connection uses bounded process-local TTL/LRU storage (16 MiB encoded payloads, 10,000 entries); it is reset on restart and not shared across processes. The harness compiles TypeScript with the project's installed compiler. A deployed enabled cache without connection configuration fails startup; it never falls back silently.
For an explicit private Valkey development connection, set SEQUENCE_FUNCTION_CACHE_CONFIG to JSON containing cache plus connection: { host, port, serverIdentity, caCertificates, keyPrefix }. Use ADC with permission on that test instance and network connectivity to its PSC endpoint. serverIdentity must match the server certificate. Do not put passwords or tokens in configuration. The deployed runtime always obtains short-lived tokens from its attached service account.
Runtime bounds
One multiplexed TLS connection per process, at most 128 pending operations, 150 ms command and 1.5 s connection deadlines. Three failures open a five-second circuit breaker. New connections acquire a valid IAM token; live authenticated connections can outlast token expiry. TLS verifies the trusted certificate chain and expected DNS/IP identity. No offline queue, mutation replay, plaintext, or verification bypass.
Cloud Run emits aggregate managed_function.cache logs every 30 seconds of active use with operation/error counts and total/maximum operation latency. Keys, values, and credentials are never logged. Cache memory, evictions and hit rate appear beside function metrics; actual compute/cache spend, credits and billing freshness remain in Billing.
Functions without a cache manifest block retain their existing two-argument handler. An explicit cache: { enabled: false } supplies a disabled context.cache, allowing getOrCompute to run its loader without caching.
Runtime tracing
Newly deployed function entrypoints install request-scoped tracing automatically,
including two-argument handlers without caching. Ordinary global fetch calls to
the current platform propagate the invocation context; ORM and storage delegation
also retain the original initiator when a function explicitly acts as its service
account. Normal resource authorization still applies.
Global fetch calls to declared external hosts record hostname, method, status
and duration. Paths, queries, bodies, credentials and arbitrary errors are not
stored. Other networking transports and redirect hops are outside this capture.
External requests receive no platform attribution headers from this runtime.
Completed external and nonprimitive Atlas API spans are exported in small batches while the handler keeps working (50 ms batching delay, 500 ms per batch including one retry). The final batch starts fire-and-forget when the HTTP response ends (or the handler returns or throws); the response and errors never wait for export. Calls limited to instrumented primitive execution routes send no export. The runtime aborts unfinished exports at their deadline or on client disconnect. Callback-style handlers remain traced until their response ends.
Delivery remains best effort: a slow or unavailable collector, or CPU throttled
after the response, can leave spans unacknowledged. Each traced invocation that
captured egress logs one traces.function_egress.summary JSON line once its
export settles. Every captured span is acknowledged, rejected, abandoned,
or unacknowledged; dropped spans were never captured. Only known loss
(rejected, abandoned, or dropped) logs at warn; unacknowledged spans log
at info because the collector may have stored them after the response. A
refusal on the retry after an uncertain first attempt stays unacknowledged. The runtime
limits each invocation to 100 spans and 10 batches (20 POST attempts including
retries), preserving span IDs across a retry. Failures preserve the handler's
result. The platform allows its own collector hostname through managed egress,
without making collector availability a readiness dependency. Existing deployed
revisions need redeployment to pick up the entrypoint. Standalone functions dev
does not mint trusted invocation context.
Same-deployment Atlas API calls outside instrumented primitive execution routes are reported as Atlas API dependencies with registered route templates. Query strings, dynamic path values and request/response bodies are not persisted. Successful reads of a Lattice process or run link to the owning process; direct HTTP run starts already use server-recorded spans and preserve the request chain. Transport classification is shared with the platform host; function authors do not need to import or configure instrumentation.
