Add minimal add-in playgrounds and orchestrator

This commit is contained in:
2025-12-07 21:26:10 -06:00
parent 40bcd939ff
commit ca95d296b5
54 changed files with 13165 additions and 26764 deletions
+23
View File
@@ -0,0 +1,23 @@
# Ignore dependencies
node_modules/
**/node_modules/
# Local secrets
*.env
.env.local
.env.*.local
.office-addin-dev-certs/
# Build artifacts
dist/
build/
# Logs and caches
*.log
npm-debug.log*
# Add-in certs
officeaddin/minimal-playground/certs/
minimal-uploader/certs/
minimal-uploader-playground/certs/
/.vscode/
+11
View File
@@ -1,2 +1,13 @@
# Job-Info # Job-Info
Development helper scripts
- start-all.js — small orchestrator script that starts the local `minimal-uploader-playground` and the `officeaddin/minimal-playground` static server, waits for their health checks, and streams logs.
Usage
```powershell
# from repo root
npm run start:all
```
Notes: I chose the small in-repo orchestrator (start-all) rather than pm2 for simplicity — it requires no global runtime and has clear, readable behavior for local development. If you prefer pm2 for process management / auto-restart, I can add pm2 config files and an alternate script.
-26251
View File
File diff suppressed because it is too large Load Diff
+2439
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -18,11 +18,15 @@
"typescript": "^5" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"@azure/msal-node": "^3.8.4",
"axios": "^1.13.2", "axios": "^1.13.2",
"cors": "^2.8.5",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.2.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"hono": "^4.10.6", "hono": "^4.10.6",
"joi": "^18.0.1", "joi": "^18.0.1",
"node-fetch": "^2.6.7",
"pino": "^10.1.0", "pino": "^10.1.0",
"pocketbase": "^0.26.3", "pocketbase": "^0.26.3",
"qs": "^6.14.0", "qs": "^6.14.0",
+194
View File
@@ -0,0 +1,194 @@
import 'dotenv/config.js';
import https from 'https';
import fs from 'fs';
import express from 'express';
import cors from 'cors';
import { ConfidentialClientApplication } from '@azure/msal-node';
import fetch from 'node-fetch';
const app = express();
app.use(cors());
app.use(express.json({ limit: '50mb' }));
const PORT = process.env.PORT || 3001;
// MSAL configuration for client credentials
const msalConfig = {
auth: {
clientId: process.env.CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
clientSecret: process.env.CLIENT_SECRET,
},
};
const cca = new ConfidentialClientApplication(msalConfig);
// Endpoint to exchange On-Behalf-Of token (legacy)
app.post('/api/obo-exchange', async (req, res) => {
const { userToken } = req.body;
if (!userToken) {
return res.status(400).json({ error: 'User token required' });
}
try {
const response = await cca.acquireTokenOnBehalfOf({
scopes: ['https://graph.microsoft.com/.default'],
oboAssertion: userToken,
});
res.json({ token: response.accessToken });
} catch (error) {
console.error('OBO exchange error:', error);
res.status(500).json({ error: error.message });
}
});
// Main endpoint: create folder + upload PDF
// Uses On-Behalf-Of flow: frontend sends user token → backend exchanges for Graph token
app.post('/api/upload-pdf', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const userAssertionToken = authHeader.substring(7); // Remove "Bearer " prefix
const { jobNumber, jobFullName, fileName, pdfBase64 } = req.body;
if (!jobNumber || !jobFullName || !fileName || !pdfBase64) {
return res.status(400).json({ error: 'Missing required fields' });
}
try {
// Exchange user token for Graph token using On-Behalf-Of flow
console.log('Exchanging user token via OBO flow...');
const tokenResponse = await cca.acquireTokenOnBehalfOf({
oboAssertion: userAssertionToken,
scopes: ['https://graph.microsoft.com/.default'],
});
const accessToken = tokenResponse.accessToken;
console.log('Token exchanged successfully. Proceeding with Graph API calls...');
const driveId = process.env.DRIVE_ID;
const parentId = process.env.PARENT_ITEM_ID;
// Create main folder: "Job_Number - Job_Full_Name"
const mainFolderName = `${jobNumber} - ${jobFullName}`;
const createFolderUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
const createFolderBody = {
name: mainFolderName,
folder: {},
'@microsoft.graph.conflictBehavior': 'rename',
};
const createFolderResp = await fetch(createFolderUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(createFolderBody),
});
if (!createFolderResp.ok) {
const errorData = await createFolderResp.text();
console.error('Create folder error:', errorData);
return res.status(500).json({ error: `Create folder failed: ${errorData}` });
}
const mainFolder = await createFolderResp.json();
const mainFolderId = mainFolder.id;
console.log(`✓ Created main folder: ${mainFolderName} (ID: ${mainFolderId})`);
// Create "Managers Info" subfolder
const subfolderName = 'Managers Info';
const createSubfolderUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${mainFolderId}/children`;
const createSubfolderBody = {
name: subfolderName,
folder: {},
'@microsoft.graph.conflictBehavior': 'rename',
};
const createSubfolderResp = await fetch(createSubfolderUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(createSubfolderBody),
});
if (!createSubfolderResp.ok) {
const errorData = await createSubfolderResp.text();
console.error('Create subfolder error:', errorData);
return res.status(500).json({ error: `Create subfolder failed: ${errorData}` });
}
const subfolder = await createSubfolderResp.json();
const subfolderId = subfolder.id;
console.log(`✓ Created subfolder: ${subfolderName} (ID: ${subfolderId})`);
// Upload PDF to subfolder
const pdfBuffer = Buffer.from(pdfBase64, 'base64');
const uploadUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subfolderId}:/${encodeURIComponent(fileName)}:/content`;
const uploadResp = await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/pdf',
},
body: pdfBuffer,
});
if (!uploadResp.ok) {
const errorData = await uploadResp.text();
console.error('Upload PDF error:', errorData);
return res.status(500).json({ error: `Upload PDF failed: ${errorData}` });
}
const uploadedItem = await uploadResp.json();
console.log(`✓ Uploaded PDF: ${fileName}`);
// Create share link for the subfolder
const shareLinkUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subfolderId}/createLink`;
const shareLinkBody = {
type: 'view',
scope: 'organization',
};
const shareLinkResp = await fetch(shareLinkUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(shareLinkBody),
});
if (!shareLinkResp.ok) {
const errorData = await shareLinkResp.text();
console.error('Create share link error:', errorData);
return res.status(500).json({ error: `Create share link failed: ${errorData}` });
}
const shareLink = await shareLinkResp.json();
console.log(`✓ Created share link`);
res.json({
uploadedItem,
shareLink: shareLink.link.webUrl,
});
} catch (error) {
console.error('Upload PDF error:', error);
res.status(500).json({ error: error.message });
}
});
// Health check
app.get('/', (req, res) => {
res.json({ message: 'Backend server running' });
});
// Fallback to HTTP (dev server doesn't need HTTPS for localhost)
app.listen(PORT, () => {
console.log(`✅ Backend listening on http://localhost:${PORT}`);
});
+8
View File
@@ -0,0 +1,8 @@
# Example .env for minimal-uploader (do not commit with real secrets)
CLIENT_SECRET=7aD8Q~d5K~_PzQv6KqDdrEnmyXHE60eVDpbcnaK_
TENANT_ID=3fd97ea7-b124-41f1-855f-52d8ac3b16c7
CLIENT_ID=3c846e71-9609-40e1-b458-0eb805e21b9f
DRIVE_ID=b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
PARENT_ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
# alias: ITEM_ID is also supported for compatibility with other folders in this repo
ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
+28
View File
@@ -0,0 +1,28 @@
# Minimal Uploader
This small utility uses environment variables in your environment (.env or other) to create a folder under a configured parent item in a drive, create a subfolder, and upload a PDF file.
Important: It expects the following environment variables to be available (use your existing .env to supply these values):
- CLIENT_ID
- CLIENT_SECRET
- TENANT_ID
- DRIVE_ID
- PARENT_ITEM_ID
Usage:
1. Ensure your environment has the required variables populated (you can copy your existing .env file into this folder or run the script from the repo root where the env is defined).
2. Install dependencies:
npm install
3. Dry-run (simulate without hitting Graph):
npm run check
4. Create folder, subfolder and upload a sample PDF (one-shot):
npm start
This script performs the operation with the app-only client credential flow (it will use the exact values from the environment variables). The uploaded PDF is a small made-up payload; all other values are read from environment variables (exact by design).
+6
View File
@@ -0,0 +1,6 @@
Name,Role,Email,Office,Phone
Alice Smith,Manager,alice@example.com,NY,555-0100
Bob Jones,Supervisor,bob@example.com,LA,555-0101
Charlie Davis,Coordinator,charlie@example.com,AUS,555-0102
David Evans,Lead,david@example.com,NY,555-0103
Eve Johnson,Manager,eve@example.com,LA,555-0104
1 Name Role Email Office Phone
2 Alice Smith Manager alice@example.com NY 555-0100
3 Bob Jones Supervisor bob@example.com LA 555-0101
4 Charlie Davis Coordinator charlie@example.com AUS 555-0102
5 David Evans Lead david@example.com NY 555-0103
6 Eve Johnson Manager eve@example.com LA 555-0104
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "minimal-uploader",
"version": "0.1.0",
"private": true,
"main": "run.js",
"scripts": {
"start": "node run.js",
"check": "node run.js --dry-run",
"server": "node server.js"
},
"dependencies": {
"@azure/msal-node": "^1.13.0",
"dotenv": "^17.2.3",
"node-fetch": "^2.6.7",
"express": "^4.18.2",
"cors": "^2.8.5",
"body-parser": "^1.20.2",
"pdfkit": "^0.13.0",
"csv-parse": "^5.4.1"
,"xlsx": "^0.18.5"
}
}
+581
View File
@@ -0,0 +1,581 @@
/* Minimal uploader: create folder, subfolder, upload PDF
Uses app-only MSAL client credentials from environment variables.
*/
// Load .env if present so this script can be run from the project folder
try { require('dotenv').config(); } catch (e) {}
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const { ConfidentialClientApplication } = require('@azure/msal-node');
const XLSX = require('xlsx');
// Shared cell formatter used by PDF generators
function formatCellValue(val) {
if (val === null || val === undefined) return '';
if (val instanceof Date && !isNaN(val.getTime())) {
const mm = String(val.getMonth() + 1).padStart(2, '0');
const dd = String(val.getDate()).padStart(2, '0');
const yyyy = val.getFullYear();
const hh = String(val.getHours()).padStart(2, '0');
const mi = String(val.getMinutes()).padStart(2, '0');
return `${mm}/${dd}/${yyyy} ${hh}:${mi}`;
}
if (typeof val === 'number') {
try {
const parsed = XLSX.SSF.parse_date_code(val);
if (parsed && parsed.y) {
const dt = new Date(Date.UTC(parsed.y, parsed.m - 1, parsed.d, parsed.H || 0, parsed.M || 0, parsed.S || 0));
return formatCellValue(dt);
}
} catch (e) { /* ignore */ }
}
if (typeof val === 'string') {
const s = val.trim();
const maybe = Date.parse(s);
if (!isNaN(maybe)) return formatCellValue(new Date(maybe));
return s;
}
return String(val);
}
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const TENANT_ID = process.env.TENANT_ID;
const DRIVE_ID = process.env.DRIVE_ID;
// Support both PARENT_ITEM_ID (preferred) and ITEM_ID (legacy / alternate)
const PARENT_ITEM_ID = process.env.PARENT_ITEM_ID || process.env.ITEM_ID;
const dryRun = process.argv.includes('--dry-run');
function assertEnv(name, val) {
if (!val) {
console.error(`Missing env var: ${name}`);
process.exitCode = 2;
throw new Error(`Missing env var ${name}`);
}
}
try {
assertEnv('CLIENT_ID', CLIENT_ID);
assertEnv('CLIENT_SECRET', CLIENT_SECRET);
assertEnv('TENANT_ID', TENANT_ID);
assertEnv('DRIVE_ID', DRIVE_ID);
assertEnv('PARENT_ITEM_ID', PARENT_ITEM_ID);
} catch (e) {
console.error('Please set required environment values before running. See .env.example');
process.exit(2);
}
const msalConfig = {
auth: {
clientId: CLIENT_ID,
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
clientSecret: CLIENT_SECRET
}
};
const cca = new ConfidentialClientApplication(msalConfig);
async function getAppToken() {
const result = await cca.acquireTokenByClientCredential({ scopes: [ 'https://graph.microsoft.com/.default' ] });
return result.accessToken;
}
// Small made-up (valid-ish) PDF bytes (1-page blank PDF) base64
const SAMPLE_PDF_BASE64 = Buffer.from(
`%PDF-1.4\n%\u00e2\u00e3\u00cf\u00d3\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 55 >>\nstream\nBT /F1 24 Tf 72 720 Td (Sample PDF uploaded by minimal-uploader) Tj ET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f\n0000000010 00000 n\n0000000066 00000 n\n0000000111 00000 n\n0000000200 00000 n\ntrailer<< /Size 5 /Root 1 0 R >>\nstartxref\n300\n%%EOF\n`).toString('base64');
// If managers.csv exists in the folder, build a PDF from it and return a base64 string
const pathToCsv = path.join(__dirname, 'managers.csv');
const { parse } = require('csv-parse/sync');
const PDFDocument = require('pdfkit');
async function pdfFromCsvToBase64(csvPath) {
if (!fs.existsSync(csvPath)) return null;
const csvRaw = fs.readFileSync(csvPath, 'utf8');
const records = parse(csvRaw, { columns: true, skip_empty_lines: true });
// Create PDF in memory using PDFKit
const doc = new PDFDocument({ size: 'A4', margin: 40 });
const buffers = [];
doc.on('data', (chunk) => buffers.push(chunk));
const title = 'Managers Info';
doc.fontSize(16).text(title, { align: 'left' }).moveDown(0.5);
// table header
const headers = Object.keys(records[0] || {});
doc.fontSize(10);
const columnWidths = headers.map(() => Math.floor((doc.page.width - 80) / headers.length));
// Render header row
headers.forEach((h, i) => {
doc.font('Helvetica-Bold').text(h, { continued: i !== headers.length - 1, width: columnWidths[i] });
});
doc.moveDown(0.25);
doc.font('Helvetica');
// Render rows limited to 49 (A1:E49) but safe if fewer
const limit = Math.min(records.length, 49);
for (let r = 0; r < limit; r++) {
const row = records[r];
headers.forEach((h, i) => {
const txt = row[h] !== undefined ? String(row[h]) : '';
doc.text(txt, { continued: i !== headers.length - 1, width: columnWidths[i] });
});
doc.moveDown(0.1);
}
doc.end();
await new Promise((res) => doc.on('end', res));
const pdfBuffer = Buffer.concat(buffers);
return pdfBuffer.toString('base64');
}
// build PDF from a records array (array of objects)
async function pdfFromRecordsToBase64(records, title = 'Managers Info') {
if (!records || records.length === 0) return null;
// Use the shared top-level formatCellValue helper
const doc = new PDFDocument({ size: 'A4', margin: 36 });
const buffers = [];
doc.on('data', (chunk) => buffers.push(chunk));
doc.on('error', (e) => console.error('PDF generation error', e));
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const startX = doc.x;
let cursorY = doc.y;
// Title
doc.font('Helvetica-Bold').fontSize(14).text(title, startX, cursorY);
cursorY = doc.y + 8;
// Setup table
const headers = Object.keys(records[0] || {});
const columnCount = Math.max(1, headers.length);
const baseWidth = Math.floor(pageWidth / columnCount);
const columnWidths = headers.map((_, idx) => {
if (columnCount >= 4 && idx === 0) return Math.floor(baseWidth * 1.5);
if (columnCount >= 4 && idx === 1) return Math.floor(baseWidth * 1.25);
return baseWidth;
});
const tableX = startX;
let tableY = cursorY;
const rowPadding = 6;
// Header row
const headerHeight = 20;
doc.rect(tableX, tableY, pageWidth, headerHeight).fill('#f2f2f2');
doc.fillColor('#000');
let cx = tableX;
for (let i = 0; i < headers.length; i++) {
doc.font('Helvetica-Bold').fontSize(10).fillColor('#000').text(String(headers[i] || ''), cx + rowPadding, tableY + 6, { width: columnWidths[i] - rowPadding * 2 });
cx += columnWidths[i];
}
doc.moveTo(tableX, tableY + headerHeight).lineTo(tableX + pageWidth, tableY + headerHeight).stroke();
cursorY = tableY + headerHeight;
// Rows
const limit = Math.min(records.length, 49);
for (let r = 0; r < limit; r++) {
const row = records[r];
const cellTexts = headers.map(h => formatCellValue(row[h]));
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: columnWidths[i] - rowPadding * 2 }));
const rowHeight = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + rowPadding * 2;
// page break
if (cursorY + rowHeight + 40 > doc.page.height - doc.page.margins.bottom) {
doc.addPage();
cursorY = doc.page.margins.top;
}
// background for alternating rows
if (r % 2 === 1) {
doc.rect(tableX, cursorY, pageWidth, rowHeight).fill('#fcfcfc');
doc.fillColor('#000');
}
cx = tableX;
for (let i = 0; i < headers.length; i++) {
doc.font('Helvetica').fontSize(9).fillColor('#000').text(cellTexts[i] || '', cx + rowPadding, cursorY + rowPadding, { width: columnWidths[i] - rowPadding * 2 });
// vertical separator
doc.moveTo(cx + columnWidths[i], cursorY).lineTo(cx + columnWidths[i], cursorY + rowHeight).stroke();
cx += columnWidths[i];
}
// bottom border
doc.moveTo(tableX, cursorY + rowHeight).lineTo(tableX + pageWidth, cursorY + rowHeight).stroke();
cursorY += rowHeight;
}
doc.end();
await new Promise((res) => doc.on('end', res));
const pdfBuffer = Buffer.concat(buffers);
return pdfBuffer.toString('base64');
}
async function downloadExcelFromGraph(accessToken, driveId, itemId) {
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
const res = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` }, });
if (!res.ok) throw new Error(`Download workbook failed: ${res.status} ${res.statusText}`);
const ab = await res.arrayBuffer();
return Buffer.from(ab);
}
function parseManagersSheetFromWorkbookBuffer(buffer) {
const workbook = XLSX.read(buffer, { type: 'buffer' });
// try the obvious names, otherwise find any sheet name that contains 'manager'
let sheet = workbook.Sheets['Managers Info'] || workbook.Sheets['Managers'] || null;
if (!sheet) {
const names = workbook.SheetNames || [];
const match = names.find(n => /manager/i.test(n));
if (match) sheet = workbook.Sheets[match];
else {
console.log('Workbook sheets:', names);
}
}
if (!sheet) return null;
// get A1:E49 region - using sheet_to_json with header row
const options = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
const rows = XLSX.utils.sheet_to_json(sheet, options);
if (!rows || rows.length === 0) return null;
// first row assumed to be headers
const headers = rows[0].map(h => String(h).trim());
const dataRows = rows.slice(1).map(r => {
const obj = {};
headers.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
return obj;
});
return dataRows;
}
// Helpers to read cells / ranges by address from a sheet
function getCellValue(sheet, addr) {
const cell = sheet[addr];
if (!cell) return '';
return cell.w || cell.v || '';
}
function getRangeRows(sheet, rangeStr) {
// decode range and return array of row arrays
const r = XLSX.utils.decode_range(rangeStr);
const rows = [];
for (let R = r.s.r; R <= r.e.r; ++R) {
const row = [];
for (let C = r.s.c; C <= r.e.c; ++C) {
const addr = XLSX.utils.encode_cell({ c: C, r: R });
row.push(getCellValue(sheet, addr));
}
rows.push(row);
}
return rows;
}
// Build PDF from workbook buffer with special region formatting (merged ranges and blocks)
async function pdfFromWorkbookBufferToBase64(buffer) {
const workbook = XLSX.read(buffer, { type: 'buffer' });
// find a Managers-like sheet
let sheetName = workbook.SheetNames.find(n => /Managers Info/i.test(n)) || workbook.SheetNames.find(n => /Managers/i.test(n));
if (!sheetName) sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
if (!sheet) return null;
// Prepare doc
const doc = new PDFDocument({ size: 'A4', margin: 36 });
const buffers = [];
doc.on('data', (chunk) => buffers.push(chunk));
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const leftX = doc.page.margins.left;
let y = doc.y;
// Top title (sheet title)
doc.font('Helvetica-Bold').fontSize(16).text(sheetName || 'Managers Info', { align: 'center' });
y = doc.y + 6;
// 1) B2:D3 merged: center horizontally and bottom align vertically, 18pt bold dark-blue
const b2Value = getCellValue(sheet, 'B2');
if (b2Value) {
// prefer Arial if available, otherwise use Helvetica-Bold as a safe built-in fallback
let usedFont = null;
try { doc.font('Arial'); usedFont = 'Arial'; } catch (e) { /* not available */ }
if (!usedFont) {
try { doc.font('Helvetica-Bold'); usedFont = 'Helvetica-Bold'; } catch (e) { /* ultimate fallback */ }
}
doc.fillColor('#003366').fontSize(18);
// center the text inside about 60% of page width
const boxW = Math.floor(pageWidth * 0.6);
const x = leftX + Math.floor((pageWidth - boxW) / 2);
// treat the merged region height as a small fixed box so we can bottom-align the text
const boxH = 48; // reasonable height for two-row merged area
// ensure there's enough space on the page
if (y + boxH > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
const h = doc.heightOfString(String(b2Value), { width: boxW });
// place the text so its bottom is just above the box bottom (6px padding)
const textY = y + Math.max(0, boxH - h - 6);
doc.text(String(b2Value), x, textY, { width: boxW, align: 'center' });
y += boxH + 8;
doc.fillColor('#000'); doc.font('Helvetica');
}
// Spacer
y += 4;
// 2) B5:D5 merged left-bottom aligned (smaller title/label)
const b5Value = getCellValue(sheet, 'B5');
if (b5Value) {
doc.font('Helvetica-Bold').fontSize(12).fillColor('#000');
const boxW2 = Math.floor(pageWidth * 0.6);
const boxH2 = 22; // single-row height approximation
if (y + boxH2 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
// compute measured height and bottom align inside box
const h2 = doc.heightOfString(String(b5Value), { width: boxW2 });
const textY2 = y + Math.max(0, boxH2 - h2 - 4);
doc.text(String(b5Value), leftX, textY2, { width: boxW2, align: 'left' });
y += boxH2 + 6;
}
// 3) B6:C9 cells -> render small grid block
const block1 = getRangeRows(sheet, 'B6:C9');
if (block1 && block1.length > 0) {
// render as two-column box
const colCount = block1[0].length;
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
// header style omitted (these are raw cells)
for (let r = 0; r < block1.length; r++) {
const row = block1[r];
let x = leftX;
let maxH = 0;
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
const h = doc.heightOfString(txt, { width: columnW - 6 });
maxH = Math.max(maxH, h);
}
// draw cells
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
doc.rect(x, y, columnW, maxH + 8).stroke();
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
x += columnW;
}
y += maxH + 10;
}
}
// 4) B11:D11 merged left bottom
const b11Value = getCellValue(sheet, 'B11');
if (b11Value) {
doc.font('Helvetica').fontSize(11).fillColor('#000');
doc.text(String(b11Value), leftX, y, { width: pageWidth * 0.6, align: 'left' });
y = doc.y + 6;
}
// 5) B12:C15 block
const block2 = getRangeRows(sheet, 'B12:C15');
if (block2 && block2.length > 0) {
const colCount = block2[0].length;
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
for (let r = 0; r < block2.length; r++) {
const row = block2[r];
let x = leftX;
let maxH = 0;
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
const h = doc.heightOfString(txt, { width: columnW - 6 });
maxH = Math.max(maxH, h);
}
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
doc.rect(x, y, columnW, maxH + 8).stroke();
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
x += columnW;
}
y += maxH + 10;
}
}
// Add small spacer
y += 8;
// Now append the main table below (A1:E49 region) - reuse previous parseManagersSheet logic
const rowsOpt = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
const rows = XLSX.utils.sheet_to_json(sheet, rowsOpt);
if (rows && rows.length > 1) {
const headersTable = rows[0].map(h => String(h).trim());
const dataRows = rows.slice(1).map(r => {
const obj = {};
headersTable.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
return obj;
});
// render the rest using the existing pdfFromRecordsToBase64 but passing doc starting state isn't simple
// so call pdfFromRecordsToBase64 to create a standalone PDF and then append? Simpler: render a simple table here.
// Use same table rendering approach as records conversion
const tableHeaders = headersTable;
const cCount = Math.max(1, tableHeaders.length);
const tBaseWidth = Math.floor(pageWidth / cCount);
const tColWidths = tableHeaders.map((_, idx) => tBaseWidth);
// draw headers
if (y + 30 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
let tx = leftX;
const th = 18;
doc.rect(tx, y, pageWidth, th).fill('#f2f2f2'); doc.fillColor('#000');
for (let i = 0; i < tableHeaders.length; i++) { doc.font('Helvetica-Bold').fontSize(10).text(tableHeaders[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 }); tx += tColWidths[i]; }
doc.moveTo(leftX, y + th).lineTo(leftX + pageWidth, y + th).stroke();
y += th;
const maxRows = Math.min(dataRows.length, 49);
for (let r = 0; r < maxRows; r++) {
const row = dataRows[r];
// compute row height
const cellTexts = tableHeaders.map(h => formatCellValue(row[h]));
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: tColWidths[i] - 8 }));
const rowH = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + 8;
if (y + rowH + 40 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
tx = leftX;
for (let i = 0; i < tableHeaders.length; i++) {
doc.font('Helvetica').fontSize(9).text(cellTexts[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 });
// vertical line
doc.moveTo(tx + tColWidths[i], y).lineTo(tx + tColWidths[i], y + rowH).stroke();
tx += tColWidths[i];
}
doc.moveTo(leftX, y + rowH).lineTo(leftX + pageWidth, y + rowH).stroke();
y += rowH;
}
}
doc.end();
await new Promise((res) => doc.on('end', res));
const pdfBuffer = Buffer.concat(buffers);
return pdfBuffer.toString('base64');
}
async function createChildFolder(accessToken, parentDriveId, parentItemId, folderName) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${parentDriveId}/items/${parentItemId}/children`;
const body = { name: folderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" };
if (dryRun) {
console.log('[dry-run] Would POST', endpoint, 'body', body);
return { id: 'dry-main-folder-id', name: folderName };
}
const resp = await fetch(endpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const json = await resp.json();
if (!resp.ok) throw new Error(`Create folder failed: ${JSON.stringify(json)}`);
return json;
}
async function uploadFileToFolder(accessToken, driveId, folderId, fileName, base64pdf) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${encodeURIComponent(fileName)}:/content`;
const buffer = Buffer.from(base64pdf, 'base64');
if (dryRun) {
console.log('[dry-run] Would PUT', endpoint, `(${buffer.length} bytes)`);
return { id: 'dry-upload-id', name: fileName, size: buffer.length };
}
const resp = await fetch(endpoint, {
method: 'PUT',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' },
body: buffer
});
const json = await resp.json();
if (!resp.ok) throw new Error(`Upload failed: ${JSON.stringify(json)}`);
return json;
}
async function createShareLink(accessToken, driveId, itemId) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/createLink`;
if (dryRun) {
console.log('[dry-run] Would POST', endpoint, 'body', { type: 'view', scope: 'organization' });
return { link: { webUrl: 'https://example.com/dry-share-link' } };
}
const resp = await fetch(endpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'view', scope: 'organization' })
});
const json = await resp.json();
if (!resp.ok) throw new Error(`Create share link failed: ${JSON.stringify(json)}`);
return json;
}
async function run() {
console.log('minimal-uploader starting', dryRun ? '(dry-run)' : '');
const token = await getAppToken();
console.log('acquired app token (truncated):', token ? token.substring(0, 30) + '...' : '(none)');
const mainFolderName = `0123 - Example Job (created ${new Date().toISOString()})`;
// sanitize names to avoid invalid SharePoint characters (e.g. ':' in ISO timestamps)
function sanitizeName(n) {
// Replace characters that are invalid in SharePoint file/folder names
// See: https://learn.microsoft.com/sharepoint/dev/general-development/avoid-using-invalid-file-name-characters
return n.replace(/["\*\\\/:<>\?|#{}%~&]/g, '-')
.replace(/\s+/g, ' ').trim();
}
const safeMainFolderName = sanitizeName(mainFolderName);
const subFolderName = 'Managers Info';
// Prefer generating from an Excel workbook if EXCEL_DRIVE_ID and EXCEL_ITEM_ID are present
let csvBase64 = await pdfFromCsvToBase64(pathToCsv);
if (process.env.EXCEL_DRIVE_ID && process.env.EXCEL_ITEM_ID) {
console.log('EXCEL_DRIVE_ID/EXCEL_ITEM_ID present, downloading workbook from Graph to build PDF');
try {
const token1 = token; // app token used for Graph
const workbookBuffer = await downloadExcelFromGraph(token1, process.env.EXCEL_DRIVE_ID, process.env.EXCEL_ITEM_ID);
// Build a PDF directly from the workbook and its formatting/regions if possible
const wbPdf = await pdfFromWorkbookBufferToBase64(workbookBuffer);
if (wbPdf) {
csvBase64 = wbPdf;
// count rows if we can
const recs = parseManagersSheetFromWorkbookBuffer(workbookBuffer) || [];
console.log('Built PDF from workbook Managers Info with', recs.length, 'rows');
} else {
console.log('No Managers Info sheet / no rows found in workbook — falling back to managers.csv or static PDF');
}
} catch (e) {
console.error('Failed to build PDF from workbook', e && e.message ? e.message : e);
}
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = sanitizeName(`Managers Info - ${timestamp}.pdf`);
console.log('Creating main folder under parent item', PARENT_ITEM_ID);
const main = await createChildFolder(token, DRIVE_ID, PARENT_ITEM_ID, safeMainFolderName);
console.log('main folder created:', main.id || mainName || JSON.stringify(main));
console.log('Creating subfolder under main folder', main.id);
const sub = await createChildFolder(token, DRIVE_ID, main.id, subFolderName);
console.log('subfolder created:', sub.id || JSON.stringify(sub));
console.log('Uploading PDF into subfolder', sub.id);
const payloadBase64 = csvBase64 || SAMPLE_PDF_BASE64;
if (!csvBase64) console.log('No managers.csv found - using fallback static PDF');
const uploaded = await uploadFileToFolder(token, DRIVE_ID, sub.id, fileName, payloadBase64);
console.log('uploaded result:', uploaded.id, uploaded.name, uploaded.size || '(size unknown)');
console.log('Creating share link for subfolder', sub.id);
const share = await createShareLink(token, DRIVE_ID, sub.id);
console.log('share link:', share && share.link && share.link.webUrl ? share.link.webUrl : JSON.stringify(share));
console.log('Done — created folder, subfolder and uploaded file.');
// return useful metadata so callers (like an API server) can report status
return { main, sub, uploaded, share };
}
// Export run so other modules (API server) can call it.
module.exports = { run };
if (require.main === module) {
// If executed directly from the CLI, run normally
run().catch(err => {
console.error('Error running minimal-uploader:', err && err.message ? err.message : err);
process.exit(1);
});
}
+31
View File
@@ -0,0 +1,31 @@
/* Simple playground server: exposes /api/upload-managers which runs the run() exporter
from run.js and returns the JSON result. Meant for local development. */
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const path = require('path');
const { run } = require('./run');
const app = express();
app.use(cors());
app.use(bodyParser.json());
// health
app.get('/api/health', (req, res) => res.json({ ok: true, ts: new Date().toISOString() }));
// main endpoint — run the uploader flow (download workbook if env vars exist, create folders and upload PDF)
app.post('/api/upload-managers', async (req, res) => {
try {
const result = await run();
res.json({ ok: true, result });
} catch (err) {
console.error('Error in /api/upload-managers', err);
res.status(500).json({ ok: false, error: (err && err.message) ? err.message : String(err) });
}
});
const port = process.env.PLAYGROUND_PORT || 3002;
app.listen(port, () => console.log(`minimal-uploader-playground server listening on http://localhost:${port}`));
+8
View File
@@ -0,0 +1,8 @@
# Example .env for minimal-uploader (do not commit with real secrets)
CLIENT_SECRET=7aD8Q~d5K~_PzQv6KqDdrEnmyXHE60eVDpbcnaK_
TENANT_ID=3fd97ea7-b124-41f1-855f-52d8ac3b16c7
CLIENT_ID=3c846e71-9609-40e1-b458-0eb805e21b9f
DRIVE_ID=b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
PARENT_ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
# alias: ITEM_ID is also supported for compatibility with other folders in this repo
ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
+28
View File
@@ -0,0 +1,28 @@
# Minimal Uploader
This small utility uses environment variables in your environment (.env or other) to create a folder under a configured parent item in a drive, create a subfolder, and upload a PDF file.
Important: It expects the following environment variables to be available (use your existing .env to supply these values):
- CLIENT_ID
- CLIENT_SECRET
- TENANT_ID
- DRIVE_ID
- PARENT_ITEM_ID
Usage:
1. Ensure your environment has the required variables populated (you can copy your existing .env file into this folder or run the script from the repo root where the env is defined).
2. Install dependencies:
npm install
3. Dry-run (simulate without hitting Graph):
npm run check
4. Create folder, subfolder and upload a sample PDF (one-shot):
npm start
This script performs the operation with the app-only client credential flow (it will use the exact values from the environment variables). The uploaded PDF is a small made-up payload; all other values are read from environment variables (exact by design).
+6
View File
@@ -0,0 +1,6 @@
Name,Role,Email,Office,Phone
Alice Smith,Manager,alice@example.com,NY,555-0100
Bob Jones,Supervisor,bob@example.com,LA,555-0101
Charlie Davis,Coordinator,charlie@example.com,AUS,555-0102
David Evans,Lead,david@example.com,NY,555-0103
Eve Johnson,Manager,eve@example.com,LA,555-0104
1 Name Role Email Office Phone
2 Alice Smith Manager alice@example.com NY 555-0100
3 Bob Jones Supervisor bob@example.com LA 555-0101
4 Charlie Davis Coordinator charlie@example.com AUS 555-0102
5 David Evans Lead david@example.com NY 555-0103
6 Eve Johnson Manager eve@example.com LA 555-0104
+1396
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "minimal-uploader",
"version": "0.1.0",
"private": true,
"main": "run.js",
"scripts": {
"start": "node run.js",
"check": "node run.js --dry-run"
},
"dependencies": {
"@azure/msal-node": "^1.13.0",
"dotenv": "^17.2.3",
"node-fetch": "^2.6.7",
"pdfkit": "^0.13.0",
"csv-parse": "^5.4.1"
,"xlsx": "^0.18.5"
}
}
+573
View File
@@ -0,0 +1,573 @@
/* Minimal uploader: create folder, subfolder, upload PDF
Uses app-only MSAL client credentials from environment variables.
*/
// Load .env if present so this script can be run from the project folder
try { require('dotenv').config(); } catch (e) {}
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const { ConfidentialClientApplication } = require('@azure/msal-node');
const XLSX = require('xlsx');
// Shared cell formatter used by PDF generators
function formatCellValue(val) {
if (val === null || val === undefined) return '';
if (val instanceof Date && !isNaN(val.getTime())) {
const mm = String(val.getMonth() + 1).padStart(2, '0');
const dd = String(val.getDate()).padStart(2, '0');
const yyyy = val.getFullYear();
const hh = String(val.getHours()).padStart(2, '0');
const mi = String(val.getMinutes()).padStart(2, '0');
return `${mm}/${dd}/${yyyy} ${hh}:${mi}`;
}
if (typeof val === 'number') {
try {
const parsed = XLSX.SSF.parse_date_code(val);
if (parsed && parsed.y) {
const dt = new Date(Date.UTC(parsed.y, parsed.m - 1, parsed.d, parsed.H || 0, parsed.M || 0, parsed.S || 0));
return formatCellValue(dt);
}
} catch (e) { /* ignore */ }
}
if (typeof val === 'string') {
const s = val.trim();
const maybe = Date.parse(s);
if (!isNaN(maybe)) return formatCellValue(new Date(maybe));
return s;
}
return String(val);
}
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const TENANT_ID = process.env.TENANT_ID;
const DRIVE_ID = process.env.DRIVE_ID;
// Support both PARENT_ITEM_ID (preferred) and ITEM_ID (legacy / alternate)
const PARENT_ITEM_ID = process.env.PARENT_ITEM_ID || process.env.ITEM_ID;
const dryRun = process.argv.includes('--dry-run');
function assertEnv(name, val) {
if (!val) {
console.error(`Missing env var: ${name}`);
process.exitCode = 2;
throw new Error(`Missing env var ${name}`);
}
}
try {
assertEnv('CLIENT_ID', CLIENT_ID);
assertEnv('CLIENT_SECRET', CLIENT_SECRET);
assertEnv('TENANT_ID', TENANT_ID);
assertEnv('DRIVE_ID', DRIVE_ID);
assertEnv('PARENT_ITEM_ID', PARENT_ITEM_ID);
} catch (e) {
console.error('Please set required environment values before running. See .env.example');
process.exit(2);
}
const msalConfig = {
auth: {
clientId: CLIENT_ID,
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
clientSecret: CLIENT_SECRET
}
};
const cca = new ConfidentialClientApplication(msalConfig);
async function getAppToken() {
const result = await cca.acquireTokenByClientCredential({ scopes: [ 'https://graph.microsoft.com/.default' ] });
return result.accessToken;
}
// Small made-up (valid-ish) PDF bytes (1-page blank PDF) base64
const SAMPLE_PDF_BASE64 = Buffer.from(
`%PDF-1.4\n%\u00e2\u00e3\u00cf\u00d3\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 55 >>\nstream\nBT /F1 24 Tf 72 720 Td (Sample PDF uploaded by minimal-uploader) Tj ET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f\n0000000010 00000 n\n0000000066 00000 n\n0000000111 00000 n\n0000000200 00000 n\ntrailer<< /Size 5 /Root 1 0 R >>\nstartxref\n300\n%%EOF\n`).toString('base64');
// If managers.csv exists in the folder, build a PDF from it and return a base64 string
const pathToCsv = path.join(__dirname, 'managers.csv');
const { parse } = require('csv-parse/sync');
const PDFDocument = require('pdfkit');
async function pdfFromCsvToBase64(csvPath) {
if (!fs.existsSync(csvPath)) return null;
const csvRaw = fs.readFileSync(csvPath, 'utf8');
const records = parse(csvRaw, { columns: true, skip_empty_lines: true });
// Create PDF in memory using PDFKit
const doc = new PDFDocument({ size: 'A4', margin: 40 });
const buffers = [];
doc.on('data', (chunk) => buffers.push(chunk));
const title = 'Managers Info';
doc.fontSize(16).text(title, { align: 'left' }).moveDown(0.5);
// table header
const headers = Object.keys(records[0] || {});
doc.fontSize(10);
const columnWidths = headers.map(() => Math.floor((doc.page.width - 80) / headers.length));
// Render header row
headers.forEach((h, i) => {
doc.font('Helvetica-Bold').text(h, { continued: i !== headers.length - 1, width: columnWidths[i] });
});
doc.moveDown(0.25);
doc.font('Helvetica');
// Render rows limited to 49 (A1:E49) but safe if fewer
const limit = Math.min(records.length, 49);
for (let r = 0; r < limit; r++) {
const row = records[r];
headers.forEach((h, i) => {
const txt = row[h] !== undefined ? String(row[h]) : '';
doc.text(txt, { continued: i !== headers.length - 1, width: columnWidths[i] });
});
doc.moveDown(0.1);
}
doc.end();
await new Promise((res) => doc.on('end', res));
const pdfBuffer = Buffer.concat(buffers);
return pdfBuffer.toString('base64');
}
// build PDF from a records array (array of objects)
async function pdfFromRecordsToBase64(records, title = 'Managers Info') {
if (!records || records.length === 0) return null;
// Use the shared top-level formatCellValue helper
const doc = new PDFDocument({ size: 'A4', margin: 36 });
const buffers = [];
doc.on('data', (chunk) => buffers.push(chunk));
doc.on('error', (e) => console.error('PDF generation error', e));
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const startX = doc.x;
let cursorY = doc.y;
// Title
doc.font('Helvetica-Bold').fontSize(14).text(title, startX, cursorY);
cursorY = doc.y + 8;
// Setup table
const headers = Object.keys(records[0] || {});
const columnCount = Math.max(1, headers.length);
const baseWidth = Math.floor(pageWidth / columnCount);
const columnWidths = headers.map((_, idx) => {
if (columnCount >= 4 && idx === 0) return Math.floor(baseWidth * 1.5);
if (columnCount >= 4 && idx === 1) return Math.floor(baseWidth * 1.25);
return baseWidth;
});
const tableX = startX;
let tableY = cursorY;
const rowPadding = 6;
// Header row
const headerHeight = 20;
doc.rect(tableX, tableY, pageWidth, headerHeight).fill('#f2f2f2');
doc.fillColor('#000');
let cx = tableX;
for (let i = 0; i < headers.length; i++) {
doc.font('Helvetica-Bold').fontSize(10).fillColor('#000').text(String(headers[i] || ''), cx + rowPadding, tableY + 6, { width: columnWidths[i] - rowPadding * 2 });
cx += columnWidths[i];
}
doc.moveTo(tableX, tableY + headerHeight).lineTo(tableX + pageWidth, tableY + headerHeight).stroke();
cursorY = tableY + headerHeight;
// Rows
const limit = Math.min(records.length, 49);
for (let r = 0; r < limit; r++) {
const row = records[r];
const cellTexts = headers.map(h => formatCellValue(row[h]));
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: columnWidths[i] - rowPadding * 2 }));
const rowHeight = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + rowPadding * 2;
// page break
if (cursorY + rowHeight + 40 > doc.page.height - doc.page.margins.bottom) {
doc.addPage();
cursorY = doc.page.margins.top;
}
// background for alternating rows
if (r % 2 === 1) {
doc.rect(tableX, cursorY, pageWidth, rowHeight).fill('#fcfcfc');
doc.fillColor('#000');
}
cx = tableX;
for (let i = 0; i < headers.length; i++) {
doc.font('Helvetica').fontSize(9).fillColor('#000').text(cellTexts[i] || '', cx + rowPadding, cursorY + rowPadding, { width: columnWidths[i] - rowPadding * 2 });
// vertical separator
doc.moveTo(cx + columnWidths[i], cursorY).lineTo(cx + columnWidths[i], cursorY + rowHeight).stroke();
cx += columnWidths[i];
}
// bottom border
doc.moveTo(tableX, cursorY + rowHeight).lineTo(tableX + pageWidth, cursorY + rowHeight).stroke();
cursorY += rowHeight;
}
doc.end();
await new Promise((res) => doc.on('end', res));
const pdfBuffer = Buffer.concat(buffers);
return pdfBuffer.toString('base64');
}
async function downloadExcelFromGraph(accessToken, driveId, itemId) {
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
const res = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` }, });
if (!res.ok) throw new Error(`Download workbook failed: ${res.status} ${res.statusText}`);
const ab = await res.arrayBuffer();
return Buffer.from(ab);
}
function parseManagersSheetFromWorkbookBuffer(buffer) {
const workbook = XLSX.read(buffer, { type: 'buffer' });
// try the obvious names, otherwise find any sheet name that contains 'manager'
let sheet = workbook.Sheets['Managers Info'] || workbook.Sheets['Managers'] || null;
if (!sheet) {
const names = workbook.SheetNames || [];
const match = names.find(n => /manager/i.test(n));
if (match) sheet = workbook.Sheets[match];
else {
console.log('Workbook sheets:', names);
}
}
if (!sheet) return null;
// get A1:E49 region - using sheet_to_json with header row
const options = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
const rows = XLSX.utils.sheet_to_json(sheet, options);
if (!rows || rows.length === 0) return null;
// first row assumed to be headers
const headers = rows[0].map(h => String(h).trim());
const dataRows = rows.slice(1).map(r => {
const obj = {};
headers.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
return obj;
});
return dataRows;
}
// Helpers to read cells / ranges by address from a sheet
function getCellValue(sheet, addr) {
const cell = sheet[addr];
if (!cell) return '';
return cell.w || cell.v || '';
}
function getRangeRows(sheet, rangeStr) {
// decode range and return array of row arrays
const r = XLSX.utils.decode_range(rangeStr);
const rows = [];
for (let R = r.s.r; R <= r.e.r; ++R) {
const row = [];
for (let C = r.s.c; C <= r.e.c; ++C) {
const addr = XLSX.utils.encode_cell({ c: C, r: R });
row.push(getCellValue(sheet, addr));
}
rows.push(row);
}
return rows;
}
// Build PDF from workbook buffer with special region formatting (merged ranges and blocks)
async function pdfFromWorkbookBufferToBase64(buffer) {
const workbook = XLSX.read(buffer, { type: 'buffer' });
// find a Managers-like sheet
let sheetName = workbook.SheetNames.find(n => /Managers Info/i.test(n)) || workbook.SheetNames.find(n => /Managers/i.test(n));
if (!sheetName) sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
if (!sheet) return null;
// Prepare doc
const doc = new PDFDocument({ size: 'A4', margin: 36 });
const buffers = [];
doc.on('data', (chunk) => buffers.push(chunk));
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const leftX = doc.page.margins.left;
let y = doc.y;
// Top title (sheet title)
doc.font('Helvetica-Bold').fontSize(16).text(sheetName || 'Managers Info', { align: 'center' });
y = doc.y + 6;
// 1) B2:D3 merged: center horizontally and bottom align vertically, 18pt bold dark-blue
const b2Value = getCellValue(sheet, 'B2');
if (b2Value) {
// prefer Arial if available, otherwise use Helvetica-Bold as a safe built-in fallback
let usedFont = null;
try { doc.font('Arial'); usedFont = 'Arial'; } catch (e) { /* not available */ }
if (!usedFont) {
try { doc.font('Helvetica-Bold'); usedFont = 'Helvetica-Bold'; } catch (e) { /* ultimate fallback */ }
}
doc.fillColor('#003366').fontSize(18);
// center the text inside about 60% of page width
const boxW = Math.floor(pageWidth * 0.6);
const x = leftX + Math.floor((pageWidth - boxW) / 2);
// treat the merged region height as a small fixed box so we can bottom-align the text
const boxH = 48; // reasonable height for two-row merged area
// ensure there's enough space on the page
if (y + boxH > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
const h = doc.heightOfString(String(b2Value), { width: boxW });
// place the text so its bottom is just above the box bottom (6px padding)
const textY = y + Math.max(0, boxH - h - 6);
doc.text(String(b2Value), x, textY, { width: boxW, align: 'center' });
y += boxH + 8;
doc.fillColor('#000'); doc.font('Helvetica');
}
// Spacer
y += 4;
// 2) B5:D5 merged left-bottom aligned (smaller title/label)
const b5Value = getCellValue(sheet, 'B5');
if (b5Value) {
doc.font('Helvetica-Bold').fontSize(12).fillColor('#000');
const boxW2 = Math.floor(pageWidth * 0.6);
const boxH2 = 22; // single-row height approximation
if (y + boxH2 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
// compute measured height and bottom align inside box
const h2 = doc.heightOfString(String(b5Value), { width: boxW2 });
const textY2 = y + Math.max(0, boxH2 - h2 - 4);
doc.text(String(b5Value), leftX, textY2, { width: boxW2, align: 'left' });
y += boxH2 + 6;
}
// 3) B6:C9 cells -> render small grid block
const block1 = getRangeRows(sheet, 'B6:C9');
if (block1 && block1.length > 0) {
// render as two-column box
const colCount = block1[0].length;
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
// header style omitted (these are raw cells)
for (let r = 0; r < block1.length; r++) {
const row = block1[r];
let x = leftX;
let maxH = 0;
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
const h = doc.heightOfString(txt, { width: columnW - 6 });
maxH = Math.max(maxH, h);
}
// draw cells
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
doc.rect(x, y, columnW, maxH + 8).stroke();
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
x += columnW;
}
y += maxH + 10;
}
}
// 4) B11:D11 merged left bottom
const b11Value = getCellValue(sheet, 'B11');
if (b11Value) {
doc.font('Helvetica').fontSize(11).fillColor('#000');
doc.text(String(b11Value), leftX, y, { width: pageWidth * 0.6, align: 'left' });
y = doc.y + 6;
}
// 5) B12:C15 block
const block2 = getRangeRows(sheet, 'B12:C15');
if (block2 && block2.length > 0) {
const colCount = block2[0].length;
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
for (let r = 0; r < block2.length; r++) {
const row = block2[r];
let x = leftX;
let maxH = 0;
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
const h = doc.heightOfString(txt, { width: columnW - 6 });
maxH = Math.max(maxH, h);
}
for (let c = 0; c < row.length; c++) {
const txt = String(row[c] || '');
doc.rect(x, y, columnW, maxH + 8).stroke();
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
x += columnW;
}
y += maxH + 10;
}
}
// Add small spacer
y += 8;
// Now append the main table below (A1:E49 region) - reuse previous parseManagersSheet logic
const rowsOpt = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
const rows = XLSX.utils.sheet_to_json(sheet, rowsOpt);
if (rows && rows.length > 1) {
const headersTable = rows[0].map(h => String(h).trim());
const dataRows = rows.slice(1).map(r => {
const obj = {};
headersTable.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
return obj;
});
// render the rest using the existing pdfFromRecordsToBase64 but passing doc starting state isn't simple
// so call pdfFromRecordsToBase64 to create a standalone PDF and then append? Simpler: render a simple table here.
// Use same table rendering approach as records conversion
const tableHeaders = headersTable;
const cCount = Math.max(1, tableHeaders.length);
const tBaseWidth = Math.floor(pageWidth / cCount);
const tColWidths = tableHeaders.map((_, idx) => tBaseWidth);
// draw headers
if (y + 30 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
let tx = leftX;
const th = 18;
doc.rect(tx, y, pageWidth, th).fill('#f2f2f2'); doc.fillColor('#000');
for (let i = 0; i < tableHeaders.length; i++) { doc.font('Helvetica-Bold').fontSize(10).text(tableHeaders[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 }); tx += tColWidths[i]; }
doc.moveTo(leftX, y + th).lineTo(leftX + pageWidth, y + th).stroke();
y += th;
const maxRows = Math.min(dataRows.length, 49);
for (let r = 0; r < maxRows; r++) {
const row = dataRows[r];
// compute row height
const cellTexts = tableHeaders.map(h => formatCellValue(row[h]));
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: tColWidths[i] - 8 }));
const rowH = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + 8;
if (y + rowH + 40 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
tx = leftX;
for (let i = 0; i < tableHeaders.length; i++) {
doc.font('Helvetica').fontSize(9).text(cellTexts[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 });
// vertical line
doc.moveTo(tx + tColWidths[i], y).lineTo(tx + tColWidths[i], y + rowH).stroke();
tx += tColWidths[i];
}
doc.moveTo(leftX, y + rowH).lineTo(leftX + pageWidth, y + rowH).stroke();
y += rowH;
}
}
doc.end();
await new Promise((res) => doc.on('end', res));
const pdfBuffer = Buffer.concat(buffers);
return pdfBuffer.toString('base64');
}
async function createChildFolder(accessToken, parentDriveId, parentItemId, folderName) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${parentDriveId}/items/${parentItemId}/children`;
const body = { name: folderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" };
if (dryRun) {
console.log('[dry-run] Would POST', endpoint, 'body', body);
return { id: 'dry-main-folder-id', name: folderName };
}
const resp = await fetch(endpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const json = await resp.json();
if (!resp.ok) throw new Error(`Create folder failed: ${JSON.stringify(json)}`);
return json;
}
async function uploadFileToFolder(accessToken, driveId, folderId, fileName, base64pdf) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${encodeURIComponent(fileName)}:/content`;
const buffer = Buffer.from(base64pdf, 'base64');
if (dryRun) {
console.log('[dry-run] Would PUT', endpoint, `(${buffer.length} bytes)`);
return { id: 'dry-upload-id', name: fileName, size: buffer.length };
}
const resp = await fetch(endpoint, {
method: 'PUT',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' },
body: buffer
});
const json = await resp.json();
if (!resp.ok) throw new Error(`Upload failed: ${JSON.stringify(json)}`);
return json;
}
async function createShareLink(accessToken, driveId, itemId) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/createLink`;
if (dryRun) {
console.log('[dry-run] Would POST', endpoint, 'body', { type: 'view', scope: 'organization' });
return { link: { webUrl: 'https://example.com/dry-share-link' } };
}
const resp = await fetch(endpoint, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'view', scope: 'organization' })
});
const json = await resp.json();
if (!resp.ok) throw new Error(`Create share link failed: ${JSON.stringify(json)}`);
return json;
}
async function run() {
console.log('minimal-uploader starting', dryRun ? '(dry-run)' : '');
const token = await getAppToken();
console.log('acquired app token (truncated):', token ? token.substring(0, 30) + '...' : '(none)');
const mainFolderName = `0123 - Example Job (created ${new Date().toISOString()})`;
// sanitize names to avoid invalid SharePoint characters (e.g. ':' in ISO timestamps)
function sanitizeName(n) {
// Replace characters that are invalid in SharePoint file/folder names
// See: https://learn.microsoft.com/sharepoint/dev/general-development/avoid-using-invalid-file-name-characters
return n.replace(/["\*\\\/:<>\?|#{}%~&]/g, '-')
.replace(/\s+/g, ' ').trim();
}
const safeMainFolderName = sanitizeName(mainFolderName);
const subFolderName = 'Managers Info';
// Prefer generating from an Excel workbook if EXCEL_DRIVE_ID and EXCEL_ITEM_ID are present
let csvBase64 = await pdfFromCsvToBase64(pathToCsv);
if (process.env.EXCEL_DRIVE_ID && process.env.EXCEL_ITEM_ID) {
console.log('EXCEL_DRIVE_ID/EXCEL_ITEM_ID present, downloading workbook from Graph to build PDF');
try {
const token1 = token; // app token used for Graph
const workbookBuffer = await downloadExcelFromGraph(token1, process.env.EXCEL_DRIVE_ID, process.env.EXCEL_ITEM_ID);
// Build a PDF directly from the workbook and its formatting/regions if possible
const wbPdf = await pdfFromWorkbookBufferToBase64(workbookBuffer);
if (wbPdf) {
csvBase64 = wbPdf;
// count rows if we can
const recs = parseManagersSheetFromWorkbookBuffer(workbookBuffer) || [];
console.log('Built PDF from workbook Managers Info with', recs.length, 'rows');
} else {
console.log('No Managers Info sheet / no rows found in workbook — falling back to managers.csv or static PDF');
}
} catch (e) {
console.error('Failed to build PDF from workbook', e && e.message ? e.message : e);
}
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = sanitizeName(`Managers Info - ${timestamp}.pdf`);
console.log('Creating main folder under parent item', PARENT_ITEM_ID);
const main = await createChildFolder(token, DRIVE_ID, PARENT_ITEM_ID, safeMainFolderName);
console.log('main folder created:', main.id || mainName || JSON.stringify(main));
console.log('Creating subfolder under main folder', main.id);
const sub = await createChildFolder(token, DRIVE_ID, main.id, subFolderName);
console.log('subfolder created:', sub.id || JSON.stringify(sub));
console.log('Uploading PDF into subfolder', sub.id);
const payloadBase64 = csvBase64 || SAMPLE_PDF_BASE64;
if (!csvBase64) console.log('No managers.csv found - using fallback static PDF');
const uploaded = await uploadFileToFolder(token, DRIVE_ID, sub.id, fileName, payloadBase64);
console.log('uploaded result:', uploaded.id, uploaded.name, uploaded.size || '(size unknown)');
console.log('Creating share link for subfolder', sub.id);
const share = await createShareLink(token, DRIVE_ID, sub.id);
console.log('share link:', share && share.link && share.link.webUrl ? share.link.webUrl : JSON.stringify(share));
console.log('Done — created folder, subfolder and uploaded file.');
}
run().catch(err => {
console.error('Error running minimal-uploader:', err && err.message ? err.message : err);
process.exit(1);
});
@@ -0,0 +1,11 @@
$ErrorActionPreference = 'Continue'
while ($true) {
$t = (Get-Date).ToString('o')
try {
$r = curl.exe -k https://localhost:3001/health 2>&1
Write-Output "$t - $r"
} catch {
Write-Output "$t - ERROR: $_"
}
Start-Sleep -Seconds 10
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@azure/msal-node": "^3.8.4",
"body-parser": "^2.2.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}
@@ -0,0 +1,216 @@
require("dotenv").config();
const tenantId = process.env.TENANT_ID;
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const fs = require("fs");
const path = require("path");
const https = require("https");
const express = require("express");
const cors = require("cors");
const { ConfidentialClientApplication } = require("@azure/msal-node");
const fetch = require('node-fetch');
const app = express();
// Use built-in JSON parser
app.use(express.json());
// Allow your task pane origin (HTTPS localhost:3000)
app.use(cors({
origin: "https://localhost:3000",
methods: ["POST", "OPTIONS"],
// frontend will send Authorization header when doing OBO/upload calls
allowedHeaders: ["Content-Type", "Authorization"],
}));
const msalConfig = {
auth: {
clientId,
authority: `https://login.microsoftonline.com/${tenantId}`,
clientSecret
}
};
const cca = new ConfidentialClientApplication(msalConfig);
const driveId = process.env.DRIVE_ID;
const parentItemId = process.env.PARENT_ITEM_ID;
// Helper to mask tokens in logs
function maskToken(token) {
if (!token || token.length < 12) return '***';
return token.slice(0, 6) + '...' + token.slice(-6);
}
// Endpoint to receive a PDF (base64) and upload it to Graph using app credentials
app.post('/api/upload-pdf', async (req, res) => {
try {
const { jobNumber, jobFullName, fileName, pdfBase64 } = req.body || {};
console.log(`[upload-pdf] incoming request jobNumber=${jobNumber} fileName=${fileName}`);
if (!jobNumber || !jobFullName || !fileName || !pdfBase64) {
console.warn('[upload-pdf] missing required fields');
return res.status(400).json({ error: 'Missing required fields' });
}
// Require a user token (OBO) — reject requests without an Authorization: Bearer <token>
const authHeader = (req.headers.authorization || req.headers.Authorization || '');
if (!authHeader.toLowerCase().startsWith('bearer ')) {
console.warn('[upload-pdf] no bearer token supplied; rejecting because OBO is required');
return res.status(401).json({ error: 'Authorization bearer token required for OBO (user delegated access)' });
}
const userAssertion = authHeader.substring(7).trim();
console.log(`[upload-pdf] Authorization present, token=${maskToken(userAssertion)}`);
// Perform OBO exchange — fail fast if OBO fails since we now require OBO
let accessToken;
try {
console.log('[upload-pdf] attempting OBO token exchange');
const oboRequest = {
oboAssertion: userAssertion,
scopes: [
'User.Read',
'Files.ReadWrite.All',
'Sites.ReadWrite.All'
]
};
const oboResult = await cca.acquireTokenOnBehalfOf(oboRequest);
accessToken = oboResult.accessToken;
console.log('[upload-pdf] OBO exchange successful; token=' + maskToken(accessToken));
} catch (oboErr) {
console.error('[upload-pdf] OBO exchange failed:', oboErr && oboErr.message ? oboErr.message : oboErr);
// include suberror/code if available for diagnostics
const details = {
error: oboErr && oboErr.errorCode ? oboErr.errorCode : undefined,
suberror: oboErr && oboErr.suberror ? oboErr.suberror : undefined,
message: oboErr && oboErr.message ? oboErr.message : String(oboErr)
};
return res.status(401).json({ error: 'OBO exchange failed', details });
}
// 1) Create main folder under configured parent
const mainFolderName = `${jobNumber} - ${jobFullName}`;
const createFolderEndpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentItemId}/children`;
console.log('[upload-pdf] creating main folder:', mainFolderName);
const folderResp = await fetch(createFolderEndpoint, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: mainFolderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" })
});
const folderData = await folderResp.json();
console.log('[upload-pdf] create folder response', { status: folderResp.status, body: folderData });
if (!folderResp.ok) {
console.error('[upload-pdf] create main folder failed', folderData);
return res.status(500).json({ error: 'Create main folder failed', details: folderData });
}
const mainFolderId = folderData.id;
// 2) Create subfolder Managers Info
console.log('[upload-pdf] creating subfolder: Managers Info under', mainFolderId);
const subFolderResp = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${mainFolderId}/children`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Managers Info', folder: {}, "@microsoft.graph.conflictBehavior": "rename" })
});
const subFolderData = await subFolderResp.json();
console.log('[upload-pdf] create subfolder response', { status: subFolderResp.status, body: subFolderData });
if (!subFolderResp.ok) {
console.error('[upload-pdf] create subfolder failed', subFolderData);
return res.status(500).json({ error: 'Create subfolder failed', details: subFolderData });
}
const subFolderId = subFolderData.id;
// 3) Upload PDF content
const fileBuffer = Buffer.from(pdfBase64, 'base64');
const uploadEndpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subFolderId}:/${encodeURIComponent(fileName)}:/content`;
console.log('[upload-pdf] uploading file to', uploadEndpoint, 'sizeBytes=', fileBuffer.length);
const uploadResp = await fetch(uploadEndpoint, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' },
body: fileBuffer
});
const uploadData = await uploadResp.json();
console.log('[upload-pdf] upload response', { status: uploadResp.status, body: uploadData });
if (!uploadResp.ok) {
console.error('[upload-pdf] upload failed', uploadData);
return res.status(500).json({ error: 'Upload failed', details: uploadData });
}
// 4) Create share link for subfolder
console.log('[upload-pdf] creating share link for subfolder', subFolderId);
const shareResp = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subFolderId}/createLink`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'view', scope: 'organization' })
});
const shareData = await shareResp.json();
console.log('[upload-pdf] share link response', { status: shareResp.status, body: shareData });
res.json({ uploadedItem: uploadData, shareLink: shareData.link && shareData.link.webUrl ? shareData.link.webUrl : null });
} catch (err) {
console.error('[upload-pdf] error:', err && err.message ? err.message : err);
res.status(500).json({ error: err.message || 'server error', details: err });
}
});
// Simple health check for local development
app.get('/health', (req, res) => {
res.json({ status: 'ok', time: new Date().toISOString() });
});
// 🔐 OBO endpoint (delegated scopes)
app.post("/api/obo-exchange", async (req, res) => {
try {
const { officeToken } = req.body || {};
if (!officeToken) {
return res.status(400).json({ error: "Missing officeToken" });
}
const oboRequest = {
oboAssertion: officeToken,
// ✅ Use standard scope strings (delegated)
scopes: [
"User.Read",
"Files.ReadWrite.All",
"Sites.ReadWrite.All"
]
};
const result = await cca.acquireTokenOnBehalfOf(oboRequest);
res.json({ graphToken: result.accessToken });
} catch (err) {
console.error("OBO exchange error:", err);
const status = (err.status || err.statusCode || 500);
res.status(status).json({
error: err.message || "OBO failed",
// helpful diagnostics in dev
details: err.errorCode || err.code || undefined,
suberror: err.suberror || undefined
});
}
});
// Try to start HTTPS using the dev cert created by office-addin-dev-certs.
// If certs are not available, fall back to plain HTTP so the server doesn't crash at startup.
const certDir = path.join(process.env.USERPROFILE || process.env.HOME, ".office-addin-dev-certs");
const keyPath = path.join(certDir, "localhost.key");
const certPath = path.join(certDir, "localhost.crt");
try {
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
const key = fs.readFileSync(keyPath);
const cert = fs.readFileSync(certPath);
https.createServer({ key, cert }, app).listen(3001, () => {
console.log("✅ Backend listening on https://localhost:3001");
});
} else {
throw new Error('Dev cert files not found');
}
} catch (err) {
console.warn('⚠️ Could not start HTTPS server on 3001:', err.message);
console.warn('Starting HTTP server on port 3001 as a fallback. Update your frontend to use http://localhost:3001 if necessary.');
app.listen(3001, () => {
console.log('⚠️ Backend listening on http://localhost:3001 (insecure fallback)');
});
}
@@ -0,0 +1,6 @@
{
"jobNumber": "E2E-TEST",
"jobFullName": "E2E Test",
"fileName": "e2e-test.pdf",
"pdfBase64": "JVBERi0xLjQKJYGBgYEKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9Db3VudCAxIC9LaWRzIFsgMyAwIFIgXSAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSA+PgplbmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9NZWRpYUJveCBbMCAwIDYxMiA3OTJdIC9Db250ZW50cyA0IDAgUiAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiA+PgplbmRvYmoKNCAwIG9iago8PCAvTGVuZ3RoIDY2ID4+CnN0cmVhbQpCBTAgMCB0ZiA8PC9GMSA0MCAvRjEgNDAgPj4KQk4KZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgNQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAxMTAgMDAwMDAgbiAKMDAwMDAwMDY1IDAwMDAwIG4gCjAwMDAwMDAxNjAgMDAwMDAgbiAKMDAwMDAwMDIyMCAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDUgL1Jvb3QgMSAwIFIgL0luZm8gPDwgL1N1YmplY3QgPj4gPj4Kc3RhcnR4cmVmCjI0NQolJUVPRgo="
}
+3 -94
View File
@@ -1,6 +1,5 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "office-addin-taskpane-js", "name": "office-addin-taskpane-js",
@@ -21,10 +20,10 @@
"html-loader": "^5.0.0", "html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0", "html-webpack-plugin": "^5.6.0",
"office-addin-cli": "^2.0.3", "office-addin-cli": "^2.0.3",
"office-addin-debugging": "^6.0.3", "office-addin-debugging": "^6.0.6",
"office-addin-dev-certs": "^2.0.3", "office-addin-dev-certs": "^2.0.6",
"office-addin-lint": "^3.0.3", "office-addin-lint": "^3.0.3",
"office-addin-manifest": "^2.0.3", "office-addin-manifest": "^2.1.2",
"office-addin-prettier-config": "^2.0.1", "office-addin-prettier-config": "^2.0.1",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"process": "^0.11.10", "process": "^0.11.10",
@@ -1814,10 +1813,6 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
"@apidevtools/swagger-parser/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@apidevtools/swagger-parser/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"@azure/arm-appservice/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], "@azure/arm-appservice/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="],
"@azure/arm-resources/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], "@azure/arm-resources/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="],
@@ -1826,8 +1821,6 @@
"@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.4", "", { "dependencies": { "@azure/msal-common": "15.13.3", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-lvuAwsDpPDE/jSuVQOBMpLbXuVuLsPNRwWCyK3/6bPlBk0fGWegqoZ0qjZclMWyQ2JNvIY3vHY7hoFmFmFQcOw=="], "@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.4", "", { "dependencies": { "@azure/msal-common": "15.13.3", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-lvuAwsDpPDE/jSuVQOBMpLbXuVuLsPNRwWCyK3/6bPlBk0fGWegqoZ0qjZclMWyQ2JNvIY3vHY7hoFmFmFQcOw=="],
"@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.13.3", "", {}, "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ=="], "@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.13.3", "", {}, "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ=="],
"@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
@@ -1840,8 +1833,6 @@
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
"@microsoft/app-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"@microsoft/kiota/adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], "@microsoft/kiota/adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
@@ -1852,8 +1843,6 @@
"@microsoft/m365-spec-parser/@microsoft/app-manifest": ["@microsoft/app-manifest@1.0.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", "ajv": "^8.5.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "node-fetch": "2.7.0" } }, "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ=="], "@microsoft/m365-spec-parser/@microsoft/app-manifest": ["@microsoft/app-manifest@1.0.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", "ajv": "^8.5.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "node-fetch": "2.7.0" } }, "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ=="],
"@microsoft/m365-spec-parser/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/core": ["@inquirer/core@5.1.2", "", { "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", "@types/node": "^20.10.7", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "figures": "^3.2.0", "mute-stream": "^1.0.0", "run-async": "^3.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0" } }, "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w=="], "@microsoft/m365agentstoolkit-cli/@inquirer/core": ["@inquirer/core@5.1.2", "", { "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", "@types/node": "^20.10.7", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "figures": "^3.2.0", "mute-stream": "^1.0.0", "run-async": "^3.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0" } }, "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts": ["@inquirer/prompts@6.0.1", "", { "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", "@inquirer/editor": "^3.0.1", "@inquirer/expand": "^3.0.1", "@inquirer/input": "^3.0.1", "@inquirer/number": "^2.0.1", "@inquirer/password": "^3.0.1", "@inquirer/rawlist": "^3.0.1", "@inquirer/search": "^2.0.1", "@inquirer/select": "^3.0.1" } }, "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A=="], "@microsoft/m365agentstoolkit-cli/@inquirer/prompts": ["@inquirer/prompts@6.0.1", "", { "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", "@inquirer/editor": "^3.0.1", "@inquirer/expand": "^3.0.1", "@inquirer/input": "^3.0.1", "@inquirer/number": "^2.0.1", "@inquirer/password": "^3.0.1", "@inquirer/rawlist": "^3.0.1", "@inquirer/search": "^2.0.1", "@inquirer/select": "^3.0.1" } }, "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A=="],
@@ -1868,18 +1857,12 @@
"@microsoft/m365agentstoolkit-cli/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@microsoft/m365agentstoolkit-cli/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@microsoft/teams-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/teams-manifest/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"@microsoft/teams-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "@microsoft/teams-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"@microsoft/teams-manifest/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@microsoft/teams-manifest/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest": ["@microsoft/app-manifest@1.0.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", "ajv": "^8.5.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "node-fetch": "2.7.0" } }, "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ=="], "@microsoft/teamsfx-api/@microsoft/app-manifest": ["@microsoft/app-manifest@1.0.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", "ajv": "^8.5.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "node-fetch": "2.7.0" } }, "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ=="],
"@microsoft/teamsfx-core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/teamsfx-core/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@microsoft/teamsfx-core/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@microsoft/teamsfx-core/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "@microsoft/teamsfx-core/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -1894,8 +1877,6 @@
"@types/express-serve-static-core/@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/express-serve-static-core/@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
@@ -1978,38 +1959,20 @@
"oas-validator/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], "oas-validator/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"office-addin-debugging/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-dev-certs/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-dev-settings/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-dev-settings/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
"office-addin-dev-settings/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="], "office-addin-dev-settings/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
"office-addin-dev-settings/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], "office-addin-dev-settings/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
"office-addin-lint/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-manifest/adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], "office-addin-manifest/adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
"office-addin-manifest/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-manifest/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "office-addin-manifest/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
"office-addin-manifest-converter/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "office-addin-manifest-converter/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"office-addin-node-debugger/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-node-debugger/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "office-addin-node-debugger/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"office-addin-project/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"office-addin-project/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "office-addin-project/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
"office-addin-usage-data/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], "pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
@@ -2058,22 +2021,10 @@
"wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"@apidevtools/swagger-parser/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.13.3", "", {}, "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ=="], "@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.13.3", "", {}, "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ=="],
"@azure/identity/open/define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"@microsoft/app-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/app-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], "@microsoft/m365agentstoolkit-cli/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
@@ -2112,20 +2063,8 @@
"@microsoft/m365agentstoolkit-cli/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "@microsoft/m365agentstoolkit-cli/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"@microsoft/teams-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/teams-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"@microsoft/teamsfx-core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
"@microsoft/teamsfx-core/office-addin-manifest/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="], "@microsoft/teamsfx-core/office-addin-manifest/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
"@microsoft/teamsfx-core/office-addin-manifest/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "@microsoft/teamsfx-core/office-addin-manifest/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
@@ -2212,20 +2151,12 @@
"serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"serve-static/send/mime": ["mime@1.6.0", "", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"@microsoft/app-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core": ["@inquirer/core@9.2.1", "", { "dependencies": { "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg=="], "@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core": ["@inquirer/core@9.2.1", "", { "dependencies": { "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="], "@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="],
@@ -2270,18 +2201,6 @@
"@microsoft/m365agentstoolkit-cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "@microsoft/m365agentstoolkit-cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"@microsoft/teams-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
"@microsoft/teamsfx-core/office-addin-manifest/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "@microsoft/teamsfx-core/office-addin-manifest/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"@microsoft/teamsfx-core/office-addin-manifest/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "@microsoft/teamsfx-core/office-addin-manifest/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
@@ -2306,8 +2225,6 @@
"wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], "@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
@@ -2370,12 +2287,6 @@
"@microsoft/m365agentstoolkit-cli/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "@microsoft/m365agentstoolkit-cli/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
"cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -2386,8 +2297,6 @@
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
"@microsoft/teamsfx-core/office-addin-manifest/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"import-local/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "import-local/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], "pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
+32 -26
View File
@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp"> <OfficeApp
xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
xsi:type="TaskPaneApp">
<Id>0b7d2846-9305-4e4b-906d-d73a6a4d286a</Id> <Id>0b7d2846-9305-4e4b-906d-d73a6a4d286a</Id>
<Version>1.0.0.0</Version> <Version>1.0.0.0</Version>
<ProviderName>Contoso</ProviderName> <ProviderName>Contoso</ProviderName>
@@ -9,26 +15,37 @@
<IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/> <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/> <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
<SupportUrl DefaultValue="https://www.contoso.com/help"/> <SupportUrl DefaultValue="https://www.contoso.com/help"/>
<AppDomains>
<AppDomain>https://www.contoso.com</AppDomain> <AppDomains>
<AppDomain>https://localhost:3000</AppDomain>
</AppDomains> </AppDomains>
<Hosts> <Hosts>
<Host Name="Workbook"/> <Host Name="Workbook"/>
</Hosts> </Hosts>
<DefaultSettings> <DefaultSettings>
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/> <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
</DefaultSettings> </DefaultSettings>
<Permissions>ReadWriteDocument</Permissions> <Permissions>ReadWriteDocument</Permissions>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
<VersionOverrides
xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
xsi:type="VersionOverridesV1_0">
<Hosts> <Hosts>
<Host xsi:type="Workbook"> <Host xsi:type="Workbook">
<DesktopFormFactor> <DesktopFormFactor>
<GetStarted> <GetStarted>
<Title resid="GetStarted.Title"/> <Title resid="GetStarted.Title"/>
<Description resid="GetStarted.Description"/> <Description resid="GetStarted.Description"/>
<LearnMoreUrl resid="GetStarted.LearnMoreUrl"/> <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
</GetStarted> </GetStarted>
<FunctionFile resid="Commands.Url"/> <FunctionFile resid="Commands.Url"/>
<ExtensionPoint xsi:type="PrimaryCommandSurface"> <ExtensionPoint xsi:type="PrimaryCommandSurface">
<OfficeTab id="TabHome"> <OfficeTab id="TabHome">
<Group id="CommandsGroup"> <Group id="CommandsGroup">
@@ -57,47 +74,36 @@
</Group> </Group>
</OfficeTab> </OfficeTab>
</ExtensionPoint> </ExtensionPoint>
</DesktopFormFactor> </DesktopFormFactor>
</Host> </Host>
</Hosts> </Hosts>
<Resources> <Resources>
<bt:Images> <bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/> <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/> <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/> <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
</bt:Images> </bt:Images>
<bt:Urls> <bt:Urls>
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/> <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/> <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/> <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
<bt:Url id="CreateFolder.Url" DefaultValue="https://localhost:3000/createFolder.html"/>
</bt:Urls> </bt:Urls>
<bt:ShortStrings> <bt:ShortStrings>
<bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/> <bt:String id="GetStarted.Title" DefaultValue="Get started with your add-in!"/>
<bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/> <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Show Task Pane"/> <bt:String id="TaskpaneButton.Label" DefaultValue="Show Task Pane"/>
</bt:ShortStrings> </bt:ShortStrings>
<bt:LongStrings> <bt:LongStrings>
<bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded successfully. Go to the HOME tab and click the 'Show Task Pane' button to get started."/> <bt:String id="GetStarted.Description" DefaultValue="Your add-in loaded successfully. Go to the HOME tab and click 'Show Task Pane'."/>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Taskpane"/> <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Taskpane"/>
</bt:LongStrings> </bt:LongStrings>
</Resources> </Resources>
<Requirements>
<Sets DefaultMinVersion="1.1">
<Set Name="IdentityAPI" MinVersion="1.3"/>
</Sets>
</Requirements>
<WebApplicationInfo>
<!-- Application (client) ID from your Azure AD / Entra app registration -->
<Id>3c846e71-9609-40e1-b458-0eb805e21b9f</Id>
<!-- The resource/audience URI your app uses; common pattern is api://{clientId} -->
<Resource>api://3c846e71-9609-40e1-b458-0eb805e21b9f</Resource>
<!-- Scopes your backend will request via OBO to call Microsoft Graph -->
<Scopes>
<Scope>User.Read</Scope>
<Scope>Files.ReadWrite.All</Scope>
<Scope>Sites.ReadWrite.All</Scope>
<!-- Add any other Graph scopes you need -->
</Scopes>
</WebApplicationInfo>
</VersionOverrides> </VersionOverrides>
</OfficeApp> </OfficeApp>
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -27,6 +27,9 @@
}, },
"dependencies": { "dependencies": {
"core-js": "^3.36.0", "core-js": "^3.36.0",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.4",
"office-js": "^0.1.0",
"regenerator-runtime": "^0.14.1" "regenerator-runtime": "^0.14.1"
}, },
"devDependencies": { "devDependencies": {
@@ -42,10 +45,10 @@
"html-loader": "^5.0.0", "html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0", "html-webpack-plugin": "^5.6.0",
"office-addin-cli": "^2.0.3", "office-addin-cli": "^2.0.3",
"office-addin-debugging": "^6.0.3", "office-addin-debugging": "^6.0.6",
"office-addin-dev-certs": "^2.0.3", "office-addin-dev-certs": "^2.0.6",
"office-addin-lint": "^3.0.3", "office-addin-lint": "^3.0.3",
"office-addin-manifest": "^2.0.3", "office-addin-manifest": "^2.1.2",
"office-addin-prettier-config": "^2.0.1", "office-addin-prettier-config": "^2.0.1",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"process": "^0.11.10", "process": "^0.11.10",
@@ -59,4 +62,4 @@
"last 2 versions", "last 2 versions",
"ie 11" "ie 11"
] ]
} }
@@ -0,0 +1,70 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const PORT = process.env.PORT || 3001;
const DIST_DIR = path.join(__dirname, 'dist');
const certDir = process.env.DEV_CERT_DIR || path.join(process.env.USERPROFILE || '~', '.office-addin-dev-certs');
const certPath = path.join(certDir, 'localhost.crt');
const keyPath = path.join(certDir, 'localhost.key');
function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.html': return 'text/html; charset=utf-8';
case '.js': return 'application/javascript; charset=utf-8';
case '.css': return 'text/css; charset=utf-8';
case '.png': return 'image/png';
case '.jpg': case '.jpeg': return 'image/jpeg';
case '.gif': return 'image/gif';
case '.svg': return 'image/svg+xml';
case '.json': return 'application/json';
case '.ico': return 'image/x-icon';
default: return 'application/octet-stream';
}
}
let options;
try {
options = {
cert: fs.readFileSync(certPath),
key: fs.readFileSync(keyPath),
};
} catch (err) {
console.error('Failed to read dev cert/key. Check DEV_CERT_DIR or ~/.office-addin-dev-certs:', err.message);
process.exitCode = 1;
}
const server = https.createServer(options, (req, res) => {
// Normalize URL and prevent directory traversal
const safeUrl = decodeURIComponent(req.url.split('?')[0] || '/');
let filePath = path.join(DIST_DIR, safeUrl.replace(/^\//, ''));
// If request is for directory or root, fallback to index.html
if (safeUrl === '/' || safeUrl.endsWith('/')) {
filePath = path.join(DIST_DIR, 'taskpane.html');
}
// If file doesn't exist, attempt to serve the file within dist, else 404
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' });
res.end('Not found');
return;
}
const contentType = getContentType(filePath);
res.writeHead(200, { 'Content-Type': contentType, 'Access-Control-Allow-Origin': '*' });
const stream = fs.createReadStream(filePath);
stream.pipe(res);
});
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`HTTPS static server running at https://localhost:${PORT}/ serving ${DIST_DIR}`);
});
server.on('error', (err) => {
console.error('Server error:', err.message || err);
});
@@ -0,0 +1,51 @@
/**
* MSAL Configuration for delegated user authentication
* Used by Office Add-in to acquire user tokens with automatic refresh
*/
export const msalConfig = {
auth: {
clientId: '3c846e71-9609-40e1-b458-0eb805e21b9f', // Replace with your app ID
authority: 'https://login.microsoftonline.com/common', // Multi-tenant
redirectUri: 'https://localhost:3000/createFolder.html', // Must match manifest
},
cache: {
cacheLocation: 'localStorage', // Persists tokens across sessions
storeAuthStateInCookie: false,
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (!containsPii) {
try {
// Forward MSAL logs to the add-in UI logger if available
if (typeof window !== 'undefined' && window.__addinLog) {
window.__addinLog(`[MSAL] ${message}`);
} else {
console.log(`[MSAL] ${message}`);
}
} catch (e) {
console.log(`[MSAL] ${message}`);
}
}
},
piiLoggingEnabled: false,
logLevel: 'Verbose',
},
},
};
/**
* Scopes required for the add-in
* Files.ReadWrite.All: Create/read/write files in SharePoint
*/
export const loginRequest = {
scopes: ['https://graph.microsoft.com/Files.ReadWrite.All'],
};
/**
* Scopes for acquiring token for API calls (backend)
*/
export const tokenRequest = {
scopes: ['https://graph.microsoft.com/Files.ReadWrite.All'],
};
@@ -0,0 +1,7 @@
# Copy this file to .env and fill values only if you implement server-side/OBO flows
# Do NOT commit your real .env with secrets
CLIENT_ID=your-client-id-here
TENANT_ID=your-tenant-id-here
CLIENT_SECRET=your-client-secret-here
DRIVE_ID=your-drive-id-here
PARENT_ITEM_ID=your-parent-item-id-here
@@ -0,0 +1,34 @@
CreateFolderOfficeAddIn (MVP)
Summary
- Taskpane that reads the sheet named "Managers Info", generates a PDF of that sheet, creates a folder named `Job_Number - Job_Full_Name` under the provided parent folder, creates a subfolder `Managers Info`, uploads the PDF with a timestamped filename, and returns a share link.
Quick start (dev, paste-token flow)
1. Start the dev server (project root `Create Job Folder`):
```powershell
Set-Location 'C:\Users\jesus\Gitea\Job-Info\officeaddin\Create Job Folder'
npm install
npm run dev-server
# or build for production
npm run build
```
2. Open the page in the Office Add-in or browser (for testing):
- https://localhost:3000/createFolder.html
3. In the add-in UI:
- Enter `Job Number` and `Job Full Name`.
- Paste a Graph delegated access token (must include `Files.ReadWrite.All`) into the token box. You can get a token from Graph Explorer or Postman using your account.
- Click `Create folder, generate PDF and upload`.
Notes
- Timestamp format used in filenames: `MM-DD-YYYY_HHMM` (local US time-style formatting).
- Token is used in-memory only and not stored.
- Do NOT commit real secrets to the repo. Use a local `.env` for any server-side flows.
Optional: .env
- If you later add server-side/OBO flows, copy `.env.sample` to `.env` and populate the values (do not commit `.env`).
Contact
- Tell me if you want me to add an MSAL sign-in flow (optional) so tokens are obtained automatically.
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="geolocation=(), microphone=(), camera=()" />
<title>Create Folder Add-in</title>
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
<!-- MSAL for user authentication (one-time sign-in, automatic token refresh) -->
<script src="https://alcdn.msauth.net/browser/2.30.0/js/msal-browser.min.js"></script>
<!-- html2canvas and jspdf are now bundled via webpack -->
<style>
body { font-family: Arial, Helvetica, sans-serif; padding: 12px; }
button { padding:8px 12px; margin-top:8px; cursor: pointer; background: #0078d4; color: white; border: none; border-radius: 4px; }
button:hover { background: #005a9e; }
#preview { border:1px solid #ddd; padding:8px; margin-top:8px; max-width:800px; overflow:auto; display:none; }
#statusMessage { margin-top: 12px; font-weight: bold; }
#shareLink { margin-top: 8px; word-break: break-all; }
</style>
</head>
<body>
<h2>Create Job Folder & Upload PDF</h2>
<!-- Fatal error container (shown when the add-in cannot initialize) -->
<div id="fatalError" role="alert" aria-live="assertive" style="display:none; background:#fff4f4; border:1px solid #f5c2c2; color:#8b0000; padding:10px; margin-top:10px; border-radius:6px;">
<div style="font-size:120%; font-weight:700;">Could not be started</div>
<strong>Sorry — we can't load the add-in right now.</strong>
<div id="fatalErrorDetails" style="margin-top:6px; font-size:90%; color:#400000"></div>
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<button id="copyDiagnostics" style="padding:6px 8px; background:#444; color:white; border-radius:4px; border:none; cursor:pointer;">Copy diagnostics</button>
<button id="downloadDiagnostics" style="padding:6px 8px; background:#2b6cb0; color:white; border-radius:4px; border:none; cursor:pointer;">Download logs</button>
<span id="diagSaved" style="font-size:90%; color:#333; margin-left:8px; display:none">Saved</span>
</div>
<div style="margin-top:8px; font-size:90%">Try reloading the taskpane, sign out/in, or check your network connection.</div>
</div>
<p><strong>How it works:</strong></p>
<ul>
<li>Sign in once with your Microsoft account (automatic token refresh)</li>
<li>Click the button to read job info from Excel and generate/upload PDF</li>
<li>You become the file owner in SharePoint</li>
</ul>
<button id="createBtn">📁 Create folder & upload PDF</button>
<span id="spinner" role="status" aria-live="polite" style="display:none; margin-left:8px;">
<svg width="18" height="18" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg" style="vertical-align:middle">
<circle cx="25" cy="25" r="20" fill="none" stroke="#0078d4" stroke-width="5" stroke-linecap="round" stroke-dasharray="31.4 31.4">
<animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
</circle>
</svg>
</span>
<p id="statusMessage"></p>
<div id="logContainer" style="margin-top:12px; max-height:160px; overflow:auto; background:#f9f9f9; border:1px solid #eee; padding:8px; font-family:monospace; font-size:12px;">
<strong style="display:block; margin-bottom:6px;">Activity Log</strong>
<div id="logArea"></div>
</div>
<div>
<strong>Share link:</strong>
<div id="shareLink"></div>
</div>
<div id="preview"></div>
<script src="createFolder.js"></script>
</body>
</html>
@@ -0,0 +1,516 @@
/* global Office, Excel, msal */
import { msalConfig, loginRequest, tokenRequest } from '../../msalConfig.js';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
// MSAL instance for user authentication (delegated flow)
let msalInstance = null;
let userToken = null;
// In-memory buffer of diagnostic lines (kept even if DevTools closes).
const DIAGNOSTIC_BUFFER_LIMIT = 1000;
const diagBuffer = [];
// restore persisted diagnostics when available
try {
const saved = localStorage.getItem('addinDiagnostics');
if (saved) {
const arr = JSON.parse(saved);
if (Array.isArray(arr)) {
for (const l of arr) diagBuffer.push(l);
}
}
} catch (e) { /* ignore */ }
function pushDiagnosticsLine(line) {
try {
const iso = new Date().toISOString();
const msg = `${iso} - ${line}`;
diagBuffer.push(msg);
if (diagBuffer.length > DIAGNOSTIC_BUFFER_LIMIT) diagBuffer.splice(0, diagBuffer.length - DIAGNOSTIC_BUFFER_LIMIT);
// persist a copy in localStorage so it survives crashes
try { localStorage.setItem('addinDiagnostics', JSON.stringify(diagBuffer)); } catch (e) { /* ignore */ }
} catch (e) { /* ignore */ }
}
// Simple UI logger that writes to #logArea (and console)
function addLog(message) {
try {
const t = new Date().toISOString();
const line = `${t} - ${message}`;
// console
console.log(line);
// in-page log
const area = document && document.getElementById ? document.getElementById('logArea') : null;
if (area) {
const div = document.createElement('div');
div.textContent = line;
area.appendChild(div);
// keep last ~200 lines
while (area.childNodes.length > 200) area.removeChild(area.firstChild);
// scroll into view
area.scrollTop = area.scrollHeight;
}
// also record into diagnostics buffer
pushDiagnosticsLine(message);
} catch (e) {
try { console.log('addLog error', e); } catch (e2) {}
}
}
// Show a fatal error message in the UI and disable interactions
function showFatalError(message, details) {
try {
console.error('[fatal] ', message, details || '');
pushDiagnosticsLine('[FATAL] ' + message + ' | ' + (details ? (typeof details === 'string' ? details : JSON.stringify(details)) : ''));
const el = document.getElementById('fatalError');
const detailsEl = document.getElementById('fatalErrorDetails');
if (el) el.style.display = 'block';
// Always show a short 'Could not be started' title (for clarity)
if (detailsEl) detailsEl.textContent = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : message;
// Also ensure the primary text includes 'Could not be started' for better clarity
const primary = el && el.querySelector ? el.querySelector('strong') : null;
if (primary) {
primary.textContent = "Sorry — we can't load the add-in right now.";
// add a secondary clarification line for 'Could not be started'
// (the HTML shows a header already)
}
// disable the create button if present
const createBtn = document.getElementById('createBtn');
if (createBtn) {
createBtn.disabled = true;
createBtn.textContent = 'Unavailable';
}
const spinner = document.getElementById('spinner');
if (spinner) spinner.style.display = 'none';
// reveal stored diagnostics in the UI (helpful when Console closes)
try {
const logArea = document.getElementById('logArea');
if (logArea) {
// append last 200 diagnostic lines to the log area so user can copy
const start = Math.max(0, diagBuffer.length - 200);
for (let i = start; i < diagBuffer.length; i++) {
const div = document.createElement('div');
div.textContent = diagBuffer[i];
logArea.appendChild(div);
}
logArea.scrollTop = logArea.scrollHeight;
}
} catch (e) { console.error('reveal diagnostics failed', e); }
} catch (e) {
console.error('Failed to render fatal error UI', e);
}
}
// expose shorthand for msalConfig loggerCallback to call
if (typeof window !== 'undefined') window.__addinLog = addLog;
function formatError(err) {
if (err instanceof Error) return err.message;
return JSON.stringify(err, null, 2);
}
/**
* Initialize MSAL on page load
* Sets up single sign-on so user only signs in once
*/
async function initMsal() {
try {
addLog('Initializing MSAL...');
msalInstance = new msal.PublicClientApplication(msalConfig);
// Check if user is already signed in (from localStorage)
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
addLog(`User already signed in: ${accounts[0].username}`);
// Try to acquire token silently
await acquireTokenSilently();
}
} catch (error) {
addLog('MSAL initialization error: ' + formatError(error));
console.error('MSAL initialization error:', error);
// Initialization failed — show a friendly fatal error to the user
showFatalError("Sorry — we can't load the add-in right now.", error && (error.message || error));
// Re-throw so callers are aware
throw error;
}
}
/**
* Acquire access token silently (with refresh if needed)
* MSAL handles token refresh automatically
*/
async function acquireTokenSilently() {
try {
const response = await msalInstance.acquireTokenSilent(tokenRequest);
userToken = response.accessToken;
addLog('Token acquired silently (auto-refreshed if needed)');
return userToken;
} catch (error) {
if (error instanceof msal.InteractionRequiredAuthError) {
addLog('Interactive sign-in required');
return acquireTokenInteractive();
} else {
addLog('Silent token acquisition failed: ' + formatError(error));
throw error;
}
}
}
/**
* Interactive sign-in (one-time, on first use)
*/
async function acquireTokenInteractive() {
try {
addLog('Prompting user to sign in...');
const response = await msalInstance.loginPopup(loginRequest);
userToken = response.accessToken;
addLog(`User signed in: ${response.account.username}`);
return userToken;
} catch (error) {
addLog('Interactive login failed: ' + formatError(error));
throw new Error(`Sign-in failed: ${error.message}`);
}
}
// Suppress Office.js debug handler prompt
if (window.Office && window.Office.onReady) {
window.Office.diagnostics = window.Office.diagnostics || {};
window.Office.diagnostics.isDebugModeEnabled = false;
}
Office.onReady(async () => {
try {
await initMsal();
const createBtn = document.getElementById('createBtn');
if (createBtn) createBtn.onclick = handleCreate;
} catch (initErr) {
// If initialization failed, show a friendly error — initMsal already calls showFatalError.
console.error('Office.onReady initialization error:', initErr);
showFatalError("Sorry — we can't load the add-in right now.", initErr && (initErr.message || initErr));
}
// wire diagnostics buttons even if init partially failed
try { setupDiagnosticsButtons(); } catch (e) {}
});
// Global handlers for uncaught errors / rejections to surface a friendly error message
window.addEventListener('error', (ev) => {
try {
const msg = ev && ev.message ? ev.message : String(ev || 'Unknown error');
pushDiagnosticsLine('[window.error] ' + msg + ' | ' + JSON.stringify(ev || {}));
showFatalError("Sorry — we can't load the add-in right now.", msg);
} catch (e) { console.error('global error handler failed', e); }
});
window.addEventListener('unhandledrejection', (ev) => {
try {
const reason = ev && ev.reason ? ev.reason : ev;
pushDiagnosticsLine('[unhandledrejection] ' + (reason && (reason.message || JSON.stringify(reason))));
showFatalError("Sorry — we can't load the add-in right now.", reason && (reason.message || JSON.stringify(reason)));
} catch (e) { console.error('unhandledrejection handler failed', e); }
});
// --- diagnostics exporter + UI wiring ---
function getDiagnosticsText() {
const header = [];
header.push('Add-in diagnostics');
header.push('Time: ' + new Date().toISOString());
header.push('Location: ' + (location && location.href));
header.push('UserAgent: ' + (navigator && navigator.userAgent));
try {
const savedAcct = msalInstance && typeof msalInstance.getAllAccounts === 'function' ? msalInstance.getAllAccounts() : null;
header.push('MSAL accounts: ' + (savedAcct ? JSON.stringify(savedAcct.map(a => ({ username: a.username, homeAccountId: a.homeAccountId }))) : 'none'));
} catch(e) {}
const body = diagBuffer.join('\n');
return header.join('\n') + '\n\n' + body;
}
function setupDiagnosticsButtons() {
try {
const copyBtn = document.getElementById('copyDiagnostics');
const downloadBtn = document.getElementById('downloadDiagnostics');
const diagSaved = document.getElementById('diagSaved');
if (copyBtn) {
copyBtn.onclick = async () => {
try {
const text = getDiagnosticsText();
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Copied to clipboard'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); }
} else {
const area = document.getElementById('logArea');
if (area) { area.focus(); }
if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Copy the logs manually'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); }
}
} catch(e) { console.error('copy diagnostics failed', e); }
};
}
if (downloadBtn) {
downloadBtn.onclick = () => {
try {
const text = getDiagnosticsText();
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const filename = `add-in-diagnostics-${new Date().toISOString().replace(/[:.]/g,'-')}.txt`;
a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Downloaded'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); }
} catch(e) { console.error('download diagnostics failed', e); }
};
}
} catch (e) { console.error('setupDiagnosticsButtons failed', e); }
}
async function handleCreate() {
const statusEl = document.getElementById('statusMessage');
try {
// Ensure user is authenticated and token is valid
if (!userToken) {
statusEl.textContent = 'Signing in...';
await acquireTokenSilently();
}
statusEl.textContent = 'Reading Job info from workbook...';
const { jobNumber, jobFullName } = await getJobInfoFromWorkbook();
statusEl.textContent = `Using Job: ${jobNumber} - ${jobFullName}`;
statusEl.textContent = 'Reading Managers Info sheet...';
const managersHtml = await readManagersInfoAsHtml();
// show preview
const preview = document.getElementById('preview');
preview.style.display = 'block';
preview.innerHTML = managersHtml;
statusEl.textContent = 'Generating PDF...';
const pdfBlob = await generatePdfFromHtml(preview);
statusEl.textContent = 'PDF generated.';
const timestamp = getTimestamp();
const pdfFileName = `${jobNumber} - ${jobFullName} - ${timestamp}.pdf`;
const createBtn = document.getElementById('createBtn');
const spinner = document.getElementById('spinner');
if (createBtn) {
createBtn.disabled = true;
createBtn.textContent = 'Working...';
}
if (spinner) spinner.style.display = 'inline-block';
statusEl.textContent = 'Sending PDF to backend for upload...';
// convert blob to base64
const arrayBuffer = await pdfBlob.arrayBuffer();
const uint8 = new Uint8Array(arrayBuffer);
let binary = '';
for (let i = 0; i < uint8.byteLength; i++) binary += String.fromCharCode(uint8[i]);
const base64 = btoa(binary);
// Backend endpoint - adjust if your backend uses HTTP fallback
const backendEndpoint = `${location.protocol}//localhost:3001/api/upload-pdf`;
console.log('Attempting to POST PDF to backend:', backendEndpoint);
console.log('Has user token?', !!userToken);
if (userToken) console.log('userToken (truncated):', userToken.substring(0, 60) + '...');
// Prepare payload
const payload = JSON.stringify({ jobNumber, jobFullName, fileName: pdfFileName, pdfBase64: base64 });
// Use XMLHttpRequest to show upload progress to the user
const xhr = new XMLHttpRequest();
xhr.open('POST', backendEndpoint, true);
xhr.setRequestHeader('Content-Type', 'application/json');
if (userToken) xhr.setRequestHeader('Authorization', `Bearer ${userToken}`);
xhr.upload.onprogress = function (ev) {
if (ev.lengthComputable) {
const pct = Math.round((ev.loaded / ev.total) * 100);
statusEl.textContent = `Uploading PDF... ${pct}%`;
} else {
statusEl.textContent = 'Uploading PDF...';
}
};
const respText = await new Promise((resolve, reject) => {
xhr.onerror = function (e) {
reject(new Error('Network error during upload'));
};
xhr.onload = function () {
resolve(xhr.responseText);
};
xhr.send(payload);
});
let result;
try {
result = JSON.parse(respText || '{}');
} catch (parseErr) {
console.error('Failed to parse JSON response from backend:', parseErr, 'raw:', respText);
throw new Error('Invalid JSON response from backend');
}
// If server returned an error structure, throw
if (!result || (!result.uploadedItem && !result.shareLink)) {
console.error('Unexpected backend response:', result);
throw new Error('Upload failed: ' + JSON.stringify(result));
}
const shareEl = document.getElementById('shareLink');
// Show detailed success info
if (result.shareLink) shareEl.innerHTML = `<a href="${result.shareLink}" target="_blank">${result.shareLink}</a>`;
if (result.uploadedItem) {
const info = result.uploadedItem;
const details = `Uploaded: ${info.name} (${info.size} bytes) — ${info.webUrl || ''}`;
statusEl.textContent = '✅ PDF uploaded successfully!';
// append a smaller detail line
const small = document.createElement('div');
small.style.fontSize = '90%';
small.style.marginTop = '6px';
small.textContent = details;
statusEl.appendChild(small);
} else {
statusEl.textContent = '✅ Upload complete.';
}
if (spinner) spinner.style.display = 'none';
if (createBtn) {
createBtn.disabled = false;
createBtn.textContent = '📁 Create folder & upload PDF';
}
} catch (err) {
console.error('Create error', err);
const statusEl = document.getElementById('statusMessage');
statusEl.textContent = 'Error: ' + formatError(err);
// restore UI state
const createBtn = document.getElementById('createBtn');
const spinner = document.getElementById('spinner');
if (spinner) spinner.style.display = 'none';
if (createBtn) {
createBtn.disabled = false;
createBtn.textContent = '📁 Create folder & upload PDF';
}
}
}
// Read job info from 'Job Sheet' as the first data row.
// Structure assumed similar to original script: header row starts at A3 with column names.
// If not found, fallback to 'Test' + timestamp.
async function getJobInfoFromWorkbook() {
try {
return Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItemOrNullObject('Job Sheet');
await context.sync();
if (sheet.isNullObject) {
// fallback
const ts = getTimestamp();
return { jobNumber: `Test`, jobFullName: `Test ${ts}` };
}
const headerStart = sheet.getRange("A3");
const headerRegion = headerStart.getSurroundingRegion();
headerRegion.load("values, rowCount, columnCount");
await context.sync();
const headers = headerRegion.values && headerRegion.values[0] ? headerRegion.values[0] : [];
if (!headers || headers.length === 0) {
const ts = getTimestamp();
return { jobNumber: `Test`, jobFullName: `Test ${ts}` };
}
// Get first data row below header
const dataRange = headerRegion.getOffsetRange(1, 0).getResizedRange(1, headerRegion.columnCount - 1);
dataRange.load("values");
await context.sync();
const row = (dataRange.values && dataRange.values[0]) ? dataRange.values[0] : [];
const normalize = h => h ? h.toString().trim().toLowerCase() : "";
const idxJobNumber = headers.findIndex(h => normalize(h) === "job_number" || normalize(h) === "job number");
const idxJobName = headers.findIndex(h => normalize(h) === "job_name" || normalize(h) === "job name" || normalize(h) === "job_name");
const jobNumber = (idxJobNumber >= 0 && row[idxJobNumber]) ? String(row[idxJobNumber]).trim() : null;
const jobFullName = (idxJobName >= 0 && row[idxJobName]) ? String(row[idxJobName]).trim() : null;
if (jobNumber && jobFullName) return { jobNumber, jobFullName };
// fallback
const ts = getTimestamp();
return {
jobNumber: jobNumber || `Test`,
jobFullName: jobFullName || `Test ${ts}`
};
});
} catch (e) {
const ts = getTimestamp();
return { jobNumber: 'Test', jobFullName: `Test ${ts}` };
}
}
function getTimestamp() {
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const mm = pad(now.getMonth() + 1);
const dd = pad(now.getDate());
const yyyy = now.getFullYear();
const hh = pad(now.getHours());
const min = pad(now.getMinutes());
return `${mm}-${dd}-${yyyy}_${hh}${min}`;
}
async function readManagersInfoAsHtml() {
return Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItemOrNullObject('Managers Info');
await context.sync();
if (sheet.isNullObject) throw new Error('Worksheet \"Managers Info\" not found.');
const range = sheet.getUsedRange();
range.load(['values', 'rowCount', 'columnCount']);
await context.sync();
const values = range.values;
if (!values || values.length === 0) throw new Error('No data in Managers Info sheet.');
// build HTML table
let html = '<table class="data-table">';
for (let r = 0; r < values.length; r++) {
html += '<tr>';
for (let c = 0; c < values[r].length; c++) {
const cell = values[r][c] !== null && values[r][c] !== undefined ? values[r][c] : '';
if (r === 0) html += `<th>${escapeHtml(cell)}</th>`;
else html += `<td>${escapeHtml(cell)}</td>`;
}
html += '</tr>';
}
html += '</table><style>table.data-table { border-collapse: collapse; width: 100%; } table.data-table th, table.data-table td { padding: 6px; border: 1px solid #ddd; }</style>';
return html;
});
}
function escapeHtml(s) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
return String(s).replace(/[&<>"']/g, (m) => map[m]);
}
async function generatePdfFromHtml(element) {
// Use html2canvas and jsPDF (imported as modules, not global)
const canvas = await html2canvas(element, { scale: 2 });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ unit: 'pt', format: 'a4' });
const pageWidth = pdf.internal.pageSize.getWidth();
const imgWidth = pageWidth - 40; // margins
const imgProps = canvas.width ? (canvas.height / canvas.width) : 1;
const imgHeight = imgWidth * imgProps;
pdf.addImage(imgData, 'PNG', 20, 20, imgWidth, imgHeight);
const blob = pdf.output('blob');
return blob;
}
@@ -1,63 +1,32 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
<!-- This file shows how to design a first-run page that provides a welcome screen to the user about the features of the add-in. -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" /> <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contoso Task Pane Add-in</title> <meta http-equiv="Permissions-Policy" content="geolocation=(), microphone=(), camera=()" />
<title>Create Folder Add-in</title>
<!-- Office JavaScript API --> <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script> <!-- html2canvas and jspdf are now bundled via webpack -->
<style>
<!-- For more information on Fluent UI, visit https://developer.microsoft.com/fluentui#/. --> body { font-family: Arial, Helvetica, sans-serif; padding: 12px; }
<link rel="stylesheet" href="https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-core/11.1.0/css/fabric.min.css"/> button { padding:8px 12px; margin-top:8px; }
#preview { border:1px solid #ddd; padding:8px; margin-top:8px; max-width:800px; overflow:auto; display:none; }
<!-- Template styles --> </style>
<link href="taskpane.css" rel="stylesheet" type="text/css" />
</head> </head>
<body>
<h2>CreateFolderOfficeAddIn (MVP)</h2>
<body class="ms-font-m ms-welcome ms-Fabric"> <p>Click the button to create a folder from the workbook data and upload the Managers Info PDF.</p>
<button id="createBtn">Create folder, generate PDF and upload</button>
<div class="container">
<h3>Create Project Folder</h3>
<input type="text" id="projectName" placeholder="Enter Job Number" />
<button id="createFolderBtn" class="action-btn">Create Folder & Document</button>
<p id="statusMessage"></p>
</div>
<header class="ms-welcome__header ms-bgColor-neutralLighter"> <p id="statusMessage"></p>
<img width="90" height="90" src="../../assets/logo-filled.png" alt="Contoso" title="Contoso" />
<h1 class="ms-font-su">Welcome</h1> <div>
</header> <strong>Share link:</strong>
<section id="sideload-msg" class="ms-welcome__main"> <div id="shareLink"></div>
<h2 class="ms-font-xl">Please <a target="_blank" href="https://learn.microsoft.com/office/dev/add-ins/testing/test-debug-office-add-ins#sideload-an-office-add-in-for-testing">sideload</a> your add-in to see app body.</h2> </div>
</section>
<main id="app-body" class="ms-welcome__main" style="display: none;"> <div id="preview"></div>
<h2 class="ms-font-xl"> Discover what Office Add-ins can do for you today! </h2>
<ul class="ms-List ms-welcome__features">
<li class="ms-ListItem">
<i class="ms-Icon ms-Icon--Ribbon ms-font-xl"></i>
<span class="ms-font-m">Achieve more with Office integration</span>
</li>
<li class="ms-ListItem">
<i class="ms-Icon ms-Icon--Unlock ms-font-xl"></i>
<span class="ms-font-m">Unlock features and functionality</span>
</li>
<li class="ms-ListItem">
<i class="ms-Icon ms-Icon--Design ms-font-xl"></i>
<span class="ms-font-m">Create and visualize like a pro</span>
</li>
</ul>
<p class="ms-font-l">Modify the source files, then click <b>Run</b>.</p>
<div role="button" id="run" class="ms-welcome__action ms-Button ms-Button--hero ms-font-xl">
<span class="ms-Button-label">Run</span>
</div>
<p><label id="item-subject"></label></p>
</main>
</body> </body>
</html>
</html>
@@ -1,252 +1,304 @@
/* /* global console, document, Excel, Word, Office */
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
*/
/* global console, document, Excel, Office */ function formatError(err) {
if (err instanceof Error) return err.message;
window.addEventListener('error', (evt) => { return JSON.stringify(err, null, 2);
console.error('WindowError:', { }
message: evt.message,
filename: evt.filename,
lineno: evt.lineno,
colno: evt.colno,
error: evt.error, // often the real Error object
});
});
window.addEventListener('unhandledrejection', (evt) => {
console.error('UnhandledRejection:', {
reason: evt.reason, // might be the original Error
});
});
Office.onReady((info) => { Office.onReady((info) => {
if (info.host === Office.HostType.Excel) { if (info.host === Office.HostType.Excel) {
document.getElementById("sideload-msg").style.display = "none"; const sideloadMsg = document.getElementById("sideload-msg");
document.getElementById("app-body").style.display = "flex"; const appBody = document.getElementById("app-body");
document.getElementById("run").onclick = run; const runBtn = document.getElementById("run");
OfficeRuntime.auth.getAccessToken().then(token => console.log(token)); const createBtn = document.getElementById("createFolderBtn");
document.getElementById("createFolderBtn").onclick = handleCreatePDF;
if (sideloadMsg) sideloadMsg.style.display = "none";
if (appBody) appBody.style.display = "flex";
if (runBtn) runBtn.onclick = run;
if (createBtn) createBtn.onclick = handleCreatePDF;
} }
}); });
// Get Graph token via backend OBO exchange
async function getGraphToken() {
return new Promise((resolve, reject) => {
Office.context.auth.getAccessTokenAsync({ allowSignInPrompt: true }, async (result) => {
if (result.status === "succeeded") {
const officeToken = result.value;
try {
const response = await fetch("http://localhost:3001/api/obo-exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ officeToken })
});
const { graphToken } = await response.json();
resolve(graphToken);
} catch (err) {
reject(err);
}
} else {
reject(new Error(result.error.message));
}
});
});
}
async function handleCreatePDF() { async function handleCreatePDF() {
const projectInput = document.getElementById("projectName").value.trim(); const projectInputEl = document.getElementById("projectNumber");
const statusEl = document.getElementById("statusMessage"); const statusEl = document.getElementById("statusMessage");
if (!projectInputEl || !statusEl) return;
if (!projectInput) { const projectInput = projectInputEl.value.trim();
statusEl.textContent = "Please enter a project number."; if (!projectInput) {
return; statusEl.textContent = "Please enter a project number.";
} return;
}
try {
statusEl.textContent = "Processing...";
// 1. Get Excel data
const jobData = await getJobData(projectInput);
if (!jobData) {
statusEl.textContent = "Job not found.";
return;
}
const formData = await getFormData();
// 2. Get access token for Graph
const accessToken = await getAccessToken();
// 3. Hardcoded SharePoint IDs
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
const parentItemId = "01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7";
// 4. Create folder structure
const mainFolderName = `[ ${jobData.Job_Number} ] - ${jobData.Job_Name}`;
const mainFolderId = await createSharePointFolder(accessToken, driveId, parentItemId, mainFolderName);
const subFolderId = await createSharePointFolder(accessToken, driveId, mainFolderId, "Manager Info");
// 5. Generate Word document and export as PDF
const pdfBlob = await generatePDF(jobData, formData);
// 6. Upload PDF to SharePoint
await uploadFileToSharePoint(accessToken, driveId, subFolderId, `${mainFolderName}.pdf`, pdfBlob);
statusEl.textContent = "✅ PDF created and uploaded successfully!";
} catch (error) {
console.error(error);
statusEl.textContent = "❌ Error: " + error.message;
}
}
// Get Job Data from "Job Sheet"
async function getJobData(jobNumber) {
return Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItem("Job Sheet");
const usedRange = sheet.getUsedRange();
usedRange.load("values");
await context.sync();
const headers = usedRange.values[0];
const rows = usedRange.values.slice(1);
const idxJobNumber = headers.indexOf("Job_Number");
const idxJobName = headers.indexOf("Job_Name");
const idxJobAddress = headers.indexOf("Job_Address");
const idxJobDivision = headers.indexOf("Job_Division");
const idxClientCustomer = headers.indexOf("Client_Customer");
const idxContactPerson = headers.indexOf("Contact_Person");
const idxEmail = headers.indexOf("Email");
const match = rows.find(r => r[idxJobNumber] == jobNumber);
if (!match) return null;
return {
Job_Number: match[idxJobNumber],
Job_Name: match[idxJobName],
Job_Address: match[idxJobAddress],
Job_Division: match[idxJobDivision],
Client_Customer: match[idxClientCustomer],
Contact_Person: match[idxContactPerson],
Email: match[idxEmail]
};
});
}
// Get FormDat range
async function getFormData() {
return Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItem("FormDat");
const range = sheet.getRange("A1:C12");
range.load("values");
await context.sync();
return range.values;
});
}
// Generate PDF using Word API
async function generatePDF(jobData, formData) {
return Word.run(async (context) => {
const body = context.document.body;
body.clear();
body.insertParagraph("Project Information", Word.InsertLocation.start);
body.insertParagraph(`Job Number: ${jobData.Job_Number}`, Word.InsertLocation.end);
body.insertParagraph(`Job Name: ${jobData.Job_Name}`, Word.InsertLocation.end);
body.insertParagraph(`Address: ${jobData.Job_Address}`, Word.InsertLocation.end);
body.insertParagraph(`Division: ${jobData.Job_Division}`, Word.InsertLocation.end);
body.insertParagraph(`Client: ${jobData.Client_Customer}`, Word.InsertLocation.end);
body.insertParagraph(`Contact: ${jobData.Contact_Person}`, Word.InsertLocation.end);
body.insertParagraph(`Email: ${jobData.Email}`, Word.InsertLocation.end);
body.insertParagraph("\nForm Data:", Word.InsertLocation.end);
formData.forEach(row => {
body.insertParagraph(row.join(" | "), Word.InsertLocation.end);
});
await context.sync();
// Export as PDF
return new Promise((resolve, reject) => {
Office.context.document.getFileAsync(Office.FileType.Pdf, (result) => {
if (result.status === Office.AsyncResultStatus.Succeeded) {
const file = result.value;
const sliceCount = file.sliceCount;
const slices = [];
let sliceIndex = 0;
const getSlice = () => {
file.getSliceAsync(sliceIndex, (sliceResult) => {
if (sliceResult.status === Office.AsyncResultStatus.Succeeded) {
slices.push(sliceResult.value.data);
sliceIndex++;
if (sliceIndex < sliceCount) {
getSlice();
} else {
file.closeAsync();
const blob = new Blob(slices, { type: "application/pdf" });
resolve(blob);
}
} else {
reject(sliceResult.error);
}
});
};
getSlice();
} else {
reject(result.error);
}
});
});
});
}
// Get Access Token for Graph
async function getAccessToken() {
return new Promise((resolve, reject) => {
Office.context.auth.getAccessTokenAsync({ allowSignInPrompt: true }, (result) => {
if (result.status === "succeeded") {
resolve(result.value);
} else {
reject(new Error(result.error.message));
}
});
});
}
// Create folder in SharePoint
async function createSharePointFolder(token, driveId, parentId, folderName) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ name: folderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" })
});
const data = await response.json();
if (!data.id) throw new Error(`Failed to create folder: ${JSON.stringify(data)}`);
return data.id;
}
// Upload PDF
async function uploadFileToSharePoint(token, driveId, folderId, fileName, pdfBlob) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${fileName}:/content`;
const response = await fetch(endpoint, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`
},
body: pdfBlob
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Upload failed: ${errorText}`);
}
}
export async function run() {
try { try {
await Excel.run(async (context) => { statusEl.textContent = "🔎 Looking up job data...";
/** const jobData = await getJobData(projectInput);
* Insert your Excel code here if (!jobData) {
*/ statusEl.textContent = "❌ Job not found.";
const range = context.workbook.getSelectedRange(); return;
}
statusEl.textContent = `✅ Found job: ${jobData.Job_Name}`;
// Read the range address statusEl.textContent = "📋 Collecting form data...";
range.load("address"); const formData = await getFormData();
// Update the fill color statusEl.textContent = "🔑 Getting Graph token...";
range.format.fill.color = "yellow"; const graphToken = await getGraphToken();
statusEl.textContent = "✅ Got Graph token";
await context.sync(); // ✅ Real IDs you provided earlier
console.log(`The range address was ${range.address}.`); const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
}); const parentItemId = "01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7";
statusEl.textContent = "📂 Creating main folder...";
const mainFolderName = `[ ${jobData.Job_Number} ] - ${jobData.Job_Name}`;
const mainFolderId = await createSharePointFolder(graphToken, driveId, parentItemId, mainFolderName);
console.log("Main folder created:", mainFolderId);
statusEl.textContent = "✅ Main folder created";
statusEl.textContent = "📂 Creating subfolder...";
const subFolderId = await createSharePointFolder(graphToken, driveId, mainFolderId, "Manager Info");
console.log("Subfolder created:", subFolderId);
statusEl.textContent = "✅ Subfolder created";
statusEl.textContent = "🔗 Generating share link...";
const shareLink = await createShareLink(graphToken, driveId, subFolderId);
const shareEl = document.getElementById("shareLink");
if (shareEl) {
shareEl.innerHTML = `<a href="${shareLink}" target="_blank">${shareLink}</a>`;
}
statusEl.textContent = "✅ Share link created";
statusEl.textContent = "📝 Generating PDF...";
const pdfBlob = await generatePDF(jobData, formData);
statusEl.textContent = "✅ PDF generated";
statusEl.textContent = "⬆️ Uploading PDF to SharePoint...";
await uploadFileToSharePoint(graphToken, driveId, subFolderId, `${mainFolderName}.pdf`, pdfBlob);
statusEl.textContent = "✅ PDF uploaded successfully!";
} catch (error) { } catch (error) {
console.error(error); console.error("handleCreatePDF error:", error);
statusEl.textContent = "❌ Error: " + formatError(error);
} }
} }
// Excel helpers
async function getJobData(jobNumber) {
return Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItem("Job Sheet");
const headerStart = sheet.getRange("A3");
const headerRegion = headerStart.getSurroundingRegion();
headerRegion.load("values, rowCount, columnCount");
await context.sync();
const headers = headerRegion.values[0];
if (!headers || headers.length === 0) {
throw new Error("No headers found at A3.");
}
// Get all rows below header
const dataRange = headerRegion
.getOffsetRange(1, 0)
.getResizedRange(headerRegion.rowCount - 2, 0);
dataRange.load("values");
await context.sync();
const rows = dataRange.values;
const normalize = h => h ? h.toString().trim().toLowerCase() : "";
const idxJobNumber = headers.findIndex(h => normalize(h) === "job_number");
const idxJobName = headers.findIndex(h => normalize(h) === "job_name");
const idxJobAddress = headers.findIndex(h => normalize(h) === "job_address");
const idxJobDivision = headers.findIndex(h => normalize(h) === "job_division");
const idxClientCustomer = headers.findIndex(h => normalize(h) === "client_customer");
const idxContactPerson = headers.findIndex(h => normalize(h) === "contact_person");
const idxEmail = headers.findIndex(h => normalize(h) === "email");
if (idxJobNumber === -1) throw new Error("No 'Job_Number' column found in header row A3.");
const match = rows.find(r => r[idxJobNumber]?.toString().trim() === jobNumber.toString().trim());
if (!match) return null;
return {
Job_Number: match[idxJobNumber],
Job_Name: idxJobName >= 0 ? match[idxJobName] : "",
Job_Address: idxJobAddress >= 0 ? match[idxJobAddress] : "",
Job_Division: idxJobDivision >= 0 ? match[idxJobDivision] : "",
Client_Customer: idxClientCustomer >= 0 ? match[idxClientCustomer] : "",
Contact_Person: idxContactPerson >= 0 ? match[idxContactPerson] : "",
Email: idxEmail >= 0 ? match[idxEmail] : ""
};
});
}
async function getFormData() {
return Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItem("FormDat");
const range = sheet.getRange("A1").getSurroundingRegion();
range.load("values");
await context.sync();
return range.values;
});
}
// Word helpers
async function generatePDF(jobData, formData) {
return Word.run(async (context) => {
const body = context.document.body;
body.clear();
body.insertParagraph("Project Information", Word.InsertLocation.start);
body.insertParagraph(`Job Number: ${jobData.Job_Number}`, Word.InsertLocation.end);
body.insertParagraph(`Job Name: ${jobData.Job_Name}`, Word.InsertLocation.end);
body.insertParagraph(`Address: ${jobData.Job_Address}`, Word.InsertLocation.end);
body.insertParagraph(`Division: ${jobData.Job_Division}`, Word.InsertLocation.end);
body.insertParagraph(`Client: ${jobData.Client_Customer}`, Word.InsertLocation.end);
body.insertParagraph(`Contact: ${jobData.Contact_Person}`, Word.InsertLocation.end);
body.insertParagraph(`Email: ${jobData.Email}`, Word.InsertLocation.end);
body.insertParagraph("\nForm Data:", Word.InsertLocation.end);
formData.forEach(row => {
body.insertParagraph(row.join(" | "), Word.InsertLocation.end);
});
await context.sync();
return new Promise((resolve, reject) => {
Office.context.document.getFileAsync(Office.FileType.Pdf, (result) => {
if (result.status === Office.AsyncResultStatus.Succeeded) {
const file = result.value;
const sliceCount = file.sliceCount;
const slices = [];
let sliceIndex = 0;
const getSlice = () => {
file.getSliceAsync(sliceIndex, (sliceResult) => {
if (sliceResult.status === Office.AsyncResultStatus.Succeeded) {
slices.push(sliceResult.value.data);
sliceIndex++;
if (sliceIndex < sliceCount) {
getSlice();
} else {
file.closeAsync();
const blob = new Blob(slices, { type: "application/pdf" });
resolve(blob);
}
} else {
reject(sliceResult.error);
}
});
};
getSlice();
} else {
reject(result.error);
}
});
});
});
}
// Graph helpers
async function createSharePointFolder(token, driveId, parentId, folderName) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: folderName,
folder: {},
"@microsoft.graph.conflictBehavior": "rename"
})
});
const data = await response.json();
console.log("Create folder response:", data);
if (!data.id) throw new Error(`Failed to create folder: ${JSON.stringify(data, null, 2)}`);
return data.id;
}
async function uploadFileToSharePoint(token, driveId, folderId, fileName, pdfBlob) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${fileName}:/content`;
const response = await fetch(endpoint, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`
},
body: pdfBlob
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Upload failed: ${errorText}`);
}
}
async function createShareLink(token, driveId, itemId) {
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/createLink`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
type: "view",
scope: "organization"
})
});
const data = await response.json();
if (!data || !data.link || !data.link.webUrl) {
throw new Error(`Failed to create share link: ${JSON.stringify(data, null, 2)}`);
}
return data.link.webUrl;
}
// Demo run function
async function run() {
try {
await Excel.run(async (context) => {
const range = context.workbook.getSelectedRange();
range.load("address");
range.format.fill.color = "yellow";
await context.sync();
console.log(`The range address was ${range.address}.`);
console.log("Headers:", headers);
console.log("First row:", rows[0]);
});
} catch (error) {
console.error("Run error:", error);
}
}
// Ensure run is available globally if needed
window.run = run;
@@ -19,6 +19,7 @@ module.exports = async (env, options) => {
entry: { entry: {
polyfill: ["core-js/stable", "regenerator-runtime/runtime"], polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"], taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"],
createfolder: ["./src/taskpane/createFolder/createFolder.js", "./src/taskpane/createFolder/createFolder.html"],
commands: "./src/commands/commands.js", commands: "./src/commands/commands.js",
}, },
output: { output: {
@@ -56,6 +57,11 @@ module.exports = async (env, options) => {
template: "./src/taskpane/taskpane.html", template: "./src/taskpane/taskpane.html",
chunks: ["polyfill", "taskpane"], chunks: ["polyfill", "taskpane"],
}), }),
new HtmlWebpackPlugin({
filename: "createFolder.html",
template: "./src/taskpane/createFolder/createFolder.html",
chunks: ["polyfill", "createfolder"],
}),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: [
{ {
+4 -3
View File
@@ -1,10 +1,11 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Excel Add-in Commands</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Office.js must be present so the commands surface initializes --> <!-- Office.js must be present so the commands surface initializes -->
https://appsforoffice.microsoft.com/lib/1/hosted/office.js</script> <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
</head> </head>
<body> <body>
<script> <script>
+26
View File
@@ -0,0 +1,26 @@
# Minimal Playground Add-in (sideload instructions)
This small add-in is a single-button taskpane that calls the local `minimal-uploader-playground` API to generate a Managers Info PDF and upload it to SharePoint.
Quick steps to run and sideload for testing:
1. Start the playground API (if not already running):
```powershell
# from repo root
npm run start:all
```
2. In Excel (Windows):
- Open Excel → File → Options → Add-ins → Manage: COM Add-ins / Office Add-ins (depending) and disable any other dev add-ins that might auto-launch the task pane.
- Follow Excel sideloading steps (Developer → Add-ins → Sideload/Upload) and pick `officeaddin/minimal-playground/manifest.xml`.
- Open the add-in from the ribbon (should appear under the new Commands group as "Managers UI").
3. Click the single button in the taskpane — it will POST to `http://localhost:3002/api/upload-managers` and stream status.
How to avoid conflicts during testing
- Use a dedicated Excel profile or temporarily disable other add-ins from the Office Add-ins / COM Add-ins UI.
- If another dev add-in is using the same port (3003), you can run the orchestrator which will detect healthy endpoints and not re-start them — or kill the conflicting process first.
Notes
- This is a minimal dev add-in and meant for local development only. Do not put secrets or production configuration into the local .env files when checked into source control.
+108
View File
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<OfficeApp
xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
xsi:type="TaskPaneApp">
<Id>9b5b1e9a-9fbd-4a3f-bcf4-7c1b2a7c5a97</Id>
<Version>1.0.0.0</Version>
<ProviderName>Local Dev</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Minimal Playground Add-in"/>
<Description DefaultValue="Very small add-in with one button that uploads Managers Info PDF to SharePoint"/>
<IconUrl DefaultValue="https://localhost:3003/assets/icon-32.png"/>
<HighResolutionIconUrl DefaultValue="https://localhost:3003/assets/icon-64.png"/>
<SupportUrl DefaultValue="https://localhost:3003/help.html"/>
<AppDomains>
<AppDomain>https://localhost:3003</AppDomain>
</AppDomains>
<Hosts>
<Host Name="Workbook"/>
</Hosts>
<DefaultSettings>
<SourceLocation DefaultValue="https://localhost:3003/taskpane.html"/>
</DefaultSettings>
<Permissions>ReadWriteDocument</Permissions>
<VersionOverrides
xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
xsi:type="VersionOverridesV1_0">
<Hosts>
<Host xsi:type="Workbook">
<DesktopFormFactor>
<GetStarted>
<Title resid="GetStarted.Title"/>
<Description resid="GetStarted.Description"/>
<LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
</GetStarted>
<FunctionFile resid="Commands.Url"/>
<ExtensionPoint xsi:type="PrimaryCommandSurface">
<OfficeTab id="TabHome">
<Group id="CommandsGroup">
<Label resid="CommandsGroup.Label"/>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Control xsi:type="Button" id="TaskpaneButton">
<Label resid="TaskpaneButton.Label"/>
<Supertip>
<Title resid="TaskpaneButton.Label"/>
<Description resid="TaskpaneButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<TaskpaneId>ButtonId1</TaskpaneId>
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
</DesktopFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3003/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3003/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3003/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3003/commands.html"/>
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3003/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<bt:String id="GetStarted.Title" DefaultValue="Get started with the Minimal Playground"/>
<bt:String id="CommandsGroup.Label" DefaultValue="Playground"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Managers UI"/>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="GetStarted.Description" DefaultValue="A tiny add-in used for testing the playground uploader"/>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Open the Managers Info uploader UI"/>
</bt:LongStrings>
</Resources>
</VersionOverrides>
</OfficeApp>
+927
View File
@@ -0,0 +1,927 @@
{
"name": "minimal-playground-addin",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "minimal-playground-addin",
"dependencies": {
"dotenv": "^17.2.3",
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
"integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/send/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-static/node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static/node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}
@@ -0,0 +1,11 @@
{
"name": "minimal-playground-addin",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"dotenv": "^17.2.3"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Commands</title>
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js" type="text/javascript"></script>
<script>
Office.onReady(function() { console.log('Commands page Office.onReady'); });
</script>
</head>
<body>
<h2>Commands placeholder</h2>
<p>This file is referenced by the manifest and is not used for the minimal sample.</p>
</body>
</html>
@@ -0,0 +1,8 @@
body{ font-family: Segoe UI, Arial, Helvetica, sans-serif; background:#fff; color:#111; }
.container{ padding:18px; width:380px }
header h1{ margin:6px 0 4px; font-size:18px }
.muted{ color:#666; font-size:12px; margin-bottom:12px }
.primary{ display:inline-block; background:#0078d4; color:#fff; padding:10px 14px; border-radius:6px; border:0; cursor:pointer; font-size:14px }
#status{ margin-top:12px; font-size:13px }
#status .ok{ color:green }
#status .err{ color:crimson }
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Minimal Playground Add-in</title>
<link rel="stylesheet" href="/taskpane.css" />
<!-- Office.js is REQUIRED for Office Add-ins -->
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js" type="text/javascript"></script>
</head>
<body>
<div class="container">
<header>
<h1>Minimal Playground Add-in</h1>
<p class="muted">One button → generates Managers Info PDF &amp; uploads to SharePoint</p>
</header>
<main>
<button id="uploadBtn" class="primary">Upload Managers Info</button>
<div id="status" aria-live="polite"></div>
</main>
<footer>
<small>Dev only — runs locally against the playground server</small>
</footer>
</div>
<script src="/taskpane.js"></script>
</body>
</html>
@@ -0,0 +1,30 @@
// Minimal add-in taskpane script — one button that calls the playground server
// Wait for Office to be ready before attaching event handlers
Office.onReady(function(info) {
// info.host will be 'Excel' when running in Excel
console.log('Office.onReady fired, host:', info.host);
const uploadBtn = document.getElementById('uploadBtn');
const status = document.getElementById('status');
function setStatus(text, ok) {
status.innerHTML = '<div class="' + (ok ? 'ok' : 'err') + '">' + text + '</div>';
}
uploadBtn.addEventListener('click', async function() {
uploadBtn.disabled = true;
setStatus('Starting upload...');
try {
const res = await fetch('http://localhost:3002/api/upload-managers', { method: 'POST' });
const json = await res.json();
if (!res.ok) throw new Error(json && json.error ? json.error : 'Unknown response');
var link = (json.result && json.result.share && json.result.share.link) ? json.result.share.link.webUrl : 'unknown';
setStatus('Upload finished. Share link: ' + link, true);
} catch (err) {
console.error(err);
setStatus('Upload failed: ' + (err.message || err), false);
} finally {
uploadBtn.disabled = false;
}
});
});
+45
View File
@@ -0,0 +1,45 @@
// Tiny static server for the minimal playground add-in (HTTPS for Office add-in compatibility)
require('dotenv').config();
const express = require('express');
const https = require('https');
const fs = require('fs');
const path = require('path');
const app = express();
const port = process.env.ADDIN_PORT || 3003;
app.use(express.static(path.join(__dirname, 'public')));
app.get('/help.html', (req,res)=> res.send('<h2>Minimal Playground Add-in</h2><p>Sideload manifest to Excel, then open the Add-ins pane and click "Managers UI".</p>'));
// Use office-addin-dev-certs (installed via npx office-addin-dev-certs install)
const userHome = process.env.USERPROFILE || process.env.HOME;
const certDir = path.join(userHome, '.office-addin-dev-certs');
const certPath = path.join(certDir, 'localhost.crt');
const keyPath = path.join(certDir, 'localhost.key');
let server;
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
const httpsOptions = {
cert: fs.readFileSync(certPath),
key: fs.readFileSync(keyPath)
};
server = https.createServer(httpsOptions, app);
server.listen(port, '0.0.0.0', () => {
console.log(`minimal-playground add-in server (HTTPS) listening on https://localhost:${port}`);
});
} else {
console.log('No office-addin-dev-certs found. Run: npx office-addin-dev-certs install');
server = app.listen(port, '0.0.0.0', () => {
console.log(`minimal-playground add-in server (HTTP) listening on http://localhost:${port}`);
});
}
server.on('error', (err) => {
console.error('Server error:', err);
});
// Only handle explicit shutdown signals
process.on('SIGINT', () => { console.log('Received SIGINT, shutting down...'); server.close(); process.exit(0); });
process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down...'); server.close(); process.exit(0); });
// Prevent the process from exiting on its own
process.stdin.resume();
+60
View File
@@ -0,0 +1,60 @@
{
"name": "job-info-dev-orchestrator",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "job-info-dev-orchestrator",
"dependencies": {
"job-info-dev-orchestrator": "file:",
"node-fetch": "^2.6.7"
}
},
"node_modules/job-info-dev-orchestrator": {
"resolved": "",
"link": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "job-info-dev-orchestrator",
"private": true,
"scripts": {
"start:all": "node ./start-all.js"
},
"dependencies": {
"job-info-dev-orchestrator": "file:",
"node-fetch": "^2.6.7"
}
}
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env node
/* start-all.js orchestrator for local dev
- Ensures the playground server (3002) and the add-in static server (3003) are running
- If healthy endpoints are reachable, does not start duplicates
- Streams logs from started processes and ensures clean shutdown
*/
const { spawn } = require('child_process');
const fetch = require('node-fetch');
const path = require('path');
const services = [
{
name: 'playground',
dir: path.join(__dirname, 'minimal-uploader-playground'),
startCmd: ['npm', ['run','server']],
healthUrl: 'http://localhost:3002/api/health'
},
{
name: 'addin',
dir: path.join(__dirname, 'officeaddin', 'minimal-playground'),
startCmd: ['npm', ['start']],
// static taskpane page used as a health endpoint
healthUrl: 'http://localhost:3003/taskpane.html'
}
];
async function checkHealth(url, timeout = 2000) {
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const res = await fetch(url, { signal: controller.signal });
clearTimeout(id);
return res.ok;
} catch (e) {
return false;
}
}
function spawnService(svc) {
const [cmd, args] = svc.startCmd;
const proc = spawn(cmd, args, { cwd: svc.dir, shell: true, env: process.env });
proc.stdout.on('data', (d) => process.stdout.write(`[${svc.name}] ${d}`));
proc.stderr.on('data', (d) => process.stderr.write(`[${svc.name}] ${d}`));
proc.on('exit', (code, sig) => console.log(`[${svc.name}] exited code=${code} signal=${sig}`));
return proc;
}
async function startAll() {
console.log('Orchestrator: checking service health...');
const started = [];
for (const svc of services) {
const healthy = await checkHealth(svc.healthUrl, 1500);
if (healthy) {
console.log(`${svc.name} is already healthy at ${svc.healthUrl}`);
started.push({ svc, proc: null, already: true });
continue;
}
console.log(`${svc.name} not healthy — starting: ${svc.startCmd.join(' ')}`);
const proc = spawnService(svc);
started.push({ svc, proc, already: false });
// wait for the service to report healthy
const maxWait = 30 * 1000; // 30s
const start = Date.now();
let ok = false;
while (Date.now() - start < maxWait) {
// wait a moment
await new Promise((r) => setTimeout(r, 500));
if (await checkHealth(svc.healthUrl, 1500)) { ok = true; break; }
}
if (!ok) {
console.error(`
Service ${svc.name} did not become healthy at ${svc.healthUrl} within ${maxWait/1000}s. Check its logs above.`);
// Continue retrying other services — do not abort so we can still start them
} else {
console.log(`${svc.name} is healthy at ${svc.healthUrl}`);
}
}
console.log('All start requests processed. Press Ctrl-C to stop (or send SIGTERM).');
// graceful cleanup
process.on('SIGINT', () => shutdown(started));
process.on('SIGTERM', () => shutdown(started));
}
function shutdown(started) {
console.log('\nStopping orchestrator and children...');
for (const { svc, proc, already } of started) {
if (proc && !proc.killed) {
try { proc.kill('SIGINT'); } catch (e) { /* ignore */ }
console.log(`Requested stop for ${svc.name}`);
} else if (already) {
console.log(`Note: ${svc.name} was already running and will not be killed by the orchestrator.`);
}
}
process.exit(0);
}
startAll().catch((err) => { console.error('Orchestrator failed:', err); process.exit(1); });