Compare commits

...
Author SHA1 Message Date
Hanzo Dev d2b7f771c6 ci(publish.yml): neutralize — native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror 2026-07-24 15:21:51 -07:00
Hanzo Dev 9e2e536a67 ci(docker-deploy.yml): neutralize — native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror 2026-07-24 15:21:45 -07:00
Hanzo Dev 0762627a76 ci(deploy.yml): neutralize — native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror 2026-07-24 15:21:39 -07:00
Hanzo Dev 29e5de1082 ci(deploy): native Hanzo pipeline — BuildKit → ghcr.io/hanzoai/sign:<sha> → operator patch (app: sign) 2026-07-24 15:21:13 -07:00
315a3fe6a5 fix(sign/goja): enforce SEQUENTIAL signing order + route PDFs through the blob seam (RED M7/M8) (#9)
M7 — SEQUENTIAL was declared but never enforced: documentsSend marked all
recipients SENT and signField/signComplete let anyone sign in any order. Add
assertTurn(doc,r): in SEQUENTIAL mode a recipient may only act once every
signer/approver ahead of them (listRecipients order: signing_order ASC,
created_at ASC) has SIGNED. Gated in signField and signComplete. PARALLEL
unchanged.

M8 — PDF bytes no longer live inline in the per-tenant SQLite (document_data.data
held 32 MiB base64, bloating the tenant DB and copied on every read). They now go
through the gojabase __blob object-storage seam (same VFS/S3 plane dataroom uses):
- documentsCreate: __blob.put(original) → store the blob KEY (type 'BLOB_KEY').
- maybeSeal: read original via __blob.get, seal, __blob.put(sealed) to a new key,
  point document_data.data at it (initial_data keeps the original key).
- signView/documentsDownload: __blob.get(dd.data) to return pdfBase64.
Keys are tenant-scoped by the host; the bundle handles base64 only.

Consumed by hanzoai/cloud clients/sign (RED consolidation review #117 M7/M8);
gojabase gains a Config.Blob seam + __blob global, sign leaf passes deps.VFS.

Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-10 14:49:50 -07:00
5 changed files with 73 additions and 221 deletions
+5 -21
View File
@@ -1,24 +1,8 @@
name: Deploy to Production
name: deploy (moved to native pipeline)
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
deploy:
runs-on: hanzo-deploy-linux-amd64
notice:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GH_TOKEN }}
- name: Push to release branch
run: |
git checkout release || git checkout -b release
git merge --ff-only main
git push origin release
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
+5 -17
View File
@@ -1,20 +1,8 @@
name: Docker
name: Docker (moved to native pipeline)
on:
workflow_dispatch:
push:
branches: [main, dev, test]
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
docker:
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
with:
image: ghcr.io/hanzoai/esign
dockerfile: docker/Dockerfile
# arm64 builds run on canonical native arcd labels — matches
# spark (self-hosted + linux + arm64). Org isolation comes from
# GitHub runner group + arcd's repo allowlist.
runner-arm64: self-hosted
secrets: inherit
notice:
runs-on: ubuntu-latest
steps:
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
+4 -178
View File
@@ -1,182 +1,8 @@
name: Publish Docker
name: Publish Docker (moved to native pipeline)
on:
push:
branches: ['release']
workflow_dispatch:
inputs:
tag:
description: 'Git tag to build and publish (e.g., v1.0.0)'
required: true
type: string
jobs:
build_and_publish_platform_containers:
name: Build and publish platform containers
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- warp-ubuntu-latest-x64-4x
- warp-ubuntu-latest-arm64-4x
notice:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
fetch-tags: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_TOKEN }}
- name: Build the docker image
env:
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
NEXT_PRIVATE_TELEMETRY_KEY: ${{ secrets.NEXT_PRIVATE_TELEMETRY_KEY }}
NEXT_PRIVATE_TELEMETRY_HOST: ${{ secrets.NEXT_PRIVATE_TELEMETRY_HOST }}
APP_VERSION: ${{ inputs.tag || '' }}
run: |
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
fi
GIT_SHA="$(git rev-parse HEAD)"
docker build \
-f ./docker/Dockerfile \
--progress=plain \
--build-arg NEXT_PRIVATE_TELEMETRY_KEY="${NEXT_PRIVATE_TELEMETRY_KEY:-}" \
--build-arg NEXT_PRIVATE_TELEMETRY_HOST="${NEXT_PRIVATE_TELEMETRY_HOST:-}" \
-t "hanzo-sign/hanzo-sign-$BUILD_PLATFORM:latest" \
-t "hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$GIT_SHA" \
-t "hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$APP_VERSION" \
-t "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM:latest" \
-t "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$GIT_SHA" \
-t "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM:$APP_VERSION" \
.
- name: Push the docker image to DockerHub
run: docker push --all-tags "hanzo-sign/hanzo-sign-$BUILD_PLATFORM"
env:
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
- name: Push the docker image to GitHub Container Registry
run: docker push --all-tags "ghcr.io/hanzo-sign/hanzo-sign-$BUILD_PLATFORM"
env:
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
create_and_publish_manifest:
name: Create and publish manifest
runs-on: hanzo-build-linux-amd64
needs: build_and_publish_platform_containers
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
fetch-tags: true
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_TOKEN }}
- name: Create and push DockerHub manifest
env:
APP_VERSION: ${{ inputs.tag || '' }}
run: |
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
fi
GIT_SHA="$(git rev-parse HEAD)"
# Check if the version is stable (no rc or beta in the version)
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
docker manifest create \
hanzo-sign/hanzo-sign:latest \
--amend hanzo-sign/hanzo-sign-amd64:latest \
--amend hanzo-sign/hanzo-sign-arm64:latest
docker manifest push hanzo-sign/hanzo-sign:latest
fi
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
docker manifest create \
hanzo-sign/hanzo-sign:rc \
--amend hanzo-sign/hanzo-sign-amd64:rc \
--amend hanzo-sign/hanzo-sign-arm64:rc
docker manifest push hanzo-sign/hanzo-sign:rc
fi
docker manifest create \
hanzo-sign/hanzo-sign:$GIT_SHA \
--amend hanzo-sign/hanzo-sign-amd64:$GIT_SHA \
--amend hanzo-sign/hanzo-sign-arm64:$GIT_SHA
docker manifest create \
hanzo-sign/hanzo-sign:$APP_VERSION \
--amend hanzo-sign/hanzo-sign-amd64:$APP_VERSION \
--amend hanzo-sign/hanzo-sign-arm64:$APP_VERSION
docker manifest push hanzo-sign/hanzo-sign:$GIT_SHA
docker manifest push hanzo-sign/hanzo-sign:$APP_VERSION
- name: Create and push Github Container Registry manifest
env:
APP_VERSION: ${{ inputs.tag || '' }}
run: |
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
fi
GIT_SHA="$(git rev-parse HEAD)"
# Check if the version is stable (no rc or beta in the version)
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
docker manifest create \
ghcr.io/hanzo-sign/hanzo-sign:latest \
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:latest \
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:latest
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:latest
fi
if [[ "$APP_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
docker manifest create \
ghcr.io/hanzo-sign/hanzo-sign:rc \
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:rc \
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:rc
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:rc
fi
docker manifest create \
ghcr.io/hanzo-sign/hanzo-sign:$GIT_SHA \
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:$GIT_SHA \
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:$GIT_SHA
docker manifest create \
ghcr.io/hanzo-sign/hanzo-sign:$APP_VERSION \
--amend ghcr.io/hanzo-sign/hanzo-sign-amd64:$APP_VERSION \
--amend ghcr.io/hanzo-sign/hanzo-sign-arm64:$APP_VERSION
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:$GIT_SHA
docker manifest push ghcr.io/hanzo-sign/hanzo-sign:$APP_VERSION
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
+24
View File
@@ -0,0 +1,24 @@
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: hanzo-linux-amd64
steps:
- uses: actions/checkout@v4
- name: Build + push image
run: |
SHA="${GITHUB_SHA::8}"
buildctl-daemonless.sh build --frontend=dockerfile.v0 \
--opt context="${{ github.server_url }}/${{ github.repository }}.git#${GITHUB_SHA}" \
--opt filename=docker/Dockerfile --opt platform=linux/amd64 \
--secret id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN \
--output "type=image,name=ghcr.io/hanzoai/sign:${SHA},push=true" --progress=plain
env:
GIT_AUTH_TOKEN: ${{ secrets.GIT_CLONE_TOKEN }}
- name: Deploy — declare tag to operator
run: |
for app in sign; do
kubectl -n hanzo patch app "$app" --type=merge -p "{\"spec\":{\"image\":{\"repository\":\"ghcr.io/hanzoai/sign\",\"tag\":\"${GITHUB_SHA::8}\"}}}"
done
+35 -5
View File
@@ -106,6 +106,22 @@
}
function signatureForField(fieldId) { return firstRow(query('SELECT * FROM signatures WHERE field_id = ?', [fieldId])); }
// assertTurn enforces SEQUENTIAL signing order: a recipient may only act once
// EVERY signer/approver ahead of them (lower signing_order, then created_at — the
// listRecipients order) has SIGNED. PARALLEL documents impose no order. Called by
// signField and signComplete so a later party cannot sign before earlier ones.
function assertTurn(doc, r) {
if (doc.signing_order !== 'SEQUENTIAL') return;
var recipients = listRecipients(doc.id); // ORDER BY signing_order ASC, created_at ASC
for (var i = 0; i < recipients.length; i++) {
var o = recipients[i];
if (o.id === r.id) return; // reached this recipient — everyone ahead has signed
if (SIGNING_ROLES[o.role] && o.signing_status !== SIGNING.SIGNED) {
throw new HttpError(403, 'waiting for an earlier signer (sequential signing order)');
}
}
}
function documentView(doc) {
var recipients = listRecipients(doc.id).map(function (r) {
return {
@@ -143,8 +159,15 @@
var t = now();
var dataId = id();
// PDF bytes live on the object-storage seam (__blob), NOT inline in the
// per-tenant SQLite — a 32 MiB base64 PDF in a TEXT column would bloat the
// tenant DB and be re-copied on every read. document_data.data holds the opaque
// blob KEY; initial_data keeps the ORIGINAL key so the pre-seal PDF is never
// overwritten. __blob is tenant-scoped by the host, so the key is local.
var initialKey = 'doc/' + dataId + '/initial';
__blob.put(initialKey, pdf);
exec('INSERT INTO document_data (id, type, data, initial_data) VALUES (?,?,?,?)',
[dataId, 'BYTES_64', pdf, pdf]);
[dataId, 'BLOB_KEY', initialKey, initialKey]);
var docId = id();
exec(
@@ -249,7 +272,7 @@
return { status: 200, body: {
id: doc.id, status: doc.status, sealed: doc.status === STATUS.COMPLETED,
filename: doc.title + (doc.status === STATUS.COMPLETED ? '_signed.pdf' : '.pdf'),
pdfBase64: dd.data,
pdfBase64: __blob.get(dd.data), // dd.data is the blob key; fetch the bytes
} };
}
@@ -292,7 +315,7 @@
document: { id: doc.id, title: doc.title, status: doc.status },
recipient: { id: r.id, email: r.email, name: r.name, role: r.role, signingStatus: r.signing_status },
fields: myFields,
pdfBase64: dd ? dd.data : null,
pdfBase64: dd ? __blob.get(dd.data) : null, // dd.data is the blob key
} };
}
@@ -303,6 +326,7 @@
if (doc.status !== STATUS.PENDING) conflict('document is not pending signature');
if (r.signing_status === SIGNING.SIGNED) conflict('recipient already completed');
if (r.signing_status === SIGNING.REJECTED) conflict('recipient already rejected');
assertTurn(doc, r); // SEQUENTIAL: refuse until earlier signers have signed
var field = firstRow(query('SELECT * FROM fields WHERE id = ? AND document_id = ?', [req.params.fieldId, doc.id]));
if (!field) notFound('field not found');
@@ -363,6 +387,7 @@
var r = rd.recipient, doc = rd.doc;
if (doc.status !== STATUS.PENDING) conflict('document is not pending signature');
if (r.signing_status === SIGNING.SIGNED) conflict('recipient already completed');
assertTurn(doc, r); // SEQUENTIAL: refuse until earlier signers have signed
var pending = firstRow(query('SELECT COUNT(*) AS c FROM fields WHERE document_id = ? AND recipient_id = ? AND inserted = 0', [doc.id, r.id]));
if (pending && Number(pending.c) > 0) bad('recipient has ' + pending.c + ' unsigned field(s)');
@@ -407,11 +432,16 @@
}
}
var stamped = __pdf.stamp(dd.data, JSON.stringify(stamps));
var original = __blob.get(dd.data); // dd.data is the (initial) blob key
var stamped = __pdf.stamp(original, JSON.stringify(stamps));
var signed = __pdf.sign(stamped);
var t = now();
exec('UPDATE document_data SET data = ? WHERE id = ?', [signed, dd.id]);
// Persist the sealed PDF to a NEW blob key and point document_data.data at it;
// initial_data still references the original key, so both survive on the seam.
var sealedKey = 'doc/' + dd.id + '/sealed';
__blob.put(sealedKey, signed);
exec('UPDATE document_data SET data = ? WHERE id = ?', [sealedKey, dd.id]);
exec('UPDATE documents SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?', [STATUS.COMPLETED, t, t, doc.id]);
audit(doc.id, AUDIT.DOCUMENT_COMPLETED, { transactionId: id() }, actor);
return true;