@shop-on/customer-runtime 0.2.0 → 0.4.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,25 @@
1
+ //#region src/handoff-state.d.ts
2
+ /**
3
+ * Shared rules for the states that bind a handoff to the browser that started
4
+ * it, used by their minters and their checkers. The formats are a shape check
5
+ * only: each consumer verifies the value against its own cookie.
6
+ *
7
+ * - `checkout_state`: `ts "." base64url(HMAC-SHA256)`, minted by checkout's
8
+ * `/begin`, carried by the theme onto the cart handoff URL.
9
+ * - `state` (theme): 32 random bytes base64url, minted by theme-server's
10
+ * `account_state` cookie; the account `return_to` sanitizer passes it
11
+ * through on storefront hosts.
12
+ */
13
+ export declare const CHECKOUT_STATE_FORMAT: RegExp;
14
+ export declare const ACCOUNT_STATE_FORMAT: RegExp;
15
+ /** Loopback: the one place a state may travel over http (local theme dev).
16
+ * Takes `URL#hostname`, which keeps the IPv6 brackets (`[::1]`). */
17
+ export declare function isLoopback(hostname: string): boolean;
18
+ /**
19
+ * For every response that redirects to or from a URL holding a state: a
20
+ * cached copy would hand one browser's state to the next, and a Referer would
21
+ * carry it to whatever the next page loads. Server-only; the server module is
22
+ * imported here so a client-reachable importer of the formats stays clean.
23
+ */
24
+ export declare function markResponsePrivate(): Promise<void>;
25
+ //#endregion
@@ -0,0 +1,32 @@
1
+ //#region src/handoff-state.ts
2
+ /**
3
+ * Shared rules for the states that bind a handoff to the browser that started
4
+ * it, used by their minters and their checkers. The formats are a shape check
5
+ * only: each consumer verifies the value against its own cookie.
6
+ *
7
+ * - `checkout_state`: `ts "." base64url(HMAC-SHA256)`, minted by checkout's
8
+ * `/begin`, carried by the theme onto the cart handoff URL.
9
+ * - `state` (theme): 32 random bytes base64url, minted by theme-server's
10
+ * `account_state` cookie; the account `return_to` sanitizer passes it
11
+ * through on storefront hosts.
12
+ */
13
+ const CHECKOUT_STATE_FORMAT = /^\d{10}\.[A-Za-z0-9_-]{43}$/;
14
+ const ACCOUNT_STATE_FORMAT = /^[A-Za-z0-9_-]{43}$/;
15
+ /** Loopback: the one place a state may travel over http (local theme dev).
16
+ * Takes `URL#hostname`, which keeps the IPv6 brackets (`[::1]`). */
17
+ function isLoopback(hostname) {
18
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
19
+ }
20
+ /**
21
+ * For every response that redirects to or from a URL holding a state: a
22
+ * cached copy would hand one browser's state to the next, and a Referer would
23
+ * carry it to whatever the next page loads. Server-only; the server module is
24
+ * imported here so a client-reachable importer of the formats stays clean.
25
+ */
26
+ async function markResponsePrivate() {
27
+ const { setResponseHeader } = await import("@tanstack/react-start/server");
28
+ setResponseHeader("Referrer-Policy", "no-referrer");
29
+ setResponseHeader("Cache-Control", "no-store");
30
+ }
31
+ //#endregion
32
+ export { ACCOUNT_STATE_FORMAT, CHECKOUT_STATE_FORMAT, isLoopback, markResponsePrivate };
@@ -10,7 +10,9 @@
10
10
  *
11
11
  * The returned `ShopBoundCookieConfig` is shape-compatible with
12
12
  * `SealedSessionConfig` — pass it straight to `openSession`
13
- * (`./sealed-session`). This module stays free of TanStack imports.
13
+ * (`./sealed-session`). theme-server's cart and customer cookies use the
14
+ * same config; a theme isolate serves one shop, so they skip the payload
15
+ * binding. This module stays free of TanStack imports.
14
16
  */
