Initial commit: parksmarter-client + BigBrainParking app

Reverse-engineered ParkSmarter API client (TypeScript, live-verified) plus a
de-Googled Expo/React Native app for GrapheneOS: MapLibre meter map with GPS,
VisionCamera QR kiosk scanning with save/share, local session-expiry reminders,
UnifiedPush wiring, and Gitea CI to publish signed APKs for Obtainium.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-06 08:33:47 -07:00
commit 1dede50995
39 changed files with 3671 additions and 0 deletions

View file

@ -0,0 +1,176 @@
/**
* 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 ['<empty>'];
// 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, `<len:${v.length}>`];
}
return [sample[0], `<len:${v.length}>`];
}
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<date>';
if (/^-?\d+(\.\d+)?$/.test(v)) return 'string<numeric>';
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.');