@bdking71/spsignature 1.2.0 → 1.3.3

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,6 +6,8 @@
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)
10
+
9
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.
10
12
 
11
13
  ---
@@ -33,7 +35,7 @@
33
35
 
34
36
  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.
35
37
 
36
- ![Diagram illustrating the 2FA workflow between SPFx, SharePoint, and Power Automate](./img/tfa.jpg)
38
+ ![Diagram illustrating the 2FA workflow between SPFx, SharePoint, and Power Automate](/img/TFA.jpg)
37
39
 
38
40
  ### How the 2FA Workflow Operates
39
41
 
@@ -109,23 +109,34 @@ export declare function promptAndGenerateSecureAudit(context: SignerContext, mod
109
109
  export declare function getReportableSignature(compressedSignatureData: string): string;
110
110
  /**
111
111
  * Re-computes the SHA-256 hash of the audit envelope constructed from
112
- * the supplied parameters and compares it to `storedHash`.
112
+ * the supplied parameters and compares it to the stored hash.
113
113
  *
114
114
  * This allows any consumer to independently verify that a signed
115
115
  * record has not been tampered with, without needing access to the
116
116
  * original signature image.
117
117
  *
118
- * **Note:** Only top-level payload keys are sorted. If your payload
118
+ * **Note:** Only top-level payload keys are sorted. If your payload
119
119
  * contains nested objects whose key order may vary, consider using a
120
120
  * deep-sort utility before calling this function.
121
121
  *
122
- * @param payloadToVerify - The payload that was originally signed.
123
- * @param signer - The signer identifier used at sign time.
124
- * @param timestamp - The ISO-8601 timestamp captured at sign
125
- * time.
126
- * @param _compressedSignatureData - The compressed signature (unused by the
127
- * hash, retained for API symmetry).
128
- * @param storedHash - The SHA-256 hex digest to compare against.
129
- * @returns `true` if the recomputed hash matches `storedHash`.
122
+ * @param auditRecord - The SharePointAuditRecord returned from `promptAndGenerateSecureAudit`.
123
+ * @param signer - The signer's email address or display name (must match original signer).
124
+ * @param payload - The original payload object that was signed.
125
+ * @returns `true` if the signature hash is valid and authentic.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const isValid = await verifySecureAuditRecord(
130
+ * auditRecord,
131
+ * "user@example.com",
132
+ * { amount: 1500, vendor: "Contoso" }
133
+ * );
134
+ *
135
+ * if (isValid) {
136
+ * console.log("Signature is authentic!");
137
+ * } else {
138
+ * console.warn("Signature has been tampered with!");
139
+ * }
140
+ * ```
130
141
  */
131
- export declare function verifySecureAuditRecord(payloadToVerify: Record<string, unknown>, signer: string, timestamp: string, _compressedSignatureData: string, storedHash: string): Promise<boolean>;
142
+ export declare function verifySecureAuditRecord(auditRecord: SharePointAuditRecord, signer: string, payload: Record<string, unknown>): Promise<boolean>;
@@ -563,35 +563,51 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
563
563
  */
564
564
  const updateButtonState = () => {
565
565
  let isCodeValid = true;
566
- if (requireTFA && verificationInput) {
567
- const codeValue = verificationInput.value.trim();
568
- isCodeValid =
569
- codeValue.length === 5 && codeValue === generatedPasscode;
566
+ // Check TFA only if it's required
567
+ if (requireTFA) {
568
+ if (verificationInput) {
569
+ const codeValue = verificationInput.value.trim();
570
+ isCodeValid =
571
+ codeValue.length === 5 && codeValue === generatedPasscode;
572
+ console.log("🔐 TFA Check: Code =", codeValue, "Valid =", isCodeValid);
573
+ }
574
+ else {
575
+ isCodeValid = false;
576
+ console.log("🔐 TFA Check: No input element found");
577
+ }
570
578
  }
579
+ else {
580
+ console.log("🔐 TFA not required - skipping TFA validation");
581
+ }
582
+ // Check signature validity
571
583
  let isSignatureValid = false;
572
584
  if (activeMode === "cached") {
573
585
  isSignatureValid = hasCachedSignature;
586
+ console.log("✍️ Signature Check (CACHED): Valid =", isSignatureValid);
574
587
  }
575
588
  else if (activeMode === "draw") {
576
- isSignatureValid =
577
- hasDrawnContent &&
578
- ctx !== null &&
579
- !isCanvasEmpty(ctx, canvas.width, canvas.height);
589
+ const canvasHasContent = ctx !== null && !isCanvasEmpty(ctx, canvas.width, canvas.height);
590
+ isSignatureValid = hasDrawnContent && canvasHasContent;
591
+ console.log("✍️ Signature Check (DRAW): hasDrawnContent =", hasDrawnContent, "canvasHasContent =", canvasHasContent, "Valid =", isSignatureValid);
580
592
  }
581
593
  else {
582
594
  isSignatureValid = compressedUploadBase64.trim() !== "";
595
+ console.log("✍️ Signature Check (UPLOAD): compressedSize =", compressedUploadBase64.length, "Valid =", isSignatureValid);
583
596
  }
597
+ console.log("🔘 FINAL STATE: Code Valid =", isCodeValid, "Sig Valid =", isSignatureValid, "Should Enable =", isCodeValid && isSignatureValid);
584
598
  if (isCodeValid && isSignatureValid) {
585
599
  signButton.disabled = false;
586
600
  signButton.style.backgroundColor = "#0078d4";
587
601
  signButton.style.cursor = "pointer";
588
602
  signButton.style.opacity = "1";
603
+ console.log("✅ Button ENABLED");
589
604
  }
590
605
  else {
591
606
  signButton.disabled = true;
592
607
  signButton.style.backgroundColor = "#c8c6c4";
593
608
  signButton.style.cursor = "not-allowed";
594
609
  signButton.style.opacity = "0.6";
610
+ console.log("❌ Button DISABLED");
595
611
  }
596
612
  };
597
613
  // -----------------------------------------------------------------
@@ -619,6 +635,7 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
619
635
  : uploadTabBtn;
620
636
  activeBtn.style.color = "#0078d4";
621
637
  activeBtn.style.borderBottom = "3px solid #0078d4";
638
+ console.log("Tab switched to:", mode);
622
639
  updateButtonState();
623
640
  };
624
641
  if (hasCachedSignature) {
@@ -689,8 +706,10 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
689
706
  });
