diff --git a/backend/command-api/scripts/seed-v2-demo-data.mjs b/backend/command-api/scripts/seed-v2-demo-data.mjs index c14ce89b..94a41e0d 100644 --- a/backend/command-api/scripts/seed-v2-demo-data.mjs +++ b/backend/command-api/scripts/seed-v2-demo-data.mjs @@ -49,6 +49,7 @@ async function main() { await upsertUser(client, fixture.users.businessOwner); await upsertUser(client, fixture.users.operationsManager); await upsertUser(client, fixture.users.vendorManager); + await upsertUser(client, fixture.users.staffAna); await client.query( ` @@ -64,13 +65,15 @@ async function main() { VALUES ($1, $2, 'ACTIVE', 'admin', '{"persona":"business_owner"}'::jsonb), ($1, $3, 'ACTIVE', 'manager', '{"persona":"ops_manager"}'::jsonb), - ($1, $4, 'ACTIVE', 'manager', '{"persona":"vendor_manager"}'::jsonb) + ($1, $4, 'ACTIVE', 'manager', '{"persona":"vendor_manager"}'::jsonb), + ($1, $5, 'ACTIVE', 'member', '{"persona":"staff"}'::jsonb) `, [ fixture.tenant.id, fixture.users.businessOwner.id, fixture.users.operationsManager.id, fixture.users.vendorManager.id, + fixture.users.staffAna.id, ] ); @@ -134,6 +137,14 @@ async function main() { [fixture.tenant.id, fixture.vendor.id, fixture.users.vendorManager.id] ); + await client.query( + ` + INSERT INTO cost_centers (id, tenant_id, business_id, code, name, status, metadata) + VALUES ($1, $2, $3, 'CAFE_OPS', $4, 'ACTIVE', '{"seeded":true}'::jsonb) + `, + [fixture.costCenters.cafeOps.id, fixture.tenant.id, fixture.business.id, fixture.costCenters.cafeOps.name] + ); + await client.query( ` INSERT INTO roles_catalog (id, tenant_id, code, name, status, metadata) @@ -158,16 +169,37 @@ async function main() { id, tenant_id, user_id, full_name, email, phone, status, primary_role, onboarding_status, average_rating, rating_count, metadata ) - VALUES ($1, $2, NULL, $3, $4, $5, 'ACTIVE', $6, 'COMPLETED', 4.50, 1, $7::jsonb) + VALUES ($1, $2, $3, $4, $5, $6, 'ACTIVE', $7, 'COMPLETED', 4.50, 1, $8::jsonb) `, [ fixture.staff.ana.id, fixture.tenant.id, + fixture.users.staffAna.id, fixture.staff.ana.fullName, fixture.staff.ana.email, fixture.staff.ana.phone, fixture.staff.ana.primaryRole, - JSON.stringify({ favoriteCandidate: true, seeded: true }), + JSON.stringify({ + favoriteCandidate: true, + seeded: true, + firstName: 'Ana', + lastName: 'Barista', + bio: 'Experienced barista and event staffing professional.', + preferredLocations: [ + { + city: 'Mountain View', + latitude: fixture.clockPoint.latitude, + longitude: fixture.clockPoint.longitude, + }, + ], + maxDistanceMiles: 20, + industries: ['CATERING', 'CAFE'], + skills: ['BARISTA', 'CUSTOMER_SERVICE'], + emergencyContact: { + name: 'Maria Barista', + phone: '+15550007777', + }, + }), ] ); @@ -196,21 +228,63 @@ async function main() { await client.query( ` - INSERT INTO clock_points ( - id, tenant_id, business_id, label, address, latitude, longitude, geofence_radius_meters, nfc_tag_uid, status, metadata + INSERT INTO staff_availability ( + id, tenant_id, staff_id, day_of_week, availability_status, time_slots, metadata ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'ACTIVE', '{}'::jsonb) + VALUES + ($1, $3, $4, 1, 'PARTIAL', '[{"start":"08:00","end":"18:00"}]'::jsonb, '{"seeded":true}'::jsonb), + ($2, $3, $4, 5, 'PARTIAL', '[{"start":"09:00","end":"17:00"}]'::jsonb, '{"seeded":true}'::jsonb) + `, + [fixture.availability.monday.id, fixture.availability.friday.id, fixture.tenant.id, fixture.staff.ana.id] + ); + + await client.query( + ` + INSERT INTO staff_benefits ( + id, tenant_id, staff_id, benefit_type, title, status, tracked_hours, target_hours, metadata + ) + VALUES ($1, $2, $3, 'COMMUTER', $4, 'ACTIVE', 32, 40, '{"description":"Commuter stipend unlocked after 40 hours"}'::jsonb) + `, + [fixture.benefits.commuter.id, fixture.tenant.id, fixture.staff.ana.id, fixture.benefits.commuter.title] + ); + + await client.query( + ` + INSERT INTO clock_points ( + id, tenant_id, business_id, cost_center_id, label, address, latitude, longitude, + geofence_radius_meters, nfc_tag_uid, status, metadata + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'ACTIVE', $11::jsonb) `, [ fixture.clockPoint.id, fixture.tenant.id, fixture.business.id, + fixture.costCenters.cafeOps.id, fixture.clockPoint.label, fixture.clockPoint.address, fixture.clockPoint.latitude, fixture.clockPoint.longitude, fixture.clockPoint.geofenceRadiusMeters, fixture.clockPoint.nfcTagUid, + JSON.stringify({ city: 'Mountain View', state: 'CA', zipCode: '94043', seeded: true }), + ] + ); + + await client.query( + ` + INSERT INTO hub_managers (id, tenant_id, hub_id, business_membership_id) + SELECT $1, $2, $3, bm.id + FROM business_memberships bm + WHERE bm.business_id = $4 + AND bm.user_id = $5 + `, + [ + fixture.hubManagers.opsLead.id, + fixture.tenant.id, + fixture.clockPoint.id, + fixture.business.id, + fixture.users.operationsManager.id, ] ); @@ -221,8 +295,8 @@ async function main() { starts_at, ends_at, location_name, location_address, latitude, longitude, notes, created_by_user_id, metadata ) VALUES - ($1, $3, $4, $5, $6, $7, 'Open order for live v2 commands', 'OPEN', 'EVENT', $8, $9, 'Google Cafe', $10, $11, $12, 'Use this order for live smoke and frontend reads', $13, '{"slice":"open"}'::jsonb), - ($2, $3, $4, $5, $14, $15, 'Completed order for favorites, reviews, invoices, and attendance history', 'COMPLETED', 'CATERING', $16, $17, 'Google Catering', $10, $11, $12, 'Completed historical example', $13, '{"slice":"completed"}'::jsonb) + ($1, $3, $4, $5, $6, $7, 'Open order for live v2 commands', 'OPEN', 'EVENT', $8, $9, 'Google Cafe', $10, $11, $12, 'Use this order for live smoke and frontend reads', $13, '{"slice":"open","orderType":"ONE_TIME"}'::jsonb), + ($2, $3, $4, $5, $14, $15, 'Completed order for favorites, reviews, invoices, and attendance history', 'COMPLETED', 'CATERING', $16, $17, 'Google Catering', $10, $11, $12, 'Completed historical example', $13, '{"slice":"completed","orderType":"ONE_TIME"}'::jsonb) `, [ fixture.orders.open.id, @@ -411,15 +485,30 @@ async function main() { await client.query( ` INSERT INTO documents (id, tenant_id, document_type, name, required_for_role_code, metadata) - VALUES ($1, $2, 'CERTIFICATION', $3, $4, '{"seeded":true}'::jsonb) + VALUES + ($1, $2, 'CERTIFICATION', $3, $6, '{"seeded":true}'::jsonb), + ($4, $2, 'ATTIRE', $5, $6, '{"seeded":true}'::jsonb), + ($7, $2, 'TAX_FORM', $8, $6, '{"seeded":true}'::jsonb) `, - [fixture.documents.foodSafety.id, fixture.tenant.id, fixture.documents.foodSafety.name, fixture.roles.barista.code] + [ + fixture.documents.foodSafety.id, + fixture.tenant.id, + fixture.documents.foodSafety.name, + fixture.documents.attireBlackShirt.id, + fixture.documents.attireBlackShirt.name, + fixture.roles.barista.code, + fixture.documents.taxFormW9.id, + fixture.documents.taxFormW9.name, + ] ); await client.query( ` INSERT INTO staff_documents (id, tenant_id, staff_id, document_id, file_uri, status, expires_at, metadata) - VALUES ($1, $2, $3, $4, $5, 'VERIFIED', $6, '{"seeded":true}'::jsonb) + VALUES + ($1, $2, $3, $4, $5, 'VERIFIED', $6, '{"seeded":true}'::jsonb), + ($7, $2, $3, $8, $9, 'VERIFIED', NULL, '{"seeded":true}'::jsonb), + ($10, $2, $3, $11, $12, 'VERIFIED', NULL, '{"seeded":true}'::jsonb) `, [ fixture.staffDocuments.foodSafety.id, @@ -428,6 +517,12 @@ async function main() { fixture.documents.foodSafety.id, `gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/food-handler-card.pdf`, hoursFromNow(24 * 180), + fixture.staffDocuments.attireBlackShirt.id, + fixture.documents.attireBlackShirt.id, + `gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/black-shirt.jpg`, + fixture.staffDocuments.taxFormW9.id, + fixture.documents.taxFormW9.id, + `gs://krow-workforce-dev-v2-private/uploads/${fixture.staff.ana.id}/w9-form.pdf`, ] ); @@ -472,8 +567,8 @@ async function main() { provider_name, provider_reference, last4, is_primary, metadata ) VALUES - ($1, $3, 'BUSINESS', $4, NULL, NULL, 'stripe', 'ba_business_demo', '6789', TRUE, '{"seeded":true}'::jsonb), - ($2, $3, 'STAFF', NULL, NULL, $5, 'stripe', 'ba_staff_demo', '4321', TRUE, '{"seeded":true}'::jsonb) + ($1, $3, 'BUSINESS', $4, NULL, NULL, 'stripe', 'ba_business_demo', '6789', TRUE, '{"seeded":true,"accountType":"CHECKING","routingNumberMasked":"*****0001"}'::jsonb), + ($2, $3, 'STAFF', NULL, NULL, $5, 'stripe', 'ba_staff_demo', '4321', TRUE, '{"seeded":true,"accountType":"CHECKING","routingNumberMasked":"*****0002"}'::jsonb) `, [ fixture.accounts.businessPrimary.id, @@ -490,7 +585,7 @@ async function main() { id, tenant_id, order_id, business_id, vendor_id, invoice_number, status, currency_code, subtotal_cents, tax_cents, total_cents, due_at, metadata ) - VALUES ($1, $2, $3, $4, $5, $6, 'PENDING_REVIEW', 'USD', 15250, 700, 15950, $7, '{"seeded":true}'::jsonb) + VALUES ($1, $2, $3, $4, $5, $6, 'PENDING_REVIEW', 'USD', 15250, 700, 15950, $7, '{"seeded":true,"savingsCents":1250}'::jsonb) `, [ fixture.invoices.completed.id, @@ -573,6 +668,7 @@ async function main() { businessId: fixture.business.id, vendorId: fixture.vendor.id, staffId: fixture.staff.ana.id, + staffUserId: fixture.users.staffAna.id, workforceId: fixture.workforce.ana.id, openOrderId: fixture.orders.open.id, openShiftId: fixture.shifts.open.id, diff --git a/backend/command-api/scripts/v2-demo-fixture.mjs b/backend/command-api/scripts/v2-demo-fixture.mjs index 04cea3e0..369ede17 100644 --- a/backend/command-api/scripts/v2-demo-fixture.mjs +++ b/backend/command-api/scripts/v2-demo-fixture.mjs @@ -20,6 +20,11 @@ export const V2DemoFixture = { email: 'vendor+v2@krowd.com', displayName: 'Vendor Manager', }, + staffAna: { + id: process.env.V2_DEMO_STAFF_UID || 'demo-staff-ana', + email: process.env.V2_DEMO_STAFF_EMAIL || 'ana.barista+v2@krowd.com', + displayName: 'Ana Barista', + }, }, business: { id: '14f4fcfb-f21f-4ba9-9328-90f794a56001', @@ -31,6 +36,12 @@ export const V2DemoFixture = { slug: 'legendary-pool-a', name: 'Legendary Staffing Pool A', }, + costCenters: { + cafeOps: { + id: '31db54dd-9b32-4504-9056-9c71a9f73001', + name: 'Cafe Operations', + }, + }, roles: { barista: { id: '67c5010e-85f0-4f6b-99b7-167c9afdf001', @@ -67,6 +78,25 @@ export const V2DemoFixture = { geofenceRadiusMeters: 120, nfcTagUid: 'NFC-DEMO-ANA-001', }, + hubManagers: { + opsLead: { + id: '3f2dfd17-e6b4-4fe4-9fea-3c91c7ca8001', + }, + }, + availability: { + monday: { + id: '887bc357-c3e0-4b2c-a174-bf27d6902001', + }, + friday: { + id: '887bc357-c3e0-4b2c-a174-bf27d6902002', + }, + }, + benefits: { + commuter: { + id: 'dbd28438-66b0-452f-a5fc-dd0f3ea61001', + title: 'Commuter Support', + }, + }, orders: { open: { id: 'b6132d7a-45c3-4879-b349-46b2fd518001', @@ -140,11 +170,25 @@ export const V2DemoFixture = { id: 'e6fd0183-34d9-4c23-9a9a-bf98da995001', name: 'Food Handler Card', }, + attireBlackShirt: { + id: 'e6fd0183-34d9-4c23-9a9a-bf98da995002', + name: 'Black Shirt', + }, + taxFormW9: { + id: 'e6fd0183-34d9-4c23-9a9a-bf98da995003', + name: 'W-9 Tax Form', + }, }, staffDocuments: { foodSafety: { id: '4b157236-a4b0-4c44-b199-7d4ea1f95001', }, + attireBlackShirt: { + id: '4b157236-a4b0-4c44-b199-7d4ea1f95002', + }, + taxFormW9: { + id: '4b157236-a4b0-4c44-b199-7d4ea1f95003', + }, }, certificates: { foodSafety: { diff --git a/backend/command-api/sql/v2/002_v2_mobile_support.sql b/backend/command-api/sql/v2/002_v2_mobile_support.sql new file mode 100644 index 00000000..1243a28f --- /dev/null +++ b/backend/command-api/sql/v2/002_v2_mobile_support.sql @@ -0,0 +1,64 @@ +CREATE TABLE IF NOT EXISTS cost_centers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, + code TEXT, + name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'ACTIVE' + CHECK (status IN ('ACTIVE', 'INACTIVE', 'ARCHIVED')), + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_cost_centers_business_name + ON cost_centers (business_id, name); + +ALTER TABLE clock_points + ADD COLUMN IF NOT EXISTS cost_center_id UUID REFERENCES cost_centers(id) ON DELETE SET NULL; + +CREATE TABLE IF NOT EXISTS hub_managers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + hub_id UUID NOT NULL REFERENCES clock_points(id) ON DELETE CASCADE, + business_membership_id UUID NOT NULL REFERENCES business_memberships(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_hub_managers_hub_membership + ON hub_managers (hub_id, business_membership_id); + +CREATE TABLE IF NOT EXISTS staff_availability ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, + day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), + availability_status TEXT NOT NULL DEFAULT 'UNAVAILABLE' + CHECK (availability_status IN ('AVAILABLE', 'UNAVAILABLE', 'PARTIAL')), + time_slots JSONB NOT NULL DEFAULT '[]'::jsonb, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_availability_staff_day + ON staff_availability (staff_id, day_of_week); + +CREATE TABLE IF NOT EXISTS staff_benefits ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, + benefit_type TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'ACTIVE' + CHECK (status IN ('ACTIVE', 'INACTIVE', 'PENDING')), + tracked_hours INTEGER NOT NULL DEFAULT 0 CHECK (tracked_hours >= 0), + target_hours INTEGER NOT NULL DEFAULT 0 CHECK (target_hours >= 0), + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_benefits_staff_type + ON staff_benefits (staff_id, benefit_type); diff --git a/backend/query-api/src/app.js b/backend/query-api/src/app.js index 4e883cb3..43ff81da 100644 --- a/backend/query-api/src/app.js +++ b/backend/query-api/src/app.js @@ -5,6 +5,7 @@ import { requestContext } from './middleware/request-context.js'; import { errorHandler, notFoundHandler } from './middleware/error-handler.js'; import { healthRouter } from './routes/health.js'; import { createQueryRouter } from './routes/query.js'; +import { createMobileQueryRouter } from './routes/mobile.js'; const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); @@ -22,6 +23,7 @@ export function createApp(options = {}) { app.use(healthRouter); app.use('/query', createQueryRouter(options.queryService)); + app.use('/query', createMobileQueryRouter(options.mobileQueryService)); app.use(notFoundHandler); app.use(errorHandler); diff --git a/backend/query-api/src/routes/mobile.js b/backend/query-api/src/routes/mobile.js new file mode 100644 index 00000000..07674ea7 --- /dev/null +++ b/backend/query-api/src/routes/mobile.js @@ -0,0 +1,464 @@ +import { Router } from 'express'; +import { requireAuth, requirePolicy } from '../middleware/auth.js'; +import { + getClientDashboard, + getClientSession, + getCoverageStats, + getCurrentAttendanceStatus, + getCurrentBill, + getPaymentChart, + getPaymentsSummary, + getPersonalInfo, + getProfileSectionsStatus, + getSavings, + getStaffDashboard, + getStaffProfileCompletion, + getStaffSession, + getStaffShiftDetail, + listAssignedShifts, + listBusinessAccounts, + listCancelledShifts, + listCertificates, + listCostCenters, + listCoverageByDate, + listCompletedShifts, + listHubManagers, + listHubs, + listIndustries, + listInvoiceHistory, + listOpenShifts, + listOrderItemsByDateRange, + listPaymentsHistory, + listPendingAssignments, + listPendingInvoices, + listProfileDocuments, + listRecentReorders, + listSkills, + listStaffAvailability, + listStaffBankAccounts, + listStaffBenefits, + listTodayShifts, + listVendorRoles, + listVendors, + getSpendBreakdown, +} from '../services/mobile-query-service.js'; + +const defaultQueryService = { + getClientDashboard, + getClientSession, + getCoverageStats, + getCurrentAttendanceStatus, + getCurrentBill, + getPaymentChart, + getPaymentsSummary, + getPersonalInfo, + getProfileSectionsStatus, + getSavings, + getSpendBreakdown, + getStaffDashboard, + getStaffProfileCompletion, + getStaffSession, + getStaffShiftDetail, + listAssignedShifts, + listBusinessAccounts, + listCancelledShifts, + listCertificates, + listCostCenters, + listCoverageByDate, + listCompletedShifts, + listHubManagers, + listHubs, + listIndustries, + listInvoiceHistory, + listOpenShifts, + listOrderItemsByDateRange, + listPaymentsHistory, + listPendingAssignments, + listPendingInvoices, + listProfileDocuments, + listRecentReorders, + listSkills, + listStaffAvailability, + listStaffBankAccounts, + listStaffBenefits, + listTodayShifts, + listVendorRoles, + listVendors, +}; + +function requireQueryParam(name, value) { + if (!value) { + const error = new Error(`${name} is required`); + error.code = 'VALIDATION_ERROR'; + error.status = 400; + error.details = { field: name }; + throw error; + } + return value; +} + +export function createMobileQueryRouter(queryService = defaultQueryService) { + const router = Router(); + + router.get('/client/session', requireAuth, requirePolicy('client.session.read', 'session'), async (req, res, next) => { + try { + const data = await queryService.getClientSession(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/dashboard', requireAuth, requirePolicy('client.dashboard.read', 'dashboard'), async (req, res, next) => { + try { + const data = await queryService.getClientDashboard(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/reorders', requireAuth, requirePolicy('orders.reorder.read', 'order'), async (req, res, next) => { + try { + const items = await queryService.listRecentReorders(req.actor.uid, req.query.limit); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/billing/accounts', requireAuth, requirePolicy('billing.accounts.read', 'billing'), async (req, res, next) => { + try { + const items = await queryService.listBusinessAccounts(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/billing/invoices/pending', requireAuth, requirePolicy('billing.invoices.read', 'billing'), async (req, res, next) => { + try { + const items = await queryService.listPendingInvoices(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/billing/invoices/history', requireAuth, requirePolicy('billing.invoices.read', 'billing'), async (req, res, next) => { + try { + const items = await queryService.listInvoiceHistory(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/billing/current-bill', requireAuth, requirePolicy('billing.summary.read', 'billing'), async (req, res, next) => { + try { + const data = await queryService.getCurrentBill(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/billing/savings', requireAuth, requirePolicy('billing.summary.read', 'billing'), async (req, res, next) => { + try { + const data = await queryService.getSavings(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/billing/spend-breakdown', requireAuth, requirePolicy('billing.summary.read', 'billing'), async (req, res, next) => { + try { + const items = await queryService.getSpendBreakdown(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/coverage', requireAuth, requirePolicy('coverage.read', 'coverage'), async (req, res, next) => { + try { + const items = await queryService.listCoverageByDate(req.actor.uid, { date: requireQueryParam('date', req.query.date) }); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/coverage/stats', requireAuth, requirePolicy('coverage.read', 'coverage'), async (req, res, next) => { + try { + const data = await queryService.getCoverageStats(req.actor.uid, { date: requireQueryParam('date', req.query.date) }); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/hubs', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => { + try { + const items = await queryService.listHubs(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/cost-centers', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => { + try { + const items = await queryService.listCostCenters(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/vendors', requireAuth, requirePolicy('vendors.read', 'vendor'), async (req, res, next) => { + try { + const items = await queryService.listVendors(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/vendors/:vendorId/roles', requireAuth, requirePolicy('vendors.read', 'vendor'), async (req, res, next) => { + try { + const items = await queryService.listVendorRoles(req.actor.uid, req.params.vendorId); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/hubs/:hubId/managers', requireAuth, requirePolicy('hubs.read', 'hub'), async (req, res, next) => { + try { + const items = await queryService.listHubManagers(req.actor.uid, req.params.hubId); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/client/orders/view', requireAuth, requirePolicy('orders.read', 'order'), async (req, res, next) => { + try { + const items = await queryService.listOrderItemsByDateRange(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/session', requireAuth, requirePolicy('staff.session.read', 'session'), async (req, res, next) => { + try { + const data = await queryService.getStaffSession(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/dashboard', requireAuth, requirePolicy('staff.dashboard.read', 'dashboard'), async (req, res, next) => { + try { + const data = await queryService.getStaffDashboard(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile-completion', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const data = await queryService.getStaffProfileCompletion(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/availability', requireAuth, requirePolicy('staff.availability.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listStaffAvailability(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/clock-in/shifts/today', requireAuth, requirePolicy('attendance.read', 'attendance'), async (req, res, next) => { + try { + const items = await queryService.listTodayShifts(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/clock-in/status', requireAuth, requirePolicy('attendance.read', 'attendance'), async (req, res, next) => { + try { + const data = await queryService.getCurrentAttendanceStatus(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/payments/summary', requireAuth, requirePolicy('payments.read', 'payment'), async (req, res, next) => { + try { + const data = await queryService.getPaymentsSummary(req.actor.uid, req.query); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/payments/history', requireAuth, requirePolicy('payments.read', 'payment'), async (req, res, next) => { + try { + const items = await queryService.listPaymentsHistory(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/payments/chart', requireAuth, requirePolicy('payments.read', 'payment'), async (req, res, next) => { + try { + const items = await queryService.getPaymentChart(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/shifts/assigned', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => { + try { + const items = await queryService.listAssignedShifts(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/shifts/open', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => { + try { + const items = await queryService.listOpenShifts(req.actor.uid, req.query); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/shifts/pending', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => { + try { + const items = await queryService.listPendingAssignments(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/shifts/cancelled', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => { + try { + const items = await queryService.listCancelledShifts(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/shifts/completed', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => { + try { + const items = await queryService.listCompletedShifts(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/shifts/:shiftId', requireAuth, requirePolicy('shifts.read', 'shift'), async (req, res, next) => { + try { + const data = await queryService.getStaffShiftDetail(req.actor.uid, req.params.shiftId); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/sections', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const data = await queryService.getProfileSectionsStatus(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/personal-info', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const data = await queryService.getPersonalInfo(req.actor.uid); + return res.status(200).json({ ...data, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/industries', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listIndustries(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/skills', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listSkills(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/documents', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listProfileDocuments(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/certificates', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listCertificates(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/bank-accounts', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listStaffBankAccounts(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + router.get('/staff/profile/benefits', requireAuth, requirePolicy('staff.profile.read', 'staff'), async (req, res, next) => { + try { + const items = await queryService.listStaffBenefits(req.actor.uid); + return res.status(200).json({ items, requestId: req.requestId }); + } catch (error) { + return next(error); + } + }); + + return router; +} diff --git a/backend/query-api/src/services/actor-context.js b/backend/query-api/src/services/actor-context.js new file mode 100644 index 00000000..30d23aa5 --- /dev/null +++ b/backend/query-api/src/services/actor-context.js @@ -0,0 +1,111 @@ +import { AppError } from '../lib/errors.js'; +import { query } from './db.js'; + +export async function loadActorContext(uid) { + const [userResult, tenantResult, businessResult, vendorResult, staffResult] = await Promise.all([ + query( + ` + SELECT id AS "userId", email, display_name AS "displayName", phone, status + FROM users + WHERE id = $1 + `, + [uid] + ), + query( + ` + SELECT tm.id AS "membershipId", + tm.tenant_id AS "tenantId", + tm.base_role AS role, + t.name AS "tenantName", + t.slug AS "tenantSlug" + FROM tenant_memberships tm + JOIN tenants t ON t.id = tm.tenant_id + WHERE tm.user_id = $1 + AND tm.membership_status = 'ACTIVE' + ORDER BY tm.created_at ASC + LIMIT 1 + `, + [uid] + ), + query( + ` + SELECT bm.id AS "membershipId", + bm.business_id AS "businessId", + bm.business_role AS role, + b.business_name AS "businessName", + b.slug AS "businessSlug", + bm.tenant_id AS "tenantId" + FROM business_memberships bm + JOIN businesses b ON b.id = bm.business_id + WHERE bm.user_id = $1 + AND bm.membership_status = 'ACTIVE' + ORDER BY bm.created_at ASC + LIMIT 1 + `, + [uid] + ), + query( + ` + SELECT vm.id AS "membershipId", + vm.vendor_id AS "vendorId", + vm.vendor_role AS role, + v.company_name AS "vendorName", + v.slug AS "vendorSlug", + vm.tenant_id AS "tenantId" + FROM vendor_memberships vm + JOIN vendors v ON v.id = vm.vendor_id + WHERE vm.user_id = $1 + AND vm.membership_status = 'ACTIVE' + ORDER BY vm.created_at ASC + LIMIT 1 + `, + [uid] + ), + query( + ` + SELECT s.id AS "staffId", + s.tenant_id AS "tenantId", + s.full_name AS "fullName", + s.email, + s.phone, + s.primary_role AS "primaryRole", + s.onboarding_status AS "onboardingStatus", + s.status, + s.metadata, + w.id AS "workforceId", + w.vendor_id AS "vendorId", + w.workforce_number AS "workforceNumber" + FROM staffs s + LEFT JOIN workforce w ON w.staff_id = s.id + WHERE s.user_id = $1 + ORDER BY s.created_at ASC + LIMIT 1 + `, + [uid] + ), + ]); + + return { + user: userResult.rows[0] || null, + tenant: tenantResult.rows[0] || null, + business: businessResult.rows[0] || null, + vendor: vendorResult.rows[0] || null, + staff: staffResult.rows[0] || null, + }; +} + +export async function requireClientContext(uid) { + const context = await loadActorContext(uid); + if (!context.user || !context.tenant || !context.business) { + throw new AppError('FORBIDDEN', 'Client business context is required for this route', 403, { uid }); + } + return context; +} + +export async function requireStaffContext(uid) { + const context = await loadActorContext(uid); + if (!context.user || !context.tenant || !context.staff) { + throw new AppError('FORBIDDEN', 'Staff context is required for this route', 403, { uid }); + } + return context; +} diff --git a/backend/query-api/src/services/mobile-query-service.js b/backend/query-api/src/services/mobile-query-service.js new file mode 100644 index 00000000..166bb331 --- /dev/null +++ b/backend/query-api/src/services/mobile-query-service.js @@ -0,0 +1,1071 @@ +import { AppError } from '../lib/errors.js'; +import { query } from './db.js'; +import { requireClientContext, requireStaffContext } from './actor-context.js'; + +function parseLimit(value, fallback = 20, max = 100) { + const parsed = Number.parseInt(`${value || fallback}`, 10); + if (!Number.isFinite(parsed) || parsed <= 0) return fallback; + return Math.min(parsed, max); +} + +function parseDate(value, field) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + throw new AppError('VALIDATION_ERROR', `${field} must be a valid ISO date`, 400, { field }); + } + return date; +} + +function parseDateRange(startDate, endDate, fallbackDays = 7) { + const start = startDate ? parseDate(startDate, 'startDate') : new Date(); + const end = endDate ? parseDate(endDate, 'endDate') : new Date(start.getTime() + (fallbackDays * 24 * 60 * 60 * 1000)); + if (start > end) { + throw new AppError('VALIDATION_ERROR', 'startDate must be before endDate', 400); + } + return { + start: start.toISOString(), + end: end.toISOString(), + }; +} + +function startOfDay(value) { + const date = parseDate(value || new Date().toISOString(), 'date'); + date.setUTCHours(0, 0, 0, 0); + return date; +} + +function endOfDay(value) { + const date = startOfDay(value); + date.setUTCDate(date.getUTCDate() + 1); + return date; +} + +function metadataArray(metadata, key) { + const value = metadata?.[key]; + return Array.isArray(value) ? value : []; +} + +function getProfileCompletionFromMetadata(staffRow) { + const metadata = staffRow?.metadata || {}; + const [firstName, ...lastParts] = (staffRow?.fullName || '').trim().split(/\s+/); + const lastName = lastParts.join(' '); + + const checks = { + firstName: Boolean(metadata.firstName || firstName), + lastName: Boolean(metadata.lastName || lastName), + email: Boolean(staffRow?.email), + phone: Boolean(staffRow?.phone), + preferredLocations: metadataArray(metadata, 'preferredLocations').length > 0, + skills: metadataArray(metadata, 'skills').length > 0, + industries: metadataArray(metadata, 'industries').length > 0, + emergencyContact: Boolean(metadata.emergencyContact?.name && metadata.emergencyContact?.phone), + }; + + const missingFields = Object.entries(checks) + .filter(([, value]) => !value) + .map(([key]) => key); + + return { + completed: missingFields.length === 0, + missingFields, + fields: checks, + }; +} + +export async function getClientSession(actorUid) { + const context = await requireClientContext(actorUid); + return context; +} + +export async function getStaffSession(actorUid) { + const context = await requireStaffContext(actorUid); + return context; +} + +export async function getClientDashboard(actorUid) { + const context = await requireClientContext(actorUid); + const businessId = context.business.businessId; + const tenantId = context.tenant.tenantId; + + const [spendResult, projectionResult, coverageResult, activityResult] = await Promise.all([ + query( + ` + SELECT + COALESCE(SUM(total_cents) FILTER (WHERE created_at >= date_trunc('week', NOW())), 0)::BIGINT AS "weeklySpendCents" + FROM invoices + WHERE tenant_id = $1 + AND business_id = $2 + `, + [tenantId, businessId] + ), + query( + ` + SELECT COALESCE(SUM(sr.bill_rate_cents * sr.workers_needed), 0)::BIGINT AS "projectedSpendCents" + FROM shifts s + JOIN shift_roles sr ON sr.shift_id = s.id + WHERE s.tenant_id = $1 + AND s.business_id = $2 + AND s.starts_at >= NOW() + AND s.starts_at < NOW() + INTERVAL '7 days' + AND s.status IN ('OPEN', 'PENDING_CONFIRMATION', 'ASSIGNED', 'ACTIVE') + `, + [tenantId, businessId] + ), + query( + ` + SELECT + COALESCE(SUM(required_workers), 0)::INTEGER AS "neededWorkersToday", + COALESCE(SUM(assigned_workers), 0)::INTEGER AS "filledWorkersToday", + COALESCE(SUM(required_workers - assigned_workers), 0)::INTEGER AS "openPositionsToday" + FROM shifts + WHERE tenant_id = $1 + AND business_id = $2 + AND starts_at >= date_trunc('day', NOW()) + AND starts_at < date_trunc('day', NOW()) + INTERVAL '1 day' + `, + [tenantId, businessId] + ), + query( + ` + SELECT + COALESCE(COUNT(*) FILTER (WHERE a.status = 'NO_SHOW'), 0)::INTEGER AS "lateWorkersToday", + COALESCE(COUNT(*) FILTER (WHERE a.status IN ('CHECKED_IN', 'CHECKED_OUT', 'COMPLETED')), 0)::INTEGER AS "checkedInWorkersToday", + COALESCE(AVG(sr.bill_rate_cents), 0)::NUMERIC(12,2) AS "averageShiftCostCents" + FROM shifts s + LEFT JOIN assignments a ON a.shift_id = s.id + LEFT JOIN shift_roles sr ON sr.shift_id = s.id + WHERE s.tenant_id = $1 + AND s.business_id = $2 + AND s.starts_at >= date_trunc('day', NOW()) + AND s.starts_at < date_trunc('day', NOW()) + INTERVAL '1 day' + `, + [tenantId, businessId] + ), + ]); + + return { + userName: context.user.displayName || context.user.email, + businessName: context.business.businessName, + businessId, + spending: { + weeklySpendCents: Number(spendResult.rows[0]?.weeklySpendCents || 0), + projectedNext7DaysCents: Number(projectionResult.rows[0]?.projectedSpendCents || 0), + }, + coverage: coverageResult.rows[0], + liveActivity: activityResult.rows[0], + }; +} + +export async function listRecentReorders(actorUid, limit) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + o.id, + o.title, + o.starts_at AS "date", + COALESCE(cp.label, o.location_name) AS "hubName", + COALESCE(COUNT(sr.id), 0)::INTEGER AS "positionCount", + COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType" + FROM orders o + LEFT JOIN shifts s ON s.order_id = o.id + LEFT JOIN shift_roles sr ON sr.shift_id = s.id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + WHERE o.tenant_id = $1 + AND o.business_id = $2 + AND o.status IN ('COMPLETED', 'ACTIVE', 'FILLED') + GROUP BY o.id, cp.label + ORDER BY o.starts_at DESC NULLS LAST + LIMIT $3 + `, + [context.tenant.tenantId, context.business.businessId, parseLimit(limit, 8, 20)] + ); + return result.rows; +} + +export async function listBusinessAccounts(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + id AS "accountId", + provider_name AS "bankName", + provider_reference AS "providerReference", + last4, + is_primary AS "isPrimary", + COALESCE(metadata->>'accountType', 'CHECKING') AS "accountType", + COALESCE(metadata->>'routingNumberMasked', '***') AS "routingNumberMasked" + FROM accounts + WHERE tenant_id = $1 + AND owner_business_id = $2 + ORDER BY is_primary DESC, created_at DESC + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows; +} + +export async function listPendingInvoices(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + i.id AS "invoiceId", + i.invoice_number AS "invoiceNumber", + i.total_cents AS "amountCents", + i.status, + i.due_at AS "dueDate", + v.id AS "vendorId", + v.company_name AS "vendorName" + FROM invoices i + LEFT JOIN vendors v ON v.id = i.vendor_id + WHERE i.tenant_id = $1 + AND i.business_id = $2 + AND i.status IN ('PENDING', 'PENDING_REVIEW', 'APPROVED', 'OVERDUE', 'DISPUTED') + ORDER BY i.due_at ASC NULLS LAST, i.created_at DESC + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows; +} + +export async function listInvoiceHistory(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + i.id AS "invoiceId", + i.invoice_number AS "invoiceNumber", + i.total_cents AS "amountCents", + i.status, + i.updated_at AS "paymentDate", + v.id AS "vendorId", + v.company_name AS "vendorName" + FROM invoices i + LEFT JOIN vendors v ON v.id = i.vendor_id + WHERE i.tenant_id = $1 + AND i.business_id = $2 + ORDER BY i.created_at DESC + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows; +} + +export async function getCurrentBill(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT COALESCE(SUM(total_cents), 0)::BIGINT AS "currentBillCents" + FROM invoices + WHERE tenant_id = $1 + AND business_id = $2 + AND status NOT IN ('PAID', 'VOID') + AND created_at >= date_trunc('month', NOW()) + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows[0]; +} + +export async function getSavings(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT COALESCE(SUM(COALESCE(NULLIF(metadata->>'savingsCents', '')::BIGINT, 0)), 0)::BIGINT AS "savingsCents" + FROM invoices + WHERE tenant_id = $1 + AND business_id = $2 + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows[0]; +} + +export async function getSpendBreakdown(actorUid, { startDate, endDate }) { + const context = await requireClientContext(actorUid); + const range = parseDateRange(startDate, endDate, 30); + const result = await query( + ` + WITH items AS ( + SELECT + COALESCE(sr.role_name, 'Unknown') AS category, + SUM(sr.bill_rate_cents * GREATEST(sr.assigned_count, sr.workers_needed))::BIGINT AS amount_cents + FROM shifts s + JOIN shift_roles sr ON sr.shift_id = s.id + WHERE s.tenant_id = $1 + AND s.business_id = $2 + AND s.starts_at >= $3::timestamptz + AND s.starts_at <= $4::timestamptz + GROUP BY sr.role_name + ) + SELECT + category, + amount_cents AS "amountCents", + CASE WHEN SUM(amount_cents) OVER () = 0 THEN 0 + ELSE ROUND((amount_cents::numeric / SUM(amount_cents) OVER ()) * 100, 2) + END AS percentage + FROM items + ORDER BY amount_cents DESC, category ASC + `, + [context.tenant.tenantId, context.business.businessId, range.start, range.end] + ); + return result.rows; +} + +export async function listCoverageByDate(actorUid, { date }) { + const context = await requireClientContext(actorUid); + const from = startOfDay(date).toISOString(); + const to = endOfDay(date).toISOString(); + const result = await query( + ` + SELECT + s.id AS "shiftId", + s.title, + s.starts_at AS "startsAt", + s.ends_at AS "endsAt", + s.required_workers AS "requiredWorkers", + s.assigned_workers AS "assignedWorkers", + sr.role_name AS "roleName", + a.id AS "assignmentId", + a.status AS "assignmentStatus", + st.id AS "staffId", + st.full_name AS "staffName", + attendance_sessions.check_in_at AS "checkInAt" + FROM shifts s + LEFT JOIN shift_roles sr ON sr.shift_id = s.id + LEFT JOIN assignments a ON a.shift_id = s.id + LEFT JOIN staffs st ON st.id = a.staff_id + LEFT JOIN attendance_sessions ON attendance_sessions.assignment_id = a.id + WHERE s.tenant_id = $1 + AND s.business_id = $2 + AND s.starts_at >= $3::timestamptz + AND s.starts_at < $4::timestamptz + ORDER BY s.starts_at ASC, st.full_name ASC NULLS LAST + `, + [context.tenant.tenantId, context.business.businessId, from, to] + ); + + const grouped = new Map(); + for (const row of result.rows) { + const current = grouped.get(row.shiftId) || { + shiftId: row.shiftId, + roleName: row.roleName, + timeRange: { + startsAt: row.startsAt, + endsAt: row.endsAt, + }, + requiredWorkerCount: row.requiredWorkers, + assignedWorkerCount: row.assignedWorkers, + assignedWorkers: [], + }; + if (row.staffId) { + current.assignedWorkers.push({ + assignmentId: row.assignmentId, + staffId: row.staffId, + fullName: row.staffName, + status: row.assignmentStatus, + checkInAt: row.checkInAt, + }); + } + grouped.set(row.shiftId, current); + } + + return Array.from(grouped.values()); +} + +export async function getCoverageStats(actorUid, { date }) { + const items = await listCoverageByDate(actorUid, { date }); + const totals = items.reduce((acc, item) => { + acc.totalPositionsNeeded += Number(item.requiredWorkerCount || 0); + acc.totalPositionsConfirmed += Number(item.assignedWorkerCount || 0); + acc.totalWorkersCheckedIn += item.assignedWorkers.filter((worker) => worker.checkInAt).length; + acc.totalWorkersEnRoute += item.assignedWorkers.filter((worker) => worker.status === 'ACCEPTED').length; + acc.totalWorkersLate += item.assignedWorkers.filter((worker) => worker.status === 'NO_SHOW').length; + return acc; + }, { + totalPositionsNeeded: 0, + totalPositionsConfirmed: 0, + totalWorkersCheckedIn: 0, + totalWorkersEnRoute: 0, + totalWorkersLate: 0, + }); + + return { + ...totals, + totalCoveragePercentage: totals.totalPositionsNeeded === 0 + ? 0 + : Math.round((totals.totalPositionsConfirmed / totals.totalPositionsNeeded) * 100), + }; +} + +export async function listHubs(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + cp.id AS "hubId", + cp.label AS name, + cp.address AS "fullAddress", + cp.latitude, + cp.longitude, + cp.nfc_tag_uid AS "nfcTagId", + cp.metadata->>'city' AS city, + cp.metadata->>'state' AS state, + cp.metadata->>'zipCode' AS "zipCode", + cc.id AS "costCenterId", + cc.name AS "costCenterName" + FROM clock_points cp + LEFT JOIN cost_centers cc ON cc.id = cp.cost_center_id + WHERE cp.tenant_id = $1 + AND cp.business_id = $2 + AND cp.status = 'ACTIVE' + ORDER BY cp.label ASC + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows; +} + +export async function listCostCenters(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT id AS "costCenterId", name + FROM cost_centers + WHERE tenant_id = $1 + AND business_id = $2 + AND status = 'ACTIVE' + ORDER BY name ASC + `, + [context.tenant.tenantId, context.business.businessId] + ); + return result.rows; +} + +export async function listVendors(actorUid) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT id AS "vendorId", company_name AS "vendorName" + FROM vendors + WHERE tenant_id = $1 + AND status = 'ACTIVE' + ORDER BY company_name ASC + `, + [context.tenant.tenantId] + ); + return result.rows; +} + +export async function listVendorRoles(actorUid, vendorId) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + rc.id AS "roleId", + rc.code AS "roleCode", + rc.name AS "roleName", + COALESCE(MAX(sr.bill_rate_cents), 0)::INTEGER AS "hourlyRateCents" + FROM roles_catalog rc + LEFT JOIN shift_roles sr ON sr.role_id = rc.id + LEFT JOIN shifts s ON s.id = sr.shift_id AND (s.vendor_id = $2 OR $2::uuid IS NULL) + WHERE rc.tenant_id = $1 + AND rc.status = 'ACTIVE' + GROUP BY rc.id + ORDER BY rc.name ASC + `, + [context.tenant.tenantId, vendorId || null] + ); + return result.rows; +} + +export async function listHubManagers(actorUid, hubId) { + const context = await requireClientContext(actorUid); + const result = await query( + ` + SELECT + hm.id AS "managerAssignmentId", + bm.id AS "businessMembershipId", + u.id AS "managerId", + COALESCE(u.display_name, u.email) AS name + FROM hub_managers hm + JOIN business_memberships bm ON bm.id = hm.business_membership_id + JOIN users u ON u.id = bm.user_id + WHERE hm.tenant_id = $1 + AND hm.hub_id = $2 + ORDER BY name ASC + `, + [context.tenant.tenantId, hubId] + ); + return result.rows; +} + +export async function listOrderItemsByDateRange(actorUid, { startDate, endDate }) { + const context = await requireClientContext(actorUid); + const range = parseDateRange(startDate, endDate, 14); + const result = await query( + ` + SELECT + sr.id AS "itemId", + o.id AS "orderId", + COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType", + sr.role_name AS "roleName", + s.starts_at AS date, + s.starts_at AS "startsAt", + s.ends_at AS "endsAt", + sr.workers_needed AS "requiredWorkerCount", + sr.assigned_count AS "filledCount", + sr.bill_rate_cents AS "hourlyRateCents", + (sr.bill_rate_cents * sr.workers_needed)::BIGINT AS "totalCostCents", + COALESCE(cp.label, s.location_name) AS "locationName", + s.status, + COALESCE( + json_agg( + json_build_object( + 'applicationId', a.application_id, + 'workerName', st.full_name, + 'role', sr.role_name, + 'confirmationStatus', a.status + ) + ) FILTER (WHERE a.id IS NOT NULL), + '[]'::json + ) AS workers + FROM shift_roles sr + JOIN shifts s ON s.id = sr.shift_id + JOIN orders o ON o.id = s.order_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + LEFT JOIN assignments a ON a.shift_role_id = sr.id + LEFT JOIN staffs st ON st.id = a.staff_id + WHERE o.tenant_id = $1 + AND o.business_id = $2 + AND s.starts_at >= $3::timestamptz + AND s.starts_at <= $4::timestamptz + GROUP BY sr.id, o.id, s.id, cp.label + ORDER BY s.starts_at ASC, sr.role_name ASC + `, + [context.tenant.tenantId, context.business.businessId, range.start, range.end] + ); + return result.rows; +} + +export async function getStaffDashboard(actorUid) { + const context = await requireStaffContext(actorUid); + const [todayShifts, tomorrowShifts, recommendedShifts, benefits] = await Promise.all([ + listTodayShifts(actorUid), + listAssignedShifts(actorUid, { + startDate: endOfDay(new Date().toISOString()).toISOString(), + endDate: endOfDay(new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString()).toISOString(), + }), + listOpenShifts(actorUid, { limit: 5 }), + listStaffBenefits(actorUid), + ]); + + return { + staffName: context.staff.fullName, + todaysShifts: todayShifts, + tomorrowsShifts: tomorrowShifts.slice(0, 5), + recommendedShifts: recommendedShifts.slice(0, 5), + benefits, + }; +} + +export async function getStaffProfileCompletion(actorUid) { + const context = await requireStaffContext(actorUid); + const completion = getProfileCompletionFromMetadata(context.staff); + return { + staffId: context.staff.staffId, + ...completion, + }; +} + +export async function listStaffAvailability(actorUid, { startDate, endDate }) { + const context = await requireStaffContext(actorUid); + const range = parseDateRange(startDate, endDate, 6); + const recurring = await query( + ` + SELECT day_of_week AS "dayOfWeek", availability_status AS status, time_slots AS slots + FROM staff_availability + WHERE tenant_id = $1 + AND staff_id = $2 + ORDER BY day_of_week ASC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + + const rowsByDay = new Map(recurring.rows.map((row) => [Number(row.dayOfWeek), row])); + const items = []; + let cursor = new Date(range.start); + const end = new Date(range.end); + while (cursor <= end) { + const day = cursor.getUTCDay(); + const recurringEntry = rowsByDay.get(day); + items.push({ + date: cursor.toISOString().slice(0, 10), + dayOfWeek: day, + availabilityStatus: recurringEntry?.status || 'UNAVAILABLE', + slots: recurringEntry?.slots || [], + }); + cursor = new Date(cursor.getTime() + (24 * 60 * 60 * 1000)); + } + return items; +} + +export async function listTodayShifts(actorUid) { + const context = await requireStaffContext(actorUid); + const from = startOfDay(new Date().toISOString()).toISOString(); + const to = endOfDay(new Date().toISOString()).toISOString(); + const result = await query( + ` + SELECT + a.id AS "assignmentId", + s.id AS "shiftId", + sr.role_name AS "roleName", + COALESCE(cp.label, s.location_name) AS location, + s.starts_at AS "startTime", + s.ends_at AS "endTime", + COALESCE(attendance_sessions.status, 'NOT_CLOCKED_IN') AS "attendanceStatus", + attendance_sessions.check_in_at AS "clockInAt" + FROM assignments a + JOIN shifts s ON s.id = a.shift_id + JOIN shift_roles sr ON sr.id = a.shift_role_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + LEFT JOIN attendance_sessions ON attendance_sessions.assignment_id = a.id + WHERE a.tenant_id = $1 + AND a.staff_id = $2 + AND s.starts_at >= $3::timestamptz + AND s.starts_at < $4::timestamptz + AND a.status IN ('ASSIGNED', 'ACCEPTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED') + ORDER BY ABS(EXTRACT(EPOCH FROM (s.starts_at - NOW()))) ASC + `, + [context.tenant.tenantId, context.staff.staffId, from, to] + ); + return result.rows; +} + +export async function getCurrentAttendanceStatus(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + a.shift_id AS "activeShiftId", + attendance_sessions.status AS "attendanceStatus", + attendance_sessions.check_in_at AS "clockInAt" + FROM attendance_sessions + JOIN assignments a ON a.id = attendance_sessions.assignment_id + WHERE attendance_sessions.tenant_id = $1 + AND attendance_sessions.staff_id = $2 + AND attendance_sessions.status = 'OPEN' + ORDER BY attendance_sessions.updated_at DESC + LIMIT 1 + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows[0] || { + attendanceStatus: 'NOT_CLOCKED_IN', + activeShiftId: null, + clockInAt: null, + }; +} + +export async function getPaymentsSummary(actorUid, { startDate, endDate }) { + const context = await requireStaffContext(actorUid); + const range = parseDateRange(startDate, endDate, 30); + const result = await query( + ` + SELECT COALESCE(SUM(amount_cents), 0)::BIGINT AS "totalEarningsCents" + FROM recent_payments + WHERE tenant_id = $1 + AND staff_id = $2 + AND created_at >= $3::timestamptz + AND created_at <= $4::timestamptz + `, + [context.tenant.tenantId, context.staff.staffId, range.start, range.end] + ); + return result.rows[0]; +} + +export async function listPaymentsHistory(actorUid, { startDate, endDate }) { + const context = await requireStaffContext(actorUid); + const range = parseDateRange(startDate, endDate, 30); + const result = await query( + ` + SELECT + rp.id AS "paymentId", + rp.amount_cents AS "amountCents", + COALESCE(rp.process_date, rp.created_at) AS date, + rp.status, + s.title AS "shiftName", + COALESCE(cp.label, s.location_name) AS location, + sr.pay_rate_cents AS "hourlyRateCents", + COALESCE(ts.regular_minutes + ts.overtime_minutes, 0) AS minutesWorked + FROM recent_payments rp + LEFT JOIN assignments a ON a.id = rp.assignment_id + LEFT JOIN shifts s ON s.id = a.shift_id + LEFT JOIN shift_roles sr ON sr.id = a.shift_role_id + LEFT JOIN timesheets ts ON ts.assignment_id = a.id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + WHERE rp.tenant_id = $1 + AND rp.staff_id = $2 + AND rp.created_at >= $3::timestamptz + AND rp.created_at <= $4::timestamptz + ORDER BY date DESC + `, + [context.tenant.tenantId, context.staff.staffId, range.start, range.end] + ); + return result.rows; +} + +export async function getPaymentChart(actorUid, { startDate, endDate, bucket = 'day' }) { + const context = await requireStaffContext(actorUid); + const range = parseDateRange(startDate, endDate, 30); + const dateBucket = bucket === 'week' ? 'week' : bucket === 'month' ? 'month' : 'day'; + const result = await query( + ` + SELECT + date_trunc('${dateBucket}', COALESCE(process_date, created_at)) AS bucket, + COALESCE(SUM(amount_cents), 0)::BIGINT AS "amountCents" + FROM recent_payments + WHERE tenant_id = $1 + AND staff_id = $2 + AND created_at >= $3::timestamptz + AND created_at <= $4::timestamptz + GROUP BY 1 + ORDER BY 1 ASC + `, + [context.tenant.tenantId, context.staff.staffId, range.start, range.end] + ); + return result.rows; +} + +export async function listAssignedShifts(actorUid, { startDate, endDate }) { + const context = await requireStaffContext(actorUid); + const range = parseDateRange(startDate, endDate, 14); + const result = await query( + ` + SELECT + a.id AS "assignmentId", + s.id AS "shiftId", + sr.role_name AS "roleName", + COALESCE(cp.label, s.location_name) AS location, + s.starts_at AS date, + s.starts_at AS "startTime", + s.ends_at AS "endTime", + sr.pay_rate_cents AS "hourlyRateCents", + COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType", + a.status + FROM assignments a + JOIN shifts s ON s.id = a.shift_id + JOIN shift_roles sr ON sr.id = a.shift_role_id + JOIN orders o ON o.id = s.order_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + WHERE a.tenant_id = $1 + AND a.staff_id = $2 + AND s.starts_at >= $3::timestamptz + AND s.starts_at <= $4::timestamptz + AND a.status IN ('ASSIGNED', 'ACCEPTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED') + ORDER BY s.starts_at ASC + `, + [context.tenant.tenantId, context.staff.staffId, range.start, range.end] + ); + return result.rows; +} + +export async function listOpenShifts(actorUid, { limit, search } = {}) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + s.id AS "shiftId", + sr.id AS "roleId", + sr.role_name AS "roleName", + COALESCE(cp.label, s.location_name) AS location, + s.starts_at AS date, + s.starts_at AS "startTime", + s.ends_at AS "endTime", + sr.pay_rate_cents AS "hourlyRateCents", + COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType", + FALSE AS "instantBook", + sr.workers_needed AS "requiredWorkerCount" + FROM shifts s + JOIN shift_roles sr ON sr.shift_id = s.id + JOIN orders o ON o.id = s.order_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + WHERE s.tenant_id = $1 + AND s.status = 'OPEN' + AND ($2::text IS NULL OR sr.role_name ILIKE '%' || $2 || '%' OR COALESCE(cp.label, s.location_name) ILIKE '%' || $2 || '%') + AND NOT EXISTS ( + SELECT 1 + FROM applications a + WHERE a.shift_role_id = sr.id + AND a.staff_id = $3 + AND a.status IN ('PENDING', 'CONFIRMED') + ) + AND sr.role_code = $4 + ORDER BY s.starts_at ASC + LIMIT $5 + `, + [ + context.tenant.tenantId, + search || null, + context.staff.staffId, + context.staff.primaryRole || 'BARISTA', + parseLimit(limit, 20, 100), + ] + ); + return result.rows; +} + +export async function listPendingAssignments(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + a.id AS "assignmentId", + s.id AS "shiftId", + s.title, + sr.role_name AS "roleName", + s.starts_at AS "startTime", + s.ends_at AS "endTime", + COALESCE(cp.label, s.location_name) AS location, + a.created_at AS "responseDeadline" + FROM assignments a + JOIN shifts s ON s.id = a.shift_id + JOIN shift_roles sr ON sr.id = a.shift_role_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + WHERE a.tenant_id = $1 + AND a.staff_id = $2 + AND a.status = 'ASSIGNED' + ORDER BY s.starts_at ASC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} + +export async function getStaffShiftDetail(actorUid, shiftId) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + s.id AS "shiftId", + s.title, + o.description, + COALESCE(cp.label, s.location_name) AS location, + s.location_address AS address, + s.starts_at AS date, + s.starts_at AS "startTime", + s.ends_at AS "endTime", + sr.id AS "roleId", + sr.role_name AS "roleName", + sr.pay_rate_cents AS "hourlyRateCents", + COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType", + sr.workers_needed AS "requiredCount", + sr.assigned_count AS "confirmedCount", + a.status AS "assignmentStatus", + app.status AS "applicationStatus" + FROM shifts s + JOIN orders o ON o.id = s.order_id + JOIN shift_roles sr ON sr.shift_id = s.id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + LEFT JOIN assignments a ON a.shift_role_id = sr.id AND a.staff_id = $3 + LEFT JOIN applications app ON app.shift_role_id = sr.id AND app.staff_id = $3 + WHERE s.tenant_id = $1 + AND s.id = $2 + ORDER BY sr.role_name ASC + LIMIT 1 + `, + [context.tenant.tenantId, shiftId, context.staff.staffId] + ); + + if (result.rowCount === 0) { + throw new AppError('NOT_FOUND', 'Shift not found', 404, { shiftId }); + } + + return result.rows[0]; +} + +export async function listCancelledShifts(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + a.id AS "assignmentId", + s.id AS "shiftId", + s.title, + COALESCE(cp.label, s.location_name) AS location, + s.starts_at AS date, + a.metadata->>'cancellationReason' AS "cancellationReason" + FROM assignments a + JOIN shifts s ON s.id = a.shift_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + WHERE a.tenant_id = $1 + AND a.staff_id = $2 + AND a.status = 'CANCELLED' + ORDER BY s.starts_at DESC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} + +export async function listCompletedShifts(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + a.id AS "assignmentId", + s.id AS "shiftId", + s.title, + COALESCE(cp.label, s.location_name) AS location, + s.starts_at AS date, + COALESCE(ts.regular_minutes + ts.overtime_minutes, 0) AS "minutesWorked", + COALESCE(rp.status, 'PENDING') AS "paymentStatus" + FROM assignments a + JOIN shifts s ON s.id = a.shift_id + LEFT JOIN clock_points cp ON cp.id = s.clock_point_id + LEFT JOIN timesheets ts ON ts.assignment_id = a.id + LEFT JOIN recent_payments rp ON rp.assignment_id = a.id + WHERE a.tenant_id = $1 + AND a.staff_id = $2 + AND a.status IN ('CHECKED_OUT', 'COMPLETED') + ORDER BY s.starts_at DESC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} + +export async function getProfileSectionsStatus(actorUid) { + const context = await requireStaffContext(actorUid); + const completion = getProfileCompletionFromMetadata(context.staff); + const [documents, certificates, benefits] = await Promise.all([ + listProfileDocuments(actorUid), + listCertificates(actorUid), + listStaffBenefits(actorUid), + ]); + return { + personalInfoCompleted: completion.fields.firstName && completion.fields.lastName && completion.fields.email && completion.fields.phone && completion.fields.preferredLocations, + emergencyContactCompleted: completion.fields.emergencyContact, + experienceCompleted: completion.fields.skills && completion.fields.industries, + attireCompleted: documents.filter((item) => item.documentType === 'ATTIRE').every((item) => item.status === 'VERIFIED'), + taxFormsCompleted: documents.filter((item) => item.documentType === 'TAX_FORM').every((item) => item.status === 'VERIFIED'), + benefitsConfigured: benefits.length > 0, + certificateCount: certificates.length, + }; +} + +export async function getPersonalInfo(actorUid) { + const context = await requireStaffContext(actorUid); + const metadata = context.staff.metadata || {}; + return { + staffId: context.staff.staffId, + firstName: metadata.firstName || context.staff.fullName.split(' ')[0] || null, + lastName: metadata.lastName || context.staff.fullName.split(' ').slice(1).join(' ') || null, + bio: metadata.bio || null, + preferredLocations: metadataArray(metadata, 'preferredLocations'), + maxDistanceMiles: metadata.maxDistanceMiles || null, + industries: metadataArray(metadata, 'industries'), + skills: metadataArray(metadata, 'skills'), + email: context.staff.email, + phone: context.staff.phone, + }; +} + +export async function listIndustries(actorUid) { + const context = await requireStaffContext(actorUid); + return metadataArray(context.staff.metadata || {}, 'industries'); +} + +export async function listSkills(actorUid) { + const context = await requireStaffContext(actorUid); + return metadataArray(context.staff.metadata || {}, 'skills'); +} + +export async function listProfileDocuments(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + sd.id AS "staffDocumentId", + d.id AS "documentId", + d.document_type AS "documentType", + d.name, + sd.file_uri AS "fileUri", + sd.status, + sd.expires_at AS "expiresAt" + FROM staff_documents sd + JOIN documents d ON d.id = sd.document_id + WHERE sd.tenant_id = $1 + AND sd.staff_id = $2 + ORDER BY d.name ASC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} + +export async function listCertificates(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + id AS "certificateId", + certificate_type AS "certificateType", + certificate_number AS "certificateNumber", + issued_at AS "issuedAt", + expires_at AS "expiresAt", + status + FROM certificates + WHERE tenant_id = $1 + AND staff_id = $2 + ORDER BY created_at DESC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} + +export async function listStaffBankAccounts(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + id AS "accountId", + provider_name AS "bankName", + provider_reference AS "providerReference", + last4, + is_primary AS "isPrimary", + COALESCE(metadata->>'accountType', 'CHECKING') AS "accountType" + FROM accounts + WHERE tenant_id = $1 + AND owner_staff_id = $2 + ORDER BY is_primary DESC, created_at DESC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} + +export async function listStaffBenefits(actorUid) { + const context = await requireStaffContext(actorUid); + const result = await query( + ` + SELECT + id AS "benefitId", + benefit_type AS "benefitType", + title, + status, + tracked_hours AS "trackedHours", + target_hours AS "targetHours", + metadata + FROM staff_benefits + WHERE tenant_id = $1 + AND staff_id = $2 + AND status = 'ACTIVE' + ORDER BY created_at ASC + `, + [context.tenant.tenantId, context.staff.staffId] + ); + return result.rows; +} diff --git a/backend/query-api/test/mobile-routes.test.js b/backend/query-api/test/mobile-routes.test.js new file mode 100644 index 00000000..c42ad7df --- /dev/null +++ b/backend/query-api/test/mobile-routes.test.js @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import request from 'supertest'; +import { createApp } from '../src/app.js'; + +process.env.AUTH_BYPASS = 'true'; + +function createMobileQueryService() { + return { + getClientDashboard: async () => ({ businessName: 'Google Cafes' }), + getClientSession: async () => ({ business: { businessId: 'b1' } }), + getCoverageStats: async () => ({ totalCoveragePercentage: 100 }), + getCurrentAttendanceStatus: async () => ({ attendanceStatus: 'NOT_CLOCKED_IN' }), + getCurrentBill: async () => ({ currentBillCents: 1000 }), + getPaymentChart: async () => ([{ amountCents: 100 }]), + getPaymentsSummary: async () => ({ totalEarningsCents: 500 }), + getPersonalInfo: async () => ({ firstName: 'Ana' }), + getProfileSectionsStatus: async () => ({ personalInfoCompleted: true }), + getSavings: async () => ({ savingsCents: 200 }), + getSpendBreakdown: async () => ([{ category: 'Barista', amountCents: 1000 }]), + getStaffDashboard: async () => ({ staffName: 'Ana Barista' }), + getStaffProfileCompletion: async () => ({ completed: true }), + getStaffSession: async () => ({ staff: { staffId: 's1' } }), + getStaffShiftDetail: async () => ({ shiftId: 'shift-1' }), + listAssignedShifts: async () => ([{ shiftId: 'assigned-1' }]), + listBusinessAccounts: async () => ([{ accountId: 'acc-1' }]), + listCancelledShifts: async () => ([{ shiftId: 'cancelled-1' }]), + listCertificates: async () => ([{ certificateId: 'cert-1' }]), + listCostCenters: async () => ([{ costCenterId: 'cc-1' }]), + listCoverageByDate: async () => ([{ shiftId: 'coverage-1' }]), + listCompletedShifts: async () => ([{ shiftId: 'completed-1' }]), + listHubManagers: async () => ([{ managerId: 'm1' }]), + listHubs: async () => ([{ hubId: 'hub-1' }]), + listIndustries: async () => (['CATERING']), + listInvoiceHistory: async () => ([{ invoiceId: 'inv-1' }]), + listOpenShifts: async () => ([{ shiftId: 'open-1' }]), + listOrderItemsByDateRange: async () => ([{ itemId: 'item-1' }]), + listPaymentsHistory: async () => ([{ paymentId: 'pay-1' }]), + listPendingAssignments: async () => ([{ assignmentId: 'asg-1' }]), + listPendingInvoices: async () => ([{ invoiceId: 'pending-1' }]), + listProfileDocuments: async () => ([{ staffDocumentId: 'doc-1' }]), + listRecentReorders: async () => ([{ id: 'order-1' }]), + listSkills: async () => (['BARISTA']), + listStaffAvailability: async () => ([{ dayOfWeek: 1 }]), + listStaffBankAccounts: async () => ([{ accountId: 'acc-2' }]), + listStaffBenefits: async () => ([{ benefitId: 'benefit-1' }]), + listTodayShifts: async () => ([{ shiftId: 'today-1' }]), + listVendorRoles: async () => ([{ roleId: 'role-1' }]), + listVendors: async () => ([{ vendorId: 'vendor-1' }]), + }; +} + +test('GET /query/client/session returns injected client session', async () => { + const app = createApp({ mobileQueryService: createMobileQueryService() }); + const res = await request(app) + .get('/query/client/session') + .set('Authorization', 'Bearer test-token'); + + assert.equal(res.status, 200); + assert.equal(res.body.business.businessId, 'b1'); +}); + +test('GET /query/client/coverage validates date query param', async () => { + const app = createApp({ mobileQueryService: createMobileQueryService() }); + const res = await request(app) + .get('/query/client/coverage') + .set('Authorization', 'Bearer test-token'); + + assert.equal(res.status, 400); + assert.equal(res.body.code, 'VALIDATION_ERROR'); +}); + +test('GET /query/staff/dashboard returns injected dashboard', async () => { + const app = createApp({ mobileQueryService: createMobileQueryService() }); + const res = await request(app) + .get('/query/staff/dashboard') + .set('Authorization', 'Bearer test-token'); + + assert.equal(res.status, 200); + assert.equal(res.body.staffName, 'Ana Barista'); +}); + +test('GET /query/staff/shifts/:shiftId returns injected shift detail', async () => { + const app = createApp({ mobileQueryService: createMobileQueryService() }); + const res = await request(app) + .get('/query/staff/shifts/shift-1') + .set('Authorization', 'Bearer test-token'); + + assert.equal(res.status, 200); + assert.equal(res.body.shiftId, 'shift-1'); +}); diff --git a/backend/unified-api/Dockerfile b/backend/unified-api/Dockerfile new file mode 100644 index 00000000..55a6a26b --- /dev/null +++ b/backend/unified-api/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --omit=dev + +COPY src ./src + +ENV PORT=8080 +EXPOSE 8080 + +CMD ["node", "src/server.js"] diff --git a/backend/unified-api/package-lock.json b/backend/unified-api/package-lock.json new file mode 100644 index 00000000..43c1ab9c --- /dev/null +++ b/backend/unified-api/package-lock.json @@ -0,0 +1,3661 @@ +{ + "name": "@krow/unified-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@krow/unified-api", + "version": "0.1.0", + "dependencies": { + "express": "^4.21.2", + "firebase-admin": "^13.0.2", + "pg": "^8.20.0", + "pino": "^9.6.0", + "pino-http": "^10.3.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "supertest": "^7.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz", + "integrity": "sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.1.tgz", + "integrity": "sha512-LwIXe8+mVHY5LBPulWECOOIEXDiatyECp/BOlu0gOhe+WOcKjWHROaCbLlkFTgHMY7RHr5MOxkLP/tltWAH3dA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.1.tgz", + "integrity": "sha512-heAEVZ9Z8c8PnBUcmGh91JHX0cXcVa1yESW/xkLuwaX7idRFyLiN8sl73KXpR8ZArGoPXVQDanBnk6SQiekRCQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/database": "1.1.1", + "@firebase/database-types": "1.0.17", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.17.tgz", + "integrity": "sha512-4eWaM5fW3qEIHjGzfi3cf0Jpqi1xQsAdT6rSDE1RZPrWu8oGjgrq6ybMjobtyHQFgwGCykBm4YM89qDzc+uG/w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.14.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz", + "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", + "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0", + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.4.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/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "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/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=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/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.3.tgz", + "integrity": "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.5.tgz", + "integrity": "sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "fast-xml-builder": "^1.1.3", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "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/firebase-admin": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.7.0.tgz", + "integrity": "sha512-o3qS8zCJbApe7aKzkO2Pa380t9cHISqeSd3blqYTtOuUUUua3qZTLwNWgGUOss3td6wbzrZhiHIj3c8+fC046Q==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "farmhash-modern": "^1.1.0", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^10.6.1", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^11.0.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.19.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "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/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", + "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "7.1.3", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "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/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, + "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/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/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", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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/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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/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/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "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", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 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-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "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/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.5.0.tgz", + "integrity": "sha512-hD91XjgaKkSsdn8P7LaebrNzhGTdB086W3pyPihX0EzGPjq5uBJBXo4N5guqNaK6mUjg9aubMF7wDViYek9dRA==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^9.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "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.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "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/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/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", + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "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/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "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" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "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", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/unified-api/package.json b/backend/unified-api/package.json new file mode 100644 index 00000000..6556c3ba --- /dev/null +++ b/backend/unified-api/package.json @@ -0,0 +1,24 @@ +{ + "name": "@krow/unified-api", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "start": "node src/server.js", + "test": "node --test" + }, + "dependencies": { + "express": "^4.21.2", + "firebase-admin": "^13.0.2", + "pg": "^8.20.0", + "pino": "^9.6.0", + "pino-http": "^10.3.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "supertest": "^7.0.0" + } +} diff --git a/backend/unified-api/src/app.js b/backend/unified-api/src/app.js new file mode 100644 index 00000000..a30f8657 --- /dev/null +++ b/backend/unified-api/src/app.js @@ -0,0 +1,31 @@ +import express from 'express'; +import pino from 'pino'; +import pinoHttp from 'pino-http'; +import { requestContext } from './middleware/request-context.js'; +import { errorHandler, notFoundHandler } from './middleware/error-handler.js'; +import { healthRouter } from './routes/health.js'; +import { createAuthRouter } from './routes/auth.js'; +import { createProxyRouter } from './routes/proxy.js'; + +const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); + +export function createApp(options = {}) { + const app = express(); + + app.use(requestContext); + app.use( + pinoHttp({ + logger, + customProps: (req) => ({ requestId: req.requestId }), + }) + ); + + app.use(healthRouter); + app.use('/auth', createAuthRouter({ fetchImpl: options.fetchImpl, authService: options.authService })); + app.use(createProxyRouter(options)); + + app.use(notFoundHandler); + app.use(errorHandler); + + return app; +} diff --git a/backend/unified-api/src/lib/errors.js b/backend/unified-api/src/lib/errors.js new file mode 100644 index 00000000..05548b32 --- /dev/null +++ b/backend/unified-api/src/lib/errors.js @@ -0,0 +1,26 @@ +export class AppError extends Error { + constructor(code, message, status = 400, details = {}) { + super(message); + this.name = 'AppError'; + this.code = code; + this.status = status; + this.details = details; + } +} + +export function toErrorEnvelope(error, requestId) { + const status = error?.status && Number.isInteger(error.status) ? error.status : 500; + const code = error?.code || 'INTERNAL_ERROR'; + const message = error?.message || 'Unexpected error'; + const details = error?.details || {}; + + return { + status, + body: { + code, + message, + details, + requestId, + }, + }; +} diff --git a/backend/unified-api/src/middleware/error-handler.js b/backend/unified-api/src/middleware/error-handler.js new file mode 100644 index 00000000..289395f3 --- /dev/null +++ b/backend/unified-api/src/middleware/error-handler.js @@ -0,0 +1,25 @@ +import { toErrorEnvelope } from '../lib/errors.js'; + +export function notFoundHandler(req, res) { + res.status(404).json({ + code: 'NOT_FOUND', + message: `Route not found: ${req.method} ${req.path}`, + details: {}, + requestId: req.requestId, + }); +} + +export function errorHandler(error, req, res, _next) { + const envelope = toErrorEnvelope(error, req.requestId); + if (req.log) { + req.log.error( + { + errCode: envelope.body.code, + status: envelope.status, + details: envelope.body.details, + }, + envelope.body.message + ); + } + res.status(envelope.status).json(envelope.body); +} diff --git a/backend/unified-api/src/middleware/request-context.js b/backend/unified-api/src/middleware/request-context.js new file mode 100644 index 00000000..c633acbb --- /dev/null +++ b/backend/unified-api/src/middleware/request-context.js @@ -0,0 +1,9 @@ +import { randomUUID } from 'node:crypto'; + +export function requestContext(req, res, next) { + const incoming = req.get('X-Request-Id'); + req.requestId = incoming || randomUUID(); + res.setHeader('X-Request-Id', req.requestId); + res.locals.startedAt = Date.now(); + next(); +} diff --git a/backend/unified-api/src/routes/auth.js b/backend/unified-api/src/routes/auth.js new file mode 100644 index 00000000..faafa952 --- /dev/null +++ b/backend/unified-api/src/routes/auth.js @@ -0,0 +1,129 @@ +import express from 'express'; +import { AppError } from '../lib/errors.js'; +import { parseClientSignIn, parseClientSignUp, signInClient, signOutActor, signUpClient, getSessionForActor } from '../services/auth-service.js'; +import { verifyFirebaseToken } from '../services/firebase-auth.js'; + +const defaultAuthService = { + parseClientSignIn, + parseClientSignUp, + signInClient, + signOutActor, + signUpClient, + getSessionForActor, +}; + +function getBearerToken(header) { + if (!header) return null; + const [scheme, token] = header.split(' '); + if (!scheme || scheme.toLowerCase() !== 'bearer' || !token) return null; + return token; +} + +async function requireAuth(req, _res, next) { + try { + const token = getBearerToken(req.get('Authorization')); + if (!token) { + throw new AppError('UNAUTHENTICATED', 'Missing bearer token', 401); + } + + if (process.env.AUTH_BYPASS === 'true') { + req.actor = { uid: 'test-user', email: 'test@krow.local', role: 'TEST' }; + return next(); + } + + const decoded = await verifyFirebaseToken(token, { checkRevoked: true }); + req.actor = { + uid: decoded.uid, + email: decoded.email || null, + role: decoded.role || null, + }; + return next(); + } catch (error) { + if (error instanceof AppError) return next(error); + return next(new AppError('UNAUTHENTICATED', 'Token verification failed', 401)); + } +} + +export function createAuthRouter(options = {}) { + const router = express.Router(); + const fetchImpl = options.fetchImpl || fetch; + const authService = options.authService || defaultAuthService; + + router.use(express.json({ limit: '1mb' })); + + router.post('/client/sign-in', async (req, res, next) => { + try { + const payload = authService.parseClientSignIn(req.body); + const session = await authService.signInClient(payload, { fetchImpl }); + return res.status(200).json({ + ...session, + requestId: req.requestId, + }); + } catch (error) { + return next(error); + } + }); + + router.post('/client/sign-up', async (req, res, next) => { + try { + const payload = authService.parseClientSignUp(req.body); + const session = await authService.signUpClient(payload, { fetchImpl }); + return res.status(201).json({ + ...session, + requestId: req.requestId, + }); + } catch (error) { + return next(error); + } + }); + + router.get('/session', requireAuth, async (req, res, next) => { + try { + const session = await authService.getSessionForActor(req.actor); + return res.status(200).json({ + ...session, + requestId: req.requestId, + }); + } catch (error) { + return next(error); + } + }); + + router.post('/sign-out', requireAuth, async (req, res, next) => { + try { + const result = await authService.signOutActor(req.actor); + return res.status(200).json({ + ...result, + requestId: req.requestId, + }); + } catch (error) { + return next(error); + } + }); + + router.post('/client/sign-out', requireAuth, async (req, res, next) => { + try { + const result = await authService.signOutActor(req.actor); + return res.status(200).json({ + ...result, + requestId: req.requestId, + }); + } catch (error) { + return next(error); + } + }); + + router.post('/staff/sign-out', requireAuth, async (req, res, next) => { + try { + const result = await authService.signOutActor(req.actor); + return res.status(200).json({ + ...result, + requestId: req.requestId, + }); + } catch (error) { + return next(error); + } + }); + + return router; +} diff --git a/backend/unified-api/src/routes/health.js b/backend/unified-api/src/routes/health.js new file mode 100644 index 00000000..7fd3a64b --- /dev/null +++ b/backend/unified-api/src/routes/health.js @@ -0,0 +1,45 @@ +import { Router } from 'express'; +import { checkDatabaseHealth, isDatabaseConfigured } from '../services/db.js'; + +export const healthRouter = Router(); + +function healthHandler(req, res) { + res.status(200).json({ + ok: true, + service: 'krow-api-v2', + version: process.env.SERVICE_VERSION || 'dev', + requestId: req.requestId, + }); +} + +healthRouter.get('/health', healthHandler); +healthRouter.get('/healthz', healthHandler); + +healthRouter.get('/readyz', async (req, res) => { + if (!isDatabaseConfigured()) { + return res.status(503).json({ + ok: false, + service: 'krow-api-v2', + status: 'DATABASE_NOT_CONFIGURED', + requestId: req.requestId, + }); + } + + try { + const ok = await checkDatabaseHealth(); + return res.status(ok ? 200 : 503).json({ + ok, + service: 'krow-api-v2', + status: ok ? 'READY' : 'DATABASE_UNAVAILABLE', + requestId: req.requestId, + }); + } catch (error) { + return res.status(503).json({ + ok: false, + service: 'krow-api-v2', + status: 'DATABASE_UNAVAILABLE', + details: { message: error.message }, + requestId: req.requestId, + }); + } +}); diff --git a/backend/unified-api/src/routes/proxy.js b/backend/unified-api/src/routes/proxy.js new file mode 100644 index 00000000..69cae510 --- /dev/null +++ b/backend/unified-api/src/routes/proxy.js @@ -0,0 +1,75 @@ +import { Router } from 'express'; +import { AppError } from '../lib/errors.js'; + +const HOP_BY_HOP_HEADERS = new Set([ + 'connection', + 'content-length', + 'host', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', +]); + +function resolveTargetBase(pathname) { + if (pathname.startsWith('/core')) return process.env.CORE_API_BASE_URL; + if (pathname.startsWith('/commands')) return process.env.COMMAND_API_BASE_URL; + if (pathname.startsWith('/query')) return process.env.QUERY_API_BASE_URL; + return null; +} + +function copyHeaders(source, target) { + for (const [key, value] of source.entries()) { + if (HOP_BY_HOP_HEADERS.has(key.toLowerCase())) continue; + target.setHeader(key, value); + } +} + +async function forwardRequest(req, res, next, fetchImpl) { + try { + const requestPath = new URL(req.originalUrl, 'http://localhost').pathname; + const baseUrl = resolveTargetBase(requestPath); + if (!baseUrl) { + throw new AppError('NOT_FOUND', `No upstream configured for ${requestPath}`, 404); + } + + const url = new URL(req.originalUrl, baseUrl); + const headers = new Headers(); + for (const [key, value] of Object.entries(req.headers)) { + if (value == null || HOP_BY_HOP_HEADERS.has(key.toLowerCase())) continue; + if (Array.isArray(value)) { + for (const item of value) headers.append(key, item); + } else { + headers.set(key, value); + } + } + headers.set('x-request-id', req.requestId); + + const upstream = await fetchImpl(url, { + method: req.method, + headers, + body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req, + duplex: req.method === 'GET' || req.method === 'HEAD' ? undefined : 'half', + }); + + copyHeaders(upstream.headers, res); + res.status(upstream.status); + + const buffer = Buffer.from(await upstream.arrayBuffer()); + return res.send(buffer); + } catch (error) { + return next(error); + } +} + +export function createProxyRouter(options = {}) { + const router = Router(); + const fetchImpl = options.fetchImpl || fetch; + + router.use(['/core', '/commands', '/query'], (req, res, next) => forwardRequest(req, res, next, fetchImpl)); + + return router; +} diff --git a/backend/unified-api/src/server.js b/backend/unified-api/src/server.js new file mode 100644 index 00000000..b14a4e88 --- /dev/null +++ b/backend/unified-api/src/server.js @@ -0,0 +1,9 @@ +import { createApp } from './app.js'; + +const port = Number(process.env.PORT || 8080); +const app = createApp(); + +app.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`krow-api-v2 listening on port ${port}`); +}); diff --git a/backend/unified-api/src/services/auth-service.js b/backend/unified-api/src/services/auth-service.js new file mode 100644 index 00000000..5f44dee0 --- /dev/null +++ b/backend/unified-api/src/services/auth-service.js @@ -0,0 +1,157 @@ +import { z } from 'zod'; +import { AppError } from '../lib/errors.js'; +import { withTransaction } from './db.js'; +import { verifyFirebaseToken, revokeUserSessions } from './firebase-auth.js'; +import { deleteAccount, signInWithPassword, signUpWithPassword } from './identity-toolkit.js'; +import { loadActorContext } from './user-context.js'; + +const clientSignInSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), +}); + +const clientSignUpSchema = z.object({ + companyName: z.string().min(2).max(120), + email: z.string().email(), + password: z.string().min(8), + displayName: z.string().min(2).max(120).optional(), +}); + +function slugify(input) { + return input + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 50); +} + +function buildAuthEnvelope(authPayload, context) { + return { + sessionToken: authPayload.idToken, + refreshToken: authPayload.refreshToken, + expiresInSeconds: Number.parseInt(`${authPayload.expiresIn || 3600}`, 10), + user: { + id: context.user?.userId || authPayload.localId, + email: context.user?.email || null, + displayName: context.user?.displayName || null, + phone: context.user?.phone || null, + }, + tenant: context.tenant, + business: context.business, + vendor: context.vendor, + staff: context.staff, + }; +} + +export function parseClientSignIn(body) { + const parsed = clientSignInSchema.safeParse(body || {}); + if (!parsed.success) { + throw new AppError('VALIDATION_ERROR', 'Invalid client sign-in payload', 400, { + issues: parsed.error.issues, + }); + } + return parsed.data; +} + +export function parseClientSignUp(body) { + const parsed = clientSignUpSchema.safeParse(body || {}); + if (!parsed.success) { + throw new AppError('VALIDATION_ERROR', 'Invalid client sign-up payload', 400, { + issues: parsed.error.issues, + }); + } + return parsed.data; +} + +export async function getSessionForActor(actor) { + return loadActorContext(actor.uid); +} + +export async function signInClient(payload, { fetchImpl = fetch } = {}) { + const authPayload = await signInWithPassword(payload, fetchImpl); + const decoded = await verifyFirebaseToken(authPayload.idToken); + const context = await loadActorContext(decoded.uid); + + if (!context.user || !context.business) { + throw new AppError('FORBIDDEN', 'Authenticated user does not have a client business membership', 403, { + uid: decoded.uid, + email: decoded.email || null, + }); + } + + return buildAuthEnvelope(authPayload, context); +} + +export async function signUpClient(payload, { fetchImpl = fetch } = {}) { + const authPayload = await signUpWithPassword(payload, fetchImpl); + + try { + const decoded = await verifyFirebaseToken(authPayload.idToken); + const defaultDisplayName = payload.displayName || payload.companyName; + const tenantSlug = slugify(payload.companyName); + const businessSlug = tenantSlug; + + await withTransaction(async (client) => { + await client.query( + ` + INSERT INTO users (id, email, display_name, status, metadata) + VALUES ($1, $2, $3, 'ACTIVE', '{}'::jsonb) + ON CONFLICT (id) DO UPDATE + SET email = EXCLUDED.email, + display_name = EXCLUDED.display_name, + updated_at = NOW() + `, + [decoded.uid, payload.email, defaultDisplayName] + ); + + const tenantResult = await client.query( + ` + INSERT INTO tenants (slug, name, status, metadata) + VALUES ($1, $2, 'ACTIVE', '{"source":"unified-api-sign-up"}'::jsonb) + RETURNING id, slug, name + `, + [tenantSlug, payload.companyName] + ); + const tenant = tenantResult.rows[0]; + + const businessResult = await client.query( + ` + INSERT INTO businesses ( + tenant_id, slug, business_name, status, contact_name, contact_email, metadata + ) + VALUES ($1, $2, $3, 'ACTIVE', $4, $5, '{"source":"unified-api-sign-up"}'::jsonb) + RETURNING id, slug, business_name + `, + [tenant.id, businessSlug, payload.companyName, defaultDisplayName, payload.email] + ); + const business = businessResult.rows[0]; + + await client.query( + ` + INSERT INTO tenant_memberships (tenant_id, user_id, membership_status, base_role, metadata) + VALUES ($1, $2, 'ACTIVE', 'admin', '{"source":"sign-up"}'::jsonb) + `, + [tenant.id, decoded.uid] + ); + + await client.query( + ` + INSERT INTO business_memberships (tenant_id, business_id, user_id, membership_status, business_role, metadata) + VALUES ($1, $2, $3, 'ACTIVE', 'owner', '{"source":"sign-up"}'::jsonb) + `, + [tenant.id, business.id, decoded.uid] + ); + }); + + const context = await loadActorContext(decoded.uid); + return buildAuthEnvelope(authPayload, context); + } catch (error) { + await deleteAccount({ idToken: authPayload.idToken }, fetchImpl).catch(() => null); + throw error; + } +} + +export async function signOutActor(actor) { + await revokeUserSessions(actor.uid); + return { signedOut: true }; +} diff --git a/backend/unified-api/src/services/db.js b/backend/unified-api/src/services/db.js new file mode 100644 index 00000000..3beaa683 --- /dev/null +++ b/backend/unified-api/src/services/db.js @@ -0,0 +1,87 @@ +import { Pool } from 'pg'; + +let pool; + +function parseIntOrDefault(value, fallback) { + const parsed = Number.parseInt(`${value || fallback}`, 10); + return Number.isFinite(parsed) ? parsed : fallback; +} + +function resolveDatabasePoolConfig() { + if (process.env.DATABASE_URL) { + return { + connectionString: process.env.DATABASE_URL, + max: parseIntOrDefault(process.env.DB_POOL_MAX, 10), + idleTimeoutMillis: parseIntOrDefault(process.env.DB_IDLE_TIMEOUT_MS, 30000), + }; + } + + const user = process.env.DB_USER; + const password = process.env.DB_PASSWORD; + const database = process.env.DB_NAME; + const host = process.env.DB_HOST || ( + process.env.INSTANCE_CONNECTION_NAME + ? `/cloudsql/${process.env.INSTANCE_CONNECTION_NAME}` + : '' + ); + + if (!user || password == null || !database || !host) { + return null; + } + + return { + host, + port: parseIntOrDefault(process.env.DB_PORT, 5432), + user, + password, + database, + max: parseIntOrDefault(process.env.DB_POOL_MAX, 10), + idleTimeoutMillis: parseIntOrDefault(process.env.DB_IDLE_TIMEOUT_MS, 30000), + }; +} + +export function isDatabaseConfigured() { + return Boolean(resolveDatabasePoolConfig()); +} + +function getPool() { + if (!pool) { + const resolved = resolveDatabasePoolConfig(); + if (!resolved) { + throw new Error('Database connection settings are required'); + } + pool = new Pool(resolved); + } + return pool; +} + +export async function query(text, params = []) { + return getPool().query(text, params); +} + +export async function withTransaction(work) { + const client = await getPool().connect(); + try { + await client.query('BEGIN'); + const result = await work(client); + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +export async function checkDatabaseHealth() { + const result = await query('SELECT 1 AS ok'); + return result.rows[0]?.ok === 1; +} + +export async function closePool() { + if (pool) { + await pool.end(); + pool = null; + } +} diff --git a/backend/unified-api/src/services/firebase-auth.js b/backend/unified-api/src/services/firebase-auth.js new file mode 100644 index 00000000..e441b13f --- /dev/null +++ b/backend/unified-api/src/services/firebase-auth.js @@ -0,0 +1,18 @@ +import { applicationDefault, getApps, initializeApp } from 'firebase-admin/app'; +import { getAuth } from 'firebase-admin/auth'; + +function ensureAdminApp() { + if (getApps().length === 0) { + initializeApp({ credential: applicationDefault() }); + } +} + +export async function verifyFirebaseToken(token, { checkRevoked = false } = {}) { + ensureAdminApp(); + return getAuth().verifyIdToken(token, checkRevoked); +} + +export async function revokeUserSessions(uid) { + ensureAdminApp(); + await getAuth().revokeRefreshTokens(uid); +} diff --git a/backend/unified-api/src/services/identity-toolkit.js b/backend/unified-api/src/services/identity-toolkit.js new file mode 100644 index 00000000..0b477e4b --- /dev/null +++ b/backend/unified-api/src/services/identity-toolkit.js @@ -0,0 +1,65 @@ +import { AppError } from '../lib/errors.js'; + +const IDENTITY_TOOLKIT_BASE_URL = 'https://identitytoolkit.googleapis.com/v1'; + +function getApiKey() { + const apiKey = process.env.FIREBASE_WEB_API_KEY; + if (!apiKey) { + throw new AppError('CONFIGURATION_ERROR', 'FIREBASE_WEB_API_KEY is required', 500); + } + return apiKey; +} + +async function callIdentityToolkit(path, payload, fetchImpl = fetch) { + const response = await fetchImpl(`${IDENTITY_TOOLKIT_BASE_URL}/${path}?key=${getApiKey()}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + const json = await response.json().catch(() => ({})); + if (!response.ok) { + throw new AppError( + 'AUTH_PROVIDER_ERROR', + json?.error?.message || `Identity Toolkit request failed: ${path}`, + response.status, + { provider: 'firebase-identity-toolkit', path } + ); + } + + return json; +} + +export async function signInWithPassword({ email, password }, fetchImpl = fetch) { + return callIdentityToolkit( + 'accounts:signInWithPassword', + { + email, + password, + returnSecureToken: true, + }, + fetchImpl + ); +} + +export async function signUpWithPassword({ email, password }, fetchImpl = fetch) { + return callIdentityToolkit( + 'accounts:signUp', + { + email, + password, + returnSecureToken: true, + }, + fetchImpl + ); +} + +export async function deleteAccount({ idToken }, fetchImpl = fetch) { + return callIdentityToolkit( + 'accounts:delete', + { idToken }, + fetchImpl + ); +} diff --git a/backend/unified-api/src/services/user-context.js b/backend/unified-api/src/services/user-context.js new file mode 100644 index 00000000..6262e886 --- /dev/null +++ b/backend/unified-api/src/services/user-context.js @@ -0,0 +1,91 @@ +import { query } from './db.js'; + +export async function loadActorContext(uid) { + const [userResult, tenantResult, businessResult, vendorResult, staffResult] = await Promise.all([ + query( + ` + SELECT id AS "userId", email, display_name AS "displayName", phone, status + FROM users + WHERE id = $1 + `, + [uid] + ), + query( + ` + SELECT tm.id AS "membershipId", + tm.tenant_id AS "tenantId", + tm.base_role AS role, + t.name AS "tenantName", + t.slug AS "tenantSlug" + FROM tenant_memberships tm + JOIN tenants t ON t.id = tm.tenant_id + WHERE tm.user_id = $1 + AND tm.membership_status = 'ACTIVE' + ORDER BY tm.created_at ASC + LIMIT 1 + `, + [uid] + ), + query( + ` + SELECT bm.id AS "membershipId", + bm.business_id AS "businessId", + bm.business_role AS role, + b.business_name AS "businessName", + b.slug AS "businessSlug", + bm.tenant_id AS "tenantId" + FROM business_memberships bm + JOIN businesses b ON b.id = bm.business_id + WHERE bm.user_id = $1 + AND bm.membership_status = 'ACTIVE' + ORDER BY bm.created_at ASC + LIMIT 1 + `, + [uid] + ), + query( + ` + SELECT vm.id AS "membershipId", + vm.vendor_id AS "vendorId", + vm.vendor_role AS role, + v.company_name AS "vendorName", + v.slug AS "vendorSlug", + vm.tenant_id AS "tenantId" + FROM vendor_memberships vm + JOIN vendors v ON v.id = vm.vendor_id + WHERE vm.user_id = $1 + AND vm.membership_status = 'ACTIVE' + ORDER BY vm.created_at ASC + LIMIT 1 + `, + [uid] + ), + query( + ` + SELECT s.id AS "staffId", + s.tenant_id AS "tenantId", + s.full_name AS "fullName", + s.primary_role AS "primaryRole", + s.onboarding_status AS "onboardingStatus", + s.status, + w.id AS "workforceId", + w.vendor_id AS "vendorId", + w.workforce_number AS "workforceNumber" + FROM staffs s + LEFT JOIN workforce w ON w.staff_id = s.id + WHERE s.user_id = $1 + ORDER BY s.created_at ASC + LIMIT 1 + `, + [uid] + ), + ]); + + return { + user: userResult.rows[0] || null, + tenant: tenantResult.rows[0] || null, + business: businessResult.rows[0] || null, + vendor: vendorResult.rows[0] || null, + staff: staffResult.rows[0] || null, + }; +} diff --git a/backend/unified-api/test/app.test.js b/backend/unified-api/test/app.test.js new file mode 100644 index 00000000..144594ef --- /dev/null +++ b/backend/unified-api/test/app.test.js @@ -0,0 +1,112 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import request from 'supertest'; +import { createApp } from '../src/app.js'; + +process.env.AUTH_BYPASS = 'true'; + +test('GET /healthz returns healthy response', async () => { + const app = createApp(); + const res = await request(app).get('/healthz'); + + assert.equal(res.status, 200); + assert.equal(res.body.ok, true); + assert.equal(res.body.service, 'krow-api-v2'); +}); + +test('GET /readyz reports database not configured when env is absent', async () => { + delete process.env.DATABASE_URL; + delete process.env.DB_HOST; + delete process.env.DB_NAME; + delete process.env.DB_USER; + delete process.env.DB_PASSWORD; + delete process.env.INSTANCE_CONNECTION_NAME; + + const app = createApp(); + const res = await request(app).get('/readyz'); + + assert.equal(res.status, 503); + assert.equal(res.body.status, 'DATABASE_NOT_CONFIGURED'); +}); + +test('POST /auth/client/sign-in validates payload', async () => { + const app = createApp(); + const res = await request(app).post('/auth/client/sign-in').send({ + email: 'bad-email', + password: 'short', + }); + + assert.equal(res.status, 400); + assert.equal(res.body.code, 'VALIDATION_ERROR'); +}); + +test('POST /auth/client/sign-in returns injected auth envelope', async () => { + const app = createApp({ + authService: { + parseClientSignIn: (body) => body, + parseClientSignUp: (body) => body, + signInClient: async () => ({ + sessionToken: 'token', + refreshToken: 'refresh', + expiresInSeconds: 3600, + user: { id: 'u1', email: 'legendary@krowd.com' }, + tenant: { tenantId: 't1' }, + business: { businessId: 'b1' }, + }), + signUpClient: async () => assert.fail('signUpClient should not be called'), + signOutActor: async () => ({ signedOut: true }), + getSessionForActor: async () => ({ user: { userId: 'u1' } }), + }, + }); + + const res = await request(app).post('/auth/client/sign-in').send({ + email: 'legendary@krowd.com', + password: 'super-secret', + }); + + assert.equal(res.status, 200); + assert.equal(res.body.sessionToken, 'token'); + assert.equal(res.body.business.businessId, 'b1'); +}); + +test('GET /auth/session returns injected session for authenticated actor', async () => { + const app = createApp({ + authService: { + parseClientSignIn: (body) => body, + parseClientSignUp: (body) => body, + signInClient: async () => assert.fail('signInClient should not be called'), + signUpClient: async () => assert.fail('signUpClient should not be called'), + signOutActor: async () => ({ signedOut: true }), + getSessionForActor: async (actor) => ({ actorUid: actor.uid }), + }, + }); + + const res = await request(app) + .get('/auth/session') + .set('Authorization', 'Bearer test-token'); + + assert.equal(res.status, 200); + assert.equal(res.body.actorUid, 'test-user'); +}); + +test('proxy forwards query routes to query base url', async () => { + process.env.QUERY_API_BASE_URL = 'https://query.example'; + process.env.CORE_API_BASE_URL = 'https://core.example'; + process.env.COMMAND_API_BASE_URL = 'https://command.example'; + + let seenUrl = null; + const app = createApp({ + fetchImpl: async (url) => { + seenUrl = `${url}`; + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + }, + }); + + const res = await request(app).get('/query/test-route?foo=bar'); + + assert.equal(res.status, 200); + assert.equal(seenUrl, 'https://query.example/query/test-route?foo=bar'); +}); diff --git a/docs/BACKEND/API_GUIDES/V2/README.md b/docs/BACKEND/API_GUIDES/V2/README.md index 4a5a66c8..1a452753 100644 --- a/docs/BACKEND/API_GUIDES/V2/README.md +++ b/docs/BACKEND/API_GUIDES/V2/README.md @@ -2,31 +2,43 @@ This is the frontend-facing source of truth for the v2 backend. -If you are building against the new backend, start here. +## 1) Frontend entrypoint -## 1) Which service to use +Frontend should target one public base URL: -| Use case | Service | +```env +API_V2_BASE_URL= +``` + +The unified v2 gateway exposes: + +- `/auth/*` +- `/core/*` +- `/commands/*` +- `/query/*` +- `/query/client/*` +- `/query/staff/*` + +Internal services still stay separate behind that gateway. + +## 2) Internal service split + +| Use case | Internal service | | --- | --- | -| File upload, signed URLs, model calls, rapid order helpers, verification flows | `core-api-v2` | +| File upload, signed URLs, model calls, verification helpers | `core-api-v2` | | Business writes and workflow actions | `command-api-v2` | -| Screen reads for the implemented v2 views | `query-api-v2` | - -## 2) Live dev base URLs - -- Core API: `https://krow-core-api-v2-e3g6witsvq-uc.a.run.app` -- Command API: `https://krow-command-api-v2-e3g6witsvq-uc.a.run.app` -- Query API: `https://krow-query-api-v2-e3g6witsvq-uc.a.run.app` +| Screen reads and mobile read models | `query-api-v2` | +| Frontend-facing single host and auth wrappers | `krow-api-v2` | ## 3) Auth and headers -All protected routes require: +Protected routes require: ```http Authorization: Bearer ``` -All command routes also require: +Command routes also require: ```http Idempotency-Key: @@ -43,78 +55,88 @@ All services return the same error envelope: } ``` -## 4) What frontend can use now +## 4) What frontend can use now on this branch -### Ready now +### Unified gateway -- `core-api-v2` - - upload file - - create signed URL - - invoke model - - rapid order transcribe - - rapid order parse - - create verification - - get verification - - review verification - - retry verification -- `command-api-v2` - - create order - - update order - - cancel order - - assign staff to shift - - accept shift - - change shift status - - clock in - - clock out - - favorite and unfavorite staff - - create staff review -- `query-api-v2` - - order list - - order detail - - favorite staff list - - staff review summary - - assignment attendance detail +- `POST /auth/client/sign-in` +- `POST /auth/client/sign-up` +- `POST /auth/sign-out` +- `POST /auth/client/sign-out` +- `POST /auth/staff/sign-out` +- `GET /auth/session` +- Proxy access to `/core/*`, `/commands/*`, `/query/*` -### Do not move yet +### Client read routes -- reports -- payments and finance screens -- undocumented dashboard reads -- undocumented scheduling reads and writes -- any flow that assumes verification history is durable in SQL +- `GET /query/client/session` +- `GET /query/client/dashboard` +- `GET /query/client/reorders` +- `GET /query/client/billing/accounts` +- `GET /query/client/billing/invoices/pending` +- `GET /query/client/billing/invoices/history` +- `GET /query/client/billing/current-bill` +- `GET /query/client/billing/savings` +- `GET /query/client/billing/spend-breakdown` +- `GET /query/client/coverage` +- `GET /query/client/coverage/stats` +- `GET /query/client/hubs` +- `GET /query/client/cost-centers` +- `GET /query/client/vendors` +- `GET /query/client/vendors/:vendorId/roles` +- `GET /query/client/hubs/:hubId/managers` +- `GET /query/client/orders/view` -## 5) Important caveat +### Staff read routes -`core-api-v2` is usable now, but verification job state is not yet persisted to `krow-sql-v2`. +- `GET /query/staff/session` +- `GET /query/staff/dashboard` +- `GET /query/staff/profile-completion` +- `GET /query/staff/availability` +- `GET /query/staff/clock-in/shifts/today` +- `GET /query/staff/clock-in/status` +- `GET /query/staff/payments/summary` +- `GET /query/staff/payments/history` +- `GET /query/staff/payments/chart` +- `GET /query/staff/shifts/assigned` +- `GET /query/staff/shifts/open` +- `GET /query/staff/shifts/pending` +- `GET /query/staff/shifts/cancelled` +- `GET /query/staff/shifts/completed` +- `GET /query/staff/shifts/:shiftId` +- `GET /query/staff/profile/sections` +- `GET /query/staff/profile/personal-info` +- `GET /query/staff/profile/industries` +- `GET /query/staff/profile/skills` +- `GET /query/staff/profile/documents` +- `GET /query/staff/profile/certificates` +- `GET /query/staff/profile/bank-accounts` +- `GET /query/staff/profile/benefits` -What is durable today: -- uploaded files in Google Cloud Storage -- generated signed URLs -- model invocation itself +### Existing v2 routes still valid -What is not yet durable: -- verification job history -- verification review history -- verification event history +- `/core/*` routes documented in `core-api.md` +- `/commands/*` routes documented in `command-api.md` +- `/query/tenants/*` routes documented in `query-api.md` -That means frontend can integrate with verification routes now, but should not treat them as mission-critical durable state yet. +## 5) Remaining gaps after this slice -## 6) Recommended frontend environment variables +Still not implemented yet: -```env -CORE_API_V2_BASE_URL=https://krow-core-api-v2-e3g6witsvq-uc.a.run.app -COMMAND_API_V2_BASE_URL=https://krow-command-api-v2-e3g6witsvq-uc.a.run.app -QUERY_API_V2_BASE_URL=https://krow-query-api-v2-e3g6witsvq-uc.a.run.app -``` +- staff phone OTP wrapper endpoints +- hub write flows +- hub NFC assignment write route +- invoice approve and dispute commands +- staff apply / decline / request swap commands +- staff profile update commands +- availability write commands +- reports suite +- durable verification persistence in `core-api-v2` -## 7) Service docs +## 6) Docs +- [Unified API](./unified-api.md) - [Core API](./core-api.md) - [Command API](./command-api.md) - [Query API](./query-api.md) - -## 8) Frontend integration rule - -Do not point screens directly at database access just because a route does not exist yet. - -If a screen is missing from the docs, the next step is to define the route contract and add it to `query-api-v2` or `command-api-v2`. +- [Mobile gap analysis](./mobile-api-gap-analysis.md) diff --git a/docs/BACKEND/API_GUIDES/V2/mobile-api-gap-analysis.md b/docs/BACKEND/API_GUIDES/V2/mobile-api-gap-analysis.md new file mode 100644 index 00000000..97e5e7e9 --- /dev/null +++ b/docs/BACKEND/API_GUIDES/V2/mobile-api-gap-analysis.md @@ -0,0 +1,66 @@ +# Mobile API Gap Analysis + +Source compared against implementation: + +- `/Users/wiel/Downloads/mobile-backend-api-specification.md` + +## Implemented in this slice + +- unified frontend-facing base URL design +- client auth wrapper for email/password sign-in and sign-up +- auth session and sign-out endpoints +- client read surface for dashboard, billing, coverage, hubs, vendor lookup, and date-range order items +- staff read surface for dashboard, availability, clock-in reads, payments, shifts, and profile sections +- schema support for: + - cost centers + - hub managers + - recurring staff availability + - staff benefits +- seed support for: + - authenticated demo staff user + - cost center and hub manager data + - staff benefits and availability + - attire and tax-form example documents + +## Still missing + +### Auth + +- staff phone OTP start +- staff OTP verify +- staff profile setup endpoint + +### Client writes + +- hub create +- hub update +- hub delete +- hub NFC assignment +- assign manager to hub +- invoice approve +- invoice dispute + +### Staff writes + +- availability update +- availability quick set +- shift apply +- shift decline +- request swap +- personal info update +- preferred locations update +- profile photo upload wrapper + +### Reports + +- report summary +- daily ops +- spend +- coverage +- forecast +- performance +- no-show + +### Core persistence + +- `core-api-v2` verification jobs still need durable SQL persistence diff --git a/docs/BACKEND/API_GUIDES/V2/unified-api.md b/docs/BACKEND/API_GUIDES/V2/unified-api.md new file mode 100644 index 00000000..9b20f022 --- /dev/null +++ b/docs/BACKEND/API_GUIDES/V2/unified-api.md @@ -0,0 +1,50 @@ +# Unified API V2 + +This service exists so frontend can use one base URL without forcing backend into one codebase. + +## Base idea + +Frontend talks to one service: + +- `krow-api-v2` + +That gateway does two things: + +1. exposes auth/session endpoints +2. forwards requests to the right internal v2 service + +## Route groups + +### Auth + +- `POST /auth/client/sign-in` +- `POST /auth/client/sign-up` +- `POST /auth/sign-out` +- `POST /auth/client/sign-out` +- `POST /auth/staff/sign-out` +- `GET /auth/session` + +### Proxy passthrough + +- `/core/*` -> `core-api-v2` +- `/commands/*` -> `command-api-v2` +- `/query/*` -> `query-api-v2` + +### Mobile read models + +These are served by `query-api-v2` but frontend should still call them through the unified host: + +- `/query/client/*` +- `/query/staff/*` + +## Why this shape + +- frontend gets one base URL +- backend keeps separate read, write, and service helpers +- we can scale or refactor internals later without breaking frontend paths + +## Current auth note + +Client email/password auth is wrapped here. + +Staff phone OTP is not wrapped here yet. That still needs its own proper provider-backed implementation rather than a fake backend OTP flow. diff --git a/makefiles/backend.mk b/makefiles/backend.mk index f2a1a5dc..c0032b14 100644 --- a/makefiles/backend.mk +++ b/makefiles/backend.mk @@ -40,12 +40,14 @@ BACKEND_V2_ARTIFACT_REPO ?= krow-backend-v2 BACKEND_V2_CORE_SERVICE_NAME ?= krow-core-api-v2 BACKEND_V2_COMMAND_SERVICE_NAME ?= krow-command-api-v2 BACKEND_V2_QUERY_SERVICE_NAME ?= krow-query-api-v2 +BACKEND_V2_UNIFIED_SERVICE_NAME ?= krow-api-v2 BACKEND_V2_RUNTIME_SA_NAME ?= krow-backend-v2-runtime BACKEND_V2_RUNTIME_SA_EMAIL := $(BACKEND_V2_RUNTIME_SA_NAME)@$(GCP_PROJECT_ID).iam.gserviceaccount.com BACKEND_V2_CORE_DIR ?= backend/core-api BACKEND_V2_COMMAND_DIR ?= backend/command-api BACKEND_V2_QUERY_DIR ?= backend/query-api +BACKEND_V2_UNIFIED_DIR ?= backend/unified-api BACKEND_V2_SQL_INSTANCE ?= krow-sql-v2 BACKEND_V2_SQL_DATABASE ?= krow_v2_db @@ -72,8 +74,10 @@ endif BACKEND_V2_CORE_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/core-api-v2:latest BACKEND_V2_COMMAND_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/command-api-v2:latest BACKEND_V2_QUERY_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/query-api-v2:latest +BACKEND_V2_UNIFIED_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_V2_ARTIFACT_REPO)/unified-api-v2:latest +BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET ?= firebase-web-api-key -.PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core backend-bootstrap-v2-dev backend-deploy-core-v2 backend-deploy-commands-v2 backend-deploy-query-v2 backend-smoke-core-v2 backend-smoke-commands-v2 backend-smoke-query-v2 backend-logs-core-v2 backend-v2-migrate-idempotency backend-v2-migrate-schema +.PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core backend-bootstrap-v2-dev backend-deploy-core-v2 backend-deploy-commands-v2 backend-deploy-query-v2 backend-deploy-unified-v2 backend-smoke-core-v2 backend-smoke-commands-v2 backend-smoke-query-v2 backend-smoke-unified-v2 backend-logs-core-v2 backend-v2-migrate-idempotency backend-v2-migrate-schema backend-help: @echo "--> Backend Foundation Commands" @@ -92,11 +96,13 @@ backend-help: @echo " make backend-deploy-core-v2 [ENV=dev] Build + deploy core API v2 service" @echo " make backend-deploy-commands-v2 [ENV=dev] Build + deploy command API v2 service" @echo " make backend-deploy-query-v2 [ENV=dev] Build + deploy query API v2 service" + @echo " make backend-deploy-unified-v2 [ENV=dev] Build + deploy unified API v2 gateway" @echo " make backend-v2-migrate-schema Apply v2 domain schema against krow-sql-v2" @echo " make backend-v2-migrate-idempotency Apply command idempotency migration against v2 DB" @echo " make backend-smoke-core-v2 [ENV=dev] Smoke test core API v2 /health" @echo " make backend-smoke-commands-v2 [ENV=dev] Smoke test command API v2 /health" @echo " make backend-smoke-query-v2 [ENV=dev] Smoke test query API v2 /health" + @echo " make backend-smoke-unified-v2 [ENV=dev] Smoke test unified API v2 /health" @echo " make backend-logs-core-v2 [ENV=dev] Read core API v2 logs" backend-enable-apis: @@ -385,6 +391,33 @@ backend-deploy-query-v2: $(BACKEND_V2_RUN_AUTH_FLAG) @echo "✅ Query backend v2 service deployed." +backend-deploy-unified-v2: + @echo "--> Deploying unified backend v2 gateway [$(BACKEND_V2_UNIFIED_SERVICE_NAME)] to [$(ENV)]..." + @test -d $(BACKEND_V2_UNIFIED_DIR) || (echo "❌ Missing directory: $(BACKEND_V2_UNIFIED_DIR)" && exit 1) + @test -f $(BACKEND_V2_UNIFIED_DIR)/Dockerfile || (echo "❌ Missing Dockerfile: $(BACKEND_V2_UNIFIED_DIR)/Dockerfile" && exit 1) + @if ! gcloud secrets describe $(BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ + echo "❌ Missing secret: $(BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET)"; \ + exit 1; \ + fi + @CORE_URL=$$(gcloud run services describe $(BACKEND_V2_CORE_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ + COMMAND_URL=$$(gcloud run services describe $(BACKEND_V2_COMMAND_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ + QUERY_URL=$$(gcloud run services describe $(BACKEND_V2_QUERY_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ + if [ -z "$$CORE_URL" ] || [ -z "$$COMMAND_URL" ] || [ -z "$$QUERY_URL" ]; then \ + echo "❌ Core, command, and query v2 services must be deployed before unified gateway"; \ + exit 1; \ + fi; \ + gcloud builds submit $(BACKEND_V2_UNIFIED_DIR) --tag $(BACKEND_V2_UNIFIED_IMAGE) --project=$(GCP_PROJECT_ID); \ + gcloud run deploy $(BACKEND_V2_UNIFIED_SERVICE_NAME) \ + --image=$(BACKEND_V2_UNIFIED_IMAGE) \ + --region=$(BACKEND_REGION) \ + --project=$(GCP_PROJECT_ID) \ + --service-account=$(BACKEND_V2_RUNTIME_SA_EMAIL) \ + --set-env-vars=APP_ENV=$(ENV),APP_STACK=v2,GCP_PROJECT_ID=$(GCP_PROJECT_ID),INSTANCE_CONNECTION_NAME=$(BACKEND_V2_SQL_CONNECTION_NAME),DB_NAME=$(BACKEND_V2_SQL_DATABASE),DB_USER=$(BACKEND_V2_SQL_APP_USER),CORE_API_BASE_URL=$$CORE_URL,COMMAND_API_BASE_URL=$$COMMAND_URL,QUERY_API_BASE_URL=$$QUERY_URL \ + --set-secrets=DB_PASSWORD=$(BACKEND_V2_SQL_PASSWORD_SECRET):latest,FIREBASE_WEB_API_KEY=$(BACKEND_V2_FIREBASE_WEB_API_KEY_SECRET):latest \ + --add-cloudsql-instances=$(BACKEND_V2_SQL_CONNECTION_NAME) \ + $(BACKEND_V2_RUN_AUTH_FLAG) + @echo "✅ Unified backend v2 gateway deployed." + backend-v2-migrate-idempotency: @echo "--> Applying idempotency table migration for command API v2..." @test -n "$(IDEMPOTENCY_DATABASE_URL)$(DATABASE_URL)" || (echo "❌ IDEMPOTENCY_DATABASE_URL or DATABASE_URL is required" && exit 1) @@ -427,6 +460,16 @@ backend-smoke-query-v2: TOKEN=$$(gcloud auth print-identity-token); \ curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/readyz" >/dev/null && echo "✅ Query v2 smoke check passed: $$URL/readyz" +backend-smoke-unified-v2: + @echo "--> Running unified v2 smoke check..." + @URL=$$(gcloud run services describe $(BACKEND_V2_UNIFIED_SERVICE_NAME) --region=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID) --format='value(status.url)'); \ + if [ -z "$$URL" ]; then \ + echo "❌ Could not resolve URL for service $(BACKEND_V2_UNIFIED_SERVICE_NAME)"; \ + exit 1; \ + fi; \ + TOKEN=$$(gcloud auth print-identity-token); \ + curl -fsS -H "Authorization: Bearer $$TOKEN" "$$URL/readyz" >/dev/null && echo "✅ Unified v2 smoke check passed: $$URL/readyz" + backend-logs-core-v2: @echo "--> Reading logs for core backend v2 service [$(BACKEND_V2_CORE_SERVICE_NAME)]..." @gcloud run services logs read $(BACKEND_V2_CORE_SERVICE_NAME) \