Pyric
Navigate

API reference

@pyric/cli/remote

18 published symbols from @pyric/cli

Generated from the TypeScript declarations shipped at this import path.

Interfaces

ConnectRemoteSandboxOptions

Properties

PropertyTypeDescription
cwd?stringProject root for .pyric/serve.json discovery. Default: process.cwd().
opTimeoutMs?numberPer-op timeout in ms on the Node side (default 35s, above the bridge’s 30s).
url?stringExplicit serve base URL (e.g. http://127.0.0.1:5000) — skips discovery.

LazyRemoteSandbox

remoteSandbox’s return type: the branded handle plus ready for eager checkers. ready kicks off the connection when first accessed and settles with the same fail-fast errors connectRemoteSandbox throws (no serve discovered / no browser tab connected).

Extends

Properties

PropertyModifierTypeDescription
[REMOTE_SANDBOX]readonlytrue-
adminreadonlySandboxAdminAdmin-plane access (rule-bypass reads). Identity-agnostic by design — admin reads aren’t gated on auth, so they live on the sandbox, not on a context. See SandboxAdmin.
authreadonlyRemoteAuthAdminAdmin auth user CRUD.
channelreadonlyRemoteSandboxChannelThe raw worker op/sub relay channel (narrowed to the wire payload types).
currentUserpublic{ token?: Record<string, unknown>; uid: string; }Current authenticated user across the sandbox. Mutated by pyric/auth’s signInAnonymously / signInWithEmailAndPassword / signOut / sandbox.setUser. Read per-call by service factories (e.g. a future getFirestore(sandbox) overload) so they see auth state changes without re-binding handles. Defaults to null (anonymous / signed out). Independent of withAuth({uid})withAuth still produces a frozen SandboxContext that carries its own identity for the runner’s test code (the existing pattern: explicit identity per service call). currentUser exists for the pyric/auth mirror, where consumer app code drives identity through a stateful Auth handle rather than naming it per call.
currentUser.token?publicRecord<string, unknown>-
currentUser.uidpublicstring-
readyreadonlyPromise<void>-
rtdbreadonlyRemoteRtdbRTDB conveniences (admin lens pinned).
serveUrlreadonlystringBase URL of the pyric dev this handle is attached to (used in error guidance: “open in a browser and retry”).
storagereadonlyRemoteStorageStorage conveniences (admin lens pinned; 8 MiB per-op byte cap).

Methods

clearPersistence()
clearPersistence(): Promise<void>;

Wipe the persisted blob for this sandbox’s key. In-memory state is left intact — call reset() if you want both. Useful for “sign out and forget” flows.

No-op when persistence is not enabled.

Returns

Promise<void>

Inherited from

RemoteSandbox.clearPersistence

close()
close(): void;

Close the connection. In-flight ops reject; subscriptions stop.

Returns

void

Inherited from

RemoteSandbox.close

dispose()
dispose(): void;

Tear down listener registries on this sandbox’s environment without replacing it. Use this when you’re about to discard the sandbox itself (e.g. runner.reseed() builds a fresh sandbox rather than calling reset()) and want to drop callback references on the outgoing instance defensively. Idempotent. Does not touch data.

Returns

void

Inherited from

RemoteSandbox.dispose

enablePersistence()
enablePersistence(options: SandboxPersistenceOptions): Promise<void>;

Persist the sandbox’s data to a backend and restore it on next enablePersistence call. The default 'indexedDB' backend turns the sandbox into the host page’s local Firestore — writes flush automatically and a fresh initializeSandbox() rehydrates from the prior session.

Restoration happens before the promise resolves; awaiting this call is sufficient to guarantee in-memory state matches the persisted blob.

Idempotent across the same key — calling twice in one process is a no-op on the second call. Different keys are rejected as an error (a sandbox can persist to at most one backend at a time).

Listener semantics: every write event the sandbox emits triggers a debounced flush (default 250ms). Browser hosts additionally flush on beforeunload so a page navigation doesn’t lose the tail of the debounce window.

See SandboxPersistenceOptions for backend selection and tuning.

Parameters
ParameterType
optionsSandboxPersistenceOptions
Returns

Promise<void>

Inherited from

RemoteSandbox.enablePersistence

enableTabSync()
enableTabSync(options?: TabSyncOptions): () => void;

Enable cross-tab realtime sync via BroadcastChannel. A write in this tab will propagate to every OTHER tab of the same origin that also called enableTabSync, causing their onSnapshot listeners to re-evaluate — restoring production’s cross-client realtime behavior.

Opt-in, OFF by default. Firestore only (RTDB is a follow-on).

Returns a disable function. Calling it removes the onEvent subscription, the channel message listener, and closes the channel (when it was created internally). After disable, no further propagation occurs in either direction.

Multi-writer note: concurrent writes from two tabs to the same doc produce last-write-wins divergence — there is no conflict resolution. The intended model is one active writer (one user, one tab) with observers in other tabs; this covers the overwhelming majority of local development scenarios.

Parameters
ParameterType
options?TabSyncOptions
Returns
(): void;
Returns

void

See

TabSyncOptions for channel injection (tests) and originId.

Example
// In every tab that should participate in realtime:
const sandbox = initializeSandbox();
const disableSync = sandbox.enableTabSync();
// Later, to stop syncing:
disableSync();
Inherited from

RemoteSandbox.enableTabSync

flush()
flush(): Promise<void>;

Force a snapshot to the configured persistence backend right now. Useful before a manual navigation, or in tests that need deterministic ordering against the debounce window. Resolves once the write hits the backend.

Throws if persistence is not enabled.

Returns

Promise<void>

Inherited from

RemoteSandbox.flush

history()
history(): SandboxEvent[];

Every SandboxEvent this sandbox has emitted since init or the last reset(). Returns a defensive copy.

Use this for replay: hand the array to replay(events, rules) from pyric/sandbox and the engine re-issues every captured write against a fresh sandbox.

Unlike onEvent (live stream from the moment of subscribe), history() returns every event the sandbox has seen — useful for consumers that attach late (e.g., loading a saved session before subscribing) or that need a snapshot at a particular moment.

reset() and dispose() each append a closing session_boundary event; reset() then clears the history. Consumers that took a snapshot before reset retain the boundary in their copy.

Returns

SandboxEvent[]

Inherited from

RemoteSandbox.history

loadSnapshot()
loadSnapshot(data: SandboxSnapshot): void;

CLOBBER-restore the sandbox’s entire state from a prior snapshot: reset() (clears firestore + the signed-in session), then rebuild firestore from data and restore each registered service. This is a TOTAL replace — documents absent from data do NOT survive — and is the counterpart to snapshot. It is what makes “transfer (clobber) one instance’s data into another” and named-branch switching possible.

Fires a session_boundary (reset phase), re-evaluates live listeners against the loaded state, and the next persistence flush writes the loaded state. Services present in data but not currently registered are skipped (a snapshot taken via snapshot always includes every registered service, so this only affects cross-instance imports from a sandbox that had a service this one lacks).

Parameters
ParameterType
dataSandboxSnapshot
Returns

void

Inherited from

RemoteSandbox.loadSnapshot

onCurrentUserChanged()
onCurrentUserChanged(cb: (user: {
  token?: Record<string, unknown>;
  uid: string;
}) => void): () => void;

Subscribe to currentUser changes. Fires on every mutation — sign-in, sign-out, user swap. Does NOT fire on subscribe.

Survives reset() and dispose() only as a no-op: a disposed sandbox emits nothing further; a reset sandbox clears currentUser to null (and fires the change) before swapping the env.

Returns an unsubscribe function. Listener errors are swallowed — subscribers are observational, the sandbox does not propagate their errors.

Parameters
ParameterType
cb(user: { token?: Record<string, unknown>; uid: string; }) => void
Returns
(): void;
Returns

void

Inherited from

RemoteSandbox.onCurrentUserChanged

onEvent()
onEvent(cb: (event: SandboxEvent) => void): () => void;

Subscribe to every event the sandbox emits — see SandboxEvent for the discriminated-union shape. One subscription covers request/denial/snapshot-error/listener-lifecycle/session-boundary; filter on event.kind to recover individual streams.

Replaces the prior three-channel surface (onRequest / onDenial / onSnapshotError) — see issue #307. Filter cookbook:

  • All denials: event.kind === 'request' && event.result === 'deny'
  • Stream errors: event.kind === 'listener_errored'
  • Per-op traffic: event.kind === 'request'

Survives sandbox.reset() — the subscription is held on the sandbox, not on the underlying environment. A session_boundary event with phase: 'reset' fires before the env swap so consumers can segment their stream.

Returns an unsubscribe function. Listener errors are swallowed so a faulty subscriber can’t change rule semantics or hide other events. Both synchronous throws and rejected Promises from async callbacks are silently discarded — subscribers are observational, the sandbox doesn’t await them and doesn’t propagate their errors.

Parameters
ParameterType
cb(event: SandboxEvent) => void
Returns
(): void;
Returns

void

Inherited from

RemoteSandbox.onEvent

registerPersistableService()
registerPersistableService(name: string, hooks: PersistableService): () => void;

Register a service (auth, storage, …) as a persistence participant. The sandbox calls hooks.snapshot() on every flush and hooks.restore(data) on restore. If hooks.subscribe is provided, the persistence controller subscribes and schedules a debounced flush on each change — so auth-user edits flush promptly, not only on the next Firestore write.

Returns an unregister function — call it if the service is torn down before the sandbox is disposed (uncommon in practice; the sandbox’s dispose() clears the registry anyway).

Throws failed-precondition when a service with the same name is already registered — the auth package registers 'auth' once when getAuth(sandbox) first creates a backend, so accidental double- registration is a caller bug, not a no-op.

Advanced / internal API. Service packages (auth, storage) call this when they first attach to a sandbox. Consumer app code should not need to call this directly.

Parameters
ParameterType
namestring
hooksPersistableService
Returns
(): void;
Returns

void

Inherited from

RemoteSandbox.registerPersistableService

reset()
reset(): void;

Reset the underlying environment to a fresh state — wipes data, rules, and any service-specific configuration.

Snapshot listeners attached to the OLD environment are dropped at the swap — they can’t survive because their target docs have been wiped. onEvent subscribers DO survive — the registry lives on the sandbox, and a session_boundary event with phase: 'reset' fires before the swap so subscribers know the rollover happened. Existing SandboxContexts continue to work — their sandbox reference is stable; subsequent operations resolve to the new env.

Returns

void

Inherited from

RemoteSandbox.reset

resetAll()
resetAll(): Promise<{
  errors: string[];
}>;

Reset the WHOLE sandbox: reset (Firestore env + signed-in session), then clear every registered persistable service that provides a PersistableService.reset hook — auth users, the RTDB tree, storage objects. This is the one sandbox-owned “wipe everything” path: because it iterates the service registry, a new service that registers with a reset hook is cleared automatically, and a consumer (Pyric Studio’s reset) cannot forget one.

Service resets may be async (storage clears IndexedDB stores); the returned promise resolves when every service has finished clearing. A service whose reset throws is isolated (others still clear) and REPORTED in the returned errors (as name: message) — a reset that leaves data behind must never look successful to the caller.

Returns

Promise<{ errors: string[]; }>

Inherited from

RemoteSandbox.resetAll

runWithProvenance()?
optional runWithProvenance<T>(provenance: EventProvenance, fn: () => T): T;

Run fn with ambient EventProvenance defaults: every event emitted SYNCHRONOUSLY during fn that doesn’t already carry a provenance field (on the event itself or via an explicit per-emit override) is stamped with these values instead of the global defaults. This is the mechanical “who issued this op” seam the serve worker uses to tag Studio-issued ops (actor: { kind: 'studio' }) and to stamp the auth lens an op ran under (authLens) — declared by the caller that issues the op, never inferred from the op’s shape.

SYNCHRONOUS WINDOW: the ambient values apply only until fn returns (for an async fn, its synchronous prefix — which covers the local environment’s rules eval + event emission, since those run before the op’s promise is handed back). Work an op DEFERS (snapshot-listener deliveries and re-evals drain on a microtask, off-stack) is intentionally OUTSIDE the window: a listener re-eval belongs to the listener’s owner, not to whoever’s write triggered it. Nested calls stack — the innermost window wins per field, and each window restores the previous one on exit (including on throw).

OPTIONAL because remote sandbox proxies can’t provide an ambient emit window (events are emitted in the worker they front). Callers spell sandbox.runWithProvenance?.(prov, fn) ?? fn().

Type Parameters
Type Parameter
T
Parameters
ParameterType
provenanceEventProvenance
fn() => T
Returns

T

Inherited from

RemoteSandbox.runWithProvenance

snapshot()
snapshot(): SandboxSnapshot;

Capture a snapshot of every service’s state. For v1 with only Firestore, the return value carries a firestore key mapping doc paths to data. Future services will add their own keys.

Returns

SandboxSnapshot

Inherited from

RemoteSandbox.snapshot

withAuth()
withAuth(auth: {
  token?: Record<string, unknown>;
  uid: string;
}): SandboxContext;

Derive a context bound to this sandbox under the given auth identity. Operations through services attached to the returned context evaluate rules under that identity. Many contexts can coexist for one sandbox; data is shared.

null is anonymous; an AuthState object names the user (and optional custom claims). Passing undefined is a deliberate error — say withAuth(null) for anonymous so the call site is unambiguous.

Parameters
ParameterType
auth{ token?: Record<string, unknown>; uid: string; }
auth.token?Record<string, unknown>
auth.uidstring
Returns

SandboxContext

Example
const sandbox = initializeSandbox();
const dbAlice = getFirestore(sandbox.withAuth({ uid: 'alice' }));
const dbAnon  = getFirestore(sandbox.withAuth(null));
Inherited from

RemoteSandbox.withAuth


RemoteAuthAdmin

Admin auth user-CRUD passthrough (never lensed — auth ops operate the worker’s user pool directly, mirroring pyric/auth’s sandbox ops).

Methods

clearUsers()
clearUsers(): Promise<void>;
Returns

Promise<void>

createUser()
createUser(request: CreateUserRequest): Promise<AuthUserRecord>;
Parameters
ParameterType
requestCreateUserRequest
Returns

Promise<AuthUserRecord>

deleteUser()
deleteUser(uid: string): Promise<void>;
Parameters
ParameterType
uidstring
Returns

Promise<void>

listUsers()
listUsers(): Promise<AuthUserRecord[]>;
Returns

Promise<AuthUserRecord[]>

updateUser()
updateUser(uid: string, request: UpdateUserRequest): Promise<AuthUserRecord>;
Parameters
ParameterType
uidstring
requestUpdateUserRequest
Returns

Promise<AuthUserRecord>


RemoteRtdb

Thin RTDB conveniences over the channel. Every call pins actAs: { mode: 'admin' } — firebase-admin’s rules-bypass semantics, matching what pyric-admin’s database backend needs. Use the raw channel for lensed (rules-evaluated) access.

Methods

get()
get(path: string): Promise<unknown>;

Read the value at path (null when absent).

Parameters
ParameterType
pathstring
Returns

Promise<unknown>

onValue()
onValue(
   path: string,
   callback: (snapshot: RemoteRtdbSnapshot) => void,
   onError?: (err: Error & {
  code: string;
}) => void): () => void;

Subscribe to the value at path (initial snapshot + every change).

Parameters
ParameterType
pathstring
callback(snapshot: RemoteRtdbSnapshot) => void
onError?(err: Error & { code: string; }) => void
Returns
(): void;
Returns

void

push()
push(path: string, value?: unknown): Promise<{
  key: string;
  path: string;
}>;

Push value under a CLIENT-minted 20-char push id (the worker-protocol contract: rtdb.push carries the key, so .key is known synchronously on the pyric-admin side). Resolves with the minted key + full path.

Parameters
ParameterType
pathstring
value?unknown
Returns

Promise<{ key: string; path: string; }>

remove()
remove(path: string): Promise<void>;
Parameters
ParameterType
pathstring
Returns

Promise<void>

set()
set(path: string, value: unknown): Promise<void>;
Parameters
ParameterType
pathstring
valueunknown
Returns

Promise<void>

update()
update(path: string, values: Record<string, unknown>): Promise<void>;
Parameters
ParameterType
pathstring
valuesRecord<string, unknown>
Returns

Promise<void>


RemoteRtdbSnapshot

Wire shape of an RTDB snapshot as the worker host serializes it.

Properties

PropertyType
existsboolean
keystring
sizenumber
valueunknown

RemoteSandbox

The Node-side remote sandbox handle. Extends pyric/sandbox’s branded RemoteSandboxBase — structurally a full Sandbox, so it can be passed to pyric-admin/app’s initializeApp({ sandbox }), whose RTDB and Auth backends dispatch on the brand and route through channel.

Sandbox members that are genuinely sync-only (admin, snapshot(), history(), onEvent, currentUser, …) cannot be mirrored over the wire in slice 1 and throw a remediating unimplemented error naming what to do instead. Implemented members: withAuth (pure local pair construction) and dispose (aliases close).

Extends

Extended by

Properties

PropertyModifierTypeDescriptionOverrides
[REMOTE_SANDBOX]readonlytrue--
adminreadonlySandboxAdminAdmin-plane access (rule-bypass reads). Identity-agnostic by design — admin reads aren’t gated on auth, so they live on the sandbox, not on a context. See SandboxAdmin.-
authreadonlyRemoteAuthAdminAdmin auth user CRUD.-
channelreadonlyRemoteSandboxChannelThe raw worker op/sub relay channel (narrowed to the wire payload types).RemoteSandbox.channel
currentUserpublic{ token?: Record<string, unknown>; uid: string; }Current authenticated user across the sandbox. Mutated by pyric/auth’s signInAnonymously / signInWithEmailAndPassword / signOut / sandbox.setUser. Read per-call by service factories (e.g. a future getFirestore(sandbox) overload) so they see auth state changes without re-binding handles. Defaults to null (anonymous / signed out). Independent of withAuth({uid})withAuth still produces a frozen SandboxContext that carries its own identity for the runner’s test code (the existing pattern: explicit identity per service call). currentUser exists for the pyric/auth mirror, where consumer app code drives identity through a stateful Auth handle rather than naming it per call.-
currentUser.token?publicRecord<string, unknown>--
currentUser.uidpublicstring--
rtdbreadonlyRemoteRtdbRTDB conveniences (admin lens pinned).-
serveUrlreadonlystringBase URL of the pyric dev this handle is attached to (used in error guidance: “open in a browser and retry”).-
storagereadonlyRemoteStorageStorage conveniences (admin lens pinned; 8 MiB per-op byte cap).-

Methods

clearPersistence()
clearPersistence(): Promise<void>;

Wipe the persisted blob for this sandbox’s key. In-memory state is left intact — call reset() if you want both. Useful for “sign out and forget” flows.

No-op when persistence is not enabled.

Returns

Promise<void>

Inherited from

RemoteSandbox.clearPersistence

close()
close(): void;

Close the connection. In-flight ops reject; subscriptions stop.

Returns

void

dispose()
dispose(): void;

Tear down listener registries on this sandbox’s environment without replacing it. Use this when you’re about to discard the sandbox itself (e.g. runner.reseed() builds a fresh sandbox rather than calling reset()) and want to drop callback references on the outgoing instance defensively. Idempotent. Does not touch data.

Returns

void

Inherited from

RemoteSandbox.dispose

enablePersistence()
enablePersistence(options: SandboxPersistenceOptions): Promise<void>;

Persist the sandbox’s data to a backend and restore it on next enablePersistence call. The default 'indexedDB' backend turns the sandbox into the host page’s local Firestore — writes flush automatically and a fresh initializeSandbox() rehydrates from the prior session.

Restoration happens before the promise resolves; awaiting this call is sufficient to guarantee in-memory state matches the persisted blob.

Idempotent across the same key — calling twice in one process is a no-op on the second call. Different keys are rejected as an error (a sandbox can persist to at most one backend at a time).

Listener semantics: every write event the sandbox emits triggers a debounced flush (default 250ms). Browser hosts additionally flush on beforeunload so a page navigation doesn’t lose the tail of the debounce window.

See SandboxPersistenceOptions for backend selection and tuning.

Parameters
ParameterType
optionsSandboxPersistenceOptions
Returns

Promise<void>

Inherited from

RemoteSandbox.enablePersistence

enableTabSync()
enableTabSync(options?: TabSyncOptions): () => void;

Enable cross-tab realtime sync via BroadcastChannel. A write in this tab will propagate to every OTHER tab of the same origin that also called enableTabSync, causing their onSnapshot listeners to re-evaluate — restoring production’s cross-client realtime behavior.

Opt-in, OFF by default. Firestore only (RTDB is a follow-on).

Returns a disable function. Calling it removes the onEvent subscription, the channel message listener, and closes the channel (when it was created internally). After disable, no further propagation occurs in either direction.

Multi-writer note: concurrent writes from two tabs to the same doc produce last-write-wins divergence — there is no conflict resolution. The intended model is one active writer (one user, one tab) with observers in other tabs; this covers the overwhelming majority of local development scenarios.

Parameters
ParameterType
options?TabSyncOptions
Returns
(): void;
Returns

void

See

TabSyncOptions for channel injection (tests) and originId.

Example
// In every tab that should participate in realtime:
const sandbox = initializeSandbox();
const disableSync = sandbox.enableTabSync();
// Later, to stop syncing:
disableSync();
Inherited from

RemoteSandbox.enableTabSync

flush()
flush(): Promise<void>;

Force a snapshot to the configured persistence backend right now. Useful before a manual navigation, or in tests that need deterministic ordering against the debounce window. Resolves once the write hits the backend.

Throws if persistence is not enabled.

Returns

Promise<void>

Inherited from

RemoteSandbox.flush

history()
history(): SandboxEvent[];

Every SandboxEvent this sandbox has emitted since init or the last reset(). Returns a defensive copy.

Use this for replay: hand the array to replay(events, rules) from pyric/sandbox and the engine re-issues every captured write against a fresh sandbox.

Unlike onEvent (live stream from the moment of subscribe), history() returns every event the sandbox has seen — useful for consumers that attach late (e.g., loading a saved session before subscribing) or that need a snapshot at a particular moment.

reset() and dispose() each append a closing session_boundary event; reset() then clears the history. Consumers that took a snapshot before reset retain the boundary in their copy.

Returns

SandboxEvent[]

Inherited from

RemoteSandbox.history

loadSnapshot()
loadSnapshot(data: SandboxSnapshot): void;

CLOBBER-restore the sandbox’s entire state from a prior snapshot: reset() (clears firestore + the signed-in session), then rebuild firestore from data and restore each registered service. This is a TOTAL replace — documents absent from data do NOT survive — and is the counterpart to snapshot. It is what makes “transfer (clobber) one instance’s data into another” and named-branch switching possible.

Fires a session_boundary (reset phase), re-evaluates live listeners against the loaded state, and the next persistence flush writes the loaded state. Services present in data but not currently registered are skipped (a snapshot taken via snapshot always includes every registered service, so this only affects cross-instance imports from a sandbox that had a service this one lacks).

Parameters
ParameterType
dataSandboxSnapshot
Returns

void

Inherited from

RemoteSandbox.loadSnapshot

onCurrentUserChanged()
onCurrentUserChanged(cb: (user: {
  token?: Record<string, unknown>;
  uid: string;
}) => void): () => void;

Subscribe to currentUser changes. Fires on every mutation — sign-in, sign-out, user swap. Does NOT fire on subscribe.

Survives reset() and dispose() only as a no-op: a disposed sandbox emits nothing further; a reset sandbox clears currentUser to null (and fires the change) before swapping the env.

Returns an unsubscribe function. Listener errors are swallowed — subscribers are observational, the sandbox does not propagate their errors.

Parameters
ParameterType
cb(user: { token?: Record<string, unknown>; uid: string; }) => void
Returns
(): void;
Returns

void

Inherited from

RemoteSandbox.onCurrentUserChanged

onEvent()
onEvent(cb: (event: SandboxEvent) => void): () => void;

Subscribe to every event the sandbox emits — see SandboxEvent for the discriminated-union shape. One subscription covers request/denial/snapshot-error/listener-lifecycle/session-boundary; filter on event.kind to recover individual streams.

Replaces the prior three-channel surface (onRequest / onDenial / onSnapshotError) — see issue #307. Filter cookbook:

  • All denials: event.kind === 'request' && event.result === 'deny'
  • Stream errors: event.kind === 'listener_errored'
  • Per-op traffic: event.kind === 'request'

Survives sandbox.reset() — the subscription is held on the sandbox, not on the underlying environment. A session_boundary event with phase: 'reset' fires before the env swap so consumers can segment their stream.

Returns an unsubscribe function. Listener errors are swallowed so a faulty subscriber can’t change rule semantics or hide other events. Both synchronous throws and rejected Promises from async callbacks are silently discarded — subscribers are observational, the sandbox doesn’t await them and doesn’t propagate their errors.

Parameters
ParameterType
cb(event: SandboxEvent) => void
Returns
(): void;
Returns

void

Inherited from

RemoteSandbox.onEvent

registerPersistableService()
registerPersistableService(name: string, hooks: PersistableService): () => void;

Register a service (auth, storage, …) as a persistence participant. The sandbox calls hooks.snapshot() on every flush and hooks.restore(data) on restore. If hooks.subscribe is provided, the persistence controller subscribes and schedules a debounced flush on each change — so auth-user edits flush promptly, not only on the next Firestore write.

Returns an unregister function — call it if the service is torn down before the sandbox is disposed (uncommon in practice; the sandbox’s dispose() clears the registry anyway).

Throws failed-precondition when a service with the same name is already registered — the auth package registers 'auth' once when getAuth(sandbox) first creates a backend, so accidental double- registration is a caller bug, not a no-op.

Advanced / internal API. Service packages (auth, storage) call this when they first attach to a sandbox. Consumer app code should not need to call this directly.

Parameters
ParameterType
namestring
hooksPersistableService
Returns
(): void;
Returns

void

Inherited from

RemoteSandbox.registerPersistableService

reset()
reset(): void;

Reset the underlying environment to a fresh state — wipes data, rules, and any service-specific configuration.

Snapshot listeners attached to the OLD environment are dropped at the swap — they can’t survive because their target docs have been wiped. onEvent subscribers DO survive — the registry lives on the sandbox, and a session_boundary event with phase: 'reset' fires before the swap so subscribers know the rollover happened. Existing SandboxContexts continue to work — their sandbox reference is stable; subsequent operations resolve to the new env.

Returns

void

Inherited from

RemoteSandbox.reset

resetAll()
resetAll(): Promise<{
  errors: string[];
}>;

Reset the WHOLE sandbox: reset (Firestore env + signed-in session), then clear every registered persistable service that provides a PersistableService.reset hook — auth users, the RTDB tree, storage objects. This is the one sandbox-owned “wipe everything” path: because it iterates the service registry, a new service that registers with a reset hook is cleared automatically, and a consumer (Pyric Studio’s reset) cannot forget one.

Service resets may be async (storage clears IndexedDB stores); the returned promise resolves when every service has finished clearing. A service whose reset throws is isolated (others still clear) and REPORTED in the returned errors (as name: message) — a reset that leaves data behind must never look successful to the caller.

Returns

Promise<{ errors: string[]; }>

Inherited from

RemoteSandbox.resetAll

runWithProvenance()?
optional runWithProvenance<T>(provenance: EventProvenance, fn: () => T): T;

Run fn with ambient EventProvenance defaults: every event emitted SYNCHRONOUSLY during fn that doesn’t already carry a provenance field (on the event itself or via an explicit per-emit override) is stamped with these values instead of the global defaults. This is the mechanical “who issued this op” seam the serve worker uses to tag Studio-issued ops (actor: { kind: 'studio' }) and to stamp the auth lens an op ran under (authLens) — declared by the caller that issues the op, never inferred from the op’s shape.

SYNCHRONOUS WINDOW: the ambient values apply only until fn returns (for an async fn, its synchronous prefix — which covers the local environment’s rules eval + event emission, since those run before the op’s promise is handed back). Work an op DEFERS (snapshot-listener deliveries and re-evals drain on a microtask, off-stack) is intentionally OUTSIDE the window: a listener re-eval belongs to the listener’s owner, not to whoever’s write triggered it. Nested calls stack — the innermost window wins per field, and each window restores the previous one on exit (including on throw).

OPTIONAL because remote sandbox proxies can’t provide an ambient emit window (events are emitted in the worker they front). Callers spell sandbox.runWithProvenance?.(prov, fn) ?? fn().

Type Parameters
Type Parameter
T
Parameters
ParameterType
provenanceEventProvenance
fn() => T
Returns

T

Inherited from

RemoteSandbox.runWithProvenance

snapshot()
snapshot(): SandboxSnapshot;

Capture a snapshot of every service’s state. For v1 with only Firestore, the return value carries a firestore key mapping doc paths to data. Future services will add their own keys.

Returns

SandboxSnapshot

Inherited from

RemoteSandbox.snapshot

withAuth()
withAuth(auth: {
  token?: Record<string, unknown>;
  uid: string;
}): SandboxContext;

Derive a context bound to this sandbox under the given auth identity. Operations through services attached to the returned context evaluate rules under that identity. Many contexts can coexist for one sandbox; data is shared.

null is anonymous; an AuthState object names the user (and optional custom claims). Passing undefined is a deliberate error — say withAuth(null) for anonymous so the call site is unambiguous.

Parameters
ParameterType
auth{ token?: Record<string, unknown>; uid: string; }
auth.token?Record<string, unknown>
auth.uidstring
Returns

SandboxContext

Example
const sandbox = initializeSandbox();
const dbAlice = getFirestore(sandbox.withAuth({ uid: 'alice' }));
const dbAnon  = getFirestore(sandbox.withAuth(null));
Inherited from

RemoteSandbox.withAuth


RemoteSandboxChannel

The raw relay channel: any worker-protocol op or snap-delivering subscription, verbatim. The typed conveniences below are built on this; checkpoint 2’s pyric-admin remote-dispatch arm consumes it directly.

Methods

op()
op(op: WorkerOpPayload): Promise<unknown>;

Dispatch one worker op. Resolves with the worker’s res.value; rejects with an Error carrying .code. NOTE: callers choose their own actAs lens — nothing is pinned here.

Parameters
ParameterType
opWorkerOpPayload
Returns

Promise<unknown>

subscribe()
subscribe(
   sub: WorkerSubPayload,
   onSnap: (value: unknown) => void,
   onError?: (err: Error & {
  code: string;
}) => void): () => void;

Register a worker subscription. onSnap receives each snap value; an establishment failure (the worker host’s { __error } snap) routes to onError instead. Returns the unsubscribe function.

Parameters
ParameterType
subWorkerSubPayload
onSnap(value: unknown) => void
onError?(err: Error & { code: string; }) => void
Returns
(): void;
Returns

void


RemoteSandboxCore

Properties

PropertyTypeDescription
channelRemoteSandboxChannel-
readyPromise<void>Resolves on attach-ack; rejects when no browser tab is connected.

Methods

dispose()
dispose(reason?: string): void;

Fail everything in flight (transport closed). Idempotent.

Parameters
ParameterType
reason?string
Returns

void

handleMessage()
handleMessage(msg: BridgeMessage): void;

Feed one parsed message from the transport into the core.

Parameters
ParameterType
msgBridgeMessage
Returns

void

start()
start(): void;

Send the attach handshake. ready settles on the ack.

Returns

void


RemoteStorage

Thin Storage conveniences over the channel — the byte-carrying base64 ops plus browse/metadata. Every call pins actAs: { mode: 'admin' } (firebase-admin’s rules-bypass semantics, matching RemoteRtdb); use the raw channel for lensed (rules-evaluated) access. Bytes are capped at 8 MiB raw (MAX_STORAGE_OP_BYTES) on both ends — streaming transfers are not supported on the sandbox backend.

Methods

deleteObject()
deleteObject(path: string): Promise<void>;

Delete the object at path. Idempotent (missing = no-op).

Parameters
ParameterType
pathstring
Returns

Promise<void>

exists()
exists(path: string): Promise<boolean>;

Does an object exist at path? (getMetadata with not-found → false.)

Parameters
ParameterType
pathstring
Returns

Promise<boolean>

getBytes()
getBytes(path: string): Promise<Buffer<ArrayBufferLike>>;

Download the object’s bytes. Rejects storage/object-not-found when absent, payload-too-large when over the op cap.

Parameters
ParameterType
pathstring
Returns

Promise<Buffer<ArrayBufferLike>>

getMetadata()
getMetadata(path: string): Promise<FullMetadata>;

Read the object’s FullMetadata.

Parameters
ParameterType
pathstring
Returns

Promise<FullMetadata>

listAll()
listAll(path: string): Promise<{
  items: {
     fullPath: string;
     name: string;
  }[];
  prefixes: {
     fullPath: string;
     name: string;
  }[];
}>;

Enumerate immediate child items + prefixes under path.

Parameters
ParameterType
pathstring
Returns

Promise<{ items: { fullPath: string; name: string; }[]; prefixes: { fullPath: string; name: string; }[]; }>

putBytes()
putBytes(
   path: string,
   data: Uint8Array,
   options?: {
  contentType?: string;
  metadata?: Record<string, unknown>;
}): Promise<FullMetadata>;

Upload data at path (replaces any existing object). Resolves with the stored object’s FullMetadata.

Parameters
ParameterType
pathstring
dataUint8Array
options?{ contentType?: string; metadata?: Record<string, unknown>; }
options.contentType?string
options.metadata?Record<string, unknown>
Returns

Promise<FullMetadata>


RemoteTransport

Minimal transport the core writes to. connectRemoteSandbox adapts a ws socket; tests inject an in-process pipe to a ConsumerSession.

Methods

ref()?
optional ref(): void;

OPTIONAL event-loop hold hooks (exit-hang fix). The WS adapter unrefs its socket once connected so an IDLE remote client never pins the Node event loop (a finished script exits); the core calls ref() when work becomes outstanding (first pending op / live subscription) and unref() when the last one settles, so in-flight delivery keeps the process alive. Pure in-process transports (tests) may omit both.

Returns

void

send()
send(msg: BridgeMessage): void;
Parameters
ParameterType
msgBridgeMessage
Returns

void

unref()?
optional unref(): void;
Returns

void

Functions

buildRemoteAuthAdmin()

function buildRemoteAuthAdmin(channel: RemoteSandboxChannel): RemoteAuthAdmin;

Parameters

ParameterType
channelRemoteSandboxChannel

Returns

RemoteAuthAdmin


buildRemoteRtdb()

function buildRemoteRtdb(channel: RemoteSandboxChannel): RemoteRtdb;

Parameters

ParameterType
channelRemoteSandboxChannel

Returns

RemoteRtdb


buildRemoteStorage()

function buildRemoteStorage(channel: RemoteSandboxChannel): RemoteStorage;

Parameters

ParameterType
channelRemoteSandboxChannel

Returns

RemoteStorage


connectRemoteSandbox()

function connectRemoteSandbox(options?: ConnectRemoteSandboxOptions): Promise<RemoteSandbox>;

Discover the running pyric dev --bridge, attach to its bridge WS as a worker-relay CONSUMER (never a peer — attaching cannot kick the browser tab out of last-connection-wins), and return the typed remote handle.

Fails fast when no serve is discoverable or no browser tab is connected — there is deliberately no headless fallback (see module doc).

Parameters

ParameterType
options?ConnectRemoteSandboxOptions

Returns

Promise<RemoteSandbox>


createLazyRemoteSandbox()

function createLazyRemoteSandbox(connect: () => Promise<RemoteSandbox>, options?: {
  url?: string;
}): LazyRemoteSandbox;

The lazy wrapper with the connect function injected — the test seam (tests inject a fake connect; production injects connectRemoteSandbox).

Parameters

ParameterType
connect() => Promise<RemoteSandbox>
options?{ url?: string; }
options.url?string

Returns

LazyRemoteSandbox


createRemoteSandboxCore()

function createRemoteSandboxCore(transport: RemoteTransport, opts: {
  opTimeoutMs?: number;
  serveUrl: string;
}): RemoteSandboxCore;

Transport-agnostic client core: correlation ids/subIds are minted HERE (this leg’s id space; the bridge re-mints for the peer leg), pending ops carry a Node-side timeout above the bridge’s 30s, and { __error } snap values are routed to the subscription’s error handler.

Parameters

ParameterType
transportRemoteTransport
opts{ opTimeoutMs?: number; serveUrl: string; }
opts.opTimeoutMs?number
opts.serveUrlstring

Returns

RemoteSandboxCore


createRemoteSandboxHandle()

function createRemoteSandboxHandle(opts: {
  channel: RemoteSandboxChannel;
  close: () => void;
  serveUrl: string;
}): RemoteSandbox;

Build the branded remote sandbox handle over an established channel.

Split out of connectRemoteSandbox so the in-process test harness (fake ports + createConsumerSession, no WS) constructs the EXACT handle production hands to pyric-admin.

The handle satisfies Sandbox structurally:

  • withAuth / dispose are real (pure local construction / teardown).
  • Everything whose contract is sync-only or worker-owned (admin, snapshot, loadSnapshot, history, onEvent, reset, currentUser, onCurrentUserChanged, tab sync, persistence) throws a remediating unimplemented error. Notably onEvent: the unified event stream (target: 'events') is not relayable until slice 2’s bounded backpressure lands, and no remote dispatch arm may depend on it — a throw (not a silent no-op) keeps a subscriber from believing it is observing events that will never arrive.

Parameters

ParameterType
opts{ channel: RemoteSandboxChannel; close: () => void; serveUrl: string; }
opts.channelRemoteSandboxChannel
opts.close() => void
opts.serveUrlstring

Returns

RemoteSandbox


remoteSandbox()

function remoteSandbox(options?: ConnectRemoteSandboxOptions): LazyRemoteSandbox;

Synchronous construction, lazy connection — the ambient-init seam.

@pyric/cli/register installs this behind the Symbol.for('pyric.remote.sandboxFactory') global so pyric-admin’s bare initializeApp() can mint a full branded handle without awaiting anything. The wire connection (discovery → WS attach) happens on the FIRST op (or ready access), so the existing fail-fast — “no browser tab is connected — open ” — surfaces on first use instead of at construction. A failed connect is NOT latched: the next op retries, matching the error’s own “…and retry” guidance. connectRemoteSandbox (eager) is unchanged.

Parameters

ParameterType
options?ConnectRemoteSandboxOptions

Returns

LazyRemoteSandbox