@exposify/next 0.0.4 → 0.0.5
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/exposify-next.js +101 -47
- package/dist/index.cjs +2 -1
- package/dist/index.js +2 -1
- package/package.json +2 -2
package/dist/exposify-next.js
CHANGED
|
@@ -2667,6 +2667,10 @@ var nextAdapter = {
|
|
|
2667
2667
|
bin: "npx @exposify/next",
|
|
2668
2668
|
entryExample: "apps/web",
|
|
2669
2669
|
entryHelp: "The Next.js project directory",
|
|
2670
|
+
commands: [
|
|
2671
|
+
["generate", "Write exposify.generated.ts (withExposify() in next.config does it on dev and build)"],
|
|
2672
|
+
["test-sign-in", "Check, on your running app, that the Supabase session Connect sends signs you in"]
|
|
2673
|
+
],
|
|
2670
2674
|
locate(cwd, entryFlag) {
|
|
2671
2675
|
const root = findNextRoot(entryFlag ? resolve2(cwd, entryFlag) : cwd);
|
|
2672
2676
|
return root ? { entry: root, root, via: "package.json" } : void 0;
|
|
@@ -2697,11 +2701,9 @@ ${block}
|
|
|
2697
2701
|
if (stubs.some((s) => s.includes("TODO(exposify)"))) {
|
|
2698
2702
|
lines.push("Go through the TODO(exposify) lines: an action whose input is still incomplete stays hidden until you write it out.");
|
|
2699
2703
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
);
|
|
2704
|
-
}
|
|
2704
|
+
const wrap = hasWithExposify(root) ? null : wrapNextConfig(root);
|
|
2705
|
+
const also = wrap && "plan" in wrap ? [{ plan: wrap.plan, manual: WRAP_HINT }] : [];
|
|
2706
|
+
if (wrap && !("plan" in wrap)) lines.push(WRAP_HINT);
|
|
2705
2707
|
const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join9(root, f)).find((f) => existsSync4(f));
|
|
2706
2708
|
const manifestFile = existing ?? generate(root, {}, () => {
|
|
2707
2709
|
}).file;
|
|
@@ -2709,11 +2711,8 @@ ${block}
|
|
|
2709
2711
|
if (result.aiReady?.site) {
|
|
2710
2712
|
const file = relative3(root, result.aiReady.site.file);
|
|
2711
2713
|
if (usesZod) lines.push('The input stubs use zod: `import { z } from "zod";` in that file, if it isn\'t there.');
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
hint: { lines: [`Replace the aiReady(...) call in ${file}:${result.aiReady.site.line} with this.`, ...lines] },
|
|
2715
|
-
apply: applyReplace(result.aiReady.site, config, entries, usesZod, file)
|
|
2716
|
-
};
|
|
2714
|
+
const apply = withAlso(applyReplace(result.aiReady.site, config, entries, usesZod, file), also, lines);
|
|
2715
|
+
return { code: options, hint: { lines: [`Replace the aiReady(...) call in ${file}:${result.aiReady.site.line} with this.`, ...lines] }, apply };
|
|
2717
2716
|
}
|
|
2718
2717
|
const routeFile = `${d.appDir ?? "app"}/${MCP_ROUTE_DIR}/route.${generated.endsWith(".ts") ? "ts" : "js"}`;
|
|
2719
2718
|
const manifestImport = relative3(join9(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
|
|
@@ -2729,13 +2728,47 @@ ${block}
|
|
|
2729
2728
|
return {
|
|
2730
2729
|
code,
|
|
2731
2730
|
apply: { plan: { file: join9(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
|
|
2732
|
-
` } },
|
|
2731
|
+
` }, ...also.length ? { also } : {} },
|
|
2733
2732
|
hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
|
|
2734
2733
|
};
|
|
2735
2734
|
}
|
|
2736
2735
|
};
|
|
2736
|
+
var WRAP_HINT = 'Wrap your next.config export: `export default withExposify(nextConfig)`, imported from "@exposify/next/config". It keeps exposify.generated.ts current on `next dev` and `next build`.';
|
|
2737
|
+
var NEXT_CONFIGS = ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs", "next.config.mts"];
|
|
2738
|
+
function withAlso(outcome, also, lines) {
|
|
2739
|
+
if ("plan" in outcome) return also.length ? { ...outcome, also } : outcome;
|
|
2740
|
+
lines.push(...also.map((a) => a.manual));
|
|
2741
|
+
return outcome;
|
|
2742
|
+
}
|
|
2743
|
+
function wrapNextConfig(root) {
|
|
2744
|
+
const name = NEXT_CONFIGS.find((n) => existsSync4(join9(root, n)));
|
|
2745
|
+
if (!name) return { blocked: "no next.config file" };
|
|
2746
|
+
const file = join9(root, name);
|
|
2747
|
+
const before = readFileSync9(file, "utf8");
|
|
2748
|
+
const program = parseModule(before, name);
|
|
2749
|
+
if (!program) return { blocked: `couldn't parse ${name}` };
|
|
2750
|
+
const body = program.body;
|
|
2751
|
+
const esm = body.find((n) => n.type === "ExportDefaultDeclaration");
|
|
2752
|
+
const cjs = body.find((n) => {
|
|
2753
|
+
const e = n.type === "ExpressionStatement" ? unwrapTs(n.expression) : void 0;
|
|
2754
|
+
return e?.type === "AssignmentExpression" && e.left?.type === "MemberExpression" && e.left.object?.name === "module" && e.left.property?.name === "exports";
|
|
2755
|
+
});
|
|
2756
|
+
const value = esm ? esm.declaration : cjs ? unwrapTs(cjs.expression)?.right : void 0;
|
|
2757
|
+
if (!value || value.start == null || value.end == null || /Declaration$/.test(value.type)) return { blocked: `couldn't find the config ${name} exports` };
|
|
2758
|
+
const wrapped = `${before.slice(0, value.start)}withExposify(${before.slice(value.start, value.end)})${before.slice(value.end)}`;
|
|
2759
|
+
const imports = body.filter((n) => n.type === "ImportDeclaration" || n.type === "VariableDeclaration" && /require\(/.test(before.slice(n.start, n.end)));
|
|
2760
|
+
const at = imports.length ? imports.at(-1).end : 0;
|
|
2761
|
+
const last = at ? before.slice(imports.at(-1).start, at) : "";
|
|
2762
|
+
const q = /'/.test(last) ? "'" : '"';
|
|
2763
|
+
const semi = !last || last.trimEnd().endsWith(";") ? ";" : "";
|
|
2764
|
+
const line = esm ? `import { withExposify } from ${q}@exposify/next/config${q}${semi}` : `const { withExposify } = require(${q}@exposify/next/config${q})${semi}`;
|
|
2765
|
+
const after = at ? `${wrapped.slice(0, at)}
|
|
2766
|
+
${line}${wrapped.slice(at)}` : `${line}
|
|
2767
|
+
${wrapped}`;
|
|
2768
|
+
return { plan: { file, summary: `Wrap ${name}'s export in withExposify()`, before, after } };
|
|
2769
|
+
}
|
|
2737
2770
|
function hasWithExposify(root) {
|
|
2738
|
-
for (const name of
|
|
2771
|
+
for (const name of NEXT_CONFIGS) {
|
|
2739
2772
|
try {
|
|
2740
2773
|
if (readFileSync9(join9(root, name), "utf8").includes("withExposify")) return true;
|
|
2741
2774
|
} catch {
|
|
@@ -2752,13 +2785,21 @@ function scanProject(root) {
|
|
|
2752
2785
|
const notes = [];
|
|
2753
2786
|
const descriptions = readDescriptions(root, (m) => notes.push(m));
|
|
2754
2787
|
const rows = [];
|
|
2788
|
+
const callbacks = new Set(d.supabase?.choice.callbacks ?? []);
|
|
2755
2789
|
for (const r of d.routes) {
|
|
2756
2790
|
if (r.router === "app") {
|
|
2791
|
+
const callback = callbacks.has(r.file);
|
|
2757
2792
|
for (const method of r.module.methods.filter((m) => m !== "OPTIONS")) {
|
|
2758
2793
|
const m = r.module.markers[method];
|
|
2759
2794
|
const marker = m ? m.kind === "described" ? { kind: "described", ...m.name ? { name: m.name } : {}, ...m.description ? { description: m.description } : {} } : { kind: m.kind } : void 0;
|
|
2760
2795
|
const source = r.module.sources[method];
|
|
2761
|
-
rows.push({
|
|
2796
|
+
rows.push({
|
|
2797
|
+
info: { method, path: r.path, params: r.params, ...marker ? { marker } : {}, ...callback ? { optIn: true } : {} },
|
|
2798
|
+
...m?.name ? { describedName: m.name } : {},
|
|
2799
|
+
...m?.description ? { describedText: m.description } : {},
|
|
2800
|
+
...source ? { source } : {},
|
|
2801
|
+
...callback ? { callback: true } : {}
|
|
2802
|
+
});
|
|
2762
2803
|
}
|
|
2763
2804
|
continue;
|
|
2764
2805
|
}
|
|
@@ -2817,8 +2858,9 @@ function scanProject(root) {
|
|
|
2817
2858
|
tool: names.get(key),
|
|
2818
2859
|
...derived ? { derived } : {},
|
|
2819
2860
|
exposed: decision.exposed,
|
|
2820
|
-
reason: row.formOnly && !decision.exposed ? `a page that's just a form posting to a Server Action (the action is the tool); list "GET ${row.info.path}" in expose to read it anyway` : row.page && decision.exposed ? "a page: read like a browser reads it" : decision.reason,
|
|
2861
|
+
reason: row.formOnly && !decision.exposed ? `a page that's just a form posting to a Server Action (the action is the tool); list "GET ${row.info.path}" in expose to read it anyway` : row.callback && !decision.exposed ? `finishes a Supabase sign-in from an email link or a redirect, which an agent has no use for; list "${key}" in expose to make it a tool anyway` : row.page && decision.exposed ? "a page: read like a browser reads it" : decision.reason,
|
|
2821
2862
|
...row.info.optIn ? { optIn: true } : {},
|
|
2863
|
+
...row.formOnly || row.callback ? { offByDefault: true } : {},
|
|
2822
2864
|
...row.page ? { kind: "page" } : {},
|
|
2823
2865
|
...row.source ? { source: row.source } : {}
|
|
2824
2866
|
};
|
|
@@ -2886,10 +2928,13 @@ var TEST_SIGN_IN_USAGE = `Usage: @exposify/next test-sign-in [options]
|
|
|
2886
2928
|
Signs in to your app's Supabase Auth as AI assistants will (a password, then
|
|
2887
2929
|
the authenticator code if you have one), and checks that a page your app
|
|
2888
2930
|
protects accepts the session cookie Connect sends. Run it with your app up.
|
|
2931
|
+
With no page that turns signed-out visitors away, it looks for one that
|
|
2932
|
+
shows your email once signed in.
|
|
2889
2933
|
|
|
2890
2934
|
Options:
|
|
2891
2935
|
--url <origin> Where the app runs (default: http://localhost:3000)
|
|
2892
|
-
--page <path> A page that redirects signed-out visitors
|
|
2936
|
+
--page <path> A page that redirects signed-out visitors, or shows
|
|
2937
|
+
who's signed in (default: found)
|
|
2893
2938
|
--entry <path> The Next.js project directory (default: detected)
|
|
2894
2939
|
|
|
2895
2940
|
Reads NEXT_PUBLIC_SUPABASE_URL and the publishable (or anon) key from the
|
|
@@ -2920,36 +2965,27 @@ async function testSignIn(root, opts, deps) {
|
|
|
2920
2965
|
return 1;
|
|
2921
2966
|
}
|
|
2922
2967
|
const origin = opts.url.replace(/\/+$/, "");
|
|
2923
|
-
const get = (path,
|
|
2924
|
-
let page
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
if (res2 === null) {
|
|
2931
|
-
deps.log(`Couldn't reach ${origin}. Start your app (next dev), or pass --url.`);
|
|
2932
|
-
return 1;
|
|
2933
|
-
}
|
|
2934
|
-
if (redirected(res2)) {
|
|
2935
|
-
page = p.path;
|
|
2936
|
-
break;
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
if (!page) {
|
|
2940
|
-
deps.log("No page turned a signed-out request away, so there's nothing to check the session against. Pass --page with one that does.");
|
|
2941
|
-
return 1;
|
|
2942
|
-
}
|
|
2943
|
-
} else {
|
|
2944
|
-
const res2 = await get(page);
|
|
2945
|
-
if (res2 === null) {
|
|
2968
|
+
const get = (path, cookie2) => deps.fetch(`${origin}${path}`, { headers: cookie2 ? { cookie: cookie2 } : {}, redirect: "manual" }).catch(() => null);
|
|
2969
|
+
let page;
|
|
2970
|
+
const open = [];
|
|
2971
|
+
const candidates2 = opts.page ? [opts.page] : d.pages.filter((p) => p.params.length === 0 && !(d.supabase.choice.loginPages ?? []).includes(p.path) && !p.path.startsWith(d.mcpPath ?? "/mcp")).slice(0, 15).map((p) => p.path);
|
|
2972
|
+
for (const path of candidates2) {
|
|
2973
|
+
const res = await get(path);
|
|
2974
|
+
if (res === null) {
|
|
2946
2975
|
deps.log(`Couldn't reach ${origin}. Start your app (next dev), or pass --url.`);
|
|
2947
2976
|
return 1;
|
|
2948
2977
|
}
|
|
2949
|
-
if (
|
|
2950
|
-
|
|
2951
|
-
|
|
2978
|
+
if (redirected(res)) {
|
|
2979
|
+
page = path;
|
|
2980
|
+
break;
|
|
2952
2981
|
}
|
|
2982
|
+
if (res.ok) open.push({ path, body: await res.text() });
|
|
2983
|
+
}
|
|
2984
|
+
if (!page && open.length === 0) {
|
|
2985
|
+
deps.log(
|
|
2986
|
+
opts.page ? `${opts.page} answered with an error without signing in, so it can't tell whether the session works. Pass --page with a page that redirects signed-out visitors or shows who's signed in.` : "No page answered a signed-out request, so there's nothing to check the session against. Pass --page with one that redirects signed-out visitors or shows who's signed in."
|
|
2987
|
+
);
|
|
2988
|
+
return 1;
|
|
2953
2989
|
}
|
|
2954
2990
|
const login = supabasePasswordLogin({ ...project, fetch: deps.fetch });
|
|
2955
2991
|
const ctx = { origin };
|
|
@@ -2967,13 +3003,31 @@ async function testSignIn(root, opts, deps) {
|
|
|
2967
3003
|
const session = result.session.supabase;
|
|
2968
3004
|
const name = d.supabase.choice.cookieName ?? supabaseCookieName(project.url);
|
|
2969
3005
|
const cookies = supabaseSessionCookies(name, session);
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3006
|
+
const cookie = cookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
3007
|
+
const cookieLabel = cookies.length === 1 ? name : `${name}.0\u2013.${cookies.length - 1}`;
|
|
3008
|
+
let ok = false;
|
|
3009
|
+
if (page) {
|
|
3010
|
+
const res = await get(page, cookie);
|
|
3011
|
+
ok = Boolean(res && res.status >= 200 && res.status < 300);
|
|
3012
|
+
if (ok) deps.log(`\u2713 ${page} answered ${res.status} with the session cookie Connect sends (${cookieLabel}), and turned you away without it.`);
|
|
3013
|
+
else deps.log(`\u2717 ${page} answered ${res?.status ?? "nothing"} with the session cookie${res?.headers.get("location") ? ` (to ${res.headers.get("location")})` : ""}: your app didn't accept it.`);
|
|
3014
|
+
} else {
|
|
3015
|
+
const shown = (typeof session.user?.email === "string" ? session.user.email : email).toLowerCase();
|
|
3016
|
+
for (const p of open.filter((o) => !o.body.toLowerCase().includes(shown))) {
|
|
3017
|
+
const res = await get(p.path, cookie);
|
|
3018
|
+
if (res?.ok && (await res.text()).toLowerCase().includes(shown)) {
|
|
3019
|
+
deps.log(`\u2713 ${p.path} showed ${shown} with the session cookie Connect sends (${cookieLabel}), and didn't without it.`);
|
|
3020
|
+
ok = true;
|
|
3021
|
+
break;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
if (!ok) {
|
|
3025
|
+
deps.log(`\u2717 No page turns signed-out visitors away, and none of ${open.map((o) => o.path).join(", ")} showed ${shown} with the session cookie: your app may not have accepted it.`);
|
|
3026
|
+
deps.log(" Pass --page with a page that redirects signed-out visitors or shows who's signed in, to check against it.");
|
|
3027
|
+
}
|
|
2975
3028
|
}
|
|
2976
|
-
deps.
|
|
3029
|
+
await deps.fetch(`${project.url.replace(/\/+$/, "")}/auth/v1/logout?scope=local`, { method: "POST", headers: { apikey: project.key, authorization: `Bearer ${session.accessToken}` } }).catch(() => null);
|
|
3030
|
+
if (ok) return 0;
|
|
2977
3031
|
if (d.supabase.choice.cookieName === void 0) deps.log(` It was named ${name}. If your app sets another cookie name, the generator didn't see it.`);
|
|
2978
3032
|
return 1;
|
|
2979
3033
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -698,6 +698,7 @@ function errorMessage(err) {
|
|
|
698
698
|
}
|
|
699
699
|
async function routesFrom(manifest, cfg, warnings) {
|
|
700
700
|
const listed = Array.isArray(cfg.expose) ? cfg.expose.map(import_core.parseMethodEntry).filter((e) => e !== null) : [];
|
|
701
|
+
const callbacks = new Set(manifest.supabase?.callbacks ?? []);
|
|
701
702
|
const perEntry = await Promise.all(
|
|
702
703
|
manifest.routes.map(async (entry) => {
|
|
703
704
|
const base = { path: entry.path, params: entry.params };
|
|
@@ -715,7 +716,7 @@ async function routesFrom(manifest, cfg, warnings) {
|
|
|
715
716
|
const methods = mod ? HTTP_METHODS.filter((m) => typeof mod[m] === "function") : entry.methods;
|
|
716
717
|
return methods.filter((m) => m !== "OPTIONS").map((method) => {
|
|
717
718
|
const marker = mod ? (0, import_core.getMarker)(mod[method]) : void 0;
|
|
718
|
-
return { ...base, method, ...marker ? { marker } : {} };
|
|
719
|
+
return { ...base, method, ...marker ? { marker } : {}, ...callbacks.has(entry.file) ? { optIn: true } : {} };
|
|
719
720
|
});
|
|
720
721
|
})
|
|
721
722
|
);
|
package/dist/index.js
CHANGED
|
@@ -712,6 +712,7 @@ function errorMessage(err) {
|
|
|
712
712
|
}
|
|
713
713
|
async function routesFrom(manifest, cfg, warnings) {
|
|
714
714
|
const listed = Array.isArray(cfg.expose) ? cfg.expose.map(parseMethodEntry).filter((e) => e !== null) : [];
|
|
715
|
+
const callbacks = new Set(manifest.supabase?.callbacks ?? []);
|
|
715
716
|
const perEntry = await Promise.all(
|
|
716
717
|
manifest.routes.map(async (entry) => {
|
|
717
718
|
const base = { path: entry.path, params: entry.params };
|
|
@@ -729,7 +730,7 @@ async function routesFrom(manifest, cfg, warnings) {
|
|
|
729
730
|
const methods = mod ? HTTP_METHODS.filter((m) => typeof mod[m] === "function") : entry.methods;
|
|
730
731
|
return methods.filter((m) => m !== "OPTIONS").map((method) => {
|
|
731
732
|
const marker = mod ? getMarker(mod[method]) : void 0;
|
|
732
|
-
return { ...base, method, ...marker ? { marker } : {} };
|
|
733
|
+
return { ...base, method, ...marker ? { marker } : {}, ...callbacks.has(entry.file) ? { optIn: true } : {} };
|
|
733
734
|
});
|
|
734
735
|
})
|
|
735
736
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@babel/parser": "^8.0.0",
|
|
35
|
-
"@exposify/cli": "0.0.
|
|
35
|
+
"@exposify/cli": "0.0.4",
|
|
36
36
|
"@exposify/core": "0.3.16",
|
|
37
37
|
"tinyglobby": "^0.2.10",
|
|
38
38
|
"zod": "^3.24.0"
|