690
707
  if (vResult.success && vResult.itemId) {
691
708
  storedVerificationItemId = vResult.itemId;
709
+ console.log("✅ OTP dispatched successfully");
692
710
  }
693
711
  else {
712
+ console.error("❌ OTP dispatch failed:", vResult.error);
694
713
  alert("Failed to dispatch verification code. Please try again.");
695
714
  }
696
715
  }
@@ -707,10 +726,14 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
707
726
  await triggerSendCode();
708
727
  };
709
728
  vInputRef.oninput = () => {
729
+ console.log("🔐 TFA input changed, checking button state");
710
730
  updateButtonState();
711
731
  };
712
732
  void triggerSendCode();
713
733
  }
734
+ else if (requireTFA && !context.channel) {
735
+ console.warn("⚠️ TFA required but no channel provided");
736
+ }
714
737
  // -----------------------------------------------------------------
715
738
  // Canvas drawing helpers
716
739
  // -----------------------------------------------------------------
@@ -766,12 +789,14 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
766
789
  const pos = getPos(e);
767
790
  ctx.lineTo(pos.x, pos.y);
768
791
  ctx.stroke();
792
+ console.log("✏️ Drawing, updating button state");
769
793
  updateButtonState();
770
794
  e.preventDefault();
771
795
  };
772
796
  /** Ends the current drawing stroke. */
773
797
  const stopDraw = () => {
774
798
  isDrawing = false;
799
+ console.log("✋ Drawing stopped, updating button state");
775
800
  updateButtonState();
776
801
  };
777
802
  canvas.addEventListener("mousedown", startDraw);
@@ -786,6 +811,7 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
786
811
  ctx.fillStyle = "#ffffff";
787
812
  ctx.fillRect(0, 0, canvas.width, canvas.height);
788
813
  hasDrawnContent = false;
814
+ console.log("🧹 Canvas cleared, updating button state");
789
815
  updateButtonState();
790
816
  }
791
817
  };
@@ -836,6 +862,7 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
836
862
  tempCanvas.toDataURL("image/png");
837
863
  uploadPreview.src = compressedUploadBase64;
838
864
  uploadPreview.style.display = "block";
865
+ console.log("📤 File uploaded, updating button state");
839
866
  updateButtonState();
840
867
  }
841
868
  };
