SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
1 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%

fix: brancher Tailwind v4 sur Turbopack (classes utilitaires manquantes en prod)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 2 h ago (Oct 17, 2026) parent 4b01172

16 changed files +4,062 −5

added drizzle.config.ts +2 −0
@@ -0,0 +1,2 @@
1 +import { defineConfig } from "drizzle-kit";
2 +export default defineConfig({ schema: "./src/db/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL ?? "postgresql://toveli_app:toveli_dev@127.0.0.1:5432/toveli" }, strict: true, verbose: false });
added drizzle/0000_daily_dexter_bennett.sql +318 −0
@@ -0,0 +1,318 @@
1 +CREATE TABLE "audit_events" (
2 + "id" bigserial PRIMARY KEY NOT NULL,
3 + "actor_id" uuid,
4 + "action" text NOT NULL,
5 + "object_type" text,
6 + "object_id" text,
7 + "ip_hash" text,
8 + "metadata_redacted" jsonb DEFAULT '{}'::jsonb NOT NULL,
9 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
10 +);
11 +--> statement-breakpoint
12 +CREATE TABLE "email_deliveries" (
13 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
14 + "to_hash" text NOT NULL,
15 + "template" text NOT NULL,
16 + "provider_id" text,
17 + "status" text NOT NULL,
18 + "error" text,
19 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
20 +);
21 +--> statement-breakpoint
22 +CREATE TABLE "favorites" (
23 + "user_id" uuid NOT NULL,
24 + "listing_uid" text NOT NULL,
25 + "created_at" timestamp with time zone DEFAULT now() NOT NULL,
26 + CONSTRAINT "favorites_user_id_listing_uid_pk" PRIMARY KEY("user_id","listing_uid")
27 +);
28 +--> statement-breakpoint
29 +CREATE TABLE "inquiries" (
30 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
31 + "listing_id" uuid NOT NULL,
32 + "sender_user_id" uuid NOT NULL,
33 + "recipient_user_id" uuid NOT NULL,
34 + "category" text DEFAULT 'INFO' NOT NULL,
35 + "status" text DEFAULT 'OPEN' NOT NULL,
36 + "last_message_at" timestamp with time zone DEFAULT now() NOT NULL,
37 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
38 +);
39 +--> statement-breakpoint
40 +CREATE TABLE "listing_claims" (
41 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
42 + "aggregated_uid" text NOT NULL,
43 + "requester_id" uuid NOT NULL,
44 + "status" text DEFAULT 'PENDING' NOT NULL,
45 + "message" text,
46 + "proof_keys" jsonb DEFAULT '[]'::jsonb NOT NULL,
47 + "reviewed_by" uuid,
48 + "reviewed_at" timestamp with time zone,
49 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
50 +);
51 +--> statement-breakpoint
52 +CREATE TABLE "listing_media" (
53 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
54 + "listing_id" uuid NOT NULL,
55 + "storage_key" text NOT NULL,
56 + "media_type" text DEFAULT 'IMAGE' NOT NULL,
57 + "mime_type" text,
58 + "position" integer DEFAULT 0 NOT NULL,
59 + "alt_text" text,
60 + "license_status" text DEFAULT 'OWNER_DECLARED' NOT NULL,
61 + "scan_status" text DEFAULT 'PENDING' NOT NULL,
62 + "width" integer,
63 + "height" integer,
64 + "bytes" integer,
65 + "status" text DEFAULT 'UPLOADING' NOT NULL,
66 + "upload_token" text,
67 + "uploaded_at" timestamp with time zone DEFAULT now() NOT NULL
68 +);
69 +--> statement-breakpoint
70 +CREATE TABLE "listing_versions" (
71 + "id" bigserial PRIMARY KEY NOT NULL,
72 + "listing_id" uuid NOT NULL,
73 + "version" integer NOT NULL,
74 + "actor_id" uuid,
75 + "snapshot" jsonb NOT NULL,
76 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
77 +);
78 +--> statement-breakpoint
79 +CREATE TABLE "listings" (
80 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
81 + "slug" text NOT NULL,
82 + "property_id" uuid NOT NULL,
83 + "owner_user_id" uuid,
84 + "listing_type" text NOT NULL,
85 + "source_type" text DEFAULT 'DIRECT_OWNER' NOT NULL,
86 + "status" text DEFAULT 'DRAFT' NOT NULL,
87 + "title" text,
88 + "description" text,
89 + "price_amount" numeric(12, 2),
90 + "currency" text DEFAULT 'CAD' NOT NULL,
91 + "rental_period" text,
92 + "availability_date" text,
93 + "furnished" boolean,
94 + "pets_policy" text,
95 + "smoking_policy" text,
96 + "parking" text,
97 + "lease_terms" text,
98 + "inclusions" jsonb DEFAULT '[]'::jsonb NOT NULL,
99 + "features" jsonb DEFAULT '{}'::jsonb NOT NULL,
100 + "publication_at" timestamp with time zone,
101 + "expires_at" timestamp with time zone,
102 + "moderation_state" text DEFAULT 'NONE' NOT NULL,
103 + "moderation_note" text,
104 + "external_url" text,
105 + "upstream_source" text,
106 + "upstream_id" text,
107 + "upstream_sync_at" timestamp with time zone,
108 + "wizard_step" integer DEFAULT 0 NOT NULL,
109 + "attested_at" timestamp with time zone,
110 + "version" integer DEFAULT 1 NOT NULL,
111 + "views_count" integer DEFAULT 0 NOT NULL,
112 + "created_at" timestamp with time zone DEFAULT now() NOT NULL,
113 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
114 + CONSTRAINT "listings_type_ck" CHECK ("listings"."listing_type" in ('SALE','RENT')),
115 + CONSTRAINT "listings_source_ck" CHECK ("listings"."source_type" in ('DIRECT_OWNER','DIRECT_AGENT','AGGREGATED_PARTNER')),
116 + CONSTRAINT "listings_status_ck" CHECK ("listings"."status" in ('DRAFT','PENDING_REVIEW','PUBLISHED','PAUSED','REJECTED','ARCHIVED','SOLD','RENTED')),
117 + CONSTRAINT "listings_price_ck" CHECK ("listings"."price_amount" is null or "listings"."price_amount" >= 0),
118 + CONSTRAINT "listings_owner_direct_ck" CHECK ("listings"."source_type" = 'AGGREGATED_PARTNER' or "listings"."owner_user_id" is not null),
119 + CONSTRAINT "listings_rent_period_ck" CHECK ("listings"."listing_type" <> 'RENT' or "listings"."rental_period" is null or "listings"."rental_period" in ('MONTH','WEEK'))
120 +);
121 +--> statement-breakpoint
122 +CREATE TABLE "messages" (
123 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
124 + "inquiry_id" uuid NOT NULL,
125 + "sender_id" uuid NOT NULL,
126 + "body" text NOT NULL,
127 + "idempotency_key" text,
128 + "created_at" timestamp with time zone DEFAULT now() NOT NULL,
129 + "read_at" timestamp with time zone
130 +);
131 +--> statement-breakpoint
132 +CREATE TABLE "moderation_actions" (
133 + "id" bigserial PRIMARY KEY NOT NULL,
134 + "actor_id" uuid NOT NULL,
135 + "target_type" text NOT NULL,
136 + "target_id" text NOT NULL,
137 + "action" text NOT NULL,
138 + "note" text,
139 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
140 +);
141 +--> statement-breakpoint
142 +CREATE TABLE "otp_challenges" (
143 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
144 + "email_normalized" text NOT NULL,
145 + "purpose" text DEFAULT 'LOGIN' NOT NULL,
146 + "secret_digest" text NOT NULL,
147 + "expires_at" timestamp with time zone NOT NULL,
148 + "attempts" integer DEFAULT 0 NOT NULL,
149 + "consumed_at" timestamp with time zone,
150 + "requested_ip_hash" text,
151 + "meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
152 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
153 +);
154 +--> statement-breakpoint
155 +CREATE TABLE "properties" (
156 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
157 + "canonical_address" text,
158 + "unit" text,
159 + "city" text,
160 + "region" text,
161 + "postal_code" text,
162 + "lat" double precision,
163 + "lng" double precision,
164 + "geographic_precision" text DEFAULT 'EXACT' NOT NULL,
165 + "geo_source" text,
166 + "role_unit_id" text,
167 + "property_type" text NOT NULL,
168 + "bedrooms" numeric(4, 1),
169 + "bathrooms" numeric(4, 1),
170 + "area_sqft" integer,
171 + "lot_sqft" integer,
172 + "year_built" integer,
173 + "privacy_level" text DEFAULT 'EXACT' NOT NULL,
174 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
175 +);
176 +--> statement-breakpoint
177 +CREATE TABLE "rate_limits" (
178 + "key" text NOT NULL,
179 + "window_start" timestamp with time zone NOT NULL,
180 + "count" integer DEFAULT 0 NOT NULL,
181 + CONSTRAINT "rate_limits_key_window_start_pk" PRIMARY KEY("key","window_start")
182 +);
183 +--> statement-breakpoint
184 +CREATE TABLE "reports" (
185 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
186 + "target_type" text NOT NULL,
187 + "target_id" text NOT NULL,
188 + "reporter_id" uuid,
189 + "reason" text NOT NULL,
190 + "details" text,
191 + "status" text DEFAULT 'OPEN' NOT NULL,
192 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
193 +);
194 +--> statement-breakpoint
195 +CREATE TABLE "saved_searches" (
196 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
197 + "user_id" uuid NOT NULL,
198 + "label" text NOT NULL,
199 + "query" jsonb NOT NULL,
200 + "alert_enabled" boolean DEFAULT false NOT NULL,
201 + "alert_frequency" text DEFAULT 'DAILY' NOT NULL,
202 + "last_alert_at" timestamp with time zone,
203 + "created_at" timestamp with time zone DEFAULT now() NOT NULL
204 +);
205 +--> statement-breakpoint
206 +CREATE TABLE "sessions" (
207 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
208 + "user_id" uuid NOT NULL,
209 + "token_digest" text NOT NULL,
210 + "issued_at" timestamp with time zone DEFAULT now() NOT NULL,
211 + "last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
212 + "expires_at" timestamp with time zone NOT NULL,
213 + "revoked_at" timestamp with time zone,
214 + "device_label" text,
215 + "ip_hash" text,
216 + "mfa_at" timestamp with time zone
217 +);
218 +--> statement-breakpoint
219 +CREATE TABLE "user_profiles" (
220 + "user_id" uuid PRIMARY KEY NOT NULL,
221 + "public_name" text,
222 + "bio" text,
223 + "avatar_key" text,
224 + "seller_type" text DEFAULT 'OWNER' NOT NULL,
225 + "verification_status" text DEFAULT 'NONE' NOT NULL,
226 + "contact_preferences" jsonb DEFAULT '{"email":true,"phone":false}'::jsonb NOT NULL,
227 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL
228 +);
229 +--> statement-breakpoint
230 +CREATE TABLE "user_roles" (
231 + "user_id" uuid NOT NULL,
232 + "role" text NOT NULL,
233 + "granted_by" uuid,
234 + "granted_at" timestamp with time zone DEFAULT now() NOT NULL,
235 + "note" text,
236 + CONSTRAINT "user_roles_user_id_role_pk" PRIMARY KEY("user_id","role"),
237 + CONSTRAINT "user_roles_role_ck" CHECK ("user_roles"."role" in ('ADMIN','MODERATOR','AGENT_VERIFIED'))
238 +);
239 +--> statement-breakpoint
240 +CREATE TABLE "users" (
241 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
242 + "email" text NOT NULL,
243 + "email_verified_at" timestamp with time zone,
244 + "display_name" text,
245 + "phone_encrypted" text,
246 + "locale" text DEFAULT 'fr-CA' NOT NULL,
247 + "status" text DEFAULT 'ACTIVE' NOT NULL,
248 + "prefs" jsonb DEFAULT '{}'::jsonb NOT NULL,
249 + "totp_secret_encrypted" text,
250 + "created_at" timestamp with time zone DEFAULT now() NOT NULL,
251 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
252 + "deleted_at" timestamp with time zone,
253 + CONSTRAINT "users_status_ck" CHECK ("users"."status" in ('ACTIVE','SUSPENDED','DELETED'))
254 +);
255 +--> statement-breakpoint
256 +CREATE TABLE "viewing_requests" (
257 + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
258 + "listing_id" uuid NOT NULL,
259 + "inquiry_id" uuid,
260 + "requester_id" uuid NOT NULL,
261 + "owner_id" uuid NOT NULL,
262 + "proposed_slots" jsonb NOT NULL,
263 + "confirmed_slot" timestamp with time zone,
264 + "status" text DEFAULT 'REQUESTED' NOT NULL,
265 + "note" text,
266 + "created_at" timestamp with time zone DEFAULT now() NOT NULL,
267 + "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
268 + CONSTRAINT "viewings_status_ck" CHECK ("viewing_requests"."status" in ('REQUESTED','CONFIRMED','CANCELLED','DONE'))
269 +);
270 +--> statement-breakpoint
271 +ALTER TABLE "favorites" ADD CONSTRAINT "favorites_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
272 +ALTER TABLE "inquiries" ADD CONSTRAINT "inquiries_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
273 +ALTER TABLE "inquiries" ADD CONSTRAINT "inquiries_sender_user_id_users_id_fk" FOREIGN KEY ("sender_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
274 +ALTER TABLE "inquiries" ADD CONSTRAINT "inquiries_recipient_user_id_users_id_fk" FOREIGN KEY ("recipient_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
275 +ALTER TABLE "listing_claims" ADD CONSTRAINT "listing_claims_requester_id_users_id_fk" FOREIGN KEY ("requester_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
276 +ALTER TABLE "listing_media" ADD CONSTRAINT "listing_media_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
277 +ALTER TABLE "listing_versions" ADD CONSTRAINT "listing_versions_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
278 +ALTER TABLE "listings" ADD CONSTRAINT "listings_property_id_properties_id_fk" FOREIGN KEY ("property_id") REFERENCES "public"."properties"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
279 +ALTER TABLE "listings" ADD CONSTRAINT "listings_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
280 +ALTER TABLE "messages" ADD CONSTRAINT "messages_inquiry_id_inquiries_id_fk" FOREIGN KEY ("inquiry_id") REFERENCES "public"."inquiries"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
281 +ALTER TABLE "messages" ADD CONSTRAINT "messages_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
282 +ALTER TABLE "reports" ADD CONSTRAINT "reports_reporter_id_users_id_fk" FOREIGN KEY ("reporter_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
283 +ALTER TABLE "saved_searches" ADD CONSTRAINT "saved_searches_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
284 +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
285 +ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
286 +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
287 +ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_listing_id_listings_id_fk" FOREIGN KEY ("listing_id") REFERENCES "public"."listings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
288 +ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_inquiry_id_inquiries_id_fk" FOREIGN KEY ("inquiry_id") REFERENCES "public"."inquiries"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
289 +ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_requester_id_users_id_fk" FOREIGN KEY ("requester_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
290 +ALTER TABLE "viewing_requests" ADD CONSTRAINT "viewing_requests_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
291 +CREATE INDEX "audit_created_idx" ON "audit_events" USING btree ("created_at");--> statement-breakpoint
292 +CREATE INDEX "audit_actor_idx" ON "audit_events" USING btree ("actor_id");--> statement-breakpoint
293 +CREATE INDEX "email_deliveries_created_idx" ON "email_deliveries" USING btree ("created_at");--> statement-breakpoint
294 +CREATE INDEX "inquiries_listing_idx" ON "inquiries" USING btree ("listing_id");--> statement-breakpoint
295 +CREATE INDEX "inquiries_sender_idx" ON "inquiries" USING btree ("sender_user_id");--> statement-breakpoint
296 +CREATE INDEX "inquiries_recipient_idx" ON "inquiries" USING btree ("recipient_user_id");--> statement-breakpoint
297 +CREATE UNIQUE INDEX "inquiries_unique_thread" ON "inquiries" USING btree ("listing_id","sender_user_id");--> statement-breakpoint
298 +CREATE INDEX "listing_claims_uid_idx" ON "listing_claims" USING btree ("aggregated_uid");--> statement-breakpoint
299 +CREATE INDEX "listing_media_listing_idx" ON "listing_media" USING btree ("listing_id","position");--> statement-breakpoint
300 +CREATE INDEX "listing_versions_listing_idx" ON "listing_versions" USING btree ("listing_id","version");--> statement-breakpoint
301 +CREATE UNIQUE INDEX "listings_slug_uq" ON "listings" USING btree ("slug");--> statement-breakpoint
302 +CREATE INDEX "listings_status_idx" ON "listings" USING btree ("status","listing_type","publication_at");--> statement-breakpoint
303 +CREATE INDEX "listings_owner_idx" ON "listings" USING btree ("owner_user_id");--> statement-breakpoint
304 +CREATE UNIQUE INDEX "listings_upstream_uq" ON "listings" USING btree ("upstream_source","upstream_id");--> statement-breakpoint
305 +CREATE INDEX "messages_inquiry_idx" ON "messages" USING btree ("inquiry_id","created_at");--> statement-breakpoint
306 +CREATE UNIQUE INDEX "messages_idem_uq" ON "messages" USING btree ("inquiry_id","idempotency_key");--> statement-breakpoint
307 +CREATE INDEX "otp_email_idx" ON "otp_challenges" USING btree ("email_normalized","created_at");--> statement-breakpoint
308 +CREATE INDEX "otp_expires_idx" ON "otp_challenges" USING btree ("expires_at");--> statement-breakpoint
309 +CREATE INDEX "properties_geo_idx" ON "properties" USING btree ("lat","lng");--> statement-breakpoint
310 +CREATE INDEX "properties_city_idx" ON "properties" USING btree ("city");--> statement-breakpoint
311 +CREATE INDEX "reports_status_idx" ON "reports" USING btree ("status","created_at");--> statement-breakpoint
312 +CREATE INDEX "saved_searches_user_idx" ON "saved_searches" USING btree ("user_id");--> statement-breakpoint
313 +CREATE UNIQUE INDEX "sessions_token_uq" ON "sessions" USING btree ("token_digest");--> statement-breakpoint
314 +CREATE INDEX "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
315 +CREATE UNIQUE INDEX "users_email_uq" ON "users" USING btree ("email");--> statement-breakpoint
316 +CREATE INDEX "viewings_listing_idx" ON "viewing_requests" USING btree ("listing_id");--> statement-breakpoint
317 +CREATE INDEX "viewings_requester_idx" ON "viewing_requests" USING btree ("requester_id");--> statement-breakpoint
318 +CREATE INDEX "viewings_owner_idx" ON "viewing_requests" USING btree ("owner_id");
\ No newline at end of file
added drizzle/meta/0000_snapshot.json +2377 −0
@@ -0,0 +1,2377 @@
1 +{
2 + "id": "b60423f1-a506-4cde-9fb2-100a0462ff66",
3 + "prevId": "00000000-0000-0000-0000-000000000000",
4 + "version": "7",
5 + "dialect": "postgresql",
6 + "tables": {
7 + "public.audit_events": {
8 + "name": "audit_events",
9 + "schema": "",
10 + "columns": {
11 + "id": {
12 + "name": "id",
13 + "type": "bigserial",
14 + "primaryKey": true,
15 + "notNull": true
16 + },
17 + "actor_id": {
18 + "name": "actor_id",
19 + "type": "uuid",
20 + "primaryKey": false,
21 + "notNull": false
22 + },
23 + "action": {
24 + "name": "action",
25 + "type": "text",
26 + "primaryKey": false,
27 + "notNull": true
28 + },
29 + "object_type": {
30 + "name": "object_type",
31 + "type": "text",
32 + "primaryKey": false,
33 + "notNull": false
34 + },
35 + "object_id": {
36 + "name": "object_id",
37 + "type": "text",
38 + "primaryKey": false,
39 + "notNull": false
40 + },
41 + "ip_hash": {
42 + "name": "ip_hash",
43 + "type": "text",
44 + "primaryKey": false,
45 + "notNull": false
46 + },
47 + "metadata_redacted": {
48 + "name": "metadata_redacted",
49 + "type": "jsonb",
50 + "primaryKey": false,
51 + "notNull": true,
52 + "default": "'{}'::jsonb"
53 + },
54 + "created_at": {
55 + "name": "created_at",
56 + "type": "timestamp with time zone",
57 + "primaryKey": false,
58 + "notNull": true,
59 + "default": "now()"
60 + }
61 + },
62 + "indexes": {
63 + "audit_created_idx": {
64 + "name": "audit_created_idx",
65 + "columns": [
66 + {
67 + "expression": "created_at",
68 + "isExpression": false,
69 + "asc": true,
70 + "nulls": "last"
71 + }
72 + ],
73 + "isUnique": false,
74 + "concurrently": false,
75 + "method": "btree",
76 + "with": {}
77 + },
78 + "audit_actor_idx": {
79 + "name": "audit_actor_idx",
80 + "columns": [
81 + {
82 + "expression": "actor_id",
83 + "isExpression": false,
84 + "asc": true,
85 + "nulls": "last"
86 + }
87 + ],
88 + "isUnique": false,
89 + "concurrently": false,
90 + "method": "btree",
91 + "with": {}
92 + }
93 + },
94 + "foreignKeys": {},
95 + "compositePrimaryKeys": {},
96 + "uniqueConstraints": {},
97 + "policies": {},
98 + "checkConstraints": {},
99 + "isRLSEnabled": false
100 + },
101 + "public.email_deliveries": {
102 + "name": "email_deliveries",
103 + "schema": "",
104 + "columns": {
105 + "id": {
106 + "name": "id",
107 + "type": "uuid",
108 + "primaryKey": true,
109 + "notNull": true,
110 + "default": "gen_random_uuid()"
111 + },
112 + "to_hash": {
113 + "name": "to_hash",
114 + "type": "text",
115 + "primaryKey": false,
116 + "notNull": true
117 + },
118 + "template": {
119 + "name": "template",
120 + "type": "text",
121 + "primaryKey": false,
122 + "notNull": true
123 + },
124 + "provider_id": {
125 + "name": "provider_id",
126 + "type": "text",
127 + "primaryKey": false,
128 + "notNull": false
129 + },
130 + "status": {
131 + "name": "status",
132 + "type": "text",
133 + "primaryKey": false,
134 + "notNull": true
135 + },
136 + "error": {
137 + "name": "error",
138 + "type": "text",
139 + "primaryKey": false,
140 + "notNull": false
141 + },
142 + "created_at": {
143 + "name": "created_at",
144 + "type": "timestamp with time zone",
145 + "primaryKey": false,
146 + "notNull": true,
147 + "default": "now()"
148 + }
149 + },
150 + "indexes": {
151 + "email_deliveries_created_idx": {
152 + "name": "email_deliveries_created_idx",
153 + "columns": [
154 + {
155 + "expression": "created_at",
156 + "isExpression": false,
157 + "asc": true,
158 + "nulls": "last"
159 + }
160 + ],
161 + "isUnique": false,
162 + "concurrently": false,
163 + "method": "btree",
164 + "with": {}
165 + }
166 + },
167 + "foreignKeys": {},
168 + "compositePrimaryKeys": {},
169 + "uniqueConstraints": {},
170 + "policies": {},
171 + "checkConstraints": {},
172 + "isRLSEnabled": false
173 + },
174 + "public.favorites": {
175 + "name": "favorites",
176 + "schema": "",
177 + "columns": {
178 + "user_id": {
179 + "name": "user_id",
180 + "type": "uuid",
181 + "primaryKey": false,
182 + "notNull": true
183 + },
184 + "listing_uid": {
185 + "name": "listing_uid",
186 + "type": "text",
187 + "primaryKey": false,
188 + "notNull": true
189 + },
190 + "created_at": {
191 + "name": "created_at",
192 + "type": "timestamp with time zone",
193 + "primaryKey": false,
194 + "notNull": true,
195 + "default": "now()"
196 + }
197 + },
198 + "indexes": {},
199 + "foreignKeys": {
200 + "favorites_user_id_users_id_fk": {
201 + "name": "favorites_user_id_users_id_fk",
202 + "tableFrom": "favorites",
203 + "tableTo": "users",
204 + "columnsFrom": [
205 + "user_id"
206 + ],
207 + "columnsTo": [
208 + "id"
209 + ],
210 + "onDelete": "cascade",
211 + "onUpdate": "no action"
212 + }
213 + },
214 + "compositePrimaryKeys": {
215 + "favorites_user_id_listing_uid_pk": {
216 + "name": "favorites_user_id_listing_uid_pk",
217 + "columns": [
218 + "user_id",
219 + "listing_uid"
220 + ]
221 + }
222 + },
223 + "uniqueConstraints": {},
224 + "policies": {},
225 + "checkConstraints": {},
226 + "isRLSEnabled": false
227 + },
228 + "public.inquiries": {
229 + "name": "inquiries",
230 + "schema": "",
231 + "columns": {
232 + "id": {
233 + "name": "id",
234 + "type": "uuid",
235 + "primaryKey": true,
236 + "notNull": true,
237 + "default": "gen_random_uuid()"
238 + },
239 + "listing_id": {
240 + "name": "listing_id",
241 + "type": "uuid",
242 + "primaryKey": false,
243 + "notNull": true
244 + },
245 + "sender_user_id": {
246 + "name": "sender_user_id",
247 + "type": "uuid",
248 + "primaryKey": false,
249 + "notNull": true
250 + },
251 + "recipient_user_id": {
252 + "name": "recipient_user_id",
253 + "type": "uuid",
254 + "primaryKey": false,
255 + "notNull": true
256 + },
257 + "category": {
258 + "name": "category",
259 + "type": "text",
260 + "primaryKey": false,
261 + "notNull": true,
262 + "default": "'INFO'"
263 + },
264 + "status": {
265 + "name": "status",
266 + "type": "text",
267 + "primaryKey": false,
268 + "notNull": true,
269 + "default": "'OPEN'"
270 + },
271 + "last_message_at": {
272 + "name": "last_message_at",
273 + "type": "timestamp with time zone",
274 + "primaryKey": false,
275 + "notNull": true,
276 + "default": "now()"
277 + },
278 + "created_at": {
279 + "name": "created_at",
280 + "type": "timestamp with time zone",
281 + "primaryKey": false,
282 + "notNull": true,
283 + "default": "now()"
284 + }
285 + },
286 + "indexes": {
287 + "inquiries_listing_idx": {
288 + "name": "inquiries_listing_idx",
289 + "columns": [
290 + {
291 + "expression": "listing_id",
292 + "isExpression": false,
293 + "asc": true,
294 + "nulls": "last"
295 + }
296 + ],
297 + "isUnique": false,
298 + "concurrently": false,
299 + "method": "btree",
300 + "with": {}
301 + },
302 + "inquiries_sender_idx": {
303 + "name": "inquiries_sender_idx",
304 + "columns": [
305 + {
306 + "expression": "sender_user_id",
307 + "isExpression": false,
308 + "asc": true,
309 + "nulls": "last"
310 + }
311 + ],
312 + "isUnique": false,
313 + "concurrently": false,
314 + "method": "btree",
315 + "with": {}
316 + },
317 + "inquiries_recipient_idx": {
318 + "name": "inquiries_recipient_idx",
319 + "columns": [
320 + {
321 + "expression": "recipient_user_id",
322 + "isExpression": false,
323 + "asc": true,
324 + "nulls": "last"
325 + }
326 + ],
327 + "isUnique": false,
328 + "concurrently": false,
329 + "method": "btree",
330 + "with": {}
331 + },
332 + "inquiries_unique_thread": {
333 + "name": "inquiries_unique_thread",
334 + "columns": [
335 + {
336 + "expression": "listing_id",
337 + "isExpression": false,
338 + "asc": true,
339 + "nulls": "last"
340 + },
341 + {
342 + "expression": "sender_user_id",
343 + "isExpression": false,
344 + "asc": true,
345 + "nulls": "last"
346 + }
347 + ],
348 + "isUnique": true,
349 + "concurrently": false,
350 + "method": "btree",
351 + "with": {}
352 + }
353 + },
354 + "foreignKeys": {
355 + "inquiries_listing_id_listings_id_fk": {
356 + "name": "inquiries_listing_id_listings_id_fk",
357 + "tableFrom": "inquiries",
358 + "tableTo": "listings",
359 + "columnsFrom": [
360 + "listing_id"
361 + ],
362 + "columnsTo": [
363 + "id"
364 + ],
365 + "onDelete": "cascade",
366 + "onUpdate": "no action"
367 + },
368 + "inquiries_sender_user_id_users_id_fk": {
369 + "name": "inquiries_sender_user_id_users_id_fk",
370 + "tableFrom": "inquiries",
371 + "tableTo": "users",
372 + "columnsFrom": [
373 + "sender_user_id"
374 + ],
375 + "columnsTo": [
376 + "id"
377 + ],
378 + "onDelete": "cascade",
379 + "onUpdate": "no action"
380 + },
381 + "inquiries_recipient_user_id_users_id_fk": {
382 + "name": "inquiries_recipient_user_id_users_id_fk",
383 + "tableFrom": "inquiries",
384 + "tableTo": "users",
385 + "columnsFrom": [
386 + "recipient_user_id"
387 + ],
388 + "columnsTo": [
389 + "id"
390 + ],
391 + "onDelete": "cascade",
392 + "onUpdate": "no action"
393 + }
394 + },
395 + "compositePrimaryKeys": {},
396 + "uniqueConstraints": {},
397 + "policies": {},
398 + "checkConstraints": {},
399 + "isRLSEnabled": false
400 + },
401 + "public.listing_claims": {
402 + "name": "listing_claims",
403 + "schema": "",
404 + "columns": {
405 + "id": {
406 + "name": "id",
407 + "type": "uuid",
408 + "primaryKey": true,
409 + "notNull": true,
410 + "default": "gen_random_uuid()"
411 + },
412 + "aggregated_uid": {
413 + "name": "aggregated_uid",
414 + "type": "text",
415 + "primaryKey": false,
416 + "notNull": true
417 + },
418 + "requester_id": {
419 + "name": "requester_id",
420 + "type": "uuid",
421 + "primaryKey": false,
422 + "notNull": true
423 + },
424 + "status": {
425 + "name": "status",
426 + "type": "text",
427 + "primaryKey": false,
428 + "notNull": true,
429 + "default": "'PENDING'"
430 + },
431 + "message": {
432 + "name": "message",
433 + "type": "text",
434 + "primaryKey": false,
435 + "notNull": false
436 + },
437 + "proof_keys": {
438 + "name": "proof_keys",
439 + "type": "jsonb",
440 + "primaryKey": false,
441 + "notNull": true,
442 + "default": "'[]'::jsonb"
443 + },
444 + "reviewed_by": {
445 + "name": "reviewed_by",
446 + "type": "uuid",
447 + "primaryKey": false,
448 + "notNull": false
449 + },
450 + "reviewed_at": {
451 + "name": "reviewed_at",
452 + "type": "timestamp with time zone",
453 + "primaryKey": false,
454 + "notNull": false
455 + },
456 + "created_at": {
457 + "name": "created_at",
458 + "type": "timestamp with time zone",
459 + "primaryKey": false,
460 + "notNull": true,
461 + "default": "now()"
462 + }
463 + },
464 + "indexes": {
465 + "listing_claims_uid_idx": {
466 + "name": "listing_claims_uid_idx",
467 + "columns": [
468 + {
469 + "expression": "aggregated_uid",
470 + "isExpression": false,
471 + "asc": true,
472 + "nulls": "last"
473 + }
474 + ],
475 + "isUnique": false,
476 + "concurrently": false,
477 + "method": "btree",
478 + "with": {}
479 + }
480 + },
481 + "foreignKeys": {
482 + "listing_claims_requester_id_users_id_fk": {
483 + "name": "listing_claims_requester_id_users_id_fk",
484 + "tableFrom": "listing_claims",
485 + "tableTo": "users",
486 + "columnsFrom": [
487 + "requester_id"
488 + ],
489 + "columnsTo": [
490 + "id"
491 + ],
492 + "onDelete": "cascade",
493 + "onUpdate": "no action"
494 + }
495 + },
496 + "compositePrimaryKeys": {},
497 + "uniqueConstraints": {},
498 + "policies": {},
499 + "checkConstraints": {},
500 + "isRLSEnabled": false
501 + },
502 + "public.listing_media": {
503 + "name": "listing_media",
504 + "schema": "",
505 + "columns": {
506 + "id": {
507 + "name": "id",
508 + "type": "uuid",
509 + "primaryKey": true,
510 + "notNull": true,
511 + "default": "gen_random_uuid()"
512 + },
513 + "listing_id": {
514 + "name": "listing_id",
515 + "type": "uuid",
516 + "primaryKey": false,
517 + "notNull": true
518 + },
519 + "storage_key": {
520 + "name": "storage_key",
521 + "type": "text",
522 + "primaryKey": false,
523 + "notNull": true
524 + },
525 + "media_type": {
526 + "name": "media_type",
527 + "type": "text",
528 + "primaryKey": false,
529 + "notNull": true,
530 + "default": "'IMAGE'"
531 + },
532 + "mime_type": {
533 + "name": "mime_type",
534 + "type": "text",
535 + "primaryKey": false,
536 + "notNull": false
537 + },
538 + "position": {
539 + "name": "position",
540 + "type": "integer",
541 + "primaryKey": false,
542 + "notNull": true,
543 + "default": 0
544 + },
545 + "alt_text": {
546 + "name": "alt_text",
547 + "type": "text",
548 + "primaryKey": false,
549 + "notNull": false
550 + },
551 + "license_status": {
552 + "name": "license_status",
553 + "type": "text",
554 + "primaryKey": false,
555 + "notNull": true,
556 + "default": "'OWNER_DECLARED'"
557 + },
558 + "scan_status": {
559 + "name": "scan_status",
560 + "type": "text",
561 + "primaryKey": false,
562 + "notNull": true,
563 + "default": "'PENDING'"
564 + },
565 + "width": {
566 + "name": "width",
567 + "type": "integer",
568 + "primaryKey": false,
569 + "notNull": false
570 + },
571 + "height": {
572 + "name": "height",
573 + "type": "integer",
574 + "primaryKey": false,
575 + "notNull": false
576 + },
577 + "bytes": {
578 + "name": "bytes",
579 + "type": "integer",
580 + "primaryKey": false,
581 + "notNull": false
582 + },
583 + "status": {
584 + "name": "status",
585 + "type": "text",
586 + "primaryKey": false,
587 + "notNull": true,
588 + "default": "'UPLOADING'"
589 + },
590 + "upload_token": {
591 + "name": "upload_token",
592 + "type": "text",
593 + "primaryKey": false,
594 + "notNull": false
595 + },
596 + "uploaded_at": {
597 + "name": "uploaded_at",
598 + "type": "timestamp with time zone",
599 + "primaryKey": false,
600 + "notNull": true,
601 + "default": "now()"
602 + }
603 + },
604 + "indexes": {
605 + "listing_media_listing_idx": {
606 + "name": "listing_media_listing_idx",
607 + "columns": [
608 + {
609 + "expression": "listing_id",
610 + "isExpression": false,
611 + "asc": true,
612 + "nulls": "last"
613 + },
614 + {
615 + "expression": "position",
616 + "isExpression": false,
617 + "asc": true,
618 + "nulls": "last"
619 + }
620 + ],
621 + "isUnique": false,
622 + "concurrently": false,
623 + "method": "btree",
624 + "with": {}
625 + }
626 + },
627 + "foreignKeys": {
628 + "listing_media_listing_id_listings_id_fk": {
629 + "name": "listing_media_listing_id_listings_id_fk",
630 + "tableFrom": "listing_media",
631 + "tableTo": "listings",
632 + "columnsFrom": [
633 + "listing_id"
634 + ],
635 + "columnsTo": [
636 + "id"
637 + ],
638 + "onDelete": "cascade",
639 + "onUpdate": "no action"
640 + }
641 + },
642 + "compositePrimaryKeys": {},
643 + "uniqueConstraints": {},
644 + "policies": {},
645 + "checkConstraints": {},
646 + "isRLSEnabled": false
647 + },
648 + "public.listing_versions": {
649 + "name": "listing_versions",
650 + "schema": "",
651 + "columns": {
652 + "id": {
653 + "name": "id",
654 + "type": "bigserial",
655 + "primaryKey": true,
656 + "notNull": true
657 + },
658 + "listing_id": {
659 + "name": "listing_id",
660 + "type": "uuid",
661 + "primaryKey": false,
662 + "notNull": true
663 + },
664 + "version": {
665 + "name": "version",
666 + "type": "integer",
667 + "primaryKey": false,
668 + "notNull": true
669 + },
670 + "actor_id": {
671 + "name": "actor_id",
672 + "type": "uuid",
673 + "primaryKey": false,
674 + "notNull": false
675 + },
676 + "snapshot": {
677 + "name": "snapshot",
678 + "type": "jsonb",
679 + "primaryKey": false,
680 + "notNull": true
681 + },
682 + "created_at": {
683 + "name": "created_at",
684 + "type": "timestamp with time zone",
685 + "primaryKey": false,
686 + "notNull": true,
687 + "default": "now()"
688 + }
689 + },
690 + "indexes": {
691 + "listing_versions_listing_idx": {
692 + "name": "listing_versions_listing_idx",
693 + "columns": [
694 + {
695 + "expression": "listing_id",
696 + "isExpression": false,
697 + "asc": true,
698 + "nulls": "last"
699 + },
700 + {
701 + "expression": "version",
702 + "isExpression": false,
703 + "asc": true,
704 + "nulls": "last"
705 + }
706 + ],
707 + "isUnique": false,
708 + "concurrently": false,
709 + "method": "btree",
710 + "with": {}
711 + }
712 + },
713 + "foreignKeys": {
714 + "listing_versions_listing_id_listings_id_fk": {
715 + "name": "listing_versions_listing_id_listings_id_fk",
716 + "tableFrom": "listing_versions",
717 + "tableTo": "listings",
718 + "columnsFrom": [
719 + "listing_id"
720 + ],
721 + "columnsTo": [
722 + "id"
723 + ],
724 + "onDelete": "cascade",
725 + "onUpdate": "no action"
726 + }
727 + },
728 + "compositePrimaryKeys": {},
729 + "uniqueConstraints": {},
730 + "policies": {},
731 + "checkConstraints": {},
732 + "isRLSEnabled": false
733 + },
734 + "public.listings": {
735 + "name": "listings",
736 + "schema": "",
737 + "columns": {
738 + "id": {
739 + "name": "id",
740 + "type": "uuid",
741 + "primaryKey": true,
742 + "notNull": true,
743 + "default": "gen_random_uuid()"
744 + },
745 + "slug": {
746 + "name": "slug",
747 + "type": "text",
748 + "primaryKey": false,
749 + "notNull": true
750 + },
751 + "property_id": {
752 + "name": "property_id",
753 + "type": "uuid",
754 + "primaryKey": false,
755 + "notNull": true
756 + },
757 + "owner_user_id": {
758 + "name": "owner_user_id",
759 + "type": "uuid",
760 + "primaryKey": false,
761 + "notNull": false
762 + },
763 + "listing_type": {
764 + "name": "listing_type",
765 + "type": "text",
766 + "primaryKey": false,
767 + "notNull": true
768 + },
769 + "source_type": {
770 + "name": "source_type",
771 + "type": "text",
772 + "primaryKey": false,
773 + "notNull": true,
774 + "default": "'DIRECT_OWNER'"
775 + },
776 + "status": {
777 + "name": "status",
778 + "type": "text",
779 + "primaryKey": false,
780 + "notNull": true,
781 + "default": "'DRAFT'"
782 + },
783 + "title": {
784 + "name": "title",
785 + "type": "text",
786 + "primaryKey": false,
787 + "notNull": false
788 + },
789 + "description": {
790 + "name": "description",
791 + "type": "text",
792 + "primaryKey": false,
793 + "notNull": false
794 + },
795 + "price_amount": {
796 + "name": "price_amount",
797 + "type": "numeric(12, 2)",
798 + "primaryKey": false,
799 + "notNull": false
800 + },
801 + "currency": {
802 + "name": "currency",
803 + "type": "text",
804 + "primaryKey": false,
805 + "notNull": true,
806 + "default": "'CAD'"
807 + },
808 + "rental_period": {
809 + "name": "rental_period",
810 + "type": "text",
811 + "primaryKey": false,
812 + "notNull": false
813 + },
814 + "availability_date": {
815 + "name": "availability_date",
816 + "type": "text",
817 + "primaryKey": false,
818 + "notNull": false
819 + },
820 + "furnished": {
821 + "name": "furnished",
822 + "type": "boolean",
823 + "primaryKey": false,
824 + "notNull": false
825 + },
826 + "pets_policy": {
827 + "name": "pets_policy",
828 + "type": "text",
829 + "primaryKey": false,
830 + "notNull": false
831 + },
832 + "smoking_policy": {
833 + "name": "smoking_policy",
834 + "type": "text",
835 + "primaryKey": false,
836 + "notNull": false
837 + },
838 + "parking": {
839 + "name": "parking",
840 + "type": "text",
841 + "primaryKey": false,
842 + "notNull": false
843 + },
844 + "lease_terms": {
845 + "name": "lease_terms",
846 + "type": "text",
847 + "primaryKey": false,
848 + "notNull": false
849 + },
850 + "inclusions": {
851 + "name": "inclusions",
852 + "type": "jsonb",
853 + "primaryKey": false,
854 + "notNull": true,
855 + "default": "'[]'::jsonb"
856 + },
857 + "features": {
858 + "name": "features",
859 + "type": "jsonb",
860 + "primaryKey": false,
861 + "notNull": true,
862 + "default": "'{}'::jsonb"
863 + },
864 + "publication_at": {
865 + "name": "publication_at",
866 + "type": "timestamp with time zone",
867 + "primaryKey": false,
868 + "notNull": false
869 + },
870 + "expires_at": {
871 + "name": "expires_at",
872 + "type": "timestamp with time zone",
873 + "primaryKey": false,
874 + "notNull": false
875 + },
876 + "moderation_state": {
877 + "name": "moderation_state",
878 + "type": "text",
879 + "primaryKey": false,
880 + "notNull": true,
881 + "default": "'NONE'"
882 + },
883 + "moderation_note": {
884 + "name": "moderation_note",
885 + "type": "text",
886 + "primaryKey": false,
887 + "notNull": false
888 + },
889 + "external_url": {
890 + "name": "external_url",
891 + "type": "text",
892 + "primaryKey": false,
893 + "notNull": false
894 + },
895 + "upstream_source": {
896 + "name": "upstream_source",
897 + "type": "text",
898 + "primaryKey": false,
899 + "notNull": false
900 + },
901 + "upstream_id": {
902 + "name": "upstream_id",
903 + "type": "text",
904 + "primaryKey": false,
905 + "notNull": false
906 + },
907 + "upstream_sync_at": {
908 + "name": "upstream_sync_at",
909 + "type": "timestamp with time zone",
910 + "primaryKey": false,
911 + "notNull": false
912 + },
913 + "wizard_step": {
914 + "name": "wizard_step",
915 + "type": "integer",
916 + "primaryKey": false,
917 + "notNull": true,
918 + "default": 0
919 + },
920 + "attested_at": {
921 + "name": "attested_at",
922 + "type": "timestamp with time zone",
923 + "primaryKey": false,
924 + "notNull": false
925 + },
926 + "version": {
927 + "name": "version",
928 + "type": "integer",
929 + "primaryKey": false,
930 + "notNull": true,
931 + "default": 1
932 + },
933 + "views_count": {
934 + "name": "views_count",
935 + "type": "integer",
936 + "primaryKey": false,
937 + "notNull": true,
938 + "default": 0
939 + },
940 + "created_at": {
941 + "name": "created_at",
942 + "type": "timestamp with time zone",
943 + "primaryKey": false,
944 + "notNull": true,
945 + "default": "now()"
946 + },
947 + "updated_at": {
948 + "name": "updated_at",
949 + "type": "timestamp with time zone",
950 + "primaryKey": false,
951 + "notNull": true,
952 + "default": "now()"
953 + }
954 + },
955 + "indexes": {
956 + "listings_slug_uq": {
957 + "name": "listings_slug_uq",
958 + "columns": [
959 + {
960 + "expression": "slug",
961 + "isExpression": false,
962 + "asc": true,
963 + "nulls": "last"
964 + }
965 + ],
966 + "isUnique": true,
967 + "concurrently": false,
968 + "method": "btree",
969 + "with": {}
970 + },
971 + "listings_status_idx": {
972 + "name": "listings_status_idx",
973 + "columns": [
974 + {
975 + "expression": "status",
976 + "isExpression": false,
977 + "asc": true,
978 + "nulls": "last"
979 + },
980 + {
981 + "expression": "listing_type",
982 + "isExpression": false,
983 + "asc": true,
984 + "nulls": "last"
985 + },
986 + {
987 + "expression": "publication_at",
988 + "isExpression": false,
989 + "asc": true,
990 + "nulls": "last"
991 + }
992 + ],
993 + "isUnique": false,
994 + "concurrently": false,
995 + "method": "btree",
996 + "with": {}
997 + },
998 + "listings_owner_idx": {
999 + "name": "listings_owner_idx",
1000 + "columns": [
1001 + {
1002 + "expression": "owner_user_id",
1003 + "isExpression": false,
1004 + "asc": true,
1005 + "nulls": "last"
1006 + }
1007 + ],
1008 + "isUnique": false,
1009 + "concurrently": false,
1010 + "method": "btree",
1011 + "with": {}
1012 + },
1013 + "listings_upstream_uq": {
1014 + "name": "listings_upstream_uq",
1015 + "columns": [
1016 + {
1017 + "expression": "upstream_source",
1018 + "isExpression": false,
1019 + "asc": true,
1020 + "nulls": "last"
1021 + },
1022 + {
1023 + "expression": "upstream_id",
1024 + "isExpression": false,
1025 + "asc": true,
1026 + "nulls": "last"
1027 + }
1028 + ],
1029 + "isUnique": true,
1030 + "concurrently": false,
1031 + "method": "btree",
1032 + "with": {}
1033 + }
1034 + },
1035 + "foreignKeys": {
1036 + "listings_property_id_properties_id_fk": {
1037 + "name": "listings_property_id_properties_id_fk",
1038 + "tableFrom": "listings",
1039 + "tableTo": "properties",
1040 + "columnsFrom": [
1041 + "property_id"
1042 + ],
1043 + "columnsTo": [
1044 + "id"
1045 + ],
1046 + "onDelete": "restrict",
1047 + "onUpdate": "no action"
1048 + },
1049 + "listings_owner_user_id_users_id_fk": {
1050 + "name": "listings_owner_user_id_users_id_fk",
1051 + "tableFrom": "listings",
1052 + "tableTo": "users",
1053 + "columnsFrom": [
1054 + "owner_user_id"
1055 + ],
1056 + "columnsTo": [
1057 + "id"
1058 + ],
1059 + "onDelete": "set null",
1060 + "onUpdate": "no action"
1061 + }
1062 + },
1063 + "compositePrimaryKeys": {},
1064 + "uniqueConstraints": {},
1065 + "policies": {},
1066 + "checkConstraints": {
1067 + "listings_type_ck": {
1068 + "name": "listings_type_ck",
1069 + "value": "\"listings\".\"listing_type\" in ('SALE','RENT')"
1070 + },
1071 + "listings_source_ck": {
1072 + "name": "listings_source_ck",
1073 + "value": "\"listings\".\"source_type\" in ('DIRECT_OWNER','DIRECT_AGENT','AGGREGATED_PARTNER')"
1074 + },
1075 + "listings_status_ck": {
1076 + "name": "listings_status_ck",
1077 + "value": "\"listings\".\"status\" in ('DRAFT','PENDING_REVIEW','PUBLISHED','PAUSED','REJECTED','ARCHIVED','SOLD','RENTED')"
1078 + },
1079 + "listings_price_ck": {
1080 + "name": "listings_price_ck",
1081 + "value": "\"listings\".\"price_amount\" is null or \"listings\".\"price_amount\" >= 0"
1082 + },
1083 + "listings_owner_direct_ck": {
1084 + "name": "listings_owner_direct_ck",
1085 + "value": "\"listings\".\"source_type\" = 'AGGREGATED_PARTNER' or \"listings\".\"owner_user_id\" is not null"
1086 + },
1087 + "listings_rent_period_ck": {
1088 + "name": "listings_rent_period_ck",
1089 + "value": "\"listings\".\"listing_type\" <> 'RENT' or \"listings\".\"rental_period\" is null or \"listings\".\"rental_period\" in ('MONTH','WEEK')"
1090 + }
1091 + },
1092 + "isRLSEnabled": false
1093 + },
1094 + "public.messages": {
1095 + "name": "messages",
1096 + "schema": "",
1097 + "columns": {
1098 + "id": {
1099 + "name": "id",
1100 + "type": "uuid",
1101 + "primaryKey": true,
1102 + "notNull": true,
1103 + "default": "gen_random_uuid()"
1104 + },
1105 + "inquiry_id": {
1106 + "name": "inquiry_id",
1107 + "type": "uuid",
1108 + "primaryKey": false,
1109 + "notNull": true
1110 + },
1111 + "sender_id": {
1112 + "name": "sender_id",
1113 + "type": "uuid",
1114 + "primaryKey": false,
1115 + "notNull": true
1116 + },
1117 + "body": {
1118 + "name": "body",
1119 + "type": "text",
1120 + "primaryKey": false,
1121 + "notNull": true
1122 + },
1123 + "idempotency_key": {
1124 + "name": "idempotency_key",
1125 + "type": "text",
1126 + "primaryKey": false,
1127 + "notNull": false
1128 + },
1129 + "created_at": {
1130 + "name": "created_at",
1131 + "type": "timestamp with time zone",
1132 + "primaryKey": false,
1133 + "notNull": true,
1134 + "default": "now()"
1135 + },
1136 + "read_at": {
1137 + "name": "read_at",
1138 + "type": "timestamp with time zone",
1139 + "primaryKey": false,
1140 + "notNull": false
1141 + }
1142 + },
1143 + "indexes": {
1144 + "messages_inquiry_idx": {
1145 + "name": "messages_inquiry_idx",
1146 + "columns": [
1147 + {
1148 + "expression": "inquiry_id",
1149 + "isExpression": false,
1150 + "asc": true,
1151 + "nulls": "last"
1152 + },
1153 + {
1154 + "expression": "created_at",
1155 + "isExpression": false,
1156 + "asc": true,
1157 + "nulls": "last"
1158 + }
1159 + ],
1160 + "isUnique": false,
1161 + "concurrently": false,
1162 + "method": "btree",
1163 + "with": {}
1164 + },
1165 + "messages_idem_uq": {
1166 + "name": "messages_idem_uq",
1167 + "columns": [
1168 + {
1169 + "expression": "inquiry_id",
1170 + "isExpression": false,
1171 + "asc": true,
1172 + "nulls": "last"
1173 + },
1174 + {
1175 + "expression": "idempotency_key",
1176 + "isExpression": false,
1177 + "asc": true,
1178 + "nulls": "last"
1179 + }
1180 + ],
1181 + "isUnique": true,
1182 + "concurrently": false,
1183 + "method": "btree",
1184 + "with": {}
1185 + }
1186 + },
1187 + "foreignKeys": {
1188 + "messages_inquiry_id_inquiries_id_fk": {
1189 + "name": "messages_inquiry_id_inquiries_id_fk",
1190 + "tableFrom": "messages",
1191 + "tableTo": "inquiries",
1192 + "columnsFrom": [
1193 + "inquiry_id"
1194 + ],
1195 + "columnsTo": [
1196 + "id"
1197 + ],
1198 + "onDelete": "cascade",
1199 + "onUpdate": "no action"
1200 + },
1201 + "messages_sender_id_users_id_fk": {
1202 + "name": "messages_sender_id_users_id_fk",
1203 + "tableFrom": "messages",
1204 + "tableTo": "users",
1205 + "columnsFrom": [
1206 + "sender_id"
1207 + ],
1208 + "columnsTo": [
1209 + "id"
1210 + ],
1211 + "onDelete": "cascade",
1212 + "onUpdate": "no action"
1213 + }
1214 + },
1215 + "compositePrimaryKeys": {},
1216 + "uniqueConstraints": {},
1217 + "policies": {},
1218 + "checkConstraints": {},
1219 + "isRLSEnabled": false
1220 + },
1221 + "public.moderation_actions": {
1222 + "name": "moderation_actions",
1223 + "schema": "",
1224 + "columns": {
1225 + "id": {
1226 + "name": "id",
1227 + "type": "bigserial",
1228 + "primaryKey": true,
1229 + "notNull": true
1230 + },
1231 + "actor_id": {
1232 + "name": "actor_id",
1233 + "type": "uuid",
1234 + "primaryKey": false,
1235 + "notNull": true
1236 + },
1237 + "target_type": {
1238 + "name": "target_type",
1239 + "type": "text",
1240 + "primaryKey": false,
1241 + "notNull": true
1242 + },
1243 + "target_id": {
1244 + "name": "target_id",
1245 + "type": "text",
1246 + "primaryKey": false,
1247 + "notNull": true
1248 + },
1249 + "action": {
1250 + "name": "action",
1251 + "type": "text",
1252 + "primaryKey": false,
1253 + "notNull": true
1254 + },
1255 + "note": {
1256 + "name": "note",
1257 + "type": "text",
1258 + "primaryKey": false,
1259 + "notNull": false
1260 + },
1261 + "created_at": {
1262 + "name": "created_at",
1263 + "type": "timestamp with time zone",
1264 + "primaryKey": false,
1265 + "notNull": true,
1266 + "default": "now()"
1267 + }
1268 + },
1269 + "indexes": {},
1270 + "foreignKeys": {},
1271 + "compositePrimaryKeys": {},
1272 + "uniqueConstraints": {},
1273 + "policies": {},
1274 + "checkConstraints": {},
1275 + "isRLSEnabled": false
1276 + },
1277 + "public.otp_challenges": {
1278 + "name": "otp_challenges",
1279 + "schema": "",
1280 + "columns": {
1281 + "id": {
1282 + "name": "id",
1283 + "type": "uuid",
1284 + "primaryKey": true,
1285 + "notNull": true,
1286 + "default": "gen_random_uuid()"
1287 + },
1288 + "email_normalized": {
1289 + "name": "email_normalized",
1290 + "type": "text",
1291 + "primaryKey": false,
1292 + "notNull": true
1293 + },
1294 + "purpose": {
1295 + "name": "purpose",
1296 + "type": "text",
1297 + "primaryKey": false,
1298 + "notNull": true,
1299 + "default": "'LOGIN'"
1300 + },
1301 + "secret_digest": {
1302 + "name": "secret_digest",
1303 + "type": "text",
1304 + "primaryKey": false,
1305 + "notNull": true
1306 + },
1307 + "expires_at": {
1308 + "name": "expires_at",
1309 + "type": "timestamp with time zone",
1310 + "primaryKey": false,
1311 + "notNull": true
1312 + },
1313 + "attempts": {
1314 + "name": "attempts",
1315 + "type": "integer",
1316 + "primaryKey": false,
1317 + "notNull": true,
1318 + "default": 0
1319 + },
1320 + "consumed_at": {
1321 + "name": "consumed_at",
1322 + "type": "timestamp with time zone",
1323 + "primaryKey": false,
1324 + "notNull": false
1325 + },
1326 + "requested_ip_hash": {
1327 + "name": "requested_ip_hash",
1328 + "type": "text",
1329 + "primaryKey": false,
1330 + "notNull": false
1331 + },
1332 + "meta": {
1333 + "name": "meta",
1334 + "type": "jsonb",
1335 + "primaryKey": false,
1336 + "notNull": true,
1337 + "default": "'{}'::jsonb"
1338 + },
1339 + "created_at": {
1340 + "name": "created_at",
1341 + "type": "timestamp with time zone",
1342 + "primaryKey": false,
1343 + "notNull": true,
1344 + "default": "now()"
1345 + }
1346 + },
1347 + "indexes": {
1348 + "otp_email_idx": {
1349 + "name": "otp_email_idx",
1350 + "columns": [
1351 + {
1352 + "expression": "email_normalized",
1353 + "isExpression": false,
1354 + "asc": true,
1355 + "nulls": "last"
1356 + },
1357 + {
1358 + "expression": "created_at",
1359 + "isExpression": false,
1360 + "asc": true,
1361 + "nulls": "last"
1362 + }
1363 + ],
1364 + "isUnique": false,
1365 + "concurrently": false,
1366 + "method": "btree",
1367 + "with": {}
1368 + },
1369 + "otp_expires_idx": {
1370 + "name": "otp_expires_idx",
1371 + "columns": [
1372 + {
1373 + "expression": "expires_at",
1374 + "isExpression": false,
1375 + "asc": true,
1376 + "nulls": "last"
1377 + }
1378 + ],
1379 + "isUnique": false,
1380 + "concurrently": false,
1381 + "method": "btree",
1382 + "with": {}
1383 + }
1384 + },
1385 + "foreignKeys": {},
1386 + "compositePrimaryKeys": {},
1387 + "uniqueConstraints": {},
1388 + "policies": {},
1389 + "checkConstraints": {},
1390 + "isRLSEnabled": false
1391 + },
1392 + "public.properties": {
1393 + "name": "properties",
1394 + "schema": "",
1395 + "columns": {
1396 + "id": {
1397 + "name": "id",
1398 + "type": "uuid",
1399 + "primaryKey": true,
1400 + "notNull": true,
1401 + "default": "gen_random_uuid()"
1402 + },
1403 + "canonical_address": {
1404 + "name": "canonical_address",
1405 + "type": "text",
1406 + "primaryKey": false,
1407 + "notNull": false
1408 + },
1409 + "unit": {
1410 + "name": "unit",
1411 + "type": "text",
1412 + "primaryKey": false,
1413 + "notNull": false
1414 + },
1415 + "city": {
1416 + "name": "city",
1417 + "type": "text",
1418 + "primaryKey": false,
1419 + "notNull": false
1420 + },
1421 + "region": {
1422 + "name": "region",
1423 + "type": "text",
1424 + "primaryKey": false,
1425 + "notNull": false
1426 + },
1427 + "postal_code": {
1428 + "name": "postal_code",
1429 + "type": "text",
1430 + "primaryKey": false,
1431 + "notNull": false
1432 + },
1433 + "lat": {
1434 + "name": "lat",
1435 + "type": "double precision",
1436 + "primaryKey": false,
1437 + "notNull": false
1438 + },
1439 + "lng": {
1440 + "name": "lng",
1441 + "type": "double precision",
1442 + "primaryKey": false,
1443 + "notNull": false
1444 + },
1445 + "geographic_precision": {
1446 + "name": "geographic_precision",
1447 + "type": "text",
1448 + "primaryKey": false,
1449 + "notNull": true,
1450 + "default": "'EXACT'"
1451 + },
1452 + "geo_source": {
1453 + "name": "geo_source",
1454 + "type": "text",
1455 + "primaryKey": false,
1456 + "notNull": false
1457 + },
1458 + "role_unit_id": {
1459 + "name": "role_unit_id",
1460 + "type": "text",
1461 + "primaryKey": false,
1462 + "notNull": false
1463 + },
1464 + "property_type": {
1465 + "name": "property_type",
1466 + "type": "text",
1467 + "primaryKey": false,
1468 + "notNull": true
1469 + },
1470 + "bedrooms": {
1471 + "name": "bedrooms",
1472 + "type": "numeric(4, 1)",
1473 + "primaryKey": false,
1474 + "notNull": false
1475 + },
1476 + "bathrooms": {
1477 + "name": "bathrooms",
1478 + "type": "numeric(4, 1)",
1479 + "primaryKey": false,
1480 + "notNull": false
1481 + },
1482 + "area_sqft": {
1483 + "name": "area_sqft",
1484 + "type": "integer",
1485 + "primaryKey": false,
1486 + "notNull": false
1487 + },
1488 + "lot_sqft": {
1489 + "name": "lot_sqft",
1490 + "type": "integer",
1491 + "primaryKey": false,
1492 + "notNull": false
1493 + },
1494 + "year_built": {
1495 + "name": "year_built",
1496 + "type": "integer",
1497 + "primaryKey": false,
1498 + "notNull": false
1499 + },
1500 + "privacy_level": {
1501 + "name": "privacy_level",
1502 + "type": "text",
1503 + "primaryKey": false,
1504 + "notNull": true,
1505 + "default": "'EXACT'"
1506 + },
1507 + "created_at": {
1508 + "name": "created_at",
1509 + "type": "timestamp with time zone",
1510 + "primaryKey": false,
1511 + "notNull": true,
1512 + "default": "now()"
1513 + }
1514 + },
1515 + "indexes": {
1516 + "properties_geo_idx": {
1517 + "name": "properties_geo_idx",
1518 + "columns": [
1519 + {
1520 + "expression": "lat",
1521 + "isExpression": false,
1522 + "asc": true,
1523 + "nulls": "last"
1524 + },
1525 + {
1526 + "expression": "lng",
1527 + "isExpression": false,
1528 + "asc": true,
1529 + "nulls": "last"
1530 + }
1531 + ],
1532 + "isUnique": false,
1533 + "concurrently": false,
1534 + "method": "btree",
1535 + "with": {}
1536 + },
1537 + "properties_city_idx": {
1538 + "name": "properties_city_idx",
1539 + "columns": [
1540 + {
1541 + "expression": "city",
1542 + "isExpression": false,
1543 + "asc": true,
1544 + "nulls": "last"
1545 + }
1546 + ],
1547 + "isUnique": false,
1548 + "concurrently": false,
1549 + "method": "btree",
1550 + "with": {}
1551 + }
1552 + },
1553 + "foreignKeys": {},
1554 + "compositePrimaryKeys": {},
1555 + "uniqueConstraints": {},
1556 + "policies": {},
1557 + "checkConstraints": {},
1558 + "isRLSEnabled": false
1559 + },
1560 + "public.rate_limits": {
1561 + "name": "rate_limits",
1562 + "schema": "",
1563 + "columns": {
1564 + "key": {
1565 + "name": "key",
1566 + "type": "text",
1567 + "primaryKey": false,
1568 + "notNull": true
1569 + },
1570 + "window_start": {
1571 + "name": "window_start",
1572 + "type": "timestamp with time zone",
1573 + "primaryKey": false,
1574 + "notNull": true
1575 + },
1576 + "count": {
1577 + "name": "count",
1578 + "type": "integer",
1579 + "primaryKey": false,
1580 + "notNull": true,
1581 + "default": 0
1582 + }
1583 + },
1584 + "indexes": {},
1585 + "foreignKeys": {},
1586 + "compositePrimaryKeys": {
1587 + "rate_limits_key_window_start_pk": {
1588 + "name": "rate_limits_key_window_start_pk",
1589 + "columns": [
1590 + "key",
1591 + "window_start"
1592 + ]
1593 + }
1594 + },
1595 + "uniqueConstraints": {},
1596 + "policies": {},
1597 + "checkConstraints": {},
1598 + "isRLSEnabled": false
1599 + },
1600 + "public.reports": {
1601 + "name": "reports",
1602 + "schema": "",
1603 + "columns": {
1604 + "id": {
1605 + "name": "id",
1606 + "type": "uuid",
1607 + "primaryKey": true,
1608 + "notNull": true,
1609 + "default": "gen_random_uuid()"
1610 + },
1611 + "target_type": {
1612 + "name": "target_type",
1613 + "type": "text",
1614 + "primaryKey": false,
1615 + "notNull": true
1616 + },
1617 + "target_id": {
1618 + "name": "target_id",
1619 + "type": "text",
1620 + "primaryKey": false,
1621 + "notNull": true
1622 + },
1623 + "reporter_id": {
1624 + "name": "reporter_id",
1625 + "type": "uuid",
1626 + "primaryKey": false,
1627 + "notNull": false
1628 + },
1629 + "reason": {
1630 + "name": "reason",
1631 + "type": "text",
1632 + "primaryKey": false,
1633 + "notNull": true
1634 + },
1635 + "details": {
1636 + "name": "details",
1637 + "type": "text",
1638 + "primaryKey": false,
1639 + "notNull": false
1640 + },
1641 + "status": {
1642 + "name": "status",
1643 + "type": "text",
1644 + "primaryKey": false,
1645 + "notNull": true,
1646 + "default": "'OPEN'"
1647 + },
1648 + "created_at": {
1649 + "name": "created_at",
1650 + "type": "timestamp with time zone",
1651 + "primaryKey": false,
1652 + "notNull": true,
1653 + "default": "now()"
1654 + }
1655 + },
1656 + "indexes": {
1657 + "reports_status_idx": {
1658 + "name": "reports_status_idx",
1659 + "columns": [
1660 + {
1661 + "expression": "status",
1662 + "isExpression": false,
1663 + "asc": true,
1664 + "nulls": "last"
1665 + },
1666 + {
1667 + "expression": "created_at",
1668 + "isExpression": false,
1669 + "asc": true,
1670 + "nulls": "last"
1671 + }
1672 + ],
1673 + "isUnique": false,
1674 + "concurrently": false,
1675 + "method": "btree",
1676 + "with": {}
1677 + }
1678 + },
1679 + "foreignKeys": {
1680 + "reports_reporter_id_users_id_fk": {
1681 + "name": "reports_reporter_id_users_id_fk",
1682 + "tableFrom": "reports",
1683 + "tableTo": "users",
1684 + "columnsFrom": [
1685 + "reporter_id"
1686 + ],
1687 + "columnsTo": [
1688 + "id"
1689 + ],
1690 + "onDelete": "set null",
1691 + "onUpdate": "no action"
1692 + }
1693 + },
1694 + "compositePrimaryKeys": {},
1695 + "uniqueConstraints": {},
1696 + "policies": {},
1697 + "checkConstraints": {},
1698 + "isRLSEnabled": false
1699 + },
1700 + "public.saved_searches": {
1701 + "name": "saved_searches",
1702 + "schema": "",
1703 + "columns": {
1704 + "id": {
1705 + "name": "id",
1706 + "type": "uuid",
1707 + "primaryKey": true,
1708 + "notNull": true,
1709 + "default": "gen_random_uuid()"
1710 + },
1711 + "user_id": {
1712 + "name": "user_id",
1713 + "type": "uuid",
1714 + "primaryKey": false,
1715 + "notNull": true
1716 + },
1717 + "label": {
1718 + "name": "label",
1719 + "type": "text",
1720 + "primaryKey": false,
1721 + "notNull": true
1722 + },
1723 + "query": {
1724 + "name": "query",
1725 + "type": "jsonb",
1726 + "primaryKey": false,
1727 + "notNull": true
1728 + },
1729 + "alert_enabled": {
1730 + "name": "alert_enabled",
1731 + "type": "boolean",
1732 + "primaryKey": false,
1733 + "notNull": true,
1734 + "default": false
1735 + },
1736 + "alert_frequency": {
1737 + "name": "alert_frequency",
1738 + "type": "text",
1739 + "primaryKey": false,
1740 + "notNull": true,
1741 + "default": "'DAILY'"
1742 + },
1743 + "last_alert_at": {
1744 + "name": "last_alert_at",
1745 + "type": "timestamp with time zone",
1746 + "primaryKey": false,
1747 + "notNull": false
1748 + },
1749 + "created_at": {
1750 + "name": "created_at",
1751 + "type": "timestamp with time zone",
1752 + "primaryKey": false,
1753 + "notNull": true,
1754 + "default": "now()"
1755 + }
1756 + },
1757 + "indexes": {
1758 + "saved_searches_user_idx": {
1759 + "name": "saved_searches_user_idx",
1760 + "columns": [
1761 + {
1762 + "expression": "user_id",
1763 + "isExpression": false,
1764 + "asc": true,
1765 + "nulls": "last"
1766 + }
1767 + ],
1768 + "isUnique": false,
1769 + "concurrently": false,
1770 + "method": "btree",
1771 + "with": {}
1772 + }
1773 + },
1774 + "foreignKeys": {
1775 + "saved_searches_user_id_users_id_fk": {
1776 + "name": "saved_searches_user_id_users_id_fk",
1777 + "tableFrom": "saved_searches",
1778 + "tableTo": "users",
1779 + "columnsFrom": [
1780 + "user_id"
1781 + ],
1782 + "columnsTo": [
1783 + "id"
1784 + ],
1785 + "onDelete": "cascade",
1786 + "onUpdate": "no action"
1787 + }
1788 + },
1789 + "compositePrimaryKeys": {},
1790 + "uniqueConstraints": {},
1791 + "policies": {},
1792 + "checkConstraints": {},
1793 + "isRLSEnabled": false
1794 + },
1795 + "public.sessions": {
1796 + "name": "sessions",
1797 + "schema": "",
1798 + "columns": {
1799 + "id": {
1800 + "name": "id",
1801 + "type": "uuid",
1802 + "primaryKey": true,
1803 + "notNull": true,
1804 + "default": "gen_random_uuid()"
1805 + },
1806 + "user_id": {
1807 + "name": "user_id",
1808 + "type": "uuid",
1809 + "primaryKey": false,
1810 + "notNull": true
1811 + },
1812 + "token_digest": {
1813 + "name": "token_digest",
1814 + "type": "text",
1815 + "primaryKey": false,
1816 + "notNull": true
1817 + },
1818 + "issued_at": {
1819 + "name": "issued_at",
1820 + "type": "timestamp with time zone",
1821 + "primaryKey": false,
1822 + "notNull": true,
1823 + "default": "now()"
1824 + },
1825 + "last_seen_at": {
1826 + "name": "last_seen_at",
1827 + "type": "timestamp with time zone",
1828 + "primaryKey": false,
1829 + "notNull": true,
1830 + "default": "now()"
1831 + },
1832 + "expires_at": {
1833 + "name": "expires_at",
1834 + "type": "timestamp with time zone",
1835 + "primaryKey": false,
1836 + "notNull": true
1837 + },
1838 + "revoked_at": {
1839 + "name": "revoked_at",
1840 + "type": "timestamp with time zone",
1841 + "primaryKey": false,
1842 + "notNull": false
1843 + },
1844 + "device_label": {
1845 + "name": "device_label",
1846 + "type": "text",
1847 + "primaryKey": false,
1848 + "notNull": false
1849 + },
1850 + "ip_hash": {
1851 + "name": "ip_hash",
1852 + "type": "text",
1853 + "primaryKey": false,
1854 + "notNull": false
1855 + },
1856 + "mfa_at": {
1857 + "name": "mfa_at",
1858 + "type": "timestamp with time zone",
1859 + "primaryKey": false,
1860 + "notNull": false
1861 + }
1862 + },
1863 + "indexes": {
1864 + "sessions_token_uq": {
1865 + "name": "sessions_token_uq",
1866 + "columns": [
1867 + {
1868 + "expression": "token_digest",
1869 + "isExpression": false,
1870 + "asc": true,
1871 + "nulls": "last"
1872 + }
1873 + ],
1874 + "isUnique": true,
1875 + "concurrently": false,
1876 + "method": "btree",
1877 + "with": {}
1878 + },
1879 + "sessions_user_idx": {
1880 + "name": "sessions_user_idx",
1881 + "columns": [
1882 + {
1883 + "expression": "user_id",
1884 + "isExpression": false,
1885 + "asc": true,
1886 + "nulls": "last"
1887 + }
1888 + ],
1889 + "isUnique": false,
1890 + "concurrently": false,
1891 + "method": "btree",
1892 + "with": {}
1893 + }
1894 + },
1895 + "foreignKeys": {
1896 + "sessions_user_id_users_id_fk": {
1897 + "name": "sessions_user_id_users_id_fk",
1898 + "tableFrom": "sessions",
1899 + "tableTo": "users",
1900 + "columnsFrom": [
1901 + "user_id"
1902 + ],
1903 + "columnsTo": [
1904 + "id"
1905 + ],
1906 + "onDelete": "cascade",
1907 + "onUpdate": "no action"
1908 + }
1909 + },
1910 + "compositePrimaryKeys": {},
1911 + "uniqueConstraints": {},
1912 + "policies": {},
1913 + "checkConstraints": {},
1914 + "isRLSEnabled": false
1915 + },
1916 + "public.user_profiles": {
1917 + "name": "user_profiles",
1918 + "schema": "",
1919 + "columns": {
1920 + "user_id": {
1921 + "name": "user_id",
1922 + "type": "uuid",
1923 + "primaryKey": true,
1924 + "notNull": true
1925 + },
1926 + "public_name": {
1927 + "name": "public_name",
1928 + "type": "text",
1929 + "primaryKey": false,
1930 + "notNull": false
1931 + },
1932 + "bio": {
1933 + "name": "bio",
1934 + "type": "text",
1935 + "primaryKey": false,
1936 + "notNull": false
1937 + },
1938 + "avatar_key": {
1939 + "name": "avatar_key",
1940 + "type": "text",
1941 + "primaryKey": false,
1942 + "notNull": false
1943 + },
1944 + "seller_type": {
1945 + "name": "seller_type",
1946 + "type": "text",
1947 + "primaryKey": false,
1948 + "notNull": true,
1949 + "default": "'OWNER'"
1950 + },
1951 + "verification_status": {
1952 + "name": "verification_status",
1953 + "type": "text",
1954 + "primaryKey": false,
1955 + "notNull": true,
1956 + "default": "'NONE'"
1957 + },
1958 + "contact_preferences": {
1959 + "name": "contact_preferences",
1960 + "type": "jsonb",
1961 + "primaryKey": false,
1962 + "notNull": true,
1963 + "default": "'{\"email\":true,\"phone\":false}'::jsonb"
1964 + },
1965 + "updated_at": {
1966 + "name": "updated_at",
1967 + "type": "timestamp with time zone",
1968 + "primaryKey": false,
1969 + "notNull": true,
1970 + "default": "now()"
1971 + }
1972 + },
1973 + "indexes": {},
1974 + "foreignKeys": {
1975 + "user_profiles_user_id_users_id_fk": {
1976 + "name": "user_profiles_user_id_users_id_fk",
1977 + "tableFrom": "user_profiles",
1978 + "tableTo": "users",
1979 + "columnsFrom": [
1980 + "user_id"
1981 + ],
1982 + "columnsTo": [
1983 + "id"
1984 + ],
1985 + "onDelete": "cascade",
1986 + "onUpdate": "no action"
1987 + }
1988 + },
1989 + "compositePrimaryKeys": {},
1990 + "uniqueConstraints": {},
1991 + "policies": {},
1992 + "checkConstraints": {},
1993 + "isRLSEnabled": false
1994 + },
1995 + "public.user_roles": {
1996 + "name": "user_roles",
1997 + "schema": "",
1998 + "columns": {
1999 + "user_id": {
2000 + "name": "user_id",
2001 + "type": "uuid",
2002 + "primaryKey": false,
2003 + "notNull": true
2004 + },
2005 + "role": {
2006 + "name": "role",
2007 + "type": "text",
2008 + "primaryKey": false,
2009 + "notNull": true
2010 + },
2011 + "granted_by": {
2012 + "name": "granted_by",
2013 + "type": "uuid",
2014 + "primaryKey": false,
2015 + "notNull": false
2016 + },
2017 + "granted_at": {
2018 + "name": "granted_at",
2019 + "type": "timestamp with time zone",
2020 + "primaryKey": false,
2021 + "notNull": true,
2022 + "default": "now()"
2023 + },
2024 + "note": {
2025 + "name": "note",
2026 + "type": "text",
2027 + "primaryKey": false,
2028 + "notNull": false
2029 + }
2030 + },
2031 + "indexes": {},
2032 + "foreignKeys": {
2033 + "user_roles_user_id_users_id_fk": {
2034 + "name": "user_roles_user_id_users_id_fk",
2035 + "tableFrom": "user_roles",
2036 + "tableTo": "users",
2037 + "columnsFrom": [
2038 + "user_id"
2039 + ],
2040 + "columnsTo": [
2041 + "id"
2042 + ],
2043 + "onDelete": "cascade",
2044 + "onUpdate": "no action"
2045 + }
2046 + },
2047 + "compositePrimaryKeys": {
2048 + "user_roles_user_id_role_pk": {
2049 + "name": "user_roles_user_id_role_pk",
2050 + "columns": [
2051 + "user_id",
2052 + "role"
2053 + ]
2054 + }
2055 + },
2056 + "uniqueConstraints": {},
2057 + "policies": {},
2058 + "checkConstraints": {
2059 + "user_roles_role_ck": {
2060 + "name": "user_roles_role_ck",
2061 + "value": "\"user_roles\".\"role\" in ('ADMIN','MODERATOR','AGENT_VERIFIED')"
2062 + }
2063 + },
2064 + "isRLSEnabled": false
2065 + },
2066 + "public.users": {
2067 + "name": "users",
2068 + "schema": "",
2069 + "columns": {
2070 + "id": {
2071 + "name": "id",
2072 + "type": "uuid",
2073 + "primaryKey": true,
2074 + "notNull": true,
2075 + "default": "gen_random_uuid()"
2076 + },
2077 + "email": {
2078 + "name": "email",
2079 + "type": "text",
2080 + "primaryKey": false,
2081 + "notNull": true
2082 + },
2083 + "email_verified_at": {
2084 + "name": "email_verified_at",
2085 + "type": "timestamp with time zone",
2086 + "primaryKey": false,
2087 + "notNull": false
2088 + },
2089 + "display_name": {
2090 + "name": "display_name",
2091 + "type": "text",
2092 + "primaryKey": false,
2093 + "notNull": false
2094 + },
2095 + "phone_encrypted": {
2096 + "name": "phone_encrypted",
2097 + "type": "text",
2098 + "primaryKey": false,
2099 + "notNull": false
2100 + },
2101 + "locale": {
2102 + "name": "locale",
2103 + "type": "text",
2104 + "primaryKey": false,
2105 + "notNull": true,
2106 + "default": "'fr-CA'"
2107 + },
2108 + "status": {
2109 + "name": "status",
2110 + "type": "text",
2111 + "primaryKey": false,
2112 + "notNull": true,
2113 + "default": "'ACTIVE'"
2114 + },
2115 + "prefs": {
2116 + "name": "prefs",
2117 + "type": "jsonb",
2118 + "primaryKey": false,
2119 + "notNull": true,
2120 + "default": "'{}'::jsonb"
2121 + },
2122 + "totp_secret_encrypted": {
2123 + "name": "totp_secret_encrypted",
2124 + "type": "text",
2125 + "primaryKey": false,
2126 + "notNull": false
2127 + },
2128 + "created_at": {
2129 + "name": "created_at",
2130 + "type": "timestamp with time zone",
2131 + "primaryKey": false,
2132 + "notNull": true,
2133 + "default": "now()"
2134 + },
2135 + "updated_at": {
2136 + "name": "updated_at",
2137 + "type": "timestamp with time zone",
2138 + "primaryKey": false,
2139 + "notNull": true,
2140 + "default": "now()"
2141 + },
2142 + "deleted_at": {
2143 + "name": "deleted_at",
2144 + "type": "timestamp with time zone",
2145 + "primaryKey": false,
2146 + "notNull": false
2147 + }
2148 + },
2149 + "indexes": {
2150 + "users_email_uq": {
2151 + "name": "users_email_uq",
2152 + "columns": [
2153 + {
2154 + "expression": "email",
2155 + "isExpression": false,
2156 + "asc": true,
2157 + "nulls": "last"
2158 + }
2159 + ],
2160 + "isUnique": true,
2161 + "concurrently": false,
2162 + "method": "btree",
2163 + "with": {}
2164 + }
2165 + },
2166 + "foreignKeys": {},
2167 + "compositePrimaryKeys": {},
2168 + "uniqueConstraints": {},
2169 + "policies": {},
2170 + "checkConstraints": {
2171 + "users_status_ck": {
2172 + "name": "users_status_ck",
2173 + "value": "\"users\".\"status\" in ('ACTIVE','SUSPENDED','DELETED')"
2174 + }
2175 + },
2176 + "isRLSEnabled": false
2177 + },
2178 + "public.viewing_requests": {
2179 + "name": "viewing_requests",
2180 + "schema": "",
2181 + "columns": {
2182 + "id": {
2183 + "name": "id",
2184 + "type": "uuid",
2185 + "primaryKey": true,
2186 + "notNull": true,
2187 + "default": "gen_random_uuid()"
2188 + },
2189 + "listing_id": {
2190 + "name": "listing_id",
2191 + "type": "uuid",
2192 + "primaryKey": false,
2193 + "notNull": true
2194 + },
2195 + "inquiry_id": {
2196 + "name": "inquiry_id",
2197 + "type": "uuid",
2198 + "primaryKey": false,
2199 + "notNull": false
2200 + },
2201 + "requester_id": {
2202 + "name": "requester_id",
2203 + "type": "uuid",
2204 + "primaryKey": false,
2205 + "notNull": true
2206 + },
2207 + "owner_id": {
2208 + "name": "owner_id",
2209 + "type": "uuid",
2210 + "primaryKey": false,
2211 + "notNull": true
2212 + },
2213 + "proposed_slots": {
2214 + "name": "proposed_slots",
2215 + "type": "jsonb",
2216 + "primaryKey": false,
2217 + "notNull": true
2218 + },
2219 + "confirmed_slot": {
2220 + "name": "confirmed_slot",
2221 + "type": "timestamp with time zone",
2222 + "primaryKey": false,
2223 + "notNull": false
2224 + },
2225 + "status": {
2226 + "name": "status",
2227 + "type": "text",
2228 + "primaryKey": false,
2229 + "notNull": true,
2230 + "default": "'REQUESTED'"
2231 + },
2232 + "note": {
2233 + "name": "note",
2234 + "type": "text",
2235 + "primaryKey": false,
2236 + "notNull": false
2237 + },
2238 + "created_at": {
2239 + "name": "created_at",
2240 + "type": "timestamp with time zone",
2241 + "primaryKey": false,
2242 + "notNull": true,
2243 + "default": "now()"
2244 + },
2245 + "updated_at": {
2246 + "name": "updated_at",
2247 + "type": "timestamp with time zone",
2248 + "primaryKey": false,
2249 + "notNull": true,
2250 + "default": "now()"
2251 + }
2252 + },
2253 + "indexes": {
2254 + "viewings_listing_idx": {
2255 + "name": "viewings_listing_idx",
2256 + "columns": [
2257 + {
2258 + "expression": "listing_id",
2259 + "isExpression": false,
2260 + "asc": true,
2261 + "nulls": "last"
2262 + }
2263 + ],
2264 + "isUnique": false,
2265 + "concurrently": false,
2266 + "method": "btree",
2267 + "with": {}
2268 + },
2269 + "viewings_requester_idx": {
2270 + "name": "viewings_requester_idx",
2271 + "columns": [
2272 + {
2273 + "expression": "requester_id",
2274 + "isExpression": false,
2275 + "asc": true,
2276 + "nulls": "last"
2277 + }
2278 + ],
2279 + "isUnique": false,
2280 + "concurrently": false,
2281 + "method": "btree",
2282 + "with": {}
2283 + },
2284 + "viewings_owner_idx": {
2285 + "name": "viewings_owner_idx",
2286 + "columns": [
2287 + {
2288 + "expression": "owner_id",
2289 + "isExpression": false,
2290 + "asc": true,
2291 + "nulls": "last"
2292 + }
2293 + ],
2294 + "isUnique": false,
2295 + "concurrently": false,
2296 + "method": "btree",
2297 + "with": {}
2298 + }
2299 + },
2300 + "foreignKeys": {
2301 + "viewing_requests_listing_id_listings_id_fk": {
2302 + "name": "viewing_requests_listing_id_listings_id_fk",
2303 + "tableFrom": "viewing_requests",
2304 + "tableTo": "listings",
2305 + "columnsFrom": [
2306 + "listing_id"
2307 + ],
2308 + "columnsTo": [
2309 + "id"
2310 + ],
2311 + "onDelete": "cascade",
2312 + "onUpdate": "no action"
2313 + },
2314 + "viewing_requests_inquiry_id_inquiries_id_fk": {
2315 + "name": "viewing_requests_inquiry_id_inquiries_id_fk",
2316 + "tableFrom": "viewing_requests",
2317 + "tableTo": "inquiries",
2318 + "columnsFrom": [
2319 + "inquiry_id"
2320 + ],
2321 + "columnsTo": [
2322 + "id"
2323 + ],
2324 + "onDelete": "set null",
2325 + "onUpdate": "no action"
2326 + },
2327 + "viewing_requests_requester_id_users_id_fk": {
2328 + "name": "viewing_requests_requester_id_users_id_fk",
2329 + "tableFrom": "viewing_requests",
2330 + "tableTo": "users",
2331 + "columnsFrom": [
2332 + "requester_id"
2333 + ],
2334 + "columnsTo": [
2335 + "id"
2336 + ],
2337 + "onDelete": "cascade",
2338 + "onUpdate": "no action"
2339 + },
2340 + "viewing_requests_owner_id_users_id_fk": {
2341 + "name": "viewing_requests_owner_id_users_id_fk",
2342 + "tableFrom": "viewing_requests",
2343 + "tableTo": "users",
2344 + "columnsFrom": [
2345 + "owner_id"
2346 + ],
2347 + "columnsTo": [
2348 + "id"
2349 + ],
2350 + "onDelete": "cascade",
2351 + "onUpdate": "no action"
2352 + }
2353 + },
2354 + "compositePrimaryKeys": {},
2355 + "uniqueConstraints": {},
2356 + "policies": {},
2357 + "checkConstraints": {
2358 + "viewings_status_ck": {
2359 + "name": "viewings_status_ck",
2360 + "value": "\"viewing_requests\".\"status\" in ('REQUESTED','CONFIRMED','CANCELLED','DONE')"
2361 + }
2362 + },
2363 + "isRLSEnabled": false
2364 + }
2365 + },
2366 + "enums": {},
2367 + "schemas": {},
2368 + "sequences": {},
2369 + "roles": {},
2370 + "policies": {},
2371 + "views": {},
2372 + "_meta": {
2373 + "columns": {},
2374 + "schemas": {},
2375 + "tables": {}
2376 + }
2377 +}
\ No newline at end of file
added drizzle/meta/_journal.json +13 −0
@@ -0,0 +1,13 @@
1 +{
2 + "version": "7",
3 + "dialect": "postgresql",
4 + "entries": [
5 + {
6 + "idx": 0,
7 + "version": "7",
8 + "when": 1792259087015,
9 + "tag": "0000_daily_dexter_bennett",
10 + "breakpoints": true
11 + }
12 + ]
13 +}
\ No newline at end of file
modified next.config.ts +2 −1
@@ -1,6 +1,7 @@
1 1 import type { NextConfig } from "next";
2 2 const nextConfig: NextConfig = {
3 − serverExternalPackages: ["better-sqlite3"],
3 + serverExternalPackages: ["better-sqlite3", "sharp", "heic-convert"],
4 + turbopack: { rules: { "*.css": { loaders: ["@tailwindcss/turbopack"], as: "*.css" } } },
4 5 poweredByHeader: false,
5 6 async headers() {
6 7 return [{ source: "/(.*)", headers: [
modified package.json +15 −1
@@ -9,25 +9,39 @@
9 9 "lint": "eslint",
10 10 "sync": "tsx scripts/sync.ts",
11 11 "test": "vitest run",
12 − "typecheck": "tsc --noEmit"
12 + "typecheck": "tsc --noEmit",
13 + "db:generate": "drizzle-kit generate",
14 + "db:migrate": "tsx scripts/migrate.ts",
15 + "db:studio": "drizzle-kit studio",
16 + "test:db": "TOVELI_TEST_DB=1 vitest run tests/db"
13 17 },
14 18 "dependencies": {
15 19 "@anthropic-ai/sdk": "^0.135.0",
16 20 "better-sqlite3": "^13.0.3",
21 + "drizzle-orm": "^0.45.4",
22 + "heic-convert": "^2.1.0",
17 23 "maplibre-gl": "^6.14.0",
24 + "nanoid": "^6.0.2",
18 25 "next": "16.4.2",
26 + "otpauth": "^9.5.2",
27 + "postgres": "^3.4.9",
19 28 "react": "19.3.0",
20 29 "react-dom": "19.3.0",
21 30 "recharts": "^3.10.1",
31 + "resend": "^6.34.1",
32 + "sharp": "^0.35.5",
22 33 "zod": "^4.6.7"
23 34 },
24 35 "devDependencies": {
25 36 "@tailwindcss/turbopack": "^4",
26 37 "@types/better-sqlite3": "^9.6.0",
38 + "@types/heic-convert": "^2.1.1",
27 39 "@types/node": "^20",
40 + "@types/pg": "^8.23.1",
28 41 "@types/react": "^19",
29 42 "@types/react-dom": "^19",
30 43 "@vitejs/plugin-react": "^6.1.2",
44 + "drizzle-kit": "^0.31.11",
31 45 "eslint": "^9",
32 46 "eslint-config-next": "16.4.2",
33 47 "tailwindcss": "^4",
modified pnpm-lock.yaml +832 −3
@@ -14,12 +14,27 @@ importers:
14 14 better-sqlite3:
15 15 specifier: ^13.0.3
16 16 version: 13.0.3
17 + drizzle-orm:
18 + specifier: ^0.45.4
19 + version: 0.45.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@9.6.0)(@types/pg@8.23.1)(better-sqlite3@13.0.3)(postgres@3.4.9)
20 + heic-convert:
21 + specifier: ^2.1.0
22 + version: 2.1.0
17 23 maplibre-gl:
18 24 specifier: ^6.14.0
19 25 version: 6.14.0
26 + nanoid:
27 + specifier: ^6.0.2
28 + version: 6.0.2
20 29 next:
21 30 specifier: 16.4.2
22 31 version: 16.4.2(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
32 + otpauth:
33 + specifier: ^9.5.2
34 + version: 9.5.2
35 + postgres:
36 + specifier: ^3.4.9
37 + version: 3.4.9
23 38 react:
24 39 specifier: 19.3.0
25 40 version: 19.3.0
@@ -29,6 +44,12 @@ importers:
29 44 recharts:
30 45 specifier: ^3.10.1
31 46 version: 3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@16.13.1)(react@19.3.0)(redux@5.0.1)
47 + resend:
48 + specifier: ^6.34.1
49 + version: 6.34.1
50 + sharp:
51 + specifier: ^0.35.5
52 + version: 0.35.5(@types/node@20.19.43)
32 53 zod:
33 54 specifier: ^4.6.7
34 55 version: 4.6.7
@@ -39,9 +60,15 @@ importers:
39 60 '@types/better-sqlite3':
40 61 specifier: ^9.6.0
41 62 version: 9.6.0
63 + '@types/heic-convert':
64 + specifier: ^2.1.1
65 + version: 2.1.1
42 66 '@types/node':
43 67 specifier: ^20
44 68 version: 20.19.43
69 + '@types/pg':
70 + specifier: ^8.23.1
71 + version: 8.23.1
45 72 '@types/react':
46 73 specifier: ^19
47 74 version: 19.3.0
@@ -51,6 +78,9 @@ importers:
51 78 '@vitejs/plugin-react':
52 79 specifier: ^6.1.2
53 80 version: 6.1.2(vite@8.3.4(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15))
81 + drizzle-kit:
82 + specifier: ^0.31.11
83 + version: 0.31.11
54 84 eslint:
55 85 specifier: ^9
56 86 version: 9.39.5(jiti@2.7.0)
@@ -156,6 +186,9 @@ packages:
156 186 resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
157 187 engines: {node: '>=6.9.0'}
158 188
189 + '@drizzle-team/brocli@0.10.2':
190 + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
191 +
159 192 '@emnapi/core@1.10.0':
160 193 resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
161 194
@@ -168,156 +201,452 @@ packages:
168 201 '@emnapi/wasi-threads@1.2.1':
169 202 resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
170 203
204 + '@esbuild-kit/core-utils@3.3.2':
205 + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
206 + deprecated: 'Merged into tsx: https://tsx.hirok.io'
207 +
208 + '@esbuild-kit/esm-loader@2.6.5':
209 + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
210 + deprecated: 'Merged into tsx: https://tsx.hirok.io'
211 +
212 + '@esbuild/aix-ppc64@0.25.12':
213 + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
214 + engines: {node: '>=18'}
215 + cpu: [ppc64]
216 + os: [aix]
217 +
171 218 '@esbuild/aix-ppc64@0.28.2':
172 219 resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
173 220 engines: {node: '>=18'}
174 221 cpu: [ppc64]
175 222 os: [aix]
176 223
224 + '@esbuild/android-arm64@0.18.20':
225 + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
226 + engines: {node: '>=12'}
227 + cpu: [arm64]
228 + os: [android]
229 +
230 + '@esbuild/android-arm64@0.25.12':
231 + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
232 + engines: {node: '>=18'}
233 + cpu: [arm64]
234 + os: [android]
235 +
177 236 '@esbuild/android-arm64@0.28.2':
178 237 resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
179 238 engines: {node: '>=18'}
180 239 cpu: [arm64]
181 240 os: [android]
182 241
242 + '@esbuild/android-arm@0.18.20':
243 + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
244 + engines: {node: '>=12'}
245 + cpu: [arm]
246 + os: [android]
247 +
248 + '@esbuild/android-arm@0.25.12':
249 + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
250 + engines: {node: '>=18'}
251 + cpu: [arm]
252 + os: [android]
253 +
183 254 '@esbuild/android-arm@0.28.2':
184 255 resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
185 256 engines: {node: '>=18'}
186 257 cpu: [arm]
187 258 os: [android]
188 259
260 + '@esbuild/android-x64@0.18.20':
261 + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
262 + engines: {node: '>=12'}
263 + cpu: [x64]
264 + os: [android]
265 +
266 + '@esbuild/android-x64@0.25.12':
267 + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
268 + engines: {node: '>=18'}
269 + cpu: [x64]
270 + os: [android]
271 +
189 272 '@esbuild/android-x64@0.28.2':
190 273 resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
191 274 engines: {node: '>=18'}
192 275 cpu: [x64]
193 276 os: [android]
194 277
278 + '@esbuild/darwin-arm64@0.18.20':
279 + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
280 + engines: {node: '>=12'}
281 + cpu: [arm64]
282 + os: [darwin]
283 +
284 + '@esbuild/darwin-arm64@0.25.12':
285 + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
286 + engines: {node: '>=18'}
287 + cpu: [arm64]
288 + os: [darwin]
289 +
195 290 '@esbuild/darwin-arm64@0.28.2':
196 291 resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
197 292 engines: {node: '>=18'}
198 293 cpu: [arm64]
199 294 os: [darwin]
200 295
296 + '@esbuild/darwin-x64@0.18.20':
297 + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
298 + engines: {node: '>=12'}
299 + cpu: [x64]
300 + os: [darwin]
301 +
302 + '@esbuild/darwin-x64@0.25.12':
303 + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
304 + engines: {node: '>=18'}
305 + cpu: [x64]
306 + os: [darwin]
307 +
201 308 '@esbuild/darwin-x64@0.28.2':
202 309 resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
203 310 engines: {node: '>=18'}
204 311 cpu: [x64]
205 312 os: [darwin]
206 313
314 + '@esbuild/freebsd-arm64@0.18.20':
315 + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
316 + engines: {node: '>=12'}
317 + cpu: [arm64]
318 + os: [freebsd]
319 +
320 + '@esbuild/freebsd-arm64@0.25.12':
321 + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
322 + engines: {node: '>=18'}
323 + cpu: [arm64]
324 + os: [freebsd]
325 +
207 326 '@esbuild/freebsd-arm64@0.28.2':
208 327 resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
209 328 engines: {node: '>=18'}
210 329 cpu: [arm64]
211 330 os: [freebsd]
212 331
332 + '@esbuild/freebsd-x64@0.18.20':
333 + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
334 + engines: {node: '>=12'}
335 + cpu: [x64]
336 + os: [freebsd]
337 +
338 + '@esbuild/freebsd-x64@0.25.12':
339 + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
340 + engines: {node: '>=18'}
341 + cpu: [x64]
342 + os: [freebsd]
343 +
213 344 '@esbuild/freebsd-x64@0.28.2':
214 345 resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
215 346 engines: {node: '>=18'}
216 347 cpu: [x64]
217 348 os: [freebsd]
218 349
350 + '@esbuild/linux-arm64@0.18.20':
351 + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
352 + engines: {node: '>=12'}
353 + cpu: [arm64]
354 + os: [linux]
355 +
356 + '@esbuild/linux-arm64@0.25.12':
357 + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
358 + engines: {node: '>=18'}
359 + cpu: [arm64]
360 + os: [linux]
361 +
219 362 '@esbuild/linux-arm64@0.28.2':
220 363 resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
221 364 engines: {node: '>=18'}
222 365 cpu: [arm64]
223 366 os: [linux]
224 367
368 + '@esbuild/linux-arm@0.18.20':
369 + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
370 + engines: {node: '>=12'}
371 + cpu: [arm]
372 + os: [linux]
373 +
374 + '@esbuild/linux-arm@0.25.12':
375 + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
376 + engines: {node: '>=18'}
377 + cpu: [arm]
378 + os: [linux]
379 +
225 380 '@esbuild/linux-arm@0.28.2':
226 381 resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
227 382 engines: {node: '>=18'}
228 383 cpu: [arm]
229 384 os: [linux]
230 385
386 + '@esbuild/linux-ia32@0.18.20':
387 + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
388 + engines: {node: '>=12'}
389 + cpu: [ia32]
390 + os: [linux]
391 +
392 + '@esbuild/linux-ia32@0.25.12':
393 + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
394 + engines: {node: '>=18'}
395 + cpu: [ia32]
396 + os: [linux]
397 +
231 398 '@esbuild/linux-ia32@0.28.2':
232 399 resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
233 400 engines: {node: '>=18'}
234 401 cpu: [ia32]
235 402 os: [linux]
236 403
404 + '@esbuild/linux-loong64@0.18.20':
405 + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
406 + engines: {node: '>=12'}
407 + cpu: [loong64]
408 + os: [linux]
409 +
410 + '@esbuild/linux-loong64@0.25.12':
411 + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
412 + engines: {node: '>=18'}
413 + cpu: [loong64]
414 + os: [linux]
415 +
237 416 '@esbuild/linux-loong64@0.28.2':
238 417 resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
239 418 engines: {node: '>=18'}
240 419 cpu: [loong64]
241 420 os: [linux]
242 421
422 + '@esbuild/linux-mips64el@0.18.20':
423 + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
424 + engines: {node: '>=12'}
425 + cpu: [mips64el]
426 + os: [linux]
427 +
428 + '@esbuild/linux-mips64el@0.25.12':
429 + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
430 + engines: {node: '>=18'}
431 + cpu: [mips64el]
432 + os: [linux]
433 +
243 434 '@esbuild/linux-mips64el@0.28.2':
244 435 resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
245 436 engines: {node: '>=18'}
246 437 cpu: [mips64el]
247 438 os: [linux]
248 439
440 + '@esbuild/linux-ppc64@0.18.20':
441 + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
442 + engines: {node: '>=12'}
443 + cpu: [ppc64]
444 + os: [linux]
445 +
446 + '@esbuild/linux-ppc64@0.25.12':
447 + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
448 + engines: {node: '>=18'}
449 + cpu: [ppc64]
450 + os: [linux]
451 +
249 452 '@esbuild/linux-ppc64@0.28.2':
250 453 resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
251 454 engines: {node: '>=18'}
252 455 cpu: [ppc64]
253 456 os: [linux]
254 457
458 + '@esbuild/linux-riscv64@0.18.20':
459 + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
460 + engines: {node: '>=12'}
461 + cpu: [riscv64]
462 + os: [linux]
463 +
464 + '@esbuild/linux-riscv64@0.25.12':
465 + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
466 + engines: {node: '>=18'}
467 + cpu: [riscv64]
468 + os: [linux]
469 +
255 470 '@esbuild/linux-riscv64@0.28.2':
256 471 resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
257 472 engines: {node: '>=18'}
258 473 cpu: [riscv64]
259 474 os: [linux]
260 475
476 + '@esbuild/linux-s390x@0.18.20':
477 + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
478 + engines: {node: '>=12'}
479 + cpu: [s390x]
480 + os: [linux]
481 +
482 + '@esbuild/linux-s390x@0.25.12':
483 + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
484 + engines: {node: '>=18'}
485 + cpu: [s390x]
486 + os: [linux]
487 +
261 488 '@esbuild/linux-s390x@0.28.2':
262 489 resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
263 490 engines: {node: '>=18'}
264 491 cpu: [s390x]
265 492 os: [linux]
266 493
494 + '@esbuild/linux-x64@0.18.20':
495 + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
496 + engines: {node: '>=12'}
497 + cpu: [x64]
498 + os: [linux]
499 +
500 + '@esbuild/linux-x64@0.25.12':
501 + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
502 + engines: {node: '>=18'}
503 + cpu: [x64]
504 + os: [linux]
505 +
267 506 '@esbuild/linux-x64@0.28.2':
268 507 resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
269 508 engines: {node: '>=18'}
270 509 cpu: [x64]
271 510 os: [linux]
272 511
512 + '@esbuild/netbsd-arm64@0.25.12':
513 + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
514 + engines: {node: '>=18'}
515 + cpu: [arm64]
516 + os: [netbsd]
517 +
273 518 '@esbuild/netbsd-arm64@0.28.2':
274 519 resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
275 520 engines: {node: '>=18'}
276 521 cpu: [arm64]
277 522 os: [netbsd]
278 523
524 + '@esbuild/netbsd-x64@0.18.20':
525 + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
526 + engines: {node: '>=12'}
527 + cpu: [x64]
528 + os: [netbsd]
529 +
530 + '@esbuild/netbsd-x64@0.25.12':
531 + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
532 + engines: {node: '>=18'}
533 + cpu: [x64]
534 + os: [netbsd]
535 +
279 536 '@esbuild/netbsd-x64@0.28.2':
280 537 resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
281 538 engines: {node: '>=18'}
282 539 cpu: [x64]
283 540 os: [netbsd]
284 541
542 + '@esbuild/openbsd-arm64@0.25.12':
543 + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
544 + engines: {node: '>=18'}
545 + cpu: [arm64]
546 + os: [openbsd]
547 +
285 548 '@esbuild/openbsd-arm64@0.28.2':
286 549 resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
287 550 engines: {node: '>=18'}
288 551 cpu: [arm64]
289 552 os: [openbsd]
290 553
554 + '@esbuild/openbsd-x64@0.18.20':
555 + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
556 + engines: {node: '>=12'}
557 + cpu: [x64]
558 + os: [openbsd]
559 +
560 + '@esbuild/openbsd-x64@0.25.12':
561 + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
562 + engines: {node: '>=18'}
563 + cpu: [x64]
564 + os: [openbsd]
565 +
291 566 '@esbuild/openbsd-x64@0.28.2':
292 567 resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
293 568 engines: {node: '>=18'}
294 569 cpu: [x64]
295 570 os: [openbsd]
296 571
572 + '@esbuild/openharmony-arm64@0.25.12':
573 + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
574 + engines: {node: '>=18'}
575 + cpu: [arm64]
576 + os: [openharmony]
577 +
297 578 '@esbuild/openharmony-arm64@0.28.2':
298 579 resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
299 580 engines: {node: '>=18'}
300 581 cpu: [arm64]
301 582 os: [openharmony]
302 583
584 + '@esbuild/sunos-x64@0.18.20':
585 + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
586 + engines: {node: '>=12'}
587 + cpu: [x64]
588 + os: [sunos]
589 +
590 + '@esbuild/sunos-x64@0.25.12':
591 + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
592 + engines: {node: '>=18'}
593 + cpu: [x64]
594 + os: [sunos]
595 +
303 596 '@esbuild/sunos-x64@0.28.2':
304 597 resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
305 598 engines: {node: '>=18'}
306 599 cpu: [x64]
307 600 os: [sunos]
308 601
602 + '@esbuild/win32-arm64@0.18.20':
603 + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
604 + engines: {node: '>=12'}
605 + cpu: [arm64]
606 + os: [win32]
607 +
608 + '@esbuild/win32-arm64@0.25.12':
609 + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
610 + engines: {node: '>=18'}
611 + cpu: [arm64]
612 + os: [win32]
613 +
309 614 '@esbuild/win32-arm64@0.28.2':
310 615 resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
311 616 engines: {node: '>=18'}
312 617 cpu: [arm64]
313 618 os: [win32]
314 619
620 + '@esbuild/win32-ia32@0.18.20':
621 + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
622 + engines: {node: '>=12'}
623 + cpu: [ia32]
624 + os: [win32]
625 +
626 + '@esbuild/win32-ia32@0.25.12':
627 + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
628 + engines: {node: '>=18'}
629 + cpu: [ia32]
630 + os: [win32]
631 +
315 632 '@esbuild/win32-ia32@0.28.2':
316 633 resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
317 634 engines: {node: '>=18'}
318 635 cpu: [ia32]
319 636 os: [win32]
320 637
638 + '@esbuild/win32-x64@0.18.20':
639 + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
640 + engines: {node: '>=12'}
641 + cpu: [x64]
642 + os: [win32]
643 +
644 + '@esbuild/win32-x64@0.25.12':
645 + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
646 + engines: {node: '>=18'}
647 + cpu: [x64]
648 + os: [win32]
649 +
321 650 '@esbuild/win32-x64@0.28.2':
322 651 resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
323 652 engines: {node: '>=18'}
@@ -660,6 +989,10 @@ packages:
660 989 cpu: [x64]
661 990 os: [win32]
662 991
992 + '@noble/hashes@2.4.0':
993 + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
994 + engines: {node: '>= 20.19.0'}
995 +
663 996 '@nodelib/fs.scandir@2.1.5':
664 997 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
665 998 engines: {node: '>= 8'}
@@ -945,6 +1278,9 @@ packages:
945 1278 '@types/geojson@7946.0.16':
946 1279 resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
947 1280
1281 + '@types/heic-convert@2.1.1':
1282 + resolution: {integrity: sha512-+s14762Nf62z9zziIs7ItvAkSUCS3ls4Z5XPT9BlVve3Q3S4DAnf6qekffMTvEWycSUF+kulkGaSN65fK6eKvg==}
1283 +
948 1284 '@types/json-schema@7.0.15':
949 1285 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
950 1286
@@ -954,6 +1290,9 @@ packages:
954 1290 '@types/node@20.19.43':
955 1291 resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
956 1292
1293 + '@types/pg@8.23.1':
1294 + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==}
1295 +
957 1296 '@types/react-dom@19.3.0':
958 1297 resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
959 1298 peerDependencies:
@@ -1288,6 +1627,9 @@ packages:
1288 1627 engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
1289 1628 hasBin: true
1290 1629
1630 + buffer-from@1.1.2:
1631 + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
1632 +
1291 1633 call-bind-apply-helpers@1.0.2:
1292 1634 resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
1293 1635 engines: {node: '>= 0.4'}
@@ -1440,6 +1782,105 @@ packages:
1440 1782 resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
1441 1783 engines: {node: '>=0.10.0'}
1442 1784
1785 + drizzle-kit@0.31.11:
1786 + resolution: {integrity: sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==}
1787 + hasBin: true
1788 +
1789 + drizzle-orm@0.45.4:
1790 + resolution: {integrity: sha512-NiM1YZpJ2UxyeE67DpDDZ+Tsw4YLX3trSlkNT7Tm9eKoUNb1LvaAaQzXgYQsWs+AQ9TUmmwMmBOCXuqqxUWKKQ==}
1791 + peerDependencies:
1792 + '@aws-sdk/client-rds-data': '>=3'
1793 + '@cloudflare/workers-types': '>=4'
1794 + '@electric-sql/pglite': '>=0.2.0'
1795 + '@libsql/client': '>=0.10.0'
1796 + '@libsql/client-wasm': '>=0.10.0'
1797 + '@neondatabase/serverless': '>=0.10.0'
1798 + '@netlify/db': '>=0.4.0'
1799 + '@op-engineering/op-sqlite': '>=2'
1800 + '@opentelemetry/api': ^1.4.1
1801 + '@planetscale/database': '>=1.13'
1802 + '@prisma/client': '*'
1803 + '@tidbcloud/serverless': '*'
1804 + '@types/better-sqlite3': '*'
1805 + '@types/pg': '*'
1806 + '@types/sql.js': '*'
1807 + '@upstash/redis': '>=1.34.7'
1808 + '@vercel/postgres': '>=0.8.0'
1809 + '@xata.io/client': '*'
1810 + better-sqlite3: '>=7'
1811 + bun-types: '*'
1812 + expo-sqlite: '>=14.0.0'
1813 + gel: '>=2'
1814 + knex: '*'
1815 + kysely: '*'
1816 + mysql2: '>=2'
1817 + pg: '>=8'
1818 + postgres: '>=3'
1819 + prisma: '*'
1820 + sql.js: '>=1'
1821 + sqlite3: '>=5'
1822 + peerDependenciesMeta:
1823 + '@aws-sdk/client-rds-data':
1824 + optional: true
1825 + '@cloudflare/workers-types':
1826 + optional: true
1827 + '@electric-sql/pglite':
1828 + optional: true
1829 + '@libsql/client':
1830 + optional: true
1831 + '@libsql/client-wasm':
1832 + optional: true
1833 + '@neondatabase/serverless':
1834 + optional: true
1835 + '@netlify/db':
1836 + optional: true
1837 + '@op-engineering/op-sqlite':
1838 + optional: true
1839 + '@opentelemetry/api':
1840 + optional: true
1841 + '@planetscale/database':
1842 + optional: true
1843 + '@prisma/client':
1844 + optional: true
1845 + '@tidbcloud/serverless':
1846 + optional: true
1847 + '@types/better-sqlite3':
1848 + optional: true
1849 + '@types/pg':
1850 + optional: true
1851 + '@types/sql.js':
1852 + optional: true
1853 + '@upstash/redis':
1854 + optional: true
1855 + '@vercel/postgres':
1856 + optional: true
1857 + '@xata.io/client':
1858 + optional: true
1859 + better-sqlite3:
1860 + optional: true
1861 + bun-types:
1862 + optional: true
1863 + expo-sqlite:
1864 + optional: true
1865 + gel:
1866 + optional: true
1867 + knex:
1868 + optional: true
1869 + kysely:
1870 + optional: true
1871 + mysql2:
1872 + optional: true
1873 + pg:
1874 + optional: true
1875 + postgres:
1876 + optional: true
1877 + prisma:
1878 + optional: true
1879 + sql.js:
1880 + optional: true
1881 + sqlite3:
1882 + optional: true
1883 +
1443 1884 dunder-proto@1.0.1:
1444 1885 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
1445 1886 engines: {node: '>= 0.4'}
@@ -1499,6 +1940,16 @@ packages:
1499 1940 es-toolkit@1.52.0:
1500 1941 resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==}
1501 1942
1943 + esbuild@0.18.20:
1944 + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
1945 + engines: {node: '>=12'}
1946 + hasBin: true
1947 +
1948 + esbuild@0.25.12:
1949 + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
1950 + engines: {node: '>=18'}
1951 + hasBin: true
1952 +
1502 1953 esbuild@0.28.2:
1503 1954 resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
1504 1955 engines: {node: '>=18'}
@@ -1789,6 +2240,14 @@ packages:
1789 2240 resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
1790 2241 engines: {node: '>= 0.4'}
1791 2242
2243 + heic-convert@2.1.0:
2244 + resolution: {integrity: sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==}
2245 + engines: {node: '>=12.0.0'}
2246 +
2247 + heic-decode@2.1.0:
2248 + resolution: {integrity: sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==}
2249 + engines: {node: '>=8.0.0'}
2250 +
1792 2251 hermes-estree@0.25.1:
1793 2252 resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
1794 2253
@@ -1943,6 +2402,9 @@ packages:
1943 2402 resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
1944 2403 hasBin: true
1945 2404
2405 + jpeg-js@0.4.4:
2406 + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
2407 +
1946 2408 js-tokens@4.0.0:
1947 2409 resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
1948 2410
@@ -2001,6 +2463,10 @@ packages:
2001 2463 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
2002 2464 engines: {node: '>= 0.8.0'}
2003 2465
2466 + libheif-js@1.23.5:
2467 + resolution: {integrity: sha512-umXZPthnWZtF3iG/5mQG+AZ2V6i0UeYpo3z2CkggTEgL8dKmIIwJT6TDuAMuabXa1yTW8uJ1ZAgR+CxNTklYYw==}
2468 + engines: {node: '>=8.0.0'}
2469 +
2004 2470 lightningcss-android-arm64@1.32.0:
2005 2471 resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
2006 2472 engines: {node: '>= 12.0.0'}
@@ -2206,6 +2672,11 @@ packages:
2206 2672 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
2207 2673 hasBin: true
2208 2674
2675 + nanoid@6.0.2:
2676 + resolution: {integrity: sha512-hIvVOb9Qr1p/9CNNLOe0OlWvgx/ziTsVwKN/GDSsuArN2K0wDiafP3s0SWmsm7o2BdnyS8Z9Z64NFBUbn6ziBg==}
2677 + engines: {node: ^22 || ^24 || >=26}
2678 + hasBin: true
2679 +
2209 2680 napi-postinstall@0.3.4:
2210 2681 resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
2211 2682 engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -2287,6 +2758,9 @@ packages:
2287 2758 resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
2288 2759 engines: {node: '>= 0.8.0'}
2289 2760
2761 + otpauth@9.5.2:
2762 + resolution: {integrity: sha512-GQ5emWR/x1tcExT62IBT0UfO95wZzJZyxYOJOGVeQF47SYEN9vmh0vISvDZaNMuFJRG+IaWCKtfm+t9Bfoal6w==}
2763 +
2290 2764 own-keys@1.0.2:
2291 2765 resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==}
2292 2766 engines: {node: '>= 0.4'}
@@ -2318,6 +2792,17 @@ packages:
2318 2792 resolution: {integrity: sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==}
2319 2793 hasBin: true
2320 2794
2795 + pg-int8@1.0.1:
2796 + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
2797 + engines: {node: '>=4.0.0'}
2798 +
2799 + pg-protocol@1.16.1:
2800 + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==}
2801 +
2802 + pg-types@2.2.0:
2803 + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
2804 + engines: {node: '>=4'}
2805 +
2321 2806 picocolors@1.1.1:
2322 2807 resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
2323 2808
@@ -2329,10 +2814,17 @@ packages:
2329 2814 resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
2330 2815 engines: {node: '>=12'}
2331 2816
2817 + pngjs@6.0.0:
2818 + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
2819 + engines: {node: '>=12.13.0'}
2820 +
2332 2821 possible-typed-array-names@1.1.0:
2333 2822 resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
2334 2823 engines: {node: '>= 0.4'}
2335 2824
2825 + postal-mime@2.7.6:
2826 + resolution: {integrity: sha512-UUlyE2KlxmvwMGq060onF/VWU3zD6dVW31FwokQ5v26jWd35fbs2MoTFzh+jXnPwAyV2MULMKXcBInGOl5TO6Q==}
2827 +
2336 2828 postcss@8.5.23:
2337 2829 resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
2338 2830 engines: {node: ^10 || ^12 || >=14}
@@ -2341,6 +2833,26 @@ packages:
2341 2833 resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==}
2342 2834 engines: {node: ^10 || ^12 || >=14}
2343 2835
2836 + postgres-array@2.0.0:
2837 + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
2838 + engines: {node: '>=4'}
2839 +
2840 + postgres-bytea@1.0.1:
2841 + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
2842 + engines: {node: '>=0.10.0'}
2843 +
2844 + postgres-date@1.0.7:
2845 + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
2846 + engines: {node: '>=0.10.0'}
2847 +
2848 + postgres-interval@1.2.0:
2849 + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
2850 + engines: {node: '>=0.10.0'}
2851 +
2852 + postgres@3.4.9:
2853 + resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
2854 + engines: {node: '>=12'}
2855 +
2344 2856 potpack@2.1.0:
2345 2857 resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
2346 2858
@@ -2419,6 +2931,15 @@ packages:
2419 2931 reselect@5.2.0:
2420 2932 resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
2421 2933
2934 + resend@6.34.1:
2935 + resolution: {integrity: sha512-3jo/j1WAW7CBkE6pzK4qLjZCfHRLIhY3ph3x/wzU+MN51cSw3EtReGce8S1cA1R0ipNDOdFYU5Lmt5r563lvYw==}
2936 + engines: {node: '>=20'}
2937 + peerDependencies:
2938 + '@react-email/render': '*'
2939 + peerDependenciesMeta:
2940 + '@react-email/render':
2941 + optional: true
2942 +
2422 2943 resolve-from@4.0.0:
2423 2944 resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
2424 2945 engines: {node: '>=4'}
@@ -2519,6 +3040,13 @@ packages:
2519 3040 resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
2520 3041 engines: {node: '>=0.10.0'}
2521 3042
3043 + source-map-support@0.5.21:
3044 + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
3045 +
3046 + source-map@0.6.1:
3047 + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
3048 + engines: {node: '>=0.10.0'}
3049 +
2522 3050 stable-hash@0.0.5:
2523 3051 resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
2524 3052
@@ -2806,6 +3334,10 @@ packages:
2806 3334 resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
2807 3335 engines: {node: '>=0.10.0'}
2808 3336
3337 + xtend@4.0.2:
3338 + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
3339 + engines: {node: '>=0.4'}
3340 +
2809 3341 yallist@3.1.1:
2810 3342 resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
2811 3343
@@ -2936,6 +3468,8 @@ snapshots:
2936 3468 '@babel/helper-string-parser': 7.29.7
2937 3469 '@babel/helper-validator-identifier': 7.29.7
2938 3470
3471 + '@drizzle-team/brocli@0.10.2': {}
3472 +
2939 3473 '@emnapi/core@1.10.0':
2940 3474 dependencies:
2941 3475 '@emnapi/wasi-threads': 1.2.1
@@ -2957,81 +3491,235 @@ snapshots:
2957 3491 tslib: 2.8.1
2958 3492 optional: true
2959 3493
3494 + '@esbuild-kit/core-utils@3.3.2':
3495 + dependencies:
3496 + esbuild: 0.18.20
3497 + source-map-support: 0.5.21
3498 +
3499 + '@esbuild-kit/esm-loader@2.6.5':
3500 + dependencies:
3501 + '@esbuild-kit/core-utils': 3.3.2
3502 + get-tsconfig: 4.14.3
3503 +
3504 + '@esbuild/aix-ppc64@0.25.12':
3505 + optional: true
3506 +
2960 3507 '@esbuild/aix-ppc64@0.28.2':
2961 3508 optional: true
2962 3509
3510 + '@esbuild/android-arm64@0.18.20':
3511 + optional: true
3512 +
3513 + '@esbuild/android-arm64@0.25.12':
3514 + optional: true
3515 +
2963 3516 '@esbuild/android-arm64@0.28.2':
2964 3517 optional: true
2965 3518
3519 + '@esbuild/android-arm@0.18.20':
3520 + optional: true
3521 +
3522 + '@esbuild/android-arm@0.25.12':
3523 + optional: true
3524 +
2966 3525 '@esbuild/android-arm@0.28.2':
2967 3526 optional: true
2968 3527
3528 + '@esbuild/android-x64@0.18.20':
3529 + optional: true
3530 +
3531 + '@esbuild/android-x64@0.25.12':
3532 + optional: true
3533 +
2969 3534 '@esbuild/android-x64@0.28.2':
2970 3535 optional: true
2971 3536
3537 + '@esbuild/darwin-arm64@0.18.20':
3538 + optional: true
3539 +
3540 + '@esbuild/darwin-arm64@0.25.12':
3541 + optional: true
3542 +
2972 3543 '@esbuild/darwin-arm64@0.28.2':
2973 3544 optional: true
2974 3545
3546 + '@esbuild/darwin-x64@0.18.20':
3547 + optional: true
3548 +
3549 + '@esbuild/darwin-x64@0.25.12':
3550 + optional: true
3551 +
2975 3552 '@esbuild/darwin-x64@0.28.2':
2976 3553 optional: true
2977 3554
3555 + '@esbuild/freebsd-arm64@0.18.20':
3556 + optional: true
3557 +
3558 + '@esbuild/freebsd-arm64@0.25.12':
3559 + optional: true
3560 +
2978 3561 '@esbuild/freebsd-arm64@0.28.2':
2979 3562 optional: true
2980 3563
3564 + '@esbuild/freebsd-x64@0.18.20':
3565 + optional: true
3566 +
3567 + '@esbuild/freebsd-x64@0.25.12':
3568 + optional: true
3569 +
2981 3570 '@esbuild/freebsd-x64@0.28.2':
2982 3571 optional: true
2983 3572
3573 + '@esbuild/linux-arm64@0.18.20':
3574 + optional: true
3575 +
3576 + '@esbuild/linux-arm64@0.25.12':
3577 + optional: true
3578 +
2984 3579 '@esbuild/linux-arm64@0.28.2':
2985 3580 optional: true
2986 3581
3582 + '@esbuild/linux-arm@0.18.20':
3583 + optional: true
3584 +
3585 + '@esbuild/linux-arm@0.25.12':
3586 + optional: true
3587 +
2987 3588 '@esbuild/linux-arm@0.28.2':
2988 3589 optional: true
2989 3590
3591 + '@esbuild/linux-ia32@0.18.20':
3592 + optional: true
3593 +
3594 + '@esbuild/linux-ia32@0.25.12':
3595 + optional: true
3596 +
2990 3597 '@esbuild/linux-ia32@0.28.2':
2991 3598 optional: true
2992 3599
3600 + '@esbuild/linux-loong64@0.18.20':
3601 + optional: true
3602 +
3603 + '@esbuild/linux-loong64@0.25.12':
3604 + optional: true
3605 +
2993 3606 '@esbuild/linux-loong64@0.28.2':
2994 3607 optional: true
2995 3608
3609 + '@esbuild/linux-mips64el@0.18.20':
3610 + optional: true
3611 +
3612 + '@esbuild/linux-mips64el@0.25.12':
3613 + optional: true
3614 +
2996 3615 '@esbuild/linux-mips64el@0.28.2':
2997 3616 optional: true
2998 3617
3618 + '@esbuild/linux-ppc64@0.18.20':
3619 + optional: true
3620 +
3621 + '@esbuild/linux-ppc64@0.25.12':
3622 + optional: true
3623 +
2999 3624 '@esbuild/linux-ppc64@0.28.2':
3000 3625 optional: true
3001 3626
3627 + '@esbuild/linux-riscv64@0.18.20':
3628 + optional: true
3629 +
3630 + '@esbuild/linux-riscv64@0.25.12':
3631 + optional: true
3632 +
3002 3633 '@esbuild/linux-riscv64@0.28.2':
3003 3634 optional: true
3004 3635
3636 + '@esbuild/linux-s390x@0.18.20':
3637 + optional: true
3638 +
3639 + '@esbuild/linux-s390x@0.25.12':
3640 + optional: true
3641 +
3005 3642 '@esbuild/linux-s390x@0.28.2':
3006 3643 optional: true
3007 3644
3645 + '@esbuild/linux-x64@0.18.20':
3646 + optional: true
3647 +
3648 + '@esbuild/linux-x64@0.25.12':
3649 + optional: true
3650 +
3008 3651 '@esbuild/linux-x64@0.28.2':
3009 3652 optional: true
3010 3653
3654 + '@esbuild/netbsd-arm64@0.25.12':
3655 + optional: true
3656 +
3011 3657 '@esbuild/netbsd-arm64@0.28.2':
3012 3658 optional: true
3013 3659
3660 + '@esbuild/netbsd-x64@0.18.20':
3661 + optional: true
3662 +
3663 + '@esbuild/netbsd-x64@0.25.12':
3664 + optional: true
3665 +
3014 3666 '@esbuild/netbsd-x64@0.28.2':
3015 3667 optional: true
3016 3668
3669 + '@esbuild/openbsd-arm64@0.25.12':
3670 + optional: true
3671 +
3017 3672 '@esbuild/openbsd-arm64@0.28.2':
3018 3673 optional: true
3019 3674
3675 + '@esbuild/openbsd-x64@0.18.20':
3676 + optional: true
3677 +
3678 + '@esbuild/openbsd-x64@0.25.12':
3679 + optional: true
3680 +
3020 3681 '@esbuild/openbsd-x64@0.28.2':
3021 3682 optional: true
3022 3683
3684 + '@esbuild/openharmony-arm64@0.25.12':
3685 + optional: true
3686 +
3023 3687 '@esbuild/openharmony-arm64@0.28.2':
3024 3688 optional: true
3025 3689
3690 + '@esbuild/sunos-x64@0.18.20':
3691 + optional: true
3692 +
3693 + '@esbuild/sunos-x64@0.25.12':
3694 + optional: true
3695 +
3026 3696 '@esbuild/sunos-x64@0.28.2':
3027 3697 optional: true
3028 3698
3699 + '@esbuild/win32-arm64@0.18.20':
3700 + optional: true
3701 +
3702 + '@esbuild/win32-arm64@0.25.12':
3703 + optional: true
3704 +
3029 3705 '@esbuild/win32-arm64@0.28.2':
3030 3706 optional: true
3031 3707
3708 + '@esbuild/win32-ia32@0.18.20':
3709 + optional: true
3710 +
3711 + '@esbuild/win32-ia32@0.25.12':
3712 + optional: true
3713 +
3032 3714 '@esbuild/win32-ia32@0.28.2':
3033 3715 optional: true
3034 3716
3717 + '@esbuild/win32-x64@0.18.20':
3718 + optional: true
3719 +
3720 + '@esbuild/win32-x64@0.25.12':
3721 + optional: true
3722 +
3035 3723 '@esbuild/win32-x64@0.28.2':
3036 3724 optional: true
3037 3725
@@ -3102,8 +3790,7 @@ snapshots:
3102 3790
3103 3791 '@humanwhocodes/retry@0.4.3': {}
3104 3792
3105 − '@img/colour@1.1.0':
3106 − optional: true
3793 + '@img/colour@1.1.0': {}
3107 3794
3108 3795 '@img/sharp-darwin-arm64@0.35.5':
3109 3796 optionalDependencies:
@@ -3305,6 +3992,8 @@ snapshots:
3305 3992 '@next/swc-win32-x64-msvc@16.4.2':
3306 3993 optional: true
3307 3994
3995 + '@noble/hashes@2.4.0': {}
3996 +
3308 3997 '@nodelib/fs.scandir@2.1.5':
3309 3998 dependencies:
3310 3999 '@nodelib/fs.stat': 2.0.5
@@ -3506,6 +4195,8 @@ snapshots:
3506 4195
3507 4196 '@types/geojson@7946.0.16': {}
3508 4197
4198 + '@types/heic-convert@2.1.1': {}
4199 +
3509 4200 '@types/json-schema@7.0.15': {}
3510 4201
3511 4202 '@types/json5@0.0.29': {}
@@ -3514,6 +4205,12 @@ snapshots:
3514 4205 dependencies:
3515 4206 undici-types: 6.21.0
3516 4207
4208 + '@types/pg@8.23.1':
4209 + dependencies:
4210 + '@types/node': 20.19.43
4211 + pg-protocol: 1.16.1
4212 + pg-types: 2.2.0
4213 +
3517 4214 '@types/react-dom@19.3.0(@types/react@19.3.0)':
3518 4215 dependencies:
3519 4216 '@types/react': 19.3.0
@@ -3838,6 +4535,8 @@ snapshots:
3838 4535 node-releases: 2.0.59
3839 4536 update-browserslist-db: 1.3.4(browserslist@4.29.3)
3840 4537
4538 + buffer-from@1.1.2: {}
4539 +
3841 4540 call-bind-apply-helpers@1.0.2:
3842 4541 dependencies:
3843 4542 es-errors: 1.3.0
@@ -3976,6 +4675,21 @@ snapshots:
3976 4675 dependencies:
3977 4676 esutils: 2.0.3
3978 4677
4678 + drizzle-kit@0.31.11:
4679 + dependencies:
4680 + '@drizzle-team/brocli': 0.10.2
4681 + '@esbuild-kit/esm-loader': 2.6.5
4682 + esbuild: 0.25.12
4683 + tsx: 4.23.15
4684 +
4685 + drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@9.6.0)(@types/pg@8.23.1)(better-sqlite3@13.0.3)(postgres@3.4.9):
4686 + optionalDependencies:
4687 + '@opentelemetry/api': 1.9.1
4688 + '@types/better-sqlite3': 9.6.0
4689 + '@types/pg': 8.23.1
4690 + better-sqlite3: 13.0.3
4691 + postgres: 3.4.9
4692 +
3979 4693 dunder-proto@1.0.1:
3980 4694 dependencies:
3981 4695 call-bind-apply-helpers: 1.0.2
@@ -4108,6 +4822,60 @@ snapshots:
4108 4822
4109 4823 es-toolkit@1.52.0: {}
4110 4824
4825 + esbuild@0.18.20:
4826 + optionalDependencies:
4827 + '@esbuild/android-arm': 0.18.20
4828 + '@esbuild/android-arm64': 0.18.20
4829 + '@esbuild/android-x64': 0.18.20
4830 + '@esbuild/darwin-arm64': 0.18.20
4831 + '@esbuild/darwin-x64': 0.18.20
4832 + '@esbuild/freebsd-arm64': 0.18.20
4833 + '@esbuild/freebsd-x64': 0.18.20
4834 + '@esbuild/linux-arm': 0.18.20
4835 + '@esbuild/linux-arm64': 0.18.20
4836 + '@esbuild/linux-ia32': 0.18.20
4837 + '@esbuild/linux-loong64': 0.18.20
4838 + '@esbuild/linux-mips64el': 0.18.20
4839 + '@esbuild/linux-ppc64': 0.18.20
4840 + '@esbuild/linux-riscv64': 0.18.20
4841 + '@esbuild/linux-s390x': 0.18.20
4842 + '@esbuild/linux-x64': 0.18.20
4843 + '@esbuild/netbsd-x64': 0.18.20
4844 + '@esbuild/openbsd-x64': 0.18.20
4845 + '@esbuild/sunos-x64': 0.18.20
4846 + '@esbuild/win32-arm64': 0.18.20
4847 + '@esbuild/win32-ia32': 0.18.20
4848 + '@esbuild/win32-x64': 0.18.20
4849 +
4850 + esbuild@0.25.12:
4851 + optionalDependencies:
4852 + '@esbuild/aix-ppc64': 0.25.12
4853 + '@esbuild/android-arm': 0.25.12
4854 + '@esbuild/android-arm64': 0.25.12
4855 + '@esbuild/android-x64': 0.25.12
4856 + '@esbuild/darwin-arm64': 0.25.12
4857 + '@esbuild/darwin-x64': 0.25.12
4858 + '@esbuild/freebsd-arm64': 0.25.12
4859 + '@esbuild/freebsd-x64': 0.25.12
4860 + '@esbuild/linux-arm': 0.25.12
4861 + '@esbuild/linux-arm64': 0.25.12
4862 + '@esbuild/linux-ia32': 0.25.12
4863 + '@esbuild/linux-loong64': 0.25.12
4864 + '@esbuild/linux-mips64el': 0.25.12
4865 + '@esbuild/linux-ppc64': 0.25.12
4866 + '@esbuild/linux-riscv64': 0.25.12
4867 + '@esbuild/linux-s390x': 0.25.12
4868 + '@esbuild/linux-x64': 0.25.12
4869 + '@esbuild/netbsd-arm64': 0.25.12
4870 + '@esbuild/netbsd-x64': 0.25.12
4871 + '@esbuild/openbsd-arm64': 0.25.12
4872 + '@esbuild/openbsd-x64': 0.25.12
4873 + '@esbuild/openharmony-arm64': 0.25.12
4874 + '@esbuild/sunos-x64': 0.25.12
4875 + '@esbuild/win32-arm64': 0.25.12
4876 + '@esbuild/win32-ia32': 0.25.12
4877 + '@esbuild/win32-x64': 0.25.12
4878 +
4111 4879 esbuild@0.28.2:
4112 4880 optionalDependencies:
4113 4881 '@esbuild/aix-ppc64': 0.28.2
@@ -4498,6 +5266,16 @@ snapshots:
4498 5266 dependencies:
4499 5267 function-bind: 1.1.2
4500 5268
5269 + heic-convert@2.1.0:
5270 + dependencies:
5271 + heic-decode: 2.1.0
5272 + jpeg-js: 0.4.4
5273 + pngjs: 6.0.0
5274 +
5275 + heic-decode@2.1.0:
5276 + dependencies:
5277 + libheif-js: 1.23.5
5278 +
4501 5279 hermes-estree@0.25.1: {}
4502 5280
4503 5281 hermes-parser@0.25.1:
@@ -4656,6 +5434,8 @@ snapshots:
4656 5434
4657 5435 jiti@2.7.0: {}
4658 5436
5437 + jpeg-js@0.4.4: {}
5438 +
4659 5439 js-tokens@4.0.0: {}
4660 5440
4661 5441 js-yaml@4.3.2:
@@ -4707,6 +5487,8 @@ snapshots:
4707 5487 prelude-ls: 1.2.1
4708 5488 type-check: 0.4.0
4709 5489
5490 + libheif-js@1.23.5: {}
5491 +
4710 5492 lightningcss-android-arm64@1.32.0:
4711 5493 optional: true
4712 5494
@@ -4873,6 +5655,8 @@ snapshots:
4873 5655
4874 5656 nanoid@3.3.20: {}
4875 5657
5658 + nanoid@6.0.2: {}
5659 +
4876 5660 napi-postinstall@0.3.4: {}
4877 5661
4878 5662 natural-compare@1.4.0: {}
@@ -4967,6 +5751,10 @@ snapshots:
4967 5751 type-check: 0.4.0
4968 5752 word-wrap: 1.2.5
4969 5753
5754 + otpauth@9.5.2:
5755 + dependencies:
5756 + '@noble/hashes': 2.4.0
5757 +
4970 5758 own-keys@1.0.2:
4971 5759 dependencies:
4972 5760 call-bound: 1.0.4
@@ -4996,14 +5784,30 @@ snapshots:
4996 5784 dependencies:
4997 5785 resolve-protobuf-schema: 2.1.0
4998 5786
5787 + pg-int8@1.0.1: {}
5788 +
5789 + pg-protocol@1.16.1: {}
5790 +
5791 + pg-types@2.2.0:
5792 + dependencies:
5793 + pg-int8: 1.0.1
5794 + postgres-array: 2.0.0
5795 + postgres-bytea: 1.0.1
5796 + postgres-date: 1.0.7
5797 + postgres-interval: 1.2.0
5798 +
4999 5799 picocolors@1.1.1: {}
5000 5800
5001 5801 picomatch@2.3.2: {}
5002 5802
5003 5803 picomatch@4.0.7: {}
5004 5804
5805 + pngjs@6.0.0: {}
5806 +
5005 5807 possible-typed-array-names@1.1.0: {}
5006 5808
5809 + postal-mime@2.7.6: {}
5810 +
5007 5811 postcss@8.5.23:
5008 5812 dependencies:
5009 5813 nanoid: 3.3.20
@@ -5016,6 +5820,18 @@ snapshots:
5016 5820 picocolors: 1.1.1
5017 5821 source-map-js: 1.2.2
5018 5822
5823 + postgres-array@2.0.0: {}
5824 +
5825 + postgres-bytea@1.0.1: {}
5826 +
5827 + postgres-date@1.0.7: {}
5828 +
5829 + postgres-interval@1.2.0:
5830 + dependencies:
5831 + xtend: 4.0.2
5832 +
5833 + postgres@3.4.9: {}
5834 +
5019 5835 potpack@2.1.0: {}
5020 5836
5021 5837 prelude-ls@1.2.1: {}
@@ -5102,6 +5918,11 @@ snapshots:
5102 5918
5103 5919 reselect@5.2.0: {}
5104 5920
5921 + resend@6.34.1:
5922 + dependencies:
5923 + postal-mime: 2.7.6
5924 + standardwebhooks: 1.1.1
5925 +
5105 5926 resolve-from@4.0.0: {}
5106 5927
5107 5928 resolve-pkg-maps@1.0.0: {}
@@ -5225,7 +6046,6 @@ snapshots:
5225 6046 '@img/sharp-win32-ia32': 0.35.5
5226 6047 '@img/sharp-win32-x64': 0.35.5
5227 6048 '@types/node': 20.19.43
5228 − optional: true
5229 6049
5230 6050 shebang-command@2.0.0:
5231 6051 dependencies:
@@ -5263,6 +6083,13 @@ snapshots:
5263 6083
5264 6084 source-map-js@1.2.2: {}
5265 6085
6086 + source-map-support@0.5.21:
6087 + dependencies:
6088 + buffer-from: 1.1.2
6089 + source-map: 0.6.1
6090 +
6091 + source-map@0.6.1: {}
6092 +
5266 6093 stable-hash@0.0.5: {}
5267 6094
5268 6095 standardwebhooks@1.1.1:
@@ -5588,6 +6415,8 @@ snapshots:
5588 6415
5589 6416 word-wrap@1.2.5: {}
5590 6417
6418 + xtend@4.0.2: {}
6419 +
5591 6420 yallist@3.1.1: {}
5592 6421
5593 6422 yocto-queue@0.1.0: {}
added scripts/migrate.ts +12 −0
@@ -0,0 +1,12 @@
1 +// Applique les migrations Drizzle (idempotent, journal __drizzle_migrations). Usage : DATABASE_URL=… pnpm db:migrate
2 +import { drizzle } from "drizzle-orm/postgres-js";
3 +import { migrate } from "drizzle-orm/postgres-js/migrator";
4 +import postgres from "postgres";
5 +async function main() {
6 + const url = process.env.DATABASE_URL; if (!url) { console.error("DATABASE_URL manquante"); process.exit(1); }
7 + const sql = postgres(url, { max: 1 });
8 + await migrate(drizzle(sql), { migrationsFolder: "./drizzle" });
9 + console.log("migrations OK →", url.replace(/:\/\/([^:]+):[^@]+@/, "://$1:***@"));
10 + await sql.end();
11 +}
12 +main().catch((e) => { console.error(e); process.exit(1); });
added src/db/client.ts +16 −0
@@ -0,0 +1,16 @@
1 +import { drizzle } from "drizzle-orm/postgres-js";
2 +import postgres from "postgres";
3 +import * as schema from "./schema";
4 +
5 +// Compte applicatif à privilèges minimaux (toveli_app), connexion locale uniquement (DATABASE_URL jamais exposée au client).
6 +const url = process.env.DATABASE_URL;
7 +declare global { var __toveliSql: ReturnType<typeof postgres> | undefined; }
8 +function sqlClient() {
9 + if (!url) throw new Error("DATABASE_URL manquante");
10 + if (!globalThis.__toveliSql) globalThis.__toveliSql = postgres(url, { max: Number(process.env.PG_POOL_MAX ?? 8), idle_timeout: 30, connect_timeout: 10, prepare: true });
11 + return globalThis.__toveliSql;
12 +}
13 +export function pgAvailable(): boolean { return !!url; }
14 +export function db() { return drizzle(sqlClient(), { schema }); }
15 +export function rawSql() { return sqlClient(); }
16 +export { schema };
added src/db/schema.ts +271 −0
@@ -0,0 +1,271 @@
1 +// Toveli — schéma PostgreSQL (identité, publications directes, échanges). Drizzle ORM.
2 +// Les annonces agrégées restent dans la base canonique SQLite (lecture seule) ; ici ne vivent que les données Toveli.
3 +import { pgTable, uuid, text, timestamp, integer, boolean, jsonb, numeric, doublePrecision, index, uniqueIndex, primaryKey, check, bigserial } from "drizzle-orm/pg-core";
4 +import { sql } from "drizzle-orm";
5 +
6 +export const users = pgTable("users", {
7 + id: uuid("id").primaryKey().defaultRandom(),
8 + email: text("email").notNull(), // adresse normalisée (minuscules, espaces retirés — points et + conservés)
9 + emailVerifiedAt: timestamp("email_verified_at", { withTimezone: true }),
10 + displayName: text("display_name"),
11 + phoneEncrypted: text("phone_encrypted"),
12 + locale: text("locale").notNull().default("fr-CA"),
13 + status: text("status").notNull().default("ACTIVE"), // ACTIVE | SUSPENDED | DELETED
14 + prefs: jsonb("prefs").notNull().default(sql`'{}'::jsonb`), // notifications, confidentialité
15 + totpSecretEncrypted: text("totp_secret_encrypted"), // MFA forte requise pour les rôles élevés
16 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
17 + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
18 + deletedAt: timestamp("deleted_at", { withTimezone: true }),
19 +}, (t) => [uniqueIndex("users_email_uq").on(t.email), check("users_status_ck", sql`${t.status} in ('ACTIVE','SUSPENDED','DELETED')`)]);
20 +
21 +export const userProfiles = pgTable("user_profiles", {
22 + userId: uuid("user_id").primaryKey().references(() => users.id, { onDelete: "cascade" }),
23 + publicName: text("public_name"),
24 + bio: text("bio"),
25 + avatarKey: text("avatar_key"),
26 + sellerType: text("seller_type").notNull().default("OWNER"), // OWNER | AGENT (déclaratif) — AGENT_VERIFIED est un rôle attribué côté serveur
27 + verificationStatus: text("verification_status").notNull().default("NONE"),
28 + contactPreferences: jsonb("contact_preferences").notNull().default(sql`'{"email":true,"phone":false}'::jsonb`),
29 + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
30 +});
31 +
32 +export const otpChallenges = pgTable("otp_challenges", {
33 + id: uuid("id").primaryKey().defaultRandom(),
34 + emailNormalized: text("email_normalized").notNull(),
35 + purpose: text("purpose").notNull().default("LOGIN"), // LOGIN | EMAIL_CHANGE
36 + secretDigest: text("secret_digest").notNull(), // HMAC-SHA256(code, OTP_HMAC_SECRET, id) — jamais le code
37 + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
38 + attempts: integer("attempts").notNull().default(0),
39 + consumedAt: timestamp("consumed_at", { withTimezone: true }),
40 + requestedIpHash: text("requested_ip_hash"),
41 + meta: jsonb("meta").notNull().default(sql`'{}'::jsonb`),
42 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
43 +}, (t) => [index("otp_email_idx").on(t.emailNormalized, t.createdAt), index("otp_expires_idx").on(t.expiresAt)]);
44 +
45 +export const sessions = pgTable("sessions", {
46 + id: uuid("id").primaryKey().defaultRandom(),
47 + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
48 + tokenDigest: text("token_digest").notNull(), // sha256(token) — le jeton n'est jamais stocké en clair
49 + issuedAt: timestamp("issued_at", { withTimezone: true }).notNull().defaultNow(),
50 + lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
51 + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
52 + revokedAt: timestamp("revoked_at", { withTimezone: true }),
53 + deviceLabel: text("device_label"),
54 + ipHash: text("ip_hash"),
55 + mfaAt: timestamp("mfa_at", { withTimezone: true }), // dernière vérification TOTP (actions admin)
56 +}, (t) => [uniqueIndex("sessions_token_uq").on(t.tokenDigest), index("sessions_user_idx").on(t.userId)]);
57 +
58 +export const userRoles = pgTable("user_roles", {
59 + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
60 + role: text("role").notNull(), // ADMIN | MODERATOR | AGENT_VERIFIED
61 + grantedBy: uuid("granted_by"),
62 + grantedAt: timestamp("granted_at", { withTimezone: true }).notNull().defaultNow(),
63 + note: text("note"),
64 +}, (t) => [primaryKey({ columns: [t.userId, t.role] }), check("user_roles_role_ck", sql`${t.role} in ('ADMIN','MODERATOR','AGENT_VERIFIED')`)]);
65 +
66 +export const properties = pgTable("properties", {
67 + id: uuid("id").primaryKey().defaultRandom(),
68 + canonicalAddress: text("canonical_address"),
69 + unit: text("unit"),
70 + city: text("city"),
71 + region: text("region"),
72 + postalCode: text("postal_code"),
73 + lat: doublePrecision("lat"),
74 + lng: doublePrecision("lng"),
75 + geographicPrecision: text("geographic_precision").notNull().default("EXACT"), // EXACT | STREET | AREA
76 + geoSource: text("geo_source"), // role_unit:<id> | manual | geocoder
77 + roleUnitId: text("role_unit_id"),
78 + propertyType: text("property_type").notNull(), // appartement | condo | maison | jumele | plex | terrain | commercial | chalet | autre
79 + bedrooms: numeric("bedrooms", { precision: 4, scale: 1 }),
80 + bathrooms: numeric("bathrooms", { precision: 4, scale: 1 }),
81 + areaSqft: integer("area_sqft"),
82 + lotSqft: integer("lot_sqft"),
83 + yearBuilt: integer("year_built"),
84 + privacyLevel: text("privacy_level").notNull().default("EXACT"), // EXACT | STREET_ONLY | AREA_ONLY
85 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
86 +}, (t) => [index("properties_geo_idx").on(t.lat, t.lng), index("properties_city_idx").on(t.city)]);
87 +
88 +export const listings = pgTable("listings", {
89 + id: uuid("id").primaryKey().defaultRandom(),
90 + slug: text("slug").notNull(),
91 + propertyId: uuid("property_id").notNull().references(() => properties.id, { onDelete: "restrict" }),
92 + ownerUserId: uuid("owner_user_id").references(() => users.id, { onDelete: "set null" }),
93 + listingType: text("listing_type").notNull(), // SALE | RENT
94 + sourceType: text("source_type").notNull().default("DIRECT_OWNER"), // DIRECT_OWNER | DIRECT_AGENT | AGGREGATED_PARTNER
95 + status: text("status").notNull().default("DRAFT"), // DRAFT | PENDING_REVIEW | PUBLISHED | PAUSED | REJECTED | ARCHIVED | SOLD | RENTED
96 + title: text("title"),
97 + description: text("description"),
98 + priceAmount: numeric("price_amount", { precision: 12, scale: 2 }),
99 + currency: text("currency").notNull().default("CAD"),
100 + rentalPeriod: text("rental_period"), // MONTH (location)
101 + availabilityDate: text("availability_date"), // YYYY-MM-DD ou 'now'
102 + furnished: boolean("furnished"),
103 + petsPolicy: text("pets_policy"), // allowed | not_allowed | ask
104 + smokingPolicy: text("smoking_policy"),
105 + parking: text("parking"),
106 + leaseTerms: text("lease_terms"),
107 + inclusions: jsonb("inclusions").notNull().default(sql`'[]'::jsonb`),
108 + features: jsonb("features").notNull().default(sql`'{}'::jsonb`), // champs extensibles par type
109 + publicationAt: timestamp("publication_at", { withTimezone: true }),
110 + expiresAt: timestamp("expires_at", { withTimezone: true }),
111 + moderationState: text("moderation_state").notNull().default("NONE"), // NONE | AUTO_OK | FLAGGED | HUMAN_OK | REJECTED
112 + moderationNote: text("moderation_note"),
113 + externalUrl: text("external_url"),
114 + upstreamSource: text("upstream_source"),
115 + upstreamId: text("upstream_id"),
116 + upstreamSyncAt: timestamp("upstream_sync_at", { withTimezone: true }),
117 + wizardStep: integer("wizard_step").notNull().default(0),
118 + attestedAt: timestamp("attested_at", { withTimezone: true }), // attestation du droit de publier
119 + version: integer("version").notNull().default(1), // contrôle de concurrence optimiste
120 + viewsCount: integer("views_count").notNull().default(0),
121 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
122 + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
123 +}, (t) => [
124 + uniqueIndex("listings_slug_uq").on(t.slug),
125 + index("listings_status_idx").on(t.status, t.listingType, t.publicationAt),
126 + index("listings_owner_idx").on(t.ownerUserId),
127 + uniqueIndex("listings_upstream_uq").on(t.upstreamSource, t.upstreamId),
128 + check("listings_type_ck", sql`${t.listingType} in ('SALE','RENT')`),
129 + check("listings_source_ck", sql`${t.sourceType} in ('DIRECT_OWNER','DIRECT_AGENT','AGGREGATED_PARTNER')`),
130 + check("listings_status_ck", sql`${t.status} in ('DRAFT','PENDING_REVIEW','PUBLISHED','PAUSED','REJECTED','ARCHIVED','SOLD','RENTED')`),
131 + check("listings_price_ck", sql`${t.priceAmount} is null or ${t.priceAmount} >= 0`),
132 + check("listings_owner_direct_ck", sql`${t.sourceType} = 'AGGREGATED_PARTNER' or ${t.ownerUserId} is not null`),
133 + check("listings_rent_period_ck", sql`${t.listingType} <> 'RENT' or ${t.rentalPeriod} is null or ${t.rentalPeriod} in ('MONTH','WEEK')`),
134 +]);
135 +
136 +export const listingMedia = pgTable("listing_media", {
137 + id: uuid("id").primaryKey().defaultRandom(),
138 + listingId: uuid("listing_id").notNull().references(() => listings.id, { onDelete: "cascade" }),
139 + storageKey: text("storage_key").notNull(),
140 + mediaType: text("media_type").notNull().default("IMAGE"),
141 + mimeType: text("mime_type"),
142 + position: integer("position").notNull().default(0),
143 + altText: text("alt_text"),
144 + licenseStatus: text("license_status").notNull().default("OWNER_DECLARED"),
145 + scanStatus: text("scan_status").notNull().default("PENDING"), // PENDING | CLEAN | REJECTED
146 + width: integer("width"), height: integer("height"), bytes: integer("bytes"),
147 + status: text("status").notNull().default("UPLOADING"), // UPLOADING | READY | DELETED
148 + uploadToken: text("upload_token"),
149 + uploadedAt: timestamp("uploaded_at", { withTimezone: true }).notNull().defaultNow(),
150 +}, (t) => [index("listing_media_listing_idx").on(t.listingId, t.position)]);
151 +
152 +export const listingVersions = pgTable("listing_versions", {
153 + id: bigserial("id", { mode: "number" }).primaryKey(),
154 + listingId: uuid("listing_id").notNull().references(() => listings.id, { onDelete: "cascade" }),
155 + version: integer("version").notNull(),
156 + actorId: uuid("actor_id"),
157 + snapshot: jsonb("snapshot").notNull(),
158 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
159 +}, (t) => [index("listing_versions_listing_idx").on(t.listingId, t.version)]);
160 +
161 +export const listingClaims = pgTable("listing_claims", {
162 + id: uuid("id").primaryKey().defaultRandom(),
163 + aggregatedUid: text("aggregated_uid").notNull(), // uid de l'annonce agrégée (base canonique)
164 + requesterId: uuid("requester_id").notNull().references(() => users.id, { onDelete: "cascade" }),
165 + status: text("status").notNull().default("PENDING"), // PENDING | APPROVED | REJECTED
166 + message: text("message"),
167 + proofKeys: jsonb("proof_keys").notNull().default(sql`'[]'::jsonb`),
168 + reviewedBy: uuid("reviewed_by"),
169 + reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
170 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
171 +}, (t) => [index("listing_claims_uid_idx").on(t.aggregatedUid)]);
172 +
173 +export const favorites = pgTable("favorites", {
174 + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
175 + listingUid: text("listing_uid").notNull(), // 'toveli:<id>' ou uid agrégé
176 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
177 +}, (t) => [primaryKey({ columns: [t.userId, t.listingUid] })]);
178 +
179 +export const savedSearches = pgTable("saved_searches", {
180 + id: uuid("id").primaryKey().defaultRandom(),
181 + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
182 + label: text("label").notNull(),
183 + query: jsonb("query").notNull(),
184 + alertEnabled: boolean("alert_enabled").notNull().default(false),
185 + alertFrequency: text("alert_frequency").notNull().default("DAILY"), // INSTANT | DAILY | WEEKLY | OFF
186 + lastAlertAt: timestamp("last_alert_at", { withTimezone: true }),
187 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
188 +}, (t) => [index("saved_searches_user_idx").on(t.userId)]);
189 +
190 +export const emailDeliveries = pgTable("email_deliveries", {
191 + id: uuid("id").primaryKey().defaultRandom(),
192 + toHash: text("to_hash").notNull(),
193 + template: text("template").notNull(),
194 + providerId: text("provider_id"),
195 + status: text("status").notNull(), // QUEUED | SENT | MOCKED | FAILED
196 + error: text("error"),
197 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
198 +}, (t) => [index("email_deliveries_created_idx").on(t.createdAt)]);
199 +
200 +export const inquiries = pgTable("inquiries", {
201 + id: uuid("id").primaryKey().defaultRandom(),
202 + listingId: uuid("listing_id").notNull().references(() => listings.id, { onDelete: "cascade" }),
203 + senderUserId: uuid("sender_user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
204 + recipientUserId: uuid("recipient_user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
205 + category: text("category").notNull().default("INFO"), // INFO | VISIT | APPLICATION | OFFER
206 + status: text("status").notNull().default("OPEN"), // OPEN | ARCHIVED | BLOCKED
207 + lastMessageAt: timestamp("last_message_at", { withTimezone: true }).notNull().defaultNow(),
208 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
209 +}, (t) => [index("inquiries_listing_idx").on(t.listingId), index("inquiries_sender_idx").on(t.senderUserId), index("inquiries_recipient_idx").on(t.recipientUserId), uniqueIndex("inquiries_unique_thread").on(t.listingId, t.senderUserId)]);
210 +
211 +export const messages = pgTable("messages", {
212 + id: uuid("id").primaryKey().defaultRandom(),
213 + inquiryId: uuid("inquiry_id").notNull().references(() => inquiries.id, { onDelete: "cascade" }),
214 + senderId: uuid("sender_id").notNull().references(() => users.id, { onDelete: "cascade" }),
215 + body: text("body").notNull(),
216 + idempotencyKey: text("idempotency_key"),
217 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
218 + readAt: timestamp("read_at", { withTimezone: true }),
219 +}, (t) => [index("messages_inquiry_idx").on(t.inquiryId, t.createdAt), uniqueIndex("messages_idem_uq").on(t.inquiryId, t.idempotencyKey)]);
220 +
221 +export const viewingRequests = pgTable("viewing_requests", {
222 + id: uuid("id").primaryKey().defaultRandom(),
223 + listingId: uuid("listing_id").notNull().references(() => listings.id, { onDelete: "cascade" }),
224 + inquiryId: uuid("inquiry_id").references(() => inquiries.id, { onDelete: "set null" }),
225 + requesterId: uuid("requester_id").notNull().references(() => users.id, { onDelete: "cascade" }),
226 + ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
227 + proposedSlots: jsonb("proposed_slots").notNull(), // [ISO America/Toronto]
228 + confirmedSlot: timestamp("confirmed_slot", { withTimezone: true }),
229 + status: text("status").notNull().default("REQUESTED"), // REQUESTED | CONFIRMED | CANCELLED | DONE
230 + note: text("note"),
231 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
232 + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
233 +}, (t) => [index("viewings_listing_idx").on(t.listingId), index("viewings_requester_idx").on(t.requesterId), index("viewings_owner_idx").on(t.ownerId), check("viewings_status_ck", sql`${t.status} in ('REQUESTED','CONFIRMED','CANCELLED','DONE')`)]);
234 +
235 +export const reports = pgTable("reports", {
236 + id: uuid("id").primaryKey().defaultRandom(),
237 + targetType: text("target_type").notNull(), // LISTING | USER | MESSAGE | AGGREGATED_LISTING
238 + targetId: text("target_id").notNull(),
239 + reporterId: uuid("reporter_id").references(() => users.id, { onDelete: "set null" }),
240 + reason: text("reason").notNull(),
241 + details: text("details"),
242 + status: text("status").notNull().default("OPEN"), // OPEN | RESOLVED | DISMISSED
243 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
244 +}, (t) => [index("reports_status_idx").on(t.status, t.createdAt)]);
245 +
246 +export const moderationActions = pgTable("moderation_actions", {
247 + id: bigserial("id", { mode: "number" }).primaryKey(),
248 + actorId: uuid("actor_id").notNull(),
249 + targetType: text("target_type").notNull(),
250 + targetId: text("target_id").notNull(),
251 + action: text("action").notNull(),
252 + note: text("note"),
253 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
254 +});
255 +
256 +export const auditEvents = pgTable("audit_events", {
257 + id: bigserial("id", { mode: "number" }).primaryKey(),
258 + actorId: uuid("actor_id"),
259 + action: text("action").notNull(),
260 + objectType: text("object_type"),
261 + objectId: text("object_id"),
262 + ipHash: text("ip_hash"),
263 + metadataRedacted: jsonb("metadata_redacted").notNull().default(sql`'{}'::jsonb`),
264 + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
265 +}, (t) => [index("audit_created_idx").on(t.createdAt), index("audit_actor_idx").on(t.actorId)]);
266 +
267 +export const rateLimits = pgTable("rate_limits", {
268 + key: text("key").notNull(), // ex. otp:ip:<hash> | otp:email:<hash>
269 + windowStart: timestamp("window_start", { withTimezone: true }).notNull(),
270 + count: integer("count").notNull().default(0),
271 +}, (t) => [primaryKey({ columns: [t.key, t.windowStart] })]);
added src/lib/auth/audit.ts +7 −0
@@ -0,0 +1,7 @@
1 +import { db } from "@/db/client";
2 +import { auditEvents } from "@/db/schema";
3 +/** Journal append-only ; métadonnées minimisées (jamais de code OTP, de jeton, d'adresse en clair). */
4 +export async function audit(action: string, opts: { actorId?: string | null; objectType?: string; objectId?: string; ipHash?: string; meta?: Record<string, unknown> } = {}) {
5 + try { await db().insert(auditEvents).values({ action, actorId: opts.actorId ?? null, objectType: opts.objectType ?? null, objectId: opts.objectId ?? null, ipHash: opts.ipHash ?? null, metadataRedacted: opts.meta ?? {} }); }
6 + catch (e) { console.error("[audit]", action, (e as Error).message); }
7 +}
added src/lib/auth/crypto.ts +28 −0
@@ -0,0 +1,28 @@
1 +import { createHash, createHmac, randomBytes, randomInt, timingSafeEqual, createCipheriv, createDecipheriv } from "node:crypto";
2 +
3 +function secret(name: "OTP_HMAC_SECRET" | "SESSION_SECRET"): string {
4 + const v = process.env[name]; if (!v || v.length < 16) throw new Error(`${name} manquante ou trop courte`); return v;
5 +}
6 +/** Adresse normalisée : minuscules, espaces retirés ; points et « + » conservés (pas de suppression arbitraire). */
7 +export function normalizeEmail(raw: string): string | null {
8 + const e = raw.trim().toLowerCase().replace(/\s+/g, "");
9 + if (!/^[^@\s]{1,64}@[^@\s]+\.[a-z]{2,}$/i.test(e) || e.length > 254) return null;
10 + return e;
11 +}
12 +export function sha256(s: string): string { return createHash("sha256").update(s).digest("hex"); }
13 +export function ipHash(ip: string | null | undefined): string { return sha256(`ip:${ip ?? "?"}:${secret("SESSION_SECRET").slice(0, 8)}`).slice(0, 32); }
14 +export function emailHash(email: string): string { return sha256(`email:${email}`).slice(0, 32); }
15 +
16 +/** Code OTP à 6 chiffres par CSPRNG (randomInt est uniforme). */
17 +export function generateOtp(): string { return String(randomInt(0, 1_000_000)).padStart(6, "0"); }
18 +/** Empreinte HMAC-SHA256 du code, liée à l'identifiant du défi (évite les tables arc-en-ciel sur 10^6 codes). */
19 +export function otpDigest(code: string, challengeId: string): string { return createHmac("sha256", secret("OTP_HMAC_SECRET")).update(`${challengeId}:${code}`).digest("hex"); }
20 +export function safeEqualHex(a: string, b: string): boolean { const ba = Buffer.from(a, "hex"), bb = Buffer.from(b, "hex"); return ba.length === bb.length && timingSafeEqual(ba, bb); }
21 +
22 +export function newSessionToken(): string { return randomBytes(32).toString("base64url"); }
23 +export function sessionDigest(token: string): string { return createHmac("sha256", secret("SESSION_SECRET")).update(token).digest("hex"); }
24 +
25 +/** Chiffrement symétrique (AES-256-GCM) pour champs sensibles (téléphone, secret TOTP) — clé dérivée de SESSION_SECRET. */
26 +function encKey(): Buffer { return createHash("sha256").update(`enc:${secret("SESSION_SECRET")}`).digest(); }
27 +export function encrypt(plain: string): string { const iv = randomBytes(12); const c = createCipheriv("aes-256-gcm", encKey(), iv); const ct = Buffer.concat([c.update(plain, "utf8"), c.final()]); return `${iv.toString("base64url")}.${ct.toString("base64url")}.${c.getAuthTag().toString("base64url")}`; }
28 +export function decrypt(blob: string): string { const [iv, ct, tag] = blob.split(".").map((x) => Buffer.from(x, "base64url")); const d = createDecipheriv("aes-256-gcm", encKey(), iv); d.setAuthTag(tag); return Buffer.concat([d.update(ct), d.final()]).toString("utf8"); }
added src/lib/auth/email.ts +48 −0
@@ -0,0 +1,48 @@
1 +// Transport courriel : Resend (SDK officiel, clé serveur) ou simulation locale (dev/tests : écrit dans data/outbox, jamais d'envoi réel).
2 +import { Resend } from "resend";
3 +import { appendFileSync, mkdirSync } from "node:fs";
4 +import path from "node:path";
5 +import { db } from "@/db/client";
6 +import { emailDeliveries } from "@/db/schema";
7 +import { emailHash } from "./crypto";
8 +
9 +export interface Mail { to: string; subject: string; html: string; text: string; template: string }
10 +export type Transport = { name: string; send(m: Mail): Promise<{ id: string | null }> };
11 +
12 +const outboxDir = path.join(process.env.STORAGE_DIR ? path.dirname(process.env.STORAGE_DIR) : path.join(process.cwd(), "data"), "outbox");
13 +export const mockTransport: Transport = {
14 + name: "mock",
15 + async send(m) { mkdirSync(outboxDir, { recursive: true }); const id = `mock-${Date.now()}`; appendFileSync(path.join(outboxDir, "mails.jsonl"), JSON.stringify({ id, at: new Date().toISOString(), ...m }) + "\n"); if (process.env.NODE_ENV !== "production") console.log(`[mail:mock] → ${m.to} « ${m.subject} »`); return { id }; },
16 +};
17 +export const lastMockMail = { set: null as Mail | null }; // utilisé par les tests
18 +function resendTransport(key: string): Transport {
19 + const r = new Resend(key); const from = process.env.RESEND_FROM_EMAIL ?? "connexion@toveli.ca";
20 + return { name: "resend", async send(m) { const res = await r.emails.send({ from: `Toveli <${from}>`, to: m.to, subject: m.subject, html: m.html, text: m.text }); if (res.error) throw new Error(res.error.message); return { id: res.data?.id ?? null }; } };
21 +}
22 +let override: Transport | null = null;
23 +export function setTransportForTests(t: Transport | null) { override = t; }
24 +export function transport(): Transport {
25 + if (override) return override;
26 + const key = process.env.RESEND_API_KEY;
27 + if (key && process.env.NODE_ENV === "production") return resendTransport(key);
28 + if (key && process.env.RESEND_FORCE === "1") return resendTransport(key);
29 + return mockTransport;
30 +}
31 +export async function sendMail(m: Mail): Promise<void> {
32 + const t = transport(); let status = "SENT", providerId: string | null = null, error: string | null = null;
33 + try { const r = await t.send(m); providerId = r.id; if (t.name === "mock") { status = "MOCKED"; lastMockMail.set = m; } }
34 + catch (e) { status = "FAILED"; error = (e as Error).message.slice(0, 300); console.error("[mail] échec", m.template, error); }
35 + try { await db().insert(emailDeliveries).values({ toHash: emailHash(m.to), template: m.template, providerId, status, error }); } catch { /* journal best-effort */ }
36 + if (status === "FAILED") throw new Error("mail_failed");
37 +}
38 +
39 +const layout = (title: string, body: string) => `<!doctype html><html lang="fr"><body style="margin:0;background:#f6f7f9;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#15181d"><div style="max-width:520px;margin:0 auto;padding:32px 16px"><div style="font-size:28px;font-weight:800;letter-spacing:-.02em">toveli<span style="color:#e0263c">.</span></div><div style="background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:24px;margin-top:16px"><h1 style="font-size:20px;margin:0 0 12px">${title}</h1>${body}</div><p style="font-size:12px;color:#7a8190;margin-top:16px">Toveli · toveli.ca · Ce message est envoyé automatiquement, merci de ne pas y répondre.</p></div></body></html>`;
40 +
41 +export function otpMail(to: string, code: string): Mail {
42 + const html = layout("Votre code de connexion", `<p style="margin:0 0 16px;color:#4b5260">Entrez ce code sur Toveli pour vous connecter. Il est valable <strong>10 minutes</strong> et ne peut servir qu'une fois.</p><p style="font-size:36px;font-weight:800;letter-spacing:.3em;text-align:center;margin:8px 0 16px;font-variant-numeric:tabular-nums">${code}</p><p style="font-size:13px;color:#7a8190;margin:0">Si vous n'avez rien demandé, ignorez ce message : personne ne peut se connecter sans ce code.</p>`);
43 + return { to, subject: "Votre code de connexion Toveli", html, text: `Votre code de connexion Toveli : ${code}\nValable 10 minutes, usage unique.\nSi vous n'avez rien demandé, ignorez ce message.`, template: "otp" };
44 +}
45 +export function notifyMail(to: string, title: string, body: string, ctaUrl: string, ctaLabel: string, template: string): Mail {
46 + const html = layout(title, `<p style="margin:0 0 16px;color:#4b5260">${body}</p><p style="text-align:center;margin:8px 0"><a href="${ctaUrl}" style="display:inline-block;background:#e0263c;color:#fff;text-decoration:none;font-weight:600;padding:12px 20px;border-radius:999px">${ctaLabel}</a></p><p style="font-size:13px;color:#7a8190;margin:12px 0 0">Vous recevez ce message parce que vous avez un compte Toveli. Préférences : ${process.env.APP_BASE_URL ?? "https://www.toveli.ca"}/membre/profil</p>`);
47 + return { to, subject: `${title} · Toveli`, html, text: `${title}\n\n${body}\n\n${ctaUrl}`, template };
48 +}
added src/lib/auth/otp.ts +56 −0
@@ -0,0 +1,56 @@
1 +import { and, eq, isNull, sql, lt } from "drizzle-orm";
2 +import { db } from "@/db/client";
3 +import { otpChallenges, users, userProfiles } from "@/db/schema";
4 +import { generateOtp, otpDigest, safeEqualHex, normalizeEmail, ipHash } from "./crypto";
5 +import { sendMail, otpMail } from "./email";
6 +import { hit } from "./ratelimit";
7 +import { audit } from "./audit";
8 +
9 +const TTL_MIN = 10, MAX_ATTEMPTS = 5, RESEND_DELAY_SEC = 60;
10 +export type RequestResult = { ok: true; challengeId: string; retryAfterSec: number } | { ok: false; reason: "invalid_email" | "rate_limited" | "cooldown"; retryAfterSec: number };
11 +
12 +/** Demande de code : réponse neutre côté API (n'indique pas si le compte existe). */
13 +export async function requestCode(rawEmail: string, ip: string | null, purpose: "LOGIN" | "EMAIL_CHANGE" = "LOGIN", meta: Record<string, unknown> = {}): Promise<RequestResult> {
14 + const email = normalizeEmail(rawEmail); if (!email) return { ok: false, reason: "invalid_email", retryAfterSec: 0 };
15 + const iph = ipHash(ip);
16 + const [byIp, byEmailHour, byEmailDay, byIpDay] = await Promise.all([hit(`otp:ip:${iph}`, 10, 600), hit(`otp:email:${email}`, 5, 3600), hit(`otp:email:day:${email}`, 12, 86400), hit(`otp:ip:day:${iph}`, 60, 86400)]);
17 + if (!byIp.ok || !byEmailHour.ok || !byEmailDay.ok || !byIpDay.ok) { await audit("otp.rate_limited", { ipHash: iph }); return { ok: false, reason: "rate_limited", retryAfterSec: Math.max(byIp.retryAfterSec, byEmailHour.retryAfterSec, 60) }; }
18 + const d = db();
19 + const [recent] = await d.select({ createdAt: otpChallenges.createdAt }).from(otpChallenges).where(and(eq(otpChallenges.emailNormalized, email), eq(otpChallenges.purpose, purpose), isNull(otpChallenges.consumedAt), sql`${otpChallenges.createdAt} > now() - interval '${sql.raw(String(RESEND_DELAY_SEC))} seconds'`)).limit(1);
20 + if (recent) return { ok: false, reason: "cooldown", retryAfterSec: Math.max(1, RESEND_DELAY_SEC - Math.floor((Date.now() - recent.createdAt.getTime()) / 1000)) };
21 + // Invalide les défis précédents encore ouverts pour cette adresse/finalité
22 + await d.update(otpChallenges).set({ consumedAt: new Date() }).where(and(eq(otpChallenges.emailNormalized, email), eq(otpChallenges.purpose, purpose), isNull(otpChallenges.consumedAt)));
23 + const code = generateOtp();
24 + const [ch] = await d.insert(otpChallenges).values({ emailNormalized: email, purpose, secretDigest: "pending", expiresAt: new Date(Date.now() + TTL_MIN * 60000), requestedIpHash: iph, meta }).returning({ id: otpChallenges.id });
25 + await d.update(otpChallenges).set({ secretDigest: otpDigest(code, ch.id) }).where(eq(otpChallenges.id, ch.id));
26 + await sendMail(otpMail(email, code));
27 + await audit("otp.requested", { objectType: "otp_challenge", objectId: ch.id, ipHash: iph, meta: { purpose } });
28 + if (Math.random() < 0.05) await d.delete(otpChallenges).where(lt(otpChallenges.expiresAt, new Date(Date.now() - 86400000)));
29 + return { ok: true, challengeId: ch.id, retryAfterSec: RESEND_DELAY_SEC };
30 +}
31 +
32 +export type VerifyResult = { ok: true; userId: string; email: string; created: boolean } | { ok: false; reason: "invalid" | "expired" | "too_many_attempts" | "not_found" };
33 +
34 +/** Vérification atomique : une seule transaction, verrou de ligne, compteur de tentatives, usage unique. */
35 +export async function verifyCode(challengeId: string, code: string, ip: string | null, acceptTerms: boolean): Promise<VerifyResult> {
36 + if (!/^\d{6}$/.test(code) || !/^[0-9a-f-]{36}$/.test(challengeId)) return { ok: false, reason: "invalid" };
37 + const d = db(); const iph = ipHash(ip);
38 + return d.transaction(async (tx) => {
39 + const [ch] = await tx.select().from(otpChallenges).where(eq(otpChallenges.id, challengeId)).for("update");
40 + if (!ch) return { ok: false, reason: "not_found" as const };
41 + if (ch.consumedAt) { await audit("otp.reuse_blocked", { objectId: ch.id, ipHash: iph }); return { ok: false, reason: "invalid" as const }; }
42 + if (ch.expiresAt.getTime() < Date.now()) { await tx.update(otpChallenges).set({ consumedAt: new Date() }).where(eq(otpChallenges.id, ch.id)); return { ok: false, reason: "expired" as const }; }
43 + if (ch.attempts >= MAX_ATTEMPTS) { await tx.update(otpChallenges).set({ consumedAt: new Date() }).where(eq(otpChallenges.id, ch.id)); await audit("otp.too_many_attempts", { objectId: ch.id, ipHash: iph }); return { ok: false, reason: "too_many_attempts" as const }; }
44 + const good = safeEqualHex(ch.secretDigest, otpDigest(code, ch.id));
45 + if (!good) { const attempts = ch.attempts + 1; await tx.update(otpChallenges).set({ attempts, consumedAt: attempts >= MAX_ATTEMPTS ? new Date() : null }).where(eq(otpChallenges.id, ch.id)); await audit("otp.failed", { objectId: ch.id, ipHash: iph, meta: { attempts } }); return { ok: false, reason: attempts >= MAX_ATTEMPTS ? ("too_many_attempts" as const) : ("invalid" as const) }; }
46 + await tx.update(otpChallenges).set({ consumedAt: new Date(), attempts: ch.attempts + 1 }).where(eq(otpChallenges.id, ch.id));
47 + if (ch.purpose !== "LOGIN") return { ok: true as const, userId: String(ch.meta && (ch.meta as { userId?: string }).userId), email: ch.emailNormalized, created: false };
48 + const [existing] = await tx.select({ id: users.id, status: users.status }).from(users).where(eq(users.email, ch.emailNormalized)).limit(1);
49 + if (existing) { if (existing.status !== "ACTIVE") return { ok: false, reason: "invalid" as const }; await tx.update(users).set({ emailVerifiedAt: sql`coalesce(${users.emailVerifiedAt}, now())`, updatedAt: new Date() }).where(eq(users.id, existing.id)); await audit("auth.login", { actorId: existing.id, ipHash: iph }); return { ok: true as const, userId: existing.id, email: ch.emailNormalized, created: false }; }
50 + if (!acceptTerms) return { ok: false, reason: "invalid" as const };
51 + const [u] = await tx.insert(users).values({ email: ch.emailNormalized, emailVerifiedAt: new Date(), prefs: { termsAcceptedAt: new Date().toISOString(), notifications: { inquiries: true, viewings: true, alerts: true, marketing: false } } }).returning({ id: users.id });
52 + await tx.insert(userProfiles).values({ userId: u.id });
53 + await audit("auth.signup", { actorId: u.id, ipHash: iph });
54 + return { ok: true as const, userId: u.id, email: ch.emailNormalized, created: true };
55 + });
56 +}
added src/lib/auth/ratelimit.ts +15 −0
@@ -0,0 +1,15 @@
1 +import { sql } from "drizzle-orm";
2 +import { db } from "@/db/client";
3 +import { rateLimits } from "@/db/schema";
4 +
5 +/** Fenêtre glissante approximée par tranches : compte les tranches actives, incrémente la courante. Jamais de blocage irréversible. */
6 +export async function hit(key: string, limit: number, windowSec: number, slices = 6): Promise<{ ok: boolean; remaining: number; retryAfterSec: number }> {
7 + const sliceMs = Math.max(1000, Math.floor((windowSec * 1000) / slices));
8 + const now = Date.now(); const cur = new Date(Math.floor(now / sliceMs) * sliceMs); const since = new Date(now - windowSec * 1000);
9 + const d = db();
10 + await d.insert(rateLimits).values({ key, windowStart: cur, count: 1 }).onConflictDoUpdate({ target: [rateLimits.key, rateLimits.windowStart], set: { count: sql`${rateLimits.count} + 1` } });
11 + const [row] = await d.select({ total: sql<number>`coalesce(sum(${rateLimits.count}), 0)::int` }).from(rateLimits).where(sql`${rateLimits.key} = ${key} and ${rateLimits.windowStart} >= ${since}`);
12 + const total = Number(row?.total ?? 0);
13 + if (Math.random() < 0.02) await d.execute(sql`delete from ${rateLimits} where ${rateLimits.windowStart} < now() - interval '2 days'`);
14 + return { ok: total <= limit, remaining: Math.max(0, limit - total), retryAfterSec: total <= limit ? 0 : Math.ceil(sliceMs / 1000) };
15 +}
added src/lib/auth/session.ts +50 −0
@@ -0,0 +1,50 @@
1 +import { and, eq, gt, isNull, sql } from "drizzle-orm";
2 +import { cookies, headers } from "next/headers";
3 +import { db } from "@/db/client";
4 +import { sessions, users, userRoles, userProfiles } from "@/db/schema";
5 +import { newSessionToken, sessionDigest, ipHash } from "./crypto";
6 +
7 +export const COOKIE = "tv_session";
8 +const TTL_DAYS = Number(process.env.SESSION_TTL_DAYS ?? 30);
9 +const INACTIVITY_DAYS = Number(process.env.SESSION_INACTIVITY_DAYS ?? 14);
10 +
11 +export interface SessionUser { id: string; email: string; displayName: string | null; roles: string[]; sessionId: string; mfaAt: Date | null; prefs: Record<string, unknown>; publicName: string | null; sellerType: string }
12 +
13 +export async function createSession(userId: string, opts: { ip?: string | null; device?: string | null }): Promise<{ token: string; expiresAt: Date }> {
14 + const token = newSessionToken(); const expiresAt = new Date(Date.now() + TTL_DAYS * 86400000);
15 + await db().insert(sessions).values({ userId, tokenDigest: sessionDigest(token), expiresAt, deviceLabel: opts.device?.slice(0, 120) ?? null, ipHash: ipHash(opts.ip) });
16 + return { token, expiresAt };
17 +}
18 +export function cookieOptions(expiresAt: Date) {
19 + return { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax" as const, path: "/", expires: expiresAt };
20 +}
21 +export async function currentUser(): Promise<SessionUser | null> {
22 + const token = (await cookies()).get(COOKIE)?.value; if (!token) return null;
23 + const d = db(); const digest = sessionDigest(token); const now = new Date();
24 + const [row] = await d.select({ s: sessions, u: users, p: userProfiles }).from(sessions).innerJoin(users, eq(users.id, sessions.userId)).leftJoin(userProfiles, eq(userProfiles.userId, users.id))
25 + .where(and(eq(sessions.tokenDigest, digest), isNull(sessions.revokedAt), gt(sessions.expiresAt, now))).limit(1);
26 + if (!row || row.u.status !== "ACTIVE") return null;
27 + if (now.getTime() - row.s.lastSeenAt.getTime() > INACTIVITY_DAYS * 86400000) { await d.update(sessions).set({ revokedAt: now }).where(eq(sessions.id, row.s.id)); return null; }
28 + if (now.getTime() - row.s.lastSeenAt.getTime() > 5 * 60000) await d.update(sessions).set({ lastSeenAt: now }).where(eq(sessions.id, row.s.id));
29 + const roles = (await d.select({ role: userRoles.role }).from(userRoles).where(eq(userRoles.userId, row.u.id))).map((r) => r.role);
30 + return { id: row.u.id, email: row.u.email, displayName: row.u.displayName, roles, sessionId: row.s.id, mfaAt: row.s.mfaAt, prefs: (row.u.prefs ?? {}) as Record<string, unknown>, publicName: row.p?.publicName ?? null, sellerType: row.p?.sellerType ?? "OWNER" };
31 +}
32 +export async function requireUser(): Promise<SessionUser> { const u = await currentUser(); if (!u) throw new AuthError(401, "Connexion requise"); return u; }
33 +export async function requireRole(role: "ADMIN" | "MODERATOR"): Promise<SessionUser> { const u = await requireUser(); if (!u.roles.includes(role) && !u.roles.includes("ADMIN")) throw new AuthError(403, "Accès refusé"); return u; }
34 +export class AuthError extends Error { constructor(public status: number, msg: string) { super(msg); } }
35 +
36 +export async function revokeSession(sessionId: string, userId: string) { await db().update(sessions).set({ revokedAt: new Date() }).where(and(eq(sessions.id, sessionId), eq(sessions.userId, userId))); }
37 +export async function revokeAllSessions(userId: string, exceptSessionId?: string) { await db().update(sessions).set({ revokedAt: new Date() }).where(and(eq(sessions.userId, userId), isNull(sessions.revokedAt), exceptSessionId ? sql`${sessions.id} <> ${exceptSessionId}` : sql`true`)); }
38 +export async function listSessions(userId: string) { return db().select({ id: sessions.id, issuedAt: sessions.issuedAt, lastSeenAt: sessions.lastSeenAt, expiresAt: sessions.expiresAt, deviceLabel: sessions.deviceLabel, revokedAt: sessions.revokedAt }).from(sessions).where(and(eq(sessions.userId, userId), isNull(sessions.revokedAt), gt(sessions.expiresAt, new Date()))).orderBy(sql`${sessions.lastSeenAt} desc`); }
39 +
40 +/** Protection CSRF des mutations : même origine obligatoire (cookie SameSite=Lax + vérification Origin/Referer). */
41 +export async function assertSameOrigin(): Promise<void> {
42 + const h = await headers(); const origin = h.get("origin") ?? (h.get("referer") ? new URL(h.get("referer")!).origin : null);
43 + const base = process.env.APP_BASE_URL; const host = h.get("x-forwarded-host") ?? h.get("host");
44 + if (!origin) throw new AuthError(403, "Origine manquante");
45 + const o = new URL(origin);
46 + if ((base && origin === base.replace(/\/$/, "")) || (host && o.host === host)) return;
47 + throw new AuthError(403, "Origine non autorisée");
48 +}
49 +export async function clientIp(): Promise<string | null> { const h = await headers(); return h.get("x-forwarded-for")?.split(",")[0].trim() ?? h.get("x-real-ip") ?? null; }
50 +export async function userAgent(): Promise<string | null> { return (await headers()).get("user-agent"); }
51