@nymbalhq/platform 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/chunk-74KODSYJ.js +2564 -0
  2. package/dist/chunk-74KODSYJ.js.map +1 -0
  3. package/dist/chunk-PW6ILJF7.js +740 -0
  4. package/dist/chunk-PW6ILJF7.js.map +1 -0
  5. package/dist/chunk-PZ5AY32C.js +10 -0
  6. package/dist/chunk-PZ5AY32C.js.map +1 -0
  7. package/dist/db/schema/index.d.ts +10125 -0
  8. package/dist/db/schema/index.js +10 -0
  9. package/dist/db/schema/index.js.map +1 -0
  10. package/dist/index.d.ts +1373 -0
  11. package/dist/index.js +6462 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/register-IVFYBJSX.js +8 -0
  14. package/dist/register-IVFYBJSX.js.map +1 -0
  15. package/drizzle.config.postgres.ts +10 -0
  16. package/drizzle.config.sqlite.ts +10 -0
  17. package/migrations/postgres/0000_tearful_longshot.sql +166 -0
  18. package/migrations/postgres/0001_neat_zeigeist.sql +1 -0
  19. package/migrations/postgres/0002_sloppy_mad_thinker.sql +20 -0
  20. package/migrations/postgres/0003_violet_violations.sql +15 -0
  21. package/migrations/postgres/0004_white_reavers.sql +15 -0
  22. package/migrations/postgres/0005_natural_nico_minoru.sql +12 -0
  23. package/migrations/postgres/0006_minor_dragon_man.sql +28 -0
  24. package/migrations/postgres/meta/0000_snapshot.json +1172 -0
  25. package/migrations/postgres/meta/0001_snapshot.json +1187 -0
  26. package/migrations/postgres/meta/0002_snapshot.json +1325 -0
  27. package/migrations/postgres/meta/0003_snapshot.json +1432 -0
  28. package/migrations/postgres/meta/0004_snapshot.json +1439 -0
  29. package/migrations/postgres/meta/0005_snapshot.json +1461 -0
  30. package/migrations/postgres/meta/0006_snapshot.json +1598 -0
  31. package/migrations/postgres/meta/_journal.json +55 -0
  32. package/migrations/sqlite/0000_glorious_vision.sql +166 -0
  33. package/migrations/sqlite/0001_tricky_glorian.sql +1 -0
  34. package/migrations/sqlite/0002_puzzling_epoch.sql +19 -0
  35. package/migrations/sqlite/0003_fantastic_captain_stacy.sql +15 -0
  36. package/migrations/sqlite/0004_remarkable_shriek.sql +12 -0
  37. package/migrations/sqlite/0005_conscious_speedball.sql +13 -0
  38. package/migrations/sqlite/0006_flat_psylocke.sql +28 -0
  39. package/migrations/sqlite/meta/0000_snapshot.json +1093 -0
  40. package/migrations/sqlite/meta/0001_snapshot.json +1100 -0
  41. package/migrations/sqlite/meta/0002_snapshot.json +1222 -0
  42. package/migrations/sqlite/meta/0003_snapshot.json +1320 -0
  43. package/migrations/sqlite/meta/0004_snapshot.json +1328 -0
  44. package/migrations/sqlite/meta/0005_snapshot.json +1343 -0
  45. package/migrations/sqlite/meta/0006_snapshot.json +1470 -0
  46. package/migrations/sqlite/meta/_journal.json +55 -0
  47. package/package.json +85 -3
  48. package/README.md +0 -3
