@stacksjs/actions 0.72.18 → 0.72.19

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.
@@ -21,4 +21,5 @@
21
21
  * Returns the vendored path if neither resolves, letting the caller (stx serve)
22
22
  * surface a clear missing-directory error rather than a silent empty glob.
23
23
  */
24
+ export declare function resolveDefaultsRoot(): string;
24
25
  export declare function resolveDefaultsResources(): string;
@@ -1 +1 @@
1
- import{existsSync}from"node:fs";import{dirname,join,resolve}from"node:path";export function resolveDefaultsResources(){const projectRoot=resolve(import.meta.dir,"../../../../../.."),vendored=join(projectRoot,"storage/framework/defaults/resources");if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return join(dirname(pkgJson),"resources")}catch{return vendored}}
1
+ import{existsSync}from"node:fs";import{dirname,join,resolve}from"node:path";export function resolveDefaultsRoot(){const projectRoot=resolve(import.meta.dir,"../../../../../.."),vendored=join(projectRoot,"storage/framework/defaults");if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return dirname(pkgJson)}catch{return vendored}}export function resolveDefaultsResources(){const projectRoot=resolve(import.meta.dir,"../../../../../.."),vendored=join(projectRoot,"storage/framework/defaults/resources");if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return join(dirname(pkgJson),"resources")}catch{return vendored}}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A request for a file rather than a page.
3
+ *
4
+ * Assets are allowed through unauthenticated because the sign-in page needs
5
+ * its stylesheet and scripts to render at all, and because a filename is not
6
+ * the data being protected. The test is deliberately narrow: a trailing
7
+ * extension, or one of the runtime's own prefixes. Every extensionless path -
8
+ * which is what a dashboard page looks like - falls through to the gate.
9
+ */
10
+ export declare function isAssetPath(pathname: string): boolean;
11
+ /**
12
+ * Whether this request even reaches the page layer.
13
+ *
14
+ * Anything under `/api/` and every mutating verb belongs to the router, which
15
+ * authenticates on its own terms - a POST to `/login` has to arrive
16
+ * unauthenticated, by definition. Gating those here would either break sign-in
17
+ * or double-gate an endpoint that already returns 401.
18
+ */
19
+ export declare function isDelegatedRequest(method: string, pathname: string): boolean;
20
+ /**
21
+ * Decide whether a dashboard request may be rendered.
22
+ *
23
+ * `validate` resolves a token to its user, or to anything falsy for a token
24
+ * that is forged, expired or revoked - the same check a bearer token gets. It
25
+ * throwing counts as invalid: a malformed token that breaks the lookup is not
26
+ * an authenticated visitor.
27
+ */
28
+ export declare function decideDashboardAccess(request: { method: string, pathname: string, token: string | undefined }, validate: (token: string) => Promise<unknown>, options?: GateOptions): Promise<GateDecision>;
29
+ export declare interface GateOptions {
30
+ publicPaths?: readonly string[]
31
+ }
32
+ /**
33
+ * Who may see a dashboard page in production.
34
+ *
35
+ * The dev dashboard runs with `auth: false` on purpose - it serves one
36
+ * developer on localhost. Deploying that same server unchanged publishes every
37
+ * staff page to the internet, so this is the piece that has to exist before a
38
+ * dashboard can be exposed at all.
39
+ *
40
+ * DENY BY DEFAULT is the whole design. stx's page middleware
41
+ * (`definePageMeta({ middleware: ['auth'] })`) is opt-in per page, which means
42
+ * a page that forgets the declaration is public - and in the app this was
43
+ * built for, ZERO of eleven dashboard pages declared it. An allowlist of the
44
+ * few pages that must render signed-out is the only shape where forgetting
45
+ * something fails closed.
46
+ *
47
+ * The decision is pure and lives here alone so it can be tested without a
48
+ * server, a database or a browser. `serve/dashboard.ts` only supplies the
49
+ * request and a token validator.
50
+ */
51
+ /** What the gate decided, and why. */
52
+ export type GateDecision = | { allow: true, reason: 'public-page' | 'asset' | 'delegated' }
53
+ /** Render it: a valid session was presented. */
54
+ | { allow: true, reason: 'authenticated' }
55
+ /** Send them to sign in. */
56
+ | { allow: false, reason: 'no-session' | 'invalid-session' }
@@ -0,0 +1 @@
1
+ const DEFAULT_PUBLIC_PATHS=["/login","/health"];export function isAssetPath(pathname){if(pathname.startsWith("/_stx/")||pathname.startsWith("/@"))return!0;return pathname.slice(pathname.lastIndexOf("/")+1).includes(".")}export function isDelegatedRequest(method,pathname){if(pathname==="/api"||pathname.startsWith("/api/"))return!0;return method!=="GET"&&method!=="HEAD"}export async function decideDashboardAccess(request,validate,options={}){const publicPaths=options.publicPaths??DEFAULT_PUBLIC_PATHS;if(isDelegatedRequest(request.method,request.pathname))return{allow:!0,reason:"delegated"};if(isAssetPath(request.pathname))return{allow:!0,reason:"asset"};const pathname=request.pathname.length>1?request.pathname.replace(/\/+$/,""):request.pathname;if(publicPaths.includes(pathname))return{allow:!0,reason:"public-page"};if(!request.token)return{allow:!1,reason:"no-session"};try{if(await validate(request.token))return{allow:!0,reason:"authenticated"}}catch{}return{allow:!1,reason:"invalid-session"}}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ import{existsSync}from"node:fs";import{join}from"node:path";import process from"node:process";import{projectPath,publicPath}from"@stacksjs/path";import{resolveDefaultsRoot}from"../dev/defaults-resources";import{decideDashboardAccess}from"./dashboard-gate";process.env.APP_ENV||="production";process.env.NODE_ENV||="production";const port=Number(process.env.PORT_DASHBOARD||process.env.PORT)||3002,defaultsRoot=resolveDefaultsRoot(),frameworkDashboard=join(defaultsRoot,"views/dashboard"),appDashboard=projectPath("resources/views/dashboard"),{authCookieName}=await import("@stacksjs/auth"),authCookie=authCookieName();function readCookie(header,name){if(!header)return;for(const part of header.split(";")){const raw=part.trim(),eq=raw.indexOf("=");if(eq<1)continue;if(raw.slice(0,eq)===name)return decodeURIComponent(raw.slice(eq+1))}return}const router=await import("@stacksjs/router"),routeRegistry=(await import(projectPath("app/Routes.ts"))).default;await router.loadRoutes(routeRegistry);const{Auth}=await import("@stacksjs/auth"),{serve}=await import("bun-plugin-stx/serve");await serve({patterns:[appDashboard,frameworkDashboard].filter((dir)=>existsSync(dir)),port,layoutsDir:frameworkDashboard,partialsDir:frameworkDashboard,componentsDir:join(defaultsRoot,"resources/components/Dashboard"),publicDir:publicPath(),quiet:!0,onRequest:async(req)=>{const url=new URL(req.url);if((await decideDashboardAccess({method:req.method,pathname:url.pathname,token:readCookie(req.headers.get("cookie"),authCookie)},(token)=>Auth.getUserFromToken(token))).allow)return null;const next=encodeURIComponent(url.pathname+url.search);return new Response(null,{status:302,headers:{location:`/login?next=${next}`,"cache-control":"no-store, private"}})}});console.log(`Dashboard server listening on port ${port}`);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/actions",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.18",
5
+ "version": "0.72.19",
6
6
  "description": "The Stacks actions.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -59,35 +59,35 @@
