@reactll/connect 0.0.0-stage → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reactor Technology
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
1
+ # Reactll Connect for Next.js and Node.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connects a Next.js or any Node.js site (Express, Fastify, Nuxt, Remix, Astro, Hono…) to its [Reactll](https://reactll.com) dashboard: visits, calls, WhatsApp and
4
+ directions clicks, form leads and site health. Built and maintained by **Reactor Technology**.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm i @reactll/connect
10
+ ```
11
+
12
+ ```dotenv
13
+ REACTLL_CONNECT_TOKEN=rcc_… # Reactll → Sites → the site → Install (server-side only)
14
+ ```
15
+
16
+ **1. The script** — once, in `app/layout.tsx`:
17
+
18
+ ```tsx
19
+ import { ReactllConnect } from '@reactll/connect';
20
+
21
+ export default function RootLayout({ children }) {
22
+ return (
23
+ <html lang="en">
24
+ <body>
25
+ {children}
26
+ <ReactllConnect />
27
+ </body>
28
+ </html>
29
+ );
30
+ }
31
+ ```
32
+
33
+ Cookieless; client-side navigations count as page views. Only the public site key reaches the browser.
34
+
35
+ **2. Health** — Reactll asks hourly (signed, nothing else can call it). `app/api/reactll/health/route.ts`:
36
+
37
+ ```ts
38
+ export { GET } from '@reactll/connect/server';
39
+ ```
40
+
41
+ **3. Form leads** — after your server action validated and saved the form:
42
+
43
+ ```ts
44
+ 'use server';
45
+ import { headers } from 'next/headers';
46
+ import { submitLeadFromFormData } from '@reactll/connect/server';
47
+
48
+ export async function contact(formData: FormData) {
49
+ // …validate and save…
50
+ await submitLeadFromFormData('Contact', formData, { headers: await headers() });
51
+ }
52
+ ```
53
+
54
+ The script stamps every form with the page view id (`_rc_pv`), so Reactll ties the lead to the visit it
55
+ came from. `submitLead(form, fields, options)` takes a plain object instead. Neither ever throws.
56
+
57
+ ## Any other Node.js app
58
+
59
+ The same package, without Next.js or React (they are optional):
60
+
61
+ ```js
62
+ import { scriptTag, expressHealth, submitLead } from '@reactll/connect/server';
63
+
64
+ app.use(expressHealth()); // Express / Connect / node:http — GET /api/reactll/health
65
+ res.render('page', { reactll: scriptTag() }); // put {{{ reactll }}} / <%- reactll %> in the <head>
66
+
67
+ // after your form was validated and saved:
68
+ await submitLead('Contact', { name, email, message }, { pv: req.body._rc_pv, page: req.get('referer') });
69
+ ```
70
+
71
+ Frameworks on web `Request`/`Response` (Hono, Remix, Nuxt/h3, Astro, Bun, Deno): route
72
+ `GET /api/reactll/health` to `health(request)` from `@reactll/connect/server`.
73
+
74
+ ## License
75
+
76
+ MIT © Reactor Technology
@@ -0,0 +1,13 @@
1
+ export { VERSION } from './token.js';
2
+ /**
3
+ * The Reactll Connect script. Put it once in your root layout (a server component):
4
+ *
5
+ * <ReactllConnect />
6
+ *
7
+ * It reads REACTLL_CONNECT_TOKEN on the server and renders nothing when it is missing. Cookieless;
8
+ * client-side navigations are counted as page views.
9
+ */
10
+ export declare function ReactllConnect({ token }: {
11
+ token?: string;
12
+ }): import("react").JSX.Element | null;
13
+ export default ReactllConnect;
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Script from 'next/script';
3
+ import { credentials } from './token.js';
4
+ export { VERSION } from './token.js';
5
+ /**
6
+ * The Reactll Connect script. Put it once in your root layout (a server component):
7
+ *
8
+ * <ReactllConnect />
9
+ *
10
+ * It reads REACTLL_CONNECT_TOKEN on the server and renders nothing when it is missing. Cookieless;
11
+ * client-side navigations are counted as page views.
12
+ */
13
+ export function ReactllConnect({ token }) {
14
+ const c = credentials(token ?? process.env.REACTLL_CONNECT_TOKEN);
15
+ if (!c)
16
+ return null;
17
+ return _jsx(Script, { id: "reactll-connect", src: `${c.u}/c.js`, "data-site": c.k, strategy: "afterInteractive" });
18
+ }
19
+ export default ReactllConnect;
@@ -0,0 +1,58 @@
1
+ export { VERSION, credentials } from './token.js';
2
+ type Fields = Record<string, unknown>;
3
+ interface LeadOptions {
4
+ /** The page view id the script stamps into every form as `_rc_pv`. */
5
+ pv?: string | null;
6
+ /** The page the form was sent from. */
7
+ page?: string | null;
8
+ /** Your own id for this submission, so a retry never doubles it. */
9
+ id?: string;
10
+ token?: string;
11
+ }
12
+ /**
13
+ * Send a form your site accepted to Reactll as a lead (from a server action or route handler, after
14
+ * validating and saving it). Never throws: false when Reactll could not be reached.
15
+ */
16
+ export declare function submitLead(form: string, fields: Fields, options?: LeadOptions): Promise<boolean>;
17
+ /**
18
+ * The same from a FormData (a server action's argument): `_rc_pv` is read from it, and the page from
19
+ * the request's Referer when you pass `headers()`.
20
+ */
21
+ export declare function submitLeadFromFormData(form: string, data: FormData, options?: LeadOptions & {
22
+ headers?: Headers;
23
+ }): Promise<boolean>;
24
+ /**
25
+ * The script tag for any server-rendered page (Express/EJS, Handlebars, Fastify, Nuxt, Astro…), or ''
26
+ * when not connected. Put it in the <head>. Only the public site key is in it.
27
+ */
28
+ export declare function scriptTag(token?: string): string;
29
+ /**
30
+ * Hourly health check, asked by Reactll (Next.js has no scheduler of its own). Export it from
31
+ * app/api/reactll/health/route.ts:
32
+ *
33
+ * export { GET } from '@reactll/connect/server';
34
+ *
35
+ * Only Reactll can call it: the request is signed with the site's secret.
36
+ */
37
+ export declare function GET(request: Request): Promise<Response>;
38
+ export declare const dynamic = "force-dynamic";
39
+ /** The health answer as plain data (platform and versions), for any framework. */
40
+ export declare function health(request: Request): Promise<Response>;
41
+ type NodeReq = {
42
+ method?: string;
43
+ url?: string;
44
+ originalUrl?: string;
45
+ headers: Record<string, string | string[] | undefined>;
46
+ };
47
+ type NodeRes = {
48
+ statusCode: number;
49
+ setHeader(name: string, value: string): unknown;
50
+ end(body?: string): unknown;
51
+ };
52
+ /**
53
+ * Express / Connect (and plain node:http) middleware answering Reactll's hourly health question at
54
+ * GET /api/reactll/health. Other requests pass through.
55
+ *
56
+ * app.use(expressHealth());
57
+ */
58
+ export declare function expressHealth(path?: string): (req: NodeReq, res: NodeRes, next?: () => void) => Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,145 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { credentials, VERSION } from './token.js';
4
+ export { VERSION, credentials } from './token.js';
5
+ const DROP = /pass(word)?|card|cvv|cvc|token|nonce|captcha|^_rc_pv$|^\$ACTION/i;
6
+ function sign(secret, payload) {
7
+ const ts = String(Math.floor(Date.now() / 1000));
8
+ return { ts, signature: createHmac('sha256', secret).update(`${ts}.${payload}`).digest('hex') };
9
+ }
10
+ /**
11
+ * Send a form your site accepted to Reactll as a lead (from a server action or route handler, after
12
+ * validating and saving it). Never throws: false when Reactll could not be reached.
13
+ */
14
+ export async function submitLead(form, fields, options = {}) {
15
+ const c = credentials(options.token);
16
+ if (!c)
17
+ return false;
18
+ const clean = {};
19
+ for (const [key, value] of Object.entries(fields)) {
20
+ if (DROP.test(key) || value === undefined || value === null || typeof value === 'function')
21
+ continue;
22
+ clean[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value);
23
+ }
24
+ const pv = options.pv && /^[0-9a-f-]{36}$/i.test(options.pv) ? options.pv : null;
25
+ const body = JSON.stringify({
26
+ id: options.id ?? `${form.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${randomUUID()}`,
27
+ form,
28
+ page: options.page ?? null,
29
+ pv,
30
+ submitted_at: new Date().toISOString(),
31
+ fields: clean,
32
+ });
33
+ const { ts, signature } = sign(c.s, body);
34
+ try {
35
+ const response = await fetch(`${c.u}/submissions`, {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ Accept: 'application/json',
40
+ 'X-Reactll-Key': c.k,
41
+ 'X-Reactll-Timestamp': ts,
42
+ 'X-Reactll-Signature': signature,
43
+ 'User-Agent': `ReactllConnect/${VERSION} Next.js`,
44
+ },
45
+ body,
46
+ signal: AbortSignal.timeout(4000),
47
+ cache: 'no-store',
48
+ });
49
+ return response.ok || response.status === 409;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * The same from a FormData (a server action's argument): `_rc_pv` is read from it, and the page from
57
+ * the request's Referer when you pass `headers()`.
58
+ */
59
+ export async function submitLeadFromFormData(form, data, options = {}) {
60
+ const fields = {};
61
+ data.forEach((value, key) => {
62
+ if (typeof value === 'string')
63
+ fields[key] = value;
64
+ });
65
+ const pv = typeof data.get('_rc_pv') === 'string' ? data.get('_rc_pv') : null;
66
+ return submitLead(form, fields, { ...options, pv: options.pv ?? pv, page: options.page ?? options.headers?.get('referer') ?? null });
67
+ }
68
+ /**
69
+ * The script tag for any server-rendered page (Express/EJS, Handlebars, Fastify, Nuxt, Astro…), or ''
70
+ * when not connected. Put it in the <head>. Only the public site key is in it.
71
+ */
72
+ export function scriptTag(token) {
73
+ const c = credentials(token);
74
+ return c ? `<script src="${c.u}/c.js" data-site="${c.k}" defer></script>` : '';
75
+ }
76
+ function versionOf(pkg) {
77
+ try {
78
+ return createRequire(import.meta.url)(`${pkg}/package.json`).version;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ /**
85
+ * Hourly health check, asked by Reactll (Next.js has no scheduler of its own). Export it from
86
+ * app/api/reactll/health/route.ts:
87
+ *
88
+ * export { GET } from '@reactll/connect/server';
89
+ *
90
+ * Only Reactll can call it: the request is signed with the site's secret.
91
+ */
92
+ export async function GET(request) {
93
+ const c = credentials();
94
+ if (!c)
95
+ return Response.json({ error: 'not connected' }, { status: 404 });
96
+ const ts = request.headers.get('x-reactll-timestamp') ?? '';
97
+ const given = request.headers.get('x-reactll-signature') ?? '';
98
+ const expected = createHmac('sha256', c.s).update(`${ts}.health`).digest('hex');
99
+ const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
100
+ if (!fresh || given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
101
+ return Response.json({ error: 'unauthorized' }, { status: 401 });
102
+ }
103
+ return Response.json({
104
+ platform: versionOf('next') ? 'next' : 'node',
105
+ version: VERSION,
106
+ runtime: Object.fromEntries(Object.entries({
107
+ next: versionOf('next'),
108
+ react: versionOf('react'),
109
+ node: process.version.replace(/^v/, ''),
110
+ environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
111
+ host: process.env.VERCEL ? 'Vercel' : undefined,
112
+ }).filter(([, v]) => v)),
113
+ }, { headers: { 'Cache-Control': 'no-store' } });
114
+ }
115
+ export const dynamic = 'force-dynamic';
116
+ /** The health answer as plain data (platform and versions), for any framework. */
117
+ export async function health(request) {
118
+ return GET(request);
119
+ }
120
+ /**
121
+ * Express / Connect (and plain node:http) middleware answering Reactll's hourly health question at
122
+ * GET /api/reactll/health. Other requests pass through.
123
+ *
124
+ * app.use(expressHealth());
125
+ */
126
+ export function expressHealth(path = '/api/reactll/health') {
127
+ return async (req, res, next) => {
128
+ const url = (req.originalUrl ?? req.url ?? '').split('?')[0];
129
+ if ((req.method ?? 'GET') !== 'GET' || url !== path) {
130
+ next?.();
131
+ return;
132
+ }
133
+ const header = (name) => {
134
+ const v = req.headers[name];
135
+ return Array.isArray(v) ? v[0] : (v ?? '');
136
+ };
137
+ const answer = await GET(new Request('http://localhost' + path, {
138
+ headers: { 'x-reactll-timestamp': header('x-reactll-timestamp'), 'x-reactll-signature': header('x-reactll-signature') },
139
+ }));
140
+ res.statusCode = answer.status;
141
+ res.setHeader('Content-Type', 'application/json');
142
+ res.setHeader('Cache-Control', 'no-store');
143
+ res.end(await answer.text());
144
+ };
145
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Reactll Connect for Next.js — built and maintained by Reactor Technology.
3
+ *
4
+ * The connection token (REACTLL_CONNECT_TOKEN, from Reactll → Sites → the site → Install) holds the
5
+ * endpoint, the site's public key and its secret. Only the endpoint and the public key ever reach
6
+ * the browser.
7
+ */
8
+ export declare const VERSION = "1.0.0";
9
+ export interface Credentials {
10
+ /** Endpoint, e.g. https://connect.reactll.com/v1 */
11
+ u: string;
12
+ /** Public site key (rc_…) */
13
+ k: string;
14
+ /** Secret: server only */
15
+ s: string;
16
+ }
17
+ export declare function credentials(token?: string | undefined | null): Credentials | null;
package/dist/token.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reactll Connect for Next.js — built and maintained by Reactor Technology.
3
+ *
4
+ * The connection token (REACTLL_CONNECT_TOKEN, from Reactll → Sites → the site → Install) holds the
5
+ * endpoint, the site's public key and its secret. Only the endpoint and the public key ever reach
6
+ * the browser.
7
+ */
8
+ export const VERSION = '1.0.0';
9
+ export function credentials(token = process.env.REACTLL_CONNECT_TOKEN) {
10
+ const raw = (token ?? '').trim();
11
+ if (!raw.startsWith('rcc_'))
12
+ return null;
13
+ try {
14
+ const json = Buffer.from(raw.slice(4).replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
15
+ const data = JSON.parse(json);
16
+ if (!data.u || !data.k || !data.s)
17
+ return null;
18
+ return { u: data.u.replace(/\/+$/, ''), k: data.k, s: data.s };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@reactll/connect",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "Reactll Connect for Next.js and any Node.js app: visits, clicks, form leads and site health for your Reactll dashboard.",
5
+ "keywords": [
6
+ "nextjs",
7
+ "nodejs",
8
+ "express",
9
+ "analytics",
10
+ "leads",
11
+ "reactll"
12
+ ],
13
+ "homepage": "https://reactll.com",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Reactll/connect-next.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Reactor Technology (https://reactll.com)",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./server": {
28
+ "types": "./dist/server.d.ts",
29
+ "default": "./dist/server.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "test": "npm run build && node --test test/",
40
+ "prepare": "tsc -p tsconfig.json"
41
+ },
42
+ "peerDependencies": {
43
+ "next": ">=13.4",
44
+ "react": ">=18"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "@types/react": "^19.0.0",
49
+ "next": "^15.5.0",
50
+ "react": "^19.0.0",
51
+ "typescript": "^5.6.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "next": {
55
+ "optional": true
56
+ },
57
+ "react": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "engines": {
62
+ "node": ">=18"
63
+ }
64
+ }