15
17
  export interface ShopBoundSession {
16
18
  shopHandle: string;
@@ -1,11 +1,22 @@
1
1
  //#region src/session-cookie.ts
2
+ /**
3
+ * Storefronts on `<handle>.adorly.mn` run merchant script, and `adorly.mn` is
4
+ * not a public suffix, so any storefront can set a same-named cookie for the
5
+ * parent domain and fix a buyer into the attacker's session. Only the host
6
+ * itself can set a `__Host-` cookie. The prefix requires `Secure`, so
7
+ * plain-http dev keeps the bare name.
8
+ */
9
+ function hostOnlyCookieName(base, secure) {
10
+ return secure ? `__Host-${base}` : base;
11
+ }
2
12
  function createShopBoundCookieConfig(opts) {
13
+ const secure = opts.secure ?? process.env.NODE_ENV === "production";
3
14
  return {
4
- name: opts.name,
15
+ name: hostOnlyCookieName(opts.name, secure),
5
16
  password: opts.password,
6
17
  ...opts.maxAge !== void 0 ? { maxAge: opts.maxAge } : {},
7
18
  cookie: {
8
- secure: opts.secure ?? process.env.NODE_ENV === "production",
19
+ secure,
9
20
  httpOnly: true,
10
21
  sameSite: "lax",
11
22
  path: "/"
@@ -0,0 +1,10 @@
1
+ //#region src/storefront-bounce.d.ts
2
+ /**
3
+ * The storefront's own `/account` bounce, coming back to `returnToPath`.
4
+ * Account and checkout sign a buyer in through it rather than linking to
5
+ * account directly: the theme issues the `state` its customer handoff is
6
+ * bound to, bounces to account, and consumes the handoff on the way back.
7
+ * Pure, so either app can build the link in a component.
8
+ */
9
+ export declare function storefrontAccountBounceUrl(storefrontRoot: string, returnToPath: string): string;
10
+ //#endregion
@@ -0,0 +1,15 @@
1
+ //#region src/storefront-bounce.ts
2
+ /**
3
+ * The storefront's own `/account` bounce, coming back to `returnToPath`.
4
+ * Account and checkout sign a buyer in through it rather than linking to
5
+ * account directly: the theme issues the `state` its customer handoff is
6
+ * bound to, bounces to account, and consumes the handoff on the way back.
7
+ * Pure, so either app can build the link in a component.
8
+ */
9
+ function storefrontAccountBounceUrl(storefrontRoot, returnToPath) {
10
+ const url = new URL("/account", storefrontRoot);
11
+ url.searchParams.set("return_to", returnToPath);
12
+ return url.toString();
13
+ }
14
+ //#endregion
15
+ export { storefrontAccountBounceUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shop-on/customer-runtime",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "SEE LICENSE IN dist/LICENSE.md",
5
5
  "files": [
6
6
  "dist"
@@ -10,6 +10,7 @@
10
10
  "./customer-session-handoff": "./dist/customer-session-handoff.js",
11
11
  "./graphql": "./dist/graphql.js",
12
12
  "./handoff": "./dist/handoff.js",
13
+ "./handoff-state": "./dist/handoff-state.js",
13
14
  "./lease": "./dist/lease.js",
14
15
  "./oauth": "./dist/oauth.js",
15
16
  "./relay": "./dist/relay.js",
@@ -17,6 +18,7 @@
17
18
  "./sealed-session": "./dist/sealed-session.js",
18
19
  "./session-cookie": "./dist/session-cookie.js",
19
20
  "./shop-context": "./dist/shop-context.js",
21
+ "./storefront-bounce": "./dist/storefront-bounce.js",
20
22
  "./trace": "./dist/trace.js",
21
23
  "./package.json": "./package.json"
22
24
  },