import 'dotenv/config' import { getDb } from './client.js' const sql = getDb() const migrate = async () => { await sql` CREATE EXTENSION IF NOT EXISTS "pgcrypto" ` await sql` CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL UNIQUE, permissions JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS customers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), firebase_uid VARCHAR(255) UNIQUE, phone VARCHAR(20) UNIQUE, display_name VARCHAR(100) NOT NULL, is_anonymous BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS mitras ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), firebase_uid VARCHAR(255) UNIQUE, phone VARCHAR(20) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS control_center_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), firebase_uid VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, role_id UUID NOT NULL REFERENCES roles(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS app_config ( key VARCHAR(100) PRIMARY KEY, value JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` INSERT INTO app_config (key, value) VALUES ('anonymity', '{"enabled": false}') ON CONFLICT (key) DO NOTHING ` // --- Phase 2: Mitra Online Status & Pairing --- await sql` CREATE TABLE IF NOT EXISTS mitra_online_status ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), mitra_id UUID NOT NULL UNIQUE REFERENCES mitras(id), is_online BOOLEAN NOT NULL DEFAULT FALSE, last_online_at TIMESTAMPTZ, last_offline_at TIMESTAMPTZ, last_heartbeat_at TIMESTAMPTZ, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS mitra_online_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), mitra_id UUID NOT NULL REFERENCES mitras(id), status VARCHAR(10) NOT NULL, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_mitra_online_logs_mitra_id ON mitra_online_logs (mitra_id) ` await sql` CREATE TABLE IF NOT EXISTS chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customers(id), mitra_id UUID REFERENCES mitras(id), status VARCHAR(30) NOT NULL DEFAULT 'searching', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), paired_at TIMESTAMPTZ, ended_at TIMESTAMPTZ, ended_by VARCHAR(20) ) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_customer_id ON chat_sessions (customer_id) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_mitra_id ON chat_sessions (mitra_id) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_status ON chat_sessions (status) ` // Composite index for the per-mitra active-session count subquery used by the // 5s availability poll and the per-blast capacity filter. await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_mitra_status ON chat_sessions (mitra_id, status) ` await sql` CREATE TABLE IF NOT EXISTS chat_request_notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), mitra_id UUID NOT NULL REFERENCES mitras(id), notified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), response VARCHAR(20), responded_at TIMESTAMPTZ ) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_request_notifications_session_id ON chat_request_notifications (session_id) ` await sql` INSERT INTO app_config (key, value) VALUES ('max_customers_per_mitra', '{"value": 3}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3: Chat Engine --- // Add session duration/pricing columns to chat_sessions await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS duration_minutes INT, ADD COLUMN IF NOT EXISTS price INT DEFAULT 0, ADD COLUMN IF NOT EXISTS is_free_trial BOOLEAN NOT NULL DEFAULT FALSE, ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS extended_minutes INT NOT NULL DEFAULT 0 ` // Add FCM token columns await sql` ALTER TABLE customers ADD COLUMN IF NOT EXISTS fcm_token VARCHAR(255) ` await sql` ALTER TABLE mitras ADD COLUMN IF NOT EXISTS fcm_token VARCHAR(255) ` await sql` CREATE TABLE IF NOT EXISTS chat_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), sender_type VARCHAR(10) NOT NULL, sender_id UUID NOT NULL, type VARCHAR(20) NOT NULL DEFAULT 'text', content TEXT NOT NULL, metadata JSONB, status VARCHAR(20) NOT NULL DEFAULT 'sent', delivered_at TIMESTAMPTZ, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_messages_session_created ON chat_messages (session_id, created_at) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_messages_session_status ON chat_messages (session_id, status) ` await sql` CREATE TABLE IF NOT EXISTS session_closures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), user_type VARCHAR(10) NOT NULL, user_id UUID NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS session_extensions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), requested_duration_minutes INT NOT NULL, requested_price INT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), responded_at TIMESTAMPTZ ) ` await sql` CREATE TABLE IF NOT EXISTS customer_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customers(id), session_id UUID NOT NULL REFERENCES chat_sessions(id), type VARCHAR(20) NOT NULL, amount INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_customer_transactions_customer_id ON customer_transactions (customer_id) ` // Phase 3 config keys await sql` INSERT INTO app_config (key, value) VALUES ('free_trial_enabled', '{"value": true}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('free_trial_duration_minutes', '{"value": 5}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('extension_timeout_seconds', '{"value": 60}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('early_end_mitra_enabled', '{"value": false}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('early_end_customer_enabled', '{"value": false}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('price_tiers', ${sql.json({ tiers: [ { duration_minutes: 1, price: 5000, label: '1 Menit (Test)' }, { duration_minutes: 15, price: 30000, label: '15 Menit' }, { duration_minutes: 30, price: 60000, label: '30 Menit' }, { duration_minutes: 45, price: 100000, label: '45 Menit' }, { duration_minutes: 60, price: 150000, label: '60 Menit' }, { duration_minutes: 1440, price: 250000, label: '24 Jam' }, ]})}) ON CONFLICT (key) DO NOTHING ` // --- Phase 3.1: Mitra Ping Config --- await sql` INSERT INTO app_config (key, value) VALUES ('require_mitra_ping', '{"value": true}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('mitra_ping_interval_seconds', '{"value": 15}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3.2: Mitra Request Activity Log --- await sql` ALTER TABLE chat_request_notifications ADD COLUMN IF NOT EXISTS active_session_count INT NOT NULL DEFAULT 0 ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_request_notifications_mitra_notified ON chat_request_notifications (mitra_id, notified_at) ` // --- Phase 3.3: Session Topic Sensitivity --- await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS topic_sensitivity VARCHAR(16) NOT NULL DEFAULT 'regular' ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_topic_sensitivity ON chat_sessions (topic_sensitivity) ` await sql` CREATE TABLE IF NOT EXISTS session_sensitivity_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE, changed_by_mitra_id UUID NOT NULL REFERENCES mitras(id), from_value VARCHAR(16) NOT NULL, to_value VARCHAR(16) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_session_sensitivity_log_session ON session_sensitivity_log (session_id) ` await sql` INSERT INTO app_config (key, value) VALUES ('sensitive_flip_confirmation_enabled', '{"value": true}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('sensitive_flag_one_way_latch', '{"value": false}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3.4: Self-Managed Auth --- // Customers: add social identity columns await sql` ALTER TABLE customers ADD COLUMN IF NOT EXISTS email VARCHAR(255), ADD COLUMN IF NOT EXISTS google_sub VARCHAR(255), ADD COLUMN IF NOT EXISTS apple_sub VARCHAR(255) ` await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_google_sub ON customers (google_sub) WHERE google_sub IS NOT NULL` await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_apple_sub ON customers (apple_sub) WHERE apple_sub IS NOT NULL` // display_name is set after sign-in via the set-display-name screen for // direct phone/Google/Apple sign-ups (no anonymous bootstrap). Allow null. await sql`ALTER TABLE customers ALTER COLUMN display_name DROP NOT NULL` // Control center users: password-based auth columns // firebase_uid stays for backward compat during migration; will be dropped in a later cleanup migration await sql`ALTER TABLE control_center_users ALTER COLUMN firebase_uid DROP NOT NULL` await sql` ALTER TABLE control_center_users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(60) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS failed_login_count INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS lockout_until TIMESTAMPTZ ` // Auth sessions (refresh tokens + multi-device) await sql` CREATE TABLE IF NOT EXISTS auth_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_type VARCHAR(16) NOT NULL, user_id UUID NOT NULL, refresh_token_hash VARCHAR(60) NOT NULL, device_info JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ ) ` await sql`CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions (user_type, user_id)` await sql`CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions (expires_at) WHERE revoked_at IS NULL` // OTP requests (Fazpass reference + rate-limit tracking) await sql` CREATE TABLE IF NOT EXISTS otp_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), phone VARCHAR(20) NOT NULL, ip_address VARCHAR(45), user_type VARCHAR(16) NOT NULL, fazpass_reference VARCHAR(255), channel VARCHAR(16), attempts INT NOT NULL DEFAULT 0, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL ) ` await sql`CREATE INDEX IF NOT EXISTS idx_otp_requests_phone_created ON otp_requests (phone, created_at)` await sql`CREATE INDEX IF NOT EXISTS idx_otp_requests_ip_created ON otp_requests (ip_address, created_at)` // Auth-related app_config defaults await sql` INSERT INTO app_config (key, value) VALUES ('otp_max_per_phone_per_hour', '{"value": 3}'), ('otp_max_per_ip_per_hour', '{"value": 10}'), ('otp_resend_cooldown_seconds', '{"value": 60}'), ('otp_verify_max_attempts', '{"value": 5}'), ('cc_login_max_attempts', '{"value": 5}'), ('cc_login_lockout_minutes', '{"value": 15}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3.7: Paid Pairing Flow + Returning-Chat + Extension Flip --- // payment_sessions: customer-initiated payment intents (mocked) that gate pairing await sql` CREATE TABLE IF NOT EXISTS payment_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customers(id), amount INTEGER NOT NULL DEFAULT 0, duration_minutes INTEGER NOT NULL, is_free_trial BOOLEAN NOT NULL DEFAULT false, is_extension BOOLEAN NOT NULL DEFAULT false, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','confirmed','consumed','failed_pairing','abandoned','expired')), targeted_mitra_id UUID REFERENCES mitras(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), confirmed_at TIMESTAMPTZ, consumed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL ) ` await sql` CREATE INDEX IF NOT EXISTS idx_payment_sessions_customer ON payment_sessions (customer_id) ` await sql` CREATE INDEX IF NOT EXISTS idx_payment_sessions_status_expires ON payment_sessions (status, expires_at) ` // pairing_failures: cause-tagged audit rows for confirmed payments that did not yield a chat await sql` CREATE TABLE IF NOT EXISTS pairing_failures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), payment_session_id UUID NOT NULL REFERENCES payment_sessions(id) ON DELETE CASCADE, customer_id UUID NOT NULL REFERENCES customers(id), targeted_mitra_id UUID REFERENCES mitras(id), cause_tag TEXT NOT NULL CHECK (cause_tag IN ( 'no_mitra_available', 'all_mitras_rejected', 'targeted_mitra_offline', 'targeted_mitra_rejected', 'targeted_mitra_timeout', 'payment_session_expired', 'customer_cancelled' )), amount INTEGER NOT NULL, operator_action TEXT CHECK (operator_action IS NULL OR operator_action IN ('refunded','credited','no_action')), actioned_by UUID REFERENCES control_center_users(id), actioned_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` // Phase 3.7 follow-up: extend the pairing_failures.cause_tag CHECK to include the two // extension-specific tags. Idempotent: drop the existing check (whatever its exact name) and // re-add the expanded list. Postgres auto-names CHECK constraints `__check` // unless we name them explicitly; the original DDL above relies on that default. await sql` DO $$ BEGIN IF EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'pairing_failures'::regclass AND conname = 'pairing_failures_cause_tag_check' ) THEN ALTER TABLE pairing_failures DROP CONSTRAINT pairing_failures_cause_tag_check; END IF; ALTER TABLE pairing_failures ADD CONSTRAINT pairing_failures_cause_tag_check CHECK (cause_tag IN ( 'no_mitra_available', 'all_mitras_rejected', 'targeted_mitra_offline', 'targeted_mitra_rejected', 'targeted_mitra_timeout', 'payment_session_expired', 'customer_cancelled', 'extension_rejected', 'extension_safeguard_tripped' )); END $$ ` await sql` CREATE INDEX IF NOT EXISTS idx_pairing_failures_created_at ON pairing_failures (created_at DESC) ` await sql` CREATE INDEX IF NOT EXISTS idx_pairing_failures_cause ON pairing_failures (cause_tag) ` await sql` CREATE INDEX IF NOT EXISTS idx_pairing_failures_unactioned ON pairing_failures (created_at DESC) WHERE operator_action IS NULL ` // chat_sessions FK to payment_sessions (nullable for backward compat with pre-3.7 rows) await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS payment_session_id UUID REFERENCES payment_sessions(id) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_payment ON chat_sessions (payment_session_id) ` // session_extensions FK to payment_sessions (extensions also have their own payment session) await sql` ALTER TABLE session_extensions ADD COLUMN IF NOT EXISTS payment_session_id UUID REFERENCES payment_sessions(id) ` // Phase 3.7 config keys (idempotent — existing dev DBs need a manual update for extension_timeout_seconds → 10) await sql` INSERT INTO app_config (key, value) VALUES ('payment_session_timeout_minutes', '{"value": 20}'), ('returning_chat_confirmation_timeout_seconds', '{"value": 20}'), ('extension_default_action_on_timeout', '{"value": "auto_approve"}'), ('pairing_blast_timeout_seconds', '{"value": 60}') ON CONFLICT (key) DO NOTHING ` // --- Phase 4 — Customer Flow Redesign --- // 1. payment_sessions + chat_sessions: replace is_free_trial with is_first_session_discount. // Phase 3.7 was the first ship of is_free_trial and never went live with real users // (per project memory), so we copy whatever values exist and drop the old column. // Idempotent: ADD/DROP both use IF [NOT] EXISTS, and each UPDATE is gated on the // old column still existing. await sql` ALTER TABLE payment_sessions ADD COLUMN IF NOT EXISTS is_first_session_discount BOOLEAN NOT NULL DEFAULT false ` await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS is_first_session_discount BOOLEAN NOT NULL DEFAULT false ` // Copy values from the legacy column to the new one. We must use dynamic SQL // (EXECUTE) inside the DO block — a static reference to is_free_trial would fail // to parse when the column has already been dropped on a previous re-run. // // The IF EXISTS check resolves the column against the *current* search_path so // test schemas don't false-positive on the dev `public` schema's leftover columns. // We use to_regclass + pg_attribute (which is search_path-aware) instead of // information_schema.columns (which lists every schema). await sql` DO $$ BEGIN IF EXISTS ( SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('payment_sessions') AND attname = 'is_free_trial' AND NOT attisdropped ) THEN EXECUTE 'UPDATE payment_sessions SET is_first_session_discount = is_free_trial WHERE is_free_trial = true AND is_first_session_discount = false'; END IF; IF EXISTS ( SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass('chat_sessions') AND attname = 'is_free_trial' AND NOT attisdropped ) THEN EXECUTE 'UPDATE chat_sessions SET is_first_session_discount = is_free_trial WHERE is_free_trial = true AND is_first_session_discount = false'; END IF; END $$ ` await sql`ALTER TABLE payment_sessions DROP COLUMN IF EXISTS is_free_trial` await sql`ALTER TABLE chat_sessions DROP COLUMN IF EXISTS is_free_trial` // 2. payment_sessions.mode — chat (default) vs voice call. Voice call is just chat // with a different price group + a header badge; no extra media handling. await sql` ALTER TABLE payment_sessions ADD COLUMN IF NOT EXISTS mode TEXT NOT NULL DEFAULT 'chat' CHECK (mode IN ('chat', 'call')) ` // 3. chat_sessions.topics — ESP picks persisted for info-only display to mitra. // Does NOT affect matching, pricing, or routing. await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS topics TEXT[] ` // 4. Phase 4 app_config rows. Use ON CONFLICT (key) DO NOTHING so re-runs don't // clobber operator edits, and the migration is idempotent against partially // populated DBs. await sql` INSERT INTO app_config (key, value) VALUES ('payment_method_qris_first', ${sql.json({ value: true })}), ('searching_timeout_minutes', ${sql.json({ value: 5 })}), ('end_session_two_step_confirm', ${sql.json({ value: true })}), ('three_minute_warning_enabled', ${sql.json({ value: true })}), ('first_session_discount_enabled', ${sql.json({ value: true })}), ('first_session_discount_actual_price_idr', ${sql.json({ value: 2000 })}), ('first_session_discount_gimmick_price_idr', ${sql.json({ value: 12000 })}), ('first_session_discount_duration_minutes', ${sql.json({ value: 12 })}), ('first_session_discount_modes', ${sql.json({ value: ['chat'] })}), ('pricing_chat_tiers_json', ${sql.json({ tiers: [ { id: '5', minutes: 5, price_idr: 5000, tag: null }, { id: '12', minutes: 12, price_idr: 12000, tag: 'paling pas' }, { id: '30', minutes: 30, price_idr: 25000, tag: 'hemat' }, { id: '60', minutes: 60, price_idr: 45000, tag: null }, { id: '120', minutes: 120, price_idr: 80000, tag: 'best deal' }, ]})}), ('pricing_call_tiers_json', ${sql.json({ tiers: [ { id: '10', minutes: 10, price_idr: 9000, tag: null }, { id: '20', minutes: 20, price_idr: 17000, tag: 'paling pas' }, { id: '45', minutes: 45, price_idr: 35000, tag: null }, { id: '60', minutes: 60, price_idr: 45000, tag: 'hemat' }, ]})}), ('support_handles_json', ${sql.json({ wa: { label: 'WhatsApp', deeplink: 'https://wa.me/6285173310010' }, telegram: { label: 'Telegram', deeplink: 'https://t.me/halobestie' }, })}) ON CONFLICT (key) DO NOTHING ` console.log('Migration complete.') await sql.end() } migrate().catch((err) => { console.error('Migration failed:', err) process.exit(1) })