Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

@enc-protocol/flow -- SDK API Reference

Enclave resolution engine. Maps app schemas to protocol enclaves, generates deployment artifacts, and produces runtime SDKs.

Install

npm config set @enc-protocol:registry https://npm-registry.ocrybit.workers.dev/
npm install @enc-protocol/flow
import {
  extractSchema,
  deriveManifest,
  deriveManifestFromSchema,  // schema-first entry point (no UI/flow needed)
  match,
  generate,
  profileEnclave,
  deriveFlow,
  createSDK,
  FLOW_CONFIG,               // inline fallback (browser-safe)
  loadFlowConfig,            // async Node-only loader for per-app flow.json
} from '@enc-protocol/flow'

Pipeline (schema-first)

ui.json + flow.json  ─►  schema.json  ─►  app.json  ─►  infra.json
                                          enclaves/*.json ─┘
  • extractSchema(ui, flow) → schema.json (absorbs tableMap, derived, enclaves, sources so downstream never re-opens flow)
  • deriveManifestFromSchema(schema, enclaves) → app.json (the new schema-first entry; takes no UI)
  • deriveManifest(app, enclaves) → app.json (legacy wrapper kept for callers without a persisted schema)

Table of Contents

  1. schema.mjs -- extractSchema()
  2. resolver.mjs -- match(), generate(), profileEnclave()
  3. manifest.mjs -- deriveManifest()
  4. sdk-gen.mjs -- createSDK(), sdkMethods()
  5. flow-config.js -- FLOW_CONFIG
  6. derive-flow.mjs -- deriveFlow()
  7. ui-analysis.mjs -- deriveUi()
  8. engine.mjs -- profileEnclave(), analyzeApp(), resolve()
  9. resolve-enclaves.mjs -- resolveEnclaves()
  10. index.mjs -- Re-exports
  11. lib/generic-dataview.js -- GenericDataView class
  12. lib/dataview-server.js -- DataView HTTP server
  13. lib/workflow-executor.mjs -- executeWorkflow(), executeMockAsCommits()

1. schema.mjs -- Schema Extraction

Source: sdk-flow/schema.mjs


extractSchema(app, flow?) -> Schema

Extract a protocol-level data schema from a UI app definition + its flow config. Derives read/write tables from action expressions and data bindings, and absorbs the flow's routing fields (tableMap, derived, enclaves, sources) into the schema output so downstream stages never re-open the flow config.

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition (from ui.json or ENC_APPS[]). Must have id, name, pages.
flowobjectFLOW_CONFIG[app.id] ?? {}Flow config entry. When omitted, falls back to the inline FLOW_CONFIG.

Returns: Schema

interface Schema {
  id: string                              // App identifier
  name: string                            // Display name
  data_types: Record<string, JSONSchema>  // Write tables with JSON Schema
  actions: Record<string, ActionDef>      // Named write triggers
  reads: Record<string, ReadSpec>         // Read subscriptions
  encrypt: string[]                       // Encrypted table names
  // Absorbed from flow (only present if flow set them):
  tableMap?: Record<string, string>       // UI table -> enclave event
  derived?: string[]                      // Client-side view tables
  enclaves?: string | string[]            // Enclave override ('*' or list)
  sources?: { protocol?: string[]; external?: string[] }
  external?: {                            // Only if app has external data
    reads: Record<string, { fields: string[] }>
    writes: Record<string, JSONSchema>
  }
}
 
interface ActionDef {
  write?: string       // Target table (for insert)
  delete?: string      // Target table (for delete)
  page: string         // Source page path
  trigger: string      // UI trigger (e.g., '#bar.submit')
}
 
interface ReadSpec {
  fields: string[]        // Column names referenced
  cross_enclave?: boolean // True if table is read-only (not written)
}
Behavior:
  1. Loads the app's flow config from FLOW_CONFIG[app.id].
  2. Iterates all pages (skipping gate pages).
  3. Scans action expressions for push(), delete(), insert() patterns.
  4. Scans data bindings for #node<table and @table.field patterns.
  5. Marks tables in flow.derived as derived (skipped).
  6. Marks tables in flow.sources.external as external (separate output).
  7. Cross-enclave marks read-only tables (read but never written).
Example:
import { extractSchema } from '@enc-protocol/flow'
 
const schema = extractSchema(helloApp)
// {
//   id: 'hello',
//   name: 'Hello',
//   data_types: {
//     messages: {
//       type: 'object',
//       properties: { draft: { type: 'string' } },
//       required: ['draft']
//     }
//   },
//   actions: {
//     feed_bar_submit: { write: 'messages', page: '/feed', trigger: '#bar.submit' },
//     my_posts_bar_submit: { write: 'messages', page: '/my_posts', trigger: '#bar.submit' }
//   },
//   reads: {
//     messages: { fields: ['from', 'body', 'trailing'] }
//   },
//   encrypt: []
// }
Edge Cases:
  • If FLOW_CONFIG[app.id] does not exist, uses empty defaults (encrypt: [], no derived, no external`).
  • Tables in derived are completely skipped -- they do not appear in data_types, actions, or reads.
  • Tables in sources.external are classified under schema.external instead of schema.data_types / schema.reads.
  • If the same table appears in multiple push() actions across pages, the data_types entry is created once and extended with new columns.
  • The external key is only present in the output if external tables exist.
  • Action key naming: pagePath_trigger with leading / stripped and #/. replaced by _. Duplicate keys are not overwritten.

2. resolver.mjs -- Matching and Generation

Source: sdk-flow/resolver.mjs


profileEnclave(name, manifest) -> EnclaveProfile | null

Profile an enclave's access model from its RBAC manifest for matching.

Parameters:
ParamTypeDefaultDescription
namestringrequiredEnclave name (e.g., 'Hello')
manifestobjectrequiredEnclave manifest object (the .manifest property from the JSON file)

Returns: EnclaveProfile | null (null if manifest is falsy)

interface EnclaveProfile {
  name: string                // Enclave name
  parties: '1' | '2' | 'N'   // Participant model
  readerModel: 'public' | 'member' | 'owner'
  events: Record<string, EventProfile>
  stateCount: number          // Number of states in the enclave
}
 
interface EventProfile {
  write: boolean              // Has Create permission (non-dataview)
  dataviewP: boolean          // Has Poll permission
  operators: string[]         // All operators for this event
}
Behavior:
  1. Extracts states, readers, customs, and moves from the manifest.
  2. Determines participant model:
    • MEMBER state + (complex moves or PENDING state) = 'N'
    • MEMBER state (simple) = '2'
    • No MEMBER state = '1'
  3. Determines reader model:
    • Public reader or non-OWNER with reads: '*' = 'public'
    • MEMBER reader = 'member'
    • Otherwise = 'owner'
  4. Builds per-event profiles from customs:
    • write: true if ops include 'C' and operator is not 'dataview'
    • dataviewP: true if ops include 'P'
Example:
import { profileEnclave } from '@enc-protocol/flow'
 
const helloManifest = {
  states: [],
  traits: ['owner(0)', 'dataview(1)'],
  readers: [{ type: 'Public', reads: '*' }],
  moves: [],
  customs: [
    { event: 'post', operator: 'owner', ops: ['C', 'U', 'D'] },
    { event: 'post', operator: 'dataview', ops: ['P'] }
  ],
  // ...
}
 
const profile = profileEnclave('Hello', helloManifest)
// {
//   name: 'Hello',
//   parties: '1',
//   readerModel: 'public',
//   events: {
//     post: { write: true, dataviewP: true, operators: ['owner', 'dataview'] }
//   },
//   stateCount: 0
// }
Edge Cases:
  • Returns null if manifest is null, undefined, or falsy.
  • Events with operator: 'dataview' and ops ['C'] are NOT counted as writable (dataview only polls, it never creates).
  • An event can have both write: true and dataviewP: true if different operators provide C and P respectively.

match(schema, enclaveRegistry) -> MatchResult

Match an app schema against existing enclaves. Returns the best-fit primary enclave, cross-enclave reads, and scores.

Parameters:
ParamTypeDefaultDescription
schemaSchemarequiredApp schema from extractSchema()
enclaveRegistryRecord<string, EnclaveData>required{ Name: { manifest: {...} } }

Returns: MatchResult

interface MatchResult {
  primary: { name: string, score: number, profile: EnclaveProfile } | null
  crossEnclave: CrossEnclaveRead[]
  enclaves: string[]           // All enclaves needed
  scores: { name: string, score: number, profile: EnclaveProfile }[]  // Top 6
  profiles: Record<string, EnclaveProfile>
}
 
interface CrossEnclaveRead {
  table: string                // Read-only table name
  enclave: string              // Source enclave name
  event: string                // Source event name
  reason: string               // Human-readable reason
}
Behavior:
  1. Profiles all enclaves in the registry via profileEnclave().
  2. Scores each profile against the schema (see scoring below).
  3. Filters out scores of -Infinity (disqualified).
  4. Sorts by score descending. Primary = highest.
  5. Resolves cross-enclave reads for external tables:
    • profiles always maps to Registry.reg_identity.
    • Other external tables: search scored enclaves (excluding primary) for one with DataView P capability.
  6. Returns union of primary + cross-enclave as enclaves.
Scoring Algorithm:
Score = 0
 
// Write coverage: min(writableEvents, writeTableCount) * 10
// Read coverage: min(totalEvents, totalTableCount) * 3
// Excess events penalty: -(totalEvents - totalTableCount) * 2  (if > 2x)
// Insufficient events: -(writeTableCount - writableEvents) * 5
 
// Encrypted writes need owner/member reader: +10
// Encrypted writes with public reader: -10
// Plaintext writes with DataView P or public: +5
 
// Disqualified: writeTableCount > 0 but writableEvents == 0
Example:
import { match, extractSchema } from '@enc-protocol/flow'
 
const schema = extractSchema(helloApp)
const result = match(schema, {
  Hello: { manifest: helloEnclave.manifest },
  DM: { manifest: dmEnclave.manifest },
  Timeline: { manifest: timelineEnclave.manifest },
})
// {
//   primary: { name: 'Hello', score: 23, profile: { ... } },
//   crossEnclave: [],
//   enclaves: ['Hello'],
//   scores: [
//     { name: 'Hello', score: 23 },
//     { name: 'Timeline', score: 15 },
//     ...
//   ],
//   profiles: { Hello: { ... }, DM: { ... }, ... }
// }
Edge Cases:
  • If no enclave scores above -Infinity, primary is null.
  • If schema.reads has no entries with external: true, crossEnclave is empty.
  • Enclave registry entries with missing or falsy manifest are profiled as null and scored as -Infinity.

generate(schema) -> GeneratedEnclave

Generate a minimum viable enclave manifest from an app schema. Used when no existing enclave fits the app's needs.

Parameters:
ParamTypeDefaultDescription
schemaSchemarequiredApp schema from extractSchema()

Returns: GeneratedEnclave

interface GeneratedEnclave {
  name: string                     // schema.name or schema.id
  description: string              // Auto-generated description
  manifest: {
    states: []                     // Always empty (1-party)
    traits: string[]               // ['owner(0)'] + optionally ['dataview(1)']
    readers: ReaderRule[]          // Owner-only or Public
    moves: []                      // Always empty
    grants: GrantRule[]            // DataView grants if needed
    transfers: []                  // Always empty
    slots: []                      // Always empty
    lifecycle: LifecycleRule[]     // Always includes Terminate
    customs: CustomRule[]          // One per data type
    init: InitRule[]               // Owner initialization
  }
  encryption: Record<string, { algorithm: string }>  // Per encrypted event
  data_types: Record<string, JSONSchema>             // Passthrough from schema
}
Behavior:
  1. Determines if app needs plaintext or encrypted writes.
  2. Creates one custom event per data type:
    • Table name singularized (messages -> message).
    • OWNER gets CUD ops.
    • If plaintext, dataview gets P ops.
  3. Sets reader model:
    • Encrypted-only: [{ type: 'OWNER', reads: '*' }]
    • Plaintext-only: [{ type: 'Public', reads: '*' }]
    • Mixed: [{ type: 'OWNER', reads: '*' }] (owner reads all, dataview polls plaintext)
  4. Adds traits: ['owner(0)'] + ['dataview(1)'] if DataView needed.
  5. Adds DataView grants if needed.
  6. Determines encryption algorithm: XChaCha20-Poly1305 for each encrypted table.
Example:
import { generate, extractSchema } from '@enc-protocol/flow'
 
const schema = extractSchema(helloApp)
const enclave = generate(schema)
// {
//   name: 'Hello',
//   description: 'Auto-generated enclave for Hello.',
//   manifest: {
//     states: [],
//     traits: ['owner(0)', 'dataview(1)'],
//     readers: [{ type: 'Public', reads: '*' }],
//     customs: [
//       { event: 'message', operator: 'OWNER', ops: ['C', 'U', 'D'] },
//       { event: 'message', operator: 'dataview', ops: ['P'] }
//     ],
//     ...
//   },
//   encryption: {},
//   data_types: { messages: { ... } }
// }
Edge Cases:
  • If schema has no data_types, produces an enclave with zero customs.
  • Table name singularization is naive (replace(/s$/, '')) -- addresses becomes addresse, not address.
  • Encryption key uses the singularized event name, not the table name.

3. manifest.mjs -- Manifest Derivation

Source: sdk-flow/manifest.mjs


deriveManifestFromSchema(schema, enclaveRegistry?) -> AppManifest

Primary entry point (schema-first). Derives an app manifest from an already-extracted schema + the available enclave registry. Never touches the UI or flow sources directly — the schema is expected to have absorbed the flow's routing fields (via extractSchema(app, flow)).

Use this when you have a persisted schema.json on hand, or when you want to hand-author a schema and skip the UI/flow pipeline entirely.

Parameters:
ParamTypeDefaultDescription
schemaSchemarequiredOutput of extractSchema(app, flow) or a hand-authored equivalent
enclaveRegistryRecord<string, EnclaveData>{}{ Name: { manifest: {...} } }

deriveManifest(app, enclaveRegistry?) -> AppManifest

Legacy wrapper: runs extractSchema(app) internally, then forwards to deriveManifestFromSchema. Kept for callers that don't have a persisted schema on hand (in-browser usage, published-SDK consumers).

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition (from ui.json or ENC_APPS[])
enclaveRegistryRecord<string, EnclaveData>{}{ Name: { manifest: {...} } }

Returns: AppManifest

interface AppManifest {
  id: string
  name: string
  version: '1.0.0'
  enclaves: string[]
  data_types: Record<string, Record<string, string | ColSpec>>
  cross_enclave_reads?: Record<string, CrossRead>
}
 
interface ColSpec {
  type: string
  encrypted?: boolean
  optional?: boolean
}
 
interface CrossRead {
  enclave: string
  event: string
  access: 'read'
}
Behavior:
  1. Resolves enclaves:
    • Check ENCLAVE_OVERRIDES[app.id] first.
    • Default: [PascalCase(app.name)].
    • Wildcard '*' expands to all keys in enclaveRegistry.
  2. Calls extractSchema(app) to derive the schema.
  3. Converts JSON Schema data_types to flat field maps: { col: typeString }.
  4. Detects cross-enclave reads: tables read but not written.
    • profiles -> Registry.reg_identity (if Registry in registry).
    • Others: search enclave customs for matching event name.
  5. Adds cross-enclave enclaves to the enclaves list.
ENCLAVE_OVERRIDES:
{
  wallet:     ['DM'],
  appstore:   ['*'],
  super:      ['DM', 'Group', 'Personal'],
  node:       ['*'],
  mini_hello: ['Hello'],
}
Type Inference Rules:
Column NameInferred Type
from, to, pub, identity, owner, media, key, *_pubid_pub
outgoing, read, pinned, starred, blocked, encrypted, public, privatebool
likes, replies, count, views, amount, balanceint
created_at, updated_at, expires_at, timestamp, timetimestamp
Everything elsestring
Example:
import { deriveManifest } from '@enc-protocol/flow'
 
const manifest = deriveManifest(helloApp, enclaveRegistry)
// {
//   id: 'hello',
//   name: 'Hello',
//   version: '1.0.0',
//   enclaves: ['Hello'],
//   data_types: { messages: { draft: 'string' } }
// }
 
const dmManifest = deriveManifest(dmApp, enclaveRegistry)
// {
//   id: 'dm',
//   name: 'DM',
//   version: '1.0.0',
//   enclaves: ['DM', 'Registry'],
//   data_types: { invites: {}, contacts: {}, messages: { message_draft: 'string' } },
//   cross_enclave_reads: {
//     profiles: { enclave: 'Registry', event: 'reg_identity', access: 'read' }
//   }
// }
Edge Cases:
  • If extractSchema() throws, falls back to empty schema: { data_types: {}, actions: {}, reads: {}, encrypt: [] }.
  • cross_enclave_reads key is only present in output when there are cross-enclave reads.
  • If the enclave registry is empty and the app has wildcard enclaves, enclaves remains ['*'].

4. sdk-gen.mjs -- SDK Generation

Source: sdk-flow/sdk-gen.mjs


createSDK(appId, infra, adapter, opts?) -> SDK

Generate a runtime SDK instance for an app. Wraps a NetworkAdapter or PureAdapter with typed methods for every protocol operation.

Parameters:
ParamTypeDefaultDescription
appIdstringrequiredApp identifier (e.g., 'hello')
infraobjectrequiredParsed infra.json content
adapterobjectrequiredCore adapter (NetworkAdapter or PureAdapter)
optsobject{}Optional configuration
opts.cryptoobjectnullCrypto hooks for encrypted tables
opts.crypto.encryptfunction--async (eventName, content, opts) => encryptedContent
opts.crypto.decryptfunction--async (eventName, encContent, senderPub) => content

Returns: SDK

interface SDK {
  // Dynamically generated per infra.json commands:
  submit_<eventName>: (content: any, cryptoOpts?: any) => Promise<any>
 
  // Dynamically generated per infra.json endpoints:
  query_<endpointName>: (opts?: any) => Promise<any[]>
 
  // Fixed methods:
  subscribe: (callback: (event: any) => void) => () => void
  query_events: (eventType: string, opts?: any) => Promise<any[]>
  create_enclave: (rbac: any) => Promise<any>
 
  // Cross-enclave:
  registry: {
    setAdapter: (adapter: any) => void
    publish: (identity: any) => Promise<any>
    lookup: (pubKey: string) => Promise<any>
    nameFor: (pubKey: string) => string
    _adapter: any
    _cache: Map<string, any>
  }
 
  // External data:
  external: {
    register: (name: string, fetchFn: Function) => void
    fetch: (name: string, opts?: any) => Promise<any>
    _handlers: Map<string, Function>
  }
 
  // Metadata:
  _adapter: any
  _appId: string
  _infra: object
  _events: string[]           // Commit event names
  _queries: string[]          // Query endpoint names
  _encrypted: string[]        // Encrypted table names
  _externalTables: string[]   // External table names
  crypto: object | null       // Injected crypto hooks
}
Behavior:
  1. Loads flow config from FLOW_CONFIG[appId].
  2. Builds reverse mapping: eventToTables from tableMap.
  3. For each commit command in infra.commands:
    • Creates submit_<eventName> method.
    • If the event's tables include an encrypted table AND crypto.encrypt exists, encrypts content before submitting.
    • Serializes payload to JSON string if not already a string.
  4. For each endpoint in infra.endpoints (excluding _-prefixed):
    • Creates query_<endpointName> method.
    • Uses adapter.queryEndpoint() if available, else falls back to adapter.query().
  5. Creates subscribe() method:
    • If event is for an encrypted table and crypto.decrypt exists, decrypts content before passing to callback.
    • Adds _encrypted: true flag to decrypted events.
    • Falls through to raw event if decryption fails.
  6. Creates registry sub-object for cross-enclave identity operations:
    • publish(identity) -- submits reg_identity event.
    • lookup(pubKey) -- queries all reg_identity events, caches results by normalized pubkey.
    • nameFor(pubKey) -- synchronous cache lookup, returns display_name or truncated pubkey.
  7. Creates external sub-object for external data sources.
Example:
import { createSDK } from '@enc-protocol/flow'
 
// Basic plaintext app
const sdk = createSDK('hello', infraJson, pureAdapter)
await sdk.submit_post({ body: 'Hello World', from: '79be...' })
const rows = await sdk.query_messages({ limit: 50 })
const unsub = sdk.subscribe(event => console.log(event.content))
 
// Encrypted app with crypto
const dmSdk = createSDK('dm', dmInfra, adapter, {
  crypto: {
    encrypt: async (eventName, content) => {
      return ecdhEncrypt(content, recipientPub)
    },
    decrypt: async (eventName, encContent, senderPub) => {
      return ecdhDecrypt(encContent, senderPub)
    },
  }
})
await dmSdk.submit_message({ body: 'secret' })  // auto-encrypts
 
// Cross-enclave identity
sdk.registry.setAdapter(registryAdapter)
await sdk.registry.publish({ id_pub: '79be...', display_name: 'Alice' })
const identity = await sdk.registry.lookup('79be...')
const name = sdk.registry.nameFor('79be...')  // 'Alice' (from cache)
Edge Cases:
  • If adapter.subscribe is falsy, sdk.subscribe() returns a no-op unsubscribe function.
  • If adapter.queryEndpoint does not exist, query_* methods fall back to adapter.query() with the first commit command's event name.
  • registry.lookup() queries up to 500 events and caches all of them. Subsequent lookups hit the cache.
  • registry.nameFor() returns truncated pubkey (slice(0, 8)) if no cached identity exists.
  • If opts.crypto is not provided, sdk.crypto is null and no encryption/decryption is performed.
  • Endpoints prefixed with _ are internal and excluded from query methods.

sdkMethods(appId, infra) -> MethodDef[]

List all SDK methods that would be generated for an app. Useful for documentation and introspection.

Parameters:
ParamTypeDefaultDescription
appIdstringrequiredApp identifier
infraobjectrequiredParsed infra.json

Returns: MethodDef[]

interface MethodDef {
  name: string        // e.g., 'submit_post', 'query_messages'
  type: 'write' | 'read' | 'subscribe' | 'lifecycle'
  event?: string      // For write methods: event name
  encrypted?: boolean // For write methods: whether event is encrypted
  method?: string     // For read methods: HTTP method
  path?: string       // For read methods: endpoint path
}
Behavior:
  1. For each commit command in infra.commands: add submit_<name> with type 'write' and encryption status.
  2. For each endpoint (excluding _-prefixed): add query_<name> with type 'read'.
  3. Always adds subscribe (type 'subscribe'), query_events (type 'read'), and create_enclave (type 'lifecycle').
Example:
import { sdkMethods } from '@enc-protocol/flow/sdk-gen'
 
const methods = sdkMethods('hello', helloInfra)
// [
//   { name: 'submit_post', type: 'write', event: 'post', encrypted: false },
//   { name: 'query_messages', type: 'read', method: 'GET', path: '/messages' },
//   { name: 'subscribe', type: 'subscribe' },
//   { name: 'query_events', type: 'read' },
//   { name: 'create_enclave', type: 'lifecycle' },
// ]
Edge Cases:
  • Encryption detection: checks if any table in tableMap that maps to the event name is in flow.encrypt. Uses the mapped event name, not the table name.
  • If infra.commands or infra.endpoints is missing/empty, only the fixed methods are returned.

5. flow-config.js -- FLOW_CONFIG + loadFlowConfig

Source: sdk-flow/flow-config.js.

Canonical data lives at config/apps/<id>/flow.json (one file per app). The SDK ships an inline FLOW_CONFIG object mirroring those files, so browser/published-SDK consumers can read the same data without the filesystem. For live on-disk reads in Node, use the async loadFlowConfig(appsDir) helper below.


FLOW_CONFIG

Exported constant object. Per-app protocol decisions, keyed by app ID. Browser-safe (no fs at module scope).

Type: Record<string, FlowConfigEntry>

interface FlowConfigEntry {
  tableMap?: Record<string, string>     // UI table -> enclave event
  encrypt?: string[]                    // Encrypted tables
  derived?: string[]                    // Client-side view tables
  sources?: {
    protocol?: string[]                 // Protocol-backed tables
    external?: string[]                 // External API-backed tables
  }
  enclaves?: string | string[]         // Explicit enclave(s) or '*'
}
Complete Config:
ApptableMapencryptderivedsourcesenclaves
hellomessages->post, my_messages->post--my_messages----
dmmessages->message, invites->invitemessagescontacts----
groupmessages->messagemessagesmembers, pending, group_info----
personalpublic->public, private->privateprivate------
timelineposts->post, my_posts->post, likes->like, comments->comment, follows->follow--10 tables----
registryreg_identities->reg_identity, reg_enclaves->reg_enclave, reg_nodes->reg_node--6 tables----
node----5 tables--'*'
walletapp_messages->message, app_threads->message, app_pending->inviteapp_messages4 tablesprotocol: 4, external: 4--
supermessages->message, moments->public--4 tables----
appstore----catalog, macros--'*'
Example:
import { FLOW_CONFIG } from '@enc-protocol/flow'
 
// Access a specific app's config
const helloConfig = FLOW_CONFIG.hello
// { tableMap: { messages: 'post', my_messages: 'post' }, derived: ['my_messages'] }
 
// Check if a table is encrypted
const isEncrypted = (FLOW_CONFIG.dm.encrypt || []).includes('messages')  // true
 
// Check if a table is derived
const isDerived = (FLOW_CONFIG.timeline.derived || []).includes('my_posts')  // true
 
// Resolve event name for a table
const eventName = FLOW_CONFIG.hello.tableMap.messages  // 'post'

loadFlowConfig(appsDir)

Async. Node-only. Reads <appsDir>/<id>/flow.json for every subdirectory and returns the combined config object. node:fs is imported lazily so browsers that never call this function don't trip Vite's fs externalization.

Signature: (appsDir: string) => Promise<Record<string, FlowConfigEntry>>

Example:
import { loadFlowConfig } from '@enc-protocol/flow'
import { join } from 'node:path'
 
const flows = await loadFlowConfig(join(process.cwd(), 'config/apps'))
// { hello: { tableMap: ..., derived: ... }, dm: { ... }, ... }

6. derive-flow.mjs -- Flow Derivation

Source: sdk-flow/derive-flow.mjs


deriveFlow(app, v1, enclaves?) -> FlowDescriptor

Derive a protocol flow description from an app definition, its derived manifest (v1), and available enclave data.

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition (from ui.json or ENC_APPS[])
v1AppManifestrequiredDerived manifest from deriveManifest()
enclavesRecord<string, EnclaveData>{}{ Name: { manifest: {...} } }

Returns: FlowDescriptor

interface FlowDescriptor {
  $schema: 'https://enc-protocol.dev/schemas/app-flow-v0.json'
  name: string
  description: string
  identity: { key: 'secp256k1' }
  events: Record<string, { fields: Record<string, { type: string }> }>
  encryption: Record<string, { _all: boolean, algorithm: string }>
  flows: Record<string, FlowDef>
  ingest: Record<string, IngestRule[]>
}
 
interface FlowDef {
  params: string[]
  steps: FlowStep[]
}
 
interface FlowStep {
  if?: string
  fail?: string
  return?: boolean
  submit?: { to_enclave: string, type: string, content: Record<string, string> }
  on_error?: { fail: string }
  http_get?: { url: string }
  set?: string
  then?: FlowStep[]
  merge_feed?: string
}
Behavior:
  1. Iterates v1.data_types, skipping derived tables.
  2. Resolves event name for each table via tableMap or enclave customs.
  3. Builds events map with field types.
  4. For encrypted tables: determines encryption algorithm based on enclave participant model (1-party: XChaCha20-Poly1305, 2-party: Ratchet_DM, N-party: MLS-lite).
  5. Generates submit_<event> flow for each event with validation steps.
  6. Generates ingest rules for non-encrypted events: maps event fields to local state table rows.
  7. Generates load_<readKey> flows for each SQL read in v1.sql.read.
Event Name Resolution Order:
  1. Check tableMap[table] in flow config.
  2. Search enclave customs for exact match (event === table).
  3. Search enclave customs for singular match (event === table.replace(/s$/, '')).
  4. Single-event heuristic: if enclave has exactly 1 non-lifecycle event, use it.
  5. Fallback: table.replace(/s$/, '').
Encryption Algorithm Selection:
Enclave PatternAlgorithm
No MEMBER state, no movesXChaCha20-Poly1305
MEMBER state, few movesRatchet_DM
MEMBER state + many movesMLS-lite
Example:
import { deriveFlow, deriveManifest } from '@enc-protocol/flow'
 
const v1 = deriveManifest(helloApp, enclaveRegistry)
const flow = deriveFlow(helloApp, v1, enclaveRegistry)
// {
//   $schema: 'https://enc-protocol.dev/schemas/app-flow-v0.json',
//   name: 'Hello',
//   identity: { key: 'secp256k1' },
//   events: {
//     post: { fields: { draft: { type: 'string' } } }
//   },
//   encryption: {},
//   flows: {
//     submit_post: {
//       params: ['draft'],
//       steps: [
//         { if: '!$draft', fail: 'empty draft' },
//         { submit: { to_enclave: '$myEnclave', type: 'post', content: { draft: '$draft' } } }
//       ]
//     }
//   },
//   ingest: {
//     post: [{ add_post: { id: '$ev.id', timestamp: '$ev.timestamp', draft: '$content.draft', outgoing: '$ev.from === $me' } }]
//   }
// }
Edge Cases:
  • Encrypted events skip ingest rule generation (can't index encrypted content).
  • If multiple writes touch the same ingest table, the richest (most fields) ingest rule wins.
  • load_* flows are only generated if v1.sql?.read exists.
  • Description falls back to "${name} app." if app.description is missing.

7. ui-analysis.mjs -- UI Derivation

Source: sdk-flow/ui-analysis.mjs


deriveUi(app, v1, enclaves?) -> UiDescriptor

Derive a structured UI tree from an algebraic app definition. Each page type is translated by a dedicated translator function.

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition (from ui.json or ENC_APPS[])
v1AppManifestrequiredDerived manifest
enclavesRecord<string, EnclaveData>{}Enclave registry

Returns: UiDescriptor

interface UiDescriptor {
  $schema: 'https://enc-protocol.dev/schemas/app-ui-v0.json'
  kit_version: '0.1.0'
  name: string
  description: string
  state: Record<string, { type: string, default: string }>
  pages: Record<string, PageDef>
}
 
interface PageDef {
  header?: { title: string, subtitle?: string, back?: string | object[] }
  body: ComponentNode[]
  background?: string
}
Behavior:
  1. Determines primary submit flow name from enclave customs (e.g., send_post).
  2. Iterates app pages. Each page type dispatches to its translator:
    • gate -> null (skipped)
    • feed -> list + compose bar
    • chat -> message bubble list + chat input
    • contacts -> contact row list + lookup bar
    • form -> form fields + submit button
    • detail -> hero stack + comments + input
    • profile -> avatar + bio + post list
    • builder -> form + pending list + submit
    • search -> search bar + filter tabs + results
  3. Collects state variables from #bar.value and #fields[].key.
  4. Builds state schema with defaults.

Throws: If a page has an unsupported page type.

Example:
import { deriveUi, deriveManifest } from '@enc-protocol/flow'
 
const v1 = deriveManifest(helloApp, enclaveRegistry)
const ui = deriveUi(helloApp, v1, enclaveRegistry)
// {
//   $schema: 'https://enc-protocol.dev/schemas/app-ui-v0.json',
//   kit_version: '0.1.0',
//   name: 'Hello',
//   state: {
//     page: { type: 'string', default: 'feed' },
//     draft: { type: 'string', default: '' }
//   },
//   pages: {
//     feed: {
//       header: { title: 'Feed', subtitle: '{{$runtime.messages.length}} posts' },
//       body: [
//         { type: 'list', from: '$runtime.messages', row: { type: 'PostCard', ... } },
//         { type: 'ComposeBar', draft: '$state.draft', ... }
//       ]
//     },
//     my_posts: { ... }
//   }
// }
Edge Cases:
  • Feed pages with /my_* paths set outgoing: true on all rows and add a filter: '$item.outgoing' to the list.
  • For owner-only pages, the runtime state key is un-prefixed: my_messages becomes messages (the canonical array).
  • Chat pages filter messages by $state.chatWith.pub.
  • The back action on chat pages clears chatWith and navigates to contacts.
  • Tabs and filter nodes are only generated if the page data contains #tabs.items or #filter.items arrays.

8. engine.mjs -- Enclave Resolution Engine

Source: sdk-flow/engine.mjs

The engine module is an alternative enclave resolver with a slightly different API and scoring model. It provides profileEnclave, analyzeApp, and resolve as a cohesive pipeline.


profileEnclave(name, manifest) -> EnclaveProfileV2 | null

Profile an enclave's access model. Similar to resolver.mjs:profileEnclave but with additional computed fields.

Parameters:
ParamTypeDefaultDescription
namestringrequiredEnclave name
manifestobjectrequiredEnclave RBAC manifest

Returns: EnclaveProfileV2 | null

interface EnclaveProfileV2 {
  name: string
  parties: '1' | '2' | 'N'
  readerModel: 'public' | 'member' | 'owner'
  ownerOnly: boolean            // True if no public/member readers
  events: Record<string, {
    write: string[]             // Operators that can write (non-dataview)
    read: string[]              // (always empty in current impl)
    dataviewP: boolean
  }>
  writeEventCount: number       // Events with at least one write operator
  plaintextEventCount: number   // Events with dataviewP or public reader
  encryptedEventCount: number   // writeEventCount - plaintextEventCount
  stateCount: number
  traitCount: number
}
Differences from resolver.mjs version:
  • events[name].write is an array of operator names (not boolean).
  • Includes writeEventCount, plaintextEventCount, encryptedEventCount.
  • Includes ownerOnly boolean.
  • Includes traitCount.
Example:
import { profileEnclave } from '@enc-protocol/flow/engine'
 
const profile = profileEnclave('DM', dmManifest)
// {
//   name: 'DM',
//   parties: '2',
//   readerModel: 'owner',
//   ownerOnly: true,
//   events: {
//     invite: { write: ['OUTSIDER'], read: [], dataviewP: false },
//     message: { write: ['FRIEND'], read: [], dataviewP: false },
//     sent: { write: ['OWNER'], read: [], dataviewP: false },
//     rotate: { write: ['OWNER'], read: [], dataviewP: false }
//   },
//   writeEventCount: 4,
//   plaintextEventCount: 0,
//   encryptedEventCount: 4,
//   stateCount: 3,
//   traitCount: 0
// }

analyzeApp(app) -> AppNeeds

Extract what the app needs from its UI config and encrypt declarations.

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition (from ui.json or ENC_APPS[])

Returns: AppNeeds

interface AppNeeds {
  id: string
  writes: { table: string, encrypted: boolean }[]
  readOnly: string[]                    // Tables read but never written
  needsEncryptedWrite: boolean
  needsPlaintextWrite: boolean
  encryptedCount: number
  plaintextCount: number
  totalWrites: number
}
Behavior:
  1. Collects encrypted table names from app.encrypt.
  2. Scans page write blocks for INSERT/UPDATE/DELETE operations.
  3. Extracts tables (ignoring _state).
  4. Scans page data bindings for read tables (#node<table, @table).
  5. Computes read-only tables (referenced but never written).
Example:
import { analyzeApp } from '@enc-protocol/flow/engine'
 
const needs = analyzeApp(dmApp)
// {
//   id: 'dm',
//   writes: [
//     { table: 'invites', encrypted: false },
//     { table: 'messages', encrypted: true }
//   ],
//   readOnly: ['profiles'],
//   needsEncryptedWrite: true,
//   needsPlaintextWrite: true,
//   encryptedCount: 1,
//   plaintextCount: 1,
//   totalWrites: 2
// }
Edge Cases:
  • Pages with page: 'gate' are skipped.
  • If page.write contains SQL strings (not arrays), parses with regex INSERT INTO (\w+).
  • Tables with name _state are always excluded.

resolve(app, enclaveRegistry) -> Resolution

Resolve enclaves for an app. Complete pipeline: analyze needs, profile enclaves, score, match, resolve cross-enclave reads.

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition
enclaveRegistryRecord<string, EnclaveData>required{ Name: { manifest: {...} } }

Returns: Resolution

interface Resolution {
  appNeeds?: AppNeeds                    // Undefined if wildcard
  primary: { name: string, score: number, profile: EnclaveProfileV2 } | null
  crossEnclave: CrossEnclaveRead[]
  enclaves: string[]
  wildcard: boolean
  scores: { name: string, score: number, profile: EnclaveProfileV2 }[]  // Top 5
}
Behavior:
  1. If app.enclaves === '*', returns all enclaves with wildcard: true.
  2. Calls analyzeApp(app) for needs.
  3. Profiles all enclaves.
  4. Scores each enclave (see section 6 of flow.md for scoring rules).
  5. Filters scores > -Infinity, sorts descending.
  6. Primary = highest score.
  7. Cross-enclave: profiles -> Registry.reg_identity. Other read-only tables: find best readable enclave with DataView P.
Example:
import { resolve } from '@enc-protocol/flow/engine'
 
const result = resolve(helloApp, enclaveRegistry)
// {
//   appNeeds: { id: 'hello', writes: [{ table: 'messages', encrypted: false }], ... },
//   primary: { name: 'Hello', score: 35, profile: { ... } },
//   crossEnclave: [],
//   enclaves: ['Hello'],
//   wildcard: false,
//   scores: [{ name: 'Hello', score: 35 }, ...]
// }

9. resolve-enclaves.mjs -- Access Model Resolver

Source: sdk-flow/resolve-enclaves.mjs

A third resolver implementation focused purely on RBAC access model compatibility. No name matching, no field matching.


resolveEnclaves(app, enclaveRegistry?) -> EnclaveResolution

Resolve which enclaves an app should use based on access patterns.

Parameters:
ParamTypeDefaultDescription
appobjectrequiredApp definition (from ui.json or ENC_APPS[])
enclaveRegistryRecord<string, EnclaveData>{}Enclave registry

Returns: EnclaveResolution

interface EnclaveResolution {
  primary: Map<string, { enclave: string, event: string }>  // table -> assignment
  crossEnclave: Map<string, { enclave: string, event: string }>
  enclaves: string[]
  wildcard: boolean
}
Behavior:
  1. If app.enclaves === '*', returns all enclaves with wildcard: true.
  2. Analyzes write needs: scans page write blocks for tables.
  3. Analyzes read needs: scans page data bindings for referenced tables.
  4. Classifies each enclave: participant model, reader type, per-event access (canWrite, hasDataviewP).
  5. Scores each enclave:
    • +10 per encrypted write need if enclave has owner/member reader.
    • -5 per encrypted write need if enclave has public reader.
    • +10 per plaintext write need if enclave has DataView P or public reader.
    • +3 bonus if event count matches write count.
    • +1 bonus if event count is within 1 of write count.
  6. Assigns best enclave to all write tables (round-robin event mapping).
  7. Resolves cross-enclave reads:
    • profiles -> Registry.reg_identity.
    • Others: first non-primary enclave with DataView P.
Example:
import { resolveEnclaves } from '@enc-protocol/flow/resolve-enclaves'
 
const result = resolveEnclaves(helloApp, enclaveRegistry)
// {
//   primary: Map { 'messages' => { enclave: 'Hello', event: 'post' } },
//   crossEnclave: Map {},
//   enclaves: ['Hello'],
//   wildcard: false
// }
 
const dmResult = resolveEnclaves(dmApp, enclaveRegistry)
// {
//   primary: Map {
//     'invites' => { enclave: 'DM', event: 'invite' },
//     'messages' => { enclave: 'DM', event: 'message' }
//   },
//   crossEnclave: Map { 'profiles' => { enclave: 'Registry', event: 'reg_identity' } },
//   enclaves: ['DM', 'Registry'],
//   wildcard: false
// }
Edge Cases:
  • Returns Map objects (not plain objects) for primary and crossEnclave.
  • Round-robin event mapping: if there are more write tables than writable events, later tables reuse events from the beginning.
  • If no enclave scores above 0, primary is an empty Map.

10. index.mjs -- Re-exports

Source: sdk-flow/index.mjs

The package entry point re-exports all public APIs.

export { extractSchema } from './schema.mjs'
export { deriveManifest } from './manifest.mjs'
export { match, generate, profileEnclave } from './resolver.mjs'
export { deriveFlow } from './derive-flow.mjs'
export { createSDK } from './sdk-gen.mjs'
export { FLOW_CONFIG } from './flow-config.js'

Not re-exported (import directly):

  • engine.mjs -- profileEnclave, analyzeApp, resolve
  • resolve-enclaves.mjs -- resolveEnclaves
  • ui-analysis.mjs -- deriveUi
  • sdk-gen.mjs -- sdkMethods

11. lib/generic-dataview.js -- GenericDataView Class

Source: lib/generic-dataview.js


new GenericDataView(sql, infra)

Create a DataView instance from an infra.json configuration.

Constructor Parameters:
ParamTypeDescription
sqlobjectSQL executor with exec(sql, ...params) method. Compatible with D1, sql.js, or the in-memory implementation.
infraobjectParsed infra.json content. Must have schema.reads, flow_config.tableMap.
Instance Properties:
PropertyTypeDescription
sqlobjectThe SQL executor
infraobjectThe infra.json object
appIdstringApp identifier from infra.manifest.id or infra.app
tableMapRecord<string, string>UI table -> event type
eventToTableRecord<string, string[]>Reverse: event type -> UI tables
tablesstring[]All table names from infra.schema.reads
Constructor Behavior:
  1. Stores references to sql and infra.
  2. Extracts appId from infra.manifest.id or lowercase infra.app.
  3. Copies tableMap from infra.flow_config.tableMap.
  4. Builds reverse mapping eventToTable.
  5. Calls _initTables() to create SQL tables.
Table Initialization:

For each table in infra.schema.reads:

  • Creates a SQL table with columns from reads[table].fields (all TEXT).
  • Adds _rowid INTEGER PRIMARY KEY AUTOINCREMENT.
  • Adds _event_id TEXT and _timestamp INTEGER.

Also creates stub tables for tableMap keys not in reads.


push(event) -> void

Process a push event from the protocol node.

Parameters:
ParamTypeDescription
eventobjectPush event: { type, from, content, timestamp, id, enclave, seq }
Event Shape:
FieldTypeDescription
typestringEvent type (e.g., 'post'). Maps via eventToTable.
fromstringAuthor public key
contentstring | objectJSON content string or parsed object
timestampnumberUnix timestamp
idstringEvent ID
enclavestringSource enclave name
seqnumberSequence number
Behavior:
  1. Returns immediately if event is falsy or has no type.
  2. Looks up target tables: eventToTable[event.type] or [event.type].
  3. Parses content (JSON string to object if needed).
  4. For each target table: a. Queries PRAGMA table_info for existing columns. b. Merges content fields with from field. c. Adds missing columns via ALTER TABLE. d. Inserts row with _event_id and _timestamp.
  5. Silently catches errors (e.g., missing tables, schema mismatches).
Example:
const dv = new GenericDataView(sql, helloInfra)
dv.push({
  type: 'post',
  from: '79be667ef9dcbbac',
  content: '{"body":"Hello World","from":"79be667ef9dcbbac"}',
  timestamp: 1713800000,
  id: 'evt_001',
  enclave: 'Hello',
  seq: 1,
})
// Row inserted into 'messages' table (and 'my_messages' if mapped)
Edge Cases:
  • If event.content is already an object, uses it directly.
  • If event.content is an unparseable string, uses {}.
  • Columns not in the original schema are added dynamically via ALTER TABLE.
  • If ALTER TABLE fails (column already exists), error is silently caught.
  • If INSERT fails (schema mismatch), error is silently caught.

query(table, opts?) -> object[]

Query a table for rows.

Parameters:
ParamTypeDefaultDescription
tablestringrequiredTable name to query
optsobject{}Query options
opts.limitnumber100Maximum rows to return
opts.orderBystring'_rowid DESC'ORDER BY clause

Returns: Array of row objects. Empty array on error.

Example:
const rows = dv.query('messages')
// [{ _rowid: 2, body: 'World', from: '79be...', _event_id: 'evt_002', _timestamp: 1713800001 },
//  { _rowid: 1, body: 'Hello', from: '79be...', _event_id: 'evt_001', _timestamp: 1713800000 }]
 
const recent = dv.query('messages', { limit: 10, orderBy: '_timestamp DESC' })
Edge Cases:
  • Returns [] if the table does not exist.
  • Returns [] on any SQL error.

getTables() -> string[]

Get all user-facing table names.

Returns: Array of table names (excluding those starting with _).

Example:
dv.getTables()  // ['messages']

handleRequest(path, method) -> Response

Handle an HTTP request. Used by the DataView server and CF Worker.

Parameters:
ParamTypeDescription
pathstringURL path (e.g., '/messages', '/tables')
methodstringHTTP method ('GET', 'POST', 'OPTIONS')

Returns: Response object:

interface Response {
  status: number
  headers: Record<string, string>   // Includes CORS headers
  body: string | null               // JSON string
}
Routing:
MethodPathStatusBody
OPTIONS*204null
GET/tables200{ "tables": [...] }
GET/<table>200{ "<table>": [rows...] }
POST/push--Returns string 'push' (caller handles body)
**404{ "error": "not_found" }
Example:
const res = dv.handleRequest('/messages', 'GET')
// { status: 200, headers: { 'Access-Control-Allow-Origin': '*', ... },
//   body: '{"messages":[...]}' }
 
const cors = dv.handleRequest('/anything', 'OPTIONS')
// { status: 204, headers: { ... }, body: null }

createDataViewHandler(infra) -> CFWorkerHandler

Create a Cloudflare Worker-compatible handler from an infra.json.

Parameters:
ParamTypeDescription
infraobjectParsed infra.json

Returns: Object with async fetch(request, env) method.

Behavior:
  • Lazily initializes GenericDataView on first request (stored on env._dv).
  • POST /push: parses JSON body, supports single event, array, or { events: [...] } wrapper. Returns { ok: true, applied: N }.
  • All other paths: delegates to handleRequest().
Example:
import { createDataViewHandler } from './lib/generic-dataview.js'
 
const handler = createDataViewHandler(helloInfra)
// Use as CF Worker:
// export default handler

12. lib/dataview-server.js -- DataView HTTP Server

Source: lib/dataview-server.js

Standalone Node.js HTTP server for local development. Wraps GenericDataView with an in-memory SQL executor.

Usage

node lib/dataview-server.js <app> <port>
node lib/dataview-server.js hello 8801

In-Memory SQL Executor

The server creates createMemSQL() which implements a minimal SQL subset:

SQL PatternSupport
CREATE TABLE IF NOT EXISTS ...Creates table with columns
ALTER TABLE ... ADD COLUMN ...Adds column dynamically
INSERT INTO ... VALUES ...Appends row with auto-increment _rowid
SELECT * FROM ... ORDER BY ... LIMIT ...Queries with DESC support
PRAGMA table_info(...)Returns column metadata
SELECT ... FROM sqlite_master ...Returns table names

Endpoints

MethodPathResponse
GET/ or /health{ "ok": true, "app": "<appId>", "tables": [...] }
GET/tables{ "tables": [...] }
GET/<table>{ "<table>": [rows...] }
POST/pushAccepts JSON, returns { "ok": true, "applied": N }
OPTIONS*CORS preflight (204)
**404 { "error": "not_found" }

Push Format

POST /push accepts:

  • Single event: { type: 'post', from: '...', content: '...' }
  • Array: [event1, event2, ...]
  • Wrapper: { events: [event1, event2, ...] }

13. lib/workflow-executor.mjs -- Workflow Execution

Source: lib/workflow-executor.mjs


executeWorkflow(opts) -> WorkflowResult

Execute a UI-Kit workflow against a real protocol node.

Parameters:
ParamTypeDefaultDescription
opts.appobjectrequiredApp definition (from ui.json or ENC_APPS[])
opts.stepsobject[]requiredWorkflow steps [{ do, see }]
opts.flowConfigobjectrequiredFlow config for this app
opts.runAlgebraStepfunctionrequired(app, state, doAction) => newState
opts.submitCommitfunctionrequired(eventType, content) => receipt
opts.executeMockfunctionundefined(mockName, mock, state) => commitCount

Returns: WorkflowResult

interface WorkflowResult {
  commits: number                // Total commits submitted
  steps: { step: object, state: object }[]  // Per-step results
  finalState: object             // Final state after all steps
}
Behavior:

For each step:

  1. If step.do names a mock in app.mocks:
    • Calls executeMock(doAction, mock, state) if provided.
    • Adds returned count to commits.
  2. Calls runAlgebraStep(app, state, doAction) to compute new state.
  3. Checks if the current page's write block matches the action trigger:
    • Extracts table from SQL (INSERT INTO <table>).
    • Skips derived tables.
    • Maps table to event type via tableMap.
    • Builds content from current state scalars.
    • Calls submitCommit(eventType, content).
    • Increments commits on success.
  4. Records { step, state } in results.
Example:
import { executeWorkflow } from './lib/workflow-executor.mjs'
 
const result = executeWorkflow({
  app: helloApp,
  steps: helloApp.tests[0],
  flowConfig: FLOW_CONFIG.hello,
  runAlgebraStep: (app, state, action) => runStep(app, state, { do: action }),
  submitCommit: (eventType, content) => {
    return node.processCommit(mkCommit(enclaveId, pub, eventType, content))
  },
  executeMock: (name, mock, state) => {
    return executeMockAsCommits(mock, FLOW_CONFIG.hello.tableMap, submitFn)
  },
})
// { commits: 3, steps: [...], finalState: { ... } }
Edge Cases:
  • Errors in executeMock and submitCommit are silently caught (try/catch).
  • If runAlgebraStep returns falsy, state is unchanged.
  • Write detection uses regex on SQL strings, which may miss complex SQL.
  • Content is built from all scalar values in state, which may include irrelevant fields.

executeMockAsCommits(mock, tableMap, submitFn) -> number

Convert a mock operation to real protocol commit(s).

Parameters:
ParamTypeDefaultDescription
mockobjectrequiredMock definition (see below)
tableMapRecord<string, string>requiredUI table -> event type mapping
submitFnfunctionrequired(eventType, content) => receipt

Returns: Number of commits submitted.

Mock Formats:
// Single insert
{ op: 'insert', table: 'messages', values: { body: 'hello', from: '79be...' } }
 
// Compound operations
{ ops: [
  { op: 'insert', table: 'messages', values: { body: 'hi' } },
  { op: 'set', values: { path: '/feed' } },
] }
 
// State-only operations (produce zero commits)
{ op: 'set', values: { path: '/feed' } }
{ op: 'delete', table: 'contacts', where: { pub: 'abc' } }
{ op: 'update', table: 'users', where: { pub: 'abc' }, set: { role: 'admin' } }
Behavior:
  1. If mock.ops exists, recursively process each operation.
  2. If mock.op === 'insert' and mock.table is set:
    • Map table to event type: tableMap[mock.table] or mock.table.replace(/s$/, '').
    • Call submitFn(eventType, mock.values).
    • Increment count on success.
  3. For set, delete, update ops: return 0 (state-only, no commits).
Example:
import { executeMockAsCommits } from './lib/workflow-executor.mjs'
 
const count = executeMockAsCommits(
  { op: 'insert', table: 'messages', values: { body: 'hello' } },
  { messages: 'post' },
  (eventType, content) => node.processCommit(mkCommit(eid, pub, eventType, content))
)
// count: 1 (submitted 'post' event with { body: 'hello' })
 
const count2 = executeMockAsCommits(
  { ops: [
    { op: 'insert', table: 'messages', values: { body: 'msg1' } },
    { op: 'insert', table: 'messages', values: { body: 'msg2' } },
    { op: 'set', values: { path: '/feed' } },
  ] },
  { messages: 'post' },
  submitFn
)
// count2: 2 (two inserts, set is state-only)
Edge Cases:
  • If submitFn throws, the error is caught and the count is not incremented.
  • If submitFn returns an object with an error property, the count is not incremented.
  • Table name singularization fallback: messages -> message.
  • Compound ops are processed sequentially; errors in one do not stop others.