@mrrocket/sdk 0.12.0 → 0.13.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,71 @@
1
+ /**
2
+ * Attack reports (0.13.0), from your app's server only: failed sign-ins, requests that look like an attack, and refused
3
+ * requests (401/403). MRRocket's Build agent watches them and puts a possible attack into Needs you as Important.
4
+ * They need the product's server key (`MRROCKET_SERVER_KEY`, MRRocket › Settings › Products › Sign-in), so nobody can
5
+ * send a false alarm from a browser. The IP and the account name you pass are hashed by MRRocket on arrival (a hash
6
+ * that changes every day) and never stored as sent. Runs on Node, the Edge runtime and Cloudflare Workers (only `fetch`).
7
+ *
8
+ * Every helper returns a promise that never throws; in a proxy or a route hand it to `waitUntil` so the response does
9
+ * not wait for it:
10
+ *
11
+ * // proxy.ts (middleware)
12
+ * export function proxy(request: NextRequest, event: NextFetchEvent) {
13
+ * const report = watchRequest(request);
14
+ * if (report) event.waitUntil(report);
15
+ * return NextResponse.next();
16
+ * }
17
+ *
18
+ * // where a sign-in fails
19
+ * await reportFailedLogin({ userId: user?.id, account: email, ip: request.headers.get("x-forwarded-for") });
20
+ */
21
+ export interface SecurityOptions {
22
+ /** Defaults to `NEXT_PUBLIC_MRROCKET_KEY`. */
23
+ apiKey?: string;
24
+ /** Defaults to `MRROCKET_SERVER_KEY`. */
25
+ serverKey?: string;
26
+ /** Defaults to `NEXT_PUBLIC_MRROCKET_HOST`. */
27
+ host?: string;
28
+ }
29
+ /** What a request looks like an attempt at; the same patterns MRRocket reads (src/data/security.ts there), smaller. */
30
+ export type AttackKind = "sqli" | "xss" | "traversal" | "scanner";
31
+ /** The attack a path and query look like, or null. Only reads the URL, sends nothing. */
32
+ export declare const attackIn: (path: string, query?: string) => AttackKind | null;
33
+ /** The address a request came from, as the hosts name it. */
34
+ export declare const ipOf: (headers: {
35
+ get(name: string): string | null;
36
+ }) => string | undefined;
37
+ /** A sign-in that failed. `userId` when the account exists, `account` the name typed in (hashed by MRRocket). */
38
+ export declare const reportFailedLogin: (a: {
39
+ userId?: string | null;
40
+ account?: string | null;
41
+ ip?: string | null;
42
+ reason?: string;
43
+ }, o?: SecurityOptions) => Promise<boolean>;
44
+ /** The app answered 401 or 403. A spike over the week's normal becomes a signal. */
45
+ export declare const reportAuthDenied: (a: {
46
+ status: 401 | 403 | number;
47
+ path: string;
48
+ ip?: string | null;
49
+ userId?: string | null;
50
+ }, o?: SecurityOptions) => Promise<boolean>;
51
+ /** A request you judged suspicious yourself (a 404 on a weak-spot path, a blocked payload). */
52
+ export declare const reportSuspiciousRequest: (a: {
53
+ path: string;
54
+ query?: string;
55
+ method?: string;
56
+ ip?: string | null;
57
+ reason?: AttackKind | string;
58
+ status?: number;
59
+ }, o?: SecurityOptions) => Promise<boolean>;
60
+ /**
61
+ * For `proxy.ts` (middleware): reads the request's path and query and, only when they look like an attack (SQL
62
+ * injection, XSS, path traversal, a scanner's path like `/.env`), reports it. Returns the report's promise, or null for
63
+ * an ordinary request (nothing is sent then). Pass the promise to `event.waitUntil`.
64
+ */
65
+ export declare const watchRequest: (request: {
66
+ url: string;
67
+ method?: string;
68
+ headers: {
69
+ get(name: string): string | null;
70
+ };
71
+ }, o?: SecurityOptions) => Promise<boolean> | null;
@@ -0,0 +1,73 @@
1
+ // src/security.ts
2
+ import { envKey, resolveHost } from "./core.js";
3
+ var SCANNER = /^\/(wp-admin|wp-login\.php|wp-content|wp-includes|xmlrpc\.php|\.env|\.git(\/|$)|\.svn|\.hg|\.aws|\.ssh|\.DS_Store|phpmyadmin|pma\/|myadmin|adminer|phpinfo|server-status|actuator|vendor\/phpunit|cgi-bin|boaform|HNAP1|owa\/|config\.(php|json|yml)|web\.config|backup\.(sql|zip|tar)|dump\.sql|.*\.(php|asp|aspx|jsp|cgi)$)/i;
4
+ var SQLI = /('|")\s*(or|and)\s+('|")?\w*('|")?\s*=\s*('|")?\w|\bor\s+1\s*=\s*1\b|union(\s|\/\*.*?\*\/)+(all\s+)?select\b|\b(pg_sleep|sleep|benchmark|waitfor\s+delay)\s*\(|;\s*(drop|delete|truncate|insert|update)\s+\w|information_schema|xp_cmdshell|('|")\s*(--|#)/i;
5
+ var XSS = /<\s*\/?\s*script\b|javascript\s*:|\bon(error|load|mouseover|focus|toggle)\s*=|document\.cookie|\balert\s*\(|<\s*iframe\b/i;
6
+ var TRAVERSAL = /(\.\.[/\\]){2,}|\/etc\/(passwd|shadow|hosts)|\/proc\/self|c:\\windows|\0/i;
7
+ var decode = (s) => {
8
+ let out = s.replace(/\+/g, " ");
9
+ for (let i = 0; i < 2; i++)
10
+ try {
11
+ out = decodeURIComponent(out);
12
+ } catch {
13
+ break;
14
+ }
15
+ return out;
16
+ };
17
+ var attackIn = (path, query = "") => {
18
+ const p = decode(path);
19
+ const q = decode(query);
20
+ const both = `${p}?${q}`;
21
+ if (SQLI.test(both)) return "sqli";
22
+ if (XSS.test(both)) return "xss";
23
+ if (TRAVERSAL.test(both)) return "traversal";
24
+ return SCANNER.test(p) ? "scanner" : null;
25
+ };
26
+ var serverKeyOf = (o) => {
27
+ if (o.serverKey) return o.serverKey;
28
+ try {
29
+ return typeof process !== "undefined" ? process.env.MRROCKET_SERVER_KEY : void 0;
30
+ } catch {
31
+ return void 0;
32
+ }
33
+ };
34
+ var ipOf = (headers) => (headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0] ?? "").trim() || void 0;
35
+ var send = async (event, props, o = {}) => {
36
+ const key = o.apiKey ?? envKey();
37
+ const secret = serverKeyOf(o);
38
+ if (!key || !secret) return false;
39
+ try {
40
+ const res = await fetch(`${resolveHost(o.host)}/api/sdk/events`, {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json", authorization: `Bearer ${secret}` },
43
+ body: JSON.stringify({ key, server: true, events: [{ event, distinctId: typeof props.userId === "string" && props.userId ? props.userId : "$security", ts: Date.now(), props }] })
44
+ });
45
+ return res.ok;
46
+ } catch {
47
+ return false;
48
+ }
49
+ };
50
+ var reportFailedLogin = (a, o) => send("$login_failed", { userId: a.userId ?? void 0, account: a.account ?? void 0, ip: a.ip ?? void 0, reason: a.reason }, o);
51
+ var reportAuthDenied = (a, o) => send("$auth_denied", { status: a.status, path: a.path, ip: a.ip ?? void 0, userId: a.userId ?? void 0 }, o);
52
+ var reportSuspiciousRequest = (a, o) => send("$suspicious_request", { path: a.path, query: a.query, method: a.method, ip: a.ip ?? void 0, reason: a.reason, status: a.status }, o);
53
+ var watchRequest = (request, o) => {
54
+ let url;
55
+ try {
56
+ url = new URL(request.url);
57
+ } catch {
58
+ return null;
59
+ }
60
+ const query = url.search.slice(1);
61
+ const reason = attackIn(url.pathname, query);
62
+ if (!reason) return null;
63
+ return reportSuspiciousRequest({ path: url.pathname, query, method: request.method, ip: ipOf(request.headers), reason }, o);
64
+ };
65
+ export {
66
+ attackIn,
67
+ ipOf,
68
+ reportAuthDenied,
69
+ reportFailedLogin,
70
+ reportSuspiciousRequest,
71
+ watchRequest
72
+ };
73
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/security.ts"],"sourcesContent":["import { envKey, resolveHost } from \"./core\";\n\n/**\n * Attack reports (0.13.0), from your app's server only: failed sign-ins, requests that look like an attack, and refused\n * requests (401/403). MRRocket's Build agent watches them and puts a possible attack into Needs you as Important.\n * They need the product's server key (`MRROCKET_SERVER_KEY`, MRRocket › Settings › Products › Sign-in), so nobody can\n * send a false alarm from a browser. The IP and the account name you pass are hashed by MRRocket on arrival (a hash\n * that changes every day) and never stored as sent. Runs on Node, the Edge runtime and Cloudflare Workers (only `fetch`).\n *\n * Every helper returns a promise that never throws; in a proxy or a route hand it to `waitUntil` so the response does\n * not wait for it:\n *\n * // proxy.ts (middleware)\n * export function proxy(request: NextRequest, event: NextFetchEvent) {\n * const report = watchRequest(request);\n * if (report) event.waitUntil(report);\n * return NextResponse.next();\n * }\n *\n * // where a sign-in fails\n * await reportFailedLogin({ userId: user?.id, account: email, ip: request.headers.get(\"x-forwarded-for\") });\n */\n\nexport interface SecurityOptions {\n /** Defaults to `NEXT_PUBLIC_MRROCKET_KEY`. */\n apiKey?: string;\n /** Defaults to `MRROCKET_SERVER_KEY`. */\n serverKey?: string;\n /** Defaults to `NEXT_PUBLIC_MRROCKET_HOST`. */\n host?: string;\n}\n\n/** What a request looks like an attempt at; the same patterns MRRocket reads (src/data/security.ts there), smaller. */\nexport type AttackKind = \"sqli\" | \"xss\" | \"traversal\" | \"scanner\";\n\nconst SCANNER =\n /^\\/(wp-admin|wp-login\\.php|wp-content|wp-includes|xmlrpc\\.php|\\.env|\\.git(\\/|$)|\\.svn|\\.hg|\\.aws|\\.ssh|\\.DS_Store|phpmyadmin|pma\\/|myadmin|adminer|phpinfo|server-status|actuator|vendor\\/phpunit|cgi-bin|boaform|HNAP1|owa\\/|config\\.(php|json|yml)|web\\.config|backup\\.(sql|zip|tar)|dump\\.sql|.*\\.(php|asp|aspx|jsp|cgi)$)/i;\nconst SQLI = /('|\")\\s*(or|and)\\s+('|\")?\\w*('|\")?\\s*=\\s*('|\")?\\w|\\bor\\s+1\\s*=\\s*1\\b|union(\\s|\\/\\*.*?\\*\\/)+(all\\s+)?select\\b|\\b(pg_sleep|sleep|benchmark|waitfor\\s+delay)\\s*\\(|;\\s*(drop|delete|truncate|insert|update)\\s+\\w|information_schema|xp_cmdshell|('|\")\\s*(--|#)/i;\nconst XSS = /<\\s*\\/?\\s*script\\b|javascript\\s*:|\\bon(error|load|mouseover|focus|toggle)\\s*=|document\\.cookie|\\balert\\s*\\(|<\\s*iframe\\b/i;\nconst TRAVERSAL = /(\\.\\.[/\\\\]){2,}|\\/etc\\/(passwd|shadow|hosts)|\\/proc\\/self|c:\\\\windows|\\0/i;\n\nconst decode = (s: string) => {\n let out = s.replace(/\\+/g, \" \");\n for (let i = 0; i < 2; i++)\n try {\n out = decodeURIComponent(out);\n } catch {\n break;\n }\n return out;\n};\n\n/** The attack a path and query look like, or null. Only reads the URL, sends nothing. */\nexport const attackIn = (path: string, query = \"\"): AttackKind | null => {\n const p = decode(path);\n const q = decode(query);\n const both = `${p}?${q}`;\n if (SQLI.test(both)) return \"sqli\";\n if (XSS.test(both)) return \"xss\";\n if (TRAVERSAL.test(both)) return \"traversal\";\n return SCANNER.test(p) ? \"scanner\" : null;\n};\n\nconst serverKeyOf = (o: SecurityOptions) => {\n if (o.serverKey) return o.serverKey;\n try {\n return typeof process !== \"undefined\" ? process.env.MRROCKET_SERVER_KEY : undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The address a request came from, as the hosts name it. */\nexport const ipOf = (headers: { get(name: string): string | null }) =>\n (headers.get(\"cf-connecting-ip\") ?? headers.get(\"x-real-ip\") ?? headers.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"\").trim() || undefined;\n\nconst send = async (event: string, props: Record<string, unknown>, o: SecurityOptions = {}): Promise<boolean> => {\n const key = o.apiKey ?? envKey();\n const secret = serverKeyOf(o);\n if (!key || !secret) return false;\n try {\n const res = await fetch(`${resolveHost(o.host)}/api/sdk/events`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", authorization: `Bearer ${secret}` },\n body: JSON.stringify({ key, server: true, events: [{ event, distinctId: typeof props.userId === \"string\" && props.userId ? props.userId : \"$security\", ts: Date.now(), props }] }),\n });\n return res.ok;\n } catch {\n return false;\n }\n};\n\n/** A sign-in that failed. `userId` when the account exists, `account` the name typed in (hashed by MRRocket). */\nexport const reportFailedLogin = (a: { userId?: string | null; account?: string | null; ip?: string | null; reason?: string }, o?: SecurityOptions) =>\n send(\"$login_failed\", { userId: a.userId ?? undefined, account: a.account ?? undefined, ip: a.ip ?? undefined, reason: a.reason }, o);\n\n/** The app answered 401 or 403. A spike over the week's normal becomes a signal. */\nexport const reportAuthDenied = (a: { status: 401 | 403 | number; path: string; ip?: string | null; userId?: string | null }, o?: SecurityOptions) =>\n send(\"$auth_denied\", { status: a.status, path: a.path, ip: a.ip ?? undefined, userId: a.userId ?? undefined }, o);\n\n/** A request you judged suspicious yourself (a 404 on a weak-spot path, a blocked payload). */\nexport const reportSuspiciousRequest = (a: { path: string; query?: string; method?: string; ip?: string | null; reason?: AttackKind | string; status?: number }, o?: SecurityOptions) =>\n send(\"$suspicious_request\", { path: a.path, query: a.query, method: a.method, ip: a.ip ?? undefined, reason: a.reason, status: a.status }, o);\n\n/**\n * For `proxy.ts` (middleware): reads the request's path and query and, only when they look like an attack (SQL\n * injection, XSS, path traversal, a scanner's path like `/.env`), reports it. Returns the report's promise, or null for\n * an ordinary request (nothing is sent then). Pass the promise to `event.waitUntil`.\n */\nexport const watchRequest = (request: { url: string; method?: string; headers: { get(name: string): string | null } }, o?: SecurityOptions): Promise<boolean> | null => {\n let url: URL;\n try {\n url = new URL(request.url);\n } catch {\n return null;\n }\n const query = url.search.slice(1);\n const reason = attackIn(url.pathname, query);\n if (!reason) return null;\n return reportSuspiciousRequest({ path: url.pathname, query, method: request.method, ip: ipOf(request.headers), reason }, o);\n};\n"],"mappings":"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","names":[]}
package/dist/server.d.ts CHANGED
@@ -55,5 +55,6 @@ export declare const getFlagVariant: (key: string, { config, cookies, distinctId
55
55
  /** Every text change for this request, to render `<Text>` defaults server-side. */
56
56
  export declare const getTexts: ({ config, searchParams, cookies, distinctId }: RequestInfo) => Record<string, string>;
57
57
  export * from "./core.js";
58
+ export { type AttackKind, type SecurityOptions, attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from "./security.js";
58
59
  /** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */
59
60
  export { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/server.ts
2
2
  import { EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from "./core.js";
3
3
  export * from "./core.js";
4
+ import { attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from "./security.js";
4
5
  import { REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
5
6
  var CONFIG_TIMEOUT_MS = 2500;
6
7
  var getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS } = {}) => {
@@ -36,15 +37,21 @@ var getFlagVariant = (key, { config, cookies, distinctId, person }) => {
36
37
  var getTexts = ({ config, searchParams, cookies, distinctId }) => textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);
37
38
  export {
38
39
  REFERRAL_COOKIE,
40
+ attackIn,
39
41
  distinctIdFrom,
40
42
  getCockpitConfig,
41
43
  getFlag,
42
44
  getFlagVariant,
43
45
  getTexts,
44
46
  getVariant,
47
+ ipOf,
45
48
  previewFrom,
46
49
  readReferralCookie,
47
50
  referralForCheckout,
48
- referralLink
51
+ referralLink,
52
+ reportAuthDenied,
53
+ reportFailedLogin,
54
+ reportSuspiciousRequest,
55
+ watchRequest
49
56
  };
50
57
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { type CockpitConfig, EMBED_PARAM, type FlagPerson, ID_COOKIE, PREVIEW_PARAM, type VariantId, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from \"./core\";\n\n/**\n * Server helpers for the App Router: load the experiments once per request, then render the right variant on the\n * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.\n */\n\ntype SearchParams = Record<string, string | string[] | undefined>;\ninterface CookieReader {\n get(name: string): { value: string } | undefined;\n}\n\n/** How long the server waits for MRRocket before it renders with control. */\nconst CONFIG_TIMEOUT_MS = 2500;\n\n/**\n * The experiments for a public key. `null` when MRRocket cannot be reached, answers with an error or is slower than\n * `timeoutMs`: everyone then sees control and the page still renders. Runs on Node, the Edge runtime and Cloudflare\n * Workers (only `fetch` and `AbortController`).\n *\n * `apiKey` and `host` default to `NEXT_PUBLIC_MRROCKET_KEY` and `NEXT_PUBLIC_MRROCKET_HOST`.\n */\nexport const getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS }: { apiKey?: string; host?: string; timeoutMs?: number } = {}): Promise<CockpitConfig | null> => {\n const key = apiKey ?? envKey();\n if (!key) return null;\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n // `next.revalidate: 0` keeps this request uncached in Next.js. Not `cache: \"no-store\"`: Cloudflare Workers\n // throws on the `cache` field of a fetch unless a compatibility flag is on, and every config would be null.\n const res = await fetch(`${resolveHost(host)}/api/sdk/config?key=${encodeURIComponent(key)}`, {\n signal: ctl.signal,\n next: { revalidate: 0 },\n } as RequestInit);\n if (!res.ok) return null;\n const config = (await res.json()) as CockpitConfig | null;\n return config && Array.isArray(config.experiments) ? config : null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/** Forced variants and the embed flag from a page's search params, for previews in Cockpit. */\nexport const previewFrom = (searchParams: SearchParams | undefined) => {\n const embed = searchParams?.[EMBED_PARAM];\n return { preview: parsePreview(searchParams?.[PREVIEW_PARAM]), embedded: (Array.isArray(embed) ? embed[0] : embed) === \"1\" };\n};\n\n/** The visitor's id from the `ck_id` cookie, if the browser has one yet (only in consent \"cookies\" mode). */\nexport const distinctIdFrom = (cookies: CookieReader | undefined) => cookies?.get(ID_COOKIE)?.value ?? \"anonymous\";\n\n/**\n * Who the visitor is for this request. Pass `distinctId` (your signed-in user id, the same you give\n * `<CockpitProvider distinctId>`) when there is no `ck_id` cookie, as in the default cookieless mode.\n */\ninterface RequestInfo {\n config: CockpitConfig | null;\n searchParams?: SearchParams;\n cookies?: CookieReader;\n distinctId?: string;\n}\n\n/** The variant of one experiment for this request. */\nexport const getVariant = (key: string, { config, searchParams, cookies, distinctId }: RequestInfo): VariantId =>\n resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\n/**\n * A feature flag for this request: true while it is on for the visitor, false while it is off or unknown (or MRRocket\n * could not be reached). `person` takes the properties the flag's audience reads (\"Email\", \"Plan\" …), as you pass them\n * to identify(). For a multivariate flag use `getFlagVariant`.\n */\nexport const getFlag = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): boolean =>\n flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person) !== false;\n\n/** The variant of a multivariate flag for this request, or null while it is off. */\nexport const getFlagVariant = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): string | null => {\n const v = flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person);\n return typeof v === \"string\" ? v : null;\n};\n\n/** Every text change for this request, to render `<Text>` defaults server-side. */\nexport const getTexts = ({ config, searchParams, cookies, distinctId }: RequestInfo) =>\n textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\nexport * from \"./core\";\n/** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */\nexport { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from \"./referral\";\n"],"mappings":";AAAA,SAA6B,aAA8B,WAAW,eAA+B,QAAQ,SAAS,cAAc,aAAa,gBAAgB,mBAAmB;AAsFpL,cAAc;AAEd,SAA+C,iBAAiB,oBAAoB,qBAAqB,oBAAoB;AA3E7H,IAAM,oBAAoB;AASnB,IAAM,mBAAmB,OAAO,EAAE,QAAQ,MAAM,YAAY,kBAAkB,IAA4D,CAAC,MAAqC;AACnL,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,IAAI,gBAAgB;AAChC,QAAM,QAAQ,WAAW,MAAM,IAAI,MAAM,GAAG,SAAS;AACrD,MAAI;AAGA,UAAM,MAAM,MAAM,MAAM,GAAG,YAAY,IAAI,CAAC,uBAAuB,mBAAmB,GAAG,CAAC,IAAI;AAAA,MAC1F,QAAQ,IAAI;AAAA,MACZ,MAAM,EAAE,YAAY,EAAE;AAAA,IAC1B,CAAgB;AAChB,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,WAAO,UAAU,MAAM,QAAQ,OAAO,WAAW,IAAI,SAAS;AAAA,EAClE,QAAQ;AACJ,WAAO;AAAA,EACX,UAAE;AACE,iBAAa,KAAK;AAAA,EACtB;AACJ;AAGO,IAAM,cAAc,CAAC,iBAA2C;AACnE,QAAM,QAAQ,eAAe,WAAW;AACxC,SAAO,EAAE,SAAS,aAAa,eAAe,aAAa,CAAC,GAAG,WAAW,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI,WAAW,IAAI;AAC/H;AAGO,IAAM,iBAAiB,CAAC,YAAsC,SAAS,IAAI,SAAS,GAAG,SAAS;AAchG,IAAM,aAAa,CAAC,KAAa,EAAE,QAAQ,cAAc,SAAS,WAAW,MAChF,eAAe,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;AAOjG,IAAM,UAAU,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MACvE,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM,MAAM;AAGrE,IAAM,iBAAiB,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MAA4D;AAC1I,QAAM,IAAI,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM;AAC5E,SAAO,OAAO,MAAM,WAAW,IAAI;AACvC;AAGO,IAAM,WAAW,CAAC,EAAE,QAAQ,cAAc,SAAS,WAAW,MACjE,YAAY,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { type CockpitConfig, EMBED_PARAM, type FlagPerson, ID_COOKIE, PREVIEW_PARAM, type VariantId, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from \"./core\";\n\n/**\n * Server helpers for the App Router: load the experiments once per request, then render the right variant on the\n * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.\n */\n\ntype SearchParams = Record<string, string | string[] | undefined>;\ninterface CookieReader {\n get(name: string): { value: string } | undefined;\n}\n\n/** How long the server waits for MRRocket before it renders with control. */\nconst CONFIG_TIMEOUT_MS = 2500;\n\n/**\n * The experiments for a public key. `null` when MRRocket cannot be reached, answers with an error or is slower than\n * `timeoutMs`: everyone then sees control and the page still renders. Runs on Node, the Edge runtime and Cloudflare\n * Workers (only `fetch` and `AbortController`).\n *\n * `apiKey` and `host` default to `NEXT_PUBLIC_MRROCKET_KEY` and `NEXT_PUBLIC_MRROCKET_HOST`.\n */\nexport const getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS }: { apiKey?: string; host?: string; timeoutMs?: number } = {}): Promise<CockpitConfig | null> => {\n const key = apiKey ?? envKey();\n if (!key) return null;\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n // `next.revalidate: 0` keeps this request uncached in Next.js. Not `cache: \"no-store\"`: Cloudflare Workers\n // throws on the `cache` field of a fetch unless a compatibility flag is on, and every config would be null.\n const res = await fetch(`${resolveHost(host)}/api/sdk/config?key=${encodeURIComponent(key)}`, {\n signal: ctl.signal,\n next: { revalidate: 0 },\n } as RequestInit);\n if (!res.ok) return null;\n const config = (await res.json()) as CockpitConfig | null;\n return config && Array.isArray(config.experiments) ? config : null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/** Forced variants and the embed flag from a page's search params, for previews in Cockpit. */\nexport const previewFrom = (searchParams: SearchParams | undefined) => {\n const embed = searchParams?.[EMBED_PARAM];\n return { preview: parsePreview(searchParams?.[PREVIEW_PARAM]), embedded: (Array.isArray(embed) ? embed[0] : embed) === \"1\" };\n};\n\n/** The visitor's id from the `ck_id` cookie, if the browser has one yet (only in consent \"cookies\" mode). */\nexport const distinctIdFrom = (cookies: CookieReader | undefined) => cookies?.get(ID_COOKIE)?.value ?? \"anonymous\";\n\n/**\n * Who the visitor is for this request. Pass `distinctId` (your signed-in user id, the same you give\n * `<CockpitProvider distinctId>`) when there is no `ck_id` cookie, as in the default cookieless mode.\n */\ninterface RequestInfo {\n config: CockpitConfig | null;\n searchParams?: SearchParams;\n cookies?: CookieReader;\n distinctId?: string;\n}\n\n/** The variant of one experiment for this request. */\nexport const getVariant = (key: string, { config, searchParams, cookies, distinctId }: RequestInfo): VariantId =>\n resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\n/**\n * A feature flag for this request: true while it is on for the visitor, false while it is off or unknown (or MRRocket\n * could not be reached). `person` takes the properties the flag's audience reads (\"Email\", \"Plan\" …), as you pass them\n * to identify(). For a multivariate flag use `getFlagVariant`.\n */\nexport const getFlag = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): boolean =>\n flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person) !== false;\n\n/** The variant of a multivariate flag for this request, or null while it is off. */\nexport const getFlagVariant = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): string | null => {\n const v = flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person);\n return typeof v === \"string\" ? v : null;\n};\n\n/** Every text change for this request, to render `<Text>` defaults server-side. */\nexport const getTexts = ({ config, searchParams, cookies, distinctId }: RequestInfo) =>\n textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\nexport * from \"./core\";\nexport { type AttackKind, type SecurityOptions, attackIn, ipOf, reportAuthDenied, reportFailedLogin, reportSuspiciousRequest, watchRequest } from \"./security\";\n/** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */\nexport { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from \"./referral\";\n"],"mappings":";AAAA,SAA6B,aAA8B,WAAW,eAA+B,QAAQ,SAAS,cAAc,aAAa,gBAAgB,mBAAmB;AAsFpL,cAAc;AACd,SAAgD,UAAU,MAAM,kBAAkB,mBAAmB,yBAAyB,oBAAoB;AAElJ,SAA+C,iBAAiB,oBAAoB,qBAAqB,oBAAoB;AA5E7H,IAAM,oBAAoB;AASnB,IAAM,mBAAmB,OAAO,EAAE,QAAQ,MAAM,YAAY,kBAAkB,IAA4D,CAAC,MAAqC;AACnL,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,IAAI,gBAAgB;AAChC,QAAM,QAAQ,WAAW,MAAM,IAAI,MAAM,GAAG,SAAS;AACrD,MAAI;AAGA,UAAM,MAAM,MAAM,MAAM,GAAG,YAAY,IAAI,CAAC,uBAAuB,mBAAmB,GAAG,CAAC,IAAI;AAAA,MAC1F,QAAQ,IAAI;AAAA,MACZ,MAAM,EAAE,YAAY,EAAE;AAAA,IAC1B,CAAgB;AAChB,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,WAAO,UAAU,MAAM,QAAQ,OAAO,WAAW,IAAI,SAAS;AAAA,EAClE,QAAQ;AACJ,WAAO;AAAA,EACX,UAAE;AACE,iBAAa,KAAK;AAAA,EACtB;AACJ;AAGO,IAAM,cAAc,CAAC,iBAA2C;AACnE,QAAM,QAAQ,eAAe,WAAW;AACxC,SAAO,EAAE,SAAS,aAAa,eAAe,aAAa,CAAC,GAAG,WAAW,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI,WAAW,IAAI;AAC/H;AAGO,IAAM,iBAAiB,CAAC,YAAsC,SAAS,IAAI,SAAS,GAAG,SAAS;AAchG,IAAM,aAAa,CAAC,KAAa,EAAE,QAAQ,cAAc,SAAS,WAAW,MAChF,eAAe,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;AAOjG,IAAM,UAAU,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MACvE,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM,MAAM;AAGrE,IAAM,iBAAiB,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MAA4D;AAC1I,QAAM,IAAI,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM;AAC5E,SAAO,OAAO,MAAM,WAAW,IAAI;AACvC;AAGO,IAAM,WAAW,CAAC,EAAE,QAAQ,cAAc,SAAS,WAAW,MACjE,YAAY,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrocket/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "MRRocket SDK: page views, events, heatmap clicks, session recordings, A/B tests, feature flags, error tracking, feedback board, surveys, a chat messenger, a cancel flow, onboarding tours and checklists, in-app messages, push notifications and redirect tests that your MRRocket agents can run and read. Next.js (@mrrocket/sdk/next), React Native and Expo (@mrrocket/sdk/react-native), and a script tag for any website.",
5
5
  "keywords": [
6
6
  "nextjs",