glashjs 0.12.1 → 0.13.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/src/auth.mjs ADDED
@@ -0,0 +1,96 @@
1
+ // glashAuth helpers for glashjs
2
+ // ---------------------------------------------------------------------------
3
+ // Thin client/server wrapper for the GlashDB auth API:
4
+ // /api/auth/v1/:projectId/signup
5
+ // /api/auth/v1/:projectId/signin
6
+ // /api/auth/v1/:projectId/me
7
+
8
+ export function glashAuth(options = {}) {
9
+ const apiUrl = trimSlash(options.apiUrl ?? process.env.GLASHDB_API_URL ?? 'https://api.glashdb.com/api');
10
+ const projectId = options.projectId
11
+ ?? process.env.GLASHDB_PROJECT_ID
12
+ ?? process.env.GLASHAUTH_PROJECT_ID
13
+ ?? process.env.NEXT_PUBLIC_GLASHDB_PROJECT_ID;
14
+ const anonKey = options.anonKey
15
+ ?? process.env.GLASHDB_ANON_KEY
16
+ ?? process.env.GLASHAUTH_ANON_KEY
17
+ ?? process.env.NEXT_PUBLIC_GLASHAUTH_ANON_KEY;
18
+
19
+ if (!projectId) throw new Error('GLASHDB_PROJECT_ID is required for glashAuth');
20
+ if (!anonKey) throw new Error('GLASHDB_ANON_KEY is required for glashAuth');
21
+
22
+ async function request(path, { body, accessToken, method = 'POST', headers = {} } = {}) {
23
+ const res = await fetch(`${apiUrl}/auth/v1/${encodeURIComponent(projectId)}/${path.replace(/^\/+/, '')}`, {
24
+ method,
25
+ headers: {
26
+ apikey: anonKey,
27
+ 'content-type': 'application/json',
28
+ ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
29
+ ...headers,
30
+ },
31
+ body: body === undefined ? undefined : JSON.stringify(body),
32
+ });
33
+ const data = await readJson(res);
34
+ if (!res.ok) {
35
+ const message = data?.message || data?.error || `glashAuth ${path} failed with HTTP ${res.status}`;
36
+ throw new Error(message);
37
+ }
38
+ return data;
39
+ }
40
+
41
+ return {
42
+ projectId,
43
+ apiUrl,
44
+ signup: ({ email, password, fullName }) => request('signup', { body: { email, password, fullName } }),
45
+ signin: ({ email, password }) => request('signin', { body: { email, password } }),
46
+ signout: (refreshToken) => request('signout', { body: { refreshToken } }),
47
+ refresh: (refreshToken) => request('refresh', { body: { refreshToken } }),
48
+ magicLink: ({ email, redirectTo }) => request('magic-link/request', { body: { email, redirectTo } }),
49
+ me: (accessToken) => request('me', { method: 'GET', accessToken }),
50
+ googleAuthorizeUrl: (redirectTo) => {
51
+ const url = new URL(`${apiUrl}/auth/v1/${encodeURIComponent(projectId)}/google/authorize`);
52
+ if (redirectTo) url.searchParams.set('redirect_to', redirectTo);
53
+ return url.toString();
54
+ },
55
+ };
56
+ }
57
+
58
+ export function readSessionCookie(ctx, name = 'glash_session') {
59
+ const cookie = ctx?.headers?.cookie || ctx?.req?.headers?.cookie || '';
60
+ const found = cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));
61
+ if (!found) return null;
62
+ try {
63
+ return JSON.parse(Buffer.from(decodeURIComponent(found.slice(name.length + 1)), 'base64url').toString('utf8'));
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ export function sessionCookie(session, options = {}) {
70
+ const name = options.name ?? 'glash_session';
71
+ const maxAge = options.maxAge ?? 60 * 60 * 24 * 30;
72
+ const encoded = Buffer.from(JSON.stringify(session), 'utf8').toString('base64url');
73
+ const parts = [
74
+ `${name}=${encodeURIComponent(encoded)}`,
75
+ 'Path=/',
76
+ `Max-Age=${maxAge}`,
77
+ 'HttpOnly',
78
+ 'SameSite=Lax',
79
+ ];
80
+ if (options.secure ?? process.env.NODE_ENV === 'production') parts.push('Secure');
81
+ return parts.join('; ');
82
+ }
83
+
84
+ export function clearSessionCookie(name = 'glash_session') {
85
+ return `${name}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`;
86
+ }
87
+
88
+ async function readJson(res) {
89
+ const text = await res.text();
90
+ if (!text) return null;
91
+ try { return JSON.parse(text); } catch { return { message: text }; }
92
+ }
93
+
94
+ function trimSlash(value) {
95
+ return String(value || '').replace(/\/+$/, '');
96
+ }
package/src/build.mjs CHANGED
@@ -13,6 +13,7 @@ import { securityHeaders } from './security/headers.mjs';
13
13
  import { discoverRoutes } from './server/router.mjs';
14
14
  import { isComponentRoute, findLayouts, loadComponentRoute, clientBundle, routeId } from './server/jsx.mjs';
15
15
  import { loadConfig } from './config.mjs';
16
+ import { loadEnvFiles } from './env.mjs';
16
17
 
17
18
  // Precompile JSX routes: server modules (-> .glash/server) + minified client
18
19
  // hydration bundles (-> outDir/_glash/<id>.js). Production `glash serve` then
@@ -56,6 +57,7 @@ function pwaManifest(cfg, version) {
56
57
  }
57
58
 
58
59
  export async function build({ root = process.cwd(), log = console.log } = {}) {
60
+ loadEnvFiles(root, { mode: process.env.NODE_ENV || 'production' });
59
61
  const cfg = await loadConfig(root);
60
62
  const publicDir = path.resolve(root, cfg.publicDir);
61
63
  const outDir = path.resolve(root, cfg.outDir);
@@ -77,6 +79,9 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
77
79
  manifest.generatedAt = new Date().toISOString();
78
80
 
79
81
  await fs.mkdir(outDir, { recursive: true });
82
+ if (existsSync(publicDir)) {
83
+ await fs.cp(publicDir, outDir, { recursive: true, force: true });
84
+ }
80
85
 
81
86
  // Precompile JSX routes (server modules + client bundles) for production.
82
87
  let routesBuilt = { compiled: 0 };
package/src/config.mjs CHANGED
@@ -19,6 +19,8 @@ export const DEFAULT_CONFIG = {
19
19
  publicDir: 'public',
20
20
  // File-based routes (pages + api/) served by `glash dev` / `glash serve`.
21
21
  routesDir: 'routes',
22
+ // Global stylesheets injected into every rendered document.
23
+ stylesheets: [],
22
24
  port: 3000,
23
25
  outDir: '.glash/out',
24
26
  themeColor: '#0b0d12',