@zalkera/client 0.11.0 → 0.13.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.cts CHANGED
@@ -356,10 +356,18 @@ interface ProductSummary {
356
356
  currency: string;
357
357
  inStock: boolean;
358
358
  }
359
- /** `listProducts` 질의. category 필터는 상품↔카테고리 매핑이 채워진 뒤 지원. */
359
+ /** `listProducts` 질의. */
360
360
  interface ListProductsParams {
361
361
  productType?: ProductType;
362
362
  keyword?: string;
363
+ /**
364
+ * 이 카테고리에 속한 상품만. 값은 [listProductCategories] 가 주는 `ProductCategory.id` 다 —
365
+ * slug 가 아니다(카테고리 페이지는 slug 로 카테고리를 찾고, 그 id 로 상품을 좁힌다).
366
+ *
367
+ * **없는 카테고리는 빈 목록이지 404 가 아니다.** 목록 API 가 "그 카테고리가 있느냐"를 답하지
368
+ * 않기 때문이다 — 404 로 갈릴 판단은 카테고리 자체를 못 찾은 라우트가 한다.
369
+ */
370
+ categoryId?: number;
363
371
  page?: number;
364
372
  size?: number;
365
373
  sort?: string;
@@ -719,7 +727,10 @@ interface ZalkeraClient {
719
727
  submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;
720
728
  /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */
721
729
  getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;
722
- /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */
730
+ /**
731
+ * 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions].
732
+ * `params.categoryId` 로 카테고리별 목록을 그린다([ListProductsParams]).
733
+ */
723
734
  listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;
724
735
  /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */
725
736
  listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;
@@ -930,8 +941,15 @@ declare class ZalkeraError extends Error {
930
941
  * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,
931
942
  * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.
932
943
  */
933
- /** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */
934
- declare const SECTION_CONTRACT_REV = 3;
944
+ /**
945
+ * 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다.
946
+ *
947
+ * rev 4 = **참조 방언·콘텐츠 파일의 1급 승격**(memo129 §1.4). 섹션 타입 12종도 config 키 선언도
948
+ * 안 바뀌었다 — 정본에 `dialects`(id ↔ 참조)와 `contentFile`(`content/pages/*.json`) 절이 생겼고,
949
+ * 이 패키지는 그 방언을 읽는 헬퍼([asHandle]·[asHandleArray]·[assetPath]·[readConfig])를 실어 나른다.
950
+ * 그래서 아래 `SECTION_CONTRACT` 리터럴은 rev 3 과 **바이트 동일**하다(동기 스크립트가 확인한다).
951
+ */
952
+ declare const SECTION_CONTRACT_REV = 4;
935
953
  /** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */
936
954
  type SectionVertical = "BEAUTY" | "GENERAL";
937
955
  interface SectionSpec {
@@ -1047,6 +1065,18 @@ declare function safeLinkUrl(raw: string | null | undefined): string;
1047
1065
  * 그 계약이 조용히 깨진다(471946c 교훈).
1048
1066
  */
1049
1067
  declare function parseConfig<T>(config: string | null): T | null;
1068
+ /**
1069
+ * config 를 **거처와 무관하게** 읽는다 — 문자열이면 파싱하고, 이미 객체면 그대로 본다(rev 4).
1070
+ *
1071
+ * 거처가 둘이 됐다: DB(`page_section.config` — JSON **문자열**)와 소스(`content/pages/*.json` 의
1072
+ * `sections[].config` — **객체**). 문자열인 것은 컬럼 저장 잔재이지 계약이 아니라(어휘 계약 rev 4
1073
+ * `dialects`), 소비자가 두 갈래로 갈리면 섹션 컴포넌트가 두 벌이 된다 — 이 패키지가 사본을 안 만드는
1074
+ * 이유 그대로다. 그래서 입구를 하나로 좁힌다.
1075
+ *
1076
+ * [parseConfig] 와 같은 계약: **절대 throw 하지 않고**, 객체가 아니면 `null`(배열도 null — 섹션 config
1077
+ * 는 객체다). 기존 `parseConfig` 는 그대로 둔다(append-only).
1078
+ */
1079
+ declare function readConfig<T>(config: unknown): T | null;
1050
1080
  /** 양의 정수 id 만 남긴다 — 배열이 아니면 빈 배열. */
1051
1081
  declare function asIdArray(value: unknown): number[];
1052
1082
  /** 객체 배열만 — 아니면 빈 배열. */
@@ -1063,6 +1093,28 @@ declare function asId(value: unknown): number | undefined;
1063
1093
  * **throw 하지 않는다**는 이 파일의 계약은 그대로다 — 인코딩만 하고 값을 판정하지 않는다.
1064
1094
  */
1065
1095
  declare function mediaSrc(assetId: number): string;
1096
+ /** 상품 handle 하나. 문자열이 아니거나 공백뿐이면 `undefined` — 형식은 검사하지 않는다(런타임은 관용). */
1097
+ declare function asHandle(value: unknown): string | undefined;
1098
+ /**
1099
+ * 상품 handle 배열. **배열 순서를 보존한다** — 콘텐츠 파일에서 이 배열이 노출 순서의 원장이다
1100
+ * (`SERVICE_MENU` 의 ItemList 가 이 순서 그대로 나가야 한다).
1101
+ * 배열이 아니면 빈 배열, 원소 중 handle 이 아닌 것만 떨군다. 중복은 남긴다 — 같은 상품을 두 번
1102
+ * 진열하는 것이 계약 위반은 아니다.
1103
+ */
1104
+ declare function asHandleArray(value: unknown): string[];
1105
+ /**
1106
+ * 레포 `public/` 에이셋 경로. **루트 절대 경로만** 통과한다.
1107
+ *
1108
+ * 왜 `safeLinkUrl` 을 그대로 안 쓰는가: 링크는 외부로 나가는 것이 정당하지만(스킴 허용목록),
1109
+ * 이 값은 `<img src>` 로 들어가는 **레포 안 파일 참조**다. 원격 호스트를 허용하면 ⑴ 개시된 사이트가
1110
+ * 남의 서버에 의존해 조용히 깨지고 ⑵ 방문자 IP·리퍼러가 그 호스트로 새며 ⑶ `data:`·`javascript:`
1111
+ * 가 같은 구멍으로 들어온다. 원격 이미지가 필요한 레포는 계약 영역이 아니라 자유 영역에서 자기
1112
+ * 컴포넌트로 그린다 — 계약이 자유를 막지는 않되, 계약이 보증하는 범위는 좁게 잡는다.
1113
+ *
1114
+ * 막는 것: 스킴 있는 URL(`http:`·`data:`·`javascript:`) · 스킴 상대(`//host`) · 상대 경로 ·
1115
+ * 경로 탈출(`..`) · 역슬래시(윈도 경로·정규화 우회) · 널바이트. **절대 throw 하지 않는다.**
1116
+ */
1117
+ declare function assetPath(value: unknown): string | undefined;
1066
1118
 
1067
1119
  /**
1068
1120
  * 테넌트 테마 색 파서 (서버 전용).
@@ -1084,4 +1136,4 @@ interface ParsedTheme {
1084
1136
  }
1085
1137
  declare function parseThemeColors(raw: string | null | undefined): ParsedTheme;
1086
1138
 
1087
- export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPagesParams, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type PageSummary, type Paginated, type ParsedTheme, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, SECTION_CONTRACT, SECTION_CONTRACT_REV, type SectionSpec, type SectionVertical, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, asId, asIdArray, asObjectArray, asString, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, safeLinkUrl, sectionsOfVertical };
1139
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPagesParams, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type PageSummary, type Paginated, type ParsedTheme, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, SECTION_CONTRACT, SECTION_CONTRACT_REV, type SectionSpec, type SectionVertical, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, asHandle, asHandleArray, asId, asIdArray, asObjectArray, asString, assetPath, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, readConfig, safeLinkUrl, sectionsOfVertical };
package/dist/index.d.ts CHANGED
@@ -356,10 +356,18 @@ interface ProductSummary {
356
356
  currency: string;
357
357
  inStock: boolean;
358
358
  }
359
- /** `listProducts` 질의. category 필터는 상품↔카테고리 매핑이 채워진 뒤 지원. */
359
+ /** `listProducts` 질의. */
360
360
  interface ListProductsParams {
361
361
  productType?: ProductType;
362
362
  keyword?: string;
363
+ /**
364
+ * 이 카테고리에 속한 상품만. 값은 [listProductCategories] 가 주는 `ProductCategory.id` 다 —
365
+ * slug 가 아니다(카테고리 페이지는 slug 로 카테고리를 찾고, 그 id 로 상품을 좁힌다).
366
+ *
367
+ * **없는 카테고리는 빈 목록이지 404 가 아니다.** 목록 API 가 "그 카테고리가 있느냐"를 답하지
368
+ * 않기 때문이다 — 404 로 갈릴 판단은 카테고리 자체를 못 찾은 라우트가 한다.
369
+ */
370
+ categoryId?: number;
363
371
  page?: number;
364
372
  size?: number;
365
373
  sort?: string;
@@ -719,7 +727,10 @@ interface ZalkeraClient {
719
727
  submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;
720
728
  /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */
721
729
  getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;
722
- /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */
730
+ /**
731
+ * 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions].
732
+ * `params.categoryId` 로 카테고리별 목록을 그린다([ListProductsParams]).
733
+ */
723
734
  listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;
724
735
  /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */
725
736
  listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;
@@ -930,8 +941,15 @@ declare class ZalkeraError extends Error {
930
941
  * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,
931
942
  * client 발행 전 `scripts/sync-section-contract.mjs` 로 대조한다.
932
943
  */
933
- /** 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다. */
934
- declare const SECTION_CONTRACT_REV = 3;
944
+ /**
945
+ * 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다.
946
+ *
947
+ * rev 4 = **참조 방언·콘텐츠 파일의 1급 승격**(memo129 §1.4). 섹션 타입 12종도 config 키 선언도
948
+ * 안 바뀌었다 — 정본에 `dialects`(id ↔ 참조)와 `contentFile`(`content/pages/*.json`) 절이 생겼고,
949
+ * 이 패키지는 그 방언을 읽는 헬퍼([asHandle]·[asHandleArray]·[assetPath]·[readConfig])를 실어 나른다.
950
+ * 그래서 아래 `SECTION_CONTRACT` 리터럴은 rev 3 과 **바이트 동일**하다(동기 스크립트가 확인한다).
951
+ */
952
+ declare const SECTION_CONTRACT_REV = 4;
935
953
  /** 업종 분류 — 콘솔 섹션 픽커의 그룹핑에 쓴다(테넌트 업종 필드는 아직 없다·memo102 §5). */
936
954
  type SectionVertical = "BEAUTY" | "GENERAL";
937
955
  interface SectionSpec {
@@ -1047,6 +1065,18 @@ declare function safeLinkUrl(raw: string | null | undefined): string;
1047
1065
  * 그 계약이 조용히 깨진다(471946c 교훈).
1048
1066
  */
1049
1067
  declare function parseConfig<T>(config: string | null): T | null;
1068
+ /**
1069
+ * config 를 **거처와 무관하게** 읽는다 — 문자열이면 파싱하고, 이미 객체면 그대로 본다(rev 4).
1070
+ *
1071
+ * 거처가 둘이 됐다: DB(`page_section.config` — JSON **문자열**)와 소스(`content/pages/*.json` 의
1072
+ * `sections[].config` — **객체**). 문자열인 것은 컬럼 저장 잔재이지 계약이 아니라(어휘 계약 rev 4
1073
+ * `dialects`), 소비자가 두 갈래로 갈리면 섹션 컴포넌트가 두 벌이 된다 — 이 패키지가 사본을 안 만드는
1074
+ * 이유 그대로다. 그래서 입구를 하나로 좁힌다.
1075
+ *
1076
+ * [parseConfig] 와 같은 계약: **절대 throw 하지 않고**, 객체가 아니면 `null`(배열도 null — 섹션 config
1077
+ * 는 객체다). 기존 `parseConfig` 는 그대로 둔다(append-only).
1078
+ */
1079
+ declare function readConfig<T>(config: unknown): T | null;
1050
1080
  /** 양의 정수 id 만 남긴다 — 배열이 아니면 빈 배열. */
1051
1081
  declare function asIdArray(value: unknown): number[];
1052
1082
  /** 객체 배열만 — 아니면 빈 배열. */
@@ -1063,6 +1093,28 @@ declare function asId(value: unknown): number | undefined;
1063
1093
  * **throw 하지 않는다**는 이 파일의 계약은 그대로다 — 인코딩만 하고 값을 판정하지 않는다.
1064
1094
  */
1065
1095
  declare function mediaSrc(assetId: number): string;
1096
+ /** 상품 handle 하나. 문자열이 아니거나 공백뿐이면 `undefined` — 형식은 검사하지 않는다(런타임은 관용). */
1097
+ declare function asHandle(value: unknown): string | undefined;
1098
+ /**
1099
+ * 상품 handle 배열. **배열 순서를 보존한다** — 콘텐츠 파일에서 이 배열이 노출 순서의 원장이다
1100
+ * (`SERVICE_MENU` 의 ItemList 가 이 순서 그대로 나가야 한다).
1101
+ * 배열이 아니면 빈 배열, 원소 중 handle 이 아닌 것만 떨군다. 중복은 남긴다 — 같은 상품을 두 번
1102
+ * 진열하는 것이 계약 위반은 아니다.
1103
+ */
1104
+ declare function asHandleArray(value: unknown): string[];
1105
+ /**
1106
+ * 레포 `public/` 에이셋 경로. **루트 절대 경로만** 통과한다.
1107
+ *
1108
+ * 왜 `safeLinkUrl` 을 그대로 안 쓰는가: 링크는 외부로 나가는 것이 정당하지만(스킴 허용목록),
1109
+ * 이 값은 `<img src>` 로 들어가는 **레포 안 파일 참조**다. 원격 호스트를 허용하면 ⑴ 개시된 사이트가
1110
+ * 남의 서버에 의존해 조용히 깨지고 ⑵ 방문자 IP·리퍼러가 그 호스트로 새며 ⑶ `data:`·`javascript:`
1111
+ * 가 같은 구멍으로 들어온다. 원격 이미지가 필요한 레포는 계약 영역이 아니라 자유 영역에서 자기
1112
+ * 컴포넌트로 그린다 — 계약이 자유를 막지는 않되, 계약이 보증하는 범위는 좁게 잡는다.
1113
+ *
1114
+ * 막는 것: 스킴 있는 URL(`http:`·`data:`·`javascript:`) · 스킴 상대(`//host`) · 상대 경로 ·
1115
+ * 경로 탈출(`..`) · 역슬래시(윈도 경로·정규화 우회) · 널바이트. **절대 throw 하지 않는다.**
1116
+ */
1117
+ declare function assetPath(value: unknown): string | undefined;
1066
1118
 
1067
1119
  /**
1068
1120
  * 테넌트 테마 색 파서 (서버 전용).
@@ -1084,4 +1136,4 @@ interface ParsedTheme {
1084
1136
  }
1085
1137
  declare function parseThemeColors(raw: string | null | undefined): ParsedTheme;
1086
1138
 
1087
- export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPagesParams, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type PageSummary, type Paginated, type ParsedTheme, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, SECTION_CONTRACT, SECTION_CONTRACT_REV, type SectionSpec, type SectionVertical, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, asId, asIdArray, asObjectArray, asString, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, safeLinkUrl, sectionsOfVertical };
1139
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPagesParams, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type PageSummary, type Paginated, type ParsedTheme, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, SECTION_CONTRACT, SECTION_CONTRACT_REV, type SectionSpec, type SectionVertical, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, type ZalkeraClient, type ZalkeraClientOptions, ZalkeraError, asHandle, asHandleArray, asId, asIdArray, asObjectArray, asString, assetPath, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, readConfig, safeLinkUrl, sectionsOfVertical };
package/dist/index.js CHANGED
@@ -193,6 +193,7 @@ function createZalkeraClient(options) {
193
193
  query: {
194
194
  productType: params?.productType,
195
195
  keyword: params?.keyword,
196
+ categoryId: params?.categoryId,
196
197
  page: params?.page,
197
198
  size: params?.size,
198
199
  sort: params?.sort
@@ -289,7 +290,7 @@ function safeJsonParse(text) {
289
290
  }
290
291
 
291
292
  // src/sections.ts
292
- var SECTION_CONTRACT_REV = 3;
293
+ var SECTION_CONTRACT_REV = 4;
293
294
  var SECTION_CONTRACT = [
294
295
  // 뷰티(memo47) — append-only 계약상 불변
295
296
  // SERVICE_MENU 의 ItemList 는 rev 2 에서 올라왔다(memo119 T6-ⓒ): 쇼핑몰 유형엔 상품 목록 표면을
@@ -339,6 +340,10 @@ function parseConfig(config) {
339
340
  return null;
340
341
  }
341
342
  }
343
+ function readConfig(config) {
344
+ if (typeof config === "string") return parseConfig(config);
345
+ return config != null && typeof config === "object" && !Array.isArray(config) ? config : null;
346
+ }
342
347
  function asIdArray(value) {
343
348
  if (!Array.isArray(value)) return [];
344
349
  return value.filter((v) => typeof v === "number" && Number.isInteger(v) && v > 0);
@@ -358,6 +363,23 @@ function asId(value) {
358
363
  function mediaSrc(assetId) {
359
364
  return `/media/${seg(assetId)}`;
360
365
  }
366
+ function asHandle(value) {
367
+ if (typeof value !== "string") return void 0;
368
+ const handle = value.trim();
369
+ return handle === "" ? void 0 : handle;
370
+ }
371
+ function asHandleArray(value) {
372
+ if (!Array.isArray(value)) return [];
373
+ return value.map(asHandle).filter((h) => h !== void 0);
374
+ }
375
+ function assetPath(value) {
376
+ if (typeof value !== "string") return void 0;
377
+ const path = value.trim();
378
+ if (!path.startsWith("/") || path.startsWith("//")) return void 0;
379
+ if (path.includes("\\") || path.includes("\0")) return void 0;
380
+ if (path.split("/").includes("..")) return void 0;
381
+ return path;
382
+ }
361
383
 
362
384
  // src/theme.ts
363
385
  var HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -430,6 +452,6 @@ function toRgb(hex) {
430
452
  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
431
453
  }
432
454
 
433
- export { SECTION_CONTRACT, SECTION_CONTRACT_REV, ZalkeraError, asId, asIdArray, asObjectArray, asString, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, safeLinkUrl, sectionsOfVertical };
455
+ export { SECTION_CONTRACT, SECTION_CONTRACT_REV, ZalkeraError, asHandle, asHandleArray, asId, asIdArray, asObjectArray, asString, assetPath, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, readConfig, safeLinkUrl, sectionsOfVertical };
434
456
  //# sourceMappingURL=index.js.map
435
457
  //# sourceMappingURL=index.js.map