Skip to content

refactor(e-signature): return signing results#29

Open
Yorizel wants to merge 1 commit into
masterfrom
e-signature-neverthrow
Open

refactor(e-signature): return signing results#29
Yorizel wants to merge 1 commit into
masterfrom
e-signature-neverthrow

Conversation

@Yorizel

@Yorizel Yorizel commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

  • make signPdf return neverthrow ResultAsync instead of rejecting for expected signing errors
  • update batch signing and React hook integrations to unwrap Result explicitly
  • update tests to assert Result success/error paths
  • replace FOT CLI/config build wiring with bunup

Validation

  • bun test (from libraries/e-signature)
  • bun x tsc -p libraries/e-signature/tsconfig.json --noEmit
  • bun run build (from libraries/e-signature)

Note: this PR is intended to pair with the digital-certificate Result API refactor.


Summary by cubic

Refactors signPdf to return a neverthrow ResultAsync so callers handle expected signing errors without exceptions, and migrates the library build to bunup for a simpler toolchain.

  • Refactors
    • signPdf now returns ResultAsync<Uint8Array, PdfSignError>; callers must check .isOk() or unwrap the result.
    • Updated batch signing and useSignPdf to unwrap results and surface standardized errors; tests assert success/error paths.
    • Replaced FOT build setup with bunup (bunup.config.ts added, fot.config.ts removed); scripts now use bunup, bun test, and tsc. Added neverthrow and bunup dependencies, and aligned with the @f-o-t/digital-certificate Result API.

Written for commit 1157be9. Summary will update on new commits.

Summary by CodeRabbit

  • Breaking Changes

    • signPdf function now returns ResultAsync instead of Promise for improved error handling
  • Improvements

    • Enhanced error handling throughout the signing workflow with Result-based patterns
    • Migrated build system to Bun-based tooling
    • Standardized test error handling patterns

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR converts the signPdf function from a direct Promise<Uint8Array> API to a ResultAsync-based error handling pattern, adding neverthrow as a dependency and migrating the build system from fot to bunup. All callers and tests are updated accordingly.

Changes

Error Handling Modernization

Layer / File(s) Summary
API Signature
libraries/e-signature/src/sign-pdf.ts
signPdf signature changed from async function(...): Promise<Uint8Array> to function(...): ResultAsync<Uint8Array, PdfSignError>. Internal implementation moved to signPdfUnsafe.
Error Normalization
libraries/e-signature/src/sign-pdf.ts
Added helpers unwrapCertificateInfoResult (normalizes direct or wrapped CertificateInfo) and toPdfSignError (maps errors to PdfSignError).
Integration Updates
libraries/e-signature/src/batch.ts, src/plugins/react/index.ts
Callers now await signPdf and check isErr() to handle errors; success cases use value.
Test Helpers
libraries/e-signature/__tests__/sign-pdf.test.ts, __tests__/performance.test.ts
Added signOk and signErr helpers to unwrap Results in tests; replaced direct signPdf calls throughout.
Test Mocks
libraries/e-signature/__tests__/plugins/react.test.tsx
Mock implementations updated to return okAsync/errAsync or ResultAsync.fromPromise to align with the new API.
Dependencies & Build Config
package.json, bunup.config.ts, fot.config.ts
Added neverthrow runtime dependency and bunup devDependency; added bunup build script; removed fot.config.ts content; added new bunup.config.ts with entry points and externals.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Errors now wrapped in Results so neat,
No throws to fear, the API's complete.
From Promise to ResultAsync we soar,
With neverthrow patterns at our core!
Bunup builds faster, the configs aligned—
A safer signing flow, refactored with mind. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(e-signature): return signing results' accurately describes the main change: signPdf now returns a ResultAsync instead of rejecting for signing errors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch e-signature-neverthrow
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch e-signature-neverthrow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libraries/e-signature/package.json">

<violation number="1" location="libraries/e-signature/package.json:42">
P2: Adding `neverthrow` introduces a third-party runtime dependency, which conflicts with the repository dependency policy (runtime deps should be limited to `@f-o-t/*` and `zod`).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

"@f-o-t/digital-certificate": "^2.3.0",
"@f-o-t/pdf": "^0.5.2",
"@f-o-t/qrcode": "^1.0.2",
"neverthrow": "^8.2.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Adding neverthrow introduces a third-party runtime dependency, which conflicts with the repository dependency policy (runtime deps should be limited to @f-o-t/* and zod).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libraries/e-signature/package.json, line 42:

<comment>Adding `neverthrow` introduces a third-party runtime dependency, which conflicts with the repository dependency policy (runtime deps should be limited to `@f-o-t/*` and `zod`).</comment>

<file context>
@@ -29,27 +29,25 @@
       "@f-o-t/digital-certificate": "^2.3.0",
       "@f-o-t/pdf": "^0.5.2",
       "@f-o-t/qrcode": "^1.0.2",
+      "neverthrow": "^8.2.0",
       "zod": "^4.3.6"
    },
</file context>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
libraries/e-signature/__tests__/plugins/react.test.tsx (1)

