@octaviaflow/oflow-templating
v0.0.1
Published
Flow ↔ .oflow mapping: export a workflow to a portable .oflow (credentials split into the encrypted secrets) and import one back (ids remapped, secrets re-installed). App-agnostic.
Readme
@octaviaflow/oflow-templating
Flow ↔ .oflow. Turns a live OctaviaFlow workflow into a portable, signed,
encrypted .oflow (credentials split out into the encrypted secrets) and
rebuilds it on import (ids regenerated, refs rewired, creds re-installed).
Built on @octaviaflow/oflow. App-agnostic — the only
app-specific bits are two callbacks you provide.
Status: v0. Depends on
@octaviaflow/oflowv0 (which itself needs a security review before production). The graph shape is permissive but tuned to the OctaviaFlow{ id, name, steps, connections }workflow model.
Export
import { exportFlow, generateSigningKeyPair, generateRecipientKeyPair } from '@octaviaflow/oflow-templating';
const bytes = await exportFlow(flow, {
manifest: { name: flow.name, version: '1.0.0', author: 'octaviaflow' },
sign: signingKey.privateKey, // provenance
keyWrap: { type: 'x25519', recipients: [recipientPubKey] }, // who may open it
// YOU provide: pull the real creds for the connector refs the package found.
resolveSecrets: async (refs) => {
const out: Record<string, unknown> = {};
for (const id of refs) out[id] = await db.getConnectorCredentials(id);
return out;
},
});
await Bun.write('my-flow.oflow', bytes);exportFlow scans the flow for connector references (default keys:
connectorId, connectorInstanceId, connectionId — overridable), calls your
resolveSecrets, strips instance/runtime fields (organizationId, _id,
status, …), then signs + encrypts via the core.
Import
import { importFlow } from '@octaviaflow/oflow-templating';
const { flow, manifest, connectorIdMap, stepIdMap } = await importFlow(bytes, {
unwrap: { type: 'x25519', privateKey: recipientPrivKey },
trustedSigners: [signingKey.publicKey], // reject unknown signers
// YOU provide: re-create connectors from the secrets, return old -> new ref map.
installSecrets: async (secrets) => {
const map: Record<string, string> = {};
for (const [oldId, cred] of Object.entries(secrets)) {
map[oldId] = await db.createConnectorInstance(cred); // new id
}
return map;
},
});
// `flow` has fresh step/flow ids, connections + parentId rewired, and connector
// refs pointing at the NEW connector instances. Save it as a new workflow.What the package owns vs. what you own
| Owns (pure, app-agnostic) | You provide (data access) |
|--------------------------------------------|----------------------------------|
| scan flow → connector refs | resolveSecrets(refs) |
| strip runtime/env fields | installSecrets(secrets) → map |
| sign + encrypt / verify + decrypt (core) | |
| regenerate ids; rewire connections/parentId| |
| rewrite connector refs old → new | |
Known v0 limitations
- Data-ref tokens inside step config strings (e.g.
{{stepId.path}}) are not remapped to the new step ids yet — onlyconnections,parentId, and the configured connector-ref keys are. If your flows use cross-step token refs, add a token remap pass (or passregenerateIds: falseand remap in the app). - No schema version migration between flow versions yet.
