/** * sweep.mjs — confirm real API response shapes WITHOUT running the app and * WITHOUT leaking your personal data. * * It logs into the ParkSmarter API with the library, calls the safe READ-ONLY * endpoints, and writes a "schema skeleton" (field names + value *types* only, * never the values) to ./capture/. Share ./capture/ freely — it contains no PII. * * It NEVER calls anything that spends money or mutates your account * (no start-session, add/update/delete card or vehicle, profile edits, etc.). * * Setup: * 1) npm run build (already done if dist/ exists) * 2) Create ./.creds.json (gitignored): * { * "phoneNumber": "5551234567", // digits only, no country code * "password": "…", * "environment": "prodv2", // optional * "lat": 40.4406, // optional: a coordinate near real meters * "lng": -79.9959 // optional * } * 3) node sweep.mjs */ import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'; import { ParkSmarterClient } from './dist/index.js'; /* ---- load creds (file or env) ---- */ let creds = {}; if (existsSync('./.creds.json')) { creds = JSON.parse(readFileSync('./.creds.json', 'utf8')); } const phoneNumber = creds.phoneNumber ?? process.env.PS_PHONE; const password = creds.password ?? process.env.PS_PASS; const environment = creds.environment ?? process.env.PS_ENV ?? 'prodv2'; const lat = creds.lat ?? (process.env.PS_LAT ? Number(process.env.PS_LAT) : undefined); const lng = creds.lng ?? (process.env.PS_LNG ? Number(process.env.PS_LNG) : undefined); if (!phoneNumber || !password) { console.error( 'Missing credentials. Create ./.creds.json {phoneNumber, password} or set PS_PHONE/PS_PASS.', ); process.exit(1); } /* ---- schema skeleton: keep KEYS and TYPES, drop VALUES (no PII) ---- */ function skeleton(v, depth = 0) { if (v === null) return 'null'; if (Array.isArray(v)) { if (v.length === 0) return ['']; // merge keys across up to 5 elements so we don't miss sparse fields const sample = v.slice(0, 5).map((e) => skeleton(e, depth + 1)); if (typeof sample[0] === 'object' && sample[0] !== null) { const merged = {}; for (const s of sample) Object.assign(merged, s); return [merged, ``]; } return [sample[0], ``]; } if (typeof v === 'object') { const out = {}; for (const k of Object.keys(v).sort()) out[k] = skeleton(v[k], depth + 1); return out; } // primitives: report type only, plus a coarse hint for strings if (typeof v === 'string') { if (/^\d{4}-\d{2}-\d{2}/.test(v)) return 'string'; if (/^-?\d+(\.\d+)?$/.test(v)) return 'string'; return 'string'; } return typeof v; // number | boolean } const capDir = './capture'; mkdirSync(capDir, { recursive: true }); const ps = new ParkSmarterClient({ environment, timeoutMs: 20000 }); const results = {}; async function grab(name, fn) { try { const data = await fn(); const skel = skeleton(data); results[name] = { ok: true, schema: skel }; writeFileSync(`${capDir}/${name}.json`, JSON.stringify(skel, null, 2)); const top = skel && typeof skel === 'object' ? Object.keys(skel) : skel; console.log(`✓ ${name}:`, JSON.stringify(top)); } catch (e) { results[name] = { ok: false, error: `${e.name} ${e.status ?? ''} ${e.message}` }; console.log(`✗ ${name}: ${e.name} ${e.status ?? ''} ${e.message}`); } } /* ---- run ---- */ console.log(`\nEnvironment: ${environment}`); await grab('applicationValidity', () => ps.getApplicationValidity()); console.log('\nLogging in…'); try { const auth = await ps.loginWithPhone({ phoneNumber, password }); results.__login = { ok: true, schema: skeleton(auth) }; writeFileSync(`${capDir}/_authResponse.json`, JSON.stringify(skeleton(auth), null, 2)); console.log('✓ login: keys =', JSON.stringify(Object.keys(auth))); } catch (e) { console.log(`✗ login FAILED: ${e.name} ${e.status ?? ''} ${e.message}`); console.log(' (a 201 here means the account needs verification — see README note.)'); process.exit(1); } /* authenticated READ-ONLY sweep */ await grab('userDetail', () => ps.getUserDetail()); await grab('notificationSettings', () => ps.getNotificationSettings()); await grab('activeSessions', () => ps.getActiveParkingSessions()); await grab('pastSessions', () => ps.getPastParkingSessions({ currentPage: 1, pageSize: 10 })); await grab('parkingLots', () => ps.getParkingLots()); await grab('states', () => ps.getStates()); await grab('states_withId', () => ps.getStates(1)); let firstZone = null; if (lat != null && lng != null) { const meters = await (async () => { try { return await ps.getMetersByLocation({ latitude: lat, longitude: lng }); } catch (e) { results.metersByLocation = { ok: false, error: `${e.name} ${e.status ?? ''} ${e.message}` }; console.log(`✗ metersByLocation: ${e.name} ${e.status ?? ''} ${e.message}`); return null; } })(); if (meters) { writeFileSync(`${capDir}/metersByLocation.json`, JSON.stringify(skeleton(meters), null, 2)); results.metersByLocation = { ok: true, schema: skeleton(meters) }; console.log('✓ metersByLocation:', JSON.stringify(Object.keys(meters))); firstZone = meters?.Zones?.[0] ?? null; } await grab('limitedMetersByLocation', () => ps.getLimitedMetersByLocation({ latitude: lat, longitude: lng }), ); } else { console.log('\n(skip meters-by-location: add "lat"/"lng" to .creds.json to include them)'); } /* estimates are read-only price quotes — try them from a real zone + the user's vehicle */ try { const me = await ps.getUserDetail(); const vehicleId = me?.VehicleDetails?.[0]?.VehicleID; const zone = firstZone; const space = zone?.Spaces?.[0]; const zoneId = zone?.ZoneId ?? zone?.ZoneID; const spaceId = space?.SpaceID ?? space?.SpaceId; const customerId = zone?.CustomerID ?? zone?.CustomerId; if (zoneId != null && spaceId != null && customerId != null && vehicleId != null) { const common = { zoneId, spaceId, customerId, vehicleId }; await grab('estimateItems', () => ps.getParkingEstimateItems(common)); await grab('estimateMulti', () => ps.getParkingEstimateMulti(common)); await grab('estimateSingle', () => ps.getParkingEstimateSingle({ ...common, durationInMinutes: 60, creditCardId: 0 }), ); } else { console.log('(skip estimates: no zone/space/vehicle available to quote against)'); } } catch (e) { console.log(`(skip estimates: ${e.message})`); } /* try a receipt shape from a past session id, if any (read-only) */ try { const past = await ps.getPastParkingSessions({ currentPage: 1, pageSize: 1 }); const tid = past?.Session?.[0]?.TransactionID; if (tid != null) await grab('parkingReceipt', () => ps.getParkingReceipt(tid)); } catch {} /* public content shapes */ await grab('states_public', () => ps.getStates()); writeFileSync(`${capDir}/_summary.json`, JSON.stringify(results, null, 2)); console.log('\nDone. Schema skeletons (no PII) written to ./capture/. Share that folder with me.');