221-234: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test no longer exercises the non-Error wrapping branch.

With the new mock setup, mockSignError is always wrapped in new Error(...) via errAsync(new Error(mockSignError)), so the awaited result's error is already an Error. The hook's err instanceof Error ? err : new Error("Unknown signing error") therefore short-circuits to the original error, and the test passes only because the chosen message happens to match. The non-Error wrap branch is now uncovered.

To actually exercise that branch, have the mock emit a non-Error rejection — e.g., errAsync("string-failure" as unknown as Error) — and assert the hook normalizes it to Error("Unknown signing error").

🧪 Suggested test adjustment
 it("wraps non-Error throws in a plain Error", async () => {
-   mockSignError = "Unknown signing error";
-
-   const { result } = renderHook(() => useSignPdf());
-
-   await act(async () => {
-      await result.current
-         .sign({ pdf: makePdf(), p12: makeP12(), password: "pw" })
-         .catch(() => {});
-   });
-
-   expect(result.current.error).toBeInstanceOf(Error);
-   expect(result.current.error?.message).toBe("Unknown signing error");
+   // Override mock to reject with a non-Error value
+   mock.module("../../src/sign-pdf.ts", () => ({
+      signPdf: () => errAsync("string-failure" as unknown as Error),
+   }));
+
+   const { result } = renderHook(() => useSignPdf());
+
+   await act(async () => {
+      await result.current
+         .sign({ pdf: makePdf(), p12: makeP12(), password: "pw" })
+         .catch(() => {});
+   });
+
+   expect(result.current.error).toBeInstanceOf(Error);
+   expect(result.current.error?.message).toBe("Unknown signing error");
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/e-signature/__tests__/plugins/react.test.tsx` around lines 221 -
234, The test currently never exercises the non-Error wrapping branch because
the mock (mockSignError / errAsync) always rejects with an Error; change the
mock to reject with a non-Error value (for example make the mock return
errAsync("string-failure" as unknown as Error) or otherwise throw a non-Error),
then call useSignPdf().sign(...) as before and assert that result.current.error
is an instance of Error with message "Unknown signing error" to verify the hook
normalizes non-Error rejections into a plain Error; adjust the mock creation
referenced by mockSignError/errAsync and keep the assertions on
result.current.error and its message.
libraries/e-signature/src/sign-pdf.ts (2)

386-401: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

toPdfSignError discards the original error context.

Mapping every failure to new PdfSignError(message) drops the original cause, stack trace, and any structured details (e.g., a ZodError.issues from pdfSignOptionsSchema.parse, or underlying @f-o-t/crypto/@f-o-t/pdf errors). With the new ResultAsync API this is the only error consumers ever receive, so debugging signing failures in production becomes considerably harder.

Extend PdfSignError to accept a cause and forward it from toPdfSignError (the Error({ cause }) form is supported on all targets a browser bundle currently ships to).

🛠️ Proposed fix
 function toPdfSignError(error: unknown): PdfSignError {
    if (error instanceof PdfSignError) return error;
    const message = error instanceof Error ? error.message : String(error);
-   return new PdfSignError(message);
+   return new PdfSignError(message, { cause: error });
 }
 export class PdfSignError extends Error {
-   constructor(message: string) {
-      super(message);
+   constructor(message: string, options?: { cause?: unknown }) {
+      super(message, options);
       this.name = "PdfSignError";
    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/e-signature/src/sign-pdf.ts` around lines 386 - 401, Update
PdfSignError to accept an optional cause (e.g., constructor(message: string,
cause?: unknown) and call super(message, { cause })) and then change
toPdfSignError to pass the original error as the cause when wrapping
non-PdfSignError values (use Error({ cause }) form). Ensure toPdfSignError still
returns the original instance if error instanceof PdfSignError, otherwise
construct new PdfSignError(message, error) so underlying stacks and structured
data (e.g., ZodError issues or upstream `@f-o-t` errors) are preserved for
consumers of ResultAsync.

46-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale JSDoc — @returns and @example no longer match the new API.

The function now returns ResultAsync<Uint8Array, PdfSignError>, so @returns The signed PDF as a Uint8Array is incorrect and the example would assign a Result, not the bytes. Update the docs so consumers know to .isOk()/.value (or .match) on the result.

📝 Proposed JSDoc fix
- * `@param` pdf - The PDF document as a Uint8Array or ReadableStream<Uint8Array>
- * `@param` options - Signing options
- * `@returns` The signed PDF as a Uint8Array
- *
- * `@example`
- * ```ts
- * const signedPdf = await signPdf(pdfBytes, {
- *   certificate: { p12, password: "secret" },
- *   reason: "Document approval",
- *   location: "Corporate Office",
- *   policy: "pades-icp-brasil",
- * });
- * ```
+ * `@param` pdf - The PDF document as a Uint8Array or ReadableStream<Uint8Array>
+ * `@param` options - Signing options
+ * `@returns` A `ResultAsync<Uint8Array, PdfSignError>` resolving to the signed PDF
+ *          on success or a `PdfSignError` on failure.
+ *
+ * `@example`
+ * ```ts
+ * const result = await signPdf(pdfBytes, {
+ *   certificate: { p12, password: "secret" },
+ *   reason: "Document approval",
+ *   location: "Corporate Office",
+ *   policy: "pades-icp-brasil",
+ * });
+ * if (result.isErr()) throw result.error;
+ * const signedPdf = result.value;
+ * ```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/e-signature/src/sign-pdf.ts` around lines 46 - 65, The JSDoc for
signPdf is stale: update the `@returns` to state the function returns a
ResultAsync<Uint8Array, PdfSignError> (resolving to the signed PDF on success or
a PdfSignError on failure) and replace the current `@example` so it shows awaiting
the ResultAsync (e.g., const result = await signPdf(...); if (result.isErr())
throw result.error; const signedPdf = result.value;) or using .match; reference
the signPdf function, ResultAsync type and PdfSignError in the doc to make usage
clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@libraries/e-signature/__tests__/plugins/react.test.tsx`:
- Around line 221-234: The test currently never exercises the non-Error wrapping
branch because the mock (mockSignError / errAsync) always rejects with an Error;
change the mock to reject with a non-Error value (for example make the mock
return errAsync("string-failure" as unknown as Error) or otherwise throw a
non-Error), then call useSignPdf().sign(...) as before and assert that
result.current.error is an instance of Error with message "Unknown signing
error" to verify the hook normalizes non-Error rejections into a plain Error;
adjust the mock creation referenced by mockSignError/errAsync and keep the
assertions on result.current.error and its message.

In `@libraries/e-signature/src/sign-pdf.ts`:
- Around line 386-401: Update PdfSignError to accept an optional cause (e.g.,
constructor(message: string, cause?: unknown) and call super(message, { cause
})) and then change toPdfSignError to pass the original error as the cause when
wrapping non-PdfSignError values (use Error({ cause }) form). Ensure
toPdfSignError still returns the original instance if error instanceof
PdfSignError, otherwise construct new PdfSignError(message, error) so underlying
stacks and structured data (e.g., ZodError issues or upstream `@f-o-t` errors) are
preserved for consumers of ResultAsync.
- Around line 46-65: The JSDoc for signPdf is stale: update the `@returns` to
state the function returns a ResultAsync<Uint8Array, PdfSignError> (resolving to
the signed PDF on success or a PdfSignError on failure) and replace the current
`@example` so it shows awaiting the ResultAsync (e.g., const result = await
signPdf(...); if (result.isErr()) throw result.error; const signedPdf =
result.value;) or using .match; reference the signPdf function, ResultAsync type
and PdfSignError in the doc to make usage clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3065f15d-1fdf-4d08-9d26-71e5e4b1de6a

📥 Commits

Reviewing files that changed from the base of the PR and between d8db79c and 1157be9.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • libraries/e-signature/__tests__/performance.test.ts
  • libraries/e-signature/__tests__/plugins/react.test.tsx
  • libraries/e-signature/__tests__/sign-pdf.test.ts
  • libraries/e-signature/bunup.config.ts
  • libraries/e-signature/fot.config.ts
  • libraries/e-signature/package.json
  • libraries/e-signature/src/batch.ts
  • libraries/e-signature/src/plugins/react/index.ts
  • libraries/e-signature/src/sign-pdf.ts
💤 Files with no reviewable changes (1)
  • libraries/e-signature/fot.config.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (6)
libraries/e-signature/src/plugins/react/index.ts (1)

113-123: LGTM!

The Result handling is correct: the type guard from isErr() narrows signedResult to Ok, and the previous public throw‑on‑failure contract of sign() (documented on lines 56–60) is preserved.

libraries/e-signature/bunup.config.ts (1)

1-28: LGTM!

Externals align with the runtime dependencies in package.json, and dts.inferTypes: true will correctly emit references to neverthrow.ResultAsync as a bare external import in the generated .d.ts (consistent with the new signPdf return type).

libraries/e-signature/package.json (1)

31-54: LGTM!

neverthrow is correctly declared as a runtime dependency to back the new ResultAsync API, and the bunup-based scripts align with the new bunup.config.ts. Pinning bunup to an exact version is a reasonable choice for build-tool reproducibility.

libraries/e-signature/src/batch.ts (1)

58-66: LGTM!

Throwing on isErr() keeps the per-file try/catchfile_error event flow intact, and signedResult.value yields the bytes unchanged for file_complete. Public event shape is unchanged.

libraries/e-signature/__tests__/performance.test.ts (1)

42-49: LGTM!

signOk is a clean, minimal adapter that preserves the previous "throws on failure" semantics these benchmarks rely on. Mirrors the helper added in sign-pdf.test.ts.

libraries/e-signature/__tests__/sign-pdf.test.ts (1)

48-64: LGTM!

Both helpers correctly bridge the ResultAsync API to the existing assertion style: signOk rethrows on isErr() so legacy throw-based tests keep working, and signErr fails loudly if signing unexpectedly succeeds. Naming and shape are consistent with the helper duplicated in performance.test.ts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant