@bdking71/spsignature 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -41
- package/lib/SignatureDisplay.d.ts +62 -0
- package/lib/SignatureDisplay.js +156 -0
- package/lib/TransactionSigner.d.ts +17 -6
- package/lib/TransactionSigner.js +456 -261
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> ⚠️ **NOT READY FOR PRODUCTION USE!** This module is currently under active development. Please do not use it in production environments at this time.
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|
`@bdking71/spsignature` provides a secure, lightweight, and framework-agnostic digital signature module engineered specifically for Microsoft 365 environments. Built to run inside custom SPFx Web Parts and Extension Application Customizers, it delivers tamper-evident signature collection, automated base64 image encoding, and structured payload generation directly integrated with SharePoint Online list infrastructure.
|
|
12
12
|
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
## Source Code & Repository
|
|
16
16
|
|
|
17
|
-
* **GitHub Repository:**
|
|
18
|
-
* **Issues & Feedback:**
|
|
19
|
-
* **NPM Package:**
|
|
17
|
+
* **GitHub Repository:** https://github.com/bdking71/spsignature
|
|
18
|
+
* **Issues & Feedback:** https://github.com/bdking71/spsignature/issues
|
|
19
|
+
* **NPM Package:** https://www.npmjs.com/package/@bdking71/spsignature
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
- **Zero Infrastructure Overhead**: Operates entirely within client-side M365 contexts — no external APIs, Azure Functions, or third-party storage backends required.
|
|
26
26
|
- **Cross-Platform M365 Integration**: Designed for seamless deployment across SharePoint Online, Microsoft Teams, and Viva Connections desktop/mobile experiences.
|
|
27
27
|
- **Integrity & Non-Repudiation**: Generates cryptographic SHA-256 hashes bound to user identities, timestamps, and target record IDs to prevent signature tampering.
|
|
28
|
-
- **Standardized JSON Payload**: Encapsulates complete signing metadata inside standard SharePoint Multiline Text columns
|
|
28
|
+
- **Standardized JSON Payload**: Encapsulates complete signing metadata inside standard SharePoint Multiline Text columns for effortless integration with Power Automate and Power BI.
|
|
29
29
|
- **LZW Compression**: Automatically compresses signature image payloads to minimize SharePoint storage footprint.
|
|
30
30
|
- **Signature Caching**: Optional local device caching so users don't have to re-draw their signature on every transaction.
|
|
31
|
+
- **React Component Display**: Built-in `SignatureDisplay` React component for displaying verified signatures with full styling control.
|
|
31
32
|
|
|
32
33
|
---
|
|
33
34
|
|
|
@@ -35,9 +36,8 @@
|
|
|
35
36
|
|
|
36
37
|
To enforce non-repudiation and meet compliance standards, `@bdking71/spsignature` includes a flexible Two-Factor Authentication engine. Rather than relying on rigid third-party SMS gateways, it delegates code delivery to **Power Automate**, allowing organizations to route standard **5-digit** verification codes via Microsoft Teams, Outlook Email, or both.
|
|
37
38
|
|
|
38
|
-

|
|
39
|
-
|
|
40
39
|
### How the 2FA Workflow Operates
|
|
40
|
+

