@bdking71/spsignature 1.3.6 → 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 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
- ![Approve Purchase Requisition Screen](/img/SignatureScreen.png)
9
+ ![Signature Screen Screenshot](/img/SignatureScreen.png)
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:** [https://github.com/bdking71/spsignature](https://github.com/bdking71/spsignature)
18
- * **Issues & Feedback:** [https://github.com/bdking71/spsignature/issues](https://github.com/bdking71/spsignature/issues)
19
- * **NPM Package:** [@bdking71/spsignature](https://www.npmjs.com/package/@bdking71/spsignature)
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 (Plain Text / JSON formatted) for effortless integration with Power Automate and Power BI.
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
- ![Diagram illustrating the 2FA workflow between SPFx, SharePoint, and Power Automate](/img/TFA.jpg)
39
-
40
39
  ### How the 2FA Workflow Operates
40
+ ![Diagram illustrating the 2FA workflow between SPFx, SharePoint, and Power Automate](/img/TFA.jpg)
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 (Plain Text format):
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
- ```json
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
- ```bash
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
- ```bash
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
- > **Note:** This method is fully **idempotent**—it can be safely called on every web part load without failing or duplicating columns.
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
- ```typescript
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
- ```typescript
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, // Record/Item ID being signed
156
+ itemID: 1042,
157
157
  signer: this.context.pageContext.user.email,
158
158
  spContext: this.context,
159
- channel: "teams", // Code delivery route: "teams" | "email" | "both"
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 LZW-compressed signature image back into a view or report, use the built-in helper utilities.
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
- ```typescript
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
- payload, // Original payload object
201
- signerEmail, // Signer's email address
202
- timestamp, // ISO-8601 timestamp from the audit record
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 standard PNG base64 Data-URI
214
- const renderableImageSrc: string = getReportableSignature(compressedSignatureData);
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
- // Use directly in an <img> tag:
217
- // <img src={renderableImageSrc} alt="Signature" />
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, its required columns, and item-level security. |
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(payload, signer, timestamp, sigData, hash)` | Re-computes the SHA-256 hash to verify audit-record integrity. |
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: [https://github.com/bdking71/spsignature](https://github.com/bdking71/spsignature)
270
- 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
271
- 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
272
- 4. Push to the Branch (`git push origin feature/AmazingFeature`)
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
  ---
@@ -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
- * **Note:** Only top-level payload keys are sorted. If your payload
119
- * contains nested objects whose key order may vary, consider using a
120
- * deep-sort utility before calling this function.
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