@bdking71/spsignature 1.0.5 → 1.2.0
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 +182 -45
- package/lib/OtpService.d.ts +24 -0
- package/lib/OtpService.js +93 -0
- package/lib/ProvisioningService.d.ts +50 -1
- package/lib/ProvisioningService.js +84 -26
- package/lib/TransactionSigner.d.ts +109 -2
- package/lib/TransactionSigner.js +437 -75
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +34 -16
- package/lib/VerificationService.d.ts +0 -16
- package/lib/VerificationService.js +0 -36
package/README.md
CHANGED
|
@@ -1,84 +1,148 @@
|
|
|
1
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
2
|
|
|
5
|
-
|
|
3
|
+
[](https://github.com/bdking71/spsignature)
|
|
4
|
+
[](https://www.npmjs.com/package/@bdking71/spsignature)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
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
|
-
|
|
9
|
+
`@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
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Source Code & Repository
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
* **GitHub Repository:** [https://github.com/bdking71/spsignature](https://github.com/bdking71/spsignature)
|
|
16
|
+
* **Issues & Feedback:** [https://github.com/bdking71/spsignature/issues](https://github.com/bdking71/spsignature/issues)
|
|
17
|
+
* **NPM Package:** [@bdking71/spsignature](https://www.npmjs.com/package/@bdking71/spsignature)
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
---
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
## Key Capabilities & Business Value
|
|
22
|
+
|
|
23
|
+
- **Zero Infrastructure Overhead**: Operates entirely within client-side M365 contexts — no external APIs, Azure Functions, or third-party storage backends required.
|
|
24
|
+
- **Cross-Platform M365 Integration**: Designed for seamless deployment across SharePoint Online, Microsoft Teams, and Viva Connections desktop/mobile experiences.
|
|
25
|
+
- **Integrity & Non-Repudiation**: Generates cryptographic SHA-256 hashes bound to user identities, timestamps, and target record IDs to prevent signature tampering.
|
|
26
|
+
- **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.
|
|
27
|
+
- **LZW Compression**: Automatically compresses signature image payloads to minimize SharePoint storage footprint.
|
|
28
|
+
- **Signature Caching**: Optional local device caching so users don't have to re-draw their signature on every transaction.
|
|
29
|
+
|
|
30
|
+
---
|
|
20
31
|
|
|
21
|
-
##
|
|
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.
|
|
32
|
+
## Two-Factor Authentication (2FA) Architecture
|
|
23
33
|
|
|
24
|
-
-
|
|
34
|
+
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.
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+

|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
### How the 2FA Workflow Operates
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
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.
|
|
41
|
+
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.
|
|
42
|
+
3. **Power Automate Trigger**: A Power Automate flow monitors the list and instantly dispatches the code to the signer via:
|
|
43
|
+
- **Microsoft Teams**: Direct Adaptive Card or Activity Feed notification.
|
|
44
|
+
- **Outlook Email**: High-priority internal notification.
|
|
45
|
+
4. **Validation & Signing**: The signer enters the 5-digit code into the `@bdking71/spsignature` UI component. Upon successful validation, the final signed JSON payload is generated and stored in the target record.
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
---
|
|
33
48
|
|
|
34
49
|
## Signature Data Schema
|
|
35
50
|
|
|
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):
|
|
51
|
+
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
52
|
|
|
38
|
-
```
|
|
53
|
+
```json
|
|
39
54
|
{
|
|
40
|
-
"signatureHash": "
|
|
41
|
-
"signatureData": "data:image/png;base64,
|
|
55
|
+
"signatureHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
56
|
+
"signatureData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
|
|
42
57
|
"signatureTimestamp": "2026-08-31T19:55:06.121Z",
|
|
43
|
-
"verificationItemId":
|
|
58
|
+
"verificationItemId": 42
|
|
44
59
|
}
|
|
45
60
|
```
|
|
46
61
|
|
|
62
|
+
| Field | Type | Description |
|
|
63
|
+
| :--- | :--- | :--- |
|
|
64
|
+
| `signatureHash` | `string` | SHA-256 hex digest of the canonical audit envelope (payload + signer + timestamp). |
|
|
65
|
+
| `signatureData` | `string` | LZW-compressed data-URI of the signature image (PNG). |
|
|
66
|
+
| `signatureTimestamp` | `string` | ISO-8601 timestamp captured at the moment of signing. |
|
|
67
|
+
| `verificationItemId` | `number` | ID of the corresponding record in the `PendingVerifications` list. |
|
|
68
|
+
|
|
69
|
+
---
|
|
47
70
|
|
|
48
71
|
## Toolchain & Compatibility Matrix
|
|
49
72
|
|
|
50
73
|
This project requires a precise local environment build. Strict adherence to the versioning matrix is mandatory to prevent toolchain compilation errors.
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
| Component | Version |
|
|
76
|
+
| :--- | :--- |
|
|
77
|
+
| **SPFx** | `v1.22.1` |
|
|
78
|
+
| **Node.js** | `v18.17.1` (Recommended use of `nvm` or `nvs`) |
|
|
79
|
+
| **Gulp CLI** | `v4.x` |
|
|
80
|
+
| **React** | `v17.0.1` |
|
|
81
|
+
| **Fluent UI** | `v8.x` |
|
|
82
|
+
| **TypeScript** | `v4.7+` |
|
|
83
|
+
|
|
84
|
+
---
|
|
56
85
|
|
|
57
|
-
##
|
|
86
|
+
## Installation
|
|
58
87
|
|
|
59
|
-
|
|
88
|
+
Install the package into your SPFx solution:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install @bdking71/spsignature --save
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Peer Dependencies
|
|
95
|
+
|
|
96
|
+
Ensure the following peer dependencies are installed in your SPFx project:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install @pnp/sp @microsoft/sp-webpart-base --save
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
60
103
|
|
|
61
104
|
## Integration & Usage Guide
|
|
62
105
|
|
|
63
|
-
### Provision Infrastructure
|
|
64
|
-
|
|
106
|
+
### Step 1: Provision SharePoint Infrastructure
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
> **Note:** This method is fully **idempotent**—it can be safely called on every web part load without failing or duplicating columns.
|
|
65
111
|
|
|
66
112
|
```typescript
|
|
67
113
|
import { ensurePendingVerificationsList } from "@bdking71/spsignature";
|
|
68
114
|
|
|
69
|
-
|
|
70
|
-
|
|
115
|
+
export default class MySignatureWebPart extends BaseClientSideWebPart<IMySignatureWebPartProps> {
|
|
116
|
+
|
|
117
|
+
public async onInit(): Promise<void> {
|
|
118
|
+
await super.onInit();
|
|
71
119
|
|
|
72
|
-
|
|
73
|
-
|
|
120
|
+
// Provisions the 'PendingVerifications' list with 'Passcode' and 'Channel' columns
|
|
121
|
+
await ensurePendingVerificationsList({ context: this.context });
|
|
122
|
+
}
|
|
74
123
|
}
|
|
75
124
|
```
|
|
76
125
|
|
|
77
|
-
###
|
|
126
|
+
### Step 2: Configure Your Power Automate Flow
|
|
127
|
+
|
|
128
|
+
Create a Power Automate flow that triggers on **"When an item is created"** for the `PendingVerifications` list. The flow should:
|
|
129
|
+
|
|
130
|
+
1. Read the `Title` (signer email), `Passcode`, and `Channel` columns.
|
|
131
|
+
2. Route notification based on the `Channel` value:
|
|
132
|
+
- `"email"` → Send an Outlook email with the passcode.
|
|
133
|
+
- `"teams"` → Post an Adaptive Card to the user in Microsoft Teams.
|
|
134
|
+
- `"both"` → Dispatch through both channels simultaneously.
|
|
135
|
+
3. Optionally delete the item after dispatch (or use a scheduled cleanup flow to purge expired codes).
|
|
78
136
|
|
|
79
|
-
|
|
137
|
+
### Step 3: Launch Signature Modal & Capture Audit
|
|
80
138
|
|
|
81
|
-
|
|
139
|
+
Execute `promptAndGenerateSecureAudit` within your component or event handler. This renders an enterprise modal overlay that:
|
|
140
|
+
- Handles drawn, uploaded, or cached signature selection.
|
|
141
|
+
- Triggers 2FA code delivery through the configured channel.
|
|
142
|
+
- Validates the 5-digit passcode.
|
|
143
|
+
- Outputs a cryptographic audit payload.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
82
146
|
import {
|
|
83
147
|
promptAndGenerateSecureAudit,
|
|
84
148
|
SharePointAuditRecord
|
|
@@ -108,11 +172,18 @@ private handleSignAction = async (): Promise<void> => {
|
|
|
108
172
|
console.log("Compressed Image Data:", auditRecord.signatureData);
|
|
109
173
|
console.log("Timestamp:", auditRecord.signatureTimestamp);
|
|
110
174
|
console.log("Verification Log ID:", auditRecord.verificationItemId);
|
|
175
|
+
|
|
176
|
+
// Example: Persist to a SharePoint list column named "SignatureAudit"
|
|
177
|
+
await sp.web.lists.getByTitle("Requisitions").items.getById(1042).update({
|
|
178
|
+
SignatureAudit: JSON.stringify(auditRecord)
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
console.log("User cancelled the signature workflow.");
|
|
111
182
|
}
|
|
112
183
|
};
|
|
113
184
|
```
|
|
114
185
|
|
|
115
|
-
### Verification & Image Reconstruction
|
|
186
|
+
### Step 4: Verification & Image Reconstruction
|
|
116
187
|
|
|
117
188
|
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
189
|
|
|
@@ -124,22 +195,88 @@ import {
|
|
|
124
195
|
|
|
125
196
|
// 1. Re-calculate SHA-256 hash to verify record integrity
|
|
126
197
|
const isValid: boolean = await verifySecureAuditRecord(
|
|
127
|
-
payload,
|
|
128
|
-
signerEmail,
|
|
129
|
-
timestamp,
|
|
130
|
-
compressedSignatureData,
|
|
131
|
-
storedHash
|
|
198
|
+
payload, // Original payload object
|
|
199
|
+
signerEmail, // Signer's email address
|
|
200
|
+
timestamp, // ISO-8601 timestamp from the audit record
|
|
201
|
+
compressedSignatureData, // Retained for API symmetry
|
|
202
|
+
storedHash // The stored signatureHash to compare against
|
|
132
203
|
);
|
|
133
204
|
|
|
205
|
+
if (isValid) {
|
|
206
|
+
console.log("✓ Signature is authentic and untampered.");
|
|
207
|
+
} else {
|
|
208
|
+
console.warn("✗ Signature verification failed - record may be tampered.");
|
|
209
|
+
}
|
|
210
|
+
|
|
134
211
|
// 2. Decompress LZW signature string into a standard PNG base64 Data-URI
|
|
135
212
|
const renderableImageSrc: string = getReportableSignature(compressedSignatureData);
|
|
213
|
+
|
|
214
|
+
// Use directly in an <img> tag:
|
|
215
|
+
// <img src={renderableImageSrc} alt="Signature" />
|
|
136
216
|
```
|
|
137
217
|
|
|
138
|
-
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## API Reference
|
|
221
|
+
|
|
222
|
+
### Exported Functions
|
|
223
|
+
|
|
224
|
+
| Function | Purpose |
|
|
225
|
+
| :--- | :--- |
|
|
226
|
+
| `ensurePendingVerificationsList(props)` | Idempotently provisions the `PendingVerifications` SharePoint list, its required columns, and item-level security. |
|
|
227
|
+
| `promptAndGenerateSecureAudit(context, title?, msg?)` | Launches the signature modal, dispatches the OTP, validates entry, and returns a `SharePointAuditRecord`. |
|
|
228
|
+
| `verifySecureAuditRecord(payload, signer, timestamp, sigData, hash)` | Re-computes the SHA-256 hash to verify audit-record integrity. |
|
|
229
|
+
| `getReportableSignature(compressedSignatureData)` | Decompresses an LZW-compressed signature back into a viewable `data:image/png` URI. |
|
|
230
|
+
|
|
231
|
+
### Exported Types
|
|
232
|
+
|
|
233
|
+
| Type | Description |
|
|
234
|
+
| :--- | :--- |
|
|
235
|
+
| `SharePointAuditRecord` | Structured JSON audit payload returned after successful signing. |
|
|
236
|
+
| `SignerContext` | Input parameters for the signature workflow. |
|
|
237
|
+
| `AuditEnvelopeRecord` | Canonical envelope structure used for hashing (payload + signer + timestamp). |
|
|
238
|
+
| `DeliveryChannel` | `"email" \| "teams" \| "both"` — Supported OTP delivery channels. |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Security Considerations
|
|
243
|
+
|
|
244
|
+
- **Client-Side Passcode Generation**: The 5-digit OTP is generated and validated in the browser using the Web Crypto API. This is well-suited for **enterprise workflow enforcement** (approvals, sign-offs, SOP compliance) but is not intended to replace hardened server-side MFA solutions for high-risk financial transactions.
|
|
245
|
+
- **Item-Level Security**: The `PendingVerifications` list is automatically configured with `ReadSecurity = 2` and `WriteSecurity = 2`, meaning users can only view and edit records they created. This prevents users from inspecting other users' active passcodes.
|
|
246
|
+
- **SHA-256 Non-Repudiation**: The signature hash binds the payload, signer identity, and timestamp together. Any modification to the stored record can be detected through `verifySecureAuditRecord()`.
|
|
247
|
+
- **Signature Caching**: Cached signatures are stored in browser `localStorage` after LZW compression. Consumers should clearly communicate this caching behavior to end users to meet compliance and privacy requirements (GDPR, CCPA, etc.).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
| Issue | Resolution |
|
|
254
|
+
| :--- | :--- |
|
|
255
|
+
| Modal never appears | Ensure `spContext` is a valid `WebPartContext` and the DOM is fully loaded. |
|
|
256
|
+
| OTP never arrives | Verify the Power Automate flow is active and listening on the correct list. Check the flow run history. |
|
|
257
|
+
| "Field name already exists" | This shouldn't happen due to idempotency, but ensure you're on the latest version of the module. |
|
|
258
|
+
| SHA-256 verification fails on nested objects | Note: Only top-level payload keys are sorted. Deeply nested objects should have consistent key ordering. |
|
|
259
|
+
| Signature not caching | Verify browser `localStorage` isn't in private/incognito mode or blocked by browser policy. |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Contributions, issues, and feature requests are welcome!
|
|
266
|
+
|
|
267
|
+
1. Fork the Project: [https://github.com/bdking71/spsignature](https://github.com/bdking71/spsignature)
|
|
268
|
+
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
269
|
+
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
270
|
+
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
271
|
+
5. Open a Pull Request
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## License
|
|
139
276
|
|
|
140
277
|
MIT License
|
|
141
278
|
|
|
142
|
-
Copyright (
|
|
279
|
+
Copyright (c) 2025 bdking71
|
|
143
280
|
|
|
144
281
|
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
282
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OtpService.ts
|
|
3
|
+
* Provides methods for dispatching One-Time Passcodes (OTP) for
|
|
4
|
+
* Two-Factor Authentication via SharePoint and external channels.
|
|
5
|
+
*/
|
|
6
|
+
import "@pnp/sp/webs";
|
|
7
|
+
import "@pnp/sp/lists";
|
|
8
|
+
import "@pnp/sp/items";
|
|
9
|
+
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
|
10
|
+
export type DeliveryChannel = "email" | "teams" | "both";
|
|
11
|
+
export interface IOtpDispatchRequest {
|
|
12
|
+
title: string;
|
|
13
|
+
passcode: string;
|
|
14
|
+
channel: DeliveryChannel;
|
|
15
|
+
}
|
|
16
|
+
export interface IOtpDispatchResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
itemId?: number;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Queues an OTP dispatch request in SharePoint to notify the user via Email/Teams.
|
|
23
|
+
*/
|
|
24
|
+
export declare function dispatchOtpPasscode(context: WebPartContext, request: IOtpDispatchRequest, listTitle?: string): Promise<IOtpDispatchResult>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file OtpService.ts
|
|
4
|
+
* Provides methods for dispatching One-Time Passcodes (OTP) for
|
|
5
|
+
* Two-Factor Authentication via SharePoint and external channels.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.dispatchOtpPasscode = void 0;
|
|
9
|
+
const sp_1 = require("@pnp/sp");
|
|
10
|
+
require("@pnp/sp/webs");
|
|
11
|
+
require("@pnp/sp/lists");
|
|
12
|
+
require("@pnp/sp/items");
|
|
13
|
+
const DEFAULT_OTP_LIST_NAME = "PendingVerifications";
|
|
14
|
+
/**
|
|
15
|
+
* Queues an OTP dispatch request in SharePoint to notify the user via Email/Teams.
|
|
16
|
+
*/
|
|
17
|
+
async function dispatchOtpPasscode(context, request, listTitle = DEFAULT_OTP_LIST_NAME) {
|
|
18
|
+
var _a, _b, _c, _d;
|
|
19
|
+
try {
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error("Execution restricted: Valid WebPartContext is required.");
|
|
22
|
+
}
|
|
23
|
+
if (!request || !((_a = request.title) === null || _a === void 0 ? void 0 : _a.trim())) {
|
|
24
|
+
throw new Error("Invalid request: Signer/Title identifier is required.");
|
|
25
|
+
}
|
|
26
|
+
if (!((_b = request.passcode) === null || _b === void 0 ? void 0 : _b.trim())) {
|
|
27
|
+
throw new Error("Invalid request: Passcode must be provided.");
|
|
28
|
+
}
|
|
29
|
+
const sp = (0, sp_1.spfi)().using((0, sp_1.SPFx)(context));
|
|
30
|
+
const addedItem = await sp.web.lists.getByTitle(listTitle).items.add({
|
|
31
|
+
Title: request.title.trim(),
|
|
32
|
+
Passcode: request.passcode.trim(),
|
|
33
|
+
Channel: request.channel || "email",
|
|
34
|
+
});
|
|
35
|
+
// DEBUG: Log the entire response structure
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const debugItem = addedItem;
|
|
38
|
+
console.log("=== PnPjs Add Item Response ===");
|
|
39
|
+
console.log("Full Response:", debugItem);
|
|
40
|
+
console.log("Response Keys:", Object.keys(debugItem || {}));
|
|
41
|
+
if (debugItem === null || debugItem === void 0 ? void 0 : debugItem.data) {
|
|
42
|
+
console.log("Data Object:", debugItem.data);
|
|
43
|
+
console.log("Data Keys:", Object.keys(debugItem.data));
|
|
44
|
+
}
|
|
45
|
+
// Try multiple extraction paths
|
|
46
|
+
let itemId;
|
|
47
|
+
// Path 1: addedItem.data.Id (most common in PnPjs v3/v4)
|
|
48
|
+
if ((_c = addedItem === null || addedItem === void 0 ? void 0 : addedItem.data) === null || _c === void 0 ? void 0 : _c.Id) {
|
|
49
|
+
itemId = addedItem.data.Id;
|
|
50
|
+
console.log("✓ Found ID in addedItem.data.Id:", itemId);
|
|
51
|
+
}
|
|
52
|
+
// Path 2: addedItem.data.ID (alternative casing)
|
|
53
|
+
else if ((_d = addedItem === null || addedItem === void 0 ? void 0 : addedItem.data) === null || _d === void 0 ? void 0 : _d.ID) {
|
|
54
|
+
itemId = addedItem.data.ID;
|
|
55
|
+
console.log("✓ Found ID in addedItem.data.ID:", itemId);
|
|
56
|
+
}
|
|
57
|
+
// Path 3: addedItem.Id (direct property)
|
|
58
|
+
else if (debugItem === null || debugItem === void 0 ? void 0 : debugItem.Id) {
|
|
59
|
+
itemId = debugItem.Id;
|
|
60
|
+
console.log("✓ Found ID in addedItem.Id:", itemId);
|
|
61
|
+
}
|
|
62
|
+
// Path 4: addedItem.ID (direct property, alternate casing)
|
|
63
|
+
else if (debugItem === null || debugItem === void 0 ? void 0 : debugItem.ID) {
|
|
64
|
+
itemId = debugItem.ID;
|
|
65
|
+
console.log("✓ Found ID in addedItem.ID:", itemId);
|
|
66
|
+
}
|
|
67
|
+
// Path 5: addedItem.id (lowercase)
|
|
68
|
+
else if (debugItem === null || debugItem === void 0 ? void 0 : debugItem.id) {
|
|
69
|
+
itemId = debugItem.id;
|
|
70
|
+
console.log("✓ Found ID in addedItem.id:", itemId);
|
|
71
|
+
}
|
|
72
|
+
// Validate the extracted ID
|
|
73
|
+
if (!itemId || typeof itemId !== "number" || itemId <= 0) {
|
|
74
|
+
console.error("❌ Item ID extraction failed. Extracted value:", itemId);
|
|
75
|
+
throw new Error(`Item created but ID extraction failed. Got: ${JSON.stringify(itemId)}`);
|
|
76
|
+
}
|
|
77
|
+
console.log(`✓ OTP Record created successfully with ID: ${itemId}`);
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
itemId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
85
|
+
console.error("❌ dispatchOtpPasscode Error:", errorMsg);
|
|
86
|
+
console.error("Full Error Object:", error);
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
error: errorMsg,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.dispatchOtpPasscode = dispatchOtpPasscode;
|
|
@@ -1,9 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file EnsurePendingVerificationsList.ts
|
|
3
|
+
*
|
|
4
|
+
* Utility module responsible for idempotent provisioning and configuration
|
|
5
|
+
* of the SharePoint list used for Two-Factor Authentication (2FA) / OTP queueing.
|
|
6
|
+
*
|
|
7
|
+
* This utility ensures:
|
|
8
|
+
* 1. The target SharePoint list exists (creates it if missing).
|
|
9
|
+
* 2. The required custom schema fields ('Passcode' and 'Channel') are provisioned.
|
|
10
|
+
* 3. Item-level security settings are hardened (ReadSecurity=2, WriteSecurity=2)
|
|
11
|
+
* so non-admin users cannot read or tamper with other users' verification codes.
|
|
12
|
+
*
|
|
13
|
+
* @module EnsurePendingVerificationsList
|
|
14
|
+
*/
|
|
1
15
|
import "@pnp/sp/webs";
|
|
2
16
|
import "@pnp/sp/lists";
|
|
3
17
|
import "@pnp/sp/fields";
|
|
4
|
-
import "@pnp/sp/views";
|
|
5
18
|
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
|
19
|
+
/**
|
|
20
|
+
* Default SharePoint list title used for queuing 2FA verification requests.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_VERIFICATION_LIST_NAME = "PendingVerifications";
|
|
23
|
+
/**
|
|
24
|
+
* Default SharePoint list description.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_VERIFICATION_LIST_DESC = "Stores temporary verification passcode records for digital signature workflows.";
|
|
27
|
+
/**
|
|
28
|
+
* Configuration properties for provisioning the pending verifications list.
|
|
29
|
+
*/
|
|
6
30
|
export interface IEnsurePendingVerificationsListProps {
|
|
31
|
+
/** The SPFx WebPartContext used to initialize the PnPjs client. */
|
|
7
32
|
context: WebPartContext;
|
|
33
|
+
/** Optional custom title for the SharePoint list (defaults to `"PendingVerifications"`). */
|
|
34
|
+
listTitle?: string;
|
|
35
|
+
/** Optional custom description for the list. */
|
|
36
|
+
listDescription?: string;
|
|
8
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Ensures that the SharePoint list for pending verification codes exists,
|
|
40
|
+
* has the required fields (`Passcode` and `Channel`), and enforces item-level
|
|
41
|
+
* read/write security.
|
|
42
|
+
*
|
|
43
|
+
* This function is fully **idempotent**: it can be safely executed multiple times
|
|
44
|
+
* without failing or duplicating columns.
|
|
45
|
+
*
|
|
46
|
+
* @param props - Configuration properties containing context and optional list overrides.
|
|
47
|
+
* @returns A promise that resolves when the list schema and security are verified.
|
|
48
|
+
*
|
|
49
|
+
* @throws {Error} If `context` is invalid or if permissions prevent list provisioning.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* await ensurePendingVerificationsList({
|
|
54
|
+
* context: this.context
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
9
58
|
export declare const ensurePendingVerificationsList: (props: IEnsurePendingVerificationsListProps) => Promise<void>;
|
|
@@ -1,62 +1,120 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file EnsurePendingVerificationsList.ts
|
|
4
|
+
*
|
|
5
|
+
* Utility module responsible for idempotent provisioning and configuration
|
|
6
|
+
* of the SharePoint list used for Two-Factor Authentication (2FA) / OTP queueing.
|
|
7
|
+
*
|
|
8
|
+
* This utility ensures:
|
|
9
|
+
* 1. The target SharePoint list exists (creates it if missing).
|
|
10
|
+
* 2. The required custom schema fields ('Passcode' and 'Channel') are provisioned.
|
|
11
|
+
* 3. Item-level security settings are hardened (ReadSecurity=2, WriteSecurity=2)
|
|
12
|
+
* so non-admin users cannot read or tamper with other users' verification codes.
|
|
13
|
+
*
|
|
14
|
+
* @module EnsurePendingVerificationsList
|
|
15
|
+
*/
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ensurePendingVerificationsList = void 0;
|
|
17
|
+
exports.ensurePendingVerificationsList = exports.DEFAULT_VERIFICATION_LIST_DESC = exports.DEFAULT_VERIFICATION_LIST_NAME = void 0;
|
|
4
18
|
const sp_1 = require("@pnp/sp");
|
|
5
19
|
require("@pnp/sp/webs");
|
|
6
20
|
require("@pnp/sp/lists");
|
|
7
21
|
require("@pnp/sp/fields");
|
|
8
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Default SharePoint list title used for queuing 2FA verification requests.
|
|
24
|
+
*/
|
|
25
|
+
exports.DEFAULT_VERIFICATION_LIST_NAME = "PendingVerifications";
|
|
26
|
+
/**
|
|
27
|
+
* Default SharePoint list description.
|
|
28
|
+
*/
|
|
29
|
+
exports.DEFAULT_VERIFICATION_LIST_DESC = "Stores temporary verification passcode records for digital signature workflows.";
|
|
30
|
+
/**
|
|
31
|
+
* Ensures that the SharePoint list for pending verification codes exists,
|
|
32
|
+
* has the required fields (`Passcode` and `Channel`), and enforces item-level
|
|
33
|
+
* read/write security.
|
|
34
|
+
*
|
|
35
|
+
* This function is fully **idempotent**: it can be safely executed multiple times
|
|
36
|
+
* without failing or duplicating columns.
|
|
37
|
+
*
|
|
38
|
+
* @param props - Configuration properties containing context and optional list overrides.
|
|
39
|
+
* @returns A promise that resolves when the list schema and security are verified.
|
|
40
|
+
*
|
|
41
|
+
* @throws {Error} If `context` is invalid or if permissions prevent list provisioning.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* await ensurePendingVerificationsList({
|
|
46
|
+
* context: this.context
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
9
50
|
const ensurePendingVerificationsList = async (props) => {
|
|
10
51
|
if (!props.context) {
|
|
11
52
|
throw new Error("Execution restricted: Valid WebPartContext is required.");
|
|
12
53
|
}
|
|
54
|
+
const listTitle = props.listTitle || exports.DEFAULT_VERIFICATION_LIST_NAME;
|
|
55
|
+
const listDescription = props.listDescription || exports.DEFAULT_VERIFICATION_LIST_DESC;
|
|
13
56
|
const sp = (0, sp_1.spfi)().using((0, sp_1.SPFx)(props.context));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
57
|
+
let list = sp.web.lists.getByTitle(listTitle);
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// 1. Ensure the List Exists
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
18
61
|
try {
|
|
19
62
|
await list();
|
|
20
63
|
}
|
|
21
64
|
catch (_a) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!listCreated) {
|
|
26
|
-
return;
|
|
65
|
+
// 100 = Generic List template, true = enable content approval/visibility
|
|
66
|
+
await sp.web.lists.add(listTitle, listDescription, 100, true);
|
|
67
|
+
list = sp.web.lists.getByTitle(listTitle);
|
|
27
68
|
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// 2. Helper: Ensure Field Exists (Idempotent Field Creation)
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* Checks if a field exists by internal name or title; if missing, calls the creation callback.
|
|
74
|
+
*
|
|
75
|
+
* @param fieldName - Internal name or title of the field.
|
|
76
|
+
* @param createFieldFn - Async callback to execute if the field does not exist.
|
|
77
|
+
*/
|
|
28
78
|
const ensureField = async (fieldName, createFieldFn) => {
|
|
29
79
|
try {
|
|
30
|
-
await
|
|
80
|
+
await list.fields.getByInternalNameOrTitle(fieldName)();
|
|
31
81
|
}
|
|
32
|
-
catch (
|
|
33
|
-
|
|
82
|
+
catch (_a) {
|
|
83
|
+
// Field does not exist, provision it
|
|
84
|
+
await createFieldFn();
|
|
34
85
|
}
|
|
35
86
|
};
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// 3. Ensure 'Passcode' Field
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
36
90
|
await ensureField("Passcode", async () => {
|
|
37
91
|
await list.fields.addText("Passcode", {
|
|
38
92
|
Required: true,
|
|
39
|
-
Description: "Temporary
|
|
93
|
+
Description: "Temporary 5-digit signature verification passcode.",
|
|
40
94
|
});
|
|
41
95
|
});
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 4. Ensure 'Channel' Field
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
42
99
|
await ensureField("Channel", async () => {
|
|
43
100
|
await list.fields.addChoice("Channel", {
|
|
44
101
|
Choices: ["email", "teams", "both"],
|
|
45
102
|
Required: true,
|
|
46
103
|
FillInChoice: false,
|
|
47
|
-
Description: "Target notification delivery route."
|
|
104
|
+
Description: "Target notification delivery route (email, teams, or both).",
|
|
48
105
|
});
|
|
49
106
|
const channelField = list.fields.getByInternalNameOrTitle("Channel");
|
|
50
|
-
await channelField.update({ DefaultValue: "
|
|
107
|
+
await channelField.update({ DefaultValue: "email" });
|
|
108
|
+
});
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// 5. Enforce Item-Level Security Settings
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// ReadSecurity: 2 -> Users can only read items that were created by themselves.
|
|
113
|
+
// WriteSecurity: 2 -> Users can only edit/delete items created by themselves.
|
|
114
|
+
// This prevents end-users from inspecting or hijacking other users' OTP codes.
|
|
115
|
+
await list.update({
|
|
116
|
+
ReadSecurity: 2,
|
|
117
|
+
WriteSecurity: 2,
|
|
51
118
|
});
|
|
52
|
-
try {
|
|
53
|
-
await list.update({
|
|
54
|
-
ReadSecurity: 2,
|
|
55
|
-
WriteSecurity: 2
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
catch (secErr) {
|
|
59
|
-
throw secErr;
|
|
60
|
-
}
|
|
61
119
|
};
|
|
62
120
|
exports.ensurePendingVerificationsList = ensurePendingVerificationsList;
|