Skip to content

Commit 58defa2

Browse files
authored
feat: Add setIntent method (#23)
* feat: Add Builder.setIntent * chore: update c2pa version * update types as well * feat: add tests * chore: add changeset
1 parent 595304e commit 58defa2

File tree

20 files changed

+348
-153
lines changed

20 files changed

+348
-153
lines changed

.changeset/breezy-pots-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@contentauth/c2pa-node": patch
3+
---
4+
5+
Add setIntent

Cargo.lock

Lines changed: 16 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ crate-type = ["cdylib"]
1111
[dependencies]
1212
async-trait = "0.1.77"
1313
ciborium = "0.2.2"
14-
c2pa = { version = "0.64", default-features = false, features = ["file_io", "pdf", "fetch_remote_manifests", "add_thumbnails", "rust_native_crypto"] }
14+
c2pa = { version = "0.67", default-features = false, features = ["file_io", "pdf", "fetch_remote_manifests", "add_thumbnails", "rust_native_crypto"] }
1515
futures = "0.3"
1616
image = "0.25.6"
1717
neon = { version = "1.0.0", default-features = false, features = [

js-src/Builder.spec.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,5 +469,173 @@ describe("Builder", () => {
469469
expect(JSON.stringify(manifestStore)).toContain("Test Ingredient");
470470
expect(JSON.stringify(manifestStore)).toContain("thumbnail.ingredient");
471471
});
472+
473+
it("should perform redaction workflow like test_redaction_async", async () => {
474+
// This test mirrors the Rust test_redaction_async test
475+
476+
// Create a reader to get the parent manifest label from the existing source
477+
const reader = await Reader.fromAsset(source);
478+
const parentManifestLabel = reader.activeLabel();
479+
expect(parentManifestLabel).toBeDefined();
480+
481+
// Create a redacted URI for the assertion we are going to redact
482+
// Using a common assertion label that might exist
483+
const assertionLabel = "stds.schema-org.CreativeWork";
484+
const redactedUri = `contentauth:urn:uuid:${parentManifestLabel}/c2pa.assertions/${assertionLabel}`;
485+
486+
// Create a builder with edit intent and redactions
487+
const redactionManifestDefinition = {
488+
claim_generator: "test-generator",
489+
claim_generator_info: [
490+
{
491+
name: "c2pa_test",
492+
version: "1.0.0",
493+
},
494+
],
495+
title: "Test_Redaction_Manifest",
496+
format: "image/jpeg",
497+
instance_id: "1234",
498+
intent: "edit",
499+
redactions: [redactedUri],
500+
assertions: [
501+
{
502+
label: "org.test.assertion",
503+
data: {},
504+
},
505+
],
506+
resources: { resources: {} },
507+
};
508+
509+
const builder = Builder.withJson(redactionManifestDefinition);
510+
511+
// Add a redacted action
512+
const redactedAction = {
513+
actions: [
514+
{
515+
action: "c2pa.redacted",
516+
},
517+
],
518+
};
519+
520+
builder.addAssertion("c2pa.actions", redactedAction, "Cbor");
521+
522+
// Use the callback signer like the other test
523+
const signerConfig: JsCallbackSignerConfig = {
524+
alg: "es256",
525+
certs: [publicKey],
526+
reserveSize: 10000,
527+
tsaUrl: undefined,
528+
directCoseHandling: false,
529+
};
530+
const testSigner = new TestSigner(privateKey);
531+
const signer = CallbackSigner.newSigner(signerConfig, testSigner.sign);
532+
533+
// Sign the manifest with the original image as input
534+
const dest = { buffer: null };
535+
const outputBuffer = await builder.signAsync(signer, source, dest);
536+
expect(outputBuffer.length).toBeGreaterThan(0);
537+
538+
// Verify the result by reading the signed manifest
539+
const signedReader = await Reader.fromAsset({
540+
buffer: dest.buffer! as Buffer,
541+
mimeType: "image/jpeg",
542+
});
543+
expect(signedReader).toBeDefined();
544+
545+
// Check that the manifest was created successfully
546+
const activeManifest = signedReader.getActive();
547+
expect(activeManifest).toBeDefined();
548+
549+
// Verify the redacted action was added
550+
const assertions = activeManifest?.assertions;
551+
const actionsAssertion = assertions?.find(
552+
(a: any) => a.label === "c2pa.actions.v2",
553+
);
554+
expect(actionsAssertion).toBeDefined();
555+
556+
if (actionsAssertion && isActionsAssertion(actionsAssertion)) {
557+
const actions = actionsAssertion.data.actions;
558+
const redactedAction = actions.find(
559+
(a: any) => a.action === "c2pa.redacted",
560+
);
561+
expect(redactedAction).toBeDefined();
562+
expect(redactedAction?.action).toBe("c2pa.redacted");
563+
}
564+
});
565+
566+
it("should test builder remote url", async () => {
567+
// This test mirrors the Rust test_builder_remote_url test
568+
569+
// Create a simple manifest definition similar to simple_manifest_json()
570+
const simpleManifestDefinition = {
571+
claim_generator_info: [
572+
{
573+
name: "c2pa_test",
574+
version: "1.0.0",
575+
},
576+
],
577+
title: "Test_Manifest",
578+
assertions: [
579+
{
580+
label: "c2pa.actions",
581+
data: {
582+
actions: [
583+
{
584+
action: "c2pa.created",
585+
digitalSourceType: "http://c2pa.org/digitalsourcetype/empty",
586+
},
587+
],
588+
},
589+
},
590+
],
591+
};
592+
593+
const builder = Builder.withJson(simpleManifestDefinition);
594+
595+
// Set remote URL and no embed flag like the Rust test
596+
builder.setRemoteUrl("http://my_remote_url");
597+
builder.setNoEmbed(true);
598+
599+
// Use the callback signer
600+
const signerConfig: JsCallbackSignerConfig = {
601+
alg: "es256",
602+
certs: [publicKey],
603+
reserveSize: 10000,
604+
tsaUrl: undefined,
605+
directCoseHandling: false,
606+
};
607+
const testSigner = new TestSigner(privateKey);
608+
const signer = CallbackSigner.newSigner(signerConfig, testSigner.sign);
609+
610+
// Sign the Builder and write it to the output stream
611+
const dest = { buffer: null };
612+
const manifestData = await builder.signAsync(signer, source, dest);
613+
614+
// Check to make sure we have a remote url and no manifest data embedded
615+
// Reading the image directly should fail since no_embed = true
616+
// Note: This might fail due to remote URL fetch, which is expected behavior
617+
try {
618+
await Reader.fromAsset({
619+
buffer: dest.buffer! as Buffer,
620+
mimeType: "image/jpeg",
621+
});
622+
// If we get here, the test should fail because we expect no embedded manifest
623+
expect.fail("Expected Reader.fromAsset to fail when no_embed = true");
624+
} catch (error) {
625+
// This is expected - there should be no embedded manifest or remote fetch should fail
626+
expect(error).toBeDefined();
627+
}
628+
629+
// Now validate the manifest against the written asset using the separate manifest data
630+
const reader = await Reader.fromManifestDataAndAsset(manifestData, {
631+
buffer: dest.buffer! as Buffer,
632+
mimeType: "image/jpeg",
633+
});
634+
635+
// Check if the manifest has the expected structure
636+
const activeManifest = reader.getActive();
637+
expect(activeManifest).toBeDefined();
638+
expect(activeManifest?.title).toBe("Test_Manifest");
639+
});
472640
});
473641
});

js-src/Builder.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// specific language governing permissions and limitations under
1212
// each license.
1313

14-
import type { Manifest } from "@contentauth/c2pa-types";
14+
import type { BuilderIntent, Manifest } from "@contentauth/c2pa-types";
1515

1616
import { getNeonBinary } from "./binary.js";
1717
import type {
@@ -57,6 +57,11 @@ export class Builder implements BuilderInterface {
5757
return new Builder(builder);
5858
}
5959

60+
setIntent(intent: BuilderIntent): void {
61+
const intentString = JSON.stringify(intent);
62+
getNeonBinary().builderSetIntent.call(this.builder, intentString);
63+
}
64+
6065
setNoEmbed(noEmbed = true): void {
6166
getNeonBinary().builderSetNoEmbed.call(this.builder, noEmbed);
6267
}

0 commit comments

Comments
 (0)