@shop-on/customer-runtime 0.1.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.
- package/dist/graphql.js +2 -0
- package/dist/handoff-state.d.ts +25 -0
- package/dist/handoff-state.js +32 -0
- package/dist/internal-api.js +3 -0
- package/dist/relay.js +6 -1
- package/dist/request-scope.js +1 -1
- package/dist/sealed-session.d.ts +27 -0
- package/dist/sealed-session.js +154 -0
- package/dist/session-cookie.d.ts +5 -3
- package/dist/session-cookie.js +13 -2
- package/dist/trace.d.ts +13 -0
- package/dist/trace.js +25 -0
- package/package.json +14 -7
package/dist/graphql.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { relayHeaders } from "./relay.js";
|
|
2
|
+
import { traceHeaders } from "./trace.js";
|
|
2
3
|
import { evictStorefrontToken, getStorefrontToken } from "./oauth.js";
|
|
3
4
|
import { ClientError, GraphQLClient } from "graphql-request";
|
|
4
5
|
//#region src/graphql.ts
|
|
@@ -23,6 +24,7 @@ function buildClient(token, extra) {
|
|
|
23
24
|
async function storefrontRequest(shopHandle, document, variables, options) {
|
|
24
25
|
const extra = {
|
|
25
26
|
...options?.customerSessionToken ? { "X-Customer-Session": options.customerSessionToken } : {},
|
|
27
|
+
...traceHeaders(),
|
|
26
28
|
...relayHeaders()
|
|
27
29
|
};
|
|
28
30
|
let token = await getStorefrontToken(shopHandle);
|
|
@@ -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, the account `return_to` sanitizer that passes
|
|
5
|
+
* them through, and their checkers. The formats are a shape check only: each
|
|
6
|
+
* consumer verifies the value against its own cookie.
|
|
7
|
+
*
|
|
8
|
+
* - `checkout_state`: `ts "." base64url(HMAC-SHA256)`, minted by checkout's
|
|
9
|
+
* `/begin` and `/sign-in`.
|
|
10
|
+
* - `state` (theme): 32 random bytes base64url, minted by theme-server's
|
|
11
|
+
* `account_state` cookie.
|
|
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, the account `return_to` sanitizer that passes
|
|
5
|
+
* them through, and their checkers. The formats are a shape check only: each
|
|
6
|
+
* consumer verifies the value against its own cookie.
|
|
7
|
+
*
|
|
8
|
+
* - `checkout_state`: `ts "." base64url(HMAC-SHA256)`, minted by checkout's
|
|
9
|
+
* `/begin` and `/sign-in`.
|
|
10
|
+
* - `state` (theme): 32 random bytes base64url, minted by theme-server's
|
|
11
|
+
* `account_state` cookie.
|
|
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 };
|
package/dist/internal-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { relayHeaders } from "./relay.js";
|
|
2
|
+
import { traceHeaders } from "./trace.js";
|
|
2
3
|
//#region src/internal-api.ts
|
|
3
4
|
function apiUrl() {
|
|
4
5
|
return process.env.SPN_CUSTOMER_API_URL ?? "http://localhost:3003";
|
|
@@ -40,6 +41,7 @@ async function internalPost(path, body, { signal } = {}) {
|
|
|
40
41
|
headers: {
|
|
41
42
|
"Content-Type": "application/json",
|
|
42
43
|
"X-Internal-Auth": secret,
|
|
44
|
+
...traceHeaders(),
|
|
43
45
|
...relayHeaders()
|
|
44
46
|
},
|
|
45
47
|
body: JSON.stringify(body),
|
|
@@ -59,6 +61,7 @@ async function internalDelete(path) {
|
|
|
59
61
|
method: "DELETE",
|
|
60
62
|
headers: {
|
|
61
63
|
"X-Internal-Auth": secret,
|
|
64
|
+
...traceHeaders(),
|
|
62
65
|
...relayHeaders()
|
|
63
66
|
}
|
|
64
67
|
});
|
package/dist/relay.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { currentRequest } from "./request-scope.js";
|
|
2
|
-
import { isIP } from "node:net";
|
|
3
2
|
//#region src/relay.ts
|
|
3
|
+
const IPV4_OCTET = "(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)";
|
|
4
|
+
const IPV4 = new RegExp(`^${IPV4_OCTET}(\\.${IPV4_OCTET}){3}$`);
|
|
5
|
+
function isIP(value) {
|
|
6
|
+
if (IPV4.test(value)) return true;
|
|
7
|
+
return /^[\da-f:.]+$/i.test(value) && value.includes(":") && URL.canParse(`http://[${value}]`);
|
|
8
|
+
}
|
|
4
9
|
/**
|
|
5
10
|
* `X-Buyer-IP` from Cloudflare's `CF-Connecting-IP` on the request this call
|
|
6
11
|
* runs under, plus `X-Relay-Auth` when SPN_RELAY_SECRET is set. Empty outside
|
package/dist/request-scope.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const EVENT_STORAGE_KEY = Symbol.for("tanstack-start:event-storage");
|
|
3
3
|
/** The request this call runs under; undefined outside one (browser, tests, pre-handler). */
|
|
4
4
|
function currentRequest() {
|
|
5
|
-
return globalThis[EVENT_STORAGE_KEY]?.getStore()?.
|
|
5
|
+
return globalThis[EVENT_STORAGE_KEY]?.getStore()?.request;
|
|
6
6
|
}
|
|
7
7
|
const slots = /* @__PURE__ */ new WeakMap();
|
|
8
8
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/sealed-session.d.ts
|
|
2
|
+
export interface SealedSessionConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
/** At least 32 chars. */
|
|
5
|
+
password: string;
|
|
6
|
+
/** Seconds. Seal TTL plus an absolute cap counted from the first write. */
|
|
7
|
+
maxAge?: number;
|
|
8
|
+
cookie: {
|
|
9
|
+
secure: boolean;
|
|
10
|
+
httpOnly: true;
|
|
11
|
+
sameSite: "lax";
|
|
12
|
+
path: "/";
|
|
13
|
+
/** Seconds. Sent as Max-Age, refreshed on every write. */
|
|
14
|
+
maxAge?: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface SealedSession<T> {
|
|
18
|
+
/** Live: reflects every write committed so far in this request. */
|
|
19
|
+
readonly data: Partial<T>;
|
|
20
|
+
update(patch: Partial<T>): Promise<void>;
|
|
21
|
+
clear(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare class SessionTooLargeError extends Error {
|
|
24
|
+
constructor(name: string, length: number);
|
|
25
|
+
}
|
|
26
|
+
export declare function openSession<T>(config: SealedSessionConfig): Promise<SealedSession<T>>;
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { requestSlot } from "./request-scope.js";
|
|
2
|
+
import { deleteCookie, getCookie, setCookie } from "@tanstack/react-start/server";
|
|
3
|
+
import { defaults, seal, unseal } from "iron-webcrypto";
|
|
4
|
+
//#region src/sealed-session.ts
|
|
5
|
+
/**
|
|
6
|
+
* Encrypted cookie sessions, wire-compatible with the `useSession` TanStack
|
|
7
|
+
* Start 1.168 shipped (h3 2.0.1-rc.20) and 1.169 removed. Both seal
|
|
8
|
+
* `{ id, createdAt, data }` with iron (Fe26.2, iron's defaults), so a cookie
|
|
9
|
+
* either one wrote opens in the other — no sign-out on deploy, and a rollback
|
|
10
|
+
* stays safe. `test/fixtures/h3-sessions.json` holds cookies Start 1.168
|
|
11
|
+
* minted; `test/sealed-session.test.ts` keeps them opening.
|
|
12
|
+
*
|
|
13
|
+
* Where it deliberately differs from h3:
|
|
14
|
+
* - A read never writes. h3 minted and set an empty session on every
|
|
15
|
+
* cookieless read.
|
|
16
|
+
* - `clear()` sticks for the rest of the request. h3 re-read the stale
|
|
17
|
+
* request cookie afterwards.
|
|
18
|
+
* - Writes are one cookie, never chunked. A seal over 3,800 chars throws
|
|
19
|
+
* `SessionTooLargeError` instead. h3's chunked cookies still open, and
|
|
20
|
+
* the next write or clear deletes their chunks.
|
|
21
|
+
* - No `x-<name>-session` request header.
|
|
22
|
+
*
|
|
23
|
+
* Each request shares one state per cookie name (`requestSlot`), so parallel
|
|
24
|
+
* opens unseal once, and writes queue: each merges onto the state the one
|
|
25
|
+
* before it committed, in call order.
|
|
26
|
+
*/
|
|
27
|
+
var SessionTooLargeError = class extends Error {
|
|
28
|
+
constructor(name, length) {
|
|
29
|
+
super(`Session "${name}" seals to ${length} chars, over the ${MAX_SEALED_LENGTH} limit`);
|
|
30
|
+
this.name = "SessionTooLargeError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const MAX_SEALED_LENGTH = 3800;
|
|
34
|
+
const CHUNKED = "__chunked__";
|
|
35
|
+
const MAX_CHUNKS = 100;
|
|
36
|
+
const DEFAULT_COOKIE = {
|
|
37
|
+
path: "/",
|
|
38
|
+
secure: true,
|
|
39
|
+
httpOnly: true
|
|
40
|
+
};
|
|
41
|
+
const empty = () => ({
|
|
42
|
+
id: "",
|
|
43
|
+
createdAt: 0,
|
|
44
|
+
data: {}
|
|
45
|
+
});
|
|
46
|
+
async function openSession(config) {
|
|
47
|
+
if (typeof config.password !== "string" || config.password.length < 32) throw new Error(`Session "${config.name}" needs a password of at least 32 characters`);
|
|
48
|
+
const init = () => {
|
|
49
|
+
const slot = {
|
|
50
|
+
chain: Promise.resolve(),
|
|
51
|
+
state: empty()
|
|
52
|
+
};
|
|
53
|
+
slot.chain = read(config).then((state) => {
|
|
54
|
+
slot.state = state;
|
|
55
|
+
});
|
|
56
|
+
return slot;
|
|
57
|
+
};
|
|
58
|
+
const slot = requestSlot(Symbol.for(`shop-on:sealed-session:${config.name}`), init) ?? init();
|
|
59
|
+
await slot.chain;
|
|
60
|
+
const enqueue = (run) => {
|
|
61
|
+
const result = slot.chain.then(run);
|
|
62
|
+
slot.chain = result.catch(() => {});
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
get data() {
|
|
67
|
+
return slot.state.data;
|
|
68
|
+
},
|
|
69
|
+
update: (patch) => enqueue(() => write(config, slot, patch)),
|
|
70
|
+
clear: () => enqueue(() => {
|
|
71
|
+
slot.state = empty();
|
|
72
|
+
deleteCookie(config.name, {
|
|
73
|
+
...DEFAULT_COOKIE,
|
|
74
|
+
...config.cookie
|
|
75
|
+
});
|
|
76
|
+
deleteChunks(config);
|
|
77
|
+
})
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function sealOptions(config) {
|
|
81
|
+
return {
|
|
82
|
+
...defaults,
|
|
83
|
+
ttl: config.maxAge ? config.maxAge * 1e3 : 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function read(config) {
|
|
87
|
+
const sealed = readSealed(config.name);
|
|
88
|
+
if (!sealed) return empty();
|
|
89
|
+
try {
|
|
90
|
+
const opened = await unseal(sealed, config.password, sealOptions(config));
|
|
91
|
+
if (typeof opened?.id !== "string" || !opened.id || typeof opened.createdAt !== "number") return empty();
|
|
92
|
+
if (config.maxAge && Date.now() - opened.createdAt > config.maxAge * 1e3) return empty();
|
|
93
|
+
const data = opened.data && typeof opened.data === "object" ? { ...opened.data } : {};
|
|
94
|
+
return {
|
|
95
|
+
id: opened.id,
|
|
96
|
+
createdAt: opened.createdAt,
|
|
97
|
+
data
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return empty();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The request's sealed value, reassembled from h3's chunks when it split one. */
|
|
104
|
+
function readSealed(name) {
|
|
105
|
+
const value = getCookie(name);
|
|
106
|
+
const count = chunkCount(value);
|
|
107
|
+
if (!count) return value?.startsWith(CHUNKED) ? void 0 : value;
|
|
108
|
+
const chunks = [];
|
|
109
|
+
for (let i = 1; i <= count; i++) {
|
|
110
|
+
const chunk = getCookie(`${name}.${i}`);
|
|
111
|
+
if (!chunk) return void 0;
|
|
112
|
+
chunks.push(chunk);
|
|
113
|
+
}
|
|
114
|
+
return chunks.join("");
|
|
115
|
+
}
|
|
116
|
+
function chunkCount(value) {
|
|
117
|
+
if (!value?.startsWith(CHUNKED)) return 0;
|
|
118
|
+
const count = Number.parseInt(value.slice(11), 10);
|
|
119
|
+
return Number.isInteger(count) && count >= 1 && count <= MAX_CHUNKS ? count : 0;
|
|
120
|
+
}
|
|
121
|
+
function deleteChunks(config) {
|
|
122
|
+
const count = chunkCount(getCookie(config.name));
|
|
123
|
+
for (let i = 1; i <= count; i++) deleteCookie(`${config.name}.${i}`, {
|
|
124
|
+
...DEFAULT_COOKIE,
|
|
125
|
+
...config.cookie
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function write(config, slot, patch) {
|
|
129
|
+
const previous = slot.state;
|
|
130
|
+
const fresh = !previous.id || Object.keys(previous.data).length === 0;
|
|
131
|
+
const data = JSON.parse(JSON.stringify({
|
|
132
|
+
...previous.data,
|
|
133
|
+
...patch
|
|
134
|
+
}));
|
|
135
|
+
const next = fresh ? {
|
|
136
|
+
id: globalThis.crypto.randomUUID(),
|
|
137
|
+
createdAt: Date.now(),
|
|
138
|
+
data
|
|
139
|
+
} : {
|
|
140
|
+
...previous,
|
|
141
|
+
data
|
|
142
|
+
};
|
|
143
|
+
const sealed = await seal(next, config.password, sealOptions(config));
|
|
144
|
+
if (sealed.length > MAX_SEALED_LENGTH) throw new SessionTooLargeError(config.name, sealed.length);
|
|
145
|
+
slot.state = next;
|
|
146
|
+
setCookie(config.name, sealed, {
|
|
147
|
+
...DEFAULT_COOKIE,
|
|
148
|
+
expires: config.maxAge ? new Date(next.createdAt + config.maxAge * 1e3) : void 0,
|
|
149
|
+
...config.cookie
|
|
150
|
+
});
|
|
151
|
+
deleteChunks(config);
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { SessionTooLargeError, openSession };
|
package/dist/session-cookie.d.ts
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* `undefined` for both "missing" and "wrong shop" so callers handle them
|
|
9
9
|
* uniformly (treat as logged-out).
|
|
10
10
|
*
|
|
11
|
-
* The returned `ShopBoundCookieConfig` is shape-compatible with
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The returned `ShopBoundCookieConfig` is shape-compatible with
|
|
12
|
+
* `SealedSessionConfig` — pass it straight to `openSession`
|
|
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;
|
package/dist/session-cookie.js
CHANGED
|
@@ -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
|
|
19
|
+
secure,
|
|
9
20
|
httpOnly: true,
|
|
10
21
|
sameSite: "lax",
|
|
11
22
|
path: "/"
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/trace.d.ts
|
|
2
|
+
export interface TraceOptions {
|
|
3
|
+
/** Continue a valid incoming `traceparent`. Only behind a proxy that started the trace (Traefik). */
|
|
4
|
+
continueIncoming?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `traceparent` for the request this call runs under: one minted once per
|
|
8
|
+
* request, or with `continueIncoming` the incoming ids when valid; always
|
|
9
|
+
* sampled. Empty outside a request. Send it to our own api only, never to a
|
|
10
|
+
* third party.
|
|
11
|
+
*/
|
|
12
|
+
export declare function traceHeaders({ continueIncoming }?: TraceOptions): Record<string, string>;
|
|
13
|
+
//#endregion
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { currentRequest, requestSlot } from "./request-scope.js";
|
|
2
|
+
//#region src/trace.ts
|
|
3
|
+
const TRACEPARENT = /^00-((?!0{32})[\da-f]{32})-((?!0{16})[\da-f]{16})-[\da-f]{2}$/;
|
|
4
|
+
const MINTED = Symbol("minted traceparent");
|
|
5
|
+
function randomHex(bytes) {
|
|
6
|
+
const buf = crypto.getRandomValues(new Uint8Array(bytes));
|
|
7
|
+
if (buf.every((b) => b === 0)) buf[0] = 1;
|
|
8
|
+
return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `traceparent` for the request this call runs under: one minted once per
|
|
12
|
+
* request, or with `continueIncoming` the incoming ids when valid; always
|
|
13
|
+
* sampled. Empty outside a request. Send it to our own api only, never to a
|
|
14
|
+
* third party.
|
|
15
|
+
*/
|
|
16
|
+
function traceHeaders({ continueIncoming = false } = {}) {
|
|
17
|
+
if (continueIncoming) {
|
|
18
|
+
const incoming = currentRequest()?.headers.get("traceparent")?.match(TRACEPARENT);
|
|
19
|
+
if (incoming) return { traceparent: `00-${incoming[1]}-${incoming[2]}-01` };
|
|
20
|
+
}
|
|
21
|
+
const traceparent = requestSlot(MINTED, () => `00-${randomHex(16)}-${randomHex(8)}-01`);
|
|
22
|
+
return traceparent ? { traceparent } : {};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { traceHeaders };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shop-on/customer-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "SEE LICENSE IN dist/LICENSE.md",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -10,27 +10,34 @@
|
|
|
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",
|
|
16
17
|
"./request-scope": "./dist/request-scope.js",
|
|
18
|
+
"./sealed-session": "./dist/sealed-session.js",
|
|
17
19
|
"./session-cookie": "./dist/session-cookie.js",
|
|
18
20
|
"./shop-context": "./dist/shop-context.js",
|
|
21
|
+
"./trace": "./dist/trace.js",
|
|
19
22
|
"./package.json": "./package.json"
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
22
|
-
"gql.tada": "1.11.
|
|
23
|
-
"graphql-request": "7.4.0"
|
|
25
|
+
"gql.tada": "1.11.4",
|
|
26
|
+
"graphql-request": "7.4.0",
|
|
27
|
+
"iron-webcrypto": "2.0.0"
|
|
24
28
|
},
|
|
25
29
|
"devDependencies": {
|
|
26
|
-
"@tanstack/react-start": "1.
|
|
27
|
-
"@types/node": "26.6.
|
|
30
|
+
"@tanstack/react-start": "1.169.3",
|
|
31
|
+
"@types/node": "26.6.5",
|
|
28
32
|
"typescript": "7.0.2",
|
|
29
|
-
"vitest": "5.0.
|
|
33
|
+
"vitest": "5.0.5"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@tanstack/react-start": "~1.169.3"
|
|
30
37
|
},
|
|
31
38
|
"scripts": {
|
|
32
39
|
"check-types": "tsc --noEmit",
|
|
33
|
-
"test": "vitest run",
|
|
40
|
+
"test": "vitest run --maxWorkers=2",
|
|
34
41
|
"test:watch": "vitest"
|
|
35
42
|
}
|
|
36
43
|
}
|