@conexus-x/next-view 1.0.0 → 1.0.2
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/.env +21 -0
- package/.env.example +21 -0
- package/.gitignore +1 -0
- package/README.md +55 -0
- package/app/dev/page.tsx +162 -0
- package/next.config.ts +16 -0
- package/package.json +5 -2
package/.env
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# DEVELOPER MODE. None of this is used by your view in production.
|
|
3
|
+
#
|
|
4
|
+
# The values here power the dev preview at /dev, which frames your view the way
|
|
5
|
+
# a Conexus X board does and feeds it REAL data from your workspace.
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
# Your Conexus X API key. Find it in the app under Settings > API.
|
|
9
|
+
#
|
|
10
|
+
# It is read by the dev server ONLY and never reaches the browser: requests go
|
|
11
|
+
# to /api/conexus/* on this app, which attaches the key server-side and forwards
|
|
12
|
+
# them. A permanent credential in client-side JavaScript is a permanent
|
|
13
|
+
# credential in every browser devtools panel that ever opens your view.
|
|
14
|
+
API_KEY=Conexus_x_api_key
|
|
15
|
+
|
|
16
|
+
# Where the Conexus X API lives.
|
|
17
|
+
CONEXUS_API_URL=http://localhost:4040/api
|
|
18
|
+
|
|
19
|
+
# Which origins may frame this view, space or comma separated.
|
|
20
|
+
# Add your tunnel's CRM origin here when previewing inside the real app.
|
|
21
|
+
CONEXUS_ORIGINS=http://localhost:3000 https://conexus-x.vercel.app
|
package/.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# DEVELOPER MODE. None of this is used by your view in production.
|
|
3
|
+
#
|
|
4
|
+
# The values here power the dev preview at /dev, which frames your view the way
|
|
5
|
+
# a Conexus X board does and feeds it REAL data from your workspace.
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
# Your Conexus X API key. Find it in the app under Settings > API.
|
|
9
|
+
#
|
|
10
|
+
# It is read by the dev server ONLY and never reaches the browser: requests go
|
|
11
|
+
# to /api/conexus/* on this app, which attaches the key server-side and forwards
|
|
12
|
+
# them. A permanent credential in client-side JavaScript is a permanent
|
|
13
|
+
# credential in every browser devtools panel that ever opens your view.
|
|
14
|
+
API_KEY=Conexus_x_api_key
|
|
15
|
+
|
|
16
|
+
# Where the Conexus X API lives.
|
|
17
|
+
CONEXUS_API_URL=http://localhost:4040/api
|
|
18
|
+
|
|
19
|
+
# Which origins may frame this view, space or comma separated.
|
|
20
|
+
# Add your tunnel's CRM origin here when previewing inside the real app.
|
|
21
|
+
CONEXUS_ORIGINS=http://localhost:3000 https://conexus-x.vercel.app
|
package/.gitignore
CHANGED
package/README.md
CHANGED
|
@@ -26,6 +26,61 @@ cd - && npm link @conexus-x/sdk
|
|
|
26
26
|
Nothing in this repo records the link, which is the point.
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
## Developer mode
|
|
30
|
+
|
|
31
|
+
You do not need a Conexus X board to start. Put your API key in `.env` and open
|
|
32
|
+
the developer page — it lists the workspaces that key can see, which is the
|
|
33
|
+
shortest proof that your key works and the API is reachable.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# .env — already in your project, just fill it in
|
|
37
|
+
API_KEY=Conexus_x_api_key # <- replace with your key (Settings > API)
|
|
38
|
+
CONEXUS_API_URL=http://localhost:4040/api
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then open **http://localhost:5173/dev**.
|
|
46
|
+
|
|
47
|
+
**The key never reaches the browser.** The page is a React Server Component: `API_KEY` is read in Node and only the finished list is sent to the browser.
|
|
48
|
+
A permanent credential in client-side JavaScript is one devtools panel away
|
|
49
|
+
from being somebody else's.
|
|
50
|
+
|
|
51
|
+
`.env` is gitignored, so your real key cannot be committed by accident. The
|
|
52
|
+
committed copy is `.env.example`.
|
|
53
|
+
|
|
54
|
+
The developer page is not part of a production build, and **your view never
|
|
55
|
+
uses the API key** — a deployed view receives data through the SDK, with the
|
|
56
|
+
permissions of whoever is looking at the board. The key is scaffolding for
|
|
57
|
+
development only.
|
|
58
|
+
|
|
59
|
+
## Previewing inside the real CRM
|
|
60
|
+
|
|
61
|
+
To see your view running inside an actual Conexus X board while you develop,
|
|
62
|
+
give it a public URL:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run dev # leave this running
|
|
66
|
+
npm run tunnel # in a second terminal
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That prints a `https://….trycloudflare.com` address. Then:
|
|
70
|
+
|
|
71
|
+
1. Put it in `entry` in `conexus.manifest.json`.
|
|
72
|
+
2. Add the CRM's origin to `CONEXUS_ORIGINS` in `.env` and restart the dev
|
|
73
|
+
server, so the view allows that origin to frame it.
|
|
74
|
+
|
|
75
|
+
The tunnel is Cloudflare's free quick tunnel — no account, no signup. It is
|
|
76
|
+
fetched on first use rather than installed with the project, because the binary
|
|
77
|
+
is large and most runs never need it. Quick tunnels have no uptime guarantee and
|
|
78
|
+
the URL changes every run; they are for previewing, not for shipping.
|
|
79
|
+
|
|
80
|
+
Chosen over the other free options because it serves your page directly. Some
|
|
81
|
+
tunnels show an interstitial "click to continue" page on first visit, which a
|
|
82
|
+
board cannot click through — the view would render as a blank frame.
|
|
83
|
+
|
|
29
84
|
## Tailwind is already set up
|
|
30
85
|
|
|
31
86
|
Tailwind v4 is installed and configured. There is no `tailwind.config.js` to
|
package/app/dev/page.tsx
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DEVELOPER MODE — http://localhost:5173/dev
|
|
5
|
+
*
|
|
6
|
+
* Put your API key in .env, reload, and this lists the workspaces that key can
|
|
7
|
+
* see. It is the shortest possible proof that your key works and the API is
|
|
8
|
+
* reachable, which is the thing that is actually wrong the first time nothing
|
|
9
|
+
* renders.
|
|
10
|
+
*
|
|
11
|
+
* A SERVER COMPONENT, deliberately. The key is read here, in Node, and only the
|
|
12
|
+
* finished list is sent to the browser — so the key never appears in a bundle,
|
|
13
|
+
* a network tab, or anybody's devtools. Fetching this from the browser would
|
|
14
|
+
* put a permanent credential in client-side JavaScript, and a permanent
|
|
15
|
+
* credential in client-side JavaScript is one screenshot away from being
|
|
16
|
+
* somebody else's.
|
|
17
|
+
*
|
|
18
|
+
* Your VIEW does not work this way and must not: it runs framed by a board and
|
|
19
|
+
* receives data through the SDK, with the signed-in person's own permissions.
|
|
20
|
+
* This page is scaffolding for development, nothing more.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Never prerendered: it reads .env and the network at request time.
|
|
24
|
+
export const dynamic = "force-dynamic";
|
|
25
|
+
|
|
26
|
+
const PLACEHOLDER = "Conexus_x_api_key";
|
|
27
|
+
|
|
28
|
+
interface WorkspaceMembership {
|
|
29
|
+
_id: string;
|
|
30
|
+
role?: string;
|
|
31
|
+
status?: string;
|
|
32
|
+
workspace?: {
|
|
33
|
+
_id: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
totalModules?: number;
|
|
37
|
+
} | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type LoadResult =
|
|
41
|
+
| { ok: true; workspaces: WorkspaceMembership[] }
|
|
42
|
+
| { ok: false; title: string; detail: string };
|
|
43
|
+
|
|
44
|
+
const loadWorkspaces = async (): Promise<LoadResult> => {
|
|
45
|
+
const apiKey = process.env.API_KEY;
|
|
46
|
+
const apiUrl = (process.env.CONEXUS_API_URL ?? "http://localhost:4040/api").replace(/\/$/, "");
|
|
47
|
+
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
title: "No API key",
|
|
52
|
+
detail: "Add API_KEY to .env in this project, then reload."
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (apiKey === PLACEHOLDER) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
title: "The API key is still the placeholder",
|
|
60
|
+
detail: `Replace API_KEY=${PLACEHOLDER} in .env with your own key from Conexus X (Settings › API), then reload.`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${apiUrl}/workspaces`, {
|
|
66
|
+
headers: { "x-api-key": apiKey },
|
|
67
|
+
cache: "no-store"
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (response.status === 401) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
title: "That key was rejected",
|
|
74
|
+
detail: "The API answered 401. Check the key, and that the account it belongs to is still active."
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
title: `The API answered ${response.status}`,
|
|
82
|
+
detail: (await response.text()).slice(0, 300)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const body = (await response.json()) as { workspaces?: WorkspaceMembership[] };
|
|
87
|
+
|
|
88
|
+
return { ok: true, workspaces: body.workspaces ?? [] };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
title: "Could not reach the API",
|
|
93
|
+
detail:
|
|
94
|
+
`Tried ${apiUrl}/workspaces — ${error instanceof Error ? error.message : String(error)}. ` +
|
|
95
|
+
"Is the Conexus X API running, and is CONEXUS_API_URL right?"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default async function DevPage() {
|
|
101
|
+
// This page reads a secret and exists only to help you build. It has no
|
|
102
|
+
// business in a deployed view, so it is not there.
|
|
103
|
+
if (process.env.NODE_ENV === "production") notFound();
|
|
104
|
+
|
|
105
|
+
const result = await loadWorkspaces();
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<main className="mx-auto max-w-2xl p-6">
|
|
109
|
+
<header className="mb-5">
|
|
110
|
+
<h1 className="text-lg font-semibold">Developer mode</h1>
|
|
111
|
+
<p className="mt-1 text-[13px] text-muted">
|
|
112
|
+
Workspaces visible to the API key in <code>.env</code>.
|
|
113
|
+
</p>
|
|
114
|
+
</header>
|
|
115
|
+
|
|
116
|
+
{!result.ok ? (
|
|
117
|
+
<div className="rounded-lg border border-danger bg-danger-soft px-4 py-3 text-danger">
|
|
118
|
+
<strong className="font-semibold">{result.title}</strong>
|
|
119
|
+
<p className="mt-1.5 text-[13px] whitespace-pre-wrap">{result.detail}</p>
|
|
120
|
+
</div>
|
|
121
|
+
) : result.workspaces.length === 0 ? (
|
|
122
|
+
<p className="text-[13px] text-muted">
|
|
123
|
+
The key works — this account is not a member of any workspace yet.
|
|
124
|
+
</p>
|
|
125
|
+
) : (
|
|
126
|
+
<ul className="rounded-lg border border-line">
|
|
127
|
+
{result.workspaces.map((membership) => (
|
|
128
|
+
<li
|
|
129
|
+
key={membership._id}
|
|
130
|
+
className="flex items-center gap-3 border-b border-line px-4 py-3 last:border-b-0"
|
|
131
|
+
>
|
|
132
|
+
<div className="min-w-0 flex-1">
|
|
133
|
+
<div className="truncate font-medium">
|
|
134
|
+
{membership.workspace?.name ?? "Untitled workspace"}
|
|
135
|
+
</div>
|
|
136
|
+
<div className="mt-0.5 font-mono text-[12px] text-muted">
|
|
137
|
+
{membership.workspace?._id}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{membership.role ? (
|
|
142
|
+
<span className="shrink-0 rounded-md border border-line px-2 py-0.5 text-[12px] text-muted">
|
|
143
|
+
{membership.role}
|
|
144
|
+
</span>
|
|
145
|
+
) : null}
|
|
146
|
+
|
|
147
|
+
<span className="shrink-0 text-[12px] text-muted">
|
|
148
|
+
{membership.workspace?.totalModules ?? 0} boards
|
|
149
|
+
</span>
|
|
150
|
+
</li>
|
|
151
|
+
))}
|
|
152
|
+
</ul>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<p className="mt-5 text-[12px] text-muted">
|
|
156
|
+
This page is development-only and returns 404 in a production build.
|
|
157
|
+
Your view itself never uses the API key — it receives data through
|
|
158
|
+
the SDK, with the permissions of whoever is looking at the board.
|
|
159
|
+
</p>
|
|
160
|
+
</main>
|
|
161
|
+
);
|
|
162
|
+
}
|
package/next.config.ts
CHANGED
|
@@ -19,6 +19,22 @@ const conexusOrigins =
|
|
|
19
19
|
"http://localhost:3000 https://conexus-x.vercel.app";
|
|
20
20
|
|
|
21
21
|
const nextConfig: NextConfig = {
|
|
22
|
+
/**
|
|
23
|
+
* Let a tunnel reach the dev server.
|
|
24
|
+
*
|
|
25
|
+
* Next refuses cross-origin dev requests it was not told about, which is
|
|
26
|
+
* the reason `npm run tunnel` would otherwise serve a broken page through
|
|
27
|
+
* the public URL — the HTML arrives and the dev assets behind it do not.
|
|
28
|
+
*
|
|
29
|
+
* Development only; a production build ignores this entirely.
|
|
30
|
+
*/
|
|
31
|
+
allowedDevOrigins: [
|
|
32
|
+
"*.trycloudflare.com",
|
|
33
|
+
"*.loca.lt",
|
|
34
|
+
"*.ngrok-free.app",
|
|
35
|
+
"*.ngrok.io"
|
|
36
|
+
],
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
39
|
* NOT pinning `turbopack.root` here, deliberately.
|
|
24
40
|
*
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conexus-x/next-view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Custom view for Conexus X — Next.js starter, wired to @conexus-x/sdk.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "next dev --port 5173",
|
|
8
8
|
"build": "next build",
|
|
9
9
|
"start": "next start --port 5173",
|
|
10
|
-
"lint": "eslint"
|
|
10
|
+
"lint": "eslint",
|
|
11
|
+
"tunnel": "npx -y cloudflared tunnel --url http://localhost:5173"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@conexus-x/sdk": "^0.1.0",
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
"postcss.config.mjs",
|
|
34
35
|
"tsconfig.json",
|
|
35
36
|
"conexus.manifest.json",
|
|
37
|
+
".env",
|
|
38
|
+
".env.example",
|
|
36
39
|
"README.md",
|
|
37
40
|
".gitignore"
|
|
38
41
|
]
|