@@ -1139,47 +1166,70 @@ function isCanvasEmpty(ctx, width, height) {
1139
1166
  // ---------------------------------------------------------------------------
1140
1167
  /**
1141
1168
  * Re-computes the SHA-256 hash of the audit envelope constructed from
1142
- * the supplied parameters and compares it to `storedHash`.
1169
+ * the supplied parameters and compares it to the stored hash.
1143
1170
  *
1144
1171
  * This allows any consumer to independently verify that a signed
1145
1172
  * record has not been tampered with, without needing access to the
1146
1173
  * original signature image.
1147
1174
  *
1148
- * **Note:** Only top-level payload keys are sorted. If your payload
1175
+ * **Note:** Only top-level payload keys are sorted. If your payload
1149
1176
  * contains nested objects whose key order may vary, consider using a
1150
1177
  * deep-sort utility before calling this function.
1151
1178
  *
1152
- * @param payloadToVerify - The payload that was originally signed.
1153
- * @param signer - The signer identifier used at sign time.
1154
- * @param timestamp - The ISO-8601 timestamp captured at sign
1155
- * time.
1156
- * @param _compressedSignatureData - The compressed signature (unused by the
1157
- * hash, retained for API symmetry).
1158
- * @param storedHash - The SHA-256 hex digest to compare against.
1159
- * @returns `true` if the recomputed hash matches `storedHash`.
1179
+ * @param auditRecord - The SharePointAuditRecord returned from `promptAndGenerateSecureAudit`.
1180
+ * @param signer - The signer's email address or display name (must match original signer).
1181
+ * @param payload - The original payload object that was signed.
1182
+ * @returns `true` if the signature hash is valid and authentic.
1183
+ *
1184
+ * @example
1185
+ * ```ts
1186
+ * const isValid = await verifySecureAuditRecord(
1187
+ * auditRecord,
1188
+ * "user@example.com",
1189
+ * { amount: 1500, vendor: "Contoso" }
1190
+ * );
1191
+ *
1192
+ * if (isValid) {
1193
+ * console.log("Signature is authentic!");
1194
+ * } else {
1195
+ * console.warn("Signature has been tampered with!");
1196
+ * }
1197
+ * ```
1160
1198
  */
1161
- async function verifySecureAuditRecord(payloadToVerify, signer, timestamp, _compressedSignatureData, storedHash) {
1199
+ async function verifySecureAuditRecord(auditRecord, signer, payload) {
1162
1200
  try {
1163
- if (!payloadToVerify || !storedHash)
1201
+ if (!auditRecord || !signer || !payload) {
1202
+ console.warn("verifySecureAuditRecord: Missing required parameters", {
1203
+ auditRecord: !!auditRecord,
1204
+ signer: !!signer,
1205
+ payload: !!payload,
1206
+ });
1164
1207
  return false;
1165
- const sortedPayloadString = JSON.stringify(payloadToVerify, Object.keys(payloadToVerify).sort());
1208
+ }
1209
+ // Sort payload keys for consistent hashing
1210
+ const sortedPayloadString = JSON.stringify(payload, Object.keys(payload).sort());
1211
+ // Reconstruct the canonical audit envelope
1166
1212
  const auditEnvelope = {
1167
1213
  payload: JSON.parse(sortedPayloadString),
1168
1214
  signer: signer.toLowerCase().trim(),
1169
- timestamp: timestamp.trim(),
1215
+ timestamp: auditRecord.signatureTimestamp.trim(),
1170
1216
  };
1217
+ // Hash the audit envelope
1171
1218
  const auditEnvelopeJson = JSON.stringify(auditEnvelope);
1172
1219
  const encoder = new TextEncoder();
1173
1220
  const encodedBytes = encoder.encode(auditEnvelopeJson);
1174
- // Fixed BufferSource type incompatibility:
1175
1221
  const hashBuffer = await window.crypto.subtle.digest("SHA-256", encodedBytes.buffer);
1176
1222
  const hashArray = Array.from(new Uint8Array(hashBuffer));
1177
1223
  const recomputedHash = hashArray
1178
1224
  .map((b) => b.toString(16).padStart(2, "0"))
1179
1225
  .join("");
1180
- return recomputedHash === storedHash.trim();
1226
+ // Compare hashes
1227
+ const isValid = recomputedHash === auditRecord.signatureHash.trim();
1228
+ console.log("verifySecureAuditRecord: Verification result =", isValid, "Recomputed hash =", recomputedHash.substring(0, 16) + "...");
1229
+ return isValid;
1181
1230
  }
1182
- catch (_a) {
1231
+ catch (error) {
1232
+ console.error("verifySecureAuditRecord: Error during verification", error);
1183
1233
  return false;
1184
1234
  }
1185
1235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bdking71/spsignature",
3
- "version": "1.2.0",
3
+ "version": "1.3.3",
4
4
  "description": "SharePoint Framework isolated digital signing, LZW compression, and OTP audit engine.",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.js",