59
59
  "prepublishOnly": "bun run build"
60
60
  },
61
61
  "dependencies": {
62
- "@stacksjs/config": "0.72.18",
62
+ "@stacksjs/config": "0.72.19",
63
63
  "@stacksjs/bumpx": "^0.2.6",
64
64
  "@stacksjs/bunpress": "^0.2.6",
65
65
  "@stacksjs/logsmith": "^0.2.3",
66
- "@stacksjs/registry": "0.72.18",
66
+ "@stacksjs/registry": "0.72.19",
67
67
  "@stacksjs/stx": "^0.2.184",
68
68
  "@stacksjs/ts-cloud": "^0.8.3",
69
69
  "@stacksjs/ts-md": "^0.1.1"
70
70
  },
71
71
  "devDependencies": {
72
- "@stacksjs/api": "0.72.18",
73
- "@stacksjs/cli": "0.72.18",
74
- "@stacksjs/database": "0.72.18",
72
+ "@stacksjs/api": "0.72.19",
73
+ "@stacksjs/cli": "0.72.19",
74
+ "@stacksjs/database": "0.72.19",
75
75
  "@stacksjs/tlsx": "^0.13.2",
76
76
  "better-dx": "^0.2.23",
77
- "@stacksjs/dns": "0.72.18",
78
- "@stacksjs/enums": "0.72.18",
79
- "@stacksjs/env": "0.72.18",
80
- "@stacksjs/error-handling": "0.72.18",
81
- "@stacksjs/image": "0.72.18",
82
- "@stacksjs/logging": "0.72.18",
83
- "@stacksjs/path": "0.72.18",
84
- "@stacksjs/security": "0.72.18",
85
- "@stacksjs/cms": "0.72.18",
86
- "@stacksjs/sites": "0.72.18",
87
- "@stacksjs/storage": "0.72.18",
88
- "@stacksjs/strings": "0.72.18",
89
- "@stacksjs/utils": "0.72.18",
90
- "@stacksjs/validation": "0.72.18"
77
+ "@stacksjs/dns": "0.72.19",
78
+ "@stacksjs/enums": "0.72.19",
79
+ "@stacksjs/env": "0.72.19",
80
+ "@stacksjs/error-handling": "0.72.19",
81
+ "@stacksjs/image": "0.72.19",
82
+ "@stacksjs/logging": "0.72.19",
83
+ "@stacksjs/path": "0.72.19",
84
+ "@stacksjs/security": "0.72.19",
85
+ "@stacksjs/cms": "0.72.19",
86
+ "@stacksjs/sites": "0.72.19",
87
+ "@stacksjs/storage": "0.72.19",
88
+ "@stacksjs/strings": "0.72.19",
89
+ "@stacksjs/utils": "0.72.19",
90
+ "@stacksjs/validation": "0.72.19"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "pickier": "^0.1.35",