Initial Node.js library and CLI
This commit is contained in:
commit
dd74209ed1
14 files changed
+1343
No files matched your search
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync, spawnSync } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const pkg = require('../package.json');
|
||||
|
||||
const cli = path.join(__dirname, '..', 'src', 'cli.js');
|
||||
|
||||
test('CLI prints concise help', () => {
|
||||
const output = execFileSync(process.execPath, [cli, '--help'], { encoding: 'utf8' });
|
||||
assert.match(output, /crypstie create \[file\|-\]/);
|
||||
assert.match(output, /crypstie open <url>/);
|
||||
});
|
||||
|
||||
test('CLI prints package version', () => {
|
||||
assert.equal(execFileSync(process.execPath, [cli, '--version'], { encoding: 'utf8' }), `${pkg.version}\n`);
|
||||
});
|
||||
|
||||
test('CLI rejects unknown commands without a stack trace', () => {
|
||||
const result = spawnSync(process.execPath, [cli, 'oops'], { encoding: 'utf8' });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /^crypstie: Unknown command/);
|
||||
assert.equal(result.stderr.includes(' at '), false);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { Boss, SymmetricKey } = require('unicrypto');
|
||||
const {
|
||||
createCrypstie,
|
||||
decryptCrypstiePayload,
|
||||
encodeCrypstieKey,
|
||||
initializeCrypto,
|
||||
openCrypstie,
|
||||
parseCrypstieUrl,
|
||||
} = require('../src');
|
||||
|
||||
const KEY_BYTES = Uint8Array.from({ length: 32 }, (_, index) => index + 1);
|
||||
const KEY_TEXT = Buffer.from(KEY_BYTES)
|
||||
.toString('base64')
|
||||
.replaceAll('+', '.')
|
||||
.replaceAll('/', '_')
|
||||
.replace(/=+$/, '');
|
||||
|
||||
test('parses the legacy Crypstie URL format', () => {
|
||||
const parsed = parseCrypstieUrl(`https://crypstie.com/_sample#${KEY_TEXT}`);
|
||||
assert.equal(parsed.id, 'sample');
|
||||
assert.deepEqual(parsed.key, KEY_BYTES);
|
||||
});
|
||||
|
||||
test('encodes and decodes legacy Crypstie keys', () => {
|
||||
assert.deepEqual(parseCrypstieUrl(`https://crypstie.com/_x#${encodeCrypstieKey(KEY_BYTES)}`).key, KEY_BYTES);
|
||||
});
|
||||
|
||||
test('rejects a link without its fragment key', () => {
|
||||
assert.throws(
|
||||
() => parseCrypstieUrl('https://crypstie.com/_sample'),
|
||||
/no decryption key/,
|
||||
);
|
||||
});
|
||||
|
||||
test('decrypts a BOSS payload produced in the Crypstie wire format', async () => {
|
||||
await initializeCrypto();
|
||||
const key = new SymmetricKey({ keyBytes: KEY_BYTES });
|
||||
const deleteAt = new Date('2030-01-02T03:04:05Z');
|
||||
const packed = Boss.dump({
|
||||
guid: 'sample',
|
||||
encryptedData: key.etaEncryptSync(new TextEncoder().encode('совместимость работает')),
|
||||
deleteAt,
|
||||
burnOnShow: false,
|
||||
showWide: true,
|
||||
syntaxHighlighting: 'text',
|
||||
editable: false,
|
||||
ownerHandle: null,
|
||||
});
|
||||
|
||||
const result = await decryptCrypstiePayload(packed, KEY_BYTES);
|
||||
assert.equal(result.text, 'совместимость работает');
|
||||
assert.equal(result.id, 'sample');
|
||||
assert.equal(result.deleteAt.toISOString(), deleteAt.toISOString());
|
||||
assert.equal(result.burnOnShow, false);
|
||||
assert.equal(result.showWide, true);
|
||||
});
|
||||
|
||||
test('downloads the encrypted payload without sending the fragment key', async () => {
|
||||
await initializeCrypto();
|
||||
const key = new SymmetricKey({ keyBytes: KEY_BYTES });
|
||||
const packed = Boss.dump({
|
||||
guid: 'sample',
|
||||
encryptedData: key.etaEncryptSync(new TextEncoder().encode('secret')),
|
||||
deleteAt: new Date('2030-01-02T03:04:05Z'),
|
||||
burnOnShow: false,
|
||||
showWide: false,
|
||||
editable: false,
|
||||
});
|
||||
let requested;
|
||||
const fakeFetch = async (url) => {
|
||||
requested = url.href;
|
||||
return new Response(packed, { status: 200 });
|
||||
};
|
||||
|
||||
const result = await openCrypstie(
|
||||
`https://crypstie.com/_sample#${KEY_TEXT}`,
|
||||
{ fetch: fakeFetch },
|
||||
);
|
||||
|
||||
assert.equal(requested, 'https://crypstie.com/api/crypstie/sample');
|
||||
assert.equal(requested.includes(KEY_TEXT), false);
|
||||
assert.equal(result.text, 'secret');
|
||||
});
|
||||
|
||||
test('creates a server-compatible encrypted BOSS payload', async () => {
|
||||
let request;
|
||||
const fakeFetch = async (url, options) => {
|
||||
request = { url: url.href, options };
|
||||
return new Response('new_id', { status: 200 });
|
||||
};
|
||||
const deleteAt = new Date(Date.now() + 60_000);
|
||||
const result = await createCrypstie('created by node', {
|
||||
key: KEY_BYTES,
|
||||
deleteAt,
|
||||
rootUrl: 'https://example.test/base',
|
||||
burnOnShow: true,
|
||||
fetch: fakeFetch,
|
||||
});
|
||||
|
||||
assert.equal(request.url, 'https://example.test/api/crypstie');
|
||||
assert.equal(request.options.method, 'POST');
|
||||
assert.equal(result.url, `https://example.test/_new_id#${KEY_TEXT}`);
|
||||
assert.equal(result.burnOnShow, true);
|
||||
|
||||
const uploaded = Boss.load(new Uint8Array(await request.options.body.get('data').arrayBuffer()));
|
||||
assert.equal(uploaded.burnOnShow, true);
|
||||
assert.equal(
|
||||
uploaded.deleteAt.toISOString(),
|
||||
new Date(Math.floor(deleteAt.getTime() / 1000) * 1000).toISOString(),
|
||||
);
|
||||
assert.equal(
|
||||
new TextDecoder().decode(new SymmetricKey({ keyBytes: KEY_BYTES }).etaDecryptSync(uploaded.encryptedData)),
|
||||
'created by node',
|
||||
);
|
||||
});
|
||||
|
||||
test('reports authentication failure for a wrong key', async () => {
|
||||
await initializeCrypto();
|
||||
const key = new SymmetricKey({ keyBytes: KEY_BYTES });
|
||||
const packed = Boss.dump({ encryptedData: key.etaEncryptSync(new TextEncoder().encode('secret')) });
|
||||
const wrongKey = Uint8Array.from(KEY_BYTES, (byte) => byte ^ 0xff);
|
||||
await assert.rejects(() => decryptCrypstiePayload(packed, wrongKey), /authenticate or decrypt/);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { createCrypstie, openCrypstie } = require('../src');
|
||||
|
||||
test(
|
||||
'opens a reusable Crypstie from the deployed service',
|
||||
{ skip: !process.env.CRYPSTIE_TEST_URL },
|
||||
async () => {
|
||||
const result = await openCrypstie(process.env.CRYPSTIE_TEST_URL);
|
||||
assert.match(result.text, /^Crypstie Node compatibility fixture /);
|
||||
assert.equal(result.burnOnShow, false);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'creates and reopens a reusable Crypstie on the deployed service',
|
||||
{ skip: process.env.CRYPSTIE_LIVE_WRITE !== '1' },
|
||||
async () => {
|
||||
const marker = `Crypstie Node compatibility fixture ${new Date().toISOString()}`;
|
||||
const created = await createCrypstie(marker);
|
||||
const opened = await openCrypstie(created.url);
|
||||
assert.equal(opened.text, marker);
|
||||
assert.equal(opened.burnOnShow, false);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user