// Drop-in Dart client library for the KI BMS HTTP API. // // Save this file under your project as `lib/ats_client.dart` and // import it directly: // // import 'package:my_project/ats_client.dart'; // // final c = AtsClient('pat_...'); // final rows = await c.accountList(opts: ListOpts(limit: 20, sort: '-created_at')); // final fresh = await c.accountCreate({{'name': 'Example GmbH'}}); // // Every endpoint exposed by the HTTP API is wrapped as a typed // `` method on AtsClient. List endpoints take an optional // ListOpts; get/update/delete endpoints take the row id as the first // argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets Dart 3.0+; uses only the platform stdlib (`dart:io`, // `dart:convert`, `dart:async`). // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; // ── Identity (substituted at generation time) ──────────────────────── const String appSlug = 'ats'; const String appName = 'KI BMS'; const String moduleName = 'ats_client'; const String clientVersion = '0.3.13'; const String language = 'dart'; const String _defaultBase = 'https://ki-bewerber-management.de'; /// Per-type metadata baked at generation time. Decoded once on first /// access; useful at runtime when calling code needs to know the legal /// filters / sort columns / max_limit for a model without a second /// round-trip. final Map types = json.decode(r'''{"application":{"ops":["list","read","create","update","delete"],"create_fields":["job_id","candidate_id","stage","previous_stage","position","applied_at","last_stage_at","source_id","source_label","cover_letter","cv_blob_id","cv_url","answers","fit_score","fit_reasoning","fit_flags","fit_computed_at","rejected_reason","rejected_note","tags"],"update_fields":["stage","previous_stage","position","applied_at","last_stage_at","source_id","source_label","cover_letter","cv_blob_id","cv_url","answers","fit_score","fit_reasoning","fit_flags","fit_computed_at","rejected_reason","rejected_note","tags"],"allowed_filters":["data__job_id","data__candidate_id","data__stage","data__source_id","data__rejected_reason","data__is_archived","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__applied_at","data__fit_score","data__position","data__last_stage_at"],"default_sort":"data__position","max_limit":500,"fields":[{"name":"tags","type":"tags"},{"name":"stage","type":"enum","values":["new","review","screening","interview","offer","hired","rejected","talent_pool"]},{"name":"cv_url","type":"url","max_len":2048},{"name":"job_id","type":"string","max_len":64,"ref":{"type":"job","owned":false,"optional":false}},{"name":"answers","type":"list"},{"name":"position","type":"number"},{"name":"fit_flags","type":"tags"},{"name":"fit_score","type":"number"},{"name":"source_id","type":"string","max_len":64,"ref":{"type":"source","owned":false,"optional":false}},{"name":"applied_at","type":"string","max_len":32},{"name":"cv_blob_id","type":"string","max_len":64},{"name":"candidate_id","type":"string","max_len":64,"ref":{"type":"candidate","owned":false,"optional":false}},{"name":"cover_letter","type":"string","max_len":16000},{"name":"source_label","type":"string","max_len":200},{"name":"fit_reasoning","type":"string","max_len":4000},{"name":"last_stage_at","type":"string","max_len":32},{"name":"rejected_note","type":"string","max_len":2000},{"name":"previous_stage","type":"string","max_len":32},{"name":"fit_computed_at","type":"string","max_len":32},{"name":"rejected_reason","type":"enum","values":["not_qualified","salary_mismatch","location_mismatch","culture_mismatch","withdrew","ghosted","filled_internally","duplicate","other"]}]},"application_note":{"ops":["list","read","create","update","delete"],"create_fields":["body","pinned","private","parent_kind","parent_id"],"update_fields":["body","pinned","private"],"allowed_filters":["data__parent_id","data__parent_kind","data__pinned","data__private","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":8000},{"name":"pinned","type":"bool"},{"name":"private","type":"bool"},{"name":"parent_id","type":"string","max_len":64},{"name":"parent_kind","type":"enum","values":["candidate","application","job"]}]},"candidate":{"ops":["list","read","create","update","delete"],"create_fields":["name","first_name","last_name","salutation","pronouns","email","phone","city","country","current_company","current_role","years_experience","available_from","salary_expectation","currency","linkedin","github","portfolio","cv_url","cv_blob_id","avatar_blob_id","summary","skills","languages","tags","source_id","source_label","pool_status","gdpr_consent","gdpr_consent_at","gdpr_retention_until","preferred_locale","last_touched_at","color"],"update_fields":["name","first_name","last_name","salutation","pronouns","email","phone","city","country","current_company","current_role","years_experience","available_from","salary_expectation","currency","linkedin","github","portfolio","cv_url","cv_blob_id","avatar_blob_id","summary","skills","languages","tags","source_id","source_label","pool_status","gdpr_consent","gdpr_consent_at","gdpr_retention_until","preferred_locale","last_touched_at","color"],"allowed_filters":["data__email","data__name","data__location","data__country","data__source_id","data__tags","data__skills","data__pool_status","data__gdpr_consent","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__name","data__last_touched_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"city","type":"string","max_len":120},{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"email","type":"string","max_len":320},{"name":"phone","type":"string","max_len":64},{"name":"cv_url","type":"url","max_len":2048},{"name":"github","type":"url","max_len":2048},{"name":"skills","type":"tags"},{"name":"country","type":"string","max_len":120},{"name":"summary","type":"string","max_len":4000},{"name":"currency","type":"string","max_len":8},{"name":"linkedin","type":"url","max_len":2048},{"name":"pronouns","type":"string","max_len":32},{"name":"languages","type":"tags"},{"name":"last_name","type":"string","max_len":120},{"name":"portfolio","type":"url","max_len":2048},{"name":"source_id","type":"string","max_len":64,"ref":{"type":"source","owned":false,"optional":false}},{"name":"cv_blob_id","type":"string","max_len":64},{"name":"first_name","type":"string","max_len":120},{"name":"salutation","type":"enum","values":["herr","frau","divers","neutral"]},{"name":"pool_status","type":"enum","values":["active","talent_pool","blocked","withdrawn"]},{"name":"current_role","type":"string","max_len":200},{"name":"gdpr_consent","type":"bool"},{"name":"source_label","type":"string","max_len":200},{"name":"available_from","type":"string","max_len":32},{"name":"avatar_blob_id","type":"string","max_len":64},{"name":"current_company","type":"string","max_len":200},{"name":"gdpr_consent_at","type":"string","max_len":32},{"name":"last_touched_at","type":"string","max_len":32},{"name":"preferred_locale","type":"string","max_len":16},{"name":"years_experience","type":"number"},{"name":"salary_expectation","type":"number"},{"name":"gdpr_retention_until","type":"string","max_len":32}]},"email_template":{"ops":["list","read","create","update","delete"],"create_fields":["name","category","subject","body","language","stage_trigger","auto_send","active","variables_doc"],"update_fields":["name","category","subject","body","language","stage_trigger","auto_send","active","variables_doc"],"allowed_filters":["data__name","data__category","data__stage_trigger","data__active","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":100,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"name","type":"string","max_len":200},{"name":"active","type":"bool"},{"name":"subject","type":"string","max_len":400},{"name":"category","type":"enum","values":["acknowledge","screening_invite","interview_invite","rejection","offer","talent_pool","other"]},{"name":"language","type":"string","max_len":16},{"name":"auto_send","type":"bool"},{"name":"stage_trigger","type":"enum","values":["","new","review","screening","interview","offer","hired","rejected","talent_pool"]},{"name":"variables_doc","type":"string","max_len":2000}]},"evaluation":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","interview_id","interviewer_id","skills_score","culture_score","communication_score","potential_score","overall_score","recommendation","highlights","concerns","summary"],"update_fields":["skills_score","culture_score","communication_score","potential_score","overall_score","recommendation","highlights","concerns","summary"],"allowed_filters":["data__application_id","data__interview_id","data__interviewer_id","data__recommendation","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__overall_score"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"summary","type":"string","max_len":4000},{"name":"concerns","type":"string","max_len":4000},{"name":"highlights","type":"string","max_len":4000},{"name":"interview_id","type":"string","max_len":64,"ref":{"type":"interview","owned":false,"optional":false}},{"name":"skills_score","type":"number"},{"name":"culture_score","type":"number"},{"name":"overall_score","type":"number"},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"interviewer_id","type":"string","max_len":64},{"name":"recommendation","type":"enum","values":["strong_yes","yes","neutral","no","strong_no"]},{"name":"potential_score","type":"number"},{"name":"communication_score","type":"number"}]},"interview":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","candidate_id","job_id","kind","status","title","scheduled_at","duration_minutes","location","meeting_url","interviewer_id","interviewer_ids","agenda","notes","send_invite"],"update_fields":["kind","status","title","scheduled_at","duration_minutes","location","meeting_url","interviewer_id","interviewer_ids","agenda","notes","send_invite"],"allowed_filters":["data__application_id","data__candidate_id","data__job_id","data__kind","data__status","data__interviewer_id","status","is_archived","owned_by","created_by"],"allowed_sorts":["data__scheduled_at","created_at","updated_at"],"default_sort":"data__scheduled_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["phone","video","onsite","take_home","panel","trial_day"]},{"name":"notes","type":"string","max_len":8000},{"name":"title","type":"string","max_len":200},{"name":"agenda","type":"string","max_len":4000},{"name":"job_id","type":"string","max_len":64},{"name":"status","type":"enum","values":["scheduled","completed","no_show","cancelled","rescheduled"]},{"name":"location","type":"string","max_len":200},{"name":"meeting_url","type":"url","max_len":2048},{"name":"send_invite","type":"bool"},{"name":"candidate_id","type":"string","max_len":64},{"name":"scheduled_at","type":"string","max_len":32},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"interviewer_id","type":"string","max_len":64},{"name":"interviewer_ids","type":"list"},{"name":"duration_minutes","type":"number"}]},"job":{"ops":["list","read","create","update","delete"],"create_fields":["title","slug","department","location","country","remote","employment_type","seniority","headcount","salary_min","salary_max","currency","salary_visibility","summary","description","responsibilities","requirements","nice_to_have","benefits","language","status","public","ai_screen_enabled","ai_screen_prompt","knockout_questions","screening_questions","tags","hiring_manager_id","team_ids","opened_at","target_close_date","closed_at","external_apply_url","color"],"update_fields":["title","slug","department","location","country","remote","employment_type","seniority","headcount","salary_min","salary_max","currency","salary_visibility","summary","description","responsibilities","requirements","nice_to_have","benefits","language","status","public","ai_screen_enabled","ai_screen_prompt","knockout_questions","screening_questions","tags","hiring_manager_id","team_ids","opened_at","target_close_date","closed_at","external_apply_url","color"],"allowed_filters":["data__title","data__department","data__location","data__employment_type","data__seniority","data__remote","data__status","data__public","data__hiring_manager_id","data__tags","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__title","data__opened_at","data__target_close_date"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"slug","type":"string","max_len":120},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"title","type":"string","max_len":200},{"name":"public","type":"bool"},{"name":"remote","type":"enum","values":["onsite","hybrid","remote"]},{"name":"status","type":"enum","values":["draft","open","paused","closed","filled"]},{"name":"country","type":"string","max_len":80},{"name":"summary","type":"string","max_len":600},{"name":"benefits","type":"string","max_len":4000},{"name":"currency","type":"string","max_len":8},{"name":"language","type":"string","max_len":16},{"name":"location","type":"string","max_len":120},{"name":"team_ids","type":"list"},{"name":"closed_at","type":"string","max_len":32},{"name":"headcount","type":"number"},{"name":"opened_at","type":"string","max_len":32},{"name":"seniority","type":"enum","values":["junior","mid","senior","lead","principal"]},{"name":"department","type":"string","max_len":120},{"name":"salary_max","type":"number"},{"name":"salary_min","type":"number"},{"name":"description","type":"string","max_len":16000},{"name":"nice_to_have","type":"string","max_len":4000},{"name":"requirements","type":"string","max_len":8000},{"name":"employment_type","type":"enum","values":["full_time","part_time","internship","working_student","freelance","contract"]},{"name":"ai_screen_prompt","type":"string","max_len":4000},{"name":"responsibilities","type":"string","max_len":8000},{"name":"ai_screen_enabled","type":"bool"},{"name":"hiring_manager_id","type":"string","max_len":64},{"name":"salary_visibility","type":"enum","values":["public","team","private"]},{"name":"target_close_date","type":"string","max_len":32},{"name":"external_apply_url","type":"url","max_len":2048},{"name":"knockout_questions","type":"list"},{"name":"screening_questions","type":"list"}]},"message":{"ops":["list","read","create","update","delete"],"create_fields":["candidate_id","application_id","channel","direction","subject","body","status","sent_at","delivered_at","read_at","template_id","from_address","to_address","cc_addresses","thread_id","error"],"update_fields":["subject","body","status","sent_at","delivered_at","read_at","thread_id","error"],"allowed_filters":["data__candidate_id","data__application_id","data__channel","data__status","data__template_id","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__sent_at"],"default_sort":"-data__sent_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"error","type":"string","max_len":600},{"name":"status","type":"enum","values":["draft","queued","sent","delivered","failed","bounced"]},{"name":"channel","type":"enum","values":["email","note"]},{"name":"read_at","type":"string","max_len":32},{"name":"sent_at","type":"string","max_len":32},{"name":"subject","type":"string","max_len":400},{"name":"direction","type":"enum","values":["outbound","inbound"]},{"name":"thread_id","type":"string","max_len":200},{"name":"to_address","type":"string","max_len":320},{"name":"template_id","type":"string","max_len":64,"ref":{"type":"email_template","owned":false,"optional":false}},{"name":"candidate_id","type":"string","max_len":64},{"name":"cc_addresses","type":"list"},{"name":"delivered_at","type":"string","max_len":32},{"name":"from_address","type":"string","max_len":320},{"name":"application_id","type":"string","max_len":64}]},"offer":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","candidate_id","job_id","salary_gross","salary_period","currency","bonus","bonus_note","vacation_days","start_date","expires_at","term","term_until","weekly_hours","remote_policy","status","sent_at","decided_at","letter_body","letter_blob_id","decline_reason"],"update_fields":["salary_gross","salary_period","currency","bonus","bonus_note","vacation_days","start_date","expires_at","term","term_until","weekly_hours","remote_policy","status","sent_at","decided_at","letter_body","letter_blob_id","decline_reason"],"allowed_filters":["data__application_id","data__candidate_id","data__job_id","data__status","data__currency","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__start_date","data__sent_at"],"default_sort":"created_at","max_limit":100,"fields":[{"name":"term","type":"enum","values":["permanent","fixed_term","trial","intern","freelance"]},{"name":"bonus","type":"number"},{"name":"job_id","type":"string","max_len":64},{"name":"status","type":"enum","values":["draft","sent","accepted","declined","withdrawn","expired"]},{"name":"sent_at","type":"string","max_len":32},{"name":"currency","type":"string","max_len":8},{"name":"bonus_note","type":"string","max_len":600},{"name":"decided_at","type":"string","max_len":32},{"name":"expires_at","type":"string","max_len":32},{"name":"start_date","type":"string","max_len":32},{"name":"term_until","type":"string","max_len":32},{"name":"letter_body","type":"string","max_len":16000},{"name":"candidate_id","type":"string","max_len":64},{"name":"salary_gross","type":"number"},{"name":"weekly_hours","type":"number"},{"name":"remote_policy","type":"string","max_len":200},{"name":"salary_period","type":"enum","values":["yearly","monthly","daily","hourly"]},{"name":"vacation_days","type":"number"},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"decline_reason","type":"string","max_len":600},{"name":"letter_blob_id","type":"string","max_len":64}]},"source":{"ops":["list","read","create","update","delete"],"create_fields":["name","kind","url","active","notes"],"update_fields":["name","kind","url","active","notes"],"allowed_filters":["data__name","data__kind","data__active","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":100,"fields":[{"name":"url","type":"url","max_len":2048},{"name":"kind","type":"enum","values":["linkedin","indeed","stepstone","xing","honeypot","kununu","careers_page","referral","active_sourcing","agency","event","other"]},{"name":"name","type":"string","max_len":200},{"name":"notes","type":"string","max_len":2000},{"name":"active","type":"bool"}]},"task":{"ops":["list","read","create","update","delete"],"create_fields":["title","description","due_date","completed","completed_at","priority","assigned_to","parent_kind","parent_id"],"update_fields":["title","description","due_date","completed","completed_at","priority","assigned_to"],"allowed_filters":["data__parent_id","data__parent_kind","data__assigned_to","data__completed","data__priority","status","is_archived","owned_by","created_by"],"allowed_sorts":["data__due_date","created_at","data__priority"],"default_sort":"data__due_date","max_limit":200,"fields":[{"name":"title","type":"string","max_len":200},{"name":"due_date","type":"string","max_len":32},{"name":"priority","type":"enum","values":["low","normal","high","urgent"]},{"name":"completed","type":"bool"},{"name":"parent_id","type":"string","max_len":64},{"name":"assigned_to","type":"string","max_len":64},{"name":"description","type":"string","max_len":4000},{"name":"parent_kind","type":"enum","values":["candidate","application","job"]},{"name":"completed_at","type":"string","max_len":32}]}}''') as Map; class ApiError implements Exception { final int status; final String message; final dynamic bodyRaw; ApiError(this.status, this.message, [this.bodyRaw]); @override String toString() => 'HTTP $status: $message'; } class ListOpts { final int? limit; final int? offset; final String? sort; final String? q; final Map? filters; ListOpts({this.limit, this.offset, this.sort, this.q, this.filters}); } class AtsClient { String _baseUrl; String _token; late final String _deviceId; late final String _sessionId; bool _autoupdateAttempted = false; bool _metaSentOnce = false; final HttpClient _http = HttpClient(); static const Set _retryableStatuses = {408, 425, 429, 500, 502, 503, 504}; static const int _maxRetries = 3; static const Duration _defaultTimeout = Duration(seconds: 30); AtsClient([String token = '']) : _baseUrl = _resolveBaseUrl(), _token = token.isNotEmpty ? token : (Platform.environment['XCLIENT_TOKEN'] ?? '') { _deviceId = _loadOrMintDeviceId(); _sessionId = _mintUuid(); _http.connectionTimeout = const Duration(seconds: 15); } void setToken(String token) { _token = token; } void setBaseUrl(String url) { _baseUrl = _trimRightSlash(url); } static String _trimRightSlash(String s) { var out = s; while (out.endsWith('/')) { out = out.substring(0, out.length - 1); } return out; } static String _resolveBaseUrl() { final env = Platform.environment['XCLIENT_BASE_URL']; return _trimRightSlash((env != null && env.isNotEmpty) ? env : _defaultBase); } // ── Identifier persistence ───────────────────────────────────────── static String? _stateDir() { final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; if (home == null || home.isEmpty) return null; final d = '$home/.${moduleName}'; try { Directory(d).createSync(recursive: true); return d; } catch (_) { return null; } } static String _mintUuid() { final rng = Random.secure(); final bytes = List.generate(16, (_) => rng.nextInt(256)); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; String hx(int i) => bytes[i].toRadixString(16).padLeft(2, '0'); return '${hx(0)}${hx(1)}${hx(2)}${hx(3)}-${hx(4)}${hx(5)}-${hx(6)}${hx(7)}-${hx(8)}${hx(9)}-${hx(10)}${hx(11)}${hx(12)}${hx(13)}${hx(14)}${hx(15)}'; } static String _loadOrMintDeviceId() { final d = _stateDir(); if (d == null) return _mintUuid(); final f = File('$d/device.json'); if (f.existsSync()) { try { final blob = json.decode(f.readAsStringSync()) as Map; final did = blob['device_id']; if (did is String && did.length >= 32) return did; } catch (_) {} } final fresh = _mintUuid(); try { f.writeAsStringSync(json.encode({'device_id': fresh})); } catch (_) {} return fresh; } static bool _autoupdateEnabled() { final v = (Platform.environment['XCLIENT_NO_AUTOUPDATE'] ?? '').toLowerCase(); return v != '1' && v != 'true' && v != 'yes'; } static Map _fingerprint() { final env = Platform.environment; final tp = (env['TERM_PROGRAM'] ?? '').toLowerCase(); return { 'dart_version': Platform.version, 'os': Platform.operatingSystem, 'os_version': Platform.operatingSystemVersion, 'term_program': env['TERM_PROGRAM'], 'editor_env': env['EDITOR'], 'ci': env.containsKey('CI') || env.containsKey('GITHUB_ACTIONS'), 'claude_code': env.containsKey('CLAUDECODE') || env.containsKey('CLAUDE_CODE_ENTRYPOINT'), 'codex': env.containsKey('CODEX_HOME'), 'vscode': tp == 'vscode' && !env.containsKey('CURSOR_TRACE_ID'), 'cursor': env.containsKey('CURSOR_TRACE_ID'), 'antigravity': env.containsKey('ANTIGRAVITY_TRACE_ID'), 'jetbrains': tp.contains('jetbrains'), }; } String _userAgent() => '$moduleName/$clientVersion (lib/$language; dart/${Platform.version.split(' ').first}; ${Platform.operatingSystem})'; static double _backoffSeconds(int attempt, double? retryAfter) { if (retryAfter != null && retryAfter >= 0) return min(retryAfter, 60.0); return min(pow(2, attempt).toDouble(), 60.0); } // ── HTTP transport ───────────────────────────────────────────────── /// Generic request helper. JSON in / JSON out. Future?> requestJson( String method, String path, dynamic body) async { _maybeAutoupdate(); Object? lastErr; for (var attempt = 0; attempt < _maxRetries; attempt++) { try { final result = await _sendFollowingRedirects( method.toUpperCase(), '$_baseUrl$path', body); final status = result.status; final headers = result.headers; final raw = result.body; final fresh = headers['x-auth-refresh-token']; if (fresh != null && fresh.isNotEmpty) _token = fresh; if (_retryableStatuses.contains(status) && attempt + 1 < _maxRetries) { double? ra; final raStr = headers['retry-after']; if (raStr != null) ra = double.tryParse(raStr); await Future.delayed( Duration(milliseconds: (_backoffSeconds(attempt, ra) * 1000).round())); continue; } dynamic parsed; if (raw.isNotEmpty) { try { parsed = json.decode(raw); } catch (_) { parsed = null; } } if (status >= 400) { var msg = 'request failed'; if (parsed is Map) { final d = parsed['detail']; final m = parsed['message']; if (d is String) msg = d; else if (m is String) msg = m; } _emitCallEvent(method, path, status, false); throw ApiError(status, msg, parsed); } _emitCallEvent(method, path, status, true); if (parsed is Map) return parsed; return null; } on ApiError { rethrow; } catch (e) { lastErr = e; if (attempt + 1 < _maxRetries) { await Future.delayed( Duration(milliseconds: (_backoffSeconds(attempt, null) * 1000).round())); continue; } _emitCallEvent(method, path, 0, false); throw ApiError(0, e.toString()); } } _emitCallEvent(method, path, 0, false); throw ApiError(0, lastErr?.toString() ?? 'request failed'); } Future?> requestList(String path, ListOpts? opts) { final qs = {}; if (opts != null) { if (opts.limit != null) qs['limit'] = opts.limit.toString(); if (opts.offset != null) qs['offset'] = opts.offset.toString(); if (opts.sort != null && opts.sort!.isNotEmpty) qs['sort'] = opts.sort!; if (opts.q != null && opts.q!.isNotEmpty) qs['q'] = opts.q!; if (opts.filters != null) { opts.filters!.forEach((k, v) { if (v != null) qs[k] = v.toString(); }); } } var p = path; if (qs.isNotEmpty) { final encoded = qs.entries.map((e) => '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}' ).join('&'); p = '$p${path.contains('?') ? '&' : '?'}$encoded'; } return requestJson('GET', p, null); } /// Walk the redirect chain manually so Authorization can be dropped /// on cross-origin hops. Caps at 5 hops; mirrors RFC 7231 method /// rewrite semantics. Future<_Response> _sendFollowingRedirects( String method, String urlIn, dynamic body) async { var url = urlIn; var currentMethod = method; dynamic currentBody = body; var stripAuth = false; for (var hop = 0; hop < 5; hop++) { final uri = Uri.parse(url); final req = await _http.openUrl(currentMethod, uri).timeout(_defaultTimeout); req.followRedirects = false; req.headers.set('Accept', 'application/json'); req.headers.set('User-Agent', _userAgent()); req.headers.set('X-Client-Channel', 'client_$language'); req.headers.set('X-Client-Version', clientVersion); req.headers.set('X-Analytics-Device-Id', _deviceId); req.headers.set('X-Analytics-Session-Id', _sessionId); if (!stripAuth && _token.isNotEmpty) { req.headers.set('Authorization', 'Bearer $_token'); } if (currentBody != null && currentMethod != 'GET' && currentMethod != 'HEAD') { req.headers.set('Content-Type', 'application/json'); final encoded = utf8.encode(json.encode(currentBody)); req.contentLength = encoded.length; req.add(encoded); } final resp = await req.close().timeout(_defaultTimeout); final raw = await resp.transform(utf8.decoder).join(); final hmap = {}; resp.headers.forEach((k, v) { hmap[k.toLowerCase()] = v.join(','); }); final status = resp.statusCode; if (status < 300 || status >= 400 || status == 304) { return _Response(status, hmap, raw); } final loc = hmap['location']; if (loc == null || loc.isEmpty) return _Response(status, hmap, raw); Uri nextUri; try { nextUri = uri.resolve(loc); } catch (_) { return _Response(status, hmap, raw); } if (nextUri.origin != uri.origin) stripAuth = true; if (status == 303 || ((status == 301 || status == 302) && currentMethod != 'GET' && currentMethod != 'HEAD')) { currentMethod = 'GET'; currentBody = null; } url = nextUri.toString(); } return _Response(0, const {}, ''); } // ── Analytics ────────────────────────────────────────────────────── void _emitCallEvent(String method, String path, int status, bool ok) { final includeEnv = !_metaSentOnce; _metaSentOnce = true; Future(() async { try { final meta = { 'channel': 'client_$language', 'client_version': clientVersion, 'module_name': moduleName, 'language': language, 'os': Platform.operatingSystem, 'dart_version': Platform.version, }; if (includeEnv) meta['env'] = _fingerprint(); final pathBase = path.split('?').first; final evt = { 'type': 'client.call', 'ts_client': DateTime.now().millisecondsSinceEpoch ~/ 1000, 'meta': { 'method': method.toUpperCase(), 'path': pathBase.length > 128 ? pathBase.substring(0, 128) : pathBase, 'status': status, 'ok': ok, }, }; final payload = json.encode({ 'device_id': _deviceId, 'session_id': _sessionId, 'events': [evt], 'meta': meta, }); final client = HttpClient(); client.connectionTimeout = const Duration(seconds: 2); try { final uri = Uri.parse('$_baseUrl/xapi2/analytics/challenge'); final req = await client.postUrl(uri).timeout(const Duration(seconds: 4)); req.headers.set('Content-Type', 'application/json'); req.headers.set('User-Agent', _userAgent()); final encoded = utf8.encode(payload); req.contentLength = encoded.length; req.add(encoded); final resp = await req.close().timeout(const Duration(seconds: 4)); await resp.drain(); } finally { client.close(force: true); } } catch (_) { /* fire-and-forget */ } }); } // ── Auto-update ──────────────────────────────────────────────────── void _maybeAutoupdate() { if (_autoupdateAttempted) return; _autoupdateAttempted = true; if (!_autoupdateEnabled()) return; Future(() async { try { final d = _stateDir(); if (d == null) return; final stamp = File('$d/update_check.json'); if (stamp.existsSync()) { try { final blob = json.decode(stamp.readAsStringSync()) as Map; final last = blob['checked_at']; if (last is num && (DateTime.now().millisecondsSinceEpoch ~/ 1000) - last.toInt() < 86400) { return; } } catch (_) {} } try { stamp.writeAsStringSync(json.encode({'checked_at': DateTime.now().millisecondsSinceEpoch ~/ 1000})); } catch (_) {} // Source replacement is intentionally a no-op in Dart - users // typically ship AOT-compiled artefacts (Flutter apps, dart // compile exe), so the .dart file on disk is just a record of // the version they vendored. Surface the new version through // the next build. } catch (_) { /* best-effort */ } }); } /// List `application` rows. Future?> applicationList({ListOpts? opts}) => requestList('/xapi2/data/application', opts); /// Fetch one `application` row by id. Future?> applicationGet(String id) => requestJson('GET', '/xapi2/data/application/' + id, null); /// Create a new `application` row. Future?> applicationCreate(Map data) => requestJson('POST', '/xapi2/data/application', data); /// Patch a `application` row. Future?> applicationUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/application/' + id, data); /// Delete a `application` row. Future applicationDelete(String id) async { await requestJson('DELETE', '/xapi2/data/application/' + id, null); return true; } /// List `application_note` rows. Future?> applicationNoteList({ListOpts? opts}) => requestList('/xapi2/data/application_note', opts); /// Fetch one `application_note` row by id. Future?> applicationNoteGet(String id) => requestJson('GET', '/xapi2/data/application_note/' + id, null); /// Create a new `application_note` row. Future?> applicationNoteCreate(Map data) => requestJson('POST', '/xapi2/data/application_note', data); /// Patch a `application_note` row. Future?> applicationNoteUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/application_note/' + id, data); /// Delete a `application_note` row. Future applicationNoteDelete(String id) async { await requestJson('DELETE', '/xapi2/data/application_note/' + id, null); return true; } /// List `candidate` rows. Future?> candidateList({ListOpts? opts}) => requestList('/xapi2/data/candidate', opts); /// Fetch one `candidate` row by id. Future?> candidateGet(String id) => requestJson('GET', '/xapi2/data/candidate/' + id, null); /// Create a new `candidate` row. Future?> candidateCreate(Map data) => requestJson('POST', '/xapi2/data/candidate', data); /// Patch a `candidate` row. Future?> candidateUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/candidate/' + id, data); /// Delete a `candidate` row. Future candidateDelete(String id) async { await requestJson('DELETE', '/xapi2/data/candidate/' + id, null); return true; } /// List `email_template` rows. Future?> emailTemplateList({ListOpts? opts}) => requestList('/xapi2/data/email_template', opts); /// Fetch one `email_template` row by id. Future?> emailTemplateGet(String id) => requestJson('GET', '/xapi2/data/email_template/' + id, null); /// Create a new `email_template` row. Future?> emailTemplateCreate(Map data) => requestJson('POST', '/xapi2/data/email_template', data); /// Patch a `email_template` row. Future?> emailTemplateUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/email_template/' + id, data); /// Delete a `email_template` row. Future emailTemplateDelete(String id) async { await requestJson('DELETE', '/xapi2/data/email_template/' + id, null); return true; } /// List `evaluation` rows. Future?> evaluationList({ListOpts? opts}) => requestList('/xapi2/data/evaluation', opts); /// Fetch one `evaluation` row by id. Future?> evaluationGet(String id) => requestJson('GET', '/xapi2/data/evaluation/' + id, null); /// Create a new `evaluation` row. Future?> evaluationCreate(Map data) => requestJson('POST', '/xapi2/data/evaluation', data); /// Patch a `evaluation` row. Future?> evaluationUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/evaluation/' + id, data); /// Delete a `evaluation` row. Future evaluationDelete(String id) async { await requestJson('DELETE', '/xapi2/data/evaluation/' + id, null); return true; } /// List `interview` rows. Future?> interviewList({ListOpts? opts}) => requestList('/xapi2/data/interview', opts); /// Fetch one `interview` row by id. Future?> interviewGet(String id) => requestJson('GET', '/xapi2/data/interview/' + id, null); /// Create a new `interview` row. Future?> interviewCreate(Map data) => requestJson('POST', '/xapi2/data/interview', data); /// Patch a `interview` row. Future?> interviewUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/interview/' + id, data); /// Delete a `interview` row. Future interviewDelete(String id) async { await requestJson('DELETE', '/xapi2/data/interview/' + id, null); return true; } /// List `job` rows. Future?> jobList({ListOpts? opts}) => requestList('/xapi2/data/job', opts); /// Fetch one `job` row by id. Future?> jobGet(String id) => requestJson('GET', '/xapi2/data/job/' + id, null); /// Create a new `job` row. Future?> jobCreate(Map data) => requestJson('POST', '/xapi2/data/job', data); /// Patch a `job` row. Future?> jobUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/job/' + id, data); /// Delete a `job` row. Future jobDelete(String id) async { await requestJson('DELETE', '/xapi2/data/job/' + id, null); return true; } /// List `message` rows. Future?> messageList({ListOpts? opts}) => requestList('/xapi2/data/message', opts); /// Fetch one `message` row by id. Future?> messageGet(String id) => requestJson('GET', '/xapi2/data/message/' + id, null); /// Create a new `message` row. Future?> messageCreate(Map data) => requestJson('POST', '/xapi2/data/message', data); /// Patch a `message` row. Future?> messageUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/message/' + id, data); /// Delete a `message` row. Future messageDelete(String id) async { await requestJson('DELETE', '/xapi2/data/message/' + id, null); return true; } /// List `offer` rows. Future?> offerList({ListOpts? opts}) => requestList('/xapi2/data/offer', opts); /// Fetch one `offer` row by id. Future?> offerGet(String id) => requestJson('GET', '/xapi2/data/offer/' + id, null); /// Create a new `offer` row. Future?> offerCreate(Map data) => requestJson('POST', '/xapi2/data/offer', data); /// Patch a `offer` row. Future?> offerUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/offer/' + id, data); /// Delete a `offer` row. Future offerDelete(String id) async { await requestJson('DELETE', '/xapi2/data/offer/' + id, null); return true; } /// List `source` rows. Future?> sourceList({ListOpts? opts}) => requestList('/xapi2/data/source', opts); /// Fetch one `source` row by id. Future?> sourceGet(String id) => requestJson('GET', '/xapi2/data/source/' + id, null); /// Create a new `source` row. Future?> sourceCreate(Map data) => requestJson('POST', '/xapi2/data/source', data); /// Patch a `source` row. Future?> sourceUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/source/' + id, data); /// Delete a `source` row. Future sourceDelete(String id) async { await requestJson('DELETE', '/xapi2/data/source/' + id, null); return true; } /// List `task` rows. Future?> taskList({ListOpts? opts}) => requestList('/xapi2/data/task', opts); /// Fetch one `task` row by id. Future?> taskGet(String id) => requestJson('GET', '/xapi2/data/task/' + id, null); /// Create a new `task` row. Future?> taskCreate(Map data) => requestJson('POST', '/xapi2/data/task', data); /// Patch a `task` row. Future?> taskUpdate(String id, Map data) => requestJson('PATCH', '/xapi2/data/task/' + id, data); /// Delete a `task` row. Future taskDelete(String id) async { await requestJson('DELETE', '/xapi2/data/task/' + id, null); return true; } } class _Response { final int status; final Map headers; final String body; const _Response(this.status, this.headers, this.body); }