@tanqory/dev-mcp 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +94 -2
- package/bin/tanqory-dev-mcp.mjs +16 -0
- package/bundled/examples/blank/config/settings.json +3 -0
- package/bundled/examples/blank/locales/en.json +22 -0
- package/bundled/examples/blank/locales/th.json +22 -0
- package/bundled/examples/blank/regions/footer.json +35 -0
- package/bundled/examples/blank/regions/header.json +13 -0
- package/bundled/examples/blank/src/bundle.ts +15 -0
- package/bundled/examples/blank/src/components/parts.tsx +32 -0
- package/bundled/examples/blank/src/entry.client.tsx +6 -0
- package/bundled/examples/blank/src/entry.server.tsx +13 -0
- package/bundled/examples/blank/src/sections/chrome.tsx +132 -0
- package/bundled/examples/blank/src/sections/commerce.tsx +242 -0
- package/bundled/examples/blank/src/sections/content.tsx +111 -0
- package/bundled/examples/blank/src/sections/editorial.tsx +107 -0
- package/bundled/examples/blank/src/settings.ts +15 -0
- package/bundled/examples/blank/src/styles/theme.css +195 -0
- package/bundled/examples/blank/src/templates.ts +14 -0
- package/bundled/examples/blank/src/theme.tsx +76 -0
- package/bundled/examples/blank/template.json +24 -0
- package/bundled/examples/blank/templates/404.json +14 -0
- package/bundled/examples/blank/templates/account.json +14 -0
- package/bundled/examples/blank/templates/article.json +14 -0
- package/bundled/examples/blank/templates/blog.json +14 -0
- package/bundled/examples/blank/templates/cart.json +14 -0
- package/bundled/examples/blank/templates/collection.json +14 -0
- package/bundled/examples/blank/templates/index.json +36 -0
- package/bundled/examples/blank/templates/page.contact.json +18 -0
- package/bundled/examples/blank/templates/page.json +14 -0
- package/bundled/examples/blank/templates/product.json +18 -0
- package/bundled/examples/blank/templates/search.json +14 -0
- package/bundled/examples/blank/tests/theme.test.ts +49 -0
- package/bundled/theme-abi/contracts/render-context.v2.d.ts +849 -0
- package/bundled/theme-abi/contracts/theme-abi-v2.md +1833 -0
- package/bundled/theme-abi/generated/capabilities.json +80 -0
- package/bundled/theme-abi/generated/field-types.json +869 -0
- package/bundled/theme-abi/generated/limits.json +173 -0
- package/bundled/theme-abi/schemas/conformance-report.v2.schema.json +482 -0
- package/bundled/theme-abi/schemas/theme-artifact.v1.schema.json +230 -0
- package/bundled/theme-abi/schemas/theme-manifest.v2.schema.json +254 -0
- package/bundled/theme-conformance/docs/storefront-tier.md +250 -0
- package/bundled/theme-conformance/schema/conformance-report.v2.schema.json +482 -0
- package/bundled/theme-conformance/schema/storefront-report.v1.schema.json +110 -0
- package/bundled/theme-conformance/schema/x-tanqory-ai.v0.schema.json +70 -0
- package/bundled/theme-kit/docs/headless-api.md +220 -0
- package/bundled/theme-kit/generated/kit-strings.json +180 -0
- package/bundled/theme-kit/types/headless/index.d.ts +36 -0
- package/bundled/theme-kit/types/headless/theme.d.ts +99 -0
- package/bundled/theme-kit/types/sdk/schema.d.ts +197 -0
- package/bundled/versions.json +8 -0
- package/package.json +53 -3
- package/src/bm25.mjs +69 -0
- package/src/browser.mjs +123 -0
- package/src/cli.mjs +127 -0
- package/src/devserver.mjs +82 -0
- package/src/docs.mjs +329 -0
- package/src/index.mjs +33 -0
- package/src/kit.mjs +150 -0
- package/src/manifest.mjs +82 -0
- package/src/prompts.mjs +97 -0
- package/src/scaffold.mjs +471 -0
- package/src/schemas.mjs +127 -0
- package/src/server.mjs +595 -0
- package/src/theme.mjs +91 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tanqory Theme Runtime ABI v2 — RenderContext and entry types.
|
|
3
|
+
*
|
|
4
|
+
* Package: @tanqory/theme-abi (planned). This file is normative: a theme
|
|
5
|
+
* compiles against it; the gateway/loader constructs exactly this object; the
|
|
6
|
+
* conformance runner (workerd) asserts both sides. The ABI OWNS the capability
|
|
7
|
+
* identifiers, the limits table and the ctx shape (J arbitration A1);
|
|
8
|
+
* capability-security-v1.md cites the `Capability` type below.
|
|
9
|
+
*
|
|
10
|
+
* Rules that the type system cannot express are repeated as JSDoc and live in
|
|
11
|
+
* theme-abi-v2.md. Where prose and this file disagree, this file wins for
|
|
12
|
+
* shapes and the prose wins for behaviour.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ───────────────────────────── versioning ─────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** ABI major.minor the host speaks. A theme built for "2.x" runs on any host ≥ 2.x within major 2. */
|
|
18
|
+
export type AbiVersion = `2.${number}`
|
|
19
|
+
|
|
20
|
+
/** Field-type registry version (independent of the ABI; every version within an ABI major stays supported). */
|
|
21
|
+
export type FieldTypeRegistryVersion = `${number}`
|
|
22
|
+
|
|
23
|
+
// ───────────────────────────── capabilities ───────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every capability identifier the platform knows. `manifest.capabilities.server`
|
|
27
|
+
* is what `render()` may use; `manifest.capabilities.browser` is what the
|
|
28
|
+
* theme's hydrated code may call through `/_tq/data/*` and `/_tq/actions/*`.
|
|
29
|
+
*
|
|
30
|
+
* Security semantics (capability-security-v1.md §2) map 1:1:
|
|
31
|
+
* data:shop = data.shop.read (implicit, always granted)
|
|
32
|
+
* data:localization = data.localization.read (implicit)
|
|
33
|
+
* data:catalog = data.catalog.read
|
|
34
|
+
* data:inventory = ProductVariant.quantityAvailable on data:catalog / data:search / recommendation answers (opt-in;
|
|
35
|
+
* present only when the merchant turned on "Show stock levels"; stripped by the receiver otherwise)
|
|
36
|
+
* data:content = data.content.read
|
|
37
|
+
* data:search = data.search
|
|
38
|
+
* data:cart = data.cart.read
|
|
39
|
+
* data:customer = data.customer.read (REDACTED profile only — see CustomerSummary)
|
|
40
|
+
* data:metaobjects, data:products.recommendations = sub-scopes of data.content.read / data.catalog.read
|
|
41
|
+
* actions:cart = actions.cart.*
|
|
42
|
+
* actions:consent = actions.consent.* (implicit)
|
|
43
|
+
* actions:customer = RESERVED. No customer action exists in this ABI (Act-1): login, logout,
|
|
44
|
+
* registration and recovery happen only in the Account BFF. Grants nothing.
|
|
45
|
+
* actions:form.submit= actions.form.submit (never browser-mintable; forms declared in manifest.forms)
|
|
46
|
+
* network:<origin> = the ONLY egress form; proxied, counted as a sub-request
|
|
47
|
+
* preview:renderSection = preview-profile entry may export renderSection
|
|
48
|
+
*/
|
|
49
|
+
export type Capability =
|
|
50
|
+
| 'data:shop' | 'data:localization' | 'data:catalog' | 'data:content' | 'data:search'
|
|
51
|
+
| 'data:cart' | 'data:customer' | 'data:metaobjects' | 'data:products.recommendations'
|
|
52
|
+
| 'data:inventory'
|
|
53
|
+
| 'actions:cart' | 'actions:consent' | 'actions:customer' | 'actions:form.submit'
|
|
54
|
+
| `network:https://${string}`
|
|
55
|
+
| 'preview:renderSection'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ceiling of what may appear in `manifest.capabilities.browser` (security §2.6, A10).
|
|
59
|
+
* `data:metaobjects` is server-only; `data:products.recommendations` and `data:inventory` are browser-mintable.
|
|
60
|
+
* `theme check` and the manifest schema reject anything else.
|
|
61
|
+
*/
|
|
62
|
+
export type BrowserCapability =
|
|
63
|
+
| 'data:shop' | 'data:localization' | 'data:catalog' | 'data:content' | 'data:search'
|
|
64
|
+
| 'data:cart' | 'data:products.recommendations' | 'data:inventory'
|
|
65
|
+
| 'actions:cart' | 'actions:consent'
|
|
66
|
+
|
|
67
|
+
/** Browser subset `tanqory theme init` scaffolds (manifest.capabilities.browser is REQUIRED and explicit — B-6). */
|
|
68
|
+
export type DefaultBrowserCapability =
|
|
69
|
+
| 'data:shop' | 'data:localization' | 'data:catalog' | 'data:content' | 'data:search'
|
|
70
|
+
| 'data:cart' | 'actions:cart' | 'actions:consent'
|
|
71
|
+
|
|
72
|
+
/** Browser-facing platform routes (same origin as the storefront). Wire shapes are part of the ABI. */
|
|
73
|
+
export declare const PLATFORM_ROUTES: {
|
|
74
|
+
/** `GET /_tq/data/<op>?<args>` with `x-tq-csrf-fetch: 1` — ops are the DataClient method paths, e.g. `catalog.product`, `cart.get`. */
|
|
75
|
+
readonly data: '/_tq/data/'
|
|
76
|
+
/** `POST /_tq/actions/<ns>.<name>` with JSON body — e.g. `cart.addLines`, `consent.set`. */
|
|
77
|
+
readonly actions: '/_tq/actions/'
|
|
78
|
+
/** Everything under this prefix is platform-owned and never reaches the theme. */
|
|
79
|
+
readonly reserved: '/_tq/'
|
|
80
|
+
/** `GET /_tq/data/consent.get` → WireEnvelope<ConsentReadout>; browser-mintable under actions:consent. */
|
|
81
|
+
readonly consent: '/_tq/data/consent.get'
|
|
82
|
+
/** `GET /_tq/csrf` with `x-tq-csrf-fetch: 1` → CsrfTokenResponse. Token refresh (ABI §4.1, Client-3). */
|
|
83
|
+
readonly csrf: '/_tq/csrf'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ───────────────────────────── route kinds ────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Platform-defined route kinds. The PLATFORM parses the URL and resolves the
|
|
90
|
+
* kind + handle + params; the theme never re-derives routing from the path.
|
|
91
|
+
*
|
|
92
|
+
* `/_tq/*` (reserved) and `/checkout*`, `/orders*`, `/api/v1/*` (platform-intercepted) never
|
|
93
|
+
* reach the isolate. `/account` and `/account/*` are NOT intercepted: they resolve to the
|
|
94
|
+
* `customer` kind, a theme-rendered shell that holds `<iframe data-tq-slot="account">` and
|
|
95
|
+
* never receives customer PII (ABI §2.1, Act-10). The Account BFF origin is the platform's.
|
|
96
|
+
*/
|
|
97
|
+
export type RouteKind =
|
|
98
|
+
| 'home'
|
|
99
|
+
| 'product'
|
|
100
|
+
| 'collection'
|
|
101
|
+
| 'collections'
|
|
102
|
+
| 'page'
|
|
103
|
+
| 'blog'
|
|
104
|
+
| 'article'
|
|
105
|
+
| 'search'
|
|
106
|
+
| 'cart'
|
|
107
|
+
| 'customer'
|
|
108
|
+
| 'policy'
|
|
109
|
+
| 'not_found'
|
|
110
|
+
| `custom:${string}`
|
|
111
|
+
|
|
112
|
+
export interface RouteInfo {
|
|
113
|
+
kind: RouteKind
|
|
114
|
+
handle?: string
|
|
115
|
+
blogHandle?: string
|
|
116
|
+
/** Template the platform selected for this request (a key of `manifest.templates`). */
|
|
117
|
+
template: string
|
|
118
|
+
/**
|
|
119
|
+
* Decoded params, strings only. Keys the platform sets for built-in kinds (ABI §2.1):
|
|
120
|
+
* - `tag` — the tag in `/blogs/<blog>/tagged/<tag>`; absent without a tag filter.
|
|
121
|
+
* - `path` — the part after `/account/` on the `customer` kind (no leading slash); absent for `/account`.
|
|
122
|
+
* - `page` (collection, collections, blog, search) and `sort` (collection, search) — the query values as received.
|
|
123
|
+
* - `q` — on kind `search`, the search query as received (decoded, untrimmed); absent when the query key is
|
|
124
|
+
* absent. Shopper-typed text: treat it as untrusted and escape it on output.
|
|
125
|
+
* Custom routes add their `:param` captures. Handles are NOT params (`handle`, `blogHandle`).
|
|
126
|
+
*/
|
|
127
|
+
params: Readonly<Record<string, string>>
|
|
128
|
+
/**
|
|
129
|
+
* `https://` + the canonical host + the route's one canonical path. No port, no query, no fragment
|
|
130
|
+
* (ABI §2.4). Nothing in it comes from the request except which route matched, so it is safe to print
|
|
131
|
+
* in `<link rel="canonical">`.
|
|
132
|
+
*/
|
|
133
|
+
canonical: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ───────────────────────────── locale / market ────────────────────────────
|
|
137
|
+
|
|
138
|
+
export interface LocaleInfo {
|
|
139
|
+
/** BCP-47 language tag the platform resolved (e.g. "th", "en-US"). */
|
|
140
|
+
language: string
|
|
141
|
+
/** ISO 3166-1 alpha-2, the same value as `x-tq-country` (`ZZ` when unknown). Reading it marks the render personalised; declare `vary: ['x-tq-country']` to keep it cacheable (ABI §5.1). */
|
|
142
|
+
country: string
|
|
143
|
+
market: string
|
|
144
|
+
/** `locale.currency` is the market's configured currency. It is NOT guaranteed to be the currency of any amount in `ctx.data`: a market the platform cannot price from stored state is answered in the shop's base currency (storefront-query-v1 §2.3.6). A theme MUST format every amount from that amount's own `Money.currencyCode` and MUST NOT format with `locale.currency`. */
|
|
145
|
+
currency: string
|
|
146
|
+
availableLanguages: readonly string[]
|
|
147
|
+
timeZone: string
|
|
148
|
+
/** Optional (independent assessment 2026-10-07): script direction of `language`. Absent ⇒ theme assumes 'ltr'. Resolves `start`/`end` in text_alignment (registry 2). */
|
|
149
|
+
direction?: 'ltr' | 'rtl'
|
|
150
|
+
/**
|
|
151
|
+
* Optional, alpha.12 (storefront-query rev 2.23 §8.8): the store's active markets, ordered by `id`, for a country /
|
|
152
|
+
* currency selector. Absent when the gateway does not know them. Per-store, not per-shopper: reading it does not
|
|
153
|
+
* personalise. A selector switches with `?country=<CC>` (and `?locale=<language>`), ABI §2 "Market and language selection".
|
|
154
|
+
*/
|
|
155
|
+
markets?: readonly MarketInfo[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One active market of the store (alpha.12). `currency` is the market's configured currency (the same caveat as `LocaleInfo.currency`). */
|
|
159
|
+
export interface MarketInfo {
|
|
160
|
+
id: string
|
|
161
|
+
currency: string
|
|
162
|
+
/** ISO 3166-1 alpha-2 codes the market serves. */
|
|
163
|
+
countries: readonly string[]
|
|
164
|
+
/** Languages the market offers; empty ⇒ every language in `availableLanguages`. */
|
|
165
|
+
languages: readonly string[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ───────────────────────────── consent ────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Read-back of the platform consent cookie (security §2.3 `ctx.consent`). Reading it marks the render personalised. */
|
|
171
|
+
export interface ConsentState {
|
|
172
|
+
known: boolean
|
|
173
|
+
analytics: boolean
|
|
174
|
+
marketing: boolean
|
|
175
|
+
functional: boolean
|
|
176
|
+
region?: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Body of `GET /_tq/data/consent.get` (owner approval 2026-10-07). Browser-mintable under
|
|
181
|
+
* `actions:consent`; always `Cache-Control: private, no-store`, `Vary: Cookie`.
|
|
182
|
+
*/
|
|
183
|
+
export type ConsentReadout = ConsentState & { bannerRequired: boolean }
|
|
184
|
+
|
|
185
|
+
// ───────────────────────────── content ────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export interface ContentNode {
|
|
188
|
+
type: string
|
|
189
|
+
/** Pattern ^[A-Za-z0-9][A-Za-z0-9._-]*$ . REQUIRED. */
|
|
190
|
+
id: string
|
|
191
|
+
settings?: Readonly<Record<string, unknown>>
|
|
192
|
+
blocks?: readonly ContentNode[]
|
|
193
|
+
/** Editor-only flag honoured by the platform: a disabled node is REMOVED before the doc reaches render(). */
|
|
194
|
+
disabled?: boolean
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface RegionDoc {
|
|
198
|
+
region: string
|
|
199
|
+
sections: readonly ContentNode[]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Fully resolved page document; shared regions already merged by the platform. */
|
|
203
|
+
export interface PageDoc {
|
|
204
|
+
contentVersion: number
|
|
205
|
+
template: string
|
|
206
|
+
regions: readonly RegionDoc[]
|
|
207
|
+
meta?: Readonly<{ title?: string; description?: string; image?: string; noindex?: boolean }>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface ContentContext {
|
|
211
|
+
/** ContentRevision digest ("sha256:<hex>"). Use in cache hints, never in markup. */
|
|
212
|
+
revision: string
|
|
213
|
+
contentVersion: number
|
|
214
|
+
page: PageDoc
|
|
215
|
+
settings: Readonly<Record<string, unknown>>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ───────────────────────────── assets ─────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export interface ClientAssetEntry {
|
|
221
|
+
/** Relative to `client/` (= assets.base), never begins with `client/` or `/`, e.g. "assets/main-1a2b3c4d.js". URL = `${assets.base}/${file}`; artifact path = `client/${file}`. css[]/imports[] use the same base. */
|
|
222
|
+
file: string
|
|
223
|
+
kind: 'script' | 'style' | 'font' | 'image' | 'other'
|
|
224
|
+
/** Hydration root. 0 or 1 script entries per profile (zero-JS themes have none). */
|
|
225
|
+
isEntry?: boolean
|
|
226
|
+
css?: readonly string[]
|
|
227
|
+
imports?: readonly string[]
|
|
228
|
+
size: number
|
|
229
|
+
/** "sha256:<hex>" */
|
|
230
|
+
digest: string
|
|
231
|
+
/** SRI value "sha256-<base64>" */
|
|
232
|
+
integrity: string
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface AssetsContext {
|
|
236
|
+
/** Absolute origin+prefix, never ends with "/". Immutable per artifact. */
|
|
237
|
+
base: string
|
|
238
|
+
manifest: Readonly<Record<string, ClientAssetEntry>>
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ───────────────────────────── data ───────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Capability-scoped query client. NO URL, NO token, NO query string. Every
|
|
245
|
+
* method is a persisted query the platform ships per ABI minor (sets retained
|
|
246
|
+
* for every minor within the major). Results are cached per
|
|
247
|
+
* (store, dataVersion, query, args) by the host.
|
|
248
|
+
*
|
|
249
|
+
* Counting (limits §8): each method call = 1 data call unless served from the
|
|
250
|
+
* request-scoped memo (same op+args). A ROUND is the set of calls started before
|
|
251
|
+
* any of them resolves — `Promise.all([...])` and concurrently-suspended React
|
|
252
|
+
* trees are one round; a call issued after an earlier call's result is a new
|
|
253
|
+
* round. Streaming does not reset counters. Exceeding a limit throws
|
|
254
|
+
* DataLimitError; if the response has already started streaming the host
|
|
255
|
+
* aborts the body, marks the response not-cacheable and sets `x-tq-degraded: data_limit`.
|
|
256
|
+
*/
|
|
257
|
+
export interface DataClient {
|
|
258
|
+
/** Opaque to themes. The store's published read-model version (storefront-query-v1 §7): it advances only when a theme-visible answer could differ, at most once per 5 s per store. Use it in cache hints only; never parse it, compare it or put it in markup. */
|
|
259
|
+
readonly dataVersion: string
|
|
260
|
+
|
|
261
|
+
/** ONE call for a page: shop, menus, the route's primary resource, and `resolve: true` setting refs. */
|
|
262
|
+
snapshot(opts?: SnapshotOptions): Promise<PageSnapshot>
|
|
263
|
+
|
|
264
|
+
shop: {
|
|
265
|
+
get(): Promise<Shop>
|
|
266
|
+
menus(handles?: readonly string[]): Promise<readonly Menu[]>
|
|
267
|
+
policies(): Promise<readonly Policy[]>
|
|
268
|
+
/** Merchant tracking pixels. In ABI 2.x the data plane returns an empty list: merchant pixel code is not delivered to themes (storefront-query-v1 §2.2.4) until the owner decides the merchant-pixel execution model. A theme MUST render correctly with `pixels` empty and MUST NOT assume a non-empty list on any host. */
|
|
269
|
+
pixels(): Promise<readonly Pixel[]>
|
|
270
|
+
}
|
|
271
|
+
localization: {
|
|
272
|
+
/** Translation bundle for `ctx.locale.language`, merged with the merchant's dashboard overrides. */
|
|
273
|
+
strings(language?: string): Promise<Readonly<Record<string, string>>>
|
|
274
|
+
}
|
|
275
|
+
catalog: {
|
|
276
|
+
/** `collections: true` ⇒ the answer carries `Product.collections` (ABI 2.x additive, 2026-10-13). */
|
|
277
|
+
product(handle: string, opts?: { variantsFirst?: number; collections?: boolean }): Promise<Product | null>
|
|
278
|
+
products(args: ProductsArgs): Promise<Connection<Product>>
|
|
279
|
+
collection(handle: string, opts?: CollectionProductsArgs): Promise<Collection | null>
|
|
280
|
+
collections(args?: { first?: number; after?: string }): Promise<Connection<Collection>>
|
|
281
|
+
/** data:search. `sortKey`/`reverse` order the `products` connection only (ABI 2.x additive, 2026-10-13); absent ⇒ RELEVANCE, the order before they existed. */
|
|
282
|
+
search(args: { query: string; first?: number; after?: string; types?: readonly ('product' | 'article' | 'page' | 'collection')[]; sortKey?: SearchSortKey; reverse?: boolean }): Promise<SearchResult>
|
|
283
|
+
/** data:search */
|
|
284
|
+
predictive(query: string, limit?: number): Promise<PredictiveResult>
|
|
285
|
+
/** data:products.recommendations */
|
|
286
|
+
recommendations(productHandle: string, intent?: 'related' | 'complementary', limit?: number): Promise<readonly Product[]>
|
|
287
|
+
}
|
|
288
|
+
content: {
|
|
289
|
+
page(handle: string): Promise<Page | null>
|
|
290
|
+
blog(handle: string, opts?: { articlesFirst?: number; after?: string; tag?: string }): Promise<Blog | null>
|
|
291
|
+
article(blogHandle: string, handle: string): Promise<Article | null>
|
|
292
|
+
policy(handle: string): Promise<Policy | null>
|
|
293
|
+
template(name: string): Promise<PageDoc | null>
|
|
294
|
+
/** data:metaobjects */
|
|
295
|
+
metaobjects(type: string, args?: { first?: number; after?: string }): Promise<Connection<Metaobject>>
|
|
296
|
+
}
|
|
297
|
+
/** data:cart — the current shopper's cart (identity from the platform cookie; never visible). Forces `private`. */
|
|
298
|
+
cart: {
|
|
299
|
+
get(): Promise<Cart | null>
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* data:customer — CONSERVATIVE DEFAULT pending owner decision D6 (A2).
|
|
303
|
+
* Only the redacted profile. Orders, addresses, full email/phone are NOT a
|
|
304
|
+
* theme capability; the Account BFF renders them inside the account slot iframe on the
|
|
305
|
+
* theme's `/account*` shell (`customer` kind). Forces `private`.
|
|
306
|
+
*/
|
|
307
|
+
customer: {
|
|
308
|
+
current(): Promise<CustomerSummary | null>
|
|
309
|
+
/** Hosted sign-in / sign-out URLs (Account BFF). Pure getters — allowed during GET render, no capability needed. */
|
|
310
|
+
loginUrl(returnTo?: string): string
|
|
311
|
+
logoutUrl(returnTo?: string): string
|
|
312
|
+
/** Hosted account-area URL for links the theme renders. */
|
|
313
|
+
/**
|
|
314
|
+
* Account BFF URL. Use as a link, or as the `src` of
|
|
315
|
+
* `<iframe data-tq-slot="account" sandbox="allow-scripts allow-forms allow-same-origin">`
|
|
316
|
+
* (owner approval 2026-10-07). The frame is the only frame-src origin; PII stays inside it (D6).
|
|
317
|
+
*/
|
|
318
|
+
accountUrl(path?: 'orders' | 'addresses' | 'profile'): string
|
|
319
|
+
/** Account BFF registration / password-recovery URLs. Pure getters; credentials never pass through theme code. */
|
|
320
|
+
registerUrl(returnTo?: string): string
|
|
321
|
+
recoverUrl(returnTo?: string): string
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface SnapshotOptions {
|
|
326
|
+
resolve?: 'all' | 'none' | readonly string[]
|
|
327
|
+
pageSize?: { products?: number; articles?: number; collections?: number }
|
|
328
|
+
/** ABI 2.x additive (2026-10-13): the `collection` route's primary resource carries `Collection.filters`. Ignored on every other route and on refs. */
|
|
329
|
+
facets?: boolean
|
|
330
|
+
/** ABI 2.x additive (2026-10-13): the `product` route's primary resource carries `Product.collections`. Ignored on every other route and on refs. */
|
|
331
|
+
productCollections?: boolean
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface PageSnapshot {
|
|
335
|
+
/** Never null: `data:shop` is implicit and always in the effective capability set (ABI §3, X-14). */
|
|
336
|
+
shop: Shop
|
|
337
|
+
/** Never null, for the same reason. */
|
|
338
|
+
menus: Readonly<Record<string, Menu>>
|
|
339
|
+
resource:
|
|
340
|
+
| { kind: 'product'; product: Product | null }
|
|
341
|
+
| { kind: 'collection'; collection: Collection | null }
|
|
342
|
+
| { kind: 'collections'; collections: Connection<Collection> | null }
|
|
343
|
+
| { kind: 'page'; page: Page | null }
|
|
344
|
+
| { kind: 'blog'; blog: Blog | null }
|
|
345
|
+
/** A denied primary resource has EVERY member null: here both `article` and `blog`. */
|
|
346
|
+
| { kind: 'article'; article: Article | null; blog: Blog | null }
|
|
347
|
+
| { kind: 'policy'; policy: Policy | null }
|
|
348
|
+
| { kind: 'search'; result: SearchResult | null }
|
|
349
|
+
| { kind: 'cart'; cart: Cart | null }
|
|
350
|
+
| { kind: 'customer'; customer: CustomerSummary | null }
|
|
351
|
+
| { kind: 'home' | 'not_found' | `custom:${string}`; resource?: undefined }
|
|
352
|
+
/** key = `${nodeId}.${settingKey}`. Refs whose capability is denied are OMITTED (not null). Ref kinds are exactly the registry's resource field types; `policy` and `metaobject` are not ref kinds. */
|
|
353
|
+
refs: Readonly<Record<string, Product | Collection | Menu | Page | Blog | Article | null>>
|
|
354
|
+
/** Never null. In ABI 2.x the data plane returns an empty list: merchant pixel code is not delivered to themes (storefront-query-v1 §2.2.4) until the owner decides the merchant-pixel execution model. A theme MUST render correctly with `pixels` empty and MUST NOT assume a non-empty list on any host. */
|
|
355
|
+
pixels: readonly Pixel[]
|
|
356
|
+
/** Parts that are `null` because the home cell could not be reached, as opposed to absent data or a denied capability (storefront-query-v1 §10.3). Absent when nothing was unavailable. */
|
|
357
|
+
unavailable?: readonly ('cart' | 'customer')[]
|
|
358
|
+
/**
|
|
359
|
+
* snapshot() NEVER throws CapabilityError (independent assessment 2026-10-07): a primary
|
|
360
|
+
* resource whose capability is not granted is returned as `null` (search: `result: null`),
|
|
361
|
+
* denied refs are omitted, and every capability that caused an omission is listed here.
|
|
362
|
+
* Individual methods (catalog.search, cart.get, …) still throw CapabilityError.
|
|
363
|
+
*/
|
|
364
|
+
denied: readonly Capability[]
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ───────────────────────────── actions ────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Typed mutations, platform-mediated (security §2.3).
|
|
371
|
+
*
|
|
372
|
+
* During a GET render ONLY `cart.create` and `consent.set` are callable
|
|
373
|
+
* (idempotent, token-bound `via:"ssr"`); any other method throws
|
|
374
|
+
* ActionNotAllowedInRender. During a VALID native form POST (ABI §1) the server subset is
|
|
375
|
+
* callable: cart.*, consent.set, form.submit. On a form POST that failed the CSRF or origin
|
|
376
|
+
* check `ctx.actions` is `undefined` and `ctx.request.formError` is set (Act-2).
|
|
377
|
+
* From the browser, `POST /_tq/actions/<ns>.<name>` accepts ONLY actions:cart and
|
|
378
|
+
* actions:consent ops; form.submit and any customer op are `not_browser_mintable` (Act-5).
|
|
379
|
+
* There is no customer action in this ABI (Act-1).
|
|
380
|
+
*/
|
|
381
|
+
export interface Actions {
|
|
382
|
+
cart: {
|
|
383
|
+
create(input?: { lines?: readonly CartLineInput[]; attributes?: Readonly<Record<string, string>>; note?: string }): Promise<Cart>
|
|
384
|
+
addLines(lines: readonly CartLineInput[], version: number): Promise<CartResult>
|
|
385
|
+
updateLines(lines: readonly { id: string; quantity?: number; attributes?: Readonly<Record<string, string>> }[], version: number): Promise<CartResult>
|
|
386
|
+
removeLines(lineIds: readonly string[], version: number): Promise<CartResult>
|
|
387
|
+
updateNote(note: string, version: number): Promise<CartResult>
|
|
388
|
+
applyDiscountCode(code: string, version: number): Promise<CartResult>
|
|
389
|
+
removeDiscountCode(code: string, version: number): Promise<CartResult>
|
|
390
|
+
/** email + country only. */
|
|
391
|
+
setBuyerIdentity(input: { email?: string; countryCode?: string }, version: number): Promise<CartResult>
|
|
392
|
+
/** Platform checkout URL; the theme redirects. Checkout never renders in the theme. */
|
|
393
|
+
checkoutUrl(): Promise<string>
|
|
394
|
+
}
|
|
395
|
+
consent: {
|
|
396
|
+
set(input: { analytics: boolean; marketing: boolean; functional: boolean; region?: string }): Promise<ConsentState>
|
|
397
|
+
}
|
|
398
|
+
/** actions:form.submit — `formId` ∈ manifest.forms; fields allowlisted per form; ≤ 8 KiB (8192 bytes); routed to the merchant inbox via Notify. Server-side (POST) only. */
|
|
399
|
+
form: {
|
|
400
|
+
submit(formId: string, fields: Readonly<Record<string, string>>): Promise<{ ok: true } | { ok: false; reason: 'invalid' | 'rate_limited' | 'unknown_form' }>
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ───────────────────────────── browser wire (/_tq/*) ──────────────────────
|
|
405
|
+
// Independent assessment 2026-10-07 (encoding); CSRF model: owner approval 2026-10-07 (ABI §4.1).
|
|
406
|
+
|
|
407
|
+
/** CSRF names (ABI §4.1). Enforced by the gateway; the token never appears in ctx. */
|
|
408
|
+
export declare const CSRF: {
|
|
409
|
+
/** HttpOnly `__Host-` cookie with the per-browser secret; unreadable by theme JS. */
|
|
410
|
+
readonly cookie: '__Host-tq-csrf'
|
|
411
|
+
/** Request header carrying the token on fetch() to /_tq/actions/*. */
|
|
412
|
+
readonly header: 'x-tq-csrf'
|
|
413
|
+
/** <meta name> the gateway injects (after the cache) holding the token for the client wrapper. */
|
|
414
|
+
readonly meta: 'tq-csrf'
|
|
415
|
+
/** Hidden CSRF field the gateway injects (post-cache) into <form data-tq-form>. Theme never renders it. */
|
|
416
|
+
readonly formField: '_tq_csrf'
|
|
417
|
+
/** Hidden field naming the form, ALSO injected by the gateway from data-tq-form. Theme never renders it. */
|
|
418
|
+
readonly formIdField: '_tq_form'
|
|
419
|
+
/**
|
|
420
|
+
* Same-origin marker, value `1`, sent on EVERY `/_tq/` GET: each /_tq/data/<op> call and the token
|
|
421
|
+
* fetch (ABI §4, Client-6). Required on GET /_tq/csrf; on /_tq/data it supplies the same-origin
|
|
422
|
+
* signal for a browser that sends no `Sec-Fetch-Site`. No signal ⇒ 403 `csrf_failed`. The name is
|
|
423
|
+
* kept from Client-3 although the header is no longer specific to the token fetch.
|
|
424
|
+
*/
|
|
425
|
+
readonly fetchHeader: 'x-tq-csrf-fetch'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 200 body of GET /_tq/csrf: exactly this object, NOT a WireEnvelope. `token` is 43 base64url
|
|
430
|
+
* characters. Every failure of that route uses the failure form of WireEnvelope.
|
|
431
|
+
*/
|
|
432
|
+
export interface CsrfTokenResponse {
|
|
433
|
+
readonly token: string
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Per-render CSP hash headers the THEME asserts (ABI §5, Csp-2). The host never computes a hash for
|
|
438
|
+
* theme-emitted inline content.
|
|
439
|
+
* - x-tq-csp-script-hash: accepted ONLY if the value is in the signed statement's
|
|
440
|
+
* `predicate.inlineScriptHashes` (fixed at build time from src/inline/*.js). Dynamic executable
|
|
441
|
+
* inline scripts are impossible; per-request data goes in <script type="application/json">.
|
|
442
|
+
* - x-tq-csp-style-hash: accepted within fixed host constants (CSP_STYLE_BOUNDS). Exceeding either
|
|
443
|
+
* is the ABI error `csp_style_budget` (security §6.4a).
|
|
444
|
+
* The gateway merges accepted values into the CSP and strips both headers. No 'unsafe-inline' exists.
|
|
445
|
+
*/
|
|
446
|
+
export type CspHashHeader = 'x-tq-csp-script-hash' | 'x-tq-csp-style-hash'
|
|
447
|
+
|
|
448
|
+
/** Fixed host constants (ABI §5). NOT limits-table rows: changing them does not bump limitsVersion. */
|
|
449
|
+
export declare const CSP_STYLE_BOUNDS: {
|
|
450
|
+
readonly maxHashesPerRender: 16
|
|
451
|
+
readonly maxInlineStyleBytesPerRender: 16384
|
|
452
|
+
readonly errorCode: 'csp_style_budget'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Cap on the `a` parameter of GET /_tq/data/<op> (ABI §4, Client-2). One limit stated twice:
|
|
457
|
+
* 3072 bytes of canonical JSON encode to exactly 4096 base64url characters. Over it: 400
|
|
458
|
+
* `invalid_argument`. A fixed host constant, NOT a limits-table row.
|
|
459
|
+
*/
|
|
460
|
+
export declare const BROWSER_DATA_ARGS: {
|
|
461
|
+
readonly maxBytes: 3072
|
|
462
|
+
readonly maxEncodedChars: 4096
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Longest `<op>` of /_tq/data/<op> and longest `<ns>.<name>` of /_tq/actions/<ns>.<name>, in
|
|
467
|
+
* characters (ABI §4.2, Client-4). A longer name is `unknown_op`. A fixed constant, NOT a limits row.
|
|
468
|
+
*/
|
|
469
|
+
export declare const WIRE_NAME_MAX_LENGTH: 64
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Largest value the reference client reports for a `Retry-After` header, in seconds (ABI §4.2,
|
|
473
|
+
* Client-5). A larger header value is clamped to this. The gateway's own default is 60.
|
|
474
|
+
*/
|
|
475
|
+
export declare const RETRY_AFTER_MAX_SECONDS: 3600
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The shim's host channel (ABI §10, Shim-7). Set by the shim on every isolate response after the
|
|
479
|
+
* theme's headers; stripped by the gateway before the shared cache and the shopper. The value is
|
|
480
|
+
* base64url(JCS(record)) with `v: 1`; members are the shim package's `HostRecordV1`.
|
|
481
|
+
*/
|
|
482
|
+
export declare const HOST_RECORD: {
|
|
483
|
+
readonly header: 'x-tq-host-record'
|
|
484
|
+
readonly version: 1
|
|
485
|
+
readonly maxBytes: 16384
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Error codes a /_tq/* response may carry — reuse of capability-security §6.4a plus action reasons.
|
|
490
|
+
* - `rate_limited` and `budget_exhausted` are both 429 and both carry `Retry-After` (seconds).
|
|
491
|
+
* - `csrf_failed` (403) also covers a failed origin check; there is no separate code (ABI §4.1).
|
|
492
|
+
*/
|
|
493
|
+
export type WireErrorCode =
|
|
494
|
+
| 'capability_denied' | 'preview_channel' | 'not_allowed_in_render' | 'budget_exhausted'
|
|
495
|
+
| 'limit_subrequests' | 'rate_limited' | 'invalid_argument' | 'unknown_op'
|
|
496
|
+
| 'cart_version_conflict' | 'csrf_failed' | 'unavailable' | 'not_browser_mintable'
|
|
497
|
+
|
|
498
|
+
/** Every /_tq/data and /_tq/actions response body. HTTP status mirrors `ok` (2xx / 4xx / 5xx). */
|
|
499
|
+
export type WireEnvelope<T> =
|
|
500
|
+
| { ok: true; data: T; dataVersion: string }
|
|
501
|
+
| { ok: false; error: { code: WireErrorCode; message?: string; requestId: string; refusal?: CartRefusal }; data?: T }
|
|
502
|
+
|
|
503
|
+
/** POST /_tq/actions/<ns>.<name> request body (application/json, ≤ 8 KiB). */
|
|
504
|
+
export interface WireActionBody<A = unknown> { v: 2; args: A }
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Optimistic concurrency: a stale `version` returns the current cart, never throws.
|
|
508
|
+
* `refusal` (alpha.13, Cart-1): on `reason: 'invalid'` only, when the home cell said WHY in a way the platform
|
|
509
|
+
* recognises. Optional: a theme that ignores it shows its generic message, as before.
|
|
510
|
+
*/
|
|
511
|
+
export type CartResult =
|
|
512
|
+
| { ok: true; cart: Cart }
|
|
513
|
+
| { ok: false; reason: 'cart_version_conflict'; cart: Cart }
|
|
514
|
+
| { ok: false; reason: 'invalid'; refusal?: CartRefusal }
|
|
515
|
+
| { ok: false; reason: 'rate_limited' | 'unavailable' }
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Why the home cell refused a cart write (alpha.13, Cart-1). Carried by `CartResult` (`reason: 'invalid'`) and by
|
|
519
|
+
* the failure `WireEnvelope` of `cart.create` (`error.code: 'invalid_argument'`). Never a free-text message: the
|
|
520
|
+
* theme words it in the shopper's language. A theme MUST treat an unknown `code` as no refusal at all (more codes
|
|
521
|
+
* may be added in a minor).
|
|
522
|
+
* - `cart_limit_exceeded`: the write would put more of one product in the cart than the merchant's
|
|
523
|
+
* "Maximum quantity per product" allows. `maxQuantity` is that limit (an integer ≥ 1) when the platform knows it.
|
|
524
|
+
* Nothing about stock is implied: the limit exists so that a cart does not reveal stock.
|
|
525
|
+
*/
|
|
526
|
+
export interface CartRefusal { code: 'cart_limit_exceeded'; maxQuantity?: number }
|
|
527
|
+
|
|
528
|
+
// ───────────────────────────── cache / log / misc ─────────────────────────
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Request headers the gateway forwards (security B-2, verbatim). The Request
|
|
532
|
+
* handed to render() is CONSTRUCTED from this allowlist (B3); no other header
|
|
533
|
+
* is ever present — in particular not cookie, authorization, referer,
|
|
534
|
+
* cf-connecting-ip, x-forwarded-*.
|
|
535
|
+
*/
|
|
536
|
+
export type AllowedRequestHeader =
|
|
537
|
+
| 'accept' | 'accept-language' | 'user-agent' | 'host' | 'traceparent'
|
|
538
|
+
| 'sec-ch-ua' | 'sec-ch-ua-mobile' | 'sec-ch-ua-platform'
|
|
539
|
+
| 'x-tq-country' | 'x-tq-market' | 'x-tq-locale'
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Personalising inputs (ABI §2 "Personalisation marking"). A render is personalised when it reads any
|
|
543
|
+
* allowlisted request header except `host`, `x-tq-market`, `x-tq-locale` (so `accept` and
|
|
544
|
+
* `traceparent` personalise too), reads `ctx.locale.country`, `request.hasCart`,
|
|
545
|
+
* `request.isSignedIn` or `ctx.consent`, receives shopper data from `ctx.data`, or calls any
|
|
546
|
+
* `ctx.actions` method. Reads are tracked by host getters; destructuring counts as a read.
|
|
547
|
+
*
|
|
548
|
+
* `VaryKey` lists what `cache.hint({ vary })` ACCEPTS. Only a CACHEABLE DIMENSION makes the response
|
|
549
|
+
* storable (ABI §5.1). In 2.x that is `x-tq-country` alone (it also covers `ctx.locale.country`).
|
|
550
|
+
* Declaring any other key is allowed and yields `private, no-store`. The type is unchanged.
|
|
551
|
+
*/
|
|
552
|
+
export type VaryKey =
|
|
553
|
+
| 'accept-language' | 'user-agent' | 'sec-ch-ua' | 'sec-ch-ua-mobile' | 'sec-ch-ua-platform' | 'x-tq-country'
|
|
554
|
+
| 'hasCart' | 'isSignedIn' | 'consent'
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The BASE cache key is owned by SiteRelease: `host|path|market|locale|releaseId|dataVersion`.
|
|
558
|
+
* `x-tq-market` / `x-tq-locale` are therefore base identity, never VaryKeys; `vary` only APPENDS.
|
|
559
|
+
*/
|
|
560
|
+
export interface CacheHint {
|
|
561
|
+
/** Seconds, clamped [0, 60]: a hint may only tighten the platform default (s-maxage=60). 0 = no-store. */
|
|
562
|
+
maxAge?: number
|
|
563
|
+
/** Seconds, clamped [0, 600]: a hint may only tighten the platform default (swr=600). */
|
|
564
|
+
swr?: number
|
|
565
|
+
/** Depends on cart/customer/consent. Sticky once set. */
|
|
566
|
+
private?: boolean
|
|
567
|
+
/** ≤ 8 tags, ^[a-z0-9:_-]{1,64}$. Platform adds release/store/dataVersion tags itself. */
|
|
568
|
+
tags?: readonly string[]
|
|
569
|
+
/** Which personalising inputs this response varies on. Must be ⊆ VaryKey; unknown keys are rejected. Only cacheable dimensions (ABI §5.1) make the response storable. */
|
|
570
|
+
vary?: readonly VaryKey[]
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export interface CacheApi {
|
|
574
|
+
/** May only TIGHTEN (shorter maxAge/swr, private, more vary keys). A hint that would loosen the platform default is ignored and logged. */
|
|
575
|
+
hint(h: CacheHint): void
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export interface Logger {
|
|
579
|
+
debug(msg: string, fields?: Readonly<Record<string, unknown>>): void
|
|
580
|
+
info(msg: string, fields?: Readonly<Record<string, unknown>>): void
|
|
581
|
+
warn(msg: string, fields?: Readonly<Record<string, unknown>>): void
|
|
582
|
+
error(msg: string, fields?: Readonly<Record<string, unknown>>): void
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface RequestInfo {
|
|
586
|
+
method: 'GET' | 'HEAD' | 'POST'
|
|
587
|
+
/**
|
|
588
|
+
* Built by the gateway, never the raw request URL (ABI §2.4): `https://` + the canonical host (the
|
|
589
|
+
* verified `rel:` host: lowercase, A-labels, no port, no trailing dot) + the resolved path + the query
|
|
590
|
+
* string exactly as received. No fragment. Identical to the `url` of the Request passed to render().
|
|
591
|
+
* The query is client-controlled: treat it as untrusted input.
|
|
592
|
+
*/
|
|
593
|
+
url: string
|
|
594
|
+
/** Exactly the allowlisted headers that were present; nothing else exists on the Request either. */
|
|
595
|
+
headers: Readonly<Partial<Record<AllowedRequestHeader, string>>>
|
|
596
|
+
/** True only when a `__Host-tq-cart` cookie is present and its tag verifies (ABI §2.3, security §2.6) — never its value. A cookie whose tag fails counts as no cart. Reading marks the render personalised. */
|
|
597
|
+
hasCart: boolean
|
|
598
|
+
/** Whether a customer session exists — not who. Reading marks the render personalised. */
|
|
599
|
+
isSignedIn: boolean
|
|
600
|
+
isPreview: boolean
|
|
601
|
+
/**
|
|
602
|
+
* Owner approval 2026-10-07 (clarified for the Nova port). Present ONLY on a CSRF-verified POST
|
|
603
|
+
* from a `<form data-tq-form="<id>">`. The gateway parsed application/x-www-form-urlencoded
|
|
604
|
+
* (≤ 8 KiB, checked before the isolate), stripped `_tq_form` and `_tq_csrf`, validated the form id
|
|
605
|
+
* against manifest.forms, and kept only `manifest.forms[id].fields`. Absent on GET and on any
|
|
606
|
+
* unverified POST. The raw body is never exposed; the Request passed to render() has a null body.
|
|
607
|
+
*/
|
|
608
|
+
form?: Readonly<Record<string, string>>
|
|
609
|
+
/** The validated declared-form id. Present exactly when `form` is present. */
|
|
610
|
+
formId?: string
|
|
611
|
+
/**
|
|
612
|
+
* Set when a native form POST failed the gateway's checks (Act-2): 'csrf' = token or cookie missing or
|
|
613
|
+
* mismatched; 'origin' = Origin / Sec-Fetch-Site not same-origin. The request renders as a normal page:
|
|
614
|
+
* `form` and `formId` are absent and `ctx.actions` is undefined. Absent on GET and on valid POSTs.
|
|
615
|
+
* The response is `private, no-store`.
|
|
616
|
+
*/
|
|
617
|
+
formError?: 'csrf' | 'origin'
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ───────────────────────────── the context ────────────────────────────────
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* MULTI-TENANCY (A7): an isolate MAY serve many stores for one artifactDigest.
|
|
624
|
+
* Theme code MUST NOT retain anything derived from `ctx` or `request` beyond the
|
|
625
|
+
* render that received it (no module-level caches keyed by store, no memoised
|
|
626
|
+
* snapshots, no closures stored on globalThis). Conformance suite 9 renders
|
|
627
|
+
* store A then store B in one isolate and fails on any A-derived byte in B.
|
|
628
|
+
*/
|
|
629
|
+
export interface RenderContext {
|
|
630
|
+
abi: AbiVersion
|
|
631
|
+
/**
|
|
632
|
+
* `serve` — a shopper on a published host.
|
|
633
|
+
* `preview` — the editor canvas or a preview channel.
|
|
634
|
+
* `check` — DEPRECATED and never set. Conformance renders with `serve` or `preview` so that a theme
|
|
635
|
+
* cannot tell it is being certified (ABI §10, security v1.10.8). Kept in the type until its
|
|
636
|
+
* removal is approved; do not branch on it.
|
|
637
|
+
*/
|
|
638
|
+
mode: 'serve' | 'preview' | 'check'
|
|
639
|
+
request: RequestInfo
|
|
640
|
+
route: RouteInfo
|
|
641
|
+
locale: LocaleInfo
|
|
642
|
+
consent: ConsentState
|
|
643
|
+
content: ContentContext
|
|
644
|
+
assets: AssetsContext
|
|
645
|
+
/** Aborted on wall-clock, data-limit breach after first byte, or client disconnect. */
|
|
646
|
+
signal: AbortSignal
|
|
647
|
+
data: DataClient
|
|
648
|
+
/** `undefined` on a POST the gateway could not CSRF-verify (B6). Always defined on GET (with the SSR-allowed subset). */
|
|
649
|
+
actions: Actions | undefined
|
|
650
|
+
cache: CacheApi
|
|
651
|
+
log: Logger
|
|
652
|
+
/**
|
|
653
|
+
* Request time (ms since epoch), frozen for the whole render — a HOST WRAPPER
|
|
654
|
+
* (A4). The runtime clocks are NOT frozen: `Date.now()` and `performance.now()`
|
|
655
|
+
* are coarsened to 100 µs (security B-1). "One clock per render" is therefore a
|
|
656
|
+
* CONFORMANCE guarantee — suite 3 fails a theme whose render calls
|
|
657
|
+
* Date.now()/performance.now() — not a runtime one.
|
|
658
|
+
*/
|
|
659
|
+
now(): number
|
|
660
|
+
release: { id: string; artifactDigest: string }
|
|
661
|
+
/** server grant = manifest.capabilities.server ∩ store grant (capPlan). */
|
|
662
|
+
capabilities: ReadonlySet<Capability>
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ───────────────────────────── entry module ───────────────────────────────
|
|
666
|
+
|
|
667
|
+
export interface ThemeEntry {
|
|
668
|
+
render(request: Request, ctx: RenderContext): Promise<Response>
|
|
669
|
+
/** Equal to manifest.json file bytes minus the trailing LF after JCS canonicalisation (S3). */
|
|
670
|
+
manifest: ThemeManifest
|
|
671
|
+
/** Preview profile only. */
|
|
672
|
+
renderSection?(input: { type: string; settings: Readonly<Record<string, unknown>>; blocks?: readonly ContentNode[] }, ctx: RenderContext): Promise<Response>
|
|
673
|
+
/** Pure, synchronous; run by the platform in the sandbox at release composition, never per request. */
|
|
674
|
+
migrate?(doc: unknown, from: number, to: number): unknown
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export interface ThemeManifest {
|
|
678
|
+
abi: AbiVersion
|
|
679
|
+
name: string
|
|
680
|
+
version: string
|
|
681
|
+
engines: { 'tanqory-abi': string; 'tanqory-field-types': FieldTypeRegistryVersion }
|
|
682
|
+
/**
|
|
683
|
+
* Security B-6: both arrays REQUIRED and explicit; browser ⊆ server and
|
|
684
|
+
* browser ⊆ BrowserCapability (never data:customer / actions:customer /
|
|
685
|
+
* actions:form.submit). The builder copies both verbatim into the signed
|
|
686
|
+
* statement; the runtime uses the statement's copy, never the manifest file.
|
|
687
|
+
* `tanqory theme init` scaffolds `browser` with DefaultBrowserCapability.
|
|
688
|
+
*/
|
|
689
|
+
capabilities: {
|
|
690
|
+
server: readonly Capability[]
|
|
691
|
+
browser: readonly BrowserCapability[]
|
|
692
|
+
}
|
|
693
|
+
/** Forms usable with actions:form.submit. */
|
|
694
|
+
forms?: Readonly<Record<string, { fields: readonly string[]; title?: string }>>
|
|
695
|
+
client: {
|
|
696
|
+
/** none = zero-JS/HTMX themes (no isEntry script; hydration-parity suite skipped). */
|
|
697
|
+
hydration: 'none' | 'full' | 'partial'
|
|
698
|
+
}
|
|
699
|
+
contentVersion: number
|
|
700
|
+
routes: Readonly<Partial<Record<RouteKind, string>>> & Readonly<Record<`custom:${string}`, string>>
|
|
701
|
+
customRoutes?: Readonly<Record<string, { path: string }>>
|
|
702
|
+
templates: Readonly<Record<string, { route: RouteKind; title?: string; regions: readonly string[] }>>
|
|
703
|
+
regions: Readonly<Record<string, { title: string; scope: 'shared' | 'template'; accepts?: readonly string[]; min?: number; max?: number }>>
|
|
704
|
+
components: Readonly<Record<string, ComponentDef>>
|
|
705
|
+
settingsSchema: readonly SettingsGroup[]
|
|
706
|
+
/** Must chain, gap-free, from `minContentVersion` up to `contentVersion`. */
|
|
707
|
+
migrations?: readonly { from: number; to: number; note?: string }[]
|
|
708
|
+
/**
|
|
709
|
+
* Oldest content version this artifact's `migrate` accepts. Older docs are migrated by the
|
|
710
|
+
* platform using the PREVIOUS installed artifact's migrate chain first (S5).
|
|
711
|
+
* When absent, equals `contentVersion`; no migration chain is required.
|
|
712
|
+
* `minContentVersion > contentVersion` is invalid (`migration_range_invalid`).
|
|
713
|
+
*/
|
|
714
|
+
minContentVersion?: number
|
|
715
|
+
[vendor: `x-${string}`]: unknown
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export interface ComponentDef {
|
|
719
|
+
role: 'section' | 'block' | 'layout'
|
|
720
|
+
title: string
|
|
721
|
+
description?: string
|
|
722
|
+
category?: string
|
|
723
|
+
icon?: string
|
|
724
|
+
regions?: readonly string[]
|
|
725
|
+
parents?: readonly string[]
|
|
726
|
+
children?: readonly string[]
|
|
727
|
+
requires?: readonly RouteKind[]
|
|
728
|
+
attributes: Readonly<Record<string, FieldSpec>>
|
|
729
|
+
presets?: readonly { name?: string; settings?: Readonly<Record<string, unknown>>; blocks?: readonly ContentNode[] }[]
|
|
730
|
+
limits?: { maxBlocks?: number }
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export interface FieldSpec {
|
|
734
|
+
/** Id in the field-type registry version named by engines["tanqory-field-types"]. Registry 1 adds generic `json` and `list`. */
|
|
735
|
+
type: string
|
|
736
|
+
label?: string
|
|
737
|
+
info?: string
|
|
738
|
+
default?: unknown
|
|
739
|
+
placeholder?: string
|
|
740
|
+
group?: string
|
|
741
|
+
min?: number
|
|
742
|
+
max?: number
|
|
743
|
+
step?: number
|
|
744
|
+
unit?: string
|
|
745
|
+
options?: readonly { value: string; label: string }[]
|
|
746
|
+
visible_if?: string | Readonly<Record<string, unknown>>
|
|
747
|
+
dynamic?: boolean
|
|
748
|
+
resolve?: boolean
|
|
749
|
+
/** `list` only: element field spec. `json` only: optional JSON Schema (draft 2020-12, ≤ 8 KB) the editor validates against. */
|
|
750
|
+
items?: FieldSpec
|
|
751
|
+
schema?: Readonly<Record<string, unknown>>
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export interface SettingsGroup { group: string; items: readonly ({ key: string } & FieldSpec)[] }
|
|
755
|
+
|
|
756
|
+
// ───────────────────────────── domain types (stable, trimmed) ────────────
|
|
757
|
+
|
|
758
|
+
/** Format every amount from its own `currencyCode`, never from `locale.currency` (see LocaleInfo.currency). */
|
|
759
|
+
export interface Money { amount: string; currencyCode: string }
|
|
760
|
+
export interface Image { url: string; altText?: string | null; width?: number; height?: number; focal?: { x: number; y: number } }
|
|
761
|
+
export interface Seo { title?: string | null; description?: string | null }
|
|
762
|
+
export interface Connection<T> { nodes: readonly T[]; pageInfo: { hasNextPage: boolean; endCursor?: string | null }; totalCount?: number }
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* `socialLinks` and `paymentSettings.acceptedCardBrands` (ABI 2.x additive, 2026-10-13; storefront-query rev 2.20):
|
|
766
|
+
* present only when the store has them — a theme MUST render without them. `acceptedCardBrands` is card brand NAMES
|
|
767
|
+
* only (payment marks); nothing else of the store's payment configuration reaches a theme.
|
|
768
|
+
*/
|
|
769
|
+
export interface Shop { name: string; description?: string | null; primaryDomain: { url: string }; brand?: { logo?: Image | null; squareLogo?: Image | null; colors?: Readonly<Record<string, string>> }; paymentSettings: { currencyCode: string; enabledPresentmentCurrencies: readonly string[]; acceptedCardBrands?: readonly CardBrand[] }; shipsToCountries: readonly string[]; socialLinks?: readonly ShopSocialLink[]; seo?: ShopSeo }
|
|
770
|
+
/** alpha.12: the home page's SEO settings (Online store → Preferences: home page title, meta description, social sharing image). Each member only when the merchant set it; absent ⇒ the theme falls back to its own store data. */
|
|
771
|
+
export interface ShopSeo { title?: string; description?: string; image?: Image }
|
|
772
|
+
/** A public social profile link. `platform` is a lower-case network slug (`instagram`, `facebook`, `tiktok`, `x`/`twitter`, …); themes pick an icon by it and MUST fall back for an unknown one. `url` is absolute `https:`. */
|
|
773
|
+
export interface ShopSocialLink { platform: string; url: string }
|
|
774
|
+
export type CardBrand = 'VISA' | 'MASTERCARD' | 'AMERICAN_EXPRESS' | 'JCB' | 'DISCOVER' | 'DINERS_CLUB' | 'UNIONPAY'
|
|
775
|
+
/** `url` is an origin-relative path beginning with `/`, or an absolute `https:` URL for external links. */
|
|
776
|
+
export interface MenuItem { id: string; title: string; url: string; type: string; resourceHandle?: string; items: readonly MenuItem[] }
|
|
777
|
+
export interface Menu { handle: string; title: string; items: readonly MenuItem[] }
|
|
778
|
+
/** `url` is an absolute URL on the shop's primary domain. */
|
|
779
|
+
export interface Policy { handle: string; title: string; body: string; url: string }
|
|
780
|
+
export interface Pixel { id: string; code: string }
|
|
781
|
+
export interface ProductOption { name: string; values: readonly string[] }
|
|
782
|
+
/**
|
|
783
|
+
* Never cost/supplier. `quantityAvailable` (ABI 2.x additive, 2026-10-13; storefront-query rev 2.21): an integer 0–999
|
|
784
|
+
* (999 = "999 or more") present ONLY when (1) the release holds `data:inventory`, (2) the merchant turned on
|
|
785
|
+
* "Show stock levels on your online store", and (3) the variant's inventory is tracked and it does not continue selling
|
|
786
|
+
* when out of stock. Absent otherwise — never `0` for "unknown": a theme MUST render no stock count when it is absent.
|
|
787
|
+
* Cart lines (`CartLine.merchandise`) never carry it.
|
|
788
|
+
*/
|
|
789
|
+
export interface ProductVariant { id: string; title: string; sku?: string | null; availableForSale: boolean; price: Money; compareAtPrice?: Money | null; selectedOptions: readonly { name: string; value: string }[]; image?: Image | null; quantityAvailable?: number }
|
|
790
|
+
/** `collections`: only when asked for (`catalog.product(…, { collections: true })`, `SnapshotOptions.productCollections`) — the collections the product is a visible member of, in `catalog.collections` order, at most 10. */
|
|
791
|
+
export interface Product { id: string; handle: string; title: string; descriptionHtml: string; vendor?: string; productType?: string; tags: readonly string[]; availableForSale: boolean; featuredImage?: Image | null; images: readonly Image[]; options: readonly ProductOption[]; variants: Connection<ProductVariant>; priceRange: { minVariantPrice: Money; maxVariantPrice: Money }; compareAtPriceRange?: { minVariantPrice: Money; maxVariantPrice: Money }; seo: Seo; templateSuffix?: string | null; metafields?: Readonly<Record<string, unknown>>; collections?: readonly ProductCollectionRef[] }
|
|
792
|
+
/** A collection a product belongs to (breadcrumbs, "more from"). */
|
|
793
|
+
export interface ProductCollectionRef { handle: string; title: string }
|
|
794
|
+
/** `filters`: only when asked for (`CollectionProductsArgs.facets`, `SnapshotOptions.facets`). */
|
|
795
|
+
export interface Collection { id: string; handle: string; title: string; descriptionHtml: string; image?: Image | null; products: Connection<Product>; seo: Seo; templateSuffix?: string | null; filters?: readonly Filter[] }
|
|
796
|
+
/**
|
|
797
|
+
* A facet of a collection's products (ABI 2.x additive, 2026-10-13). Counts are over the collection's products visible in
|
|
798
|
+
* the request's market, IGNORING the request's own `filters` — a value never vanishes while the shopper narrows.
|
|
799
|
+
* `id`s are stable: `filter.available`, `filter.vendor`, `filter.productType`, `filter.price`; a value's id is
|
|
800
|
+
* `${filter.id}.${value}`. `label` is an English default; themes translate by `id`.
|
|
801
|
+
* LIST / BOOLEAN: `values` (a LIST facet holds at most 100, by value). PRICE_RANGE: `values` is empty and `range`
|
|
802
|
+
* is the lowest and highest product price (`priceRange.minVariantPrice`) in the market; filter with `price.min`/`price.max`.
|
|
803
|
+
* A facet with no value is omitted.
|
|
804
|
+
*/
|
|
805
|
+
export interface Filter { id: string; label: string; type: 'LIST' | 'BOOLEAN' | 'PRICE_RANGE'; values: readonly FilterValue[]; range?: { min: Money; max: Money } }
|
|
806
|
+
/** `input` is exactly one entry of `CollectionProductsArgs.filters` that selects this value. */
|
|
807
|
+
export interface FilterValue { id: string; label: string; count: number; input: { key: string; value: string } }
|
|
808
|
+
export interface Page { id: string; handle: string; title: string; body: string; seo: Seo; templateSuffix?: string | null }
|
|
809
|
+
export interface Article { id: string; handle: string; title: string; excerpt?: string | null; contentHtml: string; image?: Image | null; publishedAt: string; author?: { name: string } | null; tags: readonly string[]; blog: { handle: string; title: string }; seo: Seo }
|
|
810
|
+
export interface Blog { id: string; handle: string; title: string; articles: Connection<Article>; seo: Seo }
|
|
811
|
+
export interface Metaobject { id: string; handle: string; type: string; fields: Readonly<Record<string, unknown>> }
|
|
812
|
+
export interface SearchResult { query: string; products: Connection<Product>; articles: Connection<Article>; pages: Connection<Page>; collections: Connection<Collection> }
|
|
813
|
+
export interface PredictiveResult { products: readonly Pick<Product, 'handle' | 'title' | 'featuredImage' | 'priceRange'>[]; collections: readonly Pick<Collection, 'handle' | 'title'>[]; queries: readonly string[] }
|
|
814
|
+
export interface CartLineInput { merchandiseId: string; quantity: number; attributes?: Readonly<Record<string, string>> }
|
|
815
|
+
export interface CartLine { id: string; quantity: number; merchandise: ProductVariant & { product: Pick<Product, 'handle' | 'title' | 'featuredImage'> }; cost: { totalAmount: Money; subtotalAmount: Money }; attributes: Readonly<Record<string, string>> }
|
|
816
|
+
/** `id` is visible to theme code and is NOT a credential: knowing a cart id never grants access to the cart (security §2.6). */
|
|
817
|
+
export interface Cart { id: string; version: number; totalQuantity: number; lines: readonly CartLine[]; cost: { subtotalAmount: Money; totalAmount: Money; totalTaxAmount?: Money | null }; discountCodes: readonly { code: string; applicable: boolean }[]; note?: string | null; checkoutUrl: string }
|
|
818
|
+
/** Redacted (security §2.2). CONSERVATIVE DEFAULT until decision D6. */
|
|
819
|
+
export interface CustomerSummary { id: string; firstName?: string | null; lastName?: string | null; emailMasked?: string | null; defaultAddress?: { city?: string | null; country: string } | null; isLoggedIn: true }
|
|
820
|
+
export interface ProductsArgs { first?: number; after?: string; query?: string; sortKey?: 'TITLE' | 'PRICE' | 'CREATED_AT' | 'BEST_SELLING' | 'RELEVANCE'; reverse?: boolean }
|
|
821
|
+
/** Search `products` order (ABI 2.x additive, 2026-10-13). RELEVANCE = §9.6.6; the others as `catalog.products`. */
|
|
822
|
+
export type SearchSortKey = 'RELEVANCE' | 'PRICE' | 'TITLE' | 'CREATED_AT' | 'BEST_SELLING'
|
|
823
|
+
/** `filters` keys: `tag`, `vendor`, `productType`, `available` (`'true'`/`'false'`), `price.min`, `price.max` (decimal amounts). `facets: true` ⇒ the answer carries `Collection.filters`. */
|
|
824
|
+
export interface CollectionProductsArgs { productsFirst?: number; after?: string; sortKey?: 'MANUAL' | 'TITLE' | 'PRICE' | 'CREATED' | 'BEST_SELLING'; reverse?: boolean; filters?: readonly { key: string; value: string }[]; facets?: boolean }
|
|
825
|
+
|
|
826
|
+
// ───────────────────────────── limits (export shape) ──────────────────────
|
|
827
|
+
// Exported by `@tanqory/theme-abi/limits` (ABI §8). One entry per table row; the key is also the
|
|
828
|
+
// conformance-report measurement `name`.
|
|
829
|
+
|
|
830
|
+
/** runtime = measured per render by the runner · static = artifact bytes / manifest + content structure (suite 1) · host-policy = enforced by host/edge, never measured. */
|
|
831
|
+
export type LimitClass = 'runtime' | 'static' | 'host-policy'
|
|
832
|
+
|
|
833
|
+
export interface LimitSpec {
|
|
834
|
+
readonly class: LimitClass
|
|
835
|
+
/** Warn / slow-flag threshold; null where §8 shows "—". */
|
|
836
|
+
readonly budget: number | null
|
|
837
|
+
/** Kill / reject threshold; null ONLY for budget-only rows (e.g. clientInitialTransferredGzipBytes). */
|
|
838
|
+
readonly hard: number | null
|
|
839
|
+
/** Preview-only rows: exempt from signing rule 6 while suite 6 is not_applicable. */
|
|
840
|
+
readonly profile?: 'preview'
|
|
841
|
+
/** memoryBytes only: may be not_measured (runtime-enforced; known runner limitation). */
|
|
842
|
+
readonly rule6Exempt?: true
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ───────────────────────────── errors ─────────────────────────────────────
|
|
846
|
+
|
|
847
|
+
export declare class CapabilityError extends Error { readonly capability: Capability }
|
|
848
|
+
export declare class DataLimitError extends Error { readonly limit: 'dataCallsPerRender' | 'subRequestsPerRender' | 'sequentialRounds' | 'networkCalls' }
|
|
849
|
+
export declare class ActionNotAllowedInRender extends Error { readonly action: string }
|