Skip to main content

Sealer-governed clients

Initialize with initializeFromShyConfig. See the overview for the common setup pattern and uniform primitives.

ClientContract versionRequired init options
storeClient.jsshystore-v1deriveSealerKey, signMessage, getIdentityAttestation
chatClient.jsshychat-v1deriveSealerKey (required when sealer.enabled: true)
restClient.jsshyrest-v1deriveSealerKey, signMessage, getIdentityAttestation
streamClient.jsshystream-v1deriveSealerKey, signMessage, getIdentityAttestation
browserClient.jsshybrowser-v1deriveSealerKey

Sealer-governed clients encrypt all List 1 payloads client-side (AES-GCM) before broadcast. The ABCI layer stores ciphertext verbatim — plaintext never reaches canonical state. No rescission pathway exists; erasure means the operator deletes the sealer key, making sealed payloads permanently undecryptable.

Every sealer-governed shyconfig must declare domains.private.consoleassertStoreManifest, assertChatManifest, and assertStreamManifest all throw if absent.


storeClient

assertStoreManifest enforces: contract_version: "shystore-v1", product_type: "shystore", store block present, domains.private.console present.

import { initializeFromShyConfig } from 'shyware/sdk/web/storeClient.js'

const client = initializeFromShyConfig(shyconfig, {
getAuthHeaders: async () => ({ Authorization: `Bearer ${token}` }),
deriveSealerKey: async () => derivedKeyMaterial,
signMessage: async (msg) => ({ pubKeyHex, sigBytes }),
getIdentityAttestation: async (scope) => ({ idv_attestation_sig: sigBytes })
})

Bucket lifecycle

await client.createBucket({ scopingId: 'bucket-2026-q2', allowedCategories: ['auth_seed_totp'] })
await client.listBuckets()
await client.getBucket(scopingId)
await client.closeBucket({ scopingId, closingHeight: 42100 })
await client.getBucketClosure(scopingId) // → KMS-signed ClosureRecord

Store / reveal / rotate

const { submissionId, submissionNonce } = await client.storeSubmission({
scopingId, plaintext: { service: 'github.com', seed: 'BASE32...' }, category: 'auth_seed_totp'
})

const sealed = await client.revealStore({ scopingId, submissionId })
const plain = await client.revealAndDecryptStore({ scopingId, submissionId })

const { submissionId: newId } = await client.rotateStore({
scopingId, submissionId, newPlaintext: { seed: 'NEWBASE32...' }
})

Recovery receipts

await client.writeRecoveryReceipt(scopingId, submissionId)
const receipt = await client.readRecoveryReceipt(scopingId, submissionId)

Local sealing helpers

const sealed = await client.sealPayload(plaintext)
const plain = await client.openPayload(sealed)

chatClient

assertChatManifest enforces: contract_version: "shychat-v1", product_type: "shychat", identity.surface_model: "mail" | "chat", messaging block present, domains.private.console present.

deriveSealerKey is passed through at init and used when sealer.enabled: true in the shyconfig. If absent and sealer.enabled: true, queueDispatch throws at call time.

import { initializeFromShyConfig } from 'shyware/sdk/web/chatClient.js'

const client = initializeFromShyConfig(shyconfig, {
getAuthHeaders: async () => ({ Authorization: `Bearer ${token}` }),
deriveSealerKey: async () => derivedKeyMaterial // required when sealer.enabled: true
})

Mailboxes

await client.listMailboxes()
await client.getMailbox(mailboxId) // includeContent defaults true
await client.getMailbox(mailboxId, { includeContent: false })
await client.createMailbox({
label, address, routeHint,
accountLabel, // optional — multi-account deployments
accountScope, // optional
auditPolicy
})

Dispatch

await client.queueDispatch({
mailboxId,
recipientAddress,
subject,
body,
deliveryWindow, // optional — unix timestamp pair
contentClass: 'report',
payloadFormat: 'mail_text',
auditMode: 'delivery_commitment_only',
privateFields: { ... },
attachmentRefs: ['ipfs://...']
})

auditMode: "delivery_commitment_only" — only delivery continuity appears in canonical state, no subject or routing metadata.

Close and receipts

await client.attestClose(mailboxId)
await client.writeRecoveryReceipt(mailboxId)
await client.readRecoveryReceipt(mailboxId)

restClient

Composite of storeClient + chatClient for informant-intake deployments.

import { initializeFromShyConfig } from 'shyware/sdk/web/restClient.js'

Exposes the full storeClient bucket/submission surface and the full chatClient mailbox/dispatch surface on a single client. Access underlying clients directly when needed:

const store = client.getStoreClient()
const chat = client.getChatClient()

streamClient

Wraps storeClient for video sealing and live segment queueing. All bucket/submission operations are identical to storeClient — use bucketID where storeClient uses scopingId.

