Notary Express
An Express service that publishes a digest of a submitted document and finds the same digest again on a later submission, with the timestamp of the first record.
This service answers one question: has this exact content been recorded here before, and if so, when. It publishes a digest of what you submit rather than the content itself, so the record proves the content existed at that time without revealing it.
The name is the application's, not a capability claim. What the service gives you is a tamper-evident, independently checkable record of a digest and its time. Whether that has any legal weight in your setting is a question for your counsel, and nothing here answers it.
What it does
Submit a string through a form. The service computes its SHA-256 digest, looks for a record of that digest in the project, and either returns the earlier record with its timestamp or publishes a new one.
The program
const express = require('express');
const { Mintblue, utils } = require('@mintblue/sdk');
const app = express();
const port = 3000;
const projectId = process.env.MINTBLUE_PROJECT_ID;
let client;
app.use(express.urlencoded({ extended: false }));
async function records() {
const transactions = await client.listTransactions({
project_id: projectId,
order: 'asc',
});
const found = [];
for (const summary of transactions) {
const full = await client.getTransaction({
txid: summary.txid,
parse: true,
});
const record = full.outputs.find((output) => output.type === 'data');
if (record && typeof record.value === 'string') {
found.push({
txid: summary.txid,
digest: record.value,
recordedAt: summary.published_at,
});
}
}
return found;
}
app.get('/', (request, response) => {
response.send(`
<form method="POST" action="/">
<input type="text" name="data" placeholder="content">
<input type="submit">
</form>
`);
});
app.post('/', async (request, response) => {
const digest = await utils.sha256s(request.body.data);
const match = (await records()).find((entry) => entry.digest === digest);
if (match) {
response.send(
`found<br>digest ${digest}<br>transaction ${match.txid}` +
`<br>recorded ${match.recordedAt}`,
);
return;
}
const { txid } = await client.createTransaction({
project_id: projectId,
outputs: [{ type: 'data', value: digest, sign: true }],
});
response.send(`recorded<br>digest ${digest}<br>transaction ${txid}`);
});
app.listen(port, async () => {
client = await Mintblue.create({ token: process.env.MINTBLUE_SDK_TOKEN });
console.log(`listening on http://localhost:${port}`);
});Install its two dependencies and start it:
npm install @mintblue/sdk express
MINTBLUE_SDK_TOKEN=... MINTBLUE_PROJECT_ID=... node notary.jsSubmit mydocument once and the service records the digest and reports the new
transaction id. Submit it again, once that transaction has been published, and
the service reports found with the same transaction id and the time the record
was published, because the digest matched something already in the project.
The three decisions worth copying
utils.sha256s rather than a hand-rolled digest. It ships with the SDK and
returns a hex string, which is directly comparable and directly storable. Its
sibling utils.sha256 returns the bytes if you would rather handle those.
sign: true and no encryption. A digest reveals nothing about the content
that produced it, so there is nothing here to hide, and a record nobody else can
read is useless for a check anybody should be able to make. The signature is
still worth having: it says which account recorded the digest.
The timestamp comes from the transaction, not from the value. published_at
is a property of the record. A timestamp written into the value would be a claim
by the same party making the claim, which is exactly the thing the record exists
to avoid.
What it is not
Production-shaped. It walks the whole project on every request, so response time
grows with history, and it has no authentication in front of it. Two changes fix
the first problem: keep an index of digests you have already recorded, or use
transactionsAfterTxid to follow the project forward from the last one you saw
rather than re-reading it.
It also digests a form field rather than a file. Digesting a file is the same
call with the file's bytes, and the hash output type does the digest for you
at submission time. See hash and
Store a file.