@shop-on/theme-server 0.2.0 → 0.3.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.
@@ -0,0 +1,55 @@
1
+ import { derivedCookieSecret } from "./cookie-secret.js";
2
+ import { createServerOnlyFn } from "@tanstack/react-start";
3
+ import { createShopBoundCookieConfig } from "@shop-on/customer-runtime/session-cookie";
4
+ import { ACCOUNT_STATE_FORMAT } from "@shop-on/customer-runtime/handoff-state";
5
+ //#region src/account-state.ts
6
+ const TTL_SECONDS = 1800;
7
+ const DOMAIN_LABEL = "account-state";
8
+ const sessionConfig = createServerOnlyFn(async () => {
9
+ return createShopBoundCookieConfig({
10
+ name: "account_state",
11
+ password: await derivedCookieSecret(DOMAIN_LABEL),
12
+ maxAge: TTL_SECONDS
13
+ });
14
+ });
15
+ const loadSession = createServerOnlyFn(async () => {
16
+ const { openSession } = await import("@shop-on/customer-runtime/sealed-session");
17
+ return openSession(await sessionConfig());
18
+ });
19
+ /** The state this browser holds, or a fresh one once it is past its 30
20
+ * minutes. Reused so a second tab bouncing to account doesn't void the state
21
+ * an in-flight sign-in will come back with, and rewritten on reuse: the
22
+ * cookie's expiry counts from its first write, so only clearing first lets
23
+ * it outlast the OTP round trip that reuse starts. */
24
+ const issueAccountState = createServerOnlyFn(async () => {
25
+ const session = await loadSession();
26
+ const { state: kept, issuedAt } = session.data;
27
+ if (kept && issuedAt && Date.now() - issuedAt < TTL_SECONDS * 1e3) {
28
+ await session.clear();
29
+ await session.update({
30
+ state: kept,
31
+ issuedAt
32
+ });
33
+ return kept;
34
+ }
35
+ const { randomBytes } = await import("node:crypto");
36
+ const state = randomBytes(32).toString("base64url");
37
+ await session.clear();
38
+ await session.update({
39
+ state,
40
+ issuedAt: Date.now()
41
+ });
42
+ return state;
43
+ });
44
+ /** Whether `state` is the one this browser was issued. Constant-time. */
45
+ const matchesAccountState = createServerOnlyFn(async (state) => {
46
+ if (!state || !ACCOUNT_STATE_FORMAT.test(state)) return false;
47
+ const issued = (await loadSession()).data.state;
48
+ if (!issued) return false;
49
+ const { timingSafeEqual } = await import("node:crypto");
50
+ const a = Buffer.from(issued);
51
+ const b = Buffer.from(state);
52
+ return a.length === b.length && timingSafeEqual(a, b);
53
+ });
54
+ //#endregion
55
+ export { issueAccountState, matchesAccountState };
package/dist/account.js CHANGED
@@ -22,7 +22,12 @@ async function getAccountRedirectUrl(data) {
22
22
  xForwardedHost: true,
23
23
  xForwardedProto: true
24
24
  }).origin;
25
- target.searchParams.set("return_to", `${origin}${safePath}`);
25
+ const returnTo = new URL(`${origin}${safePath}`);
26
+ const { issueAccountState } = await import("./account-state.js");
27
+ returnTo.searchParams.set("state", await issueAccountState());
28
+ const { markResponsePrivate } = await import("@shop-on/customer-runtime/handoff-state");
29
+ await markResponsePrivate();
30
+ target.searchParams.set("return_to", returnTo.toString());
26
31
  }
27
32
  return { url: target.toString() };
28
33
  }
@@ -1,25 +1,15 @@
1
+ import { derivedCookieSecret } from "./cookie-secret.js";
1
2
  import { createServerOnlyFn } from "@tanstack/react-start";
3
+ import { createShopBoundCookieConfig } from "@shop-on/customer-runtime/session-cookie";
2
4
  //#region src/cart-session.ts
3
5
  const TTL_SECONDS = 2592e3;
4
6
  const DOMAIN_LABEL = "cart";