assertStreamManifest enforces: contract_version: "shystream-v1", product_type: "shystream", anon_layer.black_box_required: true, signing.required: true (not "none"), store block present, stream block with stream.provider present, domains.private.console present. Required flows: stream_event, stream_clip, stream_read, biometric_rederive, hop_route_store, hop_route_reveal.

import { initializeFromShyConfig } from 'shyware/sdk/web/streamClient.js'

const client = initializeFromShyConfig(shyconfig, {
getAuthHeaders: async () => ({ Authorization: `Bearer ${token}` }),
deriveSealerKey: async () => derivedKeyMaterial,
signMessage: async (msg) => ({ pubKeyHex, sigBytes }),
getIdentityAttestation: async (scope) => ({ idv_attestation_sig: sigBytes })
})

Bucket lifecycle — identical to storeClient

await client.createBucket({ scopingId: 'stream-session-42', allowedCategories: ['stream_event', 'stream_clip'] })
await client.closeBucket({ scopingId, closingHeight })
await client.getBucketClosure(scopingId)

Stream events and clips

await client.createStreamEvent({ bucketID: 'stream-session-42', payload: { event: 'stream_start', streamID, timestamp }, category: 'stream_event' })
await client.createStreamClip({ bucketID: 'stream-session-42', payload: { clipID, duration_ms, ref }, category: 'stream_clip' })

Both call storeClient.storeSubmission under the two-list invariant. Returns { submissionId, submissionNonce }.

Seal a video segment (VoD)

const { submissionId } = await client.sealVideoSegment({
bucketID: 'stream-session-42',
streamID: 'stream-abc',
sequence: 42,
segment: uint8Array, // or segmentRef: 'cdn://...'
mimeType: 'video/mp2t',
codec: 'h264',
startedAt: Date.now()
})

Live segment sealing (low-latency)

const { submissionId } = await client.sealLiveSegment({
bucketID, streamID, sequence, segment, startedAt, endedAt
})

Live queue — batch flush with jitter

For continuous live streams, use createLiveQueue to batch segment sealing with configurable flush intervals and jitter:

const queue = client.createLiveQueue({
bucketID: 'stream-session-42',
streamID: 'stream-abc',
minBatchSize: 3,
maxBatchSize: 25,
flushIntervalMs: 4000,
jitterMs: 900,
onFlush: (summary) => console.log(`flushed ${summary.count} segments`),
onError: (err) => console.error(err)
})

queue.enqueue({ sequence: 0, segment: chunk0 })
queue.enqueue({ sequence: 1, segment: chunk1 })
await queue.flush({ force: true }) // flush immediately
queue.stop() // cancel scheduled flush

Reveal and rotate

const plain = await client.revealStreamRecord({ bucketID, submissionId })
const plain = await client.revealVideoSegment({ bucketID, submissionId })

await client.rotateStreamRecord({ bucketID, submissionId, newPayload })
await client.rotateLiveSegment({ bucketID, submissionId, streamID, sequence, segment })

browserClient

assertBrowserManifest enforces: contract_version: "shybrowser-v1", sealer.mode: "sealed_storage". No anon_layer.required_flows are checked — browserClient is the only sealer-governed client without required flows.

deriveSealerKey is required at call time (not init time) — storeSealedBrowserData will throw if not supplied to createBrowserClient.

import { initializeFromShyConfig } from 'shyware/sdk/web/browserClient.js'

const client = initializeFromShyConfig(shyconfig, {
getAuthHeaders: async () => ({ Authorization: `Bearer ${token}` }),
deriveSealerKey: async () => derivedKeyMaterial
})

Local sealed storage

storeSealedBrowserData seals to localStorage (or memory in non-browser environments). It takes positional arguments, not an object:

const record = await client.storeSealedBrowserData(data, 'browser_session')
// isList2 = false by default; pass true to tag as List 2 identity attribute

const records = client.getStoredBrowserData() // all records, encrypted
const records = client.getStoredBrowserData('browser_session') // filtered by category
const decrypted = await client.getStoredBrowserDataDecrypted() // all records, decrypted

Submit List 2 identity attribute

Seals and POSTs an identity attribute (e.g. IP address) to the server's /list2-identity endpoint. Takes positional arguments:

await client.submitList2IdentityAttribute(ipAddress, 'ip_address')

List 1 payload and List 2 identity attribute are both sealed — the operator's sealer key is required to read either.


Structural guarantee

No rescission pathway (TxTypeAuthorityRescind is structurally unavailable for sealer-governed embodiments). Enumeration of "which participant produced which submission" is structurally impossible from canonical state alone. Erasure means the operator (reconciling authority) deletes the sealer key — canonical records persist but become permanently undecryptable. A compelled disclosure after key deletion produces irrecoverable ciphertext.