@tokenoftrust/storefront-runner 2.0.12 → 2.0.14

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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "announcement": "Free Shipping on Orders $100",
3
+ "nav": [
4
+ {
5
+ "label": "Home",
6
+ "href": "/"
7
+ },
8
+ {
9
+ "label": "Bundles",
10
+ "href": "/collections/bundles"
11
+ },
12
+ {
13
+ "label": "Contact",
14
+ "href": "/pages/contact"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "seoTitle": "Vape Nicotine Free",
3
+ "seoDescription": "Vape Nicotine Free",
4
+ "blocks": [
5
+ {
6
+ "component": "hero",
7
+ "headline": "Vape Nicotine Free",
8
+ "subhead": "Vape Nicotine Free",
9
+ "image": "https://vapenicotinefree.com/cdn/shop/files/WhitePeachRaspberryNONIC25KVape.jpg?v=1762355719",
10
+ "imageAlt": "NONIC White Peach Raspberry 25K ZERO Nicotine Disposable vape with bold design and packaging.",
11
+ "ctaLabel": "Shop all",
12
+ "ctaHref": "/collections"
13
+ },
14
+ {
15
+ "component": "promo_tiles",
16
+ "tiles": [
17
+ {
18
+ "label": "Gold Bar Max 50K",
19
+ "href": "/collections/gold-bar-max-50k-nixodine-disposable-vapes",
20
+ "image": "https://vapenicotinefree.com/cdn/shop/files/Gold_Bar_Max_50K_Slider.jpg?v=1776040453"
21
+ },
22
+ {
23
+ "label": "ARRO Vape",
24
+ "href": "/collections/arro-vape",
25
+ "image": "https://vapenicotinefree.com/cdn/shop/files/ARRO_Vape_Home_Collection.jpg?v=1775859158"
26
+ },
27
+ {
28
+ "label": "NoNiC",
29
+ "href": "/collections/nonic",
30
+ "image": "https://vapenicotinefree.com/cdn/shop/files/NoNiC_Home_Collection.jpg?v=1775859212"
31
+ },
32
+ {
33
+ "label": "iJoy",
34
+ "href": "/collections/ijoy",
35
+ "image": "https://vapenicotinefree.com/cdn/shop/files/iJoy_Home_Collection.jpg?v=1775859427"
36
+ },
37
+ {
38
+ "label": "MELO Labs",
39
+ "href": "/collections/melo-labs",
40
+ "image": "https://vapenicotinefree.com/cdn/shop/files/MELO_Labs_56db657d-41c5-42be-863b-1008e750a2b7.jpg?v=1775859566"
41
+ },
42
+ {
43
+ "label": "Gold Bar Vape",
44
+ "href": "/collections/gold-bar",
45
+ "image": "https://vapenicotinefree.com/cdn/shop/files/Gold_Bar.jpg?v=1775866073"
46
+ },
47
+ {
48
+ "label": "Cali Pods",
49
+ "href": "/collections/cali-pods",
50
+ "image": "https://vapenicotinefree.com/cdn/shop/files/Cali_Pods_Home_Collection.jpg?v=1775859249"
51
+ },
52
+ {
53
+ "label": "RAZ Vape",
54
+ "href": "/collections/raz-vape",
55
+ "image": "https://vapenicotinefree.com/cdn/shop/files/RAZ_Vape_Home_Collection.jpg?v=1775859313"
56
+ }
57
+ ]
58
+ },
59
+ {
60
+ "component": "featured_collections",
61
+ "handles": [
62
+ "all-products",
63
+ "fruit-flavor-vapes",
64
+ "ijoy",
65
+ "ijoy-xp-100k-nixodine-disposable-vapes",
66
+ "menthol-flavor-vapes",
67
+ "nixodine"
68
+ ]
69
+ },
70
+ {
71
+ "component": "featured_products",
72
+ "source": "featured",
73
+ "limit": 8
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "slug": "about",
3
+ "title": "about",
4
+ "body_html": "<h2>About Vape Nicotine Free</h2>\n \n \n \n \n \n \n <a>\n \n</a>\n \n \n \n \n\n\n\n\n \n <ul>\n \n <li>\n \n \n \n 50,000+ Happy Customers\n \n \n <p>We've consistently created online retail spaces for nicotine vapers, and now another for nicotine-free vapers.</p>\n \n \n \n </li>\n \n <li>\n \n \n \n Goal-Driven\n \n \n <p>Our goal for VNF is to create a single location for customers seeking nicotine-free vapes, and nicotine-free vapes only.</p>\n \n \n \n </li>\n \n <li>\n \n \n \n 15 Years of Vape Experience\n \n \n <p>We've been at the forefront of vaping retail, having created some of the most recognized stores in vaping.</p>\n \n \n \n </li>\n \n </ul>\n \n\n\n\n\n \n \n \n <a href=\"https://vapenicotinefree.com/collections/geek-bar/products/geek-bar-pulse-x-strawberry-b-pop-25k-zero-nicotine-disposable\">\n \n</a>\n \n \n \n \n \n <ul>\n \n <li>\n \n \n \n Breathe Easy. Vape Without Limits.\n \n \n <p>Break free from nicotine and take control of your experience. Our nicotine-free disposables let you enjoy vaping without dependency — a cleaner, clearer choice for balance, wellness, and peace of mind.</p>\n \n \n \n </li>\n \n <li>\n \n \n \n Pure Flavor. Pure Enjoyment.\n \n \n <p>Dive into rich, full-bodied flavor without compromise. Every puff delivers smooth satisfaction and premium taste — designed purely for pleasure, not addiction.</p>\n \n \n \n </li>\n \n </ul>\n \n \n\n\n\n \n \n \n\n\n\n\n\n \n \n \n \n <a>\n \n \n \n \n \n \n \n EST 2025 • Vape Nicotine Free •\n \n \n \n </a>\n \n \n \n \n <a href=\"/collections/nicotine-free-vape\">\n</a>\n \n \n \n \n Our Story\n \n \n <p>Vape Nicotine Free was created after we saw a clear gap in the market — those looking for a nicotine-free experience had to search through countless nicotine vapes just to find what they wanted. We built a store dedicated entirely to zero-nicotine disposable vapes, making it easy to find products that are 100% nicotine-free. Our mission is simple: to offer a clean, enjoyable vaping experience for those who value flavor and peace of mind without the nicotine.</p>\n \n \n <a href=\"/collections/nicotine-free-vape\">Shop now</a>"
5
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "slug": "privacy",
3
+ "title": "Privacy Policy",
4
+ "isPolicy": true,
5
+ "body_html": "<p>Last updated: January 13, 2026</p>\n<p>Vape Nicotine Free operates this store and website, including all related information, content, features, tools, products and services, in order to provide you, the customer, with a curated shopping experience (the \"Services\"). Vape Nicotine Free is powered by Shopify, which enables us to provide the Services to you. This Privacy Policy describes how we collect, use, and disclose your personal information when you visit, use, or make a purchase or other transaction using the Services or otherwise communicate with us. If there is a conflict between our Terms of Service and this Privacy Policy, this Privacy Policy controls with respect to the collection, processing, and disclosure of your personal information.</p>\n<p>Please read this Privacy Policy carefully. By using and accessing any of the Services, you acknowledge that you have read this Privacy Policy and understand the collection, use, and disclosure of your information as described in this Privacy Policy.</p>\n<h2>Personal Information We Collect or Process</h2>\n<p>When we use the term \"personal information,\" we are referring to information that identifies or can reasonably be linked to you or another person. Personal information does not include information that is collected anonymously or that has been de-identified, so that it cannot identify or be reasonably linked to you. We may collect or process the following categories of personal information, including inferences drawn from this personal information, depending on how you interact with the Services, where you live, and as permitted or required by applicable law:</p>\n<ul>\n<li>\n<strong>Contact details</strong> including your name, address, billing address, shipping address, phone number, and email address.</li>\n<li>\n<strong>Financial information</strong> including credit card, debit card, and financial account numbers, payment card information, financial account information, transaction details, form of payment, payment confirmation and other payment details.</li>\n<li>\n<strong>Account information</strong> including your username, password, security questions, preferences and settings.</li>\n<li>\n<strong>Transaction information</strong> including the items you view, put in your cart, add to your wishlist, or purchase, return, exchange or cancel and your past transactions.</li>\n<li>\n<strong>Communications with us</strong> including the information you include in communications with us, for example, when sending a customer support inquiry.</li>\n<li>\n<strong>Device information</strong> including information about your device, browser, or network connection, your IP address, and other unique identifiers.</li>\n<li>\n<strong>Usage information</strong> including information regarding your interaction with the Services, including how and when you interact with or navigate the Services.</li>\n</ul>\n<h2>Personal Information Sources</h2>\n<p>We may collect personal information from the following sources:</p>\n<ul>\n<li>\n<strong>Directly from you</strong> including when you create an account, visit or use the Services, communicate with us, or otherwise provide us with your personal information;</li>\n<li>\n<strong>Automatically through the Services</strong> including from your device when you use our products or services or visit our websites, and through the use of cookies and similar technologies;</li>\n<li>\n<strong>From our service providers</strong> including when we engage them to enable certain technology and when they collect or process your personal information on our behalf;</li>\n<li><strong>From our partners or other third parties.</strong></li>\n</ul>\n<h2>How We Use Your Personal Information</h2>\n<p>Depending on how you interact with us or which of the Services you use, we may use personal information for the following purposes:</p>\n<ul>\n<li>\n<strong>Provide, Tailor, and Improve the Services.</strong> We use your personal information to provide you with the Services, including to perform our contract with you, to process your payments, to fulfill your orders, to remember your preferences and items you are interested in, to send notifications to you related to your account, to process purchases, returns, exchanges or other transactions, to create, maintain and otherwise manage your account, to arrange for shipping, to facilitate any returns and exchanges, to enable you to post reviews, and to create a customized shopping experience for you, such as recommending products related to your purchases. This may include using your personal information to better tailor and improve the Services.</li>\n<li>\n<strong>Marketing and Advertising.</strong> We use your personal information for marketing and promotional purposes, such as to send marketing, advertising and promotional communications by email, text message or postal mail, and to show you online advertisements for products or services on the Services or other websites, including based on items you previously have purchased or added to your cart and other activity on the Services.</li>\n<li>\n<strong>Security and Fraud Prevention.</strong> We use your personal information to authenticate your account, to provide a secure payment and shopping experience, detect, investigate or take action regarding possible fraudulent, illegal, unsafe, or malicious activity, protect public safety, and to secure our services. If you choose to use the Services and register an account, you are responsible for keeping your account credentials safe. We highly recommend that you do not share your username, password or other access details with anyone else.</li>\n<li>\n<strong>Communicating with You.</strong> We use your personal information to provide you with customer support, to be responsive to you, to provide effective services to you and to maintain our business relationship with you.</li>\n<li>\n<strong>Legal Reasons.</strong> We use your personal information to comply with applicable law or respond to valid legal process, including requests from law enforcement or government agencies, to investigate or participate in civil discovery, potential or actual litigation, or other adversarial legal proceedings, and to enforce or investigate potential violations of our terms or policies.</li>\n</ul>\n<h2>How We Disclose Personal Information</h2>\n<p>In certain circumstances, we may disclose your personal information to third parties for legitimate purposes subject to this Privacy Policy. Such circumstances may include:</p>\n<ul>\n<li>With Shopify, vendors and other third parties who perform services on our behalf (e.g. IT management, payment processing, data analytics, customer support, cloud storage, fulfillment and shipping).</li>\n<li>With business and marketing partners to provide marketing services and advertise to you. For example, we use Shopify to support personalized advertising with third-party services based on your online activity with different merchants and websites. Our business and marketing partners will use your information in accordance with their own privacy notices. Depending on where you reside, you may have a right to direct us not to share information about you to show you targeted advertisements and marketing based on your online activity with different merchants and websites. You can exercise your rights to opt-out of those uses <a href=\"https://vapenicotinefree.com/pages/data-sharing-opt-out\"> here</a> .</li>\n<li>When you direct, request us or otherwise consent to our disclosure of certain information to third parties, such as to ship you products or through your use of social media widgets or login integrations.</li>\n<li>With our affiliates or otherwise within our corporate group.</li>\n<li>In connection with a business transaction such as a merger or bankruptcy, to comply with any applicable legal obligations (including to respond to subpoenas, search warrants and similar requests), to enforce any applicable terms of service or policies, and to protect or defend the Services, our rights, and the rights of our users or others.</li>\n</ul>\n<h2>Relationship with Shopify</h2>\n<p>The Services are hosted by Shopify, which collects and processes personal information about your access to and use of the Services in order to provide and improve the Services for you. Information you submit to the Services will be transmitted to and shared with Shopify as well as third parties that may be located in countries other than where you reside, in order to provide and improve the Services for you. In addition, to help protect, grow, and improve our business, we use certain Shopify enhanced features that incorporate data and information obtained from your interactions with our Store, along with other merchants and with Shopify. To provide these enhanced features, Shopify may make use of personal information collected about your interactions with our store, along with other merchants, and with Shopify. In these circumstances, Shopify is responsible for the processing of your personal information, including for responding to your requests to exercise your rights over use of your personal information for these purposes. To learn more about how Shopify uses your personal information and any rights you may have, you can visit the <a href=\"https://www.shopify.com/legal/privacy/app-users\">Shopify Consumer Privacy Policy</a> . Depending on where you live, you may exercise certain rights with respect to your personal information here <a href=\"https://privacy.shopify.com/en\">Shopify Privacy Portal Link</a>.</p>\n<h2>Third Party Websites and Links</h2>\n<p>The Services may provide links to websites or other online platforms operated by third parties. If you follow links to sites not affiliated or controlled by us, you should review their privacy and security policies and other terms and conditions. We do not guarantee and are not responsible for the privacy or security of such sites, including the accuracy, completeness, or reliability of information found on these sites. Information you provide on public or semi-public venues, including information you share on third-party social networking platforms may also be viewable by other users of the Services and/or users of those third-party platforms without limitation as to its use by us or by a third party. Our inclusion of such links does not, by itself, imply any endorsement of the content on such platforms or of their owners or operators, except as disclosed on the Services.</p>\n<h2>Children's Data</h2>\n<p>The Services are not intended to be used by children, and we do not knowingly collect any personal information about children under the age of majority in your jurisdiction. If you are the parent or guardian of a child who has provided us with their personal information, you may contact us using the contact details set out below to request that it be deleted.As of the Effective Date of this Privacy Policy, we do not have actual knowledge that we \"share\" or \"sell\" (as those terms are defined in applicable law) personal information of individuals under 16 years of age.</p>\n<h2>Security and Retention of Your Information</h2>\n<p>Please be aware that no security measures are perfect or impenetrable, and we cannot guarantee \"perfect security.\" In addition, any information you send to us may not be secure while in transit. We recommend that you do not use unsecure channels to communicate sensitive or confidential information to us.</p>\n<p>How long we retain your personal information depends on different factors, such as whether we need the information to maintain your account, to provide you with Services, comply with legal obligations, resolve disputes or enforce other applicable contracts and policies.</p>\n<h2>Your Rights and Choices</h2>\n<p>Depending on where you live, you may have some or all of the rights listed below in relation to your personal information. However, these rights are not absolute, may apply only in certain circumstances and, in certain cases, we may decline your request as permitted by law.</p>\n<ul>\n<li>\n<strong>Right to Access / Know.</strong> You may have a right to request access to personal information that we hold about you.</li>\n<li>\n<strong>Right to Delete.</strong> You may have a right to request that we delete personal information we maintain about you.</li>\n<li>\n<strong>Right to Correct.</strong> You may have a right to request that we correct inaccurate personal information we maintain about you.</li>\n<li>\n<strong>Right of Portability.</strong> You may have a right to receive a copy of the personal information we hold about you and to request that we transfer it to a third party, in certain circumstances and with certain exceptions.</li>\n<li>\n<strong>Right to Opt out of Sale or Sharing for Targeted Advertising.</strong> Depending on where you reside, you may have a right to opt out of the \"sale\" or \"share\" of your personal information or to opt out of the processing of your personal information for purposes considered to be \"targeted advertising\", as defined in applicable privacy laws. You can exercise your rights to opt-out of those uses <a href=\"https://vapenicotinefree.com/pages/data-sharing-opt-out\">here</a>. Please note that if you visit our website with the Global Privacy Control opt-out preference signal enabled, depending on where you are, we will automatically treat this as a request to opt-out for the device and browser that you use to visit the website. If we are able to associate the device sending the signal to a Shopify account, we will apply the opt out request to the account as well. To learn more about Global Privacy Control, you can visit https://globalprivacycontrol.org/. Other than the Global Privacy Control, we do not recognize other \"Do Not Track\" signals that may be sent from your web browser or device.</li>\n<li>\n<strong>Managing Communication Preferences.</strong> We may send you promotional emails, and you may opt out of receiving these at any time by using the unsubscribe option displayed in our emails to you. If you opt out, we may still send you non-promotional emails, such as those about your account or orders that you have made.</li>\n</ul>\n\n<p><strong>If you reside in the UK or European Economic Area,</strong> and subject to exceptions and limitations provided by local law, you may exercise the following rights in addition to the rights outlined above:</p>\n<ul>\n<li>\n<strong>Objection to Processing and Restriction of Processing:</strong> You may have the right to ask us to stop or restrict our processing of personal information for certain purposes.</li>\n<li>\n<strong>Withdrawal of Consent:</strong> Where we rely on consent to process your personal information, you have the right to withdraw this consent. If you withdraw your consent, this will not affect the lawfulness of any processing based on your consent before its withdrawal.</li>\n</ul>\n\n<p>You may exercise any of these rights where indicated on the Services or by contacting us using the contact details provided below. To learn more about how Shopify uses your personal information and any rights you may have, including rights related to data processed by Shopify, you can visit https://privacy.shopify.com/en.</p>\n<p>We will not discriminate against you for exercising any of these rights. We may need to verify your identity before we can process your requests, as permitted or required under applicable law. In accordance with applicable laws, you may designate an authorized agent to make requests on your behalf to exercise your rights. Before accepting such a request from an agent, we will require that the agent provide proof you have authorized them to act on your behalf, and we may need you to verify your identity directly with us. We will respond to your request in a timely manner as required under applicable law.</p>\n<h2>Complaints</h2>\n<p>If you have complaints about how we process your personal information, please contact us using the contact details provided below. Depending on where you live, you may have the right to appeal our decision by contacting us using the contact details set out below, or lodge your complaint with your local data protection authority. For the EEA, you can find a list of the responsible data protection supervisory authorities <a href=\"https://edpb.europa.eu/about-edpb/about-edpb/members\">here</a>.</p>\n<h2>International Transfers</h2>\n<p>Please note that we may transfer, store and process your personal information outside the country you live in.</p>\n<p>If we transfer your personal information out of the European Economic Area or the United Kingdom, we will rely on recognized transfer mechanisms like the European Commission's Standard Contractual Clauses, or any equivalent contracts issued by the relevant competent authority of the UK, as relevant, unless the data transfer is to a country that has been determined to provide an adequate level of protection.</p>\n<h2>Changes to This Privacy Policy</h2>\n<p>We may update this Privacy Policy from time to time, including to reflect changes to our practices or for other operational, legal, or regulatory reasons. We will post the revised Privacy Policy on this website, update the \"Last updated\" date and provide notice as required by applicable law.</p>\n<h2>Contact</h2>\n<p>Should you have any questions about our privacy practices or this Privacy Policy, or if you would like to exercise any of the rights available to you, please call or email us at hello@vapenicotinefree.com or contact us at 15300 Valley View Avenue, C13, La Mirada, CA, 90638, US For the purpose of applicable data protection laws, we are the data controller of your personal information.</p>"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "slug": "shipping-returns",
3
+ "title": "Shipping & Returns",
4
+ "isPolicy": true,
5
+ "body_html": "<h2>Shipping Policy</h2>\n<h3>Vape Shipping Information</h3>\n<p><strong>Shipping Times:</strong> Delivery times for vape products can vary depending on your location and distance from our fulfillment center.</p>\n<p><strong>Age Verification:</strong> A person aged <strong>21 or older</strong> must be available to present identification and sign for the delivery of vape products.</p>\n<h3>Shipping Methods</h3>\n<p><strong>We DO NOT ship via USPS.</strong></p>\n<p>All orders are shipped through <strong>small regional carriers</strong> that are <strong>PACT Act compliant</strong>. These carriers require an <strong>adult signature and ID verification</strong> upon delivery.</p>\n<h3>Order Processing &amp; Delivery Timeline</h3>\n<ul>\n<li>\n<strong>Order Processing (Preparing for Shipment):</strong> 3–5 business days</li>\n<li>\n<strong>From Our Warehouse to Local Carrier:</strong> Estimated 7–10 days after dispatch from our facility</li>\n<li>\n<strong>Local Carrier to Your Address:</strong> Estimated 2–10 days, depending on the carrier’s delivery schedule</li>\n</ul>\n<h3>Important Delivery Requirements</h3>\n<p><strong>Age Verification:</strong> An adult aged 21 or older must be present to verify their identity with a government-issued ID and sign for the package upon delivery.</p>\n<p><strong>Delivery Instructions:</strong> Please include accurate details such as phone number, gate or access codes, apartment or unit number, and any other relevant information to ensure a smooth delivery process.</p>\n<h3>PACT Act Amendment Impact</h3>\n<p><strong>Effective Date:</strong> March 27, 2021</p>\n<p>Due to the amendment to the <strong>PACT Act</strong>, all vape shipments require an adult signature at the time of delivery.</p>\n<ul>\n<li>\n<strong>Signature Requirement:</strong> An adult (21+) must be present to sign and show a valid government-issued ID.</li>\n<li>\n<strong>Fee Notice:</strong> An <em>Adult Signature Fee</em> is automatically included at checkout.</li>\n<li>\n<strong>Delivery Tip:</strong> Consider shipping your order to a location where an adult is regularly available—such as your workplace—to prevent missed deliveries.</li>\n</ul>\n<p><em>Please note:</em> We cannot be held responsible for shipping carrier delays once your order has left our facility.</p>\n<h3>Shipping Restrictions</h3>\n<p>We <strong>DO NOT</strong> ship vape products to:</p>\n<ul>\n<li>P.O. Boxes</li>\n<li>APO/FPO addresses</li>\n<li>Any other USPS-only locations</li>\n</ul>\n<h3>Age Verification Requirement</h3>\n<p>In compliance with <strong>FDA regulations</strong>, Vape Nicotine Free partners with <strong>AgeChecker.net</strong>, a trusted provider of real-time identity verification services. This ensures all customers meet the <strong>legal vaping age</strong> in their state of residence.</p>\n<h3>Contact Information</h3>\n<p>For any shipping questions or support, please contact us:</p>\n<ul>\n<li>\n<strong>Email:</strong> <a href=\"mailto:hello@vapenicotinefree.com\">hello@vapenicotinefree.com</a>\n</li>\n<li>\n<strong>Text Only:</strong> 714-706-3212</li>\n</ul>\n<p><strong>Website:</strong> <a href=\"https://vapenicotinefree.com\">vapenicotinefree.com</a></p>\n<h2>Returns &amp; refunds</h2>Refund Policy\n<p>We have a 30-day return policy, which means you have 30 days after receiving your item to request a return.</p>\n<p>To be eligible for a return, your item must be in the same condition that you received it, unworn or unused, with tags, and in its original packaging. You’ll also need the receipt or proof of purchase.</p>\n<p>To start a return, you can contact us at hello@vapenicotinefree.com.</p>\n<p>If your return is accepted, we’ll send you a return shipping label, as well as instructions on how and where to send your package. Items sent back to us without first requesting a return will not be accepted.</p>\n<p>You can always contact us for any return questions at hello@vapenicotinefree.com.</p>\n<h2>Damages and Issues</h2>\n<p>Please inspect your order upon reception and contact us immediately if the item is defective, damaged or if you receive the wrong item, so that we can evaluate the issue and make it right.</p>\n<h2>Exceptions / Non-Returnable Items</h2>\n<p>Certain types of items cannot be returned, like perishable goods (such as food, flowers, or plants), custom products (such as special orders or personalized items), and personal care goods (such as beauty products). We also do not accept returns for hazardous materials, flammable liquids, or gases. Please get in touch if you have questions or concerns about your specific item.</p>\n<p>Unfortunately, we cannot accept returns on sale items or gift cards.</p>\n<h2>Exchanges</h2>\n<p>The fastest way to ensure you get what you want is to return the item you have, and once the return is accepted, make a separate purchase for the new item.</p>\n<h2>Refunds</h2>\n<p>We will notify you once we’ve received and inspected your return, and let you know if the refund was approved or not. If approved, you’ll be automatically refunded on your original payment method within 10 business days. Please remember it can take some time for your bank or credit card company to process and post the refund too.</p>\n<p>If more than 15 business days have passed since we’ve approved your return, please contact us at hello@vapenicotinefree.com.</p>"
6
+ }
@@ -23,7 +23,9 @@ import type { CustomizationEnv, KvGet, TenantConfig } from "@tot/public-runtime"
23
23
  import {
24
24
  contentArtifactPath,
25
25
  listPublishedArtifactPaths,
26
+ listVersionArtifactPaths,
26
27
  readPublishedArtifact,
28
+ readVersionArtifact,
27
29
  tenantDirRelative,
28
30
  tenantDirSegment,
29
31
  } from "@tot/public-runtime";
@@ -681,6 +683,99 @@ export class PublishedContentProvider implements ContentProvider {
681
683
  }
682
684
  }
683
685
 
686
+ // ---------------------------------------------------------------------------
687
+ // VersionPinnedContentProvider — pins content to ONE exact immutable
688
+ // `versionId`, bypassing channel/pointer resolution entirely (the candidate/
689
+ // revision preview's read path). Structurally the version-pinned twin of
690
+ // PublishedContentProvider (channel -> versionId vs. versionId direct); every
691
+ // method mirrors it 1:1 so a candidate's OWN pages/blog/products/etc. render
692
+ // through the exact same generic-content plumbing as a live/preview page,
693
+ // pinned to the exact commit under review instead of the shared channel.
694
+ // ---------------------------------------------------------------------------
695
+
696
+ export class VersionPinnedContentProvider implements ContentProvider {
697
+ constructor(
698
+ private readonly base: ContentProvider,
699
+ private readonly tenantId: string,
700
+ private readonly get: KvGet,
701
+ private readonly versionId: string,
702
+ ) {}
703
+
704
+ private pinnedRaw(relative: string): Promise<string | null> {
705
+ return readVersionArtifact(
706
+ this.get,
707
+ this.tenantId,
708
+ this.versionId,
709
+ contentArtifactPath(this.tenantId, relative),
710
+ );
711
+ }
712
+
713
+ private async pinned<T>(relative: string): Promise<T | null> {
714
+ const raw = await this.pinnedRaw(relative);
715
+ if (!raw) return null;
716
+ try {
717
+ return JSON.parse(raw) as T;
718
+ } catch {
719
+ return null;
720
+ }
721
+ }
722
+
723
+ async getHome(): Promise<HomeContent> {
724
+ return (await this.pinned<HomeContent>("home.json")) ?? this.base.getHome();
725
+ }
726
+ async getHomeHtml(): Promise<string | null> {
727
+ const raw = await this.pinnedRaw("home.html");
728
+ return raw !== null ? stampTrackingIdsAtPublish(raw) : this.base.getHomeHtml();
729
+ }
730
+ async getPageHtml(slug: string): Promise<string | null> {
731
+ const rel = `pages-html/${normalizeRawPageSlug(slug)}.html`;
732
+ const raw = await this.pinnedRaw(rel);
733
+ return raw !== null ? stampTrackingIdsAtPublish(raw) : this.base.getPageHtml(slug);
734
+ }
735
+ listPageHtmlSlugs(): Promise<string[]> {
736
+ return this.listArtifactPaths("pages-html/").then((paths) =>
737
+ paths === null
738
+ ? this.base.listPageHtmlSlugs()
739
+ : paths
740
+ .filter((path) => path.endsWith(".html"))
741
+ .map((path) => path.slice("pages-html/".length, -".html".length)),
742
+ );
743
+ }
744
+ async getChromeHtml(currentPath?: string): Promise<string | null> {
745
+ const publishedHtml = await this.pinnedRaw("chrome.html");
746
+ if (publishedHtml) return stampTrackingIdsAtPublish(publishedHtml);
747
+ const config = parseChromeConfig(await this.pinned<unknown>("chrome.json"));
748
+ if (config) {
749
+ const assignments = parseChromeAssignments(await this.pinned<unknown>("chrome-assignments.json"));
750
+ return renderRawChrome(config, currentPath, assignments);
751
+ }
752
+ return this.base.getChromeHtml(currentPath);
753
+ }
754
+ async getChrome(): Promise<ChromeConfig> {
755
+ const config = parseChromeConfig(await this.pinned<unknown>("chrome.json"));
756
+ return config ?? this.base.getChrome();
757
+ }
758
+ async getPage(slug: string): Promise<EditorialPage | null> {
759
+ return (await this.pinned<EditorialPage>(`pages/${slug}.json`)) ?? this.base.getPage(slug);
760
+ }
761
+ async listPages(): Promise<EditorialPage[]> {
762
+ const paths = await this.listArtifactPaths("pages/");
763
+ if (paths === null) return this.base.listPages();
764
+ const pages = await Promise.all(
765
+ paths.filter((path) => path.endsWith(".json")).map(async (path) => this.pinned<EditorialPage>(path)),
766
+ );
767
+ return pages.filter((page): page is EditorialPage => page !== null);
768
+ }
769
+
770
+ readArtifact(relative: string): Promise<string | null> {
771
+ return this.pinnedRaw(relative);
772
+ }
773
+
774
+ async listArtifactPaths(prefix = ""): Promise<string[] | null> {
775
+ return listVersionArtifactPaths(this.get, this.tenantId, this.versionId, prefix);
776
+ }
777
+ }
778
+
684
779
  // ---------------------------------------------------------------------------
685
780
  // DevDraftContentProvider — the per-developer DRAFT overlay. Wraps the
686
781
  // resolved provider chain and serves a
@@ -747,9 +842,22 @@ export class DevDraftContentProvider implements ContentProvider {
747
842
 
748
843
  const PLACEHOLDER_TOKEN_RE = /PLACEHOLDER|^$/i;
749
844
 
845
+ /**
846
+ * Pin content resolution to ONE exact immutable `versionId` instead of a
847
+ * `{live,test}` channel — the candidate/revision preview's read path (see
848
+ * `VersionPinnedContentProvider`). Takes priority over `customization` when
849
+ * both are given: a pinned candidate render must never fall through to the
850
+ * shared channel.
851
+ */
852
+ export interface ContentVersionPin {
853
+ get: KvGet;
854
+ versionId: string;
855
+ }
856
+
750
857
  export function createContentProvider(
751
858
  tenant: TenantConfig,
752
859
  customization?: ContentCustomization,
860
+ pin?: ContentVersionPin,
753
861
  ): ContentProvider {
754
862
  // Content dirs are keyed by tenant_id (the adopter writes content/<tenant>/).
755
863
  const local = new LocalContentProvider(tenant.tenant_id);
@@ -758,6 +866,9 @@ export function createContentProvider(
758
866
  const base: ContentProvider = usable
759
867
  ? new StoryblokContentProvider(token, local)
760
868
  : local;
869
+ if (pin) {
870
+ return new VersionPinnedContentProvider(base, tenant.tenant_id, pin.get, pin.versionId);
871
+ }
761
872
  return customization
762
873
  ? new PublishedContentProvider(base, tenant.tenant_id, customization)
763
874
  : base;
@@ -27,6 +27,8 @@ import { STATIC_SCRIPT_HASHES, STATIC_STYLE_HASHES } from "@/lib/cspStaticHashes
27
27
  import { applySecurityHeaders } from "@/lib/securityHeaders";
28
28
  import { maybeInjectPreviewReload } from "@/lib/previewReload";
29
29
  import {
30
+ CANDIDATE_CONTENT_PIN_PARAM,
31
+ verifyContentPinToken,
30
32
  resolveCustomizationEnv,
31
33
  buildCsp,
32
34
  STOREFRONT_CSP_ALLOWLIST,
@@ -288,6 +290,34 @@ export const onRequest = defineMiddleware(async (context, next) => {
288
290
  const isPreviewHost = host.toLowerCase().startsWith("preview.");
289
291
  const queryToken = url.searchParams.get("__preview");
290
292
  const cookieToken = context.cookies.get("tot_preview")?.value ?? null;
293
+
294
+ // Candidate-render CONTENT PIN (`__tot_candidate_pin`): a signed, tenant- and
295
+ // expiry-bound token minted ONLY by the control-plane candidate/revision
296
+ // renderers (`src/lib/preview/pinnedRouteRender.ts`, never client-forgeable)
297
+ // that pins THIS one render's content resolution to an exact immutable
298
+ // `versionId`, bypassing the shared `{live,test}` channel entirely. It rides
299
+ // an in-process `context.rewrite()` from those renderers so a real Astro
300
+ // route (blog, editorial pages, …) — not just the raw `home.html`/
301
+ // `pages-html/*` documents they serve directly — renders pinned to the
302
+ // candidate under review (fb-1788536381607). Verified here so it can only
303
+ // ever name the resolved tenant's own version; a mismatched/expired/absent
304
+ // token falls through to the normal channel-resolved read below, same
305
+ // fail-closed posture as the `__preview` token.
306
+ const candidatePinParam = url.searchParams.get(CANDIDATE_CONTENT_PIN_PARAM);
307
+ let candidatePin: { versionId: string; basePath: string } | null = null;
308
+ if (candidatePinParam) {
309
+ const pinSecret = (await readEnv("CUSTOMIZATION_PREVIEW_SECRET")) ?? null;
310
+ if (pinSecret) {
311
+ const verified = await verifyContentPinToken(
312
+ pinSecret,
313
+ candidatePinParam,
314
+ Math.floor(Date.now() / 1000),
315
+ );
316
+ if (verified.ok && verified.claims.tenantId === tenant.tenant_id) {
317
+ candidatePin = { versionId: verified.claims.versionId, basePath: verified.claims.basePath };
318
+ }
319
+ }
320
+ }
291
321
  // Per-developer AI-edit draft: the signed
292
322
  // tot_dev_draft cookie's mere PRESENCE opts the request out of the shared page
293
323
  // cache below (a draft view must never be cached publicly); it's verified —
@@ -329,6 +359,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
329
359
  !isPreviewHost &&
330
360
  !queryToken &&
331
361
  !cookieToken &&
362
+ !candidatePin &&
332
363
  !draftCookie &&
333
364
  managedIntegrationDeclarations.length === 0 &&
334
365
  !url.pathname.startsWith("/api/");
@@ -451,6 +482,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
451
482
  !isPreviewHost &&
452
483
  !queryToken &&
453
484
  !cookieToken &&
485
+ !candidatePin &&
454
486
  !draftCookie &&
455
487
  isCacheablePath(url.pathname, url.searchParams);
456
488
  let pageKey: Request | null = null;
@@ -662,10 +694,15 @@ export const onRequest = defineMiddleware(async (context, next) => {
662
694
  : null;
663
695
  locals.commerce = commerce ?? undefined;
664
696
  locals.tot = tot;
665
- locals.content = createContentProvider(tenant, {
666
- get: kvGet,
667
- env: customization.env,
668
- });
697
+ // A verified candidate content pin takes priority over the channel-resolved
698
+ // read — a pinned candidate/revision render must never fall through to the
699
+ // shared `{live,test}` content (see `pinnedRouteRender.ts`).
700
+ locals.content = candidatePin
701
+ ? createContentProvider(tenant, undefined, { get: kvGet, versionId: candidatePin.versionId })
702
+ : createContentProvider(tenant, {
703
+ get: kvGet,
704
+ env: customization.env,
705
+ });
669
706
  // Developer-draft content overlay — serves the AI-edited artifacts to the draft
670
707
  // owner only; every other viewer keeps the published/base chain above.
671
708
  if (draft) {
@@ -686,8 +723,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
686
723
  locals.activeWidgetPlacements = [];
687
724
  // Prefix that path-routed pages live under ("" for host-routed). Pages stay
688
725
  // tenant-agnostic; the Layout uses this to keep root-absolute links inside
689
- // the tenant prefix on the client.
690
- locals.basePath = basePath;
726
+ // the tenant prefix on the client. A candidate content pin overrides this to
727
+ // the PINNING render's own prefix (`/preview/rev/<sha>` or
728
+ // `/preview/render/<tenant>/<changeId>`) so a pinned page's OWN generated
729
+ // links stay inside that candidate instead of escaping to the live host.
730
+ locals.basePath = candidatePin ? candidatePin.basePath : basePath;
691
731
  // Renderer-owned four-stage browser environment (ADR 0014 E1 / 0017), computed
692
732
  // ONCE here from authoritative request state and serialized before app code by
693
733
  // both document mounts (Layout.astro + rawHtmlResponse). The candidate render
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -130,6 +130,106 @@ export async function verifyPreviewToken(
130
130
  return { ok: true, claims };
131
131
  }
132
132
 
133
+ /**
134
+ * Query-string param a candidate/revision renderer's `context.rewrite()` uses to
135
+ * carry a signed content-pin token into the re-dispatched request — read by
136
+ * `src/middleware/index.ts` to bind that ONE request's content resolution to an
137
+ * exact `versionId` instead of the shared `{live,test}` channel. See
138
+ * {@link ContentPinClaims}.
139
+ */
140
+ export const CANDIDATE_CONTENT_PIN_PARAM = "__tot_candidate_pin";
141
+
142
+ /**
143
+ * Claims carried by a CONTENT-PIN token — distinct from {@link PreviewClaims}
144
+ * (which grants "test" channel access). A content pin grants read access to ONE
145
+ * exact immutable site version, bypassing channel resolution entirely. It backs
146
+ * the candidate/revision renderer's generic-route fallback: rendering any real
147
+ * Astro page (not just the raw `home.html`/`pages-html/*` documents) pinned to a
148
+ * candidate's `versionId` rather than the shared `live`/`test` channel.
149
+ */
150
+ export interface ContentPinClaims {
151
+ tenantId: string;
152
+ versionId: string;
153
+ /** basePath the pinned render's own generated links must stay under. */
154
+ basePath: string;
155
+ /** Expiry, epoch seconds. */
156
+ exp: number;
157
+ }
158
+
159
+ /**
160
+ * Mint a content-pin token: `<payloadB64Url>.<sigB64Url>`. `ttlSeconds` is added
161
+ * to `nowSeconds` (caller supplies the clock so this stays pure/testable).
162
+ */
163
+ export async function signContentPinToken(
164
+ secret: string,
165
+ claims: Omit<ContentPinClaims, "exp">,
166
+ ttlSeconds: number,
167
+ nowSeconds: number,
168
+ ): Promise<string> {
169
+ const full: ContentPinClaims = { ...claims, exp: nowSeconds + ttlSeconds };
170
+ const payload = bytesToB64Url(textEncoder.encode(JSON.stringify(full)));
171
+ const key = await hmacKey(secret);
172
+ const sig = await crypto.subtle.sign(
173
+ "HMAC",
174
+ key,
175
+ textEncoder.encode(payload) as unknown as BufferSource,
176
+ );
177
+ return `${payload}.${bytesToB64Url(new Uint8Array(sig))}`;
178
+ }
179
+
180
+ export type VerifyContentPinResult =
181
+ | { ok: true; claims: ContentPinClaims }
182
+ | { ok: false; reason: "malformed" | "bad-signature" | "expired" };
183
+
184
+ /**
185
+ * Verify a content-pin token against `secret` and the caller's clock. Signature
186
+ * is checked with `crypto.subtle.verify` (constant-time) before expiry.
187
+ */
188
+ export async function verifyContentPinToken(
189
+ secret: string,
190
+ token: string,
191
+ nowSeconds: number,
192
+ ): Promise<VerifyContentPinResult> {
193
+ const dot = token.indexOf(".");
194
+ if (dot <= 0 || dot === token.length - 1) return { ok: false, reason: "malformed" };
195
+ const payload = token.slice(0, dot);
196
+ const sigPart = token.slice(dot + 1);
197
+
198
+ let sig: Uint8Array;
199
+ try {
200
+ sig = b64UrlToBytes(sigPart);
201
+ } catch {
202
+ return { ok: false, reason: "malformed" };
203
+ }
204
+
205
+ const key = await hmacKey(secret);
206
+ const valid = await crypto.subtle.verify(
207
+ "HMAC",
208
+ key,
209
+ sig as unknown as BufferSource,
210
+ textEncoder.encode(payload) as unknown as BufferSource,
211
+ );
212
+ if (!valid) return { ok: false, reason: "bad-signature" };
213
+
214
+ let claims: ContentPinClaims;
215
+ try {
216
+ claims = JSON.parse(new TextDecoder().decode(b64UrlToBytes(payload)));
217
+ } catch {
218
+ return { ok: false, reason: "malformed" };
219
+ }
220
+ if (
221
+ !claims ||
222
+ typeof claims.tenantId !== "string" ||
223
+ typeof claims.versionId !== "string" ||
224
+ typeof claims.basePath !== "string" ||
225
+ typeof claims.exp !== "number"
226
+ ) {
227
+ return { ok: false, reason: "malformed" };
228
+ }
229
+ if (claims.exp <= nowSeconds) return { ok: false, reason: "expired" };
230
+ return { ok: true, claims };
231
+ }
232
+
133
233
  export type CustomizationEnv = "live" | "test";
134
234
 
135
235
  export interface EnvResolutionInput {
@@ -286,6 +286,46 @@ export async function readPublishedArtifact(
286
286
  return get(customizationArtifactKey(tenantId, versionId));
287
287
  }
288
288
 
289
+ /**
290
+ * Read one artifact's text from a resolved immutable `versionId`, or null when
291
+ * that specific artifact is absent from the version. Pinned to the versionId,
292
+ * never a channel pointer — a candidate/revision preview must never resolve
293
+ * live/test content. `path` is the FULL artifact path (e.g. from
294
+ * `contentArtifactPath`), not tenant-content-relative.
295
+ */
296
+ export async function readVersionArtifact(
297
+ get: KvGet,
298
+ tenantId: string,
299
+ versionId: string,
300
+ path: string,
301
+ ): Promise<string | null> {
302
+ const manifest = await readSiteVersion(get, tenantId, versionId);
303
+ const contentHash = manifest?.artifacts?.[path];
304
+ if (!contentHash) return null;
305
+ return get(customizationArtifactKey(tenantId, contentHash));
306
+ }
307
+
308
+ /**
309
+ * List artifact paths in an immutable site version, pinned directly to
310
+ * `versionId` (never a channel pointer) — the enumeration companion to
311
+ * {@link readVersionArtifact}, used by a pinned `ContentProvider` to answer
312
+ * `listArtifactPaths` the same way {@link listPublishedArtifactPaths} does for
313
+ * the channel-resolved read. `prefix` is content-relative (e.g. `"blog/"`).
314
+ */
315
+ export async function listVersionArtifactPaths(
316
+ get: KvGet,
317
+ tenantId: string,
318
+ versionId: string,
319
+ prefix = "",
320
+ ): Promise<string[]> {
321
+ const manifest = await readSiteVersion(get, tenantId, versionId);
322
+ const root = contentArtifactPath(tenantId, "");
323
+ return Object.keys(manifest?.artifacts ?? {})
324
+ .flatMap((path) => (path.startsWith(root) ? [path.slice(root.length)] : []))
325
+ .filter((path) => path.startsWith(prefix))
326
+ .sort((a, b) => a.localeCompare(b));
327
+ }
328
+
289
329
  /**
290
330
  * List artifact paths in the immutable site version selected for `(tenant, env)`.
291
331
  *