5
- const readSecret = createServerOnlyFn(async () => {
6
- const customer = process.env.STOREFRONT_CUSTOMER_SESSION_SECRET;
7
- if (!customer) throw new Error("cart-session: STOREFRONT_CUSTOMER_SESSION_SECRET missing — orchestrator did not inject it");
8
- const { createHmac } = await import("node:crypto");
9
- return createHmac("sha256", customer).update(DOMAIN_LABEL).digest("hex");
10
- });
11
7
  const sessionConfig = createServerOnlyFn(async () => {
12
- return {
13
- password: await readSecret(),
8
+ return createShopBoundCookieConfig({
14
9
  name: "cart_session",
15
- maxAge: TTL_SECONDS,
16
- cookie: {
17
- secure: process.env.NODE_ENV === "production",
18
- sameSite: "lax",
19
- httpOnly: true,
20
- path: "/"
21
- }
22
- };
10
+ password: await derivedCookieSecret(DOMAIN_LABEL),
11
+ maxAge: TTL_SECONDS
12
+ });
23
13
  });
24
14
  const useCartSession = createServerOnlyFn(async () => {
25
15
  const { openSession } = await import("@shop-on/customer-runtime/sealed-session");
@@ -4,21 +4,22 @@ export type CheckoutHandoffResult = {
4
4
  redirectTo: string;
5
5
  } | {
6
6
  ok: false;
7
- reason: "no_cart";
7
+ reason: "no_cart" | "bad_state";
8
8
  };
9
9
  /**
10
- * Read this shop's `cart_session` cookie, call Rails `cartCheckout` (forwarding
11
- * the customer-session token as `X-Customer-Session` when provided so Rails
12
- * folds the customer into the handoff payload in one trip), and return the URL
13
- * on the checkout app that consumes it.
10
+ * The theme's `/checkout`, both passes. `state` is the URL's `checkout_state`,
11
+ * a REQUIRED key even when `undefined`: a caller that never passed it would
12
+ * bounce theme ↔ `/begin` forever, and this way that is a compile error.
14
13
  *
15
- * `no_cart` covers both "no cart cookie" and Rails' `NOT_FOUND` (cart digest
16
- * miss / cross-shop) / `CART_EMPTY` — the central app's "no cart" StatusPage is
17
- * the recovery surface for either. Any other userError code is unexpected and
18
- * throws; the calling route's error boundary handles it (matching `request`'s
19
- * retry-once-then-throw contract for top-level GraphQL errors).
14
+ * - No state: the URL of checkout's `/begin`, which binds this browser and
15
+ * returns the buyer here with one. Mints nothing.
16
+ * - A state: mint the cart handoff with it (`mintCheckoutHandoff`).
17
+ *
18
+ * The customer-session token is injected: forwarded as `X-Customer-Session` on
19
+ * the mint so Rails folds the buyer into the handoff payload in one trip.
20
20
  */
21
- export declare function startCheckoutHandoff(opts?: {
21
+ export declare function startCheckoutHandoff(opts: {
22
+ state: string | undefined;
22
23
  customerToken?: string;
23
24
  }): Promise<CheckoutHandoffResult>;
24
25
  //#endregion
package/dist/checkout.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { graphql, request } from "./graphql.js";
2
+ import { fetchShopInfo } from "./shop.js";
2
3
  import { useCartSession } from "./cart-session.js";
3
4
  import { parseMarketingCookies } from "./marketing-cookies.js";
4
5
  import { createServerOnlyFn } from "@tanstack/react-start";
6
+ import { CHECKOUT_STATE_FORMAT, isLoopback, markResponsePrivate } from "@shop-on/customer-runtime/handoff-state";
5
7
  //#region src/checkout.ts
6
8
  const CartCheckoutMutation = graphql(`
7
9
  mutation CartCheckout($input: CartCheckoutInput!) {
@@ -25,24 +27,73 @@ const readMarketingAttribution = createServerOnlyFn(async () => {
25
27
  return null;
26
28
  }
27
29
  });
30
+ function publicCheckoutUrl(origin) {
31
+ const url = new URL("/checkout", origin);
32
+ if (!isLoopback(url.hostname)) url.protocol = "https:";
33
+ return url.toString();
34
+ }
35
+ /**
36
+ * The theme's `/checkout`, both passes. `state` is the URL's `checkout_state`,
37
+ * a REQUIRED key even when `undefined`: a caller that never passed it would
38
+ * bounce theme ↔ `/begin` forever, and this way that is a compile error.
39
+ *
40
+ * - No state: the URL of checkout's `/begin`, which binds this browser and
41
+ * returns the buyer here with one. Mints nothing.
42
+ * - A state: mint the cart handoff with it (`mintCheckoutHandoff`).
43
+ *
44
+ * The customer-session token is injected: forwarded as `X-Customer-Session` on
45
+ * the mint so Rails folds the buyer into the handoff payload in one trip.
46
+ */
47
+ function startCheckoutHandoff(opts) {
48
+ return opts.state === void 0 ? beginCheckoutUrl() : mintCheckoutHandoff({
49
+ state: opts.state,
50
+ customerToken: opts.customerToken
51
+ });
52
+ }
53
+ async function beginCheckoutUrl() {
54
+ if (!(await useCartSession()).data.token) return {
55
+ ok: false,
56
+ reason: "no_cart"
57
+ };
58
+ await markResponsePrivate();
59
+ const shop = await fetchShopInfo();
60
+ if (!shop) throw new Error("[checkout] shop info unavailable from Rails");
61
+ const { getPublicOrigin } = await import("./seo-context.js");
62
+ const url = new URL(`${shop.checkoutUrl}/begin`);
63
+ url.searchParams.set("return_to", publicCheckoutUrl(getPublicOrigin()));
64
+ return {
65
+ ok: true,
66
+ redirectTo: url.toString()
67
+ };
68
+ }
28
69
  /**
29
- * Read this shop's `cart_session` cookie, call Rails `cartCheckout` (forwarding
30
- * the customer-session token as `X-Customer-Session` when provided so Rails
31
- * folds the customer into the handoff payload in one trip), and return the URL
32
- * on the checkout app that consumes it.
70
+ * Read this shop's `cart_session` cookie, call Rails `cartCheckout`, and return
71
+ * the URL on the checkout app that consumes it, `checkout_state` appended.
33
72
  *
34
- * `no_cart` covers both "no cart cookie" and Rails' `NOT_FOUND` (cart digest
35
- * miss / cross-shop) / `CART_EMPTY` — the central app's "no cart" StatusPage is
36
- * the recovery surface for either. Any other userError code is unexpected and
37
- * throws; the calling route's error boundary handles it (matching `request`'s
38
- * retry-once-then-throw contract for top-level GraphQL errors).
73
+ * `no_cart` covers "no cart cookie" and Rails' `NOT_FOUND` (cart digest miss /
74
+ * cross-shop) / `CART_EMPTY`; `bad_state` a state of the wrong shape (never
75
+ * bounced to `/begin` again, so it can't loop). Any other userError code is
76
+ * unexpected and throws (matching `request`'s retry-once-then-throw contract
77
+ * for top-level GraphQL errors); the calling route sends the buyer home rather
78
+ * than render on a URL holding a state.
39
79
  */
40
- async function startCheckoutHandoff(opts = {}) {
80
+ async function mintCheckoutHandoff(opts) {
41
81
  const cartToken = (await useCartSession()).data.token;
42
82
  if (!cartToken) return {
43
83
  ok: false,
44
84
  reason: "no_cart"
45
85
  };
86
+ await markResponsePrivate();
87
+ if (!CHECKOUT_STATE_FORMAT.test(opts.state)) {
88
+ console.warn(JSON.stringify({
89
+ event: "theme_checkout_bad_state",
90
+ ts: Date.now()
91
+ }));
92
+ return {
93
+ ok: false,
94
+ reason: "bad_state"
95
+ };
96
+ }
46
97
  const attribution = await readMarketingAttribution();
47
98
  const result = await request(CartCheckoutMutation, { input: attribution ? {
48
99
  cartToken,
@@ -57,9 +108,11 @@ async function startCheckoutHandoff(opts = {}) {
57
108
  };
58
109
  throw new Error(`[checkout] cartCheckout returned no URL (code=${code ?? "UNKNOWN"})`);
59
110
  }
111
+ const bound = new URL(checkoutUrl);
112
+ bound.searchParams.set("checkout_state", opts.state);
60
113
  return {
61
114
  ok: true,
62
- redirectTo: checkoutUrl
115
+ redirectTo: bound.toString()
63
116
  };
64
117
  }
65
118
  //#endregion
@@ -0,0 +1,10 @@
1
+ import { createServerOnlyFn } from "@tanstack/react-start";
2
+ //#region src/cookie-secret.ts
3
+ const derivedCookieSecret = createServerOnlyFn(async (label) => {
4
+ const secret = process.env.STOREFRONT_CUSTOMER_SESSION_SECRET;
5
+ if (!secret) throw new Error(`${label} cookie: STOREFRONT_CUSTOMER_SESSION_SECRET missing — orchestrator did not inject it`);
6
+ const { createHmac } = await import("node:crypto");
7
+ return createHmac("sha256", secret).update(label).digest("hex");
8
+ });
9
+ //#endregion
10
+ export { derivedCookieSecret };
@@ -9,5 +9,5 @@ export type ConsumeCustomerHandoffResult = {
9
9
  * Plain server-only helper. Tested directly; each theme wraps it in a thin
10
10
  * `createServerFn` for its root route's `beforeLoad`.
11
11
  */
12
- export declare const consumeCustomerHandoff: (nonce: string) => Promise<ConsumeCustomerHandoffResult>;
12
+ export declare const consumeCustomerHandoff: (nonce: string, state: string | undefined) => Promise<ConsumeCustomerHandoffResult>;
13
13
  //#endregion
@@ -1,13 +1,14 @@
1
1
  import { writeCustomerSession } from "./customer-session.js";
2
+ import { matchesAccountState } from "./account-state.js";
2
3
  import { createServerOnlyFn } from "@tanstack/react-start";
3
4
  //#region src/customer-handoff.ts
4
5
  /**
5
6
  * Plain server-only helper. Tested directly; each theme wraps it in a thin
6
7
  * `createServerFn` for its root route's `beforeLoad`.
7
8
  */
8
- const consumeCustomerHandoff = createServerOnlyFn(async (nonce) => {
9
+ const consumeCustomerHandoff = createServerOnlyFn(async (nonce, state) => {
9
10
  try {
10
- return await consumeAndStore(nonce);
11
+ return await consumeAndStore(nonce, state);
11
12
  } catch (err) {
12
13
  console.warn(JSON.stringify({
13
14
  event: "theme_customer_handoff_consume_error",
@@ -20,7 +21,18 @@ const consumeCustomerHandoff = createServerOnlyFn(async (nonce) => {
20
21
  };
21
22
  }
22
23
  });
23
- const consumeAndStore = createServerOnlyFn(async (nonce) => {
24
+ const consumeAndStore = createServerOnlyFn(async (nonce, state) => {
25
+ if (!await matchesAccountState(state)) {
26
+ console.warn(JSON.stringify({
27
+ event: "theme_customer_handoff_unbound",
28
+ hasState: Boolean(state),
29
+ ts: Date.now()
30
+ }));
31
+ return {
32
+ ok: false,
33
+ reason: "unbound"
34
+ };
35
+ }
24
36
  const { consumeCustomerSessionHandoff } = await import("@shop-on/customer-runtime/customer-session-handoff");
25
37
  const result = await consumeCustomerSessionHandoff(nonce);
26
38
  if (!result.ok) {
@@ -1,25 +1,15 @@
1
+ import { derivedCookieSecret } from "./cookie-secret.js";
1
2
  import { createServerOnlyFn } from "@tanstack/react-start";
3
+ import { createShopBoundCookieConfig } from "@shop-on/customer-runtime/session-cookie";
2
4
  //#region src/customer-session.ts
3
5
  const TTL_SECONDS = 2592e3;
4
6
  const DOMAIN_LABEL = "customer";
5
- const readSecret = createServerOnlyFn(async () => {
6
- const secret = process.env.STOREFRONT_CUSTOMER_SESSION_SECRET;
7
- if (!secret) throw new Error("customer-session: STOREFRONT_CUSTOMER_SESSION_SECRET missing — orchestrator did not inject it");
8
- const { createHmac } = await import("node:crypto");
9
- return createHmac("sha256", secret).update(DOMAIN_LABEL).digest("hex");
10
- });
11
7
  const sessionConfig = createServerOnlyFn(async () => {
12
- return {
13
- password: await readSecret(),
8
+ return createShopBoundCookieConfig({
14
9
  name: "customer_session",
15
- maxAge: TTL_SECONDS,
16
- cookie: {
17
- secure: process.env.NODE_ENV === "production",
18
- sameSite: "lax",
19
- httpOnly: true,
20
- path: "/"
21
- }
22
- };
10
+ password: await derivedCookieSecret(DOMAIN_LABEL),
11
+ maxAge: TTL_SECONDS
12
+ });
23
13
  });
24
14
  const loadSession = createServerOnlyFn(async () => {
25
15
  const { openSession } = await import("@shop-on/customer-runtime/sealed-session");
@@ -2810,6 +2810,14 @@ export type introspection_types = {
2810
2810
  };
2811
2811
  };
2812
2812
  defaultValue: null;
2813
+ }, {
2814
+ name: 'retryKey';
2815
+ type: {
2816
+ kind: 'SCALAR';
2817
+ name: 'String';
2818
+ ofType: null;
2819
+ };
2820
+ defaultValue: null;
2813
2821
  }];
2814
2822
  };
2815
2823
  'CheckoutRecoverPayload': {
@@ -2854,6 +2862,10 @@ export type introspection_types = {
2854
2862
  };
2855
2863
  };
2856
2864
  };
2865
+ 'CheckoutRecoveryStatusEnum': {
2866
+ name: 'CheckoutRecoveryStatusEnum';
2867
+ enumValues: 'EXPIRED' | 'INVALID' | 'VALID';
2868
+ };
2857
2869
  'CheckoutShippingAddress': {
2858
2870
  kind: 'OBJECT';
2859
2871
  name: 'CheckoutShippingAddress';
@@ -7185,6 +7197,18 @@ export type introspection_types = {
7185
7197
  ofType: null;
7186
7198
  };
7187
7199
  };
7200
+ 'checkoutRecoveryStatus': {
7201
+ name: 'checkoutRecoveryStatus';
7202
+ type: {
7203
+ kind: 'NON_NULL';
7204
+ name: never;
7205
+ ofType: {
7206
+ kind: 'ENUM';
7207
+ name: 'CheckoutRecoveryStatusEnum';
7208
+ ofType: null;
7209
+ };
7210
+ };
7211
+ };
7188
7212
  'collection': {
7189
7213
  name: 'collection';
7190
7214
  type: {
@@ -8745,6 +8769,18 @@ export type introspection_types = {
8745
8769
  };
8746
8770
  };
8747
8771
  };
8772
+ 'checkoutUrl': {
8773
+ name: 'checkoutUrl';
8774
+ type: {
8775
+ kind: 'NON_NULL';
8776
+ name: never;
8777
+ ofType: {
8778
+ kind: 'SCALAR';
8779
+ name: 'String';
8780
+ ofType: null;
8781
+ };
8782
+ };
8783
+ };
8748
8784
  'city': {
8749
8785
  name: 'city';
8750
8786
  type: {
@@ -1,16 +1,11 @@
1
+ import { derivedCookieSecret } from "./cookie-secret.js";
1
2
  import { createServerOnlyFn } from "@tanstack/react-start";
2
3
  //#region src/password-session.ts
3
4
  const TTL_SECONDS = 2592e3;
4
5
  const DOMAIN_LABEL = "password";
5
- const readSecret = createServerOnlyFn(async () => {
6
- const secret = process.env.STOREFRONT_CUSTOMER_SESSION_SECRET;
7
- if (!secret) throw new Error("password-session: STOREFRONT_CUSTOMER_SESSION_SECRET missing — orchestrator did not inject it");
8
- const { createHmac } = await import("node:crypto");
9
- return createHmac("sha256", secret).update(DOMAIN_LABEL).digest("hex");
10
- });
11
6
  const sessionConfig = createServerOnlyFn(async () => {
12
7
  return {
13
- password: await readSecret(),
8
+ password: await derivedCookieSecret(DOMAIN_LABEL),
14
9
  name: "storefront_unlock",
15
10
  maxAge: TTL_SECONDS,
16
11
  cookie: {
@@ -408,6 +408,10 @@ input CartCheckoutInput {
408
408
  Autogenerated return type of CartCheckout.
409
409
  """
410
410
  type CartCheckoutPayload {
411
+ """
412
+ Where to send the buyer. Append `checkout_state` from the
413
+ `shop.checkoutUrl/begin` bounce before redirecting; checkout rejects a handoff without it.
414
+ """
411
415
  checkoutUrl: String
412
416
 
413
417
  """
@@ -939,6 +943,12 @@ input CheckoutRecoverInput {
939
943
  """
940
944
  clientMutationId: String
941
945
  recoveryToken: String!
946
+
947
+ """
948
+ A random key the caller keeps for this link. If the reply to the first use is
949
+ lost, the same key may resume the checkout again for a few minutes.
950
+ """
951
+ retryKey: String
942
952
  }
943
953
 
944
954
  """
@@ -954,6 +964,26 @@ type CheckoutRecoverPayload {
954
964
  userErrors: [UserError!]!
955
965
  }
956
966
 
967
+ """
968
+ Whether a checkout recovery link can still resume its checkout.
969
+ """
970
+ enum CheckoutRecoveryStatusEnum {
971
+ """
972
+ The checkout was completed or has expired.
973
+ """
974
+ EXPIRED
975
+
976
+ """
977
+ The link is unknown, already used, or replaced by a newer one.
978
+ """
979
+ INVALID
980
+
981
+ """
982
+ The link resumes the checkout.
983
+ """
984
+ VALID
985
+ }
986
+
957
987
  """
958
988
  Buyer-facing snapshot of the shipping address on a checkout.
959
989
  """
@@ -2436,6 +2466,9 @@ type Mutation {
2436
2466
 
2437
2467
  """
2438
2468
  Mint a checkout handoff for the given cart and return the URL to redirect to.
2469
+ Checkout consumes the handoff only in the browser that started it: bounce the
2470
+ buyer through `shop.checkoutUrl` + `/begin` first, and append the
2471
+ `checkout_state` it sends back to `checkoutUrl`, or checkout rejects it.
2439
2472
  """
2440
2473
  cartCheckout(
2441
2474
  """
@@ -3272,6 +3305,18 @@ type Query {
3272
3305
  """
3273
3306
  checkout(checkoutToken: String!): ShopCheckout
3274
3307
 
3308
+ """
3309
+ Whether a recovery link can still resume its checkout. Read-only: it never spends the link.
3310
+ """
3311
+ checkoutRecoveryStatus(
3312
+ recoveryToken: String!
3313
+
3314
+ """
3315
+ The key sent with an earlier use of this link.
3316
+ """
3317
+ retryKey: String
3318
+ ): CheckoutRecoveryStatusEnum!
3319
+
3275
3320
  """
3276
3321
  Find a collection by ID or handle
3277
3322
  """
@@ -4426,6 +4471,14 @@ type Shop implements Node {
4426
4471
  due (PAYMENT_REQUIRED).
4427
4472
  """
4428
4473
  cashOnDeliveryEnabled: Boolean!
4474
+
4475
+ """
4476
+ URL of this shop's section on the central checkout app
4477
+ (`{checkoutHost}/shops/{handle}/checkout`). Theme sends the buyer to
4478
+ `{checkoutUrl}/begin` before minting a cart handoff, so the handoff is bound
4479
+ to the browser that started it.
4480
+ """
4481
+ checkoutUrl: String!
4429
4482
  city: String
4430
4483
 
4431
4484
  """
@@ -5095,6 +5148,10 @@ type ShopCheckout implements Node {
5095
5148
  availableDeliveryOptions: [ShopDeliveryOption!]!
5096
5149
  completedAt: ISO8601DateTime
5097
5150
  createdAt: ISO8601DateTime!
5151
+
5152
+ """
5153
+ The checkout's customer. Storefront callers get null without that customer's session.
5154
+ """
5098
5155
  customer: ShopCustomer
5099
5156
 
5100
5157
  """
package/dist/shop.d.ts CHANGED
@@ -3,6 +3,7 @@ import { ResultOf } from "./graphql.js";
3
3
  declare const ShopQuery: import("gql.tada").TadaDocumentNode<{
4
4
  shop: {
5
5
  accountUrl: string;
6
+ checkoutUrl: string;
6
7
  currency: string;
7
8
  handle: string;
8
9
  id: string;
package/dist/shop.js CHANGED
@@ -18,6 +18,7 @@ const ShopQuery = graphql(`
18
18
  }
19
19
  turnstileSitekey
20
20
  accountUrl
21
+ checkoutUrl
21
22
  primaryDomain {
22
23
  fqdn
23
24
  turnstileSitekey
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shop-on/theme-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "SEE LICENSE IN dist/LICENSE.md",
5
5
  "files": [
6
6
  "dist"
@@ -58,8 +58,8 @@
58
58
  "graphql": "17.0.2",
59
59
  "graphql-request": "7.4.0",
60
60
  "liquidjs": "10.30.0",
61
- "@shop-on/theme-runtime": "0.2.0",
62
- "@shop-on/customer-runtime": "0.2.0"
61
+ "@shop-on/customer-runtime": "0.3.0",
62
+ "@shop-on/theme-runtime": "0.3.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@tanstack/react-start": "1.169.3",