@bdking71/spsignature 1.0.5 → 1.1.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 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
- Key Capabilities & Business Value
3
+ [![GitHub Repository](https://img.shields.io/badge/GitHub-Repository-blue?logo=github)](https://github.com/bdking71/spsignature)
4
+ [![npm version](https://img.shields.io/npm/v/@bdking71/spsignature.svg)](https://www.npmjs.com/package/@bdking71/spsignature)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- - Zero Infrastructure Overhead: Operates entirely within client-side M365 contexts—no external APIs, azure functions, or third-party storage backends required.
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
- - Cross-Platform M365 Integration: Designed for seamless deployment across SharePoint Online, Microsoft Teams, and Viva Connections desktop/mobile experiences.
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
- - Integrity & Non-Repudiation: Generates cryptographic SHA-256 hashes bound to user identities, timestamps, and target record IDs to prevent signature tampering.
11
+ ---
12
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.
13
+ ## Source Code & Repository
14
14
 
15
- ## Two-Factor Authentication (2FA / TFA) Architecture
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
- 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.
19
+ ---
18
20
 
19
- ![Diagram illustrating the 2FA workflow between SPFx, SharePoint, and Power Automate](./img/tfa.jpg)
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
- ## 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.
32
+ ## Two-Factor Authentication (2FA) Architecture
23
33
 
24
- - Power Automate Trigger: The component triggers a light HTTP webhook or writes a record to a dedicated SharePoint verification list.
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
- - Multi-Channel Delivery: The Power Automate flow instantly dispatches the code to the signer via:
36
+ ![Diagram illustrating the 2FA workflow between SPFx, SharePoint, and Power Automate](./img/tfa.jpg)
27
37
 
28
- - Microsoft Teams: Direct Adaptive Card or Activity Feed notification.
38
+ ### How the 2FA Workflow Operates
29
39
 
30
- - Outlook Email: High-priority internal notification.
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
- - 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.
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
- ```JSON
53
+ ```json
39
54
  {
40
- "signatureHash": "e3b0c4429 ...",
41
- "signatureData": "data:image/png;base64,iVB....",
55
+ "signatureHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
56
+ "signatureData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
42
57
  "signatureTimestamp": "2026-08-31T19:55:06.121Z",
43
- "verificationItemId": 1
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
- * **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`
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
- ## Quick Start & Local Development
86
+ ## Installation
58
87
 
59
- Execute these commands in sequence to establish your local development runtime:
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
- 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].
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
- public async onInit(): Promise<void> {
70
- await super.onInit();
115
+ export default class MySignatureWebPart extends BaseClientSideWebPart<IMySignatureWebPartProps> {
116
+
117
+ public async onInit(): Promise<void> {
118
+ await super.onInit();
71
119
 
72
- // Ensures 'PendingVerifications' list with 'Passcode' and 'Channel' fields exists
73
- await ensurePendingVerificationsList({ context: this.context });
120
+ // Provisions the 'PendingVerifications' list with 'Passcode' and 'Channel' columns
121
+ await ensurePendingVerificationsList({ context: this.context });
122
+ }
74
123
  }
75
124
  ```
76
125
 
77
- ### Launch Signature Modal & Capture Audit
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
- 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.
137
+ ### Step 3: Launch Signature Modal & Capture Audit
80
138
 
81
- ```TypeScript
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
- # License
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 (C) 2012 Veselin Todorov
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,53 @@
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, _e;
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
+ const extractedId = (_d = (_c = addedItem === null || addedItem === void 0 ? void 0 : addedItem.data) === null || _c === void 0 ? void 0 : _c.Id) !== null && _d !== void 0 ? _d : (_e = addedItem === null || addedItem === void 0 ? void 0 : addedItem.data) === null || _e === void 0 ? void 0 : _e.ID;
36
+ const itemId = typeof extractedId === "number" && extractedId > 0 ? extractedId : undefined;
37
+ if (!itemId) {
38
+ throw new Error("Item was created, but failed to retrieve a valid SharePoint Item ID.");
39
+ }
40
+ return {
41
+ success: true,
42
+ itemId,
43
+ };
44
+ }
45
+ catch (error) {
46
+ const errorMsg = error instanceof Error ? error.message : String(error);
47
+ return {
48
+ success: false,
49
+ error: errorMsg,
50
+ };
51
+ }
52
+ }
53
+ 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
- require("@pnp/sp/views");
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
- 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;
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
- await sp.web.lists.add(LIST_NAME, LIST_DESCRIPTION, 100, true);
23
- listCreated = true;
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 createFieldFn();
80
+ await list.fields.getByInternalNameOrTitle(fieldName)();
31
81
  }
32
- catch (creationErr) {
33
- throw creationErr;
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 6-digit signature verification code."
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: "teams" });
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;