@@ -0,0 +1,1373 @@
1
+ import { NymbalConfig } from '@nymbalhq/config';
2
+ import { Logger, DocumentStoreAdapter, EventBusAdapter, Product, ProductStatus, ProductType, ProductMedia, Variant, VariantDimensions, VariantOption, VariantStatus, Category, Customer, Role, CustomerAddress, Order, OrderStatus, Address, OrderLineItem, CaptureMethod, OrderHistoryEntry, OrderRefundStatus, OrderRefund, InventoryReservation, StockAdjustmentReason, StockAdjustment, Review, ReviewModerationStatus, NymbalEvent, Cart, PutOptions, QueryParams, QueryResult, EventHandler, SubscriptionConfig, Subscription, NymbalEnvironment, NymbalEventType, NymbalEventMap, PaymentsAdapter, EmailAdapter, ReviewsAdapter, SearchAdapter, AnalyticsAdapter, ShippingAdapter, TaxAdapter, AiAdapter, SecurityAdapter, ProductSnapshot, CartItem, StockStatus, PaymentIntent, CheckoutPayment, SearchParams, SearchResult, OrderPayment, Refund, HttpAdapter, CommerceSlot, RateLimitAction, SecurityConfigLike } from '@nymbalhq/types';
3
+ import BetterSqlite3 from 'better-sqlite3';
4
+ import pg from 'pg';
5
+ import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
6
+ import { NodePgDatabase } from 'drizzle-orm/node-postgres';
7
+ import { postgresSchema, sqliteSchema } from './db/schema/index.js';
8
+ import { JWTPayload } from 'jose';
9
+ import { ZodTypeAny } from 'zod';
10
+ import 'drizzle-orm/sqlite-core';
11
+ import 'drizzle-orm/pg-core';
12
+
13
+ type SqliteClient = BetterSQLite3Database<typeof sqliteSchema>;
14
+ type PostgresClient = NodePgDatabase<typeof postgresSchema>;
15
+ declare const readBrand: unique symbol;
16
+ declare const writeBrand: unique symbol;
17
+ /** A handle for reads: committed data, or the open transaction's view when one is open in the call chain. */
18
+ interface ReadTx {
19
+ readonly kind: 'sqlite' | 'postgres';
20
+ readonly [readBrand]: true;
21
+ }
22
+ /** A handle for writes: only `CommandStore.transaction` hands one out. */
23
+ interface Tx extends ReadTx {
24
+ readonly [writeBrand]: true;
25
+ }
26
+ interface CommandStore {
27
+ readonly kind: 'sqlite' | 'postgres';
28
+ /** Runs reads. Inside an open transaction (same async call chain) it uses that transaction's Tx; outside, committed
29
+ * data only (SQLite: it takes its turn on the queue; Postgres: a pool connection for the callback). */
30
+ read<T>(fn: (tx: ReadTx) => Promise<T>): Promise<T>;
31
+ /** Opens a transaction, or joins the one open in the same async call chain (no new BEGIN; commits and rolls back
32
+ * with the outermost). */
33
+ transaction<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;
34
+ /** Runs `fn` behind a savepoint in the transaction open in the same async call chain: if `fn` throws, its writes
35
+ * (and only its writes) are undone, the error is rethrown, and the enclosing transaction carries on. With no
36
+ * transaction open it is `transaction`. */
37
+ savepoint<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;
38
+ close(): Promise<void>;
39
+ }
40
+ /** A repository over one command store. A caller handed only the repository (a projection, a sweeper) opens its
41
+ * reads and transactions on `store`. */
42
+ interface StoreRepository {
43
+ readonly store: CommandStore;
44
+ }
45
+ type RawClients = {
46
+ kind: 'sqlite';
47
+ db: SqliteClient;
48
+ raw: BetterSqlite3.Database;
49
+ } | {
50
+ kind: 'postgres';
51
+ db: PostgresClient;
52
+ pool: pg.Pool;
53
+ };
54
+ interface CommandStoreOptions {
55
+ sqlitePath?: string;
56
+ postgresUrl?: string;
57
+ }
58
+ /** For migrations, seeding and tests only; no repository or service imports it. */
59
+ declare function rawClients(store: CommandStore): RawClients;
60
+ declare function createCommandStore(config: NymbalConfig, options?: CommandStoreOptions): CommandStore;
61
+
62
+ interface Platform {
63
+ config: NymbalConfig;
64
+ commandStore: CommandStore;
65
+ documentStore: DocumentStoreAdapter;
66
+ eventBus: EventBusAdapter;
67
+ logger: Logger;
68
+ close(): Promise<void>;
69
+ }
70
+ interface CreatePlatformOptions {
71
+ logger?: Logger;
72
+ sqlitePath?: string;
73
+ postgresUrl?: string;
74
+ }
75
+ declare function createPlatform(config: NymbalConfig, options?: CreatePlatformOptions): Platform;
76
+
77
+ interface MigrateOptions {
78
+ migrationsRoot?: string;
79
+ }
80
+ declare function runMigrations(store: CommandStore, options?: MigrateOptions): Promise<void>;
81
+
82
+ interface ProductInsert$1 {
83
+ id: string;
84
+ slug: string;
85
+ name: string;
86
+ description: string;
87
+ shortDescription?: string;
88
+ status?: 'draft' | 'active' | 'archived';
89
+ type?: 'simple' | 'variable';
90
+ seoTitle?: string;
91
+ seoDescription?: string;
92
+ media?: unknown[];
93
+ metadata?: Record<string, unknown>;
94
+ createdAt: Date;
95
+ updatedAt: Date;
96
+ }
97
+ interface ProductRow {
98
+ id: string;
99
+ slug: string;
100
+ name: string;
101
+ description: string;
102
+ shortDescription: string;
103
+ status: 'draft' | 'active' | 'archived';
104
+ type: 'simple' | 'variable';
105
+ seoTitle: string;
106
+ seoDescription: string;
107
+ media: unknown[];
108
+ metadata: Record<string, unknown>;
109
+ createdAt: Date | string;
110
+ updatedAt: Date | string;
111
+ }
112
+ declare function createProductRepository$1(store: CommandStore): {
113
+ findAll(): Promise<ProductRow[]>;
114
+ findBySlug(slug: string): Promise<ProductRow | null>;
115
+ insertMany(items: ProductInsert$1[]): Promise<void>;
116
+ };
117
+
118
+ interface CategoryInsert$1 {
119
+ id: string;
120
+ parentId?: string | null;
121
+ slug: string;
122
+ name: string;
123
+ description: string;
124
+ position?: number;
125
+ createdAt: Date;
126
+ }
127
+ interface CategoryRow {
128
+ id: string;
129
+ parentId: string | null;
130
+ slug: string;
131
+ name: string;
132
+ description: string;
133
+ position: number;
134
+ createdAt: Date | string;
135
+ }
136
+ declare function createCategoryRepository$1(store: CommandStore): {
137
+ findAll(): Promise<CategoryRow[]>;
138
+ insertMany(items: CategoryInsert$1[]): Promise<void>;
139
+ };
140
+
141
+ interface ProductInsert {
142
+ id: string;
143
+ slug: string;
144
+ name: string;
145
+ description?: string;
146
+ shortDescription?: string;
147
+ status?: ProductStatus;
148
+ type?: ProductType;
149
+ seoTitle?: string;
150
+ seoDescription?: string;
151
+ media?: ProductMedia[];
152
+ metadata?: Record<string, unknown>;
153
+ createdAt: Date;
154
+ updatedAt: Date;
155
+ }
156
+ interface ProductUpdate {
157
+ name?: string;
158
+ description?: string;
159
+ shortDescription?: string;
160
+ status?: ProductStatus;
161
+ type?: ProductType;
162
+ seoTitle?: string;
163
+ seoDescription?: string;
164
+ media?: ProductMedia[];
165
+ metadata?: Record<string, unknown>;
166
+ updatedAt: Date;
167
+ }
168
+ interface ProductRepository extends StoreRepository {
169
+ findById(tx: ReadTx, id: string): Promise<Product | null>;
170
+ findBySlug(tx: ReadTx, slug: string): Promise<Product | null>;
171
+ list(tx: ReadTx, opts?: {
172
+ status?: ProductStatus;
173
+ limit?: number;
174
+ offset?: number;
175
+ }): Promise<Product[]>;
176
+ insert(tx: Tx, p: ProductInsert): Promise<void>;
177
+ update(tx: Tx, id: string, patch: ProductUpdate): Promise<void>;
178
+ delete(tx: Tx, id: string): Promise<void>;
179
+ setStatus(tx: Tx, id: string, status: ProductStatus, updatedAt: Date): Promise<void>;
180
+ }
181
+ declare function createProductRepository(store: CommandStore): ProductRepository;
182
+
183
+ interface VariantInsert {
184
+ id: string;
185
+ productId: string;
186
+ sku: string;
187
+ name: string;
188
+ priceMinor: number;
189
+ compareAtPriceMinor?: number | null;
190
+ weightGrams?: number | null;
191
+ dimensions?: VariantDimensions | null;
192
+ stock: number;
193
+ lowStockThreshold?: number;
194
+ options: VariantOption[];
195
+ status?: VariantStatus;
196
+ createdAt: Date;
197
+ updatedAt: Date;
198
+ }
199
+ interface VariantUpdate {
200
+ name?: string;
201
+ priceMinor?: number;
202
+ compareAtPriceMinor?: number | null;
203
+ weightGrams?: number | null;
204
+ dimensions?: VariantDimensions | null;
205
+ lowStockThreshold?: number;
206
+ options?: VariantOption[];
207
+ status?: VariantStatus;
208
+ updatedAt: Date;
209
+ }
210
+ interface VariantRepository extends StoreRepository {
211
+ findById(tx: ReadTx, id: string): Promise<Variant | null>;
212
+ findBySku(tx: ReadTx, sku: string): Promise<Variant | null>;
213
+ findByProduct(tx: ReadTx, productId: string): Promise<Variant[]>;
214
+ insert(tx: Tx, v: VariantInsert): Promise<void>;
215
+ update(tx: Tx, id: string, patch: VariantUpdate): Promise<void>;
216
+ /** One conditional UPDATE: the new stock, or null when the variant is missing or holds fewer than `qty`. */
217
+ decrementStock(tx: Tx, id: string, qty: number, at: Date): Promise<number | null>;
218
+ /** The new stock, or null when there is no such variant. */
219
+ incrementStock(tx: Tx, id: string, qty: number, at: Date): Promise<number | null>;
220
+ delete(tx: Tx, id: string): Promise<void>;
221
+ }
222
+ declare function createVariantRepository(store: CommandStore): VariantRepository;
223
+
224
+ interface CategoryInsert {
225
+ id: string;
226
+ parentId?: string | null;
227
+ slug: string;
228
+ name: string;
229
+ description?: string;
230
+ position?: number;
231
+ createdAt: Date;
232
+ }
233
+ interface CategoryUpdate {
234
+ parentId?: string | null;
235
+ name?: string;
236
+ description?: string;
237
+ position?: number;
238
+ }
239
+ interface CategoryRepository extends StoreRepository {
240
+ findById(tx: ReadTx, id: string): Promise<Category | null>;
241
+ findBySlug(tx: ReadTx, slug: string): Promise<Category | null>;
242
+ list(tx: ReadTx): Promise<Category[]>;
243
+ insert(tx: Tx, c: CategoryInsert): Promise<void>;
244
+ update(tx: Tx, id: string, patch: CategoryUpdate): Promise<void>;
245
+ delete(tx: Tx, id: string): Promise<void>;
246
+ }
247
+ declare function createCategoryRepository(store: CommandStore): CategoryRepository;
248
+
249
+ interface ProductCategoryRepository extends StoreRepository {
250
+ attach(tx: Tx, productId: string, categoryId: string): Promise<void>;
251
+ detach(tx: Tx, productId: string, categoryId: string): Promise<void>;
252
+ detachAllForProduct(tx: Tx, productId: string): Promise<void>;
253
+ listCategoriesForProduct(tx: ReadTx, productId: string): Promise<string[]>;
254
+ listProductsForCategory(tx: ReadTx, categoryId: string): Promise<string[]>;
255
+ }
256
+ declare function createProductCategoryRepository(store: CommandStore): ProductCategoryRepository;
257
+
258
+ interface CustomerInsert {
259
+ id: string;
260
+ email: string;
261
+ passwordHash: string;
262
+ firstName?: string;
263
+ lastName?: string;
264
+ phone?: string;
265
+ addresses?: CustomerAddress[];
266
+ metadata?: Record<string, unknown>;
267
+ /** Defaults to `['customer']`; only seed and admin code pass anything else. */
268
+ roles?: Role[];
269
+ requiresPasswordReset?: boolean;
270
+ createdAt: Date;
271
+ updatedAt: Date;
272
+ }
273
+ interface CustomerUpdate {
274
+ firstName?: string;
275
+ lastName?: string;
276
+ phone?: string;
277
+ addresses?: CustomerAddress[];
278
+ metadata?: Record<string, unknown>;
279
+ passwordHash?: string;
280
+ requiresPasswordReset?: boolean;
281
+ updatedAt: Date;
282
+ }
283
+ /** A stored customer: its roles column is always read. */
284
+ type CustomerWithHash = Customer & {
285
+ passwordHash: string;
286
+ roles: Role[];
287
+ };
288
+ interface CustomerRepository extends StoreRepository {
289
+ findById(tx: ReadTx, id: string): Promise<CustomerWithHash | null>;
290
+ findByEmail(tx: ReadTx, email: string): Promise<CustomerWithHash | null>;
291
+ list(tx: ReadTx, opts?: {
292
+ limit?: number;
293
+ offset?: number;
294
+ }): Promise<CustomerWithHash[]>;
295
+ insert(tx: Tx, c: CustomerInsert): Promise<void>;
296
+ update(tx: Tx, id: string, patch: CustomerUpdate): Promise<void>;
297
+ incrementStats(tx: Tx, id: string, deltaOrders: number, deltaSpentMinor: number, updatedAt: Date): Promise<void>;
298
+ delete(tx: Tx, id: string): Promise<void>;
299
+ }
300
+ declare function createCustomerRepository(store: CommandStore): CustomerRepository;
301
+
302
+ interface RefreshTokenInsert {
303
+ id: string;
304
+ customerId: string;
305
+ tokenHash: string;
306
+ expiresAt: Date;
307
+ createdAt: Date;
308
+ }
309
+ interface RefreshTokenRow {
310
+ id: string;
311
+ customerId: string;
312
+ tokenHash: string;
313
+ revokedAt: string | null;
314
+ replacedByJti: string | null;
315
+ expiresAt: string;
316
+ createdAt: string;
317
+ }
318
+ interface RefreshTokenRepository extends StoreRepository {
319
+ insert(tx: Tx, t: RefreshTokenInsert): Promise<void>;
320
+ findById(tx: ReadTx, id: string): Promise<RefreshTokenRow | null>;
321
+ rotate(tx: Tx, oldJti: string, next: RefreshTokenInsert): Promise<void>;
322
+ revoke(tx: Tx, id: string): Promise<void>;
323
+ revokeAllForCustomer(tx: Tx, customerId: string): Promise<void>;
324
+ purgeExpired(tx: Tx, now: Date): Promise<number>;
325
+ }
326
+ declare function createRefreshTokenRepository(store: CommandStore): RefreshTokenRepository;
327
+
328
+ interface OrderInsert {
329
+ id: string;
330
+ orderNumber: string;
331
+ sequence: number;
332
+ customerId: string | null;
333
+ status: OrderStatus;
334
+ email: string;
335
+ billingAddress: Address;
336
+ shippingAddress: Address;
337
+ lineItems: OrderLineItem[];
338
+ subtotalMinor: number;
339
+ taxTotalMinor: number;
340
+ shippingTotalMinor: number;
341
+ discountTotalMinor: number;
342
+ totalMinor: number;
343
+ currency: string;
344
+ notes?: string;
345
+ metadata?: Record<string, unknown>;
346
+ paymentIntentId?: string | null;
347
+ /** The cart a checkout order was placed from. */
348
+ cartToken?: string | null;
349
+ /** An imported order's money; a checkout order starts with nothing captured. */
350
+ capture?: OrderCapture & {
351
+ refundedMinor: number;
352
+ };
353
+ createdAt: Date;
354
+ updatedAt: Date;
355
+ }
356
+ interface OrderCapture {
357
+ amountMinor: number;
358
+ method: CaptureMethod;
359
+ at: Date;
360
+ }
361
+ interface OrderRepository extends StoreRepository {
362
+ findById(tx: ReadTx, id: string): Promise<Order | null>;
363
+ /** The order, read under its row lock until the transaction ends (Postgres `FOR UPDATE`; on SQLite the open
364
+ * `BEGIN IMMEDIATE` transaction already holds every write). */
365
+ lock(tx: Tx, id: string): Promise<Order | null>;
366
+ findByOrderNumber(tx: ReadTx, orderNumber: string): Promise<Order | null>;
367
+ findByPaymentIntentId(tx: ReadTx, paymentIntentId: string): Promise<Order | null>;
368
+ /** The cart's pending order: at most one, by the partial unique index on `cart_token`. */
369
+ findPendingByCartToken(tx: ReadTx, cartToken: string): Promise<Order | null>;
370
+ /** The newest order the cart became: neither pending nor cancelled. */
371
+ findLastConvertedByCartToken(tx: ReadTx, cartToken: string): Promise<Order | null>;
372
+ listByCustomer(tx: ReadTx, customerId: string, limit?: number): Promise<Order[]>;
373
+ list(tx: ReadTx, opts?: {
374
+ status?: OrderStatus;
375
+ limit?: number;
376
+ offset?: number;
377
+ }): Promise<Order[]>;
378
+ insert(tx: Tx, o: OrderInsert): Promise<void>;
379
+ setStatus(tx: Tx, id: string, status: OrderStatus, updatedAt: Date): Promise<void>;
380
+ /** Sets `status` only while the order is still `from`; false when it was not, so a concurrent writer got there
381
+ * first. */
382
+ setStatusIf(tx: Tx, id: string, from: OrderStatus, status: OrderStatus, updatedAt: Date): Promise<boolean>;
383
+ setPaymentIntent(tx: Tx, id: string, paymentIntentId: string, updatedAt: Date): Promise<void>;
384
+ recordCapture(tx: Tx, id: string, capture: OrderCapture): Promise<void>;
385
+ setRefunded(tx: Tx, id: string, refundedMinor: number, updatedAt: Date): Promise<void>;
386
+ setNotes(tx: Tx, id: string, notes: string, updatedAt: Date): Promise<void>;
387
+ }
388
+ declare function createOrderRepository(store: CommandStore): OrderRepository;
389
+
390
+ interface OrderHistoryInsert {
391
+ id: string;
392
+ orderId: string;
393
+ fromStatus: OrderStatus | null;
394
+ toStatus: OrderStatus;
395
+ actor: string;
396
+ note?: string;
397
+ timestamp: Date;
398
+ }
399
+ interface OrderHistoryRepository extends StoreRepository {
400
+ append(tx: Tx, entry: OrderHistoryInsert): Promise<void>;
401
+ listByOrder(tx: ReadTx, orderId: string): Promise<OrderHistoryEntry[]>;
402
+ }
403
+ declare function createOrderHistoryRepository(store: CommandStore): OrderHistoryRepository;
404
+
405
+ interface OrderRefundInsert {
406
+ id: string;
407
+ orderId: string;
408
+ amountMinor: number;
409
+ currency: string;
410
+ providerRefundId: string | null;
411
+ status: OrderRefundStatus;
412
+ reason: string | null;
413
+ actor: string;
414
+ createdAt: Date;
415
+ /** The client's key for the request that made the refund; unique per order. */
416
+ requestId?: string | null;
417
+ }
418
+ interface OrderRefundRepository extends StoreRepository {
419
+ insert(tx: Tx, refund: OrderRefundInsert): Promise<void>;
420
+ /** Settles a `pending` refund; false when the row is not pending (already settled). */
421
+ settle(tx: Tx, id: string, outcome: {
422
+ status: OrderRefundStatus;
423
+ providerRefundId: string | null;
424
+ }): Promise<boolean>;
425
+ /** The order's refund made by the request `requestId`, or null. */
426
+ findByRequestId(tx: ReadTx, orderId: string, requestId: string): Promise<OrderRefund | null>;
427
+ /** The order's refunds, oldest first. */
428
+ listByOrder(tx: ReadTx, orderId: string): Promise<OrderRefund[]>;
429
+ /** The refunds of each order, oldest first; an order without refunds is absent. */
430
+ listByOrders(tx: ReadTx, orderIds: string[]): Promise<Map<string, OrderRefund[]>>;
431
+ }
432
+ declare function createOrderRefundRepository(store: CommandStore): OrderRefundRepository;
433
+
434
+ interface OrderSequenceRepository extends StoreRepository {
435
+ nextSequence(tx: Tx, startFrom?: number): Promise<number>;
436
+ }
437
+ declare function createOrderSequenceRepository(store: CommandStore): OrderSequenceRepository;
438
+
439
+ interface ReservationInsert {
440
+ reservationId: string;
441
+ variantId: string;
442
+ qty: number;
443
+ orderRef?: string | null;
444
+ expiresAt: Date;
445
+ createdAt: Date;
446
+ }
447
+ interface InventoryReservationRepository extends StoreRepository {
448
+ insert(tx: Tx, r: ReservationInsert): Promise<void>;
449
+ findById(tx: ReadTx, reservationId: string): Promise<InventoryReservation | null>;
450
+ findByVariant(tx: ReadTx, variantId: string): Promise<InventoryReservation[]>;
451
+ /** Deletes the reservation and returns the row it removed; null when none was there. Of two concurrent callers
452
+ * only one gets the row, which is what makes a release and a commit of one reservation exclusive. */
453
+ deleteById(tx: Tx, reservationId: string): Promise<InventoryReservation | null>;
454
+ /** Reservations whose expiry is before `at`; deletes nothing. */
455
+ findExpiredBefore(tx: ReadTx, at: Date): Promise<InventoryReservation[]>;
456
+ setOrderRef(tx: Tx, reservationId: string, orderRef: string): Promise<void>;
457
+ }
458
+ declare function createInventoryReservationRepository(store: CommandStore): InventoryReservationRepository;
459
+
460
+ interface StockAdjustmentInsert {
461
+ id: string;
462
+ variantId: string;
463
+ adjustment: number;
464
+ reason: StockAdjustmentReason;
465
+ actor: string;
466
+ previousQty: number;
467
+ newQty: number;
468
+ timestamp: Date;
469
+ }
470
+ interface StockAdjustmentRepository extends StoreRepository {
471
+ append(tx: Tx, a: StockAdjustmentInsert): Promise<void>;
472
+ listByVariant(tx: ReadTx, variantId: string, limit?: number): Promise<StockAdjustment[]>;
473
+ }
474
+ declare function createStockAdjustmentRepository(store: CommandStore): StockAdjustmentRepository;
475
+
476
+ interface ReviewInsert {
477
+ id: string;
478
+ productId: string;
479
+ customerId: string | null;
480
+ orderId: string | null;
481
+ rating: number;
482
+ title: string;
483
+ body: string;
484
+ moderationStatus?: ReviewModerationStatus;
485
+ submittedAt: Date;
486
+ }
487
+ interface ReviewRepository extends StoreRepository {
488
+ findById(tx: ReadTx, id: string): Promise<Review | null>;
489
+ listByProduct(tx: ReadTx, productId: string, status?: ReviewModerationStatus, limit?: number): Promise<Review[]>;
490
+ listByStatus(tx: ReadTx, status: ReviewModerationStatus, limit?: number): Promise<Review[]>;
491
+ insert(tx: Tx, r: ReviewInsert): Promise<void>;
492
+ setModerationStatus(tx: Tx, id: string, status: ReviewModerationStatus, moderatedBy: string, moderatedAt: Date): Promise<void>;
493
+ aggregateByProduct(tx: ReadTx, productId: string): Promise<{
494
+ average: number;
495
+ count: number;
496
+ histogram: Record<'1' | '2' | '3' | '4' | '5', number>;
497
+ }>;
498
+ }
499
+ declare function createReviewRepository(store: CommandStore): ReviewRepository;
500
+
501
+ /** Which handler has already run for which event id: what stops a side effect running twice, across restarts. */
502
+ interface ProcessedEventRepository extends StoreRepository {
503
+ seen(tx: ReadTx, handlerName: string, eventId: string): Promise<boolean>;
504
+ /** Idempotent on (handlerName, eventId): a second call keeps the first record. */
505
+ markProcessed(tx: Tx, handlerName: string, eventId: string, at: Date): Promise<void>;
506
+ /** Deletes records processed before `at`; returns how many. */
507
+ deleteBefore(tx: Tx, at: Date): Promise<number>;
508
+ }
509
+ declare function createProcessedEventRepository(store: CommandStore): ProcessedEventRepository;
510
+
511
+ interface LoggedEvent {
512
+ seq: number;
513
+ event: NymbalEvent<unknown>;
514
+ deliveredAt: Date | null;
515
+ attempts: number;
516
+ deadAt: Date | null;
517
+ }
518
+ interface EventLogRepository extends StoreRepository {
519
+ /** Appends in array order, so `seq` follows publish order. */
520
+ append(tx: Tx, events: NymbalEvent<unknown>[]): Promise<void>;
521
+ /** Neither delivered nor dead, by `seq`; `afterSeq` pages through one pass without re-reading rows it just tried. */
522
+ listUndelivered(tx: ReadTx, limit: number, afterSeq?: number): Promise<LoggedEvent[]>;
523
+ markDelivered(tx: Tx, ids: string[], at: Date): Promise<void>;
524
+ /** One more failed attempt; the event is dead (`dead_at = at`) once its attempts reach `maxAttempts`. */
525
+ recordFailure(tx: Tx, id: string, at: Date, maxAttempts: number): Promise<void>;
526
+ /** Deletes delivered events delivered before `at`; returns how many. Undelivered and dead events are never deleted. */
527
+ deleteDeliveredBefore(tx: Tx, at: Date): Promise<number>;
528
+ listAll(tx: ReadTx): Promise<LoggedEvent[]>;
529
+ }
530
+ declare function createEventLogRepository(store: CommandStore): EventLogRepository;
531
+
532
+ /** What makes a cart due an abandonment notice at `at`: live, holding items, unwritten since `cutoff`, not yet
533
+ * notified in this inactivity period. */
534
+ interface AbandonedWindow {
535
+ cutoff: Date;
536
+ at: Date;
537
+ }
538
+ interface AbandonedCart {
539
+ token: string;
540
+ /** The cart's last write. */
541
+ lastActivityAt: string;
542
+ }
543
+ interface CartRepository extends StoreRepository {
544
+ findByToken(tx: ReadTx, token: string): Promise<Cart | null>;
545
+ /** `findByToken`, holding the row for the rest of the transaction so concurrent commands on the cart queue behind
546
+ * this one (Postgres `FOR UPDATE`; SQLite's `BEGIN IMMEDIATE` already holds the database). */
547
+ lock(tx: Tx, token: string): Promise<Cart | null>;
548
+ /** Inserts the cart, or replaces an expired row on its token. A live row on the token is left as it is and
549
+ * returned with `created: false`. */
550
+ create(tx: Tx, cart: Cart): Promise<{
551
+ cart: Cart;
552
+ created: boolean;
553
+ }>;
554
+ /** Writes the cart whole and starts a new inactivity period (clears the abandonment claim). */
555
+ put(tx: Tx, cart: Cart): Promise<void>;
556
+ remove(tx: Tx, token: string): Promise<void>;
557
+ /** Empties the cart if it is still live at `at`, in one conditional write; false when there was no live row to
558
+ * empty (expired, or deleted by a sweep or a merge). Starts a new inactivity period; the expiry stays as it was. */
559
+ clearIfLive(tx: Tx, token: string, at: Date): Promise<boolean>;
560
+ /** Deletes every cart whose expiry is at or before `at`; returns how many. */
561
+ deleteExpired(tx: Tx, at: Date): Promise<number>;
562
+ /** Up to `limit` tokens due a notice, in token order after `after`; claims nothing. */
563
+ listAbandoned(tx: ReadTx, window: AbandonedWindow, page: {
564
+ after: string | undefined;
565
+ limit: number;
566
+ }): Promise<string[]>;
567
+ /** Marks the cart notified if it is still due a notice; null when it is not (another sweeper claimed it, or a
568
+ * write, expiry or delete came first). */
569
+ claimAbandoned(tx: Tx, token: string, window: AbandonedWindow): Promise<AbandonedCart | null>;
570
+ }
571
+ declare function createCartRepository(store: CommandStore): CartRepository;
572
+
573
+ interface Repositories {
574
+ product: ProductRepository;
575
+ variant: VariantRepository;
576
+ category: CategoryRepository;
577
+ productCategory: ProductCategoryRepository;
578
+ customer: CustomerRepository;
579
+ refreshToken: RefreshTokenRepository;
580
+ order: OrderRepository;
581
+ orderHistory: OrderHistoryRepository;
582
+ orderRefund: OrderRefundRepository;
583
+ orderSequence: OrderSequenceRepository;
584
+ inventoryReservation: InventoryReservationRepository;
585
+ stockAdjustment: StockAdjustmentRepository;
586
+ review: ReviewRepository;
587
+ processedEvent: ProcessedEventRepository;
588
+ eventLog: EventLogRepository;
589
+ cart: CartRepository;
590
+ }
591
+ declare function buildRepositories(commandStore: CommandStore, _documentStore: DocumentStoreAdapter): Repositories;
592
+
593
+ interface InMemoryDocumentStoreOptions {
594
+ sweepIntervalMs?: number;
595
+ now?: () => number;
596
+ }
597
+ declare class InMemoryDocumentStore implements DocumentStoreAdapter {
598
+ #private;
599
+ constructor(options?: InMemoryDocumentStoreOptions);
600
+ capabilities(): readonly string[];
601
+ get<T>(collection: string, key: string): Promise<T | null>;
602
+ put<T>(collection: string, key: string, document: T, options?: PutOptions): Promise<void>;
603
+ delete(collection: string, key: string): Promise<void>;
604
+ query<T>(collection: string, params: QueryParams): Promise<QueryResult<T>>;
605
+ batchGet<T>(collection: string, keys: string[]): Promise<Map<string, T>>;
606
+ batchPut<T>(collection: string, items: Map<string, T>): Promise<void>;
607
+ close(): Promise<void>;
608
+ }
609
+
610
+ declare function createDocumentStore(config: NymbalConfig): DocumentStoreAdapter;
611
+
612
+ interface InProcessEventBusOptions {
613
+ logger?: Logger;
614
+ }
615
+ declare class InProcessEventBus implements EventBusAdapter {
616
+ #private;
617
+ constructor(options?: InProcessEventBusOptions);
618
+ capabilities(): readonly string[];
619
+ publish<T>(event: NymbalEvent<T>): Promise<void>;
620
+ publishBatch(events: NymbalEvent<unknown>[]): Promise<void>;
621
+ replay(events: NymbalEvent<unknown>[]): Promise<void>;
622
+ subscribe(eventPattern: string | string[], handler: EventHandler, config?: SubscriptionConfig): Promise<Subscription>;
623
+ unsubscribe(subscription: Subscription): Promise<void>;
624
+ close(): Promise<void>;
625
+ }
626
+
627
+ declare function createEventBus(config: NymbalConfig, logger?: Logger): EventBusAdapter;
628
+
629
+ interface EventBuilderContext {
630
+ storeId: string;
631
+ environment: NymbalEnvironment;
632
+ version: string;
633
+ source?: string;
634
+ }
635
+ interface MakeEventOptions {
636
+ source?: string;
637
+ correlationId?: string;
638
+ timestamp?: Date;
639
+ }
640
+ declare function createEventBuilder(ctx: EventBuilderContext): <T>(type: string, payload: T, options?: MakeEventOptions) => NymbalEvent<T>;
641
+
642
+ /** Hands committed events from the log to the bus. */
643
+ interface Relay {
644
+ /** Delivers every undelivered, non-dead event in `seq` order. Never rejects. */
645
+ drain(): Promise<void>;
646
+ /** Drains on an interval (the timer does not keep the process alive). */
647
+ start(): void;
648
+ /** Stops the interval and starts no new pass; resolves when the pass in flight, if any, has finished, so the
649
+ * store can be closed without cutting a delivery off between the handler and its bookkeeping. */
650
+ stop(): Promise<void>;
651
+ }
652
+ interface CreateRelayOptions {
653
+ store: CommandStore;
654
+ eventLog: EventLogRepository;
655
+ eventBus: EventBusAdapter;
656
+ logger: Logger;
657
+ /** Failed bus publishes after which an event is dead and never handed to the bus again. */
658
+ maxAttempts?: number;
659
+ intervalMs?: number;
660
+ }
661
+ declare function createRelay(opts: CreateRelayOptions): Relay;
662
+
663
+ interface PublishOptions extends MakeEventOptions {
664
+ }
665
+ interface EventPublisher {
666
+ publish<E extends NymbalEventType>(type: E, payload: NymbalEventMap[E], options?: PublishOptions): Promise<NymbalEvent<NymbalEventMap[E]>>;
667
+ publishMany(events: Array<{
668
+ [E in NymbalEventType]: {
669
+ type: E;
670
+ payload: NymbalEventMap[E];
671
+ options?: PublishOptions;
672
+ };
673
+ }[NymbalEventType]>): Promise<void>;
674
+ correlated(correlationId: string): EventPublisher;
675
+ }
676
+ interface CreatePublisherOptions {
677
+ eventBus: EventBusAdapter;
678
+ builderContext: EventBuilderContext;
679
+ /** Events are appended to `eventLog` on this store and reach the bus only through `relay`. */
680
+ store: CommandStore;
681
+ eventLog: EventLogRepository;
682
+ relay: Relay;
683
+ correlationId?: string;
684
+ }
685
+ declare function createEventPublisher(opts: CreatePublisherOptions): EventPublisher;
686
+
687
+ interface IdempotencyOptions {
688
+ repo: ProcessedEventRepository;
689
+ logger?: Logger;
690
+ }
691
+ declare function withIdempotency<T = unknown>(handlerName: string, handler: EventHandler<T>, { repo, logger }: IdempotencyOptions): EventHandler<T>;
692
+
693
+ /** One adapter per commerce slot. */
694
+ interface CommerceAdapters {
695
+ payments: PaymentsAdapter;
696
+ email: EmailAdapter;
697
+ reviews: ReviewsAdapter;
698
+ search: SearchAdapter;
699
+ analytics: AnalyticsAdapter;
700
+ shipping: ShippingAdapter;
701
+ tax: TaxAdapter;
702
+ ai: AiAdapter;
703
+ }
704
+ /** Adapters a project hands `createApp`, each for a slot whose provider is `custom`. */
705
+ type InjectedAdapters = Partial<CommerceAdapters>;
706
+ interface AdapterRegistry extends Omit<CommerceAdapters, 'ai'> {
707
+ security: SecurityAdapter;
708
+ /** `null` when the config's ai provider is `none`. */
709
+ ai: AiAdapter | null;
710
+ }
711
+ interface BuildAdaptersDeps {
712
+ config: NymbalConfig;
713
+ logger: Logger;
714
+ documentStore: DocumentStoreAdapter;
715
+ repos: Repositories;
716
+ injected?: InjectedAdapters;
717
+ /** The payments adapter `createApp` resolved before it opened the store. */
718
+ payments: PaymentsAdapter;
719
+ }
720
+ declare function buildAdapters(deps: BuildAdaptersDeps): Promise<AdapterRegistry>;
721
+
722
+ interface RegisterHandlersDeps {
723
+ config: NymbalConfig;
724
+ eventBus: EventBusAdapter;
725
+ documentStore: DocumentStoreAdapter;
726
+ repos: Repositories;
727
+ adapters: AdapterRegistry;
728
+ logger: Logger;
729
+ }
730
+ declare function registerHandlers(deps: RegisterHandlersDeps): Promise<void>;
731
+
732
+ interface ProductCreateInput {
733
+ slug: string;
734
+ name: string;
735
+ description?: string;
736
+ shortDescription?: string;
737
+ status?: ProductStatus;
738
+ type?: 'simple' | 'variable';
739
+ seoTitle?: string;
740
+ seoDescription?: string;
741
+ media?: Product['media'];
742
+ metadata?: Record<string, unknown>;
743
+ categoryIds?: string[];
744
+ variants?: Array<Omit<VariantInsert, 'id' | 'productId' | 'createdAt' | 'updatedAt'> & {
745
+ id?: string;
746
+ }>;
747
+ }
748
+ interface ProductUpdateInput extends Omit<ProductUpdate, 'updatedAt'> {
749
+ categoryIds?: string[];
750
+ }
751
+ interface ProductService {
752
+ create(input: ProductCreateInput): Promise<ProductSnapshot>;
753
+ update(id: string, input: ProductUpdateInput): Promise<ProductSnapshot>;
754
+ delete(id: string): Promise<void>;
755
+ publish(id: string): Promise<void>;
756
+ unpublish(id: string): Promise<void>;
757
+ getById(id: string): Promise<ProductSnapshot>;
758
+ getBySlug(slug: string): Promise<ProductSnapshot>;
759
+ list(opts?: {
760
+ status?: ProductStatus;
761
+ limit?: number;
762
+ offset?: number;
763
+ }): Promise<ProductSnapshot[]>;
764
+ }
765
+ interface CreateProductServiceDeps {
766
+ store: CommandStore;
767
+ repos: Repositories;
768
+ publisher: EventPublisher;
769
+ logger: Logger;
770
+ }
771
+ declare function createProductService(deps: CreateProductServiceDeps): ProductService;
772
+
773
+ interface CategoryCreateInput {
774
+ slug: string;
775
+ name: string;
776
+ description?: string;
777
+ parentId?: string | null;
778
+ position?: number;
779
+ }
780
+ interface CategoryUpdateInput {
781
+ name?: string;
782
+ description?: string;
783
+ parentId?: string | null;
784
+ position?: number;
785
+ }
786
+ interface CategoryService {
787
+ list(): Promise<Category[]>;
788
+ getById(id: string): Promise<Category>;
789
+ getBySlug(slug: string): Promise<Category>;
790
+ create(input: CategoryCreateInput): Promise<Category>;
791
+ update(id: string, input: CategoryUpdateInput): Promise<Category>;
792
+ delete(id: string): Promise<void>;
793
+ }
794
+ declare function createCategoryService(deps: {
795
+ repos: Repositories;
796
+ publisher: EventPublisher;
797
+ logger: Logger;
798
+ }): CategoryService;
799
+
800
+ interface CartService {
801
+ getOrCreate(token?: string, customerId?: string | null): Promise<Cart>;
802
+ get(token: string): Promise<Cart | null>;
803
+ addItem(token: string, item: Omit<CartItem, 'qty'> & {
804
+ qty?: number;
805
+ }): Promise<Cart>;
806
+ updateItemQuantity(token: string, variantId: string, qty: number): Promise<Cart>;
807
+ removeItem(token: string, variantId: string): Promise<Cart>;
808
+ clear(token: string): Promise<Cart>;
809
+ merge(guestToken: string, customerToken: string): Promise<Cart>;
810
+ attachCustomer(token: string, customerId: string): Promise<Cart>;
811
+ }
812
+ interface CreateCartServiceDeps {
813
+ store: CommandStore;
814
+ carts: CartRepository;
815
+ publisher: EventPublisher;
816
+ logger: Logger;
817
+ currency: string;
818
+ ttlSeconds?: number;
819
+ now?: () => Date;
820
+ }
821
+ declare function createCartService(deps: CreateCartServiceDeps): CartService;
822
+
823
+ interface ProfileUpdate {
824
+ firstName?: string;
825
+ lastName?: string;
826
+ phone?: string;
827
+ }
828
+ interface CustomerService {
829
+ getProfile(customerId: string): Promise<Customer>;
830
+ updateProfile(customerId: string, patch: ProfileUpdate): Promise<Customer>;
831
+ listAddresses(customerId: string): Promise<CustomerAddress[]>;
832
+ addAddress(customerId: string, addr: Omit<CustomerAddress, 'id'>): Promise<CustomerAddress>;
833
+ deleteAddress(customerId: string, addressId: string): Promise<void>;
834
+ }
835
+ declare function createCustomerService(deps: {
836
+ repos: Repositories;
837
+ publisher: EventPublisher;
838
+ logger: Logger;
839
+ }): CustomerService;
840
+
841
+ interface AuthTokens {
842
+ accessToken: string;
843
+ refreshToken: string;
844
+ expiresIn: number;
845
+ }
846
+ interface RegisterInput {
847
+ email: string;
848
+ password: string;
849
+ firstName?: string;
850
+ lastName?: string;
851
+ phone?: string;
852
+ }
853
+ interface CustomerJwtPayload extends JWTPayload {
854
+ sub: string;
855
+ email: string;
856
+ roles: Role[];
857
+ type: 'access' | 'refresh';
858
+ jti?: string;
859
+ }
860
+ interface ImportCustomerInput {
861
+ email: string;
862
+ firstName?: string;
863
+ lastName?: string;
864
+ phone?: string;
865
+ addresses?: CustomerAddress[];
866
+ metadata?: Record<string, unknown>;
867
+ }
868
+ interface AuthService {
869
+ register(input: RegisterInput): Promise<{
870
+ customer: Customer;
871
+ tokens: AuthTokens;
872
+ }>;
873
+ login(email: string, password: string): Promise<{
874
+ customer: Customer;
875
+ tokens: AuthTokens;
876
+ }>;
877
+ refresh(refreshToken: string): Promise<AuthTokens>;
878
+ logout(refreshToken: string): Promise<void>;
879
+ verifyAccessToken(token: string): Promise<CustomerJwtPayload>;
880
+ importCustomer(input: ImportCustomerInput): Promise<Customer>;
881
+ }
882
+ interface CreateAuthServiceDeps {
883
+ repos: Repositories;
884
+ publisher: EventPublisher;
885
+ logger: Logger;
886
+ jwtSecret: string;
887
+ accessTtl: string;
888
+ refreshTtl: string;
889
+ issuer?: string;
890
+ }
891
+ declare function createAuthService(deps: CreateAuthServiceDeps): AuthService;
892
+
893
+ interface OrderCreateDraftInput {
894
+ orderId: string;
895
+ customerId: string | null;
896
+ email: string;
897
+ billingAddress: Address;
898
+ shippingAddress: Address;
899
+ lineItems: OrderLineItem[];
900
+ subtotalMinor: number;
901
+ taxTotalMinor: number;
902
+ shippingTotalMinor: number;
903
+ discountTotalMinor: number;
904
+ totalMinor: number;
905
+ currency: string;
906
+ notes?: string;
907
+ metadata?: Record<string, unknown>;
908
+ paymentIntentId?: string | null;
909
+ cartToken?: string | null;
910
+ }
911
+ interface OrderImportInput extends OrderCreateDraftInput {
912
+ orderNumber: string;
913
+ status: OrderStatus;
914
+ createdAt: Date;
915
+ /** What the imported order was paid, taken as captured when it was placed; nothing when absent. */
916
+ capturedMinor?: number;
917
+ refundedMinor?: number;
918
+ captureMethod?: CaptureMethod | null;
919
+ }
920
+ interface OrderService {
921
+ createFromCheckout(input: OrderCreateDraftInput): Promise<Order>;
922
+ importOrder(input: OrderImportInput): Promise<Order>;
923
+ getById(id: string): Promise<Order>;
924
+ getByOrderNumber(orderNumber: string): Promise<Order>;
925
+ listByCustomer(customerId: string): Promise<Order[]>;
926
+ updateStatus(id: string, toStatus: OrderStatus, actor: string, note?: string): Promise<Order>;
927
+ markShipped(id: string, trackingNumber: string, carrier: string, actor: string): Promise<Order>;
928
+ markDelivered(id: string, actor: string): Promise<Order>;
929
+ /** Refuses with `order.paid` once anything is captured: a paid order is refunded, never cancelled. */
930
+ cancel(id: string, reason: string, actor: string): Promise<Order>;
931
+ }
932
+ interface CreateOrderServiceDeps {
933
+ store: CommandStore;
934
+ repos: Repositories;
935
+ publisher: EventPublisher;
936
+ logger: Logger;
937
+ orderNumberPrefix: string;
938
+ orderNumberStart: number;
939
+ }
940
+ declare function createOrderService(deps: CreateOrderServiceDeps): OrderService;
941
+
942
+ interface ReserveStockResult {
943
+ reservationId: string;
944
+ expiresAt: string;
945
+ }
946
+ interface InventoryService {
947
+ adjustStock(variantId: string, delta: number, reason: StockAdjustmentReason, actor: string): Promise<{
948
+ newQty: number;
949
+ }>;
950
+ reserveStock(variantId: string, qty: number, orderRef?: string | null, ttlSeconds?: number): Promise<ReserveStockResult>;
951
+ /** `reserveStock`, answering null instead of throwing when the variant has less than `qty` in stock. */
952
+ tryReserveStock(variantId: string, qty: number, orderRef?: string | null, ttlSeconds?: number): Promise<ReserveStockResult | null>;
953
+ releaseReservation(reservationId: string, reason: 'expired' | 'cancelled' | 'abandoned'): Promise<void>;
954
+ commitReservation(reservationId: string, actor: string): Promise<void>;
955
+ getStockStatus(variantId: string): Promise<{
956
+ stock: number;
957
+ threshold: number;
958
+ status: StockStatus;
959
+ }>;
960
+ }
961
+ interface CreateInventoryServiceDeps {
962
+ store: CommandStore;
963
+ repos: Repositories;
964
+ publisher: EventPublisher;
965
+ logger: Logger;
966
+ defaultReservationTtlSeconds?: number;
967
+ }
968
+ declare function createInventoryService(deps: CreateInventoryServiceDeps): InventoryService;
969
+
970
+ interface BeginCheckoutInput {
971
+ cartToken: string;
972
+ customerId?: string | null;
973
+ email: string;
974
+ billingAddress: Address;
975
+ shippingAddress: Address;
976
+ notes?: string;
977
+ }
978
+ interface BeginCheckoutResult {
979
+ orderId: string;
980
+ orderNumber: string;
981
+ paymentIntent: PaymentIntent;
982
+ currency: string;
983
+ totalMinor: number;
984
+ /** The order's status as the checkout answers. */
985
+ orderStatus: OrderStatus;
986
+ payment: CheckoutPayment;
987
+ }
988
+ /** What the payment provider captured. */
989
+ interface CapturedPayment {
990
+ amountMinor: number;
991
+ /** ISO 4217, in any case. */
992
+ currency: string;
993
+ }
994
+ interface FinalizeResult {
995
+ /** Null when no order has the payment intent. */
996
+ orderNumber: string | null;
997
+ /** True when the order is confirmed: by this call, or already for this intent. */
998
+ finalized: boolean;
999
+ /** Why the captured payment did not confirm the order; recorded as payment.failed. */
1000
+ reason?: string;
1001
+ }
1002
+ interface CheckoutService {
1003
+ beginCheckout(input: BeginCheckoutInput): Promise<BeginCheckoutResult>;
1004
+ /**
1005
+ * Confirms the order a captured payment is for, or records why not (a payment.failed with the reason and, when
1006
+ * there is an order, a history note) and leaves the order as it was. Joins the caller's transaction.
1007
+ */
1008
+ finalize(paymentIntentId: string, paid: CapturedPayment): Promise<FinalizeResult>;
1009
+ cancel(orderId: string, reason: string): Promise<void>;
1010
+ }
1011
+ /** The admin's side of a checkout: a payment taken outside the store. */
1012
+ interface ManualPayments {
1013
+ /**
1014
+ * Records a payment the merchant took outside the store (a manual capture of the order's total) and confirms the
1015
+ * pending order exactly as a provider capture does: its reservations committed (re-taken if released), stats,
1016
+ * payment.captured and order.paid, the cart cleared. `order.stock_unavailable` when released stock is gone.
1017
+ */
1018
+ markPaid(orderId: string, actor: string, note?: string): Promise<Order>;
1019
+ }
1020
+ interface CreateCheckoutServiceDeps {
1021
+ store: CommandStore;
1022
+ repos: Repositories;
1023
+ publisher: EventPublisher;
1024
+ logger: Logger;
1025
+ cart: CartService;
1026
+ orders: OrderService;
1027
+ inventory: InventoryService;
1028
+ payments: PaymentsAdapter;
1029
+ currency: string;
1030
+ }
1031
+ declare function createCheckoutService(deps: CreateCheckoutServiceDeps): CheckoutService & ManualPayments;
1032
+
1033
+ interface SearchService {
1034
+ products(query: string, params?: SearchParams): Promise<SearchResult>;
1035
+ }
1036
+ declare function createSearchService(deps: {
1037
+ adapter: SearchAdapter;
1038
+ logger: Logger;
1039
+ }): SearchService;
1040
+
1041
+ /** What the shop tells the provider; the merchant's own words stay on the refund row. */
1042
+ declare const PROVIDER_REFUND_REASON = "requested_by_customer";
1043
+ interface RefundInput {
1044
+ orderId: string;
1045
+ amountMinor: number;
1046
+ reason?: string;
1047
+ actor: string;
1048
+ /** The client's key for this request: a repeat on the order answers the refund the first one made. */
1049
+ requestId?: string;
1050
+ }
1051
+ interface RefundResult {
1052
+ order: Order;
1053
+ payment: OrderPayment;
1054
+ refund: OrderRefund;
1055
+ }
1056
+ interface RefundService {
1057
+ /**
1058
+ * Refunds part or all of what the order captured, through the payments provider when the provider took the money.
1059
+ * The refund is held as a `pending` row under the order's lock, the provider is called outside any transaction with
1060
+ * the row id as its idempotency key, and the row is settled under the lock again by the provider's answer. A
1061
+ * `requestId` the order has already seen answers that refund as it stands and moves no money;
1062
+ * `refund.request_reused` when the amount differs.
1063
+ */
1064
+ refund(input: RefundInput): Promise<RefundResult>;
1065
+ }
1066
+ interface CreateRefundServiceDeps {
1067
+ store: CommandStore;
1068
+ repos: Repositories;
1069
+ publisher: EventPublisher;
1070
+ logger: Logger;
1071
+ payments: PaymentsAdapter;
1072
+ }
1073
+ declare function orderPayment(order: Order, refunds: OrderRefund[]): OrderPayment;
1074
+ interface Settlement {
1075
+ status: OrderRefundStatus;
1076
+ providerRefundId: string | null;
1077
+ /** The provider answered for another amount or currency than the row's. */
1078
+ mismatch: boolean;
1079
+ }
1080
+ /** The provider's answer settles a refund row as succeeded only when it is for the row's amount and currency. */
1081
+ declare function settlementOf(answer: Refund, row: Pick<OrderRefund, 'amountMinor' | 'currency'>): Settlement;
1082
+ /** Writes `refunded_minor` as Σ of the order's succeeded refunds. */
1083
+ declare function refreshRefunded(repos: Repositories, tx: Tx, orderId: string, at: Date): Promise<{
1084
+ refunds: OrderRefund[];
1085
+ refundedMinor: number;
1086
+ }>;
1087
+ declare function createRefundService(deps: CreateRefundServiceDeps): RefundService;
1088
+
1089
+ interface ServiceRegistry {
1090
+ product: ProductService;
1091
+ category: CategoryService;
1092
+ cart: CartService;
1093
+ customer: CustomerService;
1094
+ auth: AuthService;
1095
+ order: OrderService;
1096
+ inventory: InventoryService;
1097
+ checkout: CheckoutService & ManualPayments;
1098
+ refund: RefundService;
1099
+ search: SearchService;
1100
+ }
1101
+ interface BuildServicesDeps {
1102
+ config: NymbalConfig;
1103
+ store: CommandStore;
1104
+ repos: Repositories;
1105
+ publisher: EventPublisher;
1106
+ logger: Logger;
1107
+ payments: PaymentsAdapter;
1108
+ searchAdapter: SearchAdapter;
1109
+ }
1110
+ declare function buildServices(deps: BuildServicesDeps): ServiceRegistry;
1111
+
1112
+ interface RegisterAllRoutesDeps {
1113
+ config: NymbalConfig;
1114
+ adapter: HttpAdapter;
1115
+ services: ServiceRegistry;
1116
+ adapters: AdapterRegistry;
1117
+ repos: Repositories;
1118
+ documentStore: DocumentStoreAdapter;
1119
+ eventBus: EventBusAdapter;
1120
+ publisher: EventPublisher;
1121
+ /** The admin's dist directory, or null for a 404 that says how to build it; undefined serves no admin. */
1122
+ adminRoot?: string | null | undefined;
1123
+ }
1124
+ declare function registerAllRoutes(deps: RegisterAllRoutesDeps): void;
1125
+
1126
+ /**
1127
+ * The input of one route: what `registerAllRoutes` parses before the handler runs, and what a tool definition is
1128
+ * generated from. Unknown keys are stripped, so a handler sees only what is declared here.
1129
+ */
1130
+ interface RouteSchema {
1131
+ /** Unique, dotted: `cart.addItem`. */
1132
+ name: string;
1133
+ description: string;
1134
+ params?: ZodTypeAny;
1135
+ query?: ZodTypeAny;
1136
+ body?: ZodTypeAny;
1137
+ }
1138
+ declare const routeSchemas: Record<string, RouteSchema>;
1139
+
1140
+ interface NymbalApp {
1141
+ platform: Platform;
1142
+ repos: Repositories;
1143
+ adapters: AdapterRegistry;
1144
+ services: ServiceRegistry;
1145
+ publisher: EventPublisher;
1146
+ /** Delivers the event log to the bus; started by createApp, stopped by stop(). */
1147
+ relay: Relay;
1148
+ /**
1149
+ * Registers the API on the adapter. The admin is served at /admin/ when `adminRoot` is passed (its dist directory,
1150
+ * or null for a 404 that says how to build it) or `NYMBAL_ADMIN_DIST` is set.
1151
+ */
1152
+ attachHttp(adapter: HttpAdapter, options?: AttachHttpOptions): Promise<void>;
1153
+ /** Each slot's provider and its adapter's own `capabilities()`; an ai slot of `none` offers nothing. */
1154
+ capabilities(): CapabilitiesReport;
1155
+ stop(): Promise<void>;
1156
+ }
1157
+ type CapabilitySlot = CommerceSlot | 'security' | 'documentStore' | 'eventBus' | 'media';
1158
+ interface SlotCapabilities {
1159
+ provider: string;
1160
+ capabilities: readonly string[];
1161
+ }
1162
+ type CapabilitiesReport = Record<CapabilitySlot, SlotCapabilities>;
1163
+ interface AttachHttpOptions {
1164
+ adminRoot?: string | null;
1165
+ }
1166
+ interface CreateAppOptions extends CreatePlatformOptions {
1167
+ version?: string;
1168
+ /** The project's own adapters, one per slot whose provider is `custom`. */
1169
+ adapters?: InjectedAdapters;
1170
+ }
1171
+ declare function createApp(config: NymbalConfig, options?: CreateAppOptions): Promise<NymbalApp>;
1172
+
1173
+ /**
1174
+ * Serves the admin's built files at /admin/ beside /api, one origin, so its relative API URLs and the refresh cookie
1175
+ * work without a proxy. `root` is the admin's dist directory, or null when none is installed: then /admin/ answers 404
1176
+ * with how to build it.
1177
+ */
1178
+ declare function registerAdminStaticRoute(adapter: HttpAdapter, root: string | null): void;
1179
+
1180
+ interface SweeperHandle {
1181
+ /** One sweep, awaited; the interval calls it. */
1182
+ tick(): Promise<void>;
1183
+ stop(): void;
1184
+ }
1185
+ interface InventoryReservationSweeperDeps {
1186
+ inventoryService: InventoryService;
1187
+ reservationRepo: InventoryReservationRepository;
1188
+ logger: Logger;
1189
+ intervalMs?: number;
1190
+ }
1191
+ declare function startInventoryReservationSweeper(deps: InventoryReservationSweeperDeps): SweeperHandle;
1192
+
1193
+ interface CartAbandonmentSweeperDeps {
1194
+ store: CommandStore;
1195
+ carts: CartRepository;
1196
+ publisher: EventPublisher;
1197
+ logger: Logger;
1198
+ intervalMs?: number;
1199
+ inactivityMs?: number;
1200
+ /** Carts claimed per transaction; a tick runs as many as it takes to reach every due cart. */
1201
+ batchSize?: number;
1202
+ now?: () => Date;
1203
+ }
1204
+ /**
1205
+ * Deletes expired carts, then notifies each cart idle past `inactivityMs` once per inactivity period. The notice is
1206
+ * a durable claim on the cart row, committed with its `cart.abandoned` event, so a restart or another replica's
1207
+ * sweeper does not notify the same period again; a cart write clears the claim.
1208
+ */
1209
+ declare function startCartAbandonmentSweeper(deps: CartAbandonmentSweeperDeps): SweeperHandle;
1210
+
1211
+ interface EventRetentionSweeperDeps {
1212
+ store: CommandStore;
1213
+ eventLog: EventLogRepository;
1214
+ processedEvents: ProcessedEventRepository;
1215
+ logger: Logger;
1216
+ retentionDays?: number;
1217
+ intervalMs?: number;
1218
+ }
1219
+ /** Deletes delivered events and processed-event records older than the window; undelivered and dead events stay. */
1220
+ declare function startEventRetentionSweeper(deps: EventRetentionSweeperDeps): SweeperHandle;
1221
+
1222
+ interface ProviderPackage {
1223
+ /** The adapter package, which exports `createAdapter(ctx: AdapterContext)`. */
1224
+ package: string;
1225
+ /** The provider SDK the package peers, which the project installs beside it. */
1226
+ sdk: string;
1227
+ }
1228
+ /**
1229
+ * The providers that live in their own package, by slot then provider. The kernel imports a package only when the
1230
+ * config names its provider; `native`, `none` and `custom` have no entry.
1231
+ */
1232
+ declare const PROVIDER_PACKAGES: Readonly<Partial<Record<CommerceSlot, Readonly<Record<string, ProviderPackage>>>>>;
1233
+
1234
+ /**
1235
+ * v0.1 security adapter.
1236
+ *
1237
+ * Rate limiting, CSRF verification and CSRF cookie minting need the request before
1238
+ * any middleware and the response after the handler, so the HTTP adapter runs them
1239
+ * itself on every route (`FastifyHttpAdapter`'s guard). This adapter only asserts
1240
+ * static security headers via `HttpAdapter.setDefaultHeaders(...)`. A future
1241
+ * WAF-native adapter will replace both layers.
1242
+ */
1243
+ declare function createMiddlewareSecurityAdapter(deps: {
1244
+ logger: Logger;
1245
+ }): SecurityAdapter;
1246
+
1247
+ declare class RateLimiter {
1248
+ #private;
1249
+ constructor(rules: Record<string, {
1250
+ requests: number;
1251
+ window: string;
1252
+ action: RateLimitAction;
1253
+ }>);
1254
+ check(ip: string, path: string): {
1255
+ allowed: boolean;
1256
+ action: RateLimitAction;
1257
+ retryAfter?: number;
1258
+ } | null;
1259
+ reset(): void;
1260
+ }
1261
+
1262
+ declare function buildSecurityHeaders(security: SecurityConfigLike): Record<string, string>;
1263
+ declare const CSRF_COOKIE = "nymbal.csrf";
1264
+ declare const CSRF_HEADER = "x-csrf-token";
1265
+
1266
+ /**
1267
+ * Deterministic payments adapter for local development without Stripe keys.
1268
+ * Every intent succeeds at creation unless the receipt email asks otherwise (`statusFor`); webhook verification is a
1269
+ * no-op. Refunds follow a provider's rules for the intents this process holds (captured, and never beyond the
1270
+ * capture); an intent of its own lost to a restart is refunded as asked, the shop's refundable check being the guard.
1271
+ */
1272
+ declare function createNativeStubPaymentsAdapter(deps: {
1273
+ logger: Logger;
1274
+ }): PaymentsAdapter;
1275
+
1276
+ /** The native provider logs mail to the console; SMTP is the `smtp` provider (@nymbalhq/adapter-smtp). */
1277
+ interface NativeEmailAdapterConfig {
1278
+ transport?: 'console';
1279
+ from?: string;
1280
+ }
1281
+ declare function createNativeEmailAdapter(deps: {
1282
+ logger: Logger;
1283
+ config?: NativeEmailAdapterConfig;
1284
+ }): EmailAdapter;
1285
+
1286
+ declare function createNativeSearchAdapter(deps: {
1287
+ documentStore: DocumentStoreAdapter;
1288
+ logger: Logger;
1289
+ }): SearchAdapter;
1290
+
1291
+ declare function createNativeReviewsAdapter(deps: {
1292
+ repos: Repositories;
1293
+ logger: Logger;
1294
+ }): ReviewsAdapter;
1295
+
1296
+ declare function createNativeAnalyticsAdapter(logger: Logger): AnalyticsAdapter;
1297
+
1298
+ declare function createNativeShippingAdapter(logger: Logger): ShippingAdapter;
1299
+
1300
+ declare function createNativeTaxAdapter(logger: Logger, defaultRate?: number): TaxAdapter;
1301
+
1302
+ declare function createNativeAiAdapter(_logger: Logger): AiAdapter;
1303
+
1304
+ interface CreateLoggerOptions {
1305
+ level?: string;
1306
+ pretty?: boolean;
1307
+ base?: Record<string, unknown>;
1308
+ }
1309
+ declare function createLogger(options?: CreateLoggerOptions): Logger;
1310
+
1311
+ interface SeedDeps {
1312
+ config: NymbalConfig;
1313
+ commandStore: CommandStore;
1314
+ documentStore: DocumentStoreAdapter;
1315
+ eventBus: EventBusAdapter;
1316
+ logger: Logger;
1317
+ version: string;
1318
+ }
1319
+ interface SeedResult {
1320
+ categories: number;
1321
+ products: number;
1322
+ variants: number;
1323
+ customers: number;
1324
+ admins: number;
1325
+ /** True when any demo row was already in the command store, so nothing was inserted. */
1326
+ alreadySeeded: boolean;
1327
+ }
1328
+ declare function runSeed(deps: SeedDeps): Promise<SeedResult>;
1329
+
1330
+ interface WarmDocumentStoreDeps {
1331
+ config: NymbalConfig;
1332
+ repos: Repositories;
1333
+ eventBus: EventBusAdapter;
1334
+ logger: Logger;
1335
+ version: string;
1336
+ }
1337
+ interface WarmResult {
1338
+ products: number;
1339
+ orders: number;
1340
+ customers: number;
1341
+ }
1342
+ declare function warmDocumentStore(deps: WarmDocumentStoreDeps): Promise<WarmResult>;
1343
+
1344
+ interface SeedCategory {
1345
+ slug: string;
1346
+ name: string;
1347
+ description: string;
1348
+ }
1349
+ interface SeedProduct {
1350
+ slug: string;
1351
+ title: string;
1352
+ description: string;
1353
+ priceMinor: number;
1354
+ categorySlug: string;
1355
+ imageUrl: string;
1356
+ }
1357
+ declare const seedCategories: SeedCategory[];
1358
+ declare const seedProducts: SeedProduct[];
1359
+
1360
+ interface MediaStorage {
1361
+ capabilities(): readonly string[];
1362
+ put(key: string, body: Uint8Array, contentType: string): Promise<{
1363
+ url: string;
1364
+ }>;
1365
+ }
1366
+
1367
+ interface LocalFsMediaStorageOptions {
1368
+ root: string;
1369
+ urlPrefix?: string;
1370
+ }
1371
+ declare function createLocalFsMediaStorage(options: LocalFsMediaStorageOptions): MediaStorage;
1372
+
1373
+ export { type AbandonedCart, type AbandonedWindow, type AdapterRegistry, type AttachHttpOptions, type AuthService, type AuthTokens, type BeginCheckoutInput, type BeginCheckoutResult, type BuildAdaptersDeps, type BuildServicesDeps, CSRF_COOKIE, CSRF_HEADER, type CapabilitiesReport, type CapabilitySlot, type CapturedPayment, type CartRepository, type CartService, type CategoryCreateInput, type CategoryInsert, type CategoryRepository, type CategoryService, type CategoryUpdate, type CategoryUpdateInput, type CheckoutService, type CommandStore, type CommerceAdapters, type CreateAppOptions, type CreateAuthServiceDeps, type CreateCartServiceDeps, type CreateCheckoutServiceDeps, type CreateInventoryServiceDeps, type CreateLoggerOptions, type CreateOrderServiceDeps, type CreatePlatformOptions, type CreateProductServiceDeps, type CreatePublisherOptions, type CreateRefundServiceDeps, type CreateRelayOptions, type CustomerInsert, type CustomerJwtPayload, type CustomerRepository, type CustomerService, type CustomerUpdate, type CustomerWithHash, type EventBuilderContext, type EventLogRepository, type EventPublisher, type EventRetentionSweeperDeps, type FinalizeResult, type IdempotencyOptions, type ImportCustomerInput, InMemoryDocumentStore, type InMemoryDocumentStoreOptions, InProcessEventBus, type InProcessEventBusOptions, type InjectedAdapters, type InventoryReservationRepository, type InventoryService, type CategoryInsert$1 as LegacyCategoryInsert, type CategoryRow as LegacyCategoryRow, type ProductInsert$1 as LegacyProductInsert, type ProductRow as LegacyProductRow, type LocalFsMediaStorageOptions, type LoggedEvent, type MakeEventOptions, type ManualPayments, type MediaStorage, type MigrateOptions, type NativeEmailAdapterConfig, type NymbalApp, type OrderCapture, type OrderCreateDraftInput, type OrderHistoryInsert, type OrderHistoryRepository, type OrderImportInput, type OrderInsert, type OrderRefundInsert, type OrderRefundRepository, type OrderRepository, type OrderSequenceRepository, type OrderService, PROVIDER_PACKAGES, PROVIDER_REFUND_REASON, type Platform, type PostgresClient, type ProcessedEventRepository, type ProductCategoryRepository, type ProductCreateInput, type ProductInsert, type ProductRepository, type ProductService, type ProductUpdate, type ProductUpdateInput, type ProfileUpdate, type ProviderPackage, type PublishOptions, RateLimiter, type RawClients, type ReadTx, type RefreshTokenInsert, type RefreshTokenRepository, type RefreshTokenRow, type RefundInput, type RefundResult, type RefundService, type RegisterAllRoutesDeps, type RegisterHandlersDeps, type RegisterInput, type Relay, type Repositories, type ReservationInsert, type ReserveStockResult, type ReviewInsert, type ReviewRepository, type RouteSchema, type SearchService, type SeedCategory, type SeedDeps, type SeedProduct, type SeedResult, type ServiceRegistry, type Settlement, type SlotCapabilities, type SqliteClient, type StockAdjustmentInsert, type StockAdjustmentRepository, type StoreRepository, type SweeperHandle, type Tx, type VariantInsert, type VariantRepository, type VariantUpdate, type WarmDocumentStoreDeps, type WarmResult, buildAdapters, buildRepositories, buildSecurityHeaders, buildServices, createApp, createAuthService, createCartRepository, createCartService, createCategoryRepository, createCategoryService, createCheckoutService, createCommandStore, createCustomerRepository, createCustomerService, createDocumentStore, createEventBuilder, createEventBus, createEventLogRepository, createEventPublisher, createInventoryReservationRepository, createInventoryService, createCategoryRepository$1 as createLegacyCategoryRepository, createProductRepository$1 as createLegacyProductRepository, createLocalFsMediaStorage, createLogger, createMiddlewareSecurityAdapter, createNativeAiAdapter, createNativeAnalyticsAdapter, createNativeEmailAdapter, createNativeReviewsAdapter, createNativeSearchAdapter, createNativeShippingAdapter, createNativeStubPaymentsAdapter, createNativeTaxAdapter, createOrderHistoryRepository, createOrderRefundRepository, createOrderRepository, createOrderSequenceRepository, createOrderService, createPlatform, createProcessedEventRepository, createProductCategoryRepository, createProductRepository, createProductService, createRefreshTokenRepository, createRefundService, createRelay, createReviewRepository, createSearchService, createStockAdjustmentRepository, createVariantRepository, orderPayment, rawClients, refreshRefunded, registerAdminStaticRoute, registerAllRoutes, registerHandlers, routeSchemas, runMigrations, runSeed, seedCategories, seedProducts, settlementOf, startCartAbandonmentSweeper, startEventRetentionSweeper, startInventoryReservationSweeper, warmDocumentStore, withIdempotency };