@reactll/connect 0.0.0-stage → 1.0.1
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 +21 -0
- package/README.md +58 -2
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.js +107 -0
- package/dist/token.d.ts +17 -0
- package/dist/token.js +23 -0
- package/package.json +49 -4
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,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Reactll Connect for Next.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connects a Next.js site 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
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT © Reactor Technology
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
* Hourly health check, asked by Reactll (Next.js has no scheduler of its own). Export it from
|
|
26
|
+
* app/api/reactll/health/route.ts:
|
|
27
|
+
*
|
|
28
|
+
* export { GET } from '@reactll/connect/server';
|
|
29
|
+
*
|
|
30
|
+
* Only Reactll can call it: the request is signed with the site's secret.
|
|
31
|
+
*/
|
|
32
|
+
export declare function GET(request: Request): Promise<Response>;
|
|
33
|
+
export declare const dynamic = "force-dynamic";
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
function versionOf(pkg) {
|
|
69
|
+
try {
|
|
70
|
+
return createRequire(import.meta.url)(`${pkg}/package.json`).version;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Hourly health check, asked by Reactll (Next.js has no scheduler of its own). Export it from
|
|
78
|
+
* app/api/reactll/health/route.ts:
|
|
79
|
+
*
|
|
80
|
+
* export { GET } from '@reactll/connect/server';
|
|
81
|
+
*
|
|
82
|
+
* Only Reactll can call it: the request is signed with the site's secret.
|
|
83
|
+
*/
|
|
84
|
+
export async function GET(request) {
|
|
85
|
+
const c = credentials();
|
|
86
|
+
if (!c)
|
|
87
|
+
return Response.json({ error: 'not connected' }, { status: 404 });
|
|
88
|
+
const ts = request.headers.get('x-reactll-timestamp') ?? '';
|
|
89
|
+
const given = request.headers.get('x-reactll-signature') ?? '';
|
|
90
|
+
const expected = createHmac('sha256', c.s).update(`${ts}.health`).digest('hex');
|
|
91
|
+
const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
|
|
92
|
+
if (!fresh || given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
|
|
93
|
+
return Response.json({ error: 'unauthorized' }, { status: 401 });
|
|
94
|
+
}
|
|
95
|
+
return Response.json({
|
|
96
|
+
platform: 'next',
|
|
97
|
+
version: VERSION,
|
|
98
|
+
runtime: Object.fromEntries(Object.entries({
|
|
99
|
+
next: versionOf('next'),
|
|
100
|
+
react: versionOf('react'),
|
|
101
|
+
node: process.version.replace(/^v/, ''),
|
|
102
|
+
environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
|
|
103
|
+
host: process.env.VERCEL ? 'Vercel' : undefined,
|
|
104
|
+
}).filter(([, v]) => v)),
|
|
105
|
+
}, { headers: { 'Cache-Control': 'no-store' } });
|
|
106
|
+
}
|
|
107
|
+
export const dynamic = 'force-dynamic';
|
package/dist/token.d.ts
ADDED
|
@@ -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,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactll/connect",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Reactll Connect for Next.js: visits, clicks, form leads and site health for your Reactll dashboard.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"analytics",
|
|
8
|
+
"leads",
|
|
9
|
+
"reactll"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://reactll.com",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Reactll/connect-next.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Reactor Technology (https://reactll.com)",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./dist/server.d.ts",
|
|
27
|
+
"default": "./dist/server.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"test": "npm run build && node --test test/",
|
|
38
|
+
"prepare": "tsc -p tsconfig.json"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"next": ">=13.4",
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"@types/react": "^19.0.0",
|
|
47
|
+
"next": "^15.5.0",
|
|
48
|
+
"react": "^19.0.0",
|
|
49
|
+
"typescript": "^5.6.0"
|
|
50
|
+
}
|
|
51
|
+
}
|