generated from interop-alliance/isomorphic-lib-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add validation for exported ActivityPub tarballs
- Loading branch information
Showing
8 changed files
with
169 additions
and
2 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as tar from 'tar-stream' | ||
import { Readable } from 'stream' | ||
import YAML from 'yaml' | ||
|
||
/** | ||
* Validates the structure and content of an exported ActivityPub tarball. | ||
* @param tarBuffer - A Buffer containing the .tar archive. | ||
* @returns A promise that resolves to an object with `valid` (boolean) and `errors` (string[]). | ||
*/ | ||
export async function validateExportStream( | ||
tarBuffer: Buffer | ||
): Promise<{ valid: boolean; errors: string[] }> { | ||
const extract = tar.extract() | ||
const errors: string[] = [] | ||
const requiredFiles = [ | ||
'manifest.yaml', | ||
'activitypub/actor.json', | ||
'activitypub/outbox.json' | ||
] | ||
const foundFiles = new Set() | ||
|
||
return await new Promise((resolve) => { | ||
extract.on('entry', (header, stream, next) => { | ||
const fileName = header.name | ||
foundFiles.add(fileName) | ||
|
||
let content = '' | ||
stream.on('data', (chunk) => { | ||
content += chunk.toString() | ||
}) | ||
|
||
stream.on('end', () => { | ||
try { | ||
// Validate JSON files | ||
if (fileName.endsWith('.json')) { | ||
JSON.parse(content) // Throws an error if content is not valid JSON | ||
} | ||
|
||
// Validate manifest file | ||
if (fileName === 'manifest.yaml') { | ||
const manifest = YAML.parse(content) | ||
if (!manifest['ubc-version']) { | ||
errors.push('Manifest is missing required field: ubc-version') | ||
} | ||
if (!manifest.contents?.activitypub) { | ||
errors.push( | ||
'Manifest is missing required field: contents.activitypub' | ||
) | ||
} | ||
} | ||
} catch (error: any) { | ||
errors.push(`Error processing file ${fileName}: ${error.message}`) | ||
} | ||
next() | ||
}) | ||
|
||
stream.on('error', (error) => { | ||
errors.push(`Stream error on file ${fileName}: ${error.message}`) | ||
next() | ||
}) | ||
}) | ||
|
||
extract.on('finish', () => { | ||
// Check if all required files are present | ||
for (const file of requiredFiles) { | ||
if (!foundFiles.has(file)) { | ||
errors.push(`Missing required file: ${file}`) | ||
} | ||
} | ||
|
||
resolve({ | ||
valid: errors.length === 0, | ||
errors | ||
}) | ||
}) | ||
|
||
extract.on('error', (error) => { | ||
errors.push(`Error during extraction: ${error.message}`) | ||
resolve({ | ||
valid: false, | ||
errors | ||
}) | ||
}) | ||
|
||
// Convert Buffer to a Readable stream and pipe it to the extractor | ||
const stream = Readable.from(tarBuffer) | ||
stream.pipe(extract) | ||
}) | ||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { expect } from 'chai' | ||
import { readFileSync } from 'fs' | ||
import { validateExportStream } from '../src/verify' | ||
|
||
describe('validateExportStream', () => { | ||
it('should validate a valid tarball', async () => { | ||
// Load a valid tarball (e.g., exported-profile-valid.tar) | ||
const tarBuffer = readFileSync( | ||
'test/fixtures/tarball-samples/valid-export.tar' | ||
) | ||
const result = await validateExportStream(tarBuffer) | ||
|
||
expect(result.valid).to.be.true | ||
expect(result.errors).to.be.an('array').that.is.empty | ||
}) | ||
|
||
it('should fail if manifest.yaml is missing', async () => { | ||
// Load a tarball with missing manifest.yaml | ||
const tarBuffer = readFileSync( | ||
'test/fixtures/tarball-samples/missing-manifest.tar' | ||
) | ||
const result = await validateExportStream(tarBuffer) | ||
|
||
expect(result.valid).to.be.false | ||
}) | ||
|
||
it('should fail if actor.json is missing', async () => { | ||
// Load a tarball with missing actor.json | ||
const tarBuffer = readFileSync( | ||
'test/fixtures/tarball-samples/missing-actor.tar' | ||
) | ||
const result = await validateExportStream(tarBuffer) | ||
|
||
expect(result.valid).to.be.false | ||
console.log(JSON.stringify(result.errors)) | ||
}) | ||
|
||
// it('should fail if outbox.json is missing', async () => { | ||
// // Load a tarball with missing outbox.json | ||
// const tarBuffer = readFileSync( | ||
// 'test/fixtures/exported-profile-missing-outbox.tar' | ||
// ) | ||
// const result = await validateExportStream(tarBuffer) | ||
|
||
// expect(result.valid).to.be.false | ||
// expect(result.errors).to.include( | ||
// 'Missing required file: activitypub/outbox.json' | ||
// ) | ||
// }) | ||
|
||
// it('should fail if actor.json contains invalid JSON', async () => { | ||
// // Load a tarball with invalid JSON in actor.json | ||
// const tarBuffer = readFileSync( | ||
// 'test/fixtures/exported-profile-invalid-actor-json.tar' | ||
// ) | ||
// const result = await validateExportStream(tarBuffer) | ||
|
||
// expect(result.valid).to.be.false | ||
// expect(result.errors).to.include( | ||
// 'Error processing file activitypub/actor.json: Unexpected token } in JSON at position 42' | ||
// ) | ||
// }) | ||
|
||
// it('should fail if manifest.yaml is invalid', async () => { | ||
// // Load a tarball with invalid manifest.yaml | ||
// const tarBuffer = readFileSync( | ||
// 'test/fixtures/exported-profile-invalid-manifest.tar' | ||
// ) | ||
// const result = await validateExportStream(tarBuffer) | ||
|
||
// expect(result.valid).to.be.false | ||
// expect(result.errors).to.include( | ||
// 'Manifest is missing required field: ubc-version' | ||
// ) | ||
// }) | ||
}) |