|
|
41
41
|
|
|
42
42
|
1. **Code Generation**: When a user initiates a signing transaction, the component generates a cryptographically secure 5-digit verification code (e.g., `84920`) using the Web Crypto API.
|
|
43
43
|
2. **SharePoint Queue**: The component writes a record to the dedicated `PendingVerifications` SharePoint list, secured with item-level permissions so users can only see their own codes.
|
|
@@ -50,9 +50,9 @@ To enforce non-repudiation and meet compliance standards, `@bdking71/spsignature
|
|
|
50
50
|
|
|
51
51
|
## Signature Data Schema
|
|
52
52
|
|
|
53
|
-
The module returns a single, structured JSON object designed to be stored directly inside a SharePoint **Multiple lines of text** field
|
|
53
|
+
The module returns a single, structured JSON object designed to be stored directly inside a SharePoint **Multiple lines of text** field:
|
|
54
54
|
|
|
55
|
-
```
|
|
55
|
+
```
|
|
56
56
|
{
|
|
57
57
|
"signatureHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
58
58
|
"signatureData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
|
|
@@ -89,7 +89,7 @@ This project requires a precise local environment build. Strict adherence to the
|
|
|
89
89
|
|
|
90
90
|
Install the package into your SPFx solution:
|
|
91
91
|
|
|
92
|
-
```
|
|
92
|
+
```
|
|
93
93
|
npm install @bdking71/spsignature --save
|
|
94
94
|
```
|
|
95
95
|
|
|
@@ -97,8 +97,8 @@ npm install @bdking71/spsignature --save
|
|
|
97
97
|
|
|
98
98
|
Ensure the following peer dependencies are installed in your SPFx project:
|
|
99
99
|
|
|
100
|
-
```
|
|
101
|
-
npm install @pnp/sp @microsoft/sp-webpart-base --save
|
|
100
|
+
```
|
|
101
|
+
npm install @pnp/sp @microsoft/sp-webpart-base react --save
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
---
|
|
@@ -109,9 +109,9 @@ npm install @pnp/sp @microsoft/sp-webpart-base --save
|
|
|
109
109
|
|
|
110
110
|
Call `ensurePendingVerificationsList` during the web part lifecycle (inside `onInit()`) to automatically ensure the `PendingVerifications` SharePoint list, columns, and item-level security settings exist on the current site collection.
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
Note: This method is fully **idempotent**—it can be safely called on every web part load without failing or duplicating columns.
|
|
113
113
|
|
|
114
|
-
```
|
|
114
|
+
```
|
|
115
115
|
import { ensurePendingVerificationsList } from "@bdking71/spsignature";
|
|
116
116
|
|
|
117
117
|
export default class MySignatureWebPart extends BaseClientSideWebPart<IMySignatureWebPartProps> {
|
|
@@ -140,11 +140,11 @@ Create a Power Automate flow that triggers on **"When an item is created"** for
|
|
|
140
140
|
|
|
141
141
|
Execute `promptAndGenerateSecureAudit` within your component or event handler. This renders an enterprise modal overlay that:
|
|
142
142
|
- Handles drawn, uploaded, or cached signature selection.
|
|
143
|
-
- Triggers 2FA code delivery through the configured channel.
|
|
144
|
-
- Validates the 5-digit passcode.
|
|
143
|
+
- Triggers 2FA code delivery through the configured channel (when enabled).
|
|
144
|
+
- Validates the 5-digit passcode (if TFA is required).
|
|
145
145
|
- Outputs a cryptographic audit payload.
|
|
146
146
|
|
|
147
|
-
```
|
|
147
|
+
```
|
|
148
148
|
import {
|
|
149
149
|
promptAndGenerateSecureAudit,
|
|
150
150
|
SharePointAuditRecord
|
|
@@ -153,10 +153,10 @@ import {
|
|
|
153
153
|
private handleSignAction = async (): Promise<void> => {
|
|
154
154
|
const auditRecord: SharePointAuditRecord | undefined = await promptAndGenerateSecureAudit(
|
|
155
155
|
{
|
|
156
|
-
itemID: 1042,
|
|
156
|
+
itemID: 1042,
|
|
157
157
|
signer: this.context.pageContext.user.email,
|
|
158
158
|
spContext: this.context,
|
|
159
|
-
channel: "teams",
|
|
159
|
+
channel: "teams",
|
|
160
160
|
requireTFA: true,
|
|
161
161
|
payload: {
|
|
162
162
|
DocumentTitle: "Purchase Requisition #4402",
|
|
@@ -169,27 +169,23 @@ private handleSignAction = async (): Promise<void> => {
|
|
|
169
169
|
);
|
|
170
170
|
|
|
171
171
|
if (auditRecord) {
|
|
172
|
-
// Save auditRecord JSON payload directly to your SharePoint multiline text column
|
|
173
172
|
console.log("Signature Hash:", auditRecord.signatureHash);
|
|
174
173
|
console.log("Compressed Image Data:", auditRecord.signatureData);
|
|
175
174
|
console.log("Timestamp:", auditRecord.signatureTimestamp);
|
|
176
175
|
console.log("Verification Log ID:", auditRecord.verificationItemId);
|
|
177
176
|
|
|
178
|
-
// Example: Persist to a SharePoint list column named "SignatureAudit"
|
|
179
177
|
await sp.web.lists.getByTitle("Requisitions").items.getById(1042).update({
|
|
180
178
|
SignatureAudit: JSON.stringify(auditRecord)
|
|
181
179
|
});
|
|
182
|
-
} else {
|
|
183
|
-
console.log("User cancelled the signature workflow.");
|
|
184
180
|
}
|
|
185
181
|
};
|
|
186
182
|
```
|
|
187
183
|
|
|
188
184
|
### Step 4: Verification & Image Reconstruction
|
|
189
185
|
|
|
190
|
-
To verify non-repudiation or render the stored
|
|
186
|
+
To verify non-repudiation or render the stored signature back into a view or report, use the built-in helper utilities.
|
|
191
187
|
|
|
192
|
-
```
|
|
188
|
+
```
|
|
193
189
|
import {
|
|
194
190
|
verifySecureAuditRecord,
|
|
195
191
|
getReportableSignature
|
|
@@ -197,11 +193,9 @@ import {
|
|
|
197
193
|
|
|
198
194
|
// 1. Re-calculate SHA-256 hash to verify record integrity
|
|
199
195
|
const isValid: boolean = await verifySecureAuditRecord(
|
|
200
|
-
|
|
201
|
-
signerEmail,
|
|
202
|
-
|
|
203
|
-
compressedSignatureData, // Retained for API symmetry
|
|
204
|
-
storedHash // The stored signatureHash to compare against
|
|
196
|
+
auditRecord,
|
|
197
|
+
signerEmail,
|
|
198
|
+
payload
|
|
205
199
|
);
|
|
206
200
|
|
|
207
201
|
if (isValid) {
|
|
@@ -210,13 +204,87 @@ if (isValid) {
|
|
|
210
204
|
console.warn("✗ Signature verification failed - record may be tampered.");
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
// 2. Decompress LZW signature string into a
|
|
214
|
-
const renderableImageSrc: string = getReportableSignature(
|
|
207
|
+
// 2. Decompress LZW signature string into a viewable PNG Data-URI
|
|
208
|
+
const renderableImageSrc: string = getReportableSignature(auditRecord.signatureData);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Step 5: Display Signature with React Component
|
|
212
|
+
|
|
213
|
+
Use the built-in `SignatureDisplay` React component to render a verified signature with customizable styling:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
import React from "react";
|
|
217
|
+
import {
|
|
218
|
+
SignatureDisplay,
|
|
219
|
+
SharePointAuditRecord,
|
|
220
|
+
verifySecureAuditRecord
|
|
221
|
+
} from "@bdking71/spsignature";
|
|
222
|
+
|
|
223
|
+
export const MySignatureViewer: React.FC = () => {
|
|
224
|
+
const [auditRecord, setAuditRecord] = React.useState<SharePointAuditRecord | null>(null);
|
|
225
|
+
const [isValid, setIsValid] = React.useState(false);
|
|
226
|
+
|
|
227
|
+
React.useEffect(() => {
|
|
228
|
+
const verifySignature = async () => {
|
|
229
|
+
if (!auditRecord) return;
|
|
230
|
+
|
|
231
|
+
const valid = await verifySecureAuditRecord(
|
|
232
|
+
auditRecord,
|
|
233
|
+
"user@example.com",
|
|
234
|
+
{ amount: 1500, vendor: "Contoso" }
|
|
235
|
+
);
|
|
236
|
+
setIsValid(valid);
|
|
237
|
+
};
|
|
215
238
|
|
|
216
|
-
|
|
217
|
-
|
|
239
|
+
verifySignature();
|
|
240
|
+
}, [auditRecord]);
|
|
241
|
+
|
|
242
|
+
if (!auditRecord) {
|
|
243
|
+
return <div>No signature to display.</div>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div>
|
|
248
|
+
<h1>Purchase Requisition #4402</h1>
|
|
249
|
+
|
|
250
|
+
<SignatureDisplay
|
|
251
|
+
auditRecord={auditRecord}
|
|
252
|
+
isValid={isValid}
|
|
253
|
+
style={{
|
|
254
|
+
padding: "24px",
|
|
255
|
+
border: "2px solid #0078d4",
|
|
256
|
+
backgroundColor: "#f0f7ff",
|
|
257
|
+
marginTop: "20px",
|
|
258
|
+
borderRadius: "12px",
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
|
|
262
|
+
<SignatureDisplay
|
|
263
|
+
auditRecord={auditRecord}
|
|
264
|
+
isValid={isValid}
|
|
265
|
+
className="my-signature-card"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
218
270
|
```
|
|
219
271
|
|
|
272
|
+
#### SignatureDisplay Component Props
|
|
273
|
+
|
|
274
|
+
| Prop | Type | Required | Description |
|
|
275
|
+
| :--- | :--- | :--- | :--- |
|
|
276
|
+
| `auditRecord` | `SharePointAuditRecord` | Yes | The signature audit record returned from `promptAndGenerateSecureAudit()` |
|
|
277
|
+
| `isValid` | `boolean` | Yes | Result from `verifySecureAuditRecord()` — displayed in green or red |
|
|
278
|
+
| `className` | `string` | No | CSS class name for custom styling |
|
|
279
|
+
| `style` | `React.CSSProperties` | No | Inline CSS styles for the root container |
|
|
280
|
+
|
|
281
|
+
The SignatureDisplay component renders:
|
|
282
|
+
- Signature image preview
|
|
283
|
+
- Signer name/email
|
|
284
|
+
- Validation status (green for valid, red for invalid)
|
|
285
|
+
- ISO-8601 timestamp converted to local timezone
|
|
286
|
+
- SHA-256 hash digest (truncated, light gray font)
|
|
287
|
+
|
|
220
288
|
---
|
|
221
289
|
|
|
222
290
|
## API Reference
|
|
@@ -225,9 +293,9 @@ const renderableImageSrc: string = getReportableSignature(compressedSignatureDat
|
|
|
225
293
|
|
|
226
294
|
| Function | Purpose |
|
|
227
295
|
| :--- | :--- |
|
|
228
|
-
| `ensurePendingVerificationsList(props)` | Idempotently provisions the `PendingVerifications` SharePoint list,
|
|
296
|
+
| `ensurePendingVerificationsList(props)` | Idempotently provisions the `PendingVerifications` SharePoint list, columns, and item-level security. |
|
|
229
297
|
| `promptAndGenerateSecureAudit(context, title?, msg?)` | Launches the signature modal, dispatches the OTP, validates entry, and returns a `SharePointAuditRecord`. |
|
|
230
|
-
| `verifySecureAuditRecord(
|
|
298
|
+
| `verifySecureAuditRecord(auditRecord, signer, payload)` | Re-computes the SHA-256 hash to verify audit-record integrity. |
|
|
231
299
|
| `getReportableSignature(compressedSignatureData)` | Decompresses an LZW-compressed signature back into a viewable `data:image/png` URI. |
|
|
232
300
|
|
|
233
301
|
### Exported Types
|
|
@@ -238,6 +306,13 @@ const renderableImageSrc: string = getReportableSignature(compressedSignatureDat
|
|
|
238
306
|
| `SignerContext` | Input parameters for the signature workflow. |
|
|
239
307
|
| `AuditEnvelopeRecord` | Canonical envelope structure used for hashing (payload + signer + timestamp). |
|
|
240
308
|
| `DeliveryChannel` | `"email" \| "teams" \| "both"` — Supported OTP delivery channels. |
|
|
309
|
+
| `ISignatureDisplayProps` | Props interface for the `SignatureDisplay` React component. |
|
|
310
|
+
|
|
311
|
+
### Exported React Components
|
|
312
|
+
|
|
313
|
+
| Component | Purpose |
|
|
314
|
+
| :--- | :--- |
|
|
315
|
+
| `SignatureDisplay` | Renders a verified signature with signer info, validation status, timestamp, and hash digest. Fully customizable via props. |
|
|
241
316
|
|
|
242
317
|
---
|
|
243
318
|
|
|
@@ -256,9 +331,9 @@ const renderableImageSrc: string = getReportableSignature(compressedSignatureDat
|
|
|
256
331
|
| :--- | :--- |
|
|
257
332
|
| Modal never appears | Ensure `spContext` is a valid `WebPartContext` and the DOM is fully loaded. |
|
|
258
333
|
| OTP never arrives | Verify the Power Automate flow is active and listening on the correct list. Check the flow run history. |
|
|
259
|
-
| "Field name already exists" | This shouldn't happen due to idempotency, but ensure you're on the latest version of the module. |
|
|
260
334
|
| SHA-256 verification fails on nested objects | Note: Only top-level payload keys are sorted. Deeply nested objects should have consistent key ordering. |
|
|
261
335
|
| Signature not caching | Verify browser `localStorage` isn't in private/incognito mode or blocked by browser policy. |
|
|
336
|
+
| SignatureDisplay not rendering | Ensure React is installed and `auditRecord` is properly populated with valid audit data. |
|
|
262
337
|
|
|
263
338
|
---
|
|
264
339
|
|
|
@@ -266,10 +341,10 @@ const renderableImageSrc: string = getReportableSignature(compressedSignatureDat
|
|
|
266
341
|
|
|
267
342
|
Contributions, issues, and feature requests are welcome!
|
|
268
343
|
|
|
269
|
-
1. Fork the Project:
|
|
270
|
-
2. Create your Feature Branch (
|
|
271
|
-
3. Commit your Changes (
|
|
272
|
-
4. Push to the Branch (
|
|
344
|
+
1. Fork the Project: https://github.com/bdking71/spsignature
|
|
345
|
+
2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
|
|
346
|
+
3. Commit your Changes (git commit -m 'Add some AmazingFeature')
|
|
347
|
+
4. Push to the Branch (git push origin feature/AmazingFeature)
|
|
273
348
|
5. Open a Pull Request
|
|
274
349
|
|
|
275
350
|
---
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file SignatureDisplay.tsx
|
|
3
|
+
*
|
|
4
|
+
* React component that renders a signature verification display card
|
|
5
|
+
* showing the signature image, signer information, validation status,
|
|
6
|
+
* timestamp, and cryptographic hash.
|
|
7
|
+
*
|
|
8
|
+
* @module SignatureDisplay
|
|
9
|
+
*/
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { SharePointAuditRecord } from "./TransactionSigner";
|
|
12
|
+
/**
|
|
13
|
+
* Props for the SignatureDisplay component.
|
|
14
|
+
*/
|
|
15
|
+
export interface ISignatureDisplayProps {
|
|
16
|
+
/** The audit record to display containing signature data and metadata. */
|
|
17
|
+
auditRecord: SharePointAuditRecord;
|
|
18
|
+
/** Whether the signature has been cryptographically verified as authentic. */
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
/** Optional custom CSS class name for the root container. */
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Optional inline styles for the root container. */
|
|
23
|
+
style?: React.CSSProperties;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* React component that displays a digital signature with verification status,
|
|
27
|
+
* signer information, timestamp, and hash digest.
|
|
28
|
+
*
|
|
29
|
+
* Renders a structured div containing:
|
|
30
|
+
* - Signature image preview
|
|
31
|
+
* - Validation status badge (green for valid, red for invalid)
|
|
32
|
+
* - Signer name/email
|
|
33
|
+
* - Signature date and time (localized)
|
|
34
|
+
* - SHA-256 hash digest (truncated, light gray)
|
|
35
|
+
*
|
|
36
|
+
* All styling is customizable via `className` and `style` props, allowing
|
|
37
|
+
* consumers to integrate the component into any design system.
|
|
38
|
+
*
|
|
39
|
+
* @param props - Component props.
|
|
40
|
+
* @returns JSX.Element representing the signature display.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* import { SignatureDisplay } from "@bdking71/spsignature";
|
|
45
|
+
*
|
|
46
|
+
* const MyComponent: React.FC = () => {
|
|
47
|
+
* const [auditRecord, setAuditRecord] = React.useState<SharePointAuditRecord | null>(null);
|
|
48
|
+
* const [isValid, setIsValid] = React.useState(false);
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <SignatureDisplay
|
|
52
|
+
* auditRecord={auditRecord}
|
|
53
|
+
* isValid={isValid}
|
|
54
|
+
* className="my-signature-display"
|
|
55
|
+
* style={{ padding: "20px", border: "1px solid #ccc" }}
|
|
56
|
+
* />
|
|
57
|
+
* );
|
|
58
|
+
* };
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare const SignatureDisplay: React.FC<ISignatureDisplayProps>;
|
|
62
|
+
export default SignatureDisplay;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file SignatureDisplay.tsx
|
|
4
|
+
*
|
|
5
|
+
* React component that renders a signature verification display card
|
|
6
|
+
* showing the signature image, signer information, validation status,
|
|
7
|
+
* timestamp, and cryptographic hash.
|
|
8
|
+
*
|
|
9
|
+
* @module SignatureDisplay
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SignatureDisplay = void 0;
|
|
16
|
+
const react_1 = __importDefault(require("react"));
|
|
17
|
+
/**
|
|
18
|
+
* Formats an ISO-8601 timestamp into a localized human-readable date and time string.
|
|
19
|
+
*
|
|
20
|
+
* @param isoTimestamp - ISO-8601 formatted timestamp string.
|
|
21
|
+
* @returns Formatted date/time string in local timezone (e.g., "8/31/2026, 7:55:06 PM").
|
|
22
|
+
*/
|
|
23
|
+
function formatLocalDateTime(isoTimestamp) {
|
|
24
|
+
try {
|
|
25
|
+
const date = new Date(isoTimestamp);
|
|
26
|
+
return date.toLocaleString();
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
return isoTimestamp;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Truncates a long hash string to a specified length with ellipsis.
|
|
34
|
+
*
|
|
35
|
+
* @param hash - Full hash string.
|
|
36
|
+
* @param length - Number of characters to display before ellipsis (default: 40).
|
|
37
|
+
* @returns Truncated hash string with "..." suffix.
|
|
38
|
+
*/
|
|
39
|
+
function truncateHash(hash, length = 40) {
|
|
40
|
+
if (hash.length <= length)
|
|
41
|
+
return hash;
|
|
42
|
+
return hash.substring(0, length) + "...";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extracts the signer's display name or email from the audit record.
|
|
46
|
+
* Falls back to "Unknown Signer" if not available.
|
|
47
|
+
*
|
|
48
|
+
* @param auditRecord - The audit record containing signature data.
|
|
49
|
+
* @returns The signer identifier or a default fallback string.
|
|
50
|
+
*/
|
|
51
|
+
function getSignerDisplay(auditRecord) {
|
|
52
|
+
// The audit record doesn't store signer name by design (for security),
|
|
53
|
+
// so consumers must pass it separately. This is a fallback.
|
|
54
|
+
return "Signature";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* React component that displays a digital signature with verification status,
|
|
58
|
+
* signer information, timestamp, and hash digest.
|
|
59
|
+
*
|
|
60
|
+
* Renders a structured div containing:
|
|
61
|
+
* - Signature image preview
|
|
62
|
+
* - Validation status badge (green for valid, red for invalid)
|
|
63
|
+
* - Signer name/email
|
|
64
|
+
* - Signature date and time (localized)
|
|
65
|
+
* - SHA-256 hash digest (truncated, light gray)
|
|
66
|
+
*
|
|
67
|
+
* All styling is customizable via `className` and `style` props, allowing
|
|
68
|
+
* consumers to integrate the component into any design system.
|
|
69
|
+
*
|
|
70
|
+
* @param props - Component props.
|
|
71
|
+
* @returns JSX.Element representing the signature display.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* import { SignatureDisplay } from "@bdking71/spsignature";
|
|
76
|
+
*
|
|
77
|
+
* const MyComponent: React.FC = () => {
|
|
78
|
+
* const [auditRecord, setAuditRecord] = React.useState<SharePointAuditRecord | null>(null);
|
|
79
|
+
* const [isValid, setIsValid] = React.useState(false);
|
|
80
|
+
*
|
|
81
|
+
* return (
|
|
82
|
+
* <SignatureDisplay
|
|
83
|
+
* auditRecord={auditRecord}
|
|
84
|
+
* isValid={isValid}
|
|
85
|
+
* className="my-signature-display"
|
|
86
|
+
* style={{ padding: "20px", border: "1px solid #ccc" }}
|
|
87
|
+
* />
|
|
88
|
+
* );
|
|
89
|
+
* };
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
const SignatureDisplay = ({ auditRecord, isValid, className, style, }) => {
|
|
93
|
+
const signatureImageSrc = auditRecord.signatureData;
|
|
94
|
+
const formattedDateTime = formatLocalDateTime(auditRecord.signatureTimestamp);
|
|
95
|
+
const truncatedHash = truncateHash(auditRecord.signatureHash);
|
|
96
|
+
const validationStatusColor = isValid ? "#107c10" : "#d13438";
|
|
97
|
+
const validationStatusLabel = isValid
|
|
98
|
+
? "Signature is Valid"
|
|
99
|
+
: "Signature is Invalid";
|
|
100
|
+
return (react_1.default.createElement("div", { className: className, style: Object.assign({ border: "1px solid #e0e0e0", borderRadius: "8px", padding: "16px", backgroundColor: "#ffffff", fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif" }, style) },
|
|
101
|
+
react_1.default.createElement("div", { style: {
|
|
102
|
+
marginBottom: "16px",
|
|
103
|
+
textAlign: "center",
|
|
104
|
+
padding: "12px",
|
|
105
|
+
backgroundColor: "#fafafa",
|
|
106
|
+
borderRadius: "6px",
|
|
107
|
+
border: "1px solid #e0e0e0",
|
|
108
|
+
} },
|
|
109
|
+
react_1.default.createElement("img", { src: signatureImageSrc, alt: "Digital Signature", style: {
|
|
110
|
+
maxWidth: "100%",
|
|
111
|
+
height: "auto",
|
|
112
|
+
minHeight: "80px",
|
|
113
|
+
maxHeight: "200px",
|
|
114
|
+
display: "block",
|
|
115
|
+
margin: "0 auto",
|
|
116
|
+
objectFit: "contain",
|
|
117
|
+
} })),
|
|
118
|
+
react_1.default.createElement("div", { style: {
|
|
119
|
+
marginBottom: "12px",
|
|
120
|
+
fontSize: "16px",
|
|
121
|
+
fontWeight: "600",
|
|
122
|
+
color: "#323130",
|
|
123
|
+
} }, getSignerDisplay(auditRecord)),
|
|
124
|
+
react_1.default.createElement("div", { style: {
|
|
125
|
+
marginBottom: "12px",
|
|
126
|
+
fontSize: "14px",
|
|
127
|
+
fontWeight: "600",
|
|
128
|
+
color: validationStatusColor,
|
|
129
|
+
} }, validationStatusLabel),
|
|
130
|
+
react_1.default.createElement("div", { style: {
|
|
131
|
+
marginBottom: "12px",
|
|
132
|
+
fontSize: "14px",
|
|
133
|
+
color: "#323130",
|
|
134
|
+
display: "flex",
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
gap: "8px",
|
|
137
|
+
} },
|
|
138
|
+
react_1.default.createElement("span", null, "\uD83D\uDCC5"),
|
|
139
|
+
formattedDateTime),
|
|
140
|
+
react_1.default.createElement("div", { style: {
|
|
141
|
+
fontSize: "11px",
|
|
142
|
+
color: "#999999",
|
|
143
|
+
backgroundColor: "#f5f5f5",
|
|
144
|
+
padding: "8px 12px",
|
|
145
|
+
borderRadius: "4px",
|
|
146
|
+
wordBreak: "break-all",
|
|
147
|
+
fontFamily: "'Courier New', monospace",
|
|
148
|
+
border: "1px solid #e0e0e0",
|
|
149
|
+
maxHeight: "60px",
|
|
150
|
+
overflowY: "auto",
|
|
151
|
+
}, title: auditRecord.signatureHash },
|
|
152
|
+
react_1.default.createElement("strong", { style: { color: "#666666" } }, "Hash: "),
|
|
153
|
+
truncatedHash)));
|
|
154
|
+
};
|
|
155
|
+
exports.SignatureDisplay = SignatureDisplay;
|
|
156
|
+
exports.default = exports.SignatureDisplay;
|
|
@@ -63,24 +63,28 @@ export interface AuditEnvelopeRecord {
|
|
|
63
63
|
payload: Record<string, unknown>;
|
|
64
64
|
signer: string;
|
|
65
65
|
timestamp: string;
|
|
66
|
+
itemID?: number;
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
69
|
* Opens a full-screen modal dialog that walks the user through:
|
|
69
70
|
*
|
|
70
|
-
* 1. (Optional) Two-factor passcode verification
|
|
71
|
+
* 1. (Optional) Two-factor passcode verification (user-initiated)
|
|
71
72
|
* 2. Providing a digital signature (cached / drawn / uploaded)
|
|
72
73
|
* 3. Generating a tamper-evident SHA-256 audit record
|
|
73
74
|
*
|
|
74
75
|
* The returned promise resolves with a `SharePointAuditRecord` on
|
|
75
76
|
* success or `undefined` if the user cancels.
|
|
76
77
|
*
|
|
78
|
+
* **Note:** Only one modal can be active at a time. Attempting to open
|
|
79
|
+
* a second modal while one is active will return `undefined` immediately.
|
|
80
|
+
*
|
|
77
81
|
* @param context - Signer metadata and SPFx context.
|
|
78
82
|
* @param modalTitle - Title shown in the modal header.
|
|
79
83
|
* @param warningMessage - Instructional HTML rendered above the
|
|
80
84
|
* signature area.
|
|
81
85
|
* @returns A promise that resolves to the audit record or `undefined`.
|
|
82
86
|
*
|
|
83
|
-
* @throws {Error} If `context.spContext` is falsy.
|
|
87
|
+
* @throws {Error} If `context.spContext` is falsy or signer is invalid.
|
|
84
88
|
*
|
|
85
89
|
* @example
|
|
86
90
|
* ```ts
|
|
@@ -92,6 +96,10 @@ export interface AuditEnvelopeRecord {
|
|
|
92
96
|
* channel: "email",
|
|
93
97
|
* requireTFA: true, // Enable TFA
|
|
94
98
|
* });
|
|
99
|
+
*
|
|
100
|
+
* if (record) {
|
|
101
|
+
* console.log("Signed successfully!");
|
|
102
|
+
* }
|
|
95
103
|
* ```
|
|
96
104
|
*/
|
|
97
105
|
export declare function promptAndGenerateSecureAudit(context: SignerContext, modalTitle?: string, warningMessage?: string): Promise<SharePointAuditRecord | undefined>;
|
|
@@ -115,13 +123,16 @@ export declare function getReportableSignature(compressedSignatureData: string):
|
|
|
115
123
|
* record has not been tampered with, without needing access to the
|
|
116
124
|
* original signature image.
|
|
117
125
|
*
|
|
118
|
-
* **
|
|
119
|
-
*
|
|
120
|
-
*
|
|
126
|
+
* **Deep Sorting:** Payload is deep-sorted to ensure nested objects
|
|
127
|
+
* maintain consistent key ordering. All payload values must be
|
|
128
|
+
* JSON-serializable (no functions, symbols, undefined at top level).
|
|
129
|
+
*
|
|
130
|
+
* **Timestamp Precision:** Timestamps are stored at second precision
|
|
131
|
+
* to avoid hash mismatches from millisecond variations.
|
|
121
132
|
*
|
|
122
133
|
* @param auditRecord - The SharePointAuditRecord returned from `promptAndGenerateSecureAudit`.
|
|
123
134
|
* @param signer - The signer's email address or display name (must match original signer).
|
|
124
|
-
* @param payload - The original payload object that was signed.
|
|
135
|
+
* @param payload - The original payload object that was signed (must be JSON-serializable).
|
|
125
136
|
* @returns `true` if the signature hash is valid and authentic.
|
|
126
137
|
*
|
|
127
138
|
* @example
|