@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 +3 -1
- package/lib/TransactionSigner.d.ts +22 -11
- package/lib/TransactionSigner.js +75 -25
- package/package.json +1 -1
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
|
+

|
|
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
|
-

|
|
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
|
|
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.
|
|
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
|
|
123
|
-
* @param signer
|
|
124
|
-
* @param
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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(
|
|
142
|
+
export declare function verifySecureAuditRecord(auditRecord: SharePointAuditRecord, signer: string, payload: Record<string, unknown>): Promise<boolean>;
|
package/lib/TransactionSigner.js
CHANGED
|
@@ -563,35 +563,51 @@ async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purch
|
|
|
563
563
|
*/
|
|
564
564
|
const updateButtonState = () => {
|
|
565
565
|
let isCodeValid = true;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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.
|
|
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
|
|
1153
|
-
* @param signer
|
|
1154
|
-
* @param
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
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(
|
|
1199
|
+
async function verifySecureAuditRecord(auditRecord, signer, payload) {
|
|
1162
1200
|
try {
|
|
1163
|
-
if (!
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
console.error("verifySecureAuditRecord: Error during verification", error);
|
|
1183
1233
|
return false;
|
|
1184
1234
|
}
|
|
1185
1235
|
}
|
package/package.json
CHANGED