@bdking71/spsignature 1.0.5
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 +148 -0
- package/lib/ProvisioningService.d.ts +9 -0
- package/lib/ProvisioningService.js +62 -0
- package/lib/TransactionSigner.d.ts +24 -0
- package/lib/TransactionSigner.js +871 -0
- package/lib/VerificationService.d.ts +16 -0
- package/lib/VerificationService.js +36 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +19 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @bdking71/spsignature
|
|
2
|
+
# NOT READY FOR PRODUCTION USE! Please do not use at this time. #
|
|
3
|
+
@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.
|
|
4
|
+
|
|
5
|
+
Key Capabilities & Business Value
|
|
6
|
+
|
|
7
|
+
- Zero Infrastructure Overhead: Operates entirely within client-side M365 contexts—no external APIs, azure functions, or third-party storage backends required.
|
|
8
|
+
|
|
9
|
+
- Cross-Platform M365 Integration: Designed for seamless deployment across SharePoint Online, Microsoft Teams, and Viva Connections desktop/mobile experiences.
|
|
10
|
+
|
|
11
|
+
- Integrity & Non-Repudiation: Generates cryptographic SHA-256 hashes bound to user identities, timestamps, and target record IDs to prevent signature tampering.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## Two-Factor Authentication (2FA / TFA) Architecture
|
|
16
|
+
|
|
17
|
+
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 6-digit verification codes via Microsoft Teams, Outlook Email, or both.
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## How The 2FA Workflow Operates-
|
|
22
|
+
- Code Generation: When a user initiates a signing transaction, the component generates a cryptographically secure, time-sensitive verification code (e.g., 849204) and registers a pending transaction state.
|
|
23
|
+
|
|
24
|
+
- Power Automate Trigger: The component triggers a light HTTP webhook or writes a record to a dedicated SharePoint verification list.
|
|
25
|
+
|
|
26
|
+
- Multi-Channel Delivery: The Power Automate flow instantly dispatches the code to the signer via:
|
|
27
|
+
|
|
28
|
+
- Microsoft Teams: Direct Adaptive Card or Activity Feed notification.
|
|
29
|
+
|
|
30
|
+
- Outlook Email: High-priority internal notification.
|
|
31
|
+
|
|
32
|
+
- Validation & Signing: The signer enters the 6-digit code into the @bdking71/spsignature UI component. Upon successful validation, the final signed JSON payload is generated and stored in the target record.
|
|
33
|
+
|
|
34
|
+
## Signature Data Schema
|
|
35
|
+
|
|
36
|
+
The module returns a single, structured JSON object designed to be stored directly inside a SharePoint Multiple lines of text field (Plain Text format):
|
|
37
|
+
|
|
38
|
+
```JSON
|
|
39
|
+
{
|
|
40
|
+
"signatureHash": "e3b0c4429 ...",
|
|
41
|
+
"signatureData": "data:image/png;base64,iVB....",
|
|
42
|
+
"signatureTimestamp": "2026-08-31T19:55:06.121Z",
|
|
43
|
+
"verificationItemId": 1
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Toolchain & Compatibility Matrix
|
|
49
|
+
|
|
50
|
+
This project requires a precise local environment build. Strict adherence to the versioning matrix is mandatory to prevent toolchain compilation errors.
|
|
51
|
+
|
|
52
|
+
* **SPFx Version:** `v1.22.1`
|
|
53
|
+
* **Node.js:** `v18.17.1` (Recommended use of `nvm` or `nvs`)
|
|
54
|
+
* **Gulp CLI:** `v4.x`
|
|
55
|
+
* **Primary UI Framework:** React `v17.0.1` / Fluent UI `v8.x`
|
|
56
|
+
|
|
57
|
+
## Quick Start & Local Development
|
|
58
|
+
|
|
59
|
+
Execute these commands in sequence to establish your local development runtime:
|
|
60
|
+
|
|
61
|
+
## Integration & Usage Guide
|
|
62
|
+
|
|
63
|
+
### Provision Infrastructure
|
|
64
|
+
Call `ensurePendingVerificationsList` during the WebPart lifecycle (e.g., inside `onInit()`) to automatically ensure the required `PendingVerifications` SharePoint list and columns exist on the site collection[cite: 2].
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { ensurePendingVerificationsList } from "@bdking71/spsignature";
|
|
68
|
+
|
|
69
|
+
public async onInit(): Promise<void> {
|
|
70
|
+
await super.onInit();
|
|
71
|
+
|
|
72
|
+
// Ensures 'PendingVerifications' list with 'Passcode' and 'Channel' fields exists
|
|
73
|
+
await ensurePendingVerificationsList({ context: this.context });
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Launch Signature Modal & Capture Audit
|
|
78
|
+
|
|
79
|
+
Execute promptAndGenerateSecureAudit within your component or event handler. This renders an enterprise modal overlay that handles draw, upload, or cached signature selection, triggers 2FA code delivery, validates the 5-digit passcode, and outputs a cryptographic audit payload.
|
|
80
|
+
|
|
81
|
+
```TypeScript
|
|
82
|
+
import {
|
|
83
|
+
promptAndGenerateSecureAudit,
|
|
84
|
+
SharePointAuditRecord
|
|
85
|
+
} from "@bdking71/spsignature";
|
|
86
|
+
|
|
87
|
+
private handleSignAction = async (): Promise<void> => {
|
|
88
|
+
const auditRecord: SharePointAuditRecord | undefined = await promptAndGenerateSecureAudit(
|
|
89
|
+
{
|
|
90
|
+
itemID: 1042, // Record/Item ID being signed
|
|
91
|
+
signer: this.context.pageContext.user.email,
|
|
92
|
+
spContext: this.context,
|
|
93
|
+
channel: "teams", // Code delivery route: "teams" | "email" | "both"
|
|
94
|
+
requireTFA: true,
|
|
95
|
+
payload: {
|
|
96
|
+
DocumentTitle: "Purchase Requisition #4402",
|
|
97
|
+
TotalCost: "$12,450.00",
|
|
98
|
+
ApprovedBy: this.context.pageContext.user.displayName
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"Approve Purchase Requisition",
|
|
102
|
+
"Please review your entry and apply your signature to finalize this record."
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (auditRecord) {
|
|
106
|
+
// Save auditRecord JSON payload directly to your SharePoint multiline text column
|
|
107
|
+
console.log("Signature Hash:", auditRecord.signatureHash);
|
|
108
|
+
console.log("Compressed Image Data:", auditRecord.signatureData);
|
|
109
|
+
console.log("Timestamp:", auditRecord.signatureTimestamp);
|
|
110
|
+
console.log("Verification Log ID:", auditRecord.verificationItemId);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Verification & Image Reconstruction
|
|
116
|
+
|
|
117
|
+
To verify non-repudiation or render the stored LZW-compressed signature image back into a view or report, use the built-in helper utilities.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import {
|
|
121
|
+
verifySecureAuditRecord,
|
|
122
|
+
getReportableSignature
|
|
123
|
+
} from "@bdking71/spsignature";
|
|
124
|
+
|
|
125
|
+
// 1. Re-calculate SHA-256 hash to verify record integrity
|
|
126
|
+
const isValid: boolean = await verifySecureAuditRecord(
|
|
127
|
+
payload,
|
|
128
|
+
signerEmail,
|
|
129
|
+
timestamp,
|
|
130
|
+
compressedSignatureData,
|
|
131
|
+
storedHash
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// 2. Decompress LZW signature string into a standard PNG base64 Data-URI
|
|
135
|
+
const renderableImageSrc: string = getReportableSignature(compressedSignatureData);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
# License
|
|
139
|
+
|
|
140
|
+
MIT License
|
|
141
|
+
|
|
142
|
+
Copyright (C) 2012 Veselin Todorov
|
|
143
|
+
|
|
144
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
145
|
+
|
|
146
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
147
|
+
|
|
148
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "@pnp/sp/webs";
|
|
2
|
+
import "@pnp/sp/lists";
|
|
3
|
+
import "@pnp/sp/fields";
|
|
4
|
+
import "@pnp/sp/views";
|
|
5
|
+
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
|
6
|
+
export interface IEnsurePendingVerificationsListProps {
|
|
7
|
+
context: WebPartContext;
|
|
8
|
+
}
|
|
9
|
+
export declare const ensurePendingVerificationsList: (props: IEnsurePendingVerificationsListProps) => Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensurePendingVerificationsList = void 0;
|
|
4
|
+
const sp_1 = require("@pnp/sp");
|
|
5
|
+
require("@pnp/sp/webs");
|
|
6
|
+
require("@pnp/sp/lists");
|
|
7
|
+
require("@pnp/sp/fields");
|
|
8
|
+
require("@pnp/sp/views");
|
|
9
|
+
const ensurePendingVerificationsList = async (props) => {
|
|
10
|
+
if (!props.context) {
|
|
11
|
+
throw new Error("Execution restricted: Valid WebPartContext is required.");
|
|
12
|
+
}
|
|
13
|
+
const sp = (0, sp_1.spfi)().using((0, sp_1.SPFx)(props.context));
|
|
14
|
+
const LIST_NAME = "PendingVerifications";
|
|
15
|
+
const LIST_DESCRIPTION = "Stores temporary PIN verification codes for document signatures.";
|
|
16
|
+
const list = sp.web.lists.getByTitle(LIST_NAME);
|
|
17
|
+
let listCreated = false;
|
|
18
|
+
try {
|
|
19
|
+
await list();
|
|
20
|
+
}
|
|
21
|
+
catch (_a) {
|
|
22
|
+
await sp.web.lists.add(LIST_NAME, LIST_DESCRIPTION, 100, true);
|
|
23
|
+
listCreated = true;
|
|
24
|
+
}
|
|
25
|
+
if (!listCreated) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const ensureField = async (fieldName, createFieldFn) => {
|
|
29
|
+
try {
|
|
30
|
+
await createFieldFn();
|
|
31
|
+
}
|
|
32
|
+
catch (creationErr) {
|
|
33
|
+
throw creationErr;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
await ensureField("Passcode", async () => {
|
|
37
|
+
await list.fields.addText("Passcode", {
|
|
38
|
+
Required: true,
|
|
39
|
+
Description: "Temporary 6-digit signature verification code."
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
await ensureField("Channel", async () => {
|
|
43
|
+
await list.fields.addChoice("Channel", {
|
|
44
|
+
Choices: ["email", "teams", "both"],
|
|
45
|
+
Required: true,
|
|
46
|
+
FillInChoice: false,
|
|
47
|
+
Description: "Target notification delivery route."
|
|
48
|
+
});
|
|
49
|
+
const channelField = list.fields.getByInternalNameOrTitle("Channel");
|
|
50
|
+
await channelField.update({ DefaultValue: "teams" });
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await list.update({
|
|
54
|
+
ReadSecurity: 2,
|
|
55
|
+
WriteSecurity: 2
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (secErr) {
|
|
59
|
+
throw secErr;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.ensurePendingVerificationsList = ensurePendingVerificationsList;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
|
2
|
+
import { DeliveryChannel } from "./VerificationService";
|
|
3
|
+
export interface SignerContext {
|
|
4
|
+
itemID: number;
|
|
5
|
+
signer: string;
|
|
6
|
+
payload: Record<string, unknown>;
|
|
7
|
+
spContext: WebPartContext;
|
|
8
|
+
channel?: DeliveryChannel;
|
|
9
|
+
requireTFA?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SharePointAuditRecord {
|
|
12
|
+
signatureHash: string;
|
|
13
|
+
signatureData: string;
|
|
14
|
+
signatureTimestamp: string;
|
|
15
|
+
verificationItemId: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AuditEnvelopeRecord {
|
|
18
|
+
payload: Record<string, unknown>;
|
|
19
|
+
signer: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function promptAndGenerateSecureAudit(context: SignerContext, modalTitle?: string, warningMessage?: string): Promise<SharePointAuditRecord | undefined>;
|
|
23
|
+
export declare function getReportableSignature(compressedSignatureData: string): string;
|
|
24
|
+
export declare function verifySecureAuditRecord(payloadToVerify: Record<string, unknown>, signer: string, timestamp: string, compressedSignatureData: string, storedHash: string): Promise<boolean>;
|
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifySecureAuditRecord = exports.getReportableSignature = exports.promptAndGenerateSecureAudit = void 0;
|
|
4
|
+
const VerificationService_1 = require("./VerificationService");
|
|
5
|
+
const STORAGE_KEY = "secure_audit_cached_signature_v1";
|
|
6
|
+
function generateFiveDigitPasscode() {
|
|
7
|
+
const array = new Uint32Array(1);
|
|
8
|
+
window.crypto.getRandomValues(array);
|
|
9
|
+
const code = (array[0] % 90000) + 10000;
|
|
10
|
+
return code.toString();
|
|
11
|
+
}
|
|
12
|
+
async function promptAndGenerateSecureAudit(context, modalTitle = "Approve Purchase Requisition", warningMessage = "Please review your entry and apply your signature to finalize this record.") {
|
|
13
|
+
if (!context.spContext) {
|
|
14
|
+
throw new Error("Execution restricted: Valid WebPartContext must be supplied.");
|
|
15
|
+
}
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const requireTFA = context.requireTFA !== false;
|
|
18
|
+
const generatedPasscode = generateFiveDigitPasscode();
|
|
19
|
+
let storedVerificationItemId = context.itemID;
|
|
20
|
+
const overlay = document.createElement("div");
|
|
21
|
+
overlay.style.position = "fixed";
|
|
22
|
+
overlay.style.top = "0";
|
|
23
|
+
overlay.style.left = "0";
|
|
24
|
+
overlay.style.width = "100vw";
|
|
25
|
+
overlay.style.height = "100vh";
|
|
26
|
+
overlay.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
|
|
27
|
+
overlay.style.zIndex = "999999";
|
|
28
|
+
overlay.style.display = "flex";
|
|
29
|
+
overlay.style.alignItems = "center";
|
|
30
|
+
overlay.style.justifyContent = "center";
|
|
31
|
+
overlay.style.fontFamily = "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif";
|
|
32
|
+
overlay.style.backdropFilter = "blur(3px)";
|
|
33
|
+
const modalBox = document.createElement("div");
|
|
34
|
+
modalBox.style.backgroundColor = "#ffffff";
|
|
35
|
+
modalBox.style.padding = "0";
|
|
36
|
+
modalBox.style.borderRadius = "8px";
|
|
37
|
+
modalBox.style.maxWidth = "640px";
|
|
38
|
+
modalBox.style.width = "90%";
|
|
39
|
+
modalBox.style.boxShadow = "0 8px 32px rgba(0,0,0,0.3)";
|
|
40
|
+
modalBox.style.overflow = "hidden";
|
|
41
|
+
modalBox.style.maxHeight = "90vh";
|
|
42
|
+
modalBox.style.display = "flex";
|
|
43
|
+
modalBox.style.flexDirection = "column";
|
|
44
|
+
const headerSection = document.createElement("div");
|
|
45
|
+
headerSection.style.backgroundColor = "#0078d4";
|
|
46
|
+
headerSection.style.padding = "20px 24px";
|
|
47
|
+
headerSection.style.borderBottom = "3px solid #005a9e";
|
|
48
|
+
const title = document.createElement("h2");
|
|
49
|
+
title.innerText = modalTitle;
|
|
50
|
+
title.style.margin = "0";
|
|
51
|
+
title.style.color = "#ffffff";
|
|
52
|
+
title.style.fontSize = "20px";
|
|
53
|
+
title.style.fontWeight = "600";
|
|
54
|
+
title.style.letterSpacing = "0.3px";
|
|
55
|
+
headerSection.appendChild(title);
|
|
56
|
+
const contentSection = document.createElement("div");
|
|
57
|
+
contentSection.style.padding = "24px";
|
|
58
|
+
contentSection.style.backgroundColor = "#fafafa";
|
|
59
|
+
contentSection.style.overflowY = "auto";
|
|
60
|
+
contentSection.style.flex = "1";
|
|
61
|
+
const body = document.createElement("p");
|
|
62
|
+
body.innerHTML = warningMessage;
|
|
63
|
+
body.style.fontSize = "14px";
|
|
64
|
+
body.style.color = "#323130";
|
|
65
|
+
body.style.lineHeight = "1.6";
|
|
66
|
+
body.style.margin = "0 0 20px 0";
|
|
67
|
+
const signButton = document.createElement("button");
|
|
68
|
+
signButton.innerText = "I Agree and Sign";
|
|
69
|
+
signButton.style.padding = "10px 24px";
|
|
70
|
+
signButton.style.backgroundColor = "#0078d4";
|
|
71
|
+
signButton.style.color = "#ffffff";
|
|
72
|
+
signButton.style.border = "none";
|
|
73
|
+
signButton.style.cursor = "pointer";
|
|
74
|
+
signButton.style.fontSize = "14px";
|
|
75
|
+
signButton.style.fontWeight = "600";
|
|
76
|
+
signButton.style.borderRadius = "4px";
|
|
77
|
+
signButton.style.transition = "all 0.2s ease";
|
|
78
|
+
const verificationContainer = document.createElement("div");
|
|
79
|
+
if (requireTFA) {
|
|
80
|
+
verificationContainer.style.marginBottom = "20px";
|
|
81
|
+
verificationContainer.style.padding = "16px";
|
|
82
|
+
verificationContainer.style.backgroundColor = "#fff8e1";
|
|
83
|
+
verificationContainer.style.border = "1px solid #ffd54f";
|
|
84
|
+
verificationContainer.style.borderRadius = "6px";
|
|
85
|
+
verificationContainer.style.boxShadow = "0 2px 4px rgba(0,0,0,0.05)";
|
|
86
|
+
const verificationHeader = document.createElement("div");
|
|
87
|
+
verificationHeader.style.display = "flex";
|
|
88
|
+
verificationHeader.style.alignItems = "center";
|
|
89
|
+
verificationHeader.style.marginBottom = "12px";
|
|
90
|
+
verificationHeader.style.gap = "8px";
|
|
91
|
+
const lockIcon = document.createElement("span");
|
|
92
|
+
lockIcon.innerHTML = "🔐";
|
|
93
|
+
lockIcon.style.fontSize = "18px";
|
|
94
|
+
const verificationLabel = document.createElement("label");
|
|
95
|
+
verificationLabel.innerText = "Two-Factor Authentication";
|
|
96
|
+
verificationLabel.style.display = "block";
|
|
97
|
+
verificationLabel.style.fontSize = "13px";
|
|
98
|
+
verificationLabel.style.fontWeight = "700";
|
|
99
|
+
verificationLabel.style.color = "#323130";
|
|
100
|
+
verificationLabel.style.margin = "0";
|
|
101
|
+
verificationHeader.appendChild(lockIcon);
|
|
102
|
+
verificationHeader.appendChild(verificationLabel);
|
|
103
|
+
const verificationDescription = document.createElement("p");
|
|
104
|
+
verificationDescription.innerText = `A 5-digit verification code has been sent to ${context.channel === "teams" ? "Microsoft Teams" : "your email"}.`;
|
|
105
|
+
verificationDescription.style.fontSize = "12px";
|
|
106
|
+
verificationDescription.style.color = "#605e5c";
|
|
107
|
+
verificationDescription.style.margin = "0 0 12px 0";
|
|
108
|
+
verificationDescription.style.lineHeight = "1.5";
|
|
109
|
+
const verificationRow = document.createElement("div");
|
|
110
|
+
verificationRow.style.display = "flex";
|
|
111
|
+
verificationRow.style.gap = "10px";
|
|
112
|
+
const verificationInput = document.createElement("input");
|
|
113
|
+
verificationInput.type = "text";
|
|
114
|
+
verificationInput.maxLength = 5;
|
|
115
|
+
verificationInput.placeholder = "Enter 5-digit code";
|
|
116
|
+
verificationInput.style.flex = "1";
|
|
117
|
+
verificationInput.style.padding = "10px 12px";
|
|
118
|
+
verificationInput.style.fontSize = "14px";
|
|
119
|
+
verificationInput.style.boxSizing = "border-box";
|
|
120
|
+
verificationInput.style.border = "2px solid #d1d1d1";
|
|
121
|
+
verificationInput.style.borderRadius = "4px";
|
|
122
|
+
verificationInput.style.outline = "none";
|
|
123
|
+
verificationInput.style.transition = "border-color 0.2s ease";
|
|
124
|
+
verificationInput.style.textAlign = "center";
|
|
125
|
+
verificationInput.style.letterSpacing = "2px";
|
|
126
|
+
verificationInput.style.fontWeight = "600";
|
|
127
|
+
verificationInput.onfocus = () => {
|
|
128
|
+
verificationInput.style.borderColor = "#0078d4";
|
|
129
|
+
};
|
|
130
|
+
verificationInput.onblur = () => {
|
|
131
|
+
verificationInput.style.borderColor = "#d1d1d1";
|
|
132
|
+
};
|
|
133
|
+
const sendCodeButton = document.createElement("button");
|
|
134
|
+
sendCodeButton.innerText = "Resend Code";
|
|
135
|
+
sendCodeButton.type = "button";
|
|
136
|
+
sendCodeButton.style.padding = "10px 16px";
|
|
137
|
+
sendCodeButton.style.fontSize = "12px";
|
|
138
|
+
sendCodeButton.style.backgroundColor = "#ffffff";
|
|
139
|
+
sendCodeButton.style.color = "#0078d4";
|
|
140
|
+
sendCodeButton.style.border = "2px solid #0078d4";
|
|
141
|
+
sendCodeButton.style.cursor = "pointer";
|
|
142
|
+
sendCodeButton.style.whiteSpace = "nowrap";
|
|
143
|
+
sendCodeButton.style.borderRadius = "4px";
|
|
144
|
+
sendCodeButton.style.fontWeight = "600";
|
|
145
|
+
sendCodeButton.style.transition = "all 0.2s ease";
|
|
146
|
+
sendCodeButton.onmouseover = () => {
|
|
147
|
+
sendCodeButton.style.backgroundColor = "#0078d4";
|
|
148
|
+
sendCodeButton.style.color = "#ffffff";
|
|
149
|
+
};
|
|
150
|
+
sendCodeButton.onmouseout = () => {
|
|
151
|
+
if (!sendCodeButton.disabled) {
|
|
152
|
+
sendCodeButton.style.backgroundColor = "#ffffff";
|
|
153
|
+
sendCodeButton.style.color = "#0078d4";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
verificationRow.appendChild(verificationInput);
|
|
157
|
+
verificationRow.appendChild(sendCodeButton);
|
|
158
|
+
verificationContainer.appendChild(verificationHeader);
|
|
159
|
+
verificationContainer.appendChild(verificationDescription);
|
|
160
|
+
verificationContainer.appendChild(verificationRow);
|
|
161
|
+
}
|
|
162
|
+
const cachedCompressedSig = localStorage.getItem(STORAGE_KEY);
|
|
163
|
+
const hasCachedSignature = cachedCompressedSig !== null && cachedCompressedSig.trim() !== "";
|
|
164
|
+
const decompressedCachedDataUri = (hasCachedSignature && cachedCompressedSig)
|
|
165
|
+
? lzwDecompress(cachedCompressedSig)
|
|
166
|
+
: "";
|
|
167
|
+
const signatureSection = document.createElement("div");
|
|
168
|
+
signatureSection.style.marginBottom = "20px";
|
|
169
|
+
signatureSection.style.padding = "16px";
|
|
170
|
+
signatureSection.style.backgroundColor = "#ffffff";
|
|
171
|
+
signatureSection.style.border = "1px solid #d1d1d1";
|
|
172
|
+
signatureSection.style.borderRadius = "6px";
|
|
173
|
+
signatureSection.style.boxShadow = "0 2px 4px rgba(0,0,0,0.05)";
|
|
174
|
+
const signatureHeader = document.createElement("div");
|
|
175
|
+
signatureHeader.style.marginBottom = "12px";
|
|
176
|
+
const signatureLabel = document.createElement("h3");
|
|
177
|
+
signatureLabel.innerText = "Digital Signature";
|
|
178
|
+
signatureLabel.style.fontSize = "14px";
|
|
179
|
+
signatureLabel.style.fontWeight = "700";
|
|
180
|
+
signatureLabel.style.color = "#323130";
|
|
181
|
+
signatureLabel.style.margin = "0 0 4px 0";
|
|
182
|
+
const signatureSubtext = document.createElement("p");
|
|
183
|
+
signatureSubtext.innerText = "Choose how you'd like to provide your signature";
|
|
184
|
+
signatureSubtext.style.fontSize = "12px";
|
|
185
|
+
signatureSubtext.style.color = "#605e5c";
|
|
186
|
+
signatureSubtext.style.margin = "0";
|
|
187
|
+
signatureHeader.appendChild(signatureLabel);
|
|
188
|
+
signatureHeader.appendChild(signatureSubtext);
|
|
189
|
+
const cacheControlBox = document.createElement("div");
|
|
190
|
+
cacheControlBox.style.marginTop = "16px";
|
|
191
|
+
cacheControlBox.style.padding = "12px";
|
|
192
|
+
cacheControlBox.style.backgroundColor = "#f3f2f1";
|
|
193
|
+
cacheControlBox.style.border = "1px solid #d1d1d1";
|
|
194
|
+
cacheControlBox.style.borderRadius = "4px";
|
|
195
|
+
cacheControlBox.style.fontSize = "12px";
|
|
196
|
+
const cacheCheckboxLabel = document.createElement("label");
|
|
197
|
+
cacheCheckboxLabel.style.display = "flex";
|
|
198
|
+
cacheCheckboxLabel.style.alignItems = "center";
|
|
199
|
+
cacheCheckboxLabel.style.gap = "8px";
|
|
200
|
+
cacheCheckboxLabel.style.cursor = "pointer";
|
|
201
|
+
const cacheCheckbox = document.createElement("input");
|
|
202
|
+
cacheCheckbox.type = "checkbox";
|
|
203
|
+
cacheCheckbox.checked = true;
|
|
204
|
+
cacheCheckbox.style.cursor = "pointer";
|
|
205
|
+
cacheCheckbox.style.width = "16px";
|
|
206
|
+
cacheCheckbox.style.height = "16px";
|
|
207
|
+
const cacheCheckboxText = document.createElement("span");
|
|
208
|
+
cacheCheckboxText.innerText = "Remember my signature on this device for future transactions";
|
|
209
|
+
cacheCheckboxText.style.color = "#323130";
|
|
210
|
+
cacheCheckboxText.style.fontSize = "12px";
|
|
211
|
+
cacheCheckboxLabel.appendChild(cacheCheckbox);
|
|
212
|
+
cacheCheckboxLabel.appendChild(cacheCheckboxText);
|
|
213
|
+
cacheControlBox.appendChild(cacheCheckboxLabel);
|
|
214
|
+
const cachedNoticeContainer = document.createElement("div");
|
|
215
|
+
cachedNoticeContainer.style.marginBottom = "16px";
|
|
216
|
+
cachedNoticeContainer.style.padding = "16px";
|
|
217
|
+
cachedNoticeContainer.style.backgroundColor = "#e6f4ff";
|
|
218
|
+
cachedNoticeContainer.style.border = "1px solid #0078d4";
|
|
219
|
+
cachedNoticeContainer.style.borderRadius = "6px";
|
|
220
|
+
cachedNoticeContainer.style.display = hasCachedSignature ? "block" : "none";
|
|
221
|
+
const cachedNoticeText = document.createElement("div");
|
|
222
|
+
cachedNoticeText.innerText = "✓ Saved signature on file";
|
|
223
|
+
cachedNoticeText.style.fontSize = "13px";
|
|
224
|
+
cachedNoticeText.style.marginBottom = "12px";
|
|
225
|
+
cachedNoticeText.style.fontWeight = "600";
|
|
226
|
+
cachedNoticeText.style.color = "#0078d4";
|
|
227
|
+
const cachedPreviewImage = document.createElement("img");
|
|
228
|
+
cachedPreviewImage.src = decompressedCachedDataUri;
|
|
229
|
+
cachedPreviewImage.style.maxWidth = "100%";
|
|
230
|
+
cachedPreviewImage.style.height = "auto";
|
|
231
|
+
cachedPreviewImage.style.minHeight = "80px";
|
|
232
|
+
cachedPreviewImage.style.maxHeight = "120px";
|
|
233
|
+
cachedPreviewImage.style.display = "block";
|
|
234
|
+
cachedPreviewImage.style.marginBottom = "12px";
|
|
235
|
+
cachedPreviewImage.style.border = "2px solid #0078d4";
|
|
236
|
+
cachedPreviewImage.style.backgroundColor = "#ffffff";
|
|
237
|
+
cachedPreviewImage.style.borderRadius = "4px";
|
|
238
|
+
cachedPreviewImage.style.padding = "8px";
|
|
239
|
+
cachedPreviewImage.style.objectFit = "contain";
|
|
240
|
+
const removeCachedBtn = document.createElement("button");
|
|
241
|
+
removeCachedBtn.innerText = "Remove Saved Signature";
|
|
242
|
+
removeCachedBtn.style.padding = "6px 12px";
|
|
243
|
+
removeCachedBtn.style.fontSize = "11px";
|
|
244
|
+
removeCachedBtn.style.backgroundColor = "#ffffff";
|
|
245
|
+
removeCachedBtn.style.border = "1px solid #a4262c";
|
|
246
|
+
removeCachedBtn.style.color = "#a4262c";
|
|
247
|
+
removeCachedBtn.style.cursor = "pointer";
|
|
248
|
+
removeCachedBtn.style.borderRadius = "4px";
|
|
249
|
+
removeCachedBtn.style.fontWeight = "600";
|
|
250
|
+
removeCachedBtn.style.transition = "all 0.2s ease";
|
|
251
|
+
removeCachedBtn.onmouseover = () => {
|
|
252
|
+
removeCachedBtn.style.backgroundColor = "#a4262c";
|
|
253
|
+
removeCachedBtn.style.color = "#ffffff";
|
|
254
|
+
};
|
|
255
|
+
removeCachedBtn.onmouseout = () => {
|
|
256
|
+
removeCachedBtn.style.backgroundColor = "#ffffff";
|
|
257
|
+
removeCachedBtn.style.color = "#a4262c";
|
|
258
|
+
};
|
|
259
|
+
cachedNoticeContainer.appendChild(cachedNoticeText);
|
|
260
|
+
cachedNoticeContainer.appendChild(cachedPreviewImage);
|
|
261
|
+
cachedNoticeContainer.appendChild(removeCachedBtn);
|
|
262
|
+
const modeContainer = document.createElement("div");
|
|
263
|
+
modeContainer.style.display = "flex";
|
|
264
|
+
modeContainer.style.gap = "8px";
|
|
265
|
+
modeContainer.style.marginBottom = "16px";
|
|
266
|
+
modeContainer.style.borderBottom = "2px solid #edebe9";
|
|
267
|
+
const createTab = (text, isActive) => {
|
|
268
|
+
const tab = document.createElement("button");
|
|
269
|
+
tab.innerText = text;
|
|
270
|
+
tab.style.padding = "10px 16px";
|
|
271
|
+
tab.style.fontSize = "13px";
|
|
272
|
+
tab.style.cursor = "pointer";
|
|
273
|
+
tab.style.fontWeight = "600";
|
|
274
|
+
tab.style.backgroundColor = "transparent";
|
|
275
|
+
tab.style.color = isActive ? "#0078d4" : "#605e5c";
|
|
276
|
+
tab.style.border = "none";
|
|
277
|
+
tab.style.borderBottom = isActive ? "3px solid #0078d4" : "3px solid transparent";
|
|
278
|
+
tab.style.transition = "all 0.2s ease";
|
|
279
|
+
tab.style.outline = "none";
|
|
280
|
+
tab.onmouseover = () => {
|
|
281
|
+
if (!isActive) {
|
|
282
|
+
tab.style.color = "#0078d4";
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
tab.onmouseout = () => {
|
|
286
|
+
if (!isActive) {
|
|
287
|
+
tab.style.color = "#605e5c";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
return tab;
|
|
291
|
+
};
|
|
292
|
+
const cachedTabBtn = createTab("Saved", hasCachedSignature);
|
|
293
|
+
cachedTabBtn.style.display = hasCachedSignature ? "inline-block" : "none";
|
|
294
|
+
const drawTabBtn = createTab("Draw New", !hasCachedSignature);
|
|
295
|
+
const uploadTabBtn = createTab("Upload Image", false);
|
|
296
|
+
if (hasCachedSignature) {
|
|
297
|
+
modeContainer.appendChild(cachedTabBtn);
|
|
298
|
+
}
|
|
299
|
+
modeContainer.appendChild(drawTabBtn);
|
|
300
|
+
modeContainer.appendChild(uploadTabBtn);
|
|
301
|
+
const cachedPanel = document.createElement("div");
|
|
302
|
+
cachedPanel.style.display = hasCachedSignature ? "block" : "none";
|
|
303
|
+
cachedPanel.appendChild(cachedNoticeContainer);
|
|
304
|
+
const drawPanel = document.createElement("div");
|
|
305
|
+
drawPanel.style.display = hasCachedSignature ? "none" : "block";
|
|
306
|
+
const canvasContainer = document.createElement("div");
|
|
307
|
+
canvasContainer.style.backgroundColor = "#ffffff";
|
|
308
|
+
canvasContainer.style.padding = "16px";
|
|
309
|
+
canvasContainer.style.borderRadius = "6px";
|
|
310
|
+
canvasContainer.style.border = "2px dashed #d1d1d1";
|
|
311
|
+
const canvasInstructions = document.createElement("p");
|
|
312
|
+
canvasInstructions.innerText = "Draw your signature below:";
|
|
313
|
+
canvasInstructions.style.fontSize = "12px";
|
|
314
|
+
canvasInstructions.style.color = "#605e5c";
|
|
315
|
+
canvasInstructions.style.margin = "0 0 8px 0";
|
|
316
|
+
canvasInstructions.style.fontWeight = "600";
|
|
317
|
+
const canvas = document.createElement("canvas");
|
|
318
|
+
canvas.width = 560;
|
|
319
|
+
canvas.height = 160;
|
|
320
|
+
canvas.style.border = "1px solid #d1d1d1";
|
|
321
|
+
canvas.style.backgroundColor = "#ffffff";
|
|
322
|
+
canvas.style.cursor = "crosshair";
|
|
323
|
+
canvas.style.display = "block";
|
|
324
|
+
canvas.style.width = "100%";
|
|
325
|
+
canvas.style.borderRadius = "4px";
|
|
326
|
+
canvas.style.boxShadow = "inset 0 1px 3px rgba(0,0,0,0.1)";
|
|
327
|
+
const clearButton = document.createElement("button");
|
|
328
|
+
clearButton.innerText = "Clear Canvas";
|
|
329
|
+
clearButton.style.marginTop = "10px";
|
|
330
|
+
clearButton.style.padding = "8px 16px";
|
|
331
|
+
clearButton.style.fontSize = "12px";
|
|
332
|
+
clearButton.style.backgroundColor = "#f3f2f1";
|
|
333
|
+
clearButton.style.border = "1px solid #d1d1d1";
|
|
334
|
+
clearButton.style.cursor = "pointer";
|
|
335
|
+
clearButton.style.borderRadius = "4px";
|
|
336
|
+
clearButton.style.fontWeight = "600";
|
|
337
|
+
clearButton.style.color = "#323130";
|
|
338
|
+
clearButton.style.transition = "all 0.2s ease";
|
|
339
|
+
clearButton.onmouseover = () => {
|
|
340
|
+
clearButton.style.backgroundColor = "#e1dfdd";
|
|
341
|
+
};
|
|
342
|
+
clearButton.onmouseout = () => {
|
|
343
|
+
clearButton.style.backgroundColor = "#f3f2f1";
|
|
344
|
+
};
|
|
345
|
+
canvasContainer.appendChild(canvasInstructions);
|
|
346
|
+
canvasContainer.appendChild(canvas);
|
|
347
|
+
canvasContainer.appendChild(clearButton);
|
|
348
|
+
drawPanel.appendChild(canvasContainer);
|
|
349
|
+
const uploadPanel = document.createElement("div");
|
|
350
|
+
uploadPanel.style.display = "none";
|
|
351
|
+
uploadPanel.style.padding = "32px 24px";
|
|
352
|
+
uploadPanel.style.border = "2px dashed #d1d1d1";
|
|
353
|
+
uploadPanel.style.textAlign = "center";
|
|
354
|
+
uploadPanel.style.backgroundColor = "#ffffff";
|
|
355
|
+
uploadPanel.style.borderRadius = "6px";
|
|
356
|
+
uploadPanel.style.transition = "all 0.2s ease";
|
|
357
|
+
uploadPanel.style.minHeight = "200px";
|
|
358
|
+
uploadPanel.style.display = "none";
|
|
359
|
+
uploadPanel.style.flexDirection = "column";
|
|
360
|
+
uploadPanel.style.alignItems = "center";
|
|
361
|
+
uploadPanel.style.justifyContent = "center";
|
|
362
|
+
const uploadIcon = document.createElement("div");
|
|
363
|
+
uploadIcon.innerHTML = "📁";
|
|
364
|
+
uploadIcon.style.fontSize = "48px";
|
|
365
|
+
uploadIcon.style.marginBottom = "16px";
|
|
366
|
+
const uploadText = document.createElement("p");
|
|
367
|
+
uploadText.innerText = "Click to upload or drag and drop";
|
|
368
|
+
uploadText.style.fontSize = "14px";
|
|
369
|
+
uploadText.style.color = "#605e5c";
|
|
370
|
+
uploadText.style.margin = "0 0 8px 0";
|
|
371
|
+
uploadText.style.fontWeight = "600";
|
|
372
|
+
const uploadSubtext = document.createElement("p");
|
|
373
|
+
uploadSubtext.innerText = "PNG, JPG (max 5MB)";
|
|
374
|
+
uploadSubtext.style.fontSize = "12px";
|
|
375
|
+
uploadSubtext.style.color = "#8a8886";
|
|
376
|
+
uploadSubtext.style.margin = "0 0 16px 0";
|
|
377
|
+
const fileInput = document.createElement("input");
|
|
378
|
+
fileInput.type = "file";
|
|
379
|
+
fileInput.accept = "image/png, image/jpeg, image/jpg";
|
|
380
|
+
fileInput.style.fontSize = "12px";
|
|
381
|
+
fileInput.style.marginBottom = "16px";
|
|
382
|
+
const uploadPreview = document.createElement("img");
|
|
383
|
+
uploadPreview.style.maxWidth = "100%";
|
|
384
|
+
uploadPreview.style.height = "auto";
|
|
385
|
+
uploadPreview.style.minHeight = "80px";
|
|
386
|
+
uploadPreview.style.maxHeight = "120px";
|
|
387
|
+
uploadPreview.style.marginTop = "16px";
|
|
388
|
+
uploadPreview.style.display = "none";
|
|
389
|
+
uploadPreview.style.border = "2px solid #0078d4";
|
|
390
|
+
uploadPreview.style.borderRadius = "4px";
|
|
391
|
+
uploadPreview.style.padding = "8px";
|
|
392
|
+
uploadPreview.style.backgroundColor = "#ffffff";
|
|
393
|
+
uploadPreview.style.objectFit = "contain";
|
|
394
|
+
uploadPanel.appendChild(uploadIcon);
|
|
395
|
+
uploadPanel.appendChild(uploadText);
|
|
396
|
+
uploadPanel.appendChild(uploadSubtext);
|
|
397
|
+
uploadPanel.appendChild(fileInput);
|
|
398
|
+
uploadPanel.appendChild(uploadPreview);
|
|
399
|
+
let activeMode = hasCachedSignature ? "cached" : "draw";
|
|
400
|
+
let compressedUploadBase64 = "";
|
|
401
|
+
const ctx = canvas.getContext("2d");
|
|
402
|
+
let isDrawing = false;
|
|
403
|
+
let hasDrawnContent = false;
|
|
404
|
+
if (ctx) {
|
|
405
|
+
ctx.fillStyle = "#ffffff";
|
|
406
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
407
|
+
ctx.strokeStyle = "#000000";
|
|
408
|
+
ctx.lineWidth = 2.5;
|
|
409
|
+
ctx.lineCap = "round";
|
|
410
|
+
ctx.lineJoin = "round";
|
|
411
|
+
}
|
|
412
|
+
const updateButtonState = () => {
|
|
413
|
+
let isCodeValid = true;
|
|
414
|
+
if (requireTFA) {
|
|
415
|
+
const verificationInput = verificationContainer.querySelector("input");
|
|
416
|
+
const codeValue = verificationInput.value.trim();
|
|
417
|
+
isCodeValid = codeValue.length === 5 && codeValue === generatedPasscode;
|
|
418
|
+
}
|
|
419
|
+
let isSignatureValid = false;
|
|
420
|
+
if (activeMode === "cached") {
|
|
421
|
+
isSignatureValid = hasCachedSignature;
|
|
422
|
+
}
|
|
423
|
+
else if (activeMode === "draw") {
|
|
424
|
+
isSignatureValid = hasDrawnContent && ctx !== null && !isCanvasEmpty(ctx, canvas.width, canvas.height);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
isSignatureValid = compressedUploadBase64.trim() !== "";
|
|
428
|
+
}
|
|
429
|
+
if (isCodeValid && isSignatureValid) {
|
|
430
|
+
signButton.disabled = false;
|
|
431
|
+
signButton.style.backgroundColor = "#0078d4";
|
|
432
|
+
signButton.style.cursor = "pointer";
|
|
433
|
+
signButton.style.opacity = "1";
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
signButton.disabled = true;
|
|
437
|
+
signButton.style.backgroundColor = "#c8c6c4";
|
|
438
|
+
signButton.style.cursor = "not-allowed";
|
|
439
|
+
signButton.style.opacity = "0.6";
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const setActiveTab = (mode) => {
|
|
443
|
+
activeMode = mode;
|
|
444
|
+
cachedPanel.style.display = mode === "cached" ? "block" : "none";
|
|
445
|
+
drawPanel.style.display = mode === "draw" ? "block" : "none";
|
|
446
|
+
uploadPanel.style.display = mode === "upload" ? "flex" : "none";
|
|
447
|
+
[cachedTabBtn, drawTabBtn, uploadTabBtn].forEach((btn) => {
|
|
448
|
+
btn.style.color = "#605e5c";
|
|
449
|
+
btn.style.borderBottom = "3px solid transparent";
|
|
450
|
+
});
|
|
451
|
+
const activeBtn = mode === "cached" ? cachedTabBtn : mode === "draw" ? drawTabBtn : uploadTabBtn;
|
|
452
|
+
activeBtn.style.color = "#0078d4";
|
|
453
|
+
activeBtn.style.borderBottom = "3px solid #0078d4";
|
|
454
|
+
updateButtonState();
|
|
455
|
+
};
|
|
456
|
+
if (hasCachedSignature) {
|
|
457
|
+
cachedTabBtn.onclick = () => setActiveTab("cached");
|
|
458
|
+
}
|
|
459
|
+
drawTabBtn.onclick = () => setActiveTab("draw");
|
|
460
|
+
uploadTabBtn.onclick = () => setActiveTab("upload");
|
|
461
|
+
removeCachedBtn.onclick = () => {
|
|
462
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
463
|
+
cachedTabBtn.style.display = "none";
|
|
464
|
+
setActiveTab("draw");
|
|
465
|
+
};
|
|
466
|
+
if (requireTFA && context.channel) {
|
|
467
|
+
const sendCodeButton = verificationContainer.querySelector("button");
|
|
468
|
+
const verificationInput = verificationContainer.querySelector("input");
|
|
469
|
+
let cooldownTimer = null;
|
|
470
|
+
const startCooldownTimer = () => {
|
|
471
|
+
let secondsLeft = 60;
|
|
472
|
+
sendCodeButton.disabled = true;
|
|
473
|
+
sendCodeButton.style.backgroundColor = "#f3f2f1";
|
|
474
|
+
sendCodeButton.style.color = "#a19f9d";
|
|
475
|
+
sendCodeButton.style.borderColor = "#d1d1d1";
|
|
476
|
+
sendCodeButton.style.cursor = "not-allowed";
|
|
477
|
+
sendCodeButton.innerText = `Resend (${secondsLeft}s)`;
|
|
478
|
+
if (cooldownTimer) {
|
|
479
|
+
clearInterval(cooldownTimer);
|
|
480
|
+
}
|
|
481
|
+
cooldownTimer = window.setInterval(() => {
|
|
482
|
+
secondsLeft -= 1;
|
|
483
|
+
if (secondsLeft > 0) {
|
|
484
|
+
sendCodeButton.innerText = `Resend (${secondsLeft}s)`;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
if (cooldownTimer) {
|
|
488
|
+
clearInterval(cooldownTimer);
|
|
489
|
+
}
|
|
490
|
+
sendCodeButton.disabled = false;
|
|
491
|
+
sendCodeButton.style.backgroundColor = "#ffffff";
|
|
492
|
+
sendCodeButton.style.color = "#0078d4";
|
|
493
|
+
sendCodeButton.style.borderColor = "#0078d4";
|
|
494
|
+
sendCodeButton.style.cursor = "pointer";
|
|
495
|
+
sendCodeButton.innerText = "Resend Code";
|
|
496
|
+
}
|
|
497
|
+
}, 1000);
|
|
498
|
+
};
|
|
499
|
+
const triggerSendCode = async () => {
|
|
500
|
+
sendCodeButton.innerText = "Sending...";
|
|
501
|
+
sendCodeButton.disabled = true;
|
|
502
|
+
try {
|
|
503
|
+
if (context.spContext && context.channel) {
|
|
504
|
+
const vResult = await (0, VerificationService_1.createPendingVerification)(context.spContext, {
|
|
505
|
+
title: context.signer,
|
|
506
|
+
passcode: generatedPasscode,
|
|
507
|
+
channel: context.channel,
|
|
508
|
+
});
|
|
509
|
+
if (vResult.success && vResult.itemId) {
|
|
510
|
+
storedVerificationItemId = vResult.itemId;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
alert("Failed to dispatch verification code. Please try again.");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
console.error("Failed to send code:", err);
|
|
519
|
+
alert("Failed to send verification code. Please try again.");
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
startCooldownTimer();
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
sendCodeButton.onclick = async () => {
|
|
526
|
+
await triggerSendCode();
|
|
527
|
+
};
|
|
528
|
+
verificationInput.oninput = () => {
|
|
529
|
+
updateButtonState();
|
|
530
|
+
};
|
|
531
|
+
void triggerSendCode();
|
|
532
|
+
}
|
|
533
|
+
const getPos = (e) => {
|
|
534
|
+
const rect = canvas.getBoundingClientRect();
|
|
535
|
+
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
536
|
+
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
|
537
|
+
return {
|
|
538
|
+
x: (clientX - rect.left) * (canvas.width / rect.width),
|
|
539
|
+
y: (clientY - rect.top) * (canvas.height / rect.height)
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
const startDraw = (e) => {
|
|
543
|
+
isDrawing = true;
|
|
544
|
+
hasDrawnContent = true;
|
|
545
|
+
const pos = getPos(e);
|
|
546
|
+
ctx === null || ctx === void 0 ? void 0 : ctx.beginPath();
|
|
547
|
+
ctx === null || ctx === void 0 ? void 0 : ctx.moveTo(pos.x, pos.y);
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
};
|
|
550
|
+
const drawLine = (e) => {
|
|
551
|
+
if (!isDrawing || !ctx)
|
|
552
|
+
return;
|
|
553
|
+
const pos = getPos(e);
|
|
554
|
+
ctx.lineTo(pos.x, pos.y);
|
|
555
|
+
ctx.stroke();
|
|
556
|
+
updateButtonState();
|
|
557
|
+
e.preventDefault();
|
|
558
|
+
};
|
|
559
|
+
const stopDraw = () => {
|
|
560
|
+
isDrawing = false;
|
|
561
|
+
updateButtonState();
|
|
562
|
+
};
|
|
563
|
+
canvas.addEventListener("mousedown", startDraw);
|
|
564
|
+
canvas.addEventListener("mousemove", drawLine);
|
|
565
|
+
window.addEventListener("mouseup", stopDraw);
|
|
566
|
+
canvas.addEventListener("touchstart", startDraw);
|
|
567
|
+
canvas.addEventListener("touchmove", drawLine);
|
|
568
|
+
window.addEventListener("touchend", stopDraw);
|
|
569
|
+
clearButton.onclick = () => {
|
|
570
|
+
if (ctx) {
|
|
571
|
+
ctx.fillStyle = "#ffffff";
|
|
572
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
573
|
+
hasDrawnContent = false;
|
|
574
|
+
updateButtonState();
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
fileInput.onchange = (e) => {
|
|
578
|
+
const target = e.target;
|
|
579
|
+
if (target.files && target.files[0]) {
|
|
580
|
+
const file = target.files[0];
|
|
581
|
+
const reader = new FileReader();
|
|
582
|
+
reader.onload = (uploadEvent) => {
|
|
583
|
+
var _a;
|
|
584
|
+
if ((_a = uploadEvent.target) === null || _a === void 0 ? void 0 : _a.result) {
|
|
585
|
+
const img = new Image();
|
|
586
|
+
img.onload = () => {
|
|
587
|
+
const tempCanvas = document.createElement("canvas");
|
|
588
|
+
const maxW = 560;
|
|
589
|
+
const maxH = 160;
|
|
590
|
+
let w = img.width;
|
|
591
|
+
let h = img.height;
|
|
592
|
+
if (w > maxW) {
|
|
593
|
+
h = Math.round((h * maxW) / w);
|
|
594
|
+
w = maxW;
|
|
595
|
+
}
|
|
596
|
+
if (h > maxH) {
|
|
597
|
+
w = Math.round((w * maxH) / h);
|
|
598
|
+
h = maxH;
|
|
599
|
+
}
|
|
600
|
+
tempCanvas.width = w;
|
|
601
|
+
tempCanvas.height = h;
|
|
602
|
+
const tCtx = tempCanvas.getContext("2d");
|
|
603
|
+
if (tCtx) {
|
|
604
|
+
tCtx.fillStyle = "#ffffff";
|
|
605
|
+
tCtx.fillRect(0, 0, w, h);
|
|
606
|
+
tCtx.drawImage(img, 0, 0, w, h);
|
|
607
|
+
compressedUploadBase64 = tempCanvas.toDataURL("image/png");
|
|
608
|
+
uploadPreview.src = compressedUploadBase64;
|
|
609
|
+
uploadPreview.style.display = "block";
|
|
610
|
+
updateButtonState();
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
img.src = uploadEvent.target.result;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
reader.readAsDataURL(file);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const footerSection = document.createElement("div");
|
|
620
|
+
footerSection.style.padding = "20px 24px";
|
|
621
|
+
footerSection.style.backgroundColor = "#ffffff";
|
|
622
|
+
footerSection.style.borderTop = "1px solid #edebe9";
|
|
623
|
+
footerSection.style.display = "flex";
|
|
624
|
+
footerSection.style.justifyContent = "flex-end";
|
|
625
|
+
footerSection.style.gap = "12px";
|
|
626
|
+
const cancelButton = document.createElement("button");
|
|
627
|
+
cancelButton.innerText = "Cancel";
|
|
628
|
+
cancelButton.style.padding = "10px 24px";
|
|
629
|
+
cancelButton.style.backgroundColor = "#ffffff";
|
|
630
|
+
cancelButton.style.border = "1px solid #8a8886";
|
|
631
|
+
cancelButton.style.cursor = "pointer";
|
|
632
|
+
cancelButton.style.fontSize = "14px";
|
|
633
|
+
cancelButton.style.fontWeight = "600";
|
|
634
|
+
cancelButton.style.color = "#323130";
|
|
635
|
+
cancelButton.style.borderRadius = "4px";
|
|
636
|
+
cancelButton.style.transition = "all 0.2s ease";
|
|
637
|
+
cancelButton.onmouseover = () => {
|
|
638
|
+
cancelButton.style.backgroundColor = "#f3f2f1";
|
|
639
|
+
};
|
|
640
|
+
cancelButton.onmouseout = () => {
|
|
641
|
+
cancelButton.style.backgroundColor = "#ffffff";
|
|
642
|
+
};
|
|
643
|
+
signButton.onmouseover = () => {
|
|
644
|
+
if (!signButton.disabled) {
|
|
645
|
+
signButton.style.backgroundColor = "#005a9e";
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
signButton.onmouseout = () => {
|
|
649
|
+
if (!signButton.disabled) {
|
|
650
|
+
signButton.style.backgroundColor = "#0078d4";
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
const cleanup = () => {
|
|
654
|
+
document.body.removeChild(overlay);
|
|
655
|
+
};
|
|
656
|
+
cancelButton.onclick = () => {
|
|
657
|
+
cleanup();
|
|
658
|
+
resolve(undefined);
|
|
659
|
+
};
|
|
660
|
+
signButton.onclick = async () => {
|
|
661
|
+
if (requireTFA) {
|
|
662
|
+
const verificationInput = verificationContainer.querySelector("input");
|
|
663
|
+
const enteredCode = verificationInput.value.trim();
|
|
664
|
+
if (enteredCode !== generatedPasscode) {
|
|
665
|
+
alert("Invalid verification code. Please enter the correct 5-digit code.");
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
let finalDataUrl = "";
|
|
670
|
+
if (activeMode === "cached") {
|
|
671
|
+
if (!cachedCompressedSig) {
|
|
672
|
+
alert("No cached signature found. Please select another option.");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
finalDataUrl = lzwDecompress(cachedCompressedSig);
|
|
676
|
+
}
|
|
677
|
+
else if (activeMode === "draw") {
|
|
678
|
+
if (!ctx || isCanvasEmpty(ctx, canvas.width, canvas.height)) {
|
|
679
|
+
alert("Please draw your signature before proceeding.");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
finalDataUrl = downscaleCanvas(canvas, 560, 160);
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
if (!compressedUploadBase64 || compressedUploadBase64.trim() === "") {
|
|
686
|
+
alert("Please upload a signature image before proceeding.");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
finalDataUrl = compressedUploadBase64;
|
|
690
|
+
}
|
|
691
|
+
const compressedString = lzwCompress(finalDataUrl);
|
|
692
|
+
if (cacheCheckbox.checked) {
|
|
693
|
+
localStorage.setItem(STORAGE_KEY, compressedString);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
697
|
+
}
|
|
698
|
+
cleanup();
|
|
699
|
+
try {
|
|
700
|
+
const fullContext = Object.assign(Object.assign({}, context), { signatureData: compressedString });
|
|
701
|
+
const result = await generateSecureAuditRecordInternal(fullContext);
|
|
702
|
+
if (storedVerificationItemId) {
|
|
703
|
+
result.verificationItemId = storedVerificationItemId;
|
|
704
|
+
}
|
|
705
|
+
resolve(result);
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
console.error("Signing failed:", error);
|
|
709
|
+
resolve(undefined);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
updateButtonState();
|
|
713
|
+
footerSection.appendChild(cancelButton);
|
|
714
|
+
footerSection.appendChild(signButton);
|
|
715
|
+
contentSection.appendChild(body);
|
|
716
|
+
if (requireTFA) {
|
|
717
|
+
contentSection.appendChild(verificationContainer);
|
|
718
|
+
}
|
|
719
|
+
contentSection.appendChild(signatureSection);
|
|
720
|
+
signatureSection.appendChild(signatureHeader);
|
|
721
|
+
signatureSection.appendChild(modeContainer);
|
|
722
|
+
signatureSection.appendChild(cachedPanel);
|
|
723
|
+
signatureSection.appendChild(drawPanel);
|
|
724
|
+
signatureSection.appendChild(uploadPanel);
|
|
725
|
+
signatureSection.appendChild(cacheControlBox);
|
|
726
|
+
modalBox.appendChild(headerSection);
|
|
727
|
+
modalBox.appendChild(contentSection);
|
|
728
|
+
modalBox.appendChild(footerSection);
|
|
729
|
+
overlay.appendChild(modalBox);
|
|
730
|
+
document.body.appendChild(overlay);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
exports.promptAndGenerateSecureAudit = promptAndGenerateSecureAudit;
|
|
734
|
+
function getReportableSignature(compressedSignatureData) {
|
|
735
|
+
const decompressed = lzwDecompress(compressedSignatureData);
|
|
736
|
+
if (!decompressed || !decompressed.startsWith("data:image")) {
|
|
737
|
+
return "";
|
|
738
|
+
}
|
|
739
|
+
return decompressed;
|
|
740
|
+
}
|
|
741
|
+
exports.getReportableSignature = getReportableSignature;
|
|
742
|
+
function lzwCompress(input) {
|
|
743
|
+
const dictionary = {};
|
|
744
|
+
let c = "";
|
|
745
|
+
let wc = "";
|
|
746
|
+
let w = "";
|
|
747
|
+
const result = [];
|
|
748
|
+
let dictionarySize = 256;
|
|
749
|
+
for (let i = 0; i < 256; i += 1) {
|
|
750
|
+
dictionary[String.fromCharCode(i)] = i;
|
|
751
|
+
}
|
|
752
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
753
|
+
c = input.charAt(i);
|
|
754
|
+
wc = w + c;
|
|
755
|
+
if (Object.prototype.hasOwnProperty.call(dictionary, wc)) {
|
|
756
|
+
w = wc;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
result.push(dictionary[w]);
|
|
760
|
+
dictionary[wc] = dictionarySize++;
|
|
761
|
+
w = String(c);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (w !== "") {
|
|
765
|
+
result.push(dictionary[w]);
|
|
766
|
+
}
|
|
767
|
+
return result.map((n) => String.fromCharCode(n)).join("");
|
|
768
|
+
}
|
|
769
|
+
function lzwDecompress(compressed) {
|
|
770
|
+
const dictionary = {};
|
|
771
|
+
const result = [];
|
|
772
|
+
let dictionarySize = 256;
|
|
773
|
+
for (let i = 0; i < 256; i += 1) {
|
|
774
|
+
dictionary[i] = String.fromCharCode(i);
|
|
775
|
+
}
|
|
776
|
+
if (compressed.length === 0)
|
|
777
|
+
return "";
|
|
778
|
+
let w = String.fromCharCode(compressed.charCodeAt(0));
|
|
779
|
+
result.push(w);
|
|
780
|
+
let entry = "";
|
|
781
|
+
for (let i = 1; i < compressed.length; i += 1) {
|
|
782
|
+
const k = compressed.charCodeAt(i);
|
|
783
|
+
if (dictionary[k]) {
|
|
784
|
+
entry = dictionary[k];
|
|
785
|
+
}
|
|
786
|
+
else if (k === dictionarySize) {
|
|
787
|
+
entry = w + w.charAt(0);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
return "";
|
|
791
|
+
}
|
|
792
|
+
result.push(entry);
|
|
793
|
+
dictionary[dictionarySize++] = w + entry.charAt(0);
|
|
794
|
+
w = entry;
|
|
795
|
+
}
|
|
796
|
+
return result.join("");
|
|
797
|
+
}
|
|
798
|
+
function downscaleCanvas(sourceCanvas, targetWidth, targetHeight) {
|
|
799
|
+
const tempCanvas = document.createElement("canvas");
|
|
800
|
+
tempCanvas.width = targetWidth;
|
|
801
|
+
tempCanvas.height = targetHeight;
|
|
802
|
+
const ctx = tempCanvas.getContext("2d");
|
|
803
|
+
if (ctx) {
|
|
804
|
+
ctx.fillStyle = "#ffffff";
|
|
805
|
+
ctx.fillRect(0, 0, targetWidth, targetHeight);
|
|
806
|
+
ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);
|
|
807
|
+
}
|
|
808
|
+
return tempCanvas.toDataURL("image/png");
|
|
809
|
+
}
|
|
810
|
+
function isCanvasEmpty(ctx, width, height) {
|
|
811
|
+
const pixelBuffer = ctx.getImageData(0, 0, width, height).data;
|
|
812
|
+
for (let i = 0; i < pixelBuffer.length; i += 4) {
|
|
813
|
+
if (pixelBuffer[i] !== 255 ||
|
|
814
|
+
pixelBuffer[i + 1] !== 255 ||
|
|
815
|
+
pixelBuffer[i + 2] !== 255) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
async function verifySecureAuditRecord(payloadToVerify, signer, timestamp, compressedSignatureData, storedHash) {
|
|
822
|
+
try {
|
|
823
|
+
if (!payloadToVerify || !storedHash)
|
|
824
|
+
return false;
|
|
825
|
+
const sortedPayloadString = JSON.stringify(payloadToVerify, Object.keys(payloadToVerify).sort());
|
|
826
|
+
const auditEnvelope = {
|
|
827
|
+
payload: JSON.parse(sortedPayloadString),
|
|
828
|
+
signer: signer.toLowerCase().trim(),
|
|
829
|
+
timestamp: timestamp.trim(),
|
|
830
|
+
};
|
|
831
|
+
const auditEnvelopeJson = JSON.stringify(auditEnvelope);
|
|
832
|
+
const encoder = new TextEncoder();
|
|
833
|
+
const encodedBytes = encoder.encode(auditEnvelopeJson);
|
|
834
|
+
const hashBuffer = await window.crypto.subtle.digest("SHA-256", encodedBytes);
|
|
835
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
836
|
+
const recomputedHash = hashArray
|
|
837
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
838
|
+
.join("");
|
|
839
|
+
return recomputedHash === storedHash.trim();
|
|
840
|
+
}
|
|
841
|
+
catch (_a) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
exports.verifySecureAuditRecord = verifySecureAuditRecord;
|
|
846
|
+
async function generateSecureAuditRecordInternal(context) {
|
|
847
|
+
const { payload, signer, signatureData } = context;
|
|
848
|
+
if (!payload || Object.keys(payload).length === 0)
|
|
849
|
+
throw new Error("Payload cannot be empty.");
|
|
850
|
+
if (!signer || signer.trim() === "")
|
|
851
|
+
throw new Error("Signer identifier is required for sealing.");
|
|
852
|
+
const signatureTimestamp = new Date().toISOString();
|
|
853
|
+
const sortedPayloadString = JSON.stringify(payload, Object.keys(payload).sort());
|
|
854
|
+
const auditEnvelope = {
|
|
855
|
+
payload: JSON.parse(sortedPayloadString),
|
|
856
|
+
signer: signer.toLowerCase().trim(),
|
|
857
|
+
timestamp: signatureTimestamp.trim()
|
|
858
|
+
};
|
|
859
|
+
const auditEnvelopeJson = JSON.stringify(auditEnvelope);
|
|
860
|
+
const encoder = new TextEncoder();
|
|
861
|
+
const encodedBytes = encoder.encode(auditEnvelopeJson);
|
|
862
|
+
const hashBuffer = await window.crypto.subtle.digest("SHA-256", encodedBytes);
|
|
863
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
864
|
+
const signatureHash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
865
|
+
return {
|
|
866
|
+
signatureHash,
|
|
867
|
+
signatureData,
|
|
868
|
+
signatureTimestamp,
|
|
869
|
+
verificationItemId: context.itemID
|
|
870
|
+
};
|
|
871
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "@pnp/sp/webs";
|
|
2
|
+
import "@pnp/sp/lists";
|
|
3
|
+
import "@pnp/sp/items";
|
|
4
|
+
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
|
5
|
+
export type DeliveryChannel = "email" | "teams" | "both";
|
|
6
|
+
export interface IVerificationRequest {
|
|
7
|
+
title: string;
|
|
8
|
+
passcode: string;
|
|
9
|
+
channel: DeliveryChannel;
|
|
10
|
+
}
|
|
11
|
+
export interface IVerificationResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
itemId?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createPendingVerification(context: WebPartContext, request: IVerificationRequest): Promise<IVerificationResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPendingVerification = void 0;
|
|
4
|
+
const sp_1 = require("@pnp/sp");
|
|
5
|
+
require("@pnp/sp/webs");
|
|
6
|
+
require("@pnp/sp/lists");
|
|
7
|
+
require("@pnp/sp/items");
|
|
8
|
+
async function createPendingVerification(context, request) {
|
|
9
|
+
var _a, _b, _c, _d, _e;
|
|
10
|
+
try {
|
|
11
|
+
if (!context) {
|
|
12
|
+
throw new Error("Execution restricted: Valid WebPartContext is required.");
|
|
13
|
+
}
|
|
14
|
+
const sp = (0, sp_1.spfi)().using((0, sp_1.SPFx)(context));
|
|
15
|
+
const LIST_NAME = "PendingVerifications";
|
|
16
|
+
const addedItem = await sp.web.lists.getByTitle(LIST_NAME).items.add({
|
|
17
|
+
Title: request.title,
|
|
18
|
+
Passcode: request.passcode,
|
|
19
|
+
Channel: request.channel,
|
|
20
|
+
});
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
const itemId = (_e = (_d = (_b = (_a = addedItem === null || addedItem === void 0 ? void 0 : addedItem.data) === null || _a === void 0 ? void 0 : _a.Id) !== null && _b !== void 0 ? _b : (_c = addedItem === null || addedItem === void 0 ? void 0 : addedItem.data) === null || _c === void 0 ? void 0 : _c.ID) !== null && _d !== void 0 ? _d : addedItem === null || addedItem === void 0 ? void 0 : addedItem.Id) !== null && _e !== void 0 ? _e : 0;
|
|
23
|
+
return {
|
|
24
|
+
success: true,
|
|
25
|
+
itemId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: errorMsg,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.createPendingVerification = createPendingVerification;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./ProvisioningService"), exports);
|
|
18
|
+
__exportStar(require("./VerificationService"), exports);
|
|
19
|
+
__exportStar(require("./TransactionSigner"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bdking71/spsignature",
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "SharePoint Framework WebPart isolated digital signing and provisioning engine.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"clean": "rimraf lib",
|
|
12
|
+
"build": "tsc --build",
|
|
13
|
+
"prepare": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"spfx",
|
|
17
|
+
"sharepoint",
|
|
18
|
+
"pnp",
|
|
19
|
+
"digital-signature",
|
|
20
|
+
"audit"
|
|
21
|
+
],
|
|
22
|
+
"author": "Your Organization Name",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@microsoft/sp-webpart-base": ">=1.18.0",
|
|
26
|
+
"@pnp/sp": ">=3.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@microsoft/sp-webpart-base": "^1.18.0",
|
|
30
|
+
"@pnp/sp": "^3.0.0",
|
|
31
|
+
"rimraf": "^5.0.0",
|
|
32
|
+
"typescript": "~5.3.3"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/bdking71/spsignature"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/bdking71/spsignature/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/bdking71/spsignature#readme"
|
|
42
|
+
}
|