@nymbalhq/types 1.0.0 → 1.2.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.
- package/dist/index.d.ts +313 -6
- package/dist/index.js +271 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ interface Product {
|
|
|
142
142
|
declare const PRODUCT_SORT_KEYS: readonly ["newest", "price-asc", "price-desc", "name-asc", "name-desc"];
|
|
143
143
|
type ProductSortKey = (typeof PRODUCT_SORT_KEYS)[number];
|
|
144
144
|
declare const DEFAULT_PRODUCT_SORT: ProductSortKey;
|
|
145
|
+
/** What a storefront shows for each sort. */
|
|
146
|
+
declare const PRODUCT_SORT_LABELS: Readonly<Record<ProductSortKey, string>>;
|
|
145
147
|
|
|
146
148
|
interface Category {
|
|
147
149
|
id: string;
|
|
@@ -407,6 +409,7 @@ declare const EVT_ORDER_DELIVERED = "order.delivered.v1";
|
|
|
407
409
|
declare const EVT_ORDER_CANCELLED = "order.cancelled.v1";
|
|
408
410
|
declare const EVT_ORDER_REFUNDED = "order.refunded.v1";
|
|
409
411
|
declare const EVT_ORDER_PARTIALLY_REFUNDED = "order.partially_refunded.v1";
|
|
412
|
+
declare const EVT_ORDER_STATUS_CHANGED = "order.status_changed.v1";
|
|
410
413
|
declare const EVT_INVENTORY_CHANGED = "inventory.changed.v1";
|
|
411
414
|
declare const EVT_INVENTORY_RESERVED = "inventory.reserved.v1";
|
|
412
415
|
declare const EVT_INVENTORY_RELEASED = "inventory.released.v1";
|
|
@@ -422,11 +425,13 @@ declare const EVT_CART_ITEM_REMOVED = "cart.item_removed.v1";
|
|
|
422
425
|
declare const EVT_CART_UPDATED = "cart.updated.v1";
|
|
423
426
|
declare const EVT_CART_ABANDONED = "cart.abandoned.v1";
|
|
424
427
|
declare const EVT_CART_CONVERTED = "cart.converted.v1";
|
|
428
|
+
declare const EVT_PAYMENT_INTENT_CREATED = "payment.intent_created.v1";
|
|
425
429
|
declare const EVT_PAYMENT_CAPTURED = "payment.captured.v1";
|
|
426
430
|
declare const EVT_PAYMENT_FAILED = "payment.failed.v1";
|
|
427
431
|
declare const EVT_PAYMENT_REFUNDED = "payment.refunded.v1";
|
|
428
432
|
declare const EVT_ADAPTER_ERROR = "adapter.error.v1";
|
|
429
|
-
declare const
|
|
433
|
+
declare const EVT_SETTINGS_CHANGED = "settings.changed.v1";
|
|
434
|
+
declare const STANDARD_EVENT_TYPES: readonly ["product.created.v1", "product.updated.v1", "product.deleted.v1", "product.published.v1", "product.unpublished.v1", "category.created.v1", "category.updated.v1", "category.deleted.v1", "order.placed.v1", "order.paid.v1", "order.fulfilled.v1", "order.shipped.v1", "order.delivered.v1", "order.cancelled.v1", "order.refunded.v1", "order.partially_refunded.v1", "order.status_changed.v1", "inventory.changed.v1", "inventory.reserved.v1", "inventory.released.v1", "inventory.low_stock.v1", "inventory.out_of_stock.v1", "customer.created.v1", "customer.updated.v1", "customer.deleted.v1", "cart.created.v1", "cart.item_added.v1", "cart.item_removed.v1", "cart.updated.v1", "cart.abandoned.v1", "cart.converted.v1", "payment.intent_created.v1", "payment.captured.v1", "payment.failed.v1", "payment.refunded.v1", "adapter.error.v1", "settings.changed.v1"];
|
|
430
435
|
|
|
431
436
|
interface ProductSnapshot extends Product {
|
|
432
437
|
variants: Variant[];
|
|
@@ -498,6 +503,12 @@ interface OrderPartiallyRefundedV1Payload {
|
|
|
498
503
|
currency: string;
|
|
499
504
|
reason: string;
|
|
500
505
|
}
|
|
506
|
+
/** A status change no other order event names (an admin moving an order to confirmed or processing). */
|
|
507
|
+
interface OrderStatusChangedV1Payload {
|
|
508
|
+
orderId: string;
|
|
509
|
+
from: OrderStatus;
|
|
510
|
+
to: OrderStatus;
|
|
511
|
+
}
|
|
501
512
|
interface InventoryChangedV1Payload {
|
|
502
513
|
variantId: string;
|
|
503
514
|
previousQty: number;
|
|
@@ -560,6 +571,11 @@ interface CartConvertedV1Payload {
|
|
|
560
571
|
cartId: string;
|
|
561
572
|
orderId: string;
|
|
562
573
|
}
|
|
574
|
+
/** The provider's intent for an order, stored on the order whatever its outcome. */
|
|
575
|
+
interface PaymentIntentCreatedV1Payload {
|
|
576
|
+
paymentId: string;
|
|
577
|
+
orderId: string;
|
|
578
|
+
}
|
|
563
579
|
interface PaymentCapturedV1Payload {
|
|
564
580
|
paymentId: string;
|
|
565
581
|
orderId: string;
|
|
@@ -587,6 +603,12 @@ interface AdapterErrorV1Payload {
|
|
|
587
603
|
};
|
|
588
604
|
context: Record<string, unknown>;
|
|
589
605
|
}
|
|
606
|
+
/** A Settings field was saved or cleared; never the value. `source` is where the field comes from afterwards. */
|
|
607
|
+
interface SettingsChangedV1Payload {
|
|
608
|
+
slot: string;
|
|
609
|
+
path: string;
|
|
610
|
+
source: 'settings' | null;
|
|
611
|
+
}
|
|
590
612
|
|
|
591
613
|
interface NymbalEventMap {
|
|
592
614
|
[EVT_PRODUCT_CREATED]: ProductCreatedV1Payload;
|
|
@@ -605,6 +627,7 @@ interface NymbalEventMap {
|
|
|
605
627
|
[EVT_ORDER_CANCELLED]: OrderCancelledV1Payload;
|
|
606
628
|
[EVT_ORDER_REFUNDED]: OrderRefundedV1Payload;
|
|
607
629
|
[EVT_ORDER_PARTIALLY_REFUNDED]: OrderPartiallyRefundedV1Payload;
|
|
630
|
+
[EVT_ORDER_STATUS_CHANGED]: OrderStatusChangedV1Payload;
|
|
608
631
|
[EVT_INVENTORY_CHANGED]: InventoryChangedV1Payload;
|
|
609
632
|
[EVT_INVENTORY_RESERVED]: InventoryReservedV1Payload;
|
|
610
633
|
[EVT_INVENTORY_RELEASED]: InventoryReleasedV1Payload;
|
|
@@ -619,10 +642,12 @@ interface NymbalEventMap {
|
|
|
619
642
|
[EVT_CART_UPDATED]: CartUpdatedV1Payload;
|
|
620
643
|
[EVT_CART_ABANDONED]: CartAbandonedV1Payload;
|
|
621
644
|
[EVT_CART_CONVERTED]: CartConvertedV1Payload;
|
|
645
|
+
[EVT_PAYMENT_INTENT_CREATED]: PaymentIntentCreatedV1Payload;
|
|
622
646
|
[EVT_PAYMENT_CAPTURED]: PaymentCapturedV1Payload;
|
|
623
647
|
[EVT_PAYMENT_FAILED]: PaymentFailedV1Payload;
|
|
624
648
|
[EVT_PAYMENT_REFUNDED]: PaymentRefundedV1Payload;
|
|
625
649
|
[EVT_ADAPTER_ERROR]: AdapterErrorV1Payload;
|
|
650
|
+
[EVT_SETTINGS_CHANGED]: SettingsChangedV1Payload;
|
|
626
651
|
}
|
|
627
652
|
type NymbalEventType = keyof NymbalEventMap;
|
|
628
653
|
type PayloadOf<E extends NymbalEventType> = NymbalEventMap[E];
|
|
@@ -645,6 +670,13 @@ interface NymbalEvent<T = unknown> {
|
|
|
645
670
|
type EventHandler<T = unknown> = (event: NymbalEvent<T>) => Promise<void> | void;
|
|
646
671
|
|
|
647
672
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
673
|
+
/** The request header a caller may name its own correlation id in, and the response header that echoes the one used. */
|
|
674
|
+
declare const CORRELATION_ID_HEADER = "x-correlation-id";
|
|
675
|
+
/**
|
|
676
|
+
* Whether `s` may be used as a correlation id: it reaches logs and the event log, so an inbound value that fails is
|
|
677
|
+
* replaced, never cleaned. The id is a label only; nothing reads it to decide access.
|
|
678
|
+
*/
|
|
679
|
+
declare function isValidCorrelationId(s: string): boolean;
|
|
648
680
|
interface RequestAuth {
|
|
649
681
|
userId?: string;
|
|
650
682
|
roles?: string[];
|
|
@@ -670,6 +702,7 @@ interface RequestContext<TBody = unknown, TQuery = Record<string, string | strin
|
|
|
670
702
|
cookies: Record<string, string>;
|
|
671
703
|
auth?: RequestAuth;
|
|
672
704
|
requestId: string;
|
|
705
|
+
/** The inbound `x-correlation-id` when `isValidCorrelationId` accepts it, else the request id. */
|
|
673
706
|
correlationId: string;
|
|
674
707
|
logger: Logger;
|
|
675
708
|
ip: string;
|
|
@@ -723,6 +756,39 @@ interface HealthReport {
|
|
|
723
756
|
status: HealthStatus;
|
|
724
757
|
detail?: string;
|
|
725
758
|
}
|
|
759
|
+
/** How long a health check may run before it reports `down`. */
|
|
760
|
+
declare const HEALTH_CHECK_TIMEOUT_MS = 5000;
|
|
761
|
+
interface BoundedHealthCheckOptions {
|
|
762
|
+
/** `HEALTH_CHECK_TIMEOUT_MS` when not given. */
|
|
763
|
+
timeoutMs?: number;
|
|
764
|
+
/** Values an error's message must never show (a URL's user, password or query values); each becomes `***`. */
|
|
765
|
+
redact?: readonly string[];
|
|
766
|
+
}
|
|
767
|
+
/** An error's message for a report; an AggregateError (a refused connection to every address of a name) lists its own. */
|
|
768
|
+
declare function errorText(error: unknown): string;
|
|
769
|
+
/**
|
|
770
|
+
* The shortest value worth removing from text as a secret. A shorter one is too likely to be an ordinary word (a local
|
|
771
|
+
* password of `nymbal`), whose removal garbles the text; user info and query strings go with the URL rewrite anyway.
|
|
772
|
+
*/
|
|
773
|
+
declare const SECRET_MIN_LENGTH = 8;
|
|
774
|
+
/**
|
|
775
|
+
* The values in a connection URL that are credentials or may carry one: its password and its query values, and its
|
|
776
|
+
* user unless `user` is false, each as written and decoded, if at least `SECRET_MIN_LENGTH` long. A user is worth
|
|
777
|
+
* hiding in a driver's error, not in our own text.
|
|
778
|
+
*/
|
|
779
|
+
declare function connectionSecrets(url: string, options?: {
|
|
780
|
+
user?: boolean;
|
|
781
|
+
}): string[];
|
|
782
|
+
/**
|
|
783
|
+
* A connection URL as it may be printed: `scheme://host:port/path`, with no user, password, query or fragment. Text
|
|
784
|
+
* that is not a URL comes back as `<unparsable url>`, since it may be a URL with a secret the parser could not read.
|
|
785
|
+
*/
|
|
786
|
+
declare function displayUrl(url: string): string;
|
|
787
|
+
/**
|
|
788
|
+
* Runs `check` within the bound. A check that throws is `down` with the error's message, every `redact` value in it
|
|
789
|
+
* replaced; one still running when the bound passes is `down` with `timed out after <n> ms`, and its signal aborted.
|
|
790
|
+
*/
|
|
791
|
+
declare function boundedHealthCheck(check: (signal: AbortSignal) => Promise<HealthReport> | HealthReport, options?: BoundedHealthCheckOptions): Promise<HealthReport>;
|
|
726
792
|
interface AdapterBase {
|
|
727
793
|
/** What this adapter offers, for the app's per-slot report; each adapter answers for itself. */
|
|
728
794
|
capabilities(): readonly string[];
|
|
@@ -733,16 +799,45 @@ interface AdapterBase {
|
|
|
733
799
|
}
|
|
734
800
|
/** The commerce slots a store's config names a provider for. */
|
|
735
801
|
type CommerceSlot = 'payments' | 'email' | 'reviews' | 'search' | 'analytics' | 'shipping' | 'tax' | 'ai';
|
|
802
|
+
/**
|
|
803
|
+
* Every slot an adapter package can fill: the commerce slots, the infrastructure primitives and `security`, whose
|
|
804
|
+
* choice is `security.adapter`.
|
|
805
|
+
*/
|
|
806
|
+
type Slot = CommerceSlot | 'documentStore' | 'eventBus' | 'media' | 'security';
|
|
736
807
|
/**
|
|
737
808
|
* What the kernel hands an adapter package's `createAdapter`. A package that serves more than one slot switches on
|
|
738
|
-
* `slot`; `config` is
|
|
809
|
+
* `slot`; `config` is a commerce slot's `config` block from nymbal.config.ts, a primitive's connection block, or for
|
|
810
|
+
* `security` the block named after the adapter (`security.cloudflare`).
|
|
739
811
|
*/
|
|
740
812
|
interface AdapterContext {
|
|
741
|
-
slot:
|
|
813
|
+
slot: Slot;
|
|
742
814
|
config: Record<string, unknown>;
|
|
743
815
|
logger: Logger;
|
|
744
816
|
env: NodeJS.ProcessEnv;
|
|
745
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* One field of a slot's config block that a store owner may fill from the admin's Settings, as a provider package
|
|
820
|
+
* declares it in its `settings` export. The config block wins, then `env`, then a saved value.
|
|
821
|
+
*/
|
|
822
|
+
interface AdapterSetting {
|
|
823
|
+
/** A dot path inside the slot's config block: `secretKey`, `smtp.pass`. */
|
|
824
|
+
path: string;
|
|
825
|
+
label: string;
|
|
826
|
+
/** Stored encrypted and never shown again once saved. */
|
|
827
|
+
secret: boolean;
|
|
828
|
+
/** The environment variable that sets the field when the config block does not. */
|
|
829
|
+
env?: string;
|
|
830
|
+
/**
|
|
831
|
+
* The adapter works without it (SMTP without authentication), so a secret left unset does not leave the slot
|
|
832
|
+
* unconfigured.
|
|
833
|
+
*/
|
|
834
|
+
optional?: boolean;
|
|
835
|
+
}
|
|
836
|
+
/** What a provider package exports: `createAdapter`, and the fields it lets Settings fill. */
|
|
837
|
+
interface ProviderPackageModule {
|
|
838
|
+
createAdapter(ctx: AdapterContext): unknown;
|
|
839
|
+
settings?: readonly AdapterSetting[];
|
|
840
|
+
}
|
|
746
841
|
|
|
747
842
|
interface PutOptions {
|
|
748
843
|
ttl?: number;
|
|
@@ -761,8 +856,9 @@ interface QueryParams {
|
|
|
761
856
|
value: unknown;
|
|
762
857
|
};
|
|
763
858
|
/**
|
|
764
|
-
* Orders the results without filtering them; `field` may be a dotted path (`priceRange.minMinor`).
|
|
765
|
-
*
|
|
859
|
+
* Orders the results without filtering them; `field` may be a dotted path (`priceRange.minMinor`). Values compare
|
|
860
|
+
* with `compareDocumentValues`; null and missing values come last in either direction, and ties break by document
|
|
861
|
+
* key, ascending, so the order is total. A cursor issued under one `orderBy` is refused under another.
|
|
766
862
|
*/
|
|
767
863
|
orderBy?: {
|
|
768
864
|
field: string;
|
|
@@ -785,9 +881,37 @@ interface DocumentStoreAdapter {
|
|
|
785
881
|
query<T>(collection: string, params: QueryParams): Promise<QueryResult<T>>;
|
|
786
882
|
batchGet<T>(collection: string, keys: string[]): Promise<Map<string, T>>;
|
|
787
883
|
batchPut<T>(collection: string, items: Map<string, T>): Promise<void>;
|
|
884
|
+
/**
|
|
885
|
+
* Replaces the whole collection with `entries`: afterwards it holds exactly those documents (none with a TTL), and a
|
|
886
|
+
* reader at any moment sees either the old contents or the new, never a mix. A source that throws leaves the old
|
|
887
|
+
* contents in place, and the call rejects with its error.
|
|
888
|
+
*/
|
|
889
|
+
replaceCollection<T>(collection: string, entries: AsyncIterable<[string, T]>): Promise<void>;
|
|
788
890
|
close(): Promise<void>;
|
|
891
|
+
/**
|
|
892
|
+
* Whether the store can serve, within `HEALTH_CHECK_TIMEOUT_MS`. Optional, so a store written before it existed
|
|
893
|
+
* still type-checks; `nymbal doctor` reports one without it as `no healthCheck`.
|
|
894
|
+
*/
|
|
895
|
+
healthCheck?(): Promise<HealthReport>;
|
|
789
896
|
}
|
|
790
897
|
|
|
898
|
+
/**
|
|
899
|
+
* A total order over document values: numbers numerically, strings by `Intl.Collator` at base sensitivity (case and
|
|
900
|
+
* accents tie), other types after strings, and null or missing last. Values of different types rank by type first.
|
|
901
|
+
*/
|
|
902
|
+
declare function compareDocumentValues(a: unknown, b: unknown): number;
|
|
903
|
+
type SortKeyCondition = Pick<NonNullable<QueryParams['sortKey']>, 'operator' | 'value'>;
|
|
904
|
+
/** `eq` is `===`; `gt`, `lt` and `between` (inclusive, `[low, high]`) are `compareDocumentValues`. */
|
|
905
|
+
declare function matchesSortKey(value: unknown, condition: SortKeyCondition): boolean;
|
|
906
|
+
/** The value at a dotted path (`priceRange.minMinor`); missing anywhere along it is `undefined`. */
|
|
907
|
+
declare function documentValueAt(doc: unknown, path: string): unknown;
|
|
908
|
+
declare const DEFAULT_QUERY_LIMIT = 50;
|
|
909
|
+
/**
|
|
910
|
+
* One page of `params` over the documents of a collection: filter by partition (`'*'` for all) and sort key, order,
|
|
911
|
+
* then seek past the cursor by value, so a document deleted between pages neither restarts nor repeats the walk.
|
|
912
|
+
*/
|
|
913
|
+
declare function queryDocuments<T>(documents: Iterable<readonly [string, unknown]>, params: QueryParams): QueryResult<T>;
|
|
914
|
+
|
|
791
915
|
interface RetryPolicy {
|
|
792
916
|
maxAttempts: number;
|
|
793
917
|
backoff: 'exponential' | 'linear' | 'fixed';
|
|
@@ -797,6 +921,10 @@ interface SubscriptionConfig {
|
|
|
797
921
|
retryPolicy?: RetryPolicy;
|
|
798
922
|
deadLetterQueue?: string;
|
|
799
923
|
batchSize?: number;
|
|
924
|
+
/**
|
|
925
|
+
* A named subscription is handled once per name across every bus instance (one consumer group); an unnamed one is
|
|
926
|
+
* handled by every subscriber on every instance. On a single-process bus the two are the same.
|
|
927
|
+
*/
|
|
800
928
|
name?: string;
|
|
801
929
|
/** The subscriber only maintains derived state (a projection, a search index): it also receives `replay`. */
|
|
802
930
|
derived?: boolean;
|
|
@@ -806,8 +934,16 @@ interface Subscription {
|
|
|
806
934
|
patterns: string[];
|
|
807
935
|
unsubscribe(): Promise<void>;
|
|
808
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Delivery is at least once: a publish that is retried may reach a handler twice, so every handler is idempotent
|
|
939
|
+
* (projections overwrite their documents, side effects use `withIdempotency`).
|
|
940
|
+
*/
|
|
809
941
|
interface EventBusAdapter {
|
|
810
942
|
capabilities(): readonly string[];
|
|
943
|
+
/**
|
|
944
|
+
* Resolves only after every matching subscription this instance holds has handled the event, so a read straight
|
|
945
|
+
* after a command sees its projections. A handler that throws is logged, and does not reject the publish.
|
|
946
|
+
*/
|
|
811
947
|
publish<T>(event: NymbalEvent<T>): Promise<void>;
|
|
812
948
|
publishBatch(events: NymbalEvent<unknown>[]): Promise<void>;
|
|
813
949
|
/** Delivers to `derived` subscriptions only, so rebuilding derived state never triggers side effects. */
|
|
@@ -815,6 +951,24 @@ interface EventBusAdapter {
|
|
|
815
951
|
subscribe(eventPattern: string | string[], handler: EventHandler, config?: SubscriptionConfig): Promise<Subscription>;
|
|
816
952
|
unsubscribe(subscription: Subscription): Promise<void>;
|
|
817
953
|
close(): Promise<void>;
|
|
954
|
+
/**
|
|
955
|
+
* Whether the bus can deliver, within `HEALTH_CHECK_TIMEOUT_MS`. Optional, so a bus written before it existed still
|
|
956
|
+
* type-checks; `nymbal doctor` reports one without it as `no healthCheck`.
|
|
957
|
+
*/
|
|
958
|
+
healthCheck?(): Promise<HealthReport>;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Where uploaded and imported media go. `put` returns the URL the stored bytes are served from. */
|
|
962
|
+
interface MediaStorage {
|
|
963
|
+
capabilities(): readonly string[];
|
|
964
|
+
put(key: string, body: Uint8Array, contentType: string): Promise<{
|
|
965
|
+
url: string;
|
|
966
|
+
}>;
|
|
967
|
+
/**
|
|
968
|
+
* Whether `put` can store, checked without storing anything, within `HEALTH_CHECK_TIMEOUT_MS`. Optional, so a
|
|
969
|
+
* storage written before it existed still type-checks; `nymbal doctor` reports one without it as `no healthCheck`.
|
|
970
|
+
*/
|
|
971
|
+
healthCheck?(): Promise<HealthReport>;
|
|
818
972
|
}
|
|
819
973
|
|
|
820
974
|
type RateLimitAction = 'block' | 'challenge' | 'throttle';
|
|
@@ -865,6 +1019,8 @@ interface SecurityAdapter extends AdapterBase {
|
|
|
865
1019
|
drift?(config: SecurityConfigLike): Promise<DriftReport>;
|
|
866
1020
|
inspect?(): Promise<SecurityState>;
|
|
867
1021
|
}
|
|
1022
|
+
/** The response headers every security adapter sets in process, from the config's `headers` block. */
|
|
1023
|
+
declare function buildSecurityHeaders(security: SecurityConfigLike): Record<string, string>;
|
|
868
1024
|
|
|
869
1025
|
interface CommerceAdapter extends AdapterBase {
|
|
870
1026
|
readonly providerName: string;
|
|
@@ -875,8 +1031,18 @@ interface RefundOptions {
|
|
|
875
1031
|
/** The order's currency: what a provider that no longer holds the payment answers in. */
|
|
876
1032
|
currency?: string;
|
|
877
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* The storefront half of a payments adapter: which client-side driver collects the payment and the public values it
|
|
1036
|
+
* needs. GET /api/capabilities serves `options` to every shopper, so it holds public values only.
|
|
1037
|
+
*/
|
|
1038
|
+
interface PaymentClient {
|
|
1039
|
+
driver: string;
|
|
1040
|
+
options: Record<string, string>;
|
|
1041
|
+
}
|
|
878
1042
|
interface PaymentsAdapter extends CommerceAdapter {
|
|
879
1043
|
readonly kind: 'payments';
|
|
1044
|
+
/** How a storefront collects a payment; absent or `null` when there is no client step. */
|
|
1045
|
+
client?(): PaymentClient | null;
|
|
880
1046
|
createPaymentIntent(params: PaymentIntentParams): Promise<PaymentIntent>;
|
|
881
1047
|
capturePayment(intentId: string, amountMinor?: number): Promise<PaymentCapture>;
|
|
882
1048
|
/** Throws `PaymentError('refund_rejected')` when no money moved, `PaymentError('refund_unknown')` when it may have. */
|
|
@@ -956,6 +1122,8 @@ interface SearchResult {
|
|
|
956
1122
|
nextCursor: string | null;
|
|
957
1123
|
}
|
|
958
1124
|
interface FacetParams {
|
|
1125
|
+
/** Count only the documents this query matches. */
|
|
1126
|
+
query?: string;
|
|
959
1127
|
limit?: number;
|
|
960
1128
|
filters?: Record<string, unknown>;
|
|
961
1129
|
}
|
|
@@ -972,6 +1140,11 @@ interface SearchAdapter extends CommerceAdapter {
|
|
|
972
1140
|
readonly kind: 'search';
|
|
973
1141
|
index(documents: SearchDocument[]): Promise<IndexResult>;
|
|
974
1142
|
remove(documentIds: string[]): Promise<void>;
|
|
1143
|
+
/**
|
|
1144
|
+
* Replaces the whole index with `documents`, so nothing outside them survives. Optional: without it a rebuild can
|
|
1145
|
+
* only `index` every document, and cannot remove ids the index holds beyond them (it has no way to list them).
|
|
1146
|
+
*/
|
|
1147
|
+
replaceAll?(documents: Iterable<SearchDocument> | AsyncIterable<SearchDocument>): Promise<void>;
|
|
975
1148
|
search(query: string, params?: SearchParams): Promise<SearchResult>;
|
|
976
1149
|
facet(field: string, params?: FacetParams): Promise<Facet>;
|
|
977
1150
|
}
|
|
@@ -1041,6 +1214,17 @@ interface AiAdapter extends CommerceAdapter {
|
|
|
1041
1214
|
maxTokens?: number;
|
|
1042
1215
|
}): Promise<string>;
|
|
1043
1216
|
}
|
|
1217
|
+
/** The adapter interface each commerce slot takes: what a project injects for a slot whose provider is `custom`. */
|
|
1218
|
+
interface CommerceSlotAdapters {
|
|
1219
|
+
payments: PaymentsAdapter;
|
|
1220
|
+
email: EmailAdapter;
|
|
1221
|
+
reviews: ReviewsAdapter;
|
|
1222
|
+
search: SearchAdapter;
|
|
1223
|
+
analytics: AnalyticsAdapter;
|
|
1224
|
+
shipping: ShippingAdapter;
|
|
1225
|
+
tax: TaxAdapter;
|
|
1226
|
+
ai: AiAdapter;
|
|
1227
|
+
}
|
|
1044
1228
|
|
|
1045
1229
|
interface RenderedEmail {
|
|
1046
1230
|
subject: string;
|
|
@@ -1049,4 +1233,127 @@ interface RenderedEmail {
|
|
|
1049
1233
|
}
|
|
1050
1234
|
declare function renderEmailTemplate(template: string, vars: Record<string, unknown>): RenderedEmail;
|
|
1051
1235
|
|
|
1052
|
-
|
|
1236
|
+
/**
|
|
1237
|
+
* What GET /api/capabilities tells a storefront about the store: what its adapters can do, never which vendor they
|
|
1238
|
+
* are. It names no provider and holds no secret.
|
|
1239
|
+
*/
|
|
1240
|
+
interface StoreCapabilities {
|
|
1241
|
+
payments: {
|
|
1242
|
+
capabilities: string[];
|
|
1243
|
+
client: PaymentClient | null;
|
|
1244
|
+
};
|
|
1245
|
+
search: {
|
|
1246
|
+
capabilities: string[];
|
|
1247
|
+
};
|
|
1248
|
+
/** The listing orders GET /api/products accepts, in display order, and the one it uses when none is asked. */
|
|
1249
|
+
catalog: {
|
|
1250
|
+
sortKeys: ProductSortKey[];
|
|
1251
|
+
defaultSort: ProductSortKey;
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* A value a recipe writes into `nymbal.config.ts`. `{ env }` is written as `env('<name>')`, or
|
|
1257
|
+
* `env('<name>', '<default>')` when a default is given, so no secret ever lands in the file.
|
|
1258
|
+
*/
|
|
1259
|
+
type RecipeValue = string | boolean | number | {
|
|
1260
|
+
env: string;
|
|
1261
|
+
default?: string;
|
|
1262
|
+
} | {
|
|
1263
|
+
[k: string]: RecipeValue;
|
|
1264
|
+
};
|
|
1265
|
+
/**
|
|
1266
|
+
* What `nymbal add <name>` does to a project: data shipped as `recipes/<name>.json` inside the package it belongs to.
|
|
1267
|
+
*/
|
|
1268
|
+
interface Recipe {
|
|
1269
|
+
name: string;
|
|
1270
|
+
summary: string;
|
|
1271
|
+
/** npm packages to install, SDKs included; empty when the service is built in. */
|
|
1272
|
+
install: string[];
|
|
1273
|
+
/** Config paths and the values they take, e.g. `infrastructure.documentStore` = `'redis'`. */
|
|
1274
|
+
set: Array<{
|
|
1275
|
+
path: string;
|
|
1276
|
+
value: RecipeValue;
|
|
1277
|
+
}>;
|
|
1278
|
+
/** The variables the service reads, appended to `.env.example`; an optional one is written commented out. */
|
|
1279
|
+
env: Array<{
|
|
1280
|
+
name: string;
|
|
1281
|
+
example: string;
|
|
1282
|
+
required: boolean;
|
|
1283
|
+
comment: string;
|
|
1284
|
+
}>;
|
|
1285
|
+
/** Lines printed after a successful add. */
|
|
1286
|
+
next: string[];
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
type StockLevel = 'out' | 'low' | 'in';
|
|
1290
|
+
/** The one stock rule every storefront renders from. */
|
|
1291
|
+
declare function stockLevel(stock: number): StockLevel;
|
|
1292
|
+
declare const STOCK_LABELS: Record<StockLevel, string>;
|
|
1293
|
+
|
|
1294
|
+
/** Where a Settings field's value comes from: the config block, its declared env, a saved value, or nowhere. */
|
|
1295
|
+
type SettingSourceName = 'config' | 'env' | 'settings';
|
|
1296
|
+
/** One declared field as `GET /api/admin/settings` lists it; a secret never carries its value. */
|
|
1297
|
+
interface SettingFieldView {
|
|
1298
|
+
path: string;
|
|
1299
|
+
label: string;
|
|
1300
|
+
secret: boolean;
|
|
1301
|
+
state: 'unset' | 'set';
|
|
1302
|
+
source: SettingSourceName | null;
|
|
1303
|
+
updatedAt: string | null;
|
|
1304
|
+
/** Non-secret fields only. */
|
|
1305
|
+
value?: unknown;
|
|
1306
|
+
}
|
|
1307
|
+
/** A slot whose provider declares settings, with its fields. */
|
|
1308
|
+
interface SettingsSlotView {
|
|
1309
|
+
slot: string;
|
|
1310
|
+
provider: string;
|
|
1311
|
+
fields: SettingFieldView[];
|
|
1312
|
+
}
|
|
1313
|
+
interface SettingsListing {
|
|
1314
|
+
slots: SettingsSlotView[];
|
|
1315
|
+
}
|
|
1316
|
+
/** What a `PUT` or `DELETE` of a field answers: the change applies at the next start. */
|
|
1317
|
+
interface SettingWriteResult {
|
|
1318
|
+
slot: string;
|
|
1319
|
+
path: string;
|
|
1320
|
+
state: 'unset' | 'set';
|
|
1321
|
+
source: 'settings' | null;
|
|
1322
|
+
restartRequired: true;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** Who a command ran for. A guest is `guest:anonymous`: a cart's token is a bearer credential and is never an id. */
|
|
1326
|
+
interface AuditActor {
|
|
1327
|
+
kind: 'admin' | 'customer' | 'guest' | 'system' | 'import';
|
|
1328
|
+
id: string;
|
|
1329
|
+
}
|
|
1330
|
+
/** `applied`: the command ran and committed. `refused`: its service refused it. `replayed`: a keyed retry answered. */
|
|
1331
|
+
type AuditOutcome = 'applied' | 'refused' | 'replayed';
|
|
1332
|
+
/** A record a command touched, by id only. */
|
|
1333
|
+
interface AuditSubject {
|
|
1334
|
+
type: string;
|
|
1335
|
+
id: string;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* One audit entry as `GET /api/admin/activity` lists it and the exports print it. A stored response is shown by its
|
|
1339
|
+
* status only; its body never leaves the store.
|
|
1340
|
+
*/
|
|
1341
|
+
interface AuditEntry {
|
|
1342
|
+
id: string;
|
|
1343
|
+
at: string;
|
|
1344
|
+
actor: AuditActor;
|
|
1345
|
+
command: string;
|
|
1346
|
+
subjects: AuditSubject[];
|
|
1347
|
+
correlationId: string | null;
|
|
1348
|
+
idempotencyKey: string | null;
|
|
1349
|
+
outcome: AuditOutcome;
|
|
1350
|
+
responseStatus: number | null;
|
|
1351
|
+
}
|
|
1352
|
+
/** The request header an admin write carries to be applied once, and the response header a replay carries. */
|
|
1353
|
+
declare const IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
1354
|
+
declare const IDEMPOTENT_REPLAYED_HEADER = "idempotent-replayed";
|
|
1355
|
+
/** 1-128 characters of `[A-Za-z0-9._:-]`. */
|
|
1356
|
+
declare function isValidIdempotencyKey(value: string): boolean;
|
|
1357
|
+
|
|
1358
|
+
export { AdapterError, AuthError, CORRELATION_ID_HEADER, CheckoutError, ConfigError, DEFAULT_PRODUCT_SORT, DEFAULT_QUERY_LIMIT, EVT_ADAPTER_ERROR, EVT_CART_ABANDONED, EVT_CART_CONVERTED, EVT_CART_CREATED, EVT_CART_ITEM_ADDED, EVT_CART_ITEM_REMOVED, EVT_CART_UPDATED, EVT_CATEGORY_CREATED, EVT_CATEGORY_DELETED, EVT_CATEGORY_UPDATED, EVT_CUSTOMER_CREATED, EVT_CUSTOMER_DELETED, EVT_CUSTOMER_REGISTERED, EVT_CUSTOMER_UPDATED, EVT_INVENTORY_CHANGED, EVT_INVENTORY_LOW_STOCK, EVT_INVENTORY_OUT_OF_STOCK, EVT_INVENTORY_RELEASED, EVT_INVENTORY_RESERVED, EVT_ORDER_CANCELLED, EVT_ORDER_DELIVERED, EVT_ORDER_FULFILLED, EVT_ORDER_PAID, EVT_ORDER_PARTIALLY_REFUNDED, EVT_ORDER_PLACED, EVT_ORDER_REFUNDED, EVT_ORDER_SHIPPED, EVT_ORDER_STATUS_CHANGED, EVT_PAYMENT_CAPTURED, EVT_PAYMENT_FAILED, EVT_PAYMENT_INTENT_CREATED, EVT_PAYMENT_REFUNDED, EVT_PRODUCT_CREATED, EVT_PRODUCT_DELETED, EVT_PRODUCT_PUBLISHED, EVT_PRODUCT_UNPUBLISHED, EVT_PRODUCT_UPDATED, EVT_SETTINGS_CHANGED, HEALTH_CHECK_TIMEOUT_MS, HttpError, IDEMPOTENCY_KEY_HEADER, IDEMPOTENT_REPLAYED_HEADER, NotFoundError, NymbalError, ORDER_STATUS_TRANSITIONS, OrderConflictError, PRODUCT_SORT_KEYS, PRODUCT_SORT_LABELS, PaymentError, SECRET_MIN_LENGTH, STANDARD_EVENT_TYPES, STOCK_LABELS, StatusTransitionError, ValidationError, add, allocate, assertMinorUnits, assertOrderTransition, boundedHealthCheck, buildSecurityHeaders, canTransitionOrder, compareDocumentValues, connectionSecrets, displayUrl, documentValueAt, errorText, isValidCorrelationId, isValidIdempotencyKey, matchesSortKey, money, mulBy, paymentState, queryDocuments, refundableMinor, renderEmailTemplate, stockLevel, sub, sumMoney };
|
|
1359
|
+
export type { AdapterBase, AdapterContext, AdapterErrorV1Payload, AdapterSetting, Address, AggregateRating, AiAdapter, AnalyticsAdapter, AuditActor, AuditEntry, AuditOutcome, AuditSubject, BoundedHealthCheckOptions, CaptureMethod, Cart, CartAbandonedV1Payload, CartConvertedV1Payload, CartCreatedV1Payload, CartItem, CartItemAddedV1Payload, CartItemRemovedV1Payload, CartUpdatedV1Payload, Category, CategoryCreatedV1Payload, CategoryDeletedV1Payload, CategoryUpdatedV1Payload, CheckoutPayment, CommerceAdapter, CommerceSlot, CommerceSlotAdapters, Customer, CustomerAddress, CustomerCreatedV1Payload, CustomerDeletedV1Payload, CustomerProfile, CustomerUpdatedV1Payload, DocumentStoreAdapter, DriftReport, EmailAdapter, EmailSendResult, ErrorContext, EventBusAdapter, EventHandler, EventMetadata, Facet, FacetParams, HealthReport, HealthStatus, HttpAdapter, HttpMethod, IndexResult, InventoryChangedV1Payload, InventoryLowStockV1Payload, InventoryOutOfStockV1Payload, InventoryReleasedV1Payload, InventoryReservation, InventoryReservedV1Payload, Logger, MediaStorage, MiddlewareHandler, Money, NymbalEnvironment, NymbalEvent, NymbalEventMap, NymbalEventType, Order, OrderCancelledV1Payload, OrderDeliveredV1Payload, OrderFulfilledV1Payload, OrderHistoryEntry, OrderLineItem, OrderPaidV1Payload, OrderPartiallyRefundedV1Payload, OrderPayment, OrderPlacedV1Payload, OrderRefund, OrderRefundStatus, OrderRefundedV1Payload, OrderShippedV1Payload, OrderStatus, OrderStatusChangedV1Payload, PaginationParams, PayloadOf, PaymentCapture, PaymentCapturedV1Payload, PaymentClient, PaymentFailedV1Payload, PaymentIntent, PaymentIntentCreatedV1Payload, PaymentIntentParams, PaymentRefundedV1Payload, PaymentState, PaymentStatus, PaymentsAdapter, Product, ProductCreatedV1Payload, ProductDeletedV1Payload, ProductMedia, ProductPublishedV1Payload, ProductSnapshot, ProductSortKey, ProductStatus, ProductType, ProductUnpublishedV1Payload, ProductUpdatedV1Payload, ProviderPackageModule, PutOptions, QueryParams, QueryResult, RateLimitAction, RateLimitDecision, RateLimitRule, Recipe, RecipeValue, Refund, RefundOptions, RenderedEmail, RequestAuth, RequestContext, ResponseCookie, ResponseEnvelope, RetryPolicy, Review, ReviewModerationStatus, ReviewResult, ReviewSubmission, ReviewsAdapter, ReviewsResult, Role, RouteHandler, RouteOptions, SearchAdapter, SearchDocument, SearchParams, SearchResult, SecurityAdapter, SecurityConfigLike, SecurityState, SettingFieldView, SettingSourceName, SettingWriteResult, SettingsChangedV1Payload, SettingsListing, SettingsSlotView, ShippingAdapter, ShippingLabel, ShippingLabelParams, ShippingRate, ShippingRateParams, ShippingTracking, Slot, SortKeyCondition, SseEvent, StockAdjustment, StockAdjustmentReason, StockLevel, StockStatus, StoreCapabilities, StreamContext, StreamHandler, Subscription, SubscriptionConfig, TaxAdapter, TaxCalculationParams, TaxCalculationResult, TaxLine, TransactionalEmail, ValidationResult, Variant, VariantDimensions, VariantOption, VariantStatus, WebhookResult };
|
package/dist/index.js
CHANGED
|
@@ -142,6 +142,13 @@ var PRODUCT_SORT_KEYS = [
|
|
|
142
142
|
"name-desc"
|
|
143
143
|
];
|
|
144
144
|
var DEFAULT_PRODUCT_SORT = "newest";
|
|
145
|
+
var PRODUCT_SORT_LABELS = {
|
|
146
|
+
newest: "Newest",
|
|
147
|
+
"price-asc": "Price: Low to High",
|
|
148
|
+
"price-desc": "Price: High to Low",
|
|
149
|
+
"name-asc": "Name: A to Z",
|
|
150
|
+
"name-desc": "Name: Z to A"
|
|
151
|
+
};
|
|
145
152
|
|
|
146
153
|
// src/domain/order.ts
|
|
147
154
|
var ORDER_STATUS_TRANSITIONS = {
|
|
@@ -189,6 +196,7 @@ var EVT_ORDER_DELIVERED = "order.delivered.v1";
|
|
|
189
196
|
var EVT_ORDER_CANCELLED = "order.cancelled.v1";
|
|
190
197
|
var EVT_ORDER_REFUNDED = "order.refunded.v1";
|
|
191
198
|
var EVT_ORDER_PARTIALLY_REFUNDED = "order.partially_refunded.v1";
|
|
199
|
+
var EVT_ORDER_STATUS_CHANGED = "order.status_changed.v1";
|
|
192
200
|
var EVT_INVENTORY_CHANGED = "inventory.changed.v1";
|
|
193
201
|
var EVT_INVENTORY_RESERVED = "inventory.reserved.v1";
|
|
194
202
|
var EVT_INVENTORY_RELEASED = "inventory.released.v1";
|
|
@@ -204,10 +212,12 @@ var EVT_CART_ITEM_REMOVED = "cart.item_removed.v1";
|
|
|
204
212
|
var EVT_CART_UPDATED = "cart.updated.v1";
|
|
205
213
|
var EVT_CART_ABANDONED = "cart.abandoned.v1";
|
|
206
214
|
var EVT_CART_CONVERTED = "cart.converted.v1";
|
|
215
|
+
var EVT_PAYMENT_INTENT_CREATED = "payment.intent_created.v1";
|
|
207
216
|
var EVT_PAYMENT_CAPTURED = "payment.captured.v1";
|
|
208
217
|
var EVT_PAYMENT_FAILED = "payment.failed.v1";
|
|
209
218
|
var EVT_PAYMENT_REFUNDED = "payment.refunded.v1";
|
|
210
219
|
var EVT_ADAPTER_ERROR = "adapter.error.v1";
|
|
220
|
+
var EVT_SETTINGS_CHANGED = "settings.changed.v1";
|
|
211
221
|
var STANDARD_EVENT_TYPES = [
|
|
212
222
|
EVT_PRODUCT_CREATED,
|
|
213
223
|
EVT_PRODUCT_UPDATED,
|
|
@@ -225,6 +235,7 @@ var STANDARD_EVENT_TYPES = [
|
|
|
225
235
|
EVT_ORDER_CANCELLED,
|
|
226
236
|
EVT_ORDER_REFUNDED,
|
|
227
237
|
EVT_ORDER_PARTIALLY_REFUNDED,
|
|
238
|
+
EVT_ORDER_STATUS_CHANGED,
|
|
228
239
|
EVT_INVENTORY_CHANGED,
|
|
229
240
|
EVT_INVENTORY_RESERVED,
|
|
230
241
|
EVT_INVENTORY_RELEASED,
|
|
@@ -239,12 +250,229 @@ var STANDARD_EVENT_TYPES = [
|
|
|
239
250
|
EVT_CART_UPDATED,
|
|
240
251
|
EVT_CART_ABANDONED,
|
|
241
252
|
EVT_CART_CONVERTED,
|
|
253
|
+
EVT_PAYMENT_INTENT_CREATED,
|
|
242
254
|
EVT_PAYMENT_CAPTURED,
|
|
243
255
|
EVT_PAYMENT_FAILED,
|
|
244
256
|
EVT_PAYMENT_REFUNDED,
|
|
245
|
-
EVT_ADAPTER_ERROR
|
|
257
|
+
EVT_ADAPTER_ERROR,
|
|
258
|
+
EVT_SETTINGS_CHANGED
|
|
246
259
|
];
|
|
247
260
|
|
|
261
|
+
// src/adapters/adapter-base.ts
|
|
262
|
+
var HEALTH_CHECK_TIMEOUT_MS = 5e3;
|
|
263
|
+
function errorText(error) {
|
|
264
|
+
if (!(error instanceof Error)) return String(error);
|
|
265
|
+
if (error.message !== "") return error.message;
|
|
266
|
+
const inner = error.errors;
|
|
267
|
+
if (Array.isArray(inner) && inner.length > 0) return inner.map(errorText).join("; ");
|
|
268
|
+
const code = error.code;
|
|
269
|
+
return typeof code === "string" ? code : error.name;
|
|
270
|
+
}
|
|
271
|
+
var SECRET_MIN_LENGTH = 8;
|
|
272
|
+
function connectionSecrets(url, options = {}) {
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
parsed = new URL(url);
|
|
276
|
+
} catch {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const raw = [
|
|
280
|
+
...options.user === false ? [] : [parsed.username],
|
|
281
|
+
parsed.password,
|
|
282
|
+
...parsed.searchParams.values()
|
|
283
|
+
];
|
|
284
|
+
const decoded = raw.map((value) => {
|
|
285
|
+
try {
|
|
286
|
+
return decodeURIComponent(value);
|
|
287
|
+
} catch {
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return [.../* @__PURE__ */ new Set([...raw, ...decoded])].filter((value) => value.length >= SECRET_MIN_LENGTH);
|
|
292
|
+
}
|
|
293
|
+
function displayUrl(url) {
|
|
294
|
+
try {
|
|
295
|
+
const parsed = new URL(url);
|
|
296
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname === "/" ? "" : parsed.pathname}`;
|
|
297
|
+
} catch {
|
|
298
|
+
return "<unparsable url>";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function boundedHealthCheck(check, options = {}) {
|
|
302
|
+
const timeoutMs = options.timeoutMs ?? HEALTH_CHECK_TIMEOUT_MS;
|
|
303
|
+
const controller = new AbortController();
|
|
304
|
+
let timer;
|
|
305
|
+
const timedOut = new Promise((resolve) => {
|
|
306
|
+
timer = setTimeout(() => {
|
|
307
|
+
controller.abort();
|
|
308
|
+
resolve({ status: "down", detail: `timed out after ${timeoutMs} ms` });
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
});
|
|
311
|
+
const failed = (error) => {
|
|
312
|
+
let detail = errorText(error);
|
|
313
|
+
for (const secret of options.redact ?? []) {
|
|
314
|
+
if (secret !== "") detail = detail.split(secret).join("***");
|
|
315
|
+
}
|
|
316
|
+
return { status: "down", detail };
|
|
317
|
+
};
|
|
318
|
+
try {
|
|
319
|
+
const checked = Promise.resolve().then(() => check(controller.signal)).catch(failed);
|
|
320
|
+
return await Promise.race([checked, timedOut]);
|
|
321
|
+
} finally {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/adapters/document-order.ts
|
|
327
|
+
var collator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
328
|
+
function rankOf(value) {
|
|
329
|
+
if (typeof value === "number") return 0;
|
|
330
|
+
if (typeof value === "string") return 1;
|
|
331
|
+
if (value === null || value === void 0) return 3;
|
|
332
|
+
return 2;
|
|
333
|
+
}
|
|
334
|
+
function sign(n) {
|
|
335
|
+
return n > 0 ? 1 : n < 0 ? -1 : 0;
|
|
336
|
+
}
|
|
337
|
+
function compareOther(a, b) {
|
|
338
|
+
const ta = typeof a;
|
|
339
|
+
const tb = typeof b;
|
|
340
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
341
|
+
if (ta === "boolean") return Number(a) - Number(b);
|
|
342
|
+
const ja = JSON.stringify(a) ?? "";
|
|
343
|
+
const jb = JSON.stringify(b) ?? "";
|
|
344
|
+
return ja < jb ? -1 : ja > jb ? 1 : 0;
|
|
345
|
+
}
|
|
346
|
+
function compareDocumentValues(a, b) {
|
|
347
|
+
const ra = rankOf(a);
|
|
348
|
+
const rb = rankOf(b);
|
|
349
|
+
if (ra !== rb) return ra - rb;
|
|
350
|
+
switch (ra) {
|
|
351
|
+
case 0:
|
|
352
|
+
return sign(a - b);
|
|
353
|
+
case 1:
|
|
354
|
+
return sign(collator.compare(a, b));
|
|
355
|
+
case 2:
|
|
356
|
+
return sign(compareOther(a, b));
|
|
357
|
+
default:
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function matchesSortKey(value, condition) {
|
|
362
|
+
const target = condition.value;
|
|
363
|
+
switch (condition.operator) {
|
|
364
|
+
case "eq":
|
|
365
|
+
return value === target;
|
|
366
|
+
case "gt":
|
|
367
|
+
return compareDocumentValues(value, target) > 0;
|
|
368
|
+
case "lt":
|
|
369
|
+
return compareDocumentValues(value, target) < 0;
|
|
370
|
+
case "between": {
|
|
371
|
+
if (!Array.isArray(target) || target.length !== 2) return false;
|
|
372
|
+
const [low, high] = target;
|
|
373
|
+
return compareDocumentValues(value, low) >= 0 && compareDocumentValues(value, high) <= 0;
|
|
374
|
+
}
|
|
375
|
+
default:
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function documentValueAt(doc, path) {
|
|
380
|
+
let value = doc;
|
|
381
|
+
for (const part of path.split(".")) {
|
|
382
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
383
|
+
value = value[part];
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
var DEFAULT_QUERY_LIMIT = 50;
|
|
388
|
+
function orderOf(params) {
|
|
389
|
+
if (params.orderBy !== void 0) {
|
|
390
|
+
return { field: params.orderBy.field, direction: params.orderBy.direction };
|
|
391
|
+
}
|
|
392
|
+
const direction = params.ascending ?? true ? "asc" : "desc";
|
|
393
|
+
return { field: params.sortKey?.field ?? null, direction };
|
|
394
|
+
}
|
|
395
|
+
var isNullish = (value) => value === null || value === void 0;
|
|
396
|
+
function comparePositions(a, b, order) {
|
|
397
|
+
if (order.field !== null) {
|
|
398
|
+
const an = isNullish(a.value);
|
|
399
|
+
const bn = isNullish(b.value);
|
|
400
|
+
if (an !== bn) return an ? 1 : -1;
|
|
401
|
+
let c2 = an ? 0 : compareDocumentValues(a.value, b.value);
|
|
402
|
+
if (order.direction === "desc") c2 = -c2;
|
|
403
|
+
if (c2 !== 0) return c2;
|
|
404
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
405
|
+
}
|
|
406
|
+
const c = a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
407
|
+
return order.direction === "desc" ? -c : c;
|
|
408
|
+
}
|
|
409
|
+
function cursorScope(order, params) {
|
|
410
|
+
return JSON.stringify([order.field, order.direction, params.partitionKey]);
|
|
411
|
+
}
|
|
412
|
+
function decodeCursor(cursor, scope) {
|
|
413
|
+
let decoded;
|
|
414
|
+
try {
|
|
415
|
+
decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
416
|
+
} catch {
|
|
417
|
+
decoded = null;
|
|
418
|
+
}
|
|
419
|
+
const c = decoded;
|
|
420
|
+
if (c === null || typeof c !== "object" || typeof c.o !== "string" || typeof c.k !== "string") {
|
|
421
|
+
throw new ValidationError("The cursor is not valid", { context: { cursor } });
|
|
422
|
+
}
|
|
423
|
+
if (c.o !== scope) {
|
|
424
|
+
throw new ValidationError("The cursor was issued under another order", { context: { cursor } });
|
|
425
|
+
}
|
|
426
|
+
return { key: c.k, value: c.v ?? null };
|
|
427
|
+
}
|
|
428
|
+
function queryDocuments(documents, params) {
|
|
429
|
+
const limit = params.limit ?? DEFAULT_QUERY_LIMIT;
|
|
430
|
+
const order = orderOf(params);
|
|
431
|
+
const scope = cursorScope(order, params);
|
|
432
|
+
const after = params.cursor ? decodeCursor(params.cursor, scope) : null;
|
|
433
|
+
const { partitionKey, sortKey } = params;
|
|
434
|
+
const matches = [];
|
|
435
|
+
for (const [key, doc] of documents) {
|
|
436
|
+
const record = doc ?? {};
|
|
437
|
+
if (partitionKey.value !== "*" && record[partitionKey.field] !== partitionKey.value) continue;
|
|
438
|
+
if (sortKey !== void 0 && !matchesSortKey(record[sortKey.field], sortKey)) continue;
|
|
439
|
+
const value = order.field === null ? null : documentValueAt(record, order.field) ?? null;
|
|
440
|
+
const position = { key, value };
|
|
441
|
+
if (after !== null && comparePositions(position, after, order) <= 0) continue;
|
|
442
|
+
matches.push({ position, doc });
|
|
443
|
+
}
|
|
444
|
+
matches.sort((a, b) => comparePositions(a.position, b.position, order));
|
|
445
|
+
const page = matches.slice(0, limit);
|
|
446
|
+
const last = page[page.length - 1];
|
|
447
|
+
const nextCursor = matches.length > limit && last !== void 0 ? Buffer.from(
|
|
448
|
+
JSON.stringify({ o: scope, k: last.position.key, v: last.position.value }),
|
|
449
|
+
"utf8"
|
|
450
|
+
).toString("base64url") : null;
|
|
451
|
+
return { items: page.map((m) => m.doc), nextCursor };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/adapters/http.ts
|
|
455
|
+
var CORRELATION_ID_HEADER = "x-correlation-id";
|
|
456
|
+
var CORRELATION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
457
|
+
function isValidCorrelationId(s) {
|
|
458
|
+
return CORRELATION_ID.test(s);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/adapters/security.ts
|
|
462
|
+
function buildSecurityHeaders(security) {
|
|
463
|
+
const headers = {};
|
|
464
|
+
if (security.headers.hsts) {
|
|
465
|
+
headers["strict-transport-security"] = "max-age=31536000; includeSubDomains";
|
|
466
|
+
}
|
|
467
|
+
const csp = security.headers.contentSecurityPolicy;
|
|
468
|
+
const cspValue = csp === "strict" ? "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" : csp === "relaxed" ? "default-src 'self' https:; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'" : csp === "off" ? null : csp;
|
|
469
|
+
if (cspValue) headers["content-security-policy"] = cspValue;
|
|
470
|
+
headers["referrer-policy"] = security.headers.referrerPolicy;
|
|
471
|
+
headers["x-frame-options"] = security.headers.xFrameOptions === "deny" ? "DENY" : "SAMEORIGIN";
|
|
472
|
+
headers["x-content-type-options"] = "nosniff";
|
|
473
|
+
return headers;
|
|
474
|
+
}
|
|
475
|
+
|
|
248
476
|
// src/adapters/email-templates.ts
|
|
249
477
|
function render(template, vars) {
|
|
250
478
|
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
|
|
@@ -301,12 +529,33 @@ function renderEmailTemplate(template, vars) {
|
|
|
301
529
|
const html = `<pre>${text.replace(/</g, "<")}</pre>`;
|
|
302
530
|
return { subject, text, html };
|
|
303
531
|
}
|
|
532
|
+
|
|
533
|
+
// src/stock-level.ts
|
|
534
|
+
var LOW_STOCK_BELOW = 5;
|
|
535
|
+
function stockLevel(stock) {
|
|
536
|
+
if (stock <= 0) return "out";
|
|
537
|
+
return stock < LOW_STOCK_BELOW ? "low" : "in";
|
|
538
|
+
}
|
|
539
|
+
var STOCK_LABELS = {
|
|
540
|
+
out: "Out of Stock",
|
|
541
|
+
low: "Low Stock",
|
|
542
|
+
in: "In Stock"
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// src/audit.ts
|
|
546
|
+
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
547
|
+
var IDEMPOTENT_REPLAYED_HEADER = "idempotent-replayed";
|
|
548
|
+
function isValidIdempotencyKey(value) {
|
|
549
|
+
return /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
550
|
+
}
|
|
304
551
|
export {
|
|
305
552
|
AdapterError,
|
|
306
553
|
AuthError,
|
|
554
|
+
CORRELATION_ID_HEADER,
|
|
307
555
|
CheckoutError,
|
|
308
556
|
ConfigError,
|
|
309
557
|
DEFAULT_PRODUCT_SORT,
|
|
558
|
+
DEFAULT_QUERY_LIMIT,
|
|
310
559
|
EVT_ADAPTER_ERROR,
|
|
311
560
|
EVT_CART_ABANDONED,
|
|
312
561
|
EVT_CART_CONVERTED,
|
|
@@ -334,34 +583,55 @@ export {
|
|
|
334
583
|
EVT_ORDER_PLACED,
|
|
335
584
|
EVT_ORDER_REFUNDED,
|
|
336
585
|
EVT_ORDER_SHIPPED,
|
|
586
|
+
EVT_ORDER_STATUS_CHANGED,
|
|
337
587
|
EVT_PAYMENT_CAPTURED,
|
|
338
588
|
EVT_PAYMENT_FAILED,
|
|
589
|
+
EVT_PAYMENT_INTENT_CREATED,
|
|
339
590
|
EVT_PAYMENT_REFUNDED,
|
|
340
591
|
EVT_PRODUCT_CREATED,
|
|
341
592
|
EVT_PRODUCT_DELETED,
|
|
342
593
|
EVT_PRODUCT_PUBLISHED,
|
|
343
594
|
EVT_PRODUCT_UNPUBLISHED,
|
|
344
595
|
EVT_PRODUCT_UPDATED,
|
|
596
|
+
EVT_SETTINGS_CHANGED,
|
|
597
|
+
HEALTH_CHECK_TIMEOUT_MS,
|
|
345
598
|
HttpError,
|
|
599
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
600
|
+
IDEMPOTENT_REPLAYED_HEADER,
|
|
346
601
|
NotFoundError,
|
|
347
602
|
NymbalError,
|
|
348
603
|
ORDER_STATUS_TRANSITIONS,
|
|
349
604
|
OrderConflictError,
|
|
350
605
|
PRODUCT_SORT_KEYS,
|
|
606
|
+
PRODUCT_SORT_LABELS,
|
|
351
607
|
PaymentError,
|
|
608
|
+
SECRET_MIN_LENGTH,
|
|
352
609
|
STANDARD_EVENT_TYPES,
|
|
610
|
+
STOCK_LABELS,
|
|
353
611
|
StatusTransitionError,
|
|
354
612
|
ValidationError,
|
|
355
613
|
add,
|
|
356
614
|
allocate,
|
|
357
615
|
assertMinorUnits,
|
|
358
616
|
assertOrderTransition,
|
|
617
|
+
boundedHealthCheck,
|
|
618
|
+
buildSecurityHeaders,
|
|
359
619
|
canTransitionOrder,
|
|
620
|
+
compareDocumentValues,
|
|
621
|
+
connectionSecrets,
|
|
622
|
+
displayUrl,
|
|
623
|
+
documentValueAt,
|
|
624
|
+
errorText,
|
|
625
|
+
isValidCorrelationId,
|
|
626
|
+
isValidIdempotencyKey,
|
|
627
|
+
matchesSortKey,
|
|
360
628
|
money,
|
|
361
629
|
mulBy,
|
|
362
630
|
paymentState,
|
|
631
|
+
queryDocuments,
|
|
363
632
|
refundableMinor,
|
|
364
633
|
renderEmailTemplate,
|
|
634
|
+
stockLevel,
|
|
365
635
|
sub,
|
|
366
636
|
sumMoney
|
|
367
637
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/domain/money.ts","../src/domain/product.ts","../src/domain/order.ts","../src/events/constants.ts","../src/adapters/email-templates.ts"],"sourcesContent":["export type ErrorContext = Record<string, unknown>\n\nexport class NymbalError extends Error {\n readonly code: string\n readonly context: ErrorContext\n\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined)\n this.name = this.constructor.name\n this.code = code\n this.context = options.context ?? {}\n }\n\n toJSON(): { name: string; code: string; message: string; context: ErrorContext } {\n return { name: this.name, code: this.code, message: this.message, context: this.context }\n }\n}\n\nexport class ConfigError extends NymbalError {\n constructor(\n message: string,\n options: {\n cause?: unknown\n context?: ErrorContext\n code?: `config.${string}` | `adapter.${string}`\n } = {},\n ) {\n super(options.code ?? 'config.invalid', message, options)\n }\n}\n\nexport class ValidationError extends NymbalError {\n constructor(message: string, options: { cause?: unknown; context?: ErrorContext } = {}) {\n super('validation.failed', message, options)\n }\n}\n\nexport class NotFoundError extends NymbalError {\n constructor(\n resource: string,\n identifier: unknown,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super('not_found', `${resource} not found: ${String(identifier)}`, {\n ...options,\n context: { resource, identifier, ...(options.context ?? {}) },\n })\n }\n}\n\nexport class AdapterError extends NymbalError {\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`adapter.${code}`, message, options)\n }\n}\n\nexport class HttpError extends NymbalError {\n readonly status: number\n\n constructor(\n status: number,\n message: string,\n options: { cause?: unknown; context?: ErrorContext; code?: string } = {},\n ) {\n super(options.code ?? `http.${status}`, message, options)\n this.status = status\n }\n}\n\nexport class AuthError extends HttpError {\n constructor(\n code: 'unauthorized' | 'forbidden' | 'invalid_credentials' | 'token_expired' | 'token_invalid',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n const status = code === 'forbidden' ? 403 : 401\n super(status, message, { ...options, code: `auth.${code}` })\n }\n}\n\nexport class PaymentError extends NymbalError {\n constructor(\n code:\n | 'intent_failed'\n | 'capture_failed'\n // A definite refusal: no money moved.\n | 'refund_rejected'\n // An outcome the shop cannot know (a timeout, a provider fault): money may have moved.\n | 'refund_unknown'\n | 'cancel_refused'\n | 'webhook_invalid'\n | 'not_configured',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`payment.${code}`, message, options)\n }\n}\n\n/** A command the order's money does not allow (confirm unpaid, cancel paid, refund beyond the capture). */\nexport class OrderConflictError extends NymbalError {\n constructor(\n code:\n | 'order.unpaid'\n | 'order.paid'\n | 'order.stock_unavailable'\n | 'order.payment_in_progress'\n | 'refund.not_captured'\n | 'refund.exceeds_captured'\n | 'refund.request_reused',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(code, message, options)\n }\n}\n\nexport class CheckoutError extends NymbalError {\n constructor(\n code:\n | 'cart_empty'\n | 'in_progress'\n | 'payment_in_progress'\n | 'variant_missing'\n | 'out_of_stock'\n | 'reservation_failed'\n | 'finalize_failed',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`checkout.${code}`, message, options)\n }\n}\n\nexport class StatusTransitionError extends NymbalError {\n constructor(\n entity: string,\n fromStatus: string,\n toStatus: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(\n 'status.invalid_transition',\n `Invalid ${entity} status transition: ${fromStatus} → ${toStatus}`,\n {\n ...options,\n context: { entity, fromStatus, toStatus, ...(options.context ?? {}) },\n },\n )\n }\n}\n","import { ValidationError } from '../errors.js'\n\nexport interface Money {\n amount: number\n currency: string\n}\n\nexport function money(amount: number, currency: string): Money {\n assertMinorUnits(amount)\n return { amount, currency }\n}\n\nexport function assertMinorUnits(amount: number): void {\n if (!Number.isInteger(amount)) {\n throw new ValidationError(`Monetary amount must be an integer (minor units), got ${amount}`)\n }\n}\n\nfunction assertSameCurrency(a: Money, b: Money): void {\n if (a.currency !== b.currency) {\n throw new ValidationError(\n `Currency mismatch: ${a.currency} vs ${b.currency}`,\n )\n }\n}\n\nexport function add(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount + b.amount, currency: a.currency }\n}\n\nexport function sub(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount - b.amount, currency: a.currency }\n}\n\nexport function mulBy(m: Money, scalar: number): Money {\n if (!Number.isFinite(scalar)) {\n throw new ValidationError(`Scalar must be finite, got ${scalar}`)\n }\n return { amount: Math.round(m.amount * scalar), currency: m.currency }\n}\n\nexport function sumMoney(values: Money[], currency: string): Money {\n const total = values.reduce((acc, m) => {\n assertSameCurrency(m, { amount: 0, currency })\n return acc + m.amount\n }, 0)\n return { amount: total, currency }\n}\n\nexport function allocate(m: Money, weights: number[]): Money[] {\n if (weights.length === 0) return []\n const totalWeight = weights.reduce((s, w) => s + w, 0)\n if (totalWeight <= 0) {\n throw new ValidationError('allocate: sum of weights must be > 0')\n }\n const portions = weights.map((w) => Math.floor((m.amount * w) / totalWeight))\n let remainder = m.amount - portions.reduce((s, p) => s + p, 0)\n const result = portions.slice()\n let idx = 0\n while (remainder !== 0 && idx < result.length) {\n const delta = remainder > 0 ? 1 : -1\n result[idx] = (result[idx] ?? 0) + delta\n remainder -= delta\n idx += 1\n }\n return result.map((amount) => ({ amount, currency: m.currency }))\n}\n","export type ProductStatus = 'draft' | 'active' | 'archived'\nexport type ProductType = 'simple' | 'variable'\nexport type VariantStatus = 'active' | 'inactive'\n\nexport interface ProductMedia {\n url: string\n altText: string\n position: number\n}\n\nexport interface VariantOption {\n name: string\n value: string\n}\n\nexport interface VariantDimensions {\n lengthMm?: number\n widthMm?: number\n heightMm?: number\n}\n\nexport interface Variant {\n id: string\n productId: string\n sku: string\n name: string\n priceMinor: number\n compareAtPriceMinor: number | null\n weightGrams: number | null\n dimensions: VariantDimensions | null\n stock: number\n lowStockThreshold: number\n options: VariantOption[]\n status: VariantStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface Product {\n id: string\n slug: string\n name: string\n description: string\n shortDescription: string\n status: ProductStatus\n type: ProductType\n seoTitle: string\n seoDescription: string\n media: ProductMedia[]\n metadata: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/** The orders a product listing can be requested in; ties break by slug, ascending. */\nexport const PRODUCT_SORT_KEYS = [\n 'newest',\n 'price-asc',\n 'price-desc',\n 'name-asc',\n 'name-desc',\n] as const\nexport type ProductSortKey = (typeof PRODUCT_SORT_KEYS)[number]\nexport const DEFAULT_PRODUCT_SORT: ProductSortKey = 'newest'\n","import { StatusTransitionError } from '../errors.js'\n\nexport type OrderStatus =\n | 'pending'\n | 'confirmed'\n | 'processing'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'refunded'\n | 'partially_refunded'\n\n// A captured order is refunded from any status between confirmed and delivered; a partial refund leaves room for more.\nexport const ORDER_STATUS_TRANSITIONS: Record<OrderStatus, readonly OrderStatus[]> = {\n pending: ['confirmed', 'cancelled'],\n confirmed: ['processing', 'cancelled', 'refunded', 'partially_refunded'],\n processing: ['shipped', 'cancelled', 'refunded', 'partially_refunded'],\n shipped: ['delivered', 'refunded', 'partially_refunded'],\n delivered: ['refunded', 'partially_refunded'],\n cancelled: [],\n refunded: [],\n partially_refunded: ['refunded'],\n}\n\nexport function canTransitionOrder(from: OrderStatus, to: OrderStatus): boolean {\n return ORDER_STATUS_TRANSITIONS[from].includes(to)\n}\n\nexport function assertOrderTransition(from: OrderStatus, to: OrderStatus): void {\n if (!canTransitionOrder(from, to)) {\n throw new StatusTransitionError('order', from, to)\n }\n}\n\nexport interface Address {\n firstName: string\n lastName: string\n company?: string\n addressLine1: string\n addressLine2?: string\n city: string\n region: string\n postalCode: string\n country: string\n phone?: string\n}\n\nexport interface OrderLineItem {\n variantId: string\n productId: string\n productName: string\n variantName: string\n sku: string\n qty: number\n unitPriceMinor: number\n lineSubtotalMinor: number\n lineTaxMinor: number\n lineTotalMinor: number\n imageUrl: string\n}\n\nexport interface Order {\n id: string\n orderNumber: string\n sequence: number\n customerId: string | null\n status: OrderStatus\n email: string\n billingAddress: Address\n shippingAddress: Address\n lineItems: OrderLineItem[]\n subtotalMinor: number\n taxTotalMinor: number\n shippingTotalMinor: number\n discountTotalMinor: number\n totalMinor: number\n currency: string\n notes: string\n metadata: Record<string, unknown>\n paymentIntentId: string | null\n /** Money taken for the order, in minor units of its currency; 0 while nothing is captured. */\n capturedMinor: number\n capturedAt: string | null\n /** Σ of the order's succeeded refunds (or, for an import or a pre-refund-table order, what was refunded then). */\n refundedMinor: number\n captureMethod: CaptureMethod | null\n createdAt: string\n updatedAt: string\n}\n\n/** Who took the money: the payments provider, the merchant recording an offline payment, or an imported history. */\nexport type CaptureMethod = 'provider' | 'manual' | 'import'\n\nexport type OrderRefundStatus = 'pending' | 'succeeded' | 'failed'\n\n/** One refund of an order. `pending` until the provider's answer settles it, and counted against what is refundable. */\nexport interface OrderRefund {\n id: string\n orderId: string\n amountMinor: number\n currency: string\n providerRefundId: string | null\n status: OrderRefundStatus\n reason: string | null\n actor: string\n createdAt: string\n}\n\nexport type PaymentState = 'unpaid' | 'paid' | 'partially_refunded' | 'refunded'\n\n/** What the admin shows about an order's money: derived from the stored amounts and refunds alone. */\nexport interface OrderPayment {\n state: PaymentState\n capturedMinor: number\n refundedMinor: number\n refundableMinor: number\n currency: string\n refunds: OrderRefund[]\n}\n\nexport function paymentState(order: Pick<Order, 'capturedMinor' | 'refundedMinor'>): PaymentState {\n if (order.capturedMinor <= 0) return 'unpaid'\n if (order.refundedMinor <= 0) return 'paid'\n return order.refundedMinor < order.capturedMinor ? 'partially_refunded' : 'refunded'\n}\n\n/** What can still be refunded: the capture less what was refunded and what is still pending with the provider. */\nexport function refundableMinor(\n order: Pick<Order, 'capturedMinor' | 'refundedMinor'>,\n refunds: ReadonlyArray<Pick<OrderRefund, 'amountMinor' | 'status'>>,\n): number {\n const pending = refunds\n .filter((r) => r.status === 'pending')\n .reduce((sum, r) => sum + r.amountMinor, 0)\n return Math.max(0, order.capturedMinor - order.refundedMinor - pending)\n}\n\nexport interface OrderHistoryEntry {\n id: string\n orderId: string\n fromStatus: OrderStatus | null\n toStatus: OrderStatus\n actor: string\n note: string\n timestamp: string\n}\n","// Product\nexport const EVT_PRODUCT_CREATED = 'product.created.v1'\nexport const EVT_PRODUCT_UPDATED = 'product.updated.v1'\nexport const EVT_PRODUCT_DELETED = 'product.deleted.v1'\nexport const EVT_PRODUCT_PUBLISHED = 'product.published.v1'\nexport const EVT_PRODUCT_UNPUBLISHED = 'product.unpublished.v1'\n\n// Category\nexport const EVT_CATEGORY_CREATED = 'category.created.v1'\nexport const EVT_CATEGORY_UPDATED = 'category.updated.v1'\nexport const EVT_CATEGORY_DELETED = 'category.deleted.v1'\n\n// Order\nexport const EVT_ORDER_PLACED = 'order.placed.v1'\nexport const EVT_ORDER_PAID = 'order.paid.v1'\nexport const EVT_ORDER_FULFILLED = 'order.fulfilled.v1'\nexport const EVT_ORDER_SHIPPED = 'order.shipped.v1'\nexport const EVT_ORDER_DELIVERED = 'order.delivered.v1'\nexport const EVT_ORDER_CANCELLED = 'order.cancelled.v1'\nexport const EVT_ORDER_REFUNDED = 'order.refunded.v1'\nexport const EVT_ORDER_PARTIALLY_REFUNDED = 'order.partially_refunded.v1'\n\n// Inventory\nexport const EVT_INVENTORY_CHANGED = 'inventory.changed.v1'\nexport const EVT_INVENTORY_RESERVED = 'inventory.reserved.v1'\nexport const EVT_INVENTORY_RELEASED = 'inventory.released.v1'\nexport const EVT_INVENTORY_LOW_STOCK = 'inventory.low_stock.v1'\nexport const EVT_INVENTORY_OUT_OF_STOCK = 'inventory.out_of_stock.v1'\n\n// Customer\nexport const EVT_CUSTOMER_CREATED = 'customer.created.v1'\nexport const EVT_CUSTOMER_UPDATED = 'customer.updated.v1'\nexport const EVT_CUSTOMER_DELETED = 'customer.deleted.v1'\n// Legacy alias (kept so Prompt 1 seed code continues to compile).\nexport const EVT_CUSTOMER_REGISTERED = EVT_CUSTOMER_CREATED\n\n// Cart\nexport const EVT_CART_CREATED = 'cart.created.v1'\nexport const EVT_CART_ITEM_ADDED = 'cart.item_added.v1'\nexport const EVT_CART_ITEM_REMOVED = 'cart.item_removed.v1'\nexport const EVT_CART_UPDATED = 'cart.updated.v1'\nexport const EVT_CART_ABANDONED = 'cart.abandoned.v1'\nexport const EVT_CART_CONVERTED = 'cart.converted.v1'\n\n// Payment\nexport const EVT_PAYMENT_CAPTURED = 'payment.captured.v1'\nexport const EVT_PAYMENT_FAILED = 'payment.failed.v1'\nexport const EVT_PAYMENT_REFUNDED = 'payment.refunded.v1'\n\n// System\nexport const EVT_ADAPTER_ERROR = 'adapter.error.v1'\n\nexport const STANDARD_EVENT_TYPES = [\n EVT_PRODUCT_CREATED,\n EVT_PRODUCT_UPDATED,\n EVT_PRODUCT_DELETED,\n EVT_PRODUCT_PUBLISHED,\n EVT_PRODUCT_UNPUBLISHED,\n EVT_CATEGORY_CREATED,\n EVT_CATEGORY_UPDATED,\n EVT_CATEGORY_DELETED,\n EVT_ORDER_PLACED,\n EVT_ORDER_PAID,\n EVT_ORDER_FULFILLED,\n EVT_ORDER_SHIPPED,\n EVT_ORDER_DELIVERED,\n EVT_ORDER_CANCELLED,\n EVT_ORDER_REFUNDED,\n EVT_ORDER_PARTIALLY_REFUNDED,\n EVT_INVENTORY_CHANGED,\n EVT_INVENTORY_RESERVED,\n EVT_INVENTORY_RELEASED,\n EVT_INVENTORY_LOW_STOCK,\n EVT_INVENTORY_OUT_OF_STOCK,\n EVT_CUSTOMER_CREATED,\n EVT_CUSTOMER_UPDATED,\n EVT_CUSTOMER_DELETED,\n EVT_CART_CREATED,\n EVT_CART_ITEM_ADDED,\n EVT_CART_ITEM_REMOVED,\n EVT_CART_UPDATED,\n EVT_CART_ABANDONED,\n EVT_CART_CONVERTED,\n EVT_PAYMENT_CAPTURED,\n EVT_PAYMENT_FAILED,\n EVT_PAYMENT_REFUNDED,\n EVT_ADAPTER_ERROR,\n] as const\n","// The kernel's transactional email templates, shared by every email adapter (the console one and the SMTP package).\nexport interface RenderedEmail {\n subject: string\n text: string\n html: string\n}\n\nfunction render(template: string, vars: Record<string, unknown>): string {\n return template.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_, path: string) => {\n const parts = path.split('.')\n let cur: unknown = vars\n for (const p of parts) {\n if (cur && typeof cur === 'object' && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p]\n } else {\n return ''\n }\n }\n return cur == null ? '' : String(cur)\n })\n}\n\nconst TEMPLATES: Record<string, { subject: string; body: string }> = {\n 'order-placed': {\n subject: 'Order {{orderNumber}} confirmed',\n body: `Hi {{customerName}},\\n\\nThank you for your order {{orderNumber}}. Total: {{totalFormatted}}.\\n\\nWe'll let you know when it ships.`,\n },\n 'order-shipped': {\n subject: 'Order {{orderNumber}} is on its way',\n body: `Hi {{customerName}},\\n\\nYour order {{orderNumber}} has shipped via {{carrier}}.\\nTracking: {{trackingNumber}}`,\n },\n welcome: {\n subject: 'Welcome to {{storeName}}',\n body: `Welcome {{customerName}}! Your account is ready.`,\n },\n 'cart-abandoned': {\n subject: \"Don't forget your cart at {{storeName}}\",\n body: `Hi,\\n\\nYou left items in your cart. Pick up where you left off: {{resumeUrl}}`,\n },\n}\n\nexport function renderEmailTemplate(\n template: string,\n vars: Record<string, unknown>,\n): RenderedEmail {\n const t = TEMPLATES[template]\n if (!t) {\n return {\n subject: `[nymbal] ${template}`,\n text: JSON.stringify(vars),\n html: `<pre>${JSON.stringify(vars, null, 2)}</pre>`,\n }\n }\n const subject = render(t.subject, vars)\n const text = render(t.body, vars)\n const html = `<pre>${text.replace(/</g, '<')}</pre>`\n return { subject, text, html }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/domain/money.ts","../src/domain/product.ts","../src/domain/order.ts","../src/events/constants.ts","../src/adapters/adapter-base.ts","../src/adapters/document-order.ts","../src/adapters/http.ts","../src/adapters/security.ts","../src/adapters/email-templates.ts","../src/stock-level.ts","../src/audit.ts"],"sourcesContent":["export type ErrorContext = Record<string, unknown>\n\nexport class NymbalError extends Error {\n readonly code: string\n readonly context: ErrorContext\n\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined)\n this.name = this.constructor.name\n this.code = code\n this.context = options.context ?? {}\n }\n\n toJSON(): { name: string; code: string; message: string; context: ErrorContext } {\n return { name: this.name, code: this.code, message: this.message, context: this.context }\n }\n}\n\nexport class ConfigError extends NymbalError {\n constructor(\n message: string,\n options: {\n cause?: unknown\n context?: ErrorContext\n code?: `config.${string}` | `adapter.${string}`\n } = {},\n ) {\n super(options.code ?? 'config.invalid', message, options)\n }\n}\n\nexport class ValidationError extends NymbalError {\n constructor(message: string, options: { cause?: unknown; context?: ErrorContext } = {}) {\n super('validation.failed', message, options)\n }\n}\n\nexport class NotFoundError extends NymbalError {\n constructor(\n resource: string,\n identifier: unknown,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super('not_found', `${resource} not found: ${String(identifier)}`, {\n ...options,\n context: { resource, identifier, ...(options.context ?? {}) },\n })\n }\n}\n\nexport class AdapterError extends NymbalError {\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`adapter.${code}`, message, options)\n }\n}\n\nexport class HttpError extends NymbalError {\n readonly status: number\n\n constructor(\n status: number,\n message: string,\n options: { cause?: unknown; context?: ErrorContext; code?: string } = {},\n ) {\n super(options.code ?? `http.${status}`, message, options)\n this.status = status\n }\n}\n\nexport class AuthError extends HttpError {\n constructor(\n code: 'unauthorized' | 'forbidden' | 'invalid_credentials' | 'token_expired' | 'token_invalid',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n const status = code === 'forbidden' ? 403 : 401\n super(status, message, { ...options, code: `auth.${code}` })\n }\n}\n\nexport class PaymentError extends NymbalError {\n constructor(\n code:\n | 'intent_failed'\n | 'capture_failed'\n // A definite refusal: no money moved.\n | 'refund_rejected'\n // An outcome the shop cannot know (a timeout, a provider fault): money may have moved.\n | 'refund_unknown'\n | 'cancel_refused'\n | 'webhook_invalid'\n | 'not_configured',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`payment.${code}`, message, options)\n }\n}\n\n/** A command the order's money does not allow (confirm unpaid, cancel paid, refund beyond the capture). */\nexport class OrderConflictError extends NymbalError {\n constructor(\n code:\n | 'order.unpaid'\n | 'order.paid'\n | 'order.stock_unavailable'\n | 'order.payment_in_progress'\n | 'refund.not_captured'\n | 'refund.exceeds_captured'\n | 'refund.request_reused',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(code, message, options)\n }\n}\n\nexport class CheckoutError extends NymbalError {\n constructor(\n code:\n | 'cart_empty'\n | 'in_progress'\n | 'payment_in_progress'\n | 'variant_missing'\n | 'out_of_stock'\n | 'reservation_failed'\n | 'finalize_failed',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`checkout.${code}`, message, options)\n }\n}\n\nexport class StatusTransitionError extends NymbalError {\n constructor(\n entity: string,\n fromStatus: string,\n toStatus: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(\n 'status.invalid_transition',\n `Invalid ${entity} status transition: ${fromStatus} → ${toStatus}`,\n {\n ...options,\n context: { entity, fromStatus, toStatus, ...(options.context ?? {}) },\n },\n )\n }\n}\n","import { ValidationError } from '../errors.js'\n\nexport interface Money {\n amount: number\n currency: string\n}\n\nexport function money(amount: number, currency: string): Money {\n assertMinorUnits(amount)\n return { amount, currency }\n}\n\nexport function assertMinorUnits(amount: number): void {\n if (!Number.isInteger(amount)) {\n throw new ValidationError(`Monetary amount must be an integer (minor units), got ${amount}`)\n }\n}\n\nfunction assertSameCurrency(a: Money, b: Money): void {\n if (a.currency !== b.currency) {\n throw new ValidationError(\n `Currency mismatch: ${a.currency} vs ${b.currency}`,\n )\n }\n}\n\nexport function add(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount + b.amount, currency: a.currency }\n}\n\nexport function sub(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount - b.amount, currency: a.currency }\n}\n\nexport function mulBy(m: Money, scalar: number): Money {\n if (!Number.isFinite(scalar)) {\n throw new ValidationError(`Scalar must be finite, got ${scalar}`)\n }\n return { amount: Math.round(m.amount * scalar), currency: m.currency }\n}\n\nexport function sumMoney(values: Money[], currency: string): Money {\n const total = values.reduce((acc, m) => {\n assertSameCurrency(m, { amount: 0, currency })\n return acc + m.amount\n }, 0)\n return { amount: total, currency }\n}\n\nexport function allocate(m: Money, weights: number[]): Money[] {\n if (weights.length === 0) return []\n const totalWeight = weights.reduce((s, w) => s + w, 0)\n if (totalWeight <= 0) {\n throw new ValidationError('allocate: sum of weights must be > 0')\n }\n const portions = weights.map((w) => Math.floor((m.amount * w) / totalWeight))\n let remainder = m.amount - portions.reduce((s, p) => s + p, 0)\n const result = portions.slice()\n let idx = 0\n while (remainder !== 0 && idx < result.length) {\n const delta = remainder > 0 ? 1 : -1\n result[idx] = (result[idx] ?? 0) + delta\n remainder -= delta\n idx += 1\n }\n return result.map((amount) => ({ amount, currency: m.currency }))\n}\n","export type ProductStatus = 'draft' | 'active' | 'archived'\nexport type ProductType = 'simple' | 'variable'\nexport type VariantStatus = 'active' | 'inactive'\n\nexport interface ProductMedia {\n url: string\n altText: string\n position: number\n}\n\nexport interface VariantOption {\n name: string\n value: string\n}\n\nexport interface VariantDimensions {\n lengthMm?: number\n widthMm?: number\n heightMm?: number\n}\n\nexport interface Variant {\n id: string\n productId: string\n sku: string\n name: string\n priceMinor: number\n compareAtPriceMinor: number | null\n weightGrams: number | null\n dimensions: VariantDimensions | null\n stock: number\n lowStockThreshold: number\n options: VariantOption[]\n status: VariantStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface Product {\n id: string\n slug: string\n name: string\n description: string\n shortDescription: string\n status: ProductStatus\n type: ProductType\n seoTitle: string\n seoDescription: string\n media: ProductMedia[]\n metadata: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/** The orders a product listing can be requested in; ties break by slug, ascending. */\nexport const PRODUCT_SORT_KEYS = [\n 'newest',\n 'price-asc',\n 'price-desc',\n 'name-asc',\n 'name-desc',\n] as const\nexport type ProductSortKey = (typeof PRODUCT_SORT_KEYS)[number]\nexport const DEFAULT_PRODUCT_SORT: ProductSortKey = 'newest'\n/** What a storefront shows for each sort. */\nexport const PRODUCT_SORT_LABELS: Readonly<Record<ProductSortKey, string>> = {\n newest: 'Newest',\n 'price-asc': 'Price: Low to High',\n 'price-desc': 'Price: High to Low',\n 'name-asc': 'Name: A to Z',\n 'name-desc': 'Name: Z to A',\n}\n","import { StatusTransitionError } from '../errors.js'\n\nexport type OrderStatus =\n | 'pending'\n | 'confirmed'\n | 'processing'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'refunded'\n | 'partially_refunded'\n\n// A captured order is refunded from any status between confirmed and delivered; a partial refund leaves room for more.\nexport const ORDER_STATUS_TRANSITIONS: Record<OrderStatus, readonly OrderStatus[]> = {\n pending: ['confirmed', 'cancelled'],\n confirmed: ['processing', 'cancelled', 'refunded', 'partially_refunded'],\n processing: ['shipped', 'cancelled', 'refunded', 'partially_refunded'],\n shipped: ['delivered', 'refunded', 'partially_refunded'],\n delivered: ['refunded', 'partially_refunded'],\n cancelled: [],\n refunded: [],\n partially_refunded: ['refunded'],\n}\n\nexport function canTransitionOrder(from: OrderStatus, to: OrderStatus): boolean {\n return ORDER_STATUS_TRANSITIONS[from].includes(to)\n}\n\nexport function assertOrderTransition(from: OrderStatus, to: OrderStatus): void {\n if (!canTransitionOrder(from, to)) {\n throw new StatusTransitionError('order', from, to)\n }\n}\n\nexport interface Address {\n firstName: string\n lastName: string\n company?: string\n addressLine1: string\n addressLine2?: string\n city: string\n region: string\n postalCode: string\n country: string\n phone?: string\n}\n\nexport interface OrderLineItem {\n variantId: string\n productId: string\n productName: string\n variantName: string\n sku: string\n qty: number\n unitPriceMinor: number\n lineSubtotalMinor: number\n lineTaxMinor: number\n lineTotalMinor: number\n imageUrl: string\n}\n\nexport interface Order {\n id: string\n orderNumber: string\n sequence: number\n customerId: string | null\n status: OrderStatus\n email: string\n billingAddress: Address\n shippingAddress: Address\n lineItems: OrderLineItem[]\n subtotalMinor: number\n taxTotalMinor: number\n shippingTotalMinor: number\n discountTotalMinor: number\n totalMinor: number\n currency: string\n notes: string\n metadata: Record<string, unknown>\n paymentIntentId: string | null\n /** Money taken for the order, in minor units of its currency; 0 while nothing is captured. */\n capturedMinor: number\n capturedAt: string | null\n /** Σ of the order's succeeded refunds (or, for an import or a pre-refund-table order, what was refunded then). */\n refundedMinor: number\n captureMethod: CaptureMethod | null\n createdAt: string\n updatedAt: string\n}\n\n/** Who took the money: the payments provider, the merchant recording an offline payment, or an imported history. */\nexport type CaptureMethod = 'provider' | 'manual' | 'import'\n\nexport type OrderRefundStatus = 'pending' | 'succeeded' | 'failed'\n\n/** One refund of an order. `pending` until the provider's answer settles it, and counted against what is refundable. */\nexport interface OrderRefund {\n id: string\n orderId: string\n amountMinor: number\n currency: string\n providerRefundId: string | null\n status: OrderRefundStatus\n reason: string | null\n actor: string\n createdAt: string\n}\n\nexport type PaymentState = 'unpaid' | 'paid' | 'partially_refunded' | 'refunded'\n\n/** What the admin shows about an order's money: derived from the stored amounts and refunds alone. */\nexport interface OrderPayment {\n state: PaymentState\n capturedMinor: number\n refundedMinor: number\n refundableMinor: number\n currency: string\n refunds: OrderRefund[]\n}\n\nexport function paymentState(order: Pick<Order, 'capturedMinor' | 'refundedMinor'>): PaymentState {\n if (order.capturedMinor <= 0) return 'unpaid'\n if (order.refundedMinor <= 0) return 'paid'\n return order.refundedMinor < order.capturedMinor ? 'partially_refunded' : 'refunded'\n}\n\n/** What can still be refunded: the capture less what was refunded and what is still pending with the provider. */\nexport function refundableMinor(\n order: Pick<Order, 'capturedMinor' | 'refundedMinor'>,\n refunds: ReadonlyArray<Pick<OrderRefund, 'amountMinor' | 'status'>>,\n): number {\n const pending = refunds\n .filter((r) => r.status === 'pending')\n .reduce((sum, r) => sum + r.amountMinor, 0)\n return Math.max(0, order.capturedMinor - order.refundedMinor - pending)\n}\n\nexport interface OrderHistoryEntry {\n id: string\n orderId: string\n fromStatus: OrderStatus | null\n toStatus: OrderStatus\n actor: string\n note: string\n timestamp: string\n}\n","// Product\nexport const EVT_PRODUCT_CREATED = 'product.created.v1'\nexport const EVT_PRODUCT_UPDATED = 'product.updated.v1'\nexport const EVT_PRODUCT_DELETED = 'product.deleted.v1'\nexport const EVT_PRODUCT_PUBLISHED = 'product.published.v1'\nexport const EVT_PRODUCT_UNPUBLISHED = 'product.unpublished.v1'\n\n// Category\nexport const EVT_CATEGORY_CREATED = 'category.created.v1'\nexport const EVT_CATEGORY_UPDATED = 'category.updated.v1'\nexport const EVT_CATEGORY_DELETED = 'category.deleted.v1'\n\n// Order\nexport const EVT_ORDER_PLACED = 'order.placed.v1'\nexport const EVT_ORDER_PAID = 'order.paid.v1'\nexport const EVT_ORDER_FULFILLED = 'order.fulfilled.v1'\nexport const EVT_ORDER_SHIPPED = 'order.shipped.v1'\nexport const EVT_ORDER_DELIVERED = 'order.delivered.v1'\nexport const EVT_ORDER_CANCELLED = 'order.cancelled.v1'\nexport const EVT_ORDER_REFUNDED = 'order.refunded.v1'\nexport const EVT_ORDER_PARTIALLY_REFUNDED = 'order.partially_refunded.v1'\nexport const EVT_ORDER_STATUS_CHANGED = 'order.status_changed.v1'\n\n// Inventory\nexport const EVT_INVENTORY_CHANGED = 'inventory.changed.v1'\nexport const EVT_INVENTORY_RESERVED = 'inventory.reserved.v1'\nexport const EVT_INVENTORY_RELEASED = 'inventory.released.v1'\nexport const EVT_INVENTORY_LOW_STOCK = 'inventory.low_stock.v1'\nexport const EVT_INVENTORY_OUT_OF_STOCK = 'inventory.out_of_stock.v1'\n\n// Customer\nexport const EVT_CUSTOMER_CREATED = 'customer.created.v1'\nexport const EVT_CUSTOMER_UPDATED = 'customer.updated.v1'\nexport const EVT_CUSTOMER_DELETED = 'customer.deleted.v1'\n// Legacy alias (kept so Prompt 1 seed code continues to compile).\nexport const EVT_CUSTOMER_REGISTERED = EVT_CUSTOMER_CREATED\n\n// Cart\nexport const EVT_CART_CREATED = 'cart.created.v1'\nexport const EVT_CART_ITEM_ADDED = 'cart.item_added.v1'\nexport const EVT_CART_ITEM_REMOVED = 'cart.item_removed.v1'\nexport const EVT_CART_UPDATED = 'cart.updated.v1'\nexport const EVT_CART_ABANDONED = 'cart.abandoned.v1'\nexport const EVT_CART_CONVERTED = 'cart.converted.v1'\n\n// Payment\nexport const EVT_PAYMENT_INTENT_CREATED = 'payment.intent_created.v1'\nexport const EVT_PAYMENT_CAPTURED = 'payment.captured.v1'\nexport const EVT_PAYMENT_FAILED = 'payment.failed.v1'\nexport const EVT_PAYMENT_REFUNDED = 'payment.refunded.v1'\n\n// System\nexport const EVT_ADAPTER_ERROR = 'adapter.error.v1'\nexport const EVT_SETTINGS_CHANGED = 'settings.changed.v1'\n\nexport const STANDARD_EVENT_TYPES = [\n EVT_PRODUCT_CREATED,\n EVT_PRODUCT_UPDATED,\n EVT_PRODUCT_DELETED,\n EVT_PRODUCT_PUBLISHED,\n EVT_PRODUCT_UNPUBLISHED,\n EVT_CATEGORY_CREATED,\n EVT_CATEGORY_UPDATED,\n EVT_CATEGORY_DELETED,\n EVT_ORDER_PLACED,\n EVT_ORDER_PAID,\n EVT_ORDER_FULFILLED,\n EVT_ORDER_SHIPPED,\n EVT_ORDER_DELIVERED,\n EVT_ORDER_CANCELLED,\n EVT_ORDER_REFUNDED,\n EVT_ORDER_PARTIALLY_REFUNDED,\n EVT_ORDER_STATUS_CHANGED,\n EVT_INVENTORY_CHANGED,\n EVT_INVENTORY_RESERVED,\n EVT_INVENTORY_RELEASED,\n EVT_INVENTORY_LOW_STOCK,\n EVT_INVENTORY_OUT_OF_STOCK,\n EVT_CUSTOMER_CREATED,\n EVT_CUSTOMER_UPDATED,\n EVT_CUSTOMER_DELETED,\n EVT_CART_CREATED,\n EVT_CART_ITEM_ADDED,\n EVT_CART_ITEM_REMOVED,\n EVT_CART_UPDATED,\n EVT_CART_ABANDONED,\n EVT_CART_CONVERTED,\n EVT_PAYMENT_INTENT_CREATED,\n EVT_PAYMENT_CAPTURED,\n EVT_PAYMENT_FAILED,\n EVT_PAYMENT_REFUNDED,\n EVT_ADAPTER_ERROR,\n EVT_SETTINGS_CHANGED,\n] as const\n","import type { Logger } from './http.js'\n\nexport type HealthStatus = 'healthy' | 'degraded' | 'down'\n\nexport interface HealthReport {\n status: HealthStatus\n detail?: string\n}\n\n/** How long a health check may run before it reports `down`. */\nexport const HEALTH_CHECK_TIMEOUT_MS = 5000\n\nexport interface BoundedHealthCheckOptions {\n /** `HEALTH_CHECK_TIMEOUT_MS` when not given. */\n timeoutMs?: number\n /** Values an error's message must never show (a URL's user, password or query values); each becomes `***`. */\n redact?: readonly string[]\n}\n\n/** An error's message for a report; an AggregateError (a refused connection to every address of a name) lists its own. */\nexport function errorText(error: unknown): string {\n if (!(error instanceof Error)) return String(error)\n if (error.message !== '') return error.message\n const inner = (error as { errors?: unknown }).errors\n if (Array.isArray(inner) && inner.length > 0) return inner.map(errorText).join('; ')\n const code = (error as { code?: unknown }).code\n return typeof code === 'string' ? code : error.name\n}\n\n/**\n * The shortest value worth removing from text as a secret. A shorter one is too likely to be an ordinary word (a local\n * password of `nymbal`), whose removal garbles the text; user info and query strings go with the URL rewrite anyway.\n */\nexport const SECRET_MIN_LENGTH = 8\n\n/**\n * The values in a connection URL that are credentials or may carry one: its password and its query values, and its\n * user unless `user` is false, each as written and decoded, if at least `SECRET_MIN_LENGTH` long. A user is worth\n * hiding in a driver's error, not in our own text.\n */\nexport function connectionSecrets(url: string, options: { user?: boolean } = {}): string[] {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return []\n }\n const raw = [\n ...(options.user === false ? [] : [parsed.username]),\n parsed.password,\n ...parsed.searchParams.values(),\n ]\n const decoded = raw.map((value) => {\n try {\n return decodeURIComponent(value)\n } catch {\n return value\n }\n })\n return [...new Set([...raw, ...decoded])].filter((value) => value.length >= SECRET_MIN_LENGTH)\n}\n\n/**\n * A connection URL as it may be printed: `scheme://host:port/path`, with no user, password, query or fragment. Text\n * that is not a URL comes back as `<unparsable url>`, since it may be a URL with a secret the parser could not read.\n */\nexport function displayUrl(url: string): string {\n try {\n const parsed = new URL(url)\n return `${parsed.protocol}//${parsed.host}${parsed.pathname === '/' ? '' : parsed.pathname}`\n } catch {\n return '<unparsable url>'\n }\n}\n\n/**\n * Runs `check` within the bound. A check that throws is `down` with the error's message, every `redact` value in it\n * replaced; one still running when the bound passes is `down` with `timed out after <n> ms`, and its signal aborted.\n */\nexport async function boundedHealthCheck(\n check: (signal: AbortSignal) => Promise<HealthReport> | HealthReport,\n options: BoundedHealthCheckOptions = {},\n): Promise<HealthReport> {\n const timeoutMs = options.timeoutMs ?? HEALTH_CHECK_TIMEOUT_MS\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const timedOut = new Promise<HealthReport>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve({ status: 'down', detail: `timed out after ${timeoutMs} ms` })\n }, timeoutMs)\n })\n const failed = (error: unknown): HealthReport => {\n let detail = errorText(error)\n for (const secret of options.redact ?? []) {\n if (secret !== '') detail = detail.split(secret).join('***')\n }\n return { status: 'down', detail }\n }\n try {\n // Started inside a promise so a check that throws before returning one, or returns a plain report, is still\n // mapped rather than escaping the caller.\n const checked = Promise.resolve()\n .then(() => check(controller.signal))\n .catch(failed)\n return await Promise.race([checked, timedOut])\n } finally {\n clearTimeout(timer)\n }\n}\n\nexport interface AdapterBase {\n /** What this adapter offers, for the app's per-slot report; each adapter answers for itself. */\n capabilities(): readonly string[]\n readonly producesEvents: readonly string[]\n readonly consumesEvents: readonly string[]\n initialize(config: unknown): Promise<void>\n healthCheck(): Promise<HealthReport>\n}\n\n/** The commerce slots a store's config names a provider for. */\nexport type CommerceSlot =\n | 'payments'\n | 'email'\n | 'reviews'\n | 'search'\n | 'analytics'\n | 'shipping'\n | 'tax'\n | 'ai'\n\n/**\n * Every slot an adapter package can fill: the commerce slots, the infrastructure primitives and `security`, whose\n * choice is `security.adapter`.\n */\nexport type Slot = CommerceSlot | 'documentStore' | 'eventBus' | 'media' | 'security'\n\n/**\n * What the kernel hands an adapter package's `createAdapter`. A package that serves more than one slot switches on\n * `slot`; `config` is a commerce slot's `config` block from nymbal.config.ts, a primitive's connection block, or for\n * `security` the block named after the adapter (`security.cloudflare`).\n */\nexport interface AdapterContext {\n slot: Slot\n config: Record<string, unknown>\n logger: Logger\n env: NodeJS.ProcessEnv\n}\n\n/**\n * One field of a slot's config block that a store owner may fill from the admin's Settings, as a provider package\n * declares it in its `settings` export. The config block wins, then `env`, then a saved value.\n */\nexport interface AdapterSetting {\n /** A dot path inside the slot's config block: `secretKey`, `smtp.pass`. */\n path: string\n label: string\n /** Stored encrypted and never shown again once saved. */\n secret: boolean\n /** The environment variable that sets the field when the config block does not. */\n env?: string\n /**\n * The adapter works without it (SMTP without authentication), so a secret left unset does not leave the slot\n * unconfigured.\n */\n optional?: boolean\n}\n\n/** What a provider package exports: `createAdapter`, and the fields it lets Settings fill. */\nexport interface ProviderPackageModule {\n createAdapter(ctx: AdapterContext): unknown\n settings?: readonly AdapterSetting[]\n}\n","import { ValidationError } from '../errors.js'\nimport type { QueryParams, QueryResult } from './document-store.js'\n\n// The one order and filter every document store is held to: the in-memory store runs it, and stores that cannot\n// order or filter the same way on their server fetch candidates and run it in the process.\n\nconst collator = new Intl.Collator(undefined, { sensitivity: 'base' })\n\n/** Number, then string, then any other type, then null and missing: so values of different types never tie. */\nfunction rankOf(value: unknown): number {\n if (typeof value === 'number') return 0\n if (typeof value === 'string') return 1\n if (value === null || value === undefined) return 3\n return 2\n}\n\nfunction sign(n: number): number {\n return n > 0 ? 1 : n < 0 ? -1 : 0\n}\n\nfunction compareOther(a: unknown, b: unknown): number {\n const ta = typeof a\n const tb = typeof b\n if (ta !== tb) return ta < tb ? -1 : 1\n if (ta === 'boolean') return Number(a) - Number(b)\n const ja = JSON.stringify(a) ?? ''\n const jb = JSON.stringify(b) ?? ''\n return ja < jb ? -1 : ja > jb ? 1 : 0\n}\n\n/**\n * A total order over document values: numbers numerically, strings by `Intl.Collator` at base sensitivity (case and\n * accents tie), other types after strings, and null or missing last. Values of different types rank by type first.\n */\nexport function compareDocumentValues(a: unknown, b: unknown): number {\n const ra = rankOf(a)\n const rb = rankOf(b)\n if (ra !== rb) return ra - rb\n switch (ra) {\n case 0:\n return sign((a as number) - (b as number))\n case 1:\n return sign(collator.compare(a as string, b as string))\n case 2:\n return sign(compareOther(a, b))\n default:\n return 0\n }\n}\n\nexport type SortKeyCondition = Pick<NonNullable<QueryParams['sortKey']>, 'operator' | 'value'>\n\n/** `eq` is `===`; `gt`, `lt` and `between` (inclusive, `[low, high]`) are `compareDocumentValues`. */\nexport function matchesSortKey(value: unknown, condition: SortKeyCondition): boolean {\n const target = condition.value\n switch (condition.operator) {\n case 'eq':\n return value === target\n case 'gt':\n return compareDocumentValues(value, target) > 0\n case 'lt':\n return compareDocumentValues(value, target) < 0\n case 'between': {\n if (!Array.isArray(target) || target.length !== 2) return false\n const [low, high] = target as [unknown, unknown]\n return compareDocumentValues(value, low) >= 0 && compareDocumentValues(value, high) <= 0\n }\n default:\n return false\n }\n}\n\n/** The value at a dotted path (`priceRange.minMinor`); missing anywhere along it is `undefined`. */\nexport function documentValueAt(doc: unknown, path: string): unknown {\n let value: unknown = doc\n for (const part of path.split('.')) {\n if (value === null || typeof value !== 'object') return undefined\n value = (value as Record<string, unknown>)[part]\n }\n return value\n}\n\nexport const DEFAULT_QUERY_LIMIT = 50\n\ninterface Order {\n /** The field ordered by, or null for the document key alone. */\n field: string | null\n direction: 'asc' | 'desc'\n}\n\ninterface Position {\n key: string\n value: unknown\n}\n\nfunction orderOf(params: QueryParams): Order {\n if (params.orderBy !== undefined) {\n return { field: params.orderBy.field, direction: params.orderBy.direction }\n }\n const direction = (params.ascending ?? true) ? 'asc' : 'desc'\n return { field: params.sortKey?.field ?? null, direction }\n}\n\nconst isNullish = (value: unknown): boolean => value === null || value === undefined\n\n/** Nulls and missing values last in either direction; ties by key ascending, so no two documents tie. */\nfunction comparePositions(a: Position, b: Position, order: Order): number {\n if (order.field !== null) {\n const an = isNullish(a.value)\n const bn = isNullish(b.value)\n if (an !== bn) return an ? 1 : -1\n let c = an ? 0 : compareDocumentValues(a.value, b.value)\n if (order.direction === 'desc') c = -c\n if (c !== 0) return c\n return a.key < b.key ? -1 : a.key > b.key ? 1 : 0\n }\n const c = a.key < b.key ? -1 : a.key > b.key ? 1 : 0\n return order.direction === 'desc' ? -c : c\n}\n\n/** What a cursor is bound to: the order and the partition, so a cursor reused under another is refused. */\nfunction cursorScope(order: Order, params: QueryParams): string {\n return JSON.stringify([order.field, order.direction, params.partitionKey])\n}\n\nfunction decodeCursor(cursor: string, scope: string): Position {\n let decoded: unknown\n try {\n decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'))\n } catch {\n decoded = null\n }\n const c = decoded as { o?: unknown; k?: unknown; v?: unknown } | null\n if (c === null || typeof c !== 'object' || typeof c.o !== 'string' || typeof c.k !== 'string') {\n throw new ValidationError('The cursor is not valid', { context: { cursor } })\n }\n if (c.o !== scope) {\n throw new ValidationError('The cursor was issued under another order', { context: { cursor } })\n }\n return { key: c.k, value: c.v ?? null }\n}\n\n/**\n * One page of `params` over the documents of a collection: filter by partition (`'*'` for all) and sort key, order,\n * then seek past the cursor by value, so a document deleted between pages neither restarts nor repeats the walk.\n */\nexport function queryDocuments<T>(\n documents: Iterable<readonly [string, unknown]>,\n params: QueryParams,\n): QueryResult<T> {\n const limit = params.limit ?? DEFAULT_QUERY_LIMIT\n const order = orderOf(params)\n const scope = cursorScope(order, params)\n const after = params.cursor ? decodeCursor(params.cursor, scope) : null\n const { partitionKey, sortKey } = params\n\n const matches: Array<{ position: Position; doc: unknown }> = []\n for (const [key, doc] of documents) {\n const record = (doc ?? {}) as Record<string, unknown>\n if (partitionKey.value !== '*' && record[partitionKey.field] !== partitionKey.value) continue\n if (sortKey !== undefined && !matchesSortKey(record[sortKey.field], sortKey)) continue\n const value = order.field === null ? null : (documentValueAt(record, order.field) ?? null)\n const position = { key, value }\n if (after !== null && comparePositions(position, after, order) <= 0) continue\n matches.push({ position, doc })\n }\n matches.sort((a, b) => comparePositions(a.position, b.position, order))\n\n const page = matches.slice(0, limit)\n const last = page[page.length - 1]\n const nextCursor =\n matches.length > limit && last !== undefined\n ? Buffer.from(\n JSON.stringify({ o: scope, k: last.position.key, v: last.position.value }),\n 'utf8',\n ).toString('base64url')\n : null\n return { items: page.map((m) => m.doc as T), nextCursor }\n}\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'\n\n/** The request header a caller may name its own correlation id in, and the response header that echoes the one used. */\nexport const CORRELATION_ID_HEADER = 'x-correlation-id'\n\nconst CORRELATION_ID = /^[A-Za-z0-9._:-]{1,128}$/\n\n/**\n * Whether `s` may be used as a correlation id: it reaches logs and the event log, so an inbound value that fails is\n * replaced, never cleaned. The id is a label only; nothing reads it to decide access.\n */\nexport function isValidCorrelationId(s: string): boolean {\n return CORRELATION_ID.test(s)\n}\n\nexport interface RequestAuth {\n userId?: string\n roles?: string[]\n token?: string\n}\n\nexport interface Logger {\n trace(obj: unknown, msg?: string): void\n debug(obj: unknown, msg?: string): void\n info(obj: unknown, msg?: string): void\n warn(obj: unknown, msg?: string): void\n error(obj: unknown, msg?: string): void\n child(bindings: Record<string, unknown>): Logger\n}\n\nexport interface RequestContext<TBody = unknown, TQuery = Record<string, string | string[] | undefined>, TParams = Record<string, string>> {\n method: HttpMethod\n path: string\n url: string\n params: TParams\n query: TQuery\n body: TBody\n rawBody?: Buffer\n headers: Record<string, string | string[] | undefined>\n cookies: Record<string, string>\n auth?: RequestAuth\n requestId: string\n /** The inbound `x-correlation-id` when `isValidCorrelationId` accepts it, else the request id. */\n correlationId: string\n logger: Logger\n ip: string\n}\n\nexport interface ResponseCookie {\n name: string\n value: string\n path?: string\n domain?: string\n maxAge?: number\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n}\n\nexport interface ResponseEnvelope<T = unknown> {\n status: number\n headers?: Record<string, string>\n body?: T\n cookies?: ResponseCookie[]\n}\n\nexport type RouteHandler<TBody = unknown, TQuery = Record<string, string | string[] | undefined>, TParams = Record<string, string>, TResponse = unknown> = (\n ctx: RequestContext<TBody, TQuery, TParams>,\n) => Promise<ResponseEnvelope<TResponse>> | ResponseEnvelope<TResponse>\n\nexport interface RouteOptions {\n name?: string\n csrf?: boolean\n rateLimit?: string\n rawBody?: boolean\n}\n\nexport type MiddlewareHandler = (\n ctx: RequestContext,\n) => Promise<ResponseEnvelope | void> | ResponseEnvelope | void\n\nexport interface SseEvent {\n id?: string\n event?: string\n data: unknown\n}\n\nexport interface StreamContext {\n send(event: SseEvent): void\n close(): void\n onClose(callback: () => void): void\n}\n\nexport type StreamHandler = (\n ctx: RequestContext,\n stream: StreamContext,\n) => Promise<void> | void\n\nexport interface HttpAdapter {\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void\n registerStreamRoute(\n method: 'GET',\n path: string,\n handler: StreamHandler,\n options?: RouteOptions,\n ): void\n registerMiddleware(middleware: MiddlewareHandler): void\n setDefaultHeaders(headers: Record<string, string>): void\n start(port: number, host?: string): Promise<void>\n stop(): Promise<void>\n}\n","import type { AdapterBase } from './adapter-base.js'\nimport type { HttpAdapter } from './http.js'\n\nexport type RateLimitAction = 'block' | 'challenge' | 'throttle'\n\nexport interface RateLimitRule {\n requests: number\n window: string\n action: RateLimitAction\n}\n\nexport interface RateLimitDecision {\n allowed: boolean\n retryAfterSeconds?: number\n action: RateLimitAction\n}\n\nexport interface SecurityConfigLike {\n rateLimit: Record<string, RateLimitRule>\n csrf: boolean\n headers: {\n hsts: boolean\n contentSecurityPolicy: string\n referrerPolicy: string\n xFrameOptions: string\n }\n [key: string]: unknown\n}\n\nexport interface ValidationResult {\n valid: boolean\n errors: Array<{ code: string; message: string; path?: string }>\n}\n\nexport interface DriftReport {\n drift: boolean\n differences: Array<{ path: string; configured: unknown; actual: unknown }>\n}\n\nexport interface SecurityState {\n rulesApplied: string[]\n lastValidatedAt: string\n}\n\nexport interface SecurityAdapter extends AdapterBase {\n applyMiddleware(app: HttpAdapter, config: SecurityConfigLike): Promise<void>\n validate(config: SecurityConfigLike): Promise<ValidationResult>\n drift?(config: SecurityConfigLike): Promise<DriftReport>\n inspect?(): Promise<SecurityState>\n}\n\n/** The response headers every security adapter sets in process, from the config's `headers` block. */\nexport function buildSecurityHeaders(security: SecurityConfigLike): Record<string, string> {\n const headers: Record<string, string> = {}\n if (security.headers.hsts) {\n headers['strict-transport-security'] = 'max-age=31536000; includeSubDomains'\n }\n const csp = security.headers.contentSecurityPolicy\n const cspValue =\n csp === 'strict'\n ? \"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'\"\n : csp === 'relaxed'\n ? \"default-src 'self' https:; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'\"\n : csp === 'off'\n ? null\n : csp\n if (cspValue) headers['content-security-policy'] = cspValue\n headers['referrer-policy'] = security.headers.referrerPolicy\n headers['x-frame-options'] = security.headers.xFrameOptions === 'deny' ? 'DENY' : 'SAMEORIGIN'\n headers['x-content-type-options'] = 'nosniff'\n return headers\n}\n","// The kernel's transactional email templates, shared by every email adapter (the console one and the SMTP package).\nexport interface RenderedEmail {\n subject: string\n text: string\n html: string\n}\n\nfunction render(template: string, vars: Record<string, unknown>): string {\n return template.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_, path: string) => {\n const parts = path.split('.')\n let cur: unknown = vars\n for (const p of parts) {\n if (cur && typeof cur === 'object' && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p]\n } else {\n return ''\n }\n }\n return cur == null ? '' : String(cur)\n })\n}\n\nconst TEMPLATES: Record<string, { subject: string; body: string }> = {\n 'order-placed': {\n subject: 'Order {{orderNumber}} confirmed',\n body: `Hi {{customerName}},\\n\\nThank you for your order {{orderNumber}}. Total: {{totalFormatted}}.\\n\\nWe'll let you know when it ships.`,\n },\n 'order-shipped': {\n subject: 'Order {{orderNumber}} is on its way',\n body: `Hi {{customerName}},\\n\\nYour order {{orderNumber}} has shipped via {{carrier}}.\\nTracking: {{trackingNumber}}`,\n },\n welcome: {\n subject: 'Welcome to {{storeName}}',\n body: `Welcome {{customerName}}! Your account is ready.`,\n },\n 'cart-abandoned': {\n subject: \"Don't forget your cart at {{storeName}}\",\n body: `Hi,\\n\\nYou left items in your cart. Pick up where you left off: {{resumeUrl}}`,\n },\n}\n\nexport function renderEmailTemplate(\n template: string,\n vars: Record<string, unknown>,\n): RenderedEmail {\n const t = TEMPLATES[template]\n if (!t) {\n return {\n subject: `[nymbal] ${template}`,\n text: JSON.stringify(vars),\n html: `<pre>${JSON.stringify(vars, null, 2)}</pre>`,\n }\n }\n const subject = render(t.subject, vars)\n const text = render(t.body, vars)\n const html = `<pre>${text.replace(/</g, '<')}</pre>`\n return { subject, text, html }\n}\n","export type StockLevel = 'out' | 'low' | 'in'\n\n/** Below this many units a variant is low on stock. */\nconst LOW_STOCK_BELOW = 5\n\n/** The one stock rule every storefront renders from. */\nexport function stockLevel(stock: number): StockLevel {\n if (stock <= 0) return 'out'\n return stock < LOW_STOCK_BELOW ? 'low' : 'in'\n}\n\nexport const STOCK_LABELS: Record<StockLevel, string> = {\n out: 'Out of Stock',\n low: 'Low Stock',\n in: 'In Stock',\n}\n","/** Who a command ran for. A guest is `guest:anonymous`: a cart's token is a bearer credential and is never an id. */\nexport interface AuditActor {\n kind: 'admin' | 'customer' | 'guest' | 'system' | 'import'\n id: string\n}\n\n/** `applied`: the command ran and committed. `refused`: its service refused it. `replayed`: a keyed retry answered. */\nexport type AuditOutcome = 'applied' | 'refused' | 'replayed'\n\n/** A record a command touched, by id only. */\nexport interface AuditSubject {\n type: string\n id: string\n}\n\n/**\n * One audit entry as `GET /api/admin/activity` lists it and the exports print it. A stored response is shown by its\n * status only; its body never leaves the store.\n */\nexport interface AuditEntry {\n id: string\n at: string\n actor: AuditActor\n command: string\n subjects: AuditSubject[]\n correlationId: string | null\n idempotencyKey: string | null\n outcome: AuditOutcome\n responseStatus: number | null\n}\n\n/** The request header an admin write carries to be applied once, and the response header a replay carries. */\nexport const IDEMPOTENCY_KEY_HEADER = 'idempotency-key'\nexport const IDEMPOTENT_REPLAYED_HEADER = 'idempotent-replayed'\n\n/** 1-128 characters of `[A-Za-z0-9._:-]`. */\nexport function isValidIdempotencyKey(value: string): boolean {\n return /^[A-Za-z0-9._:-]{1,128}$/.test(value)\n}\n"],"mappings":"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","names":["c"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nymbalhq/types",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Shared TypeScript types and adapter contracts for Nymbal",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
"dist"
|
|
28
28
|
],
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
31
|
+
"tsup": "^9.0.0",
|
|
32
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsup",
|
|
35
36
|
"dev": "tsup --watch",
|
|
36
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --rootDir ../.."
|
|
37
38
|
}
|
|
38
39
|
}
|