@autumnsgrove/groveengine 0.4.0 → 0.4.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.
@@ -40,3 +40,21 @@ export function parseSessionCookie(cookieHeader: string): string | null;
40
40
  * @returns {boolean} - Whether the user is allowed
41
41
  */
42
42
  export function isAllowedAdmin(email: string, allowedList: string): boolean;
43
+ /**
44
+ * Verify that a user owns/has access to a tenant
45
+ * @param {Object} db - D1 database instance
46
+ * @param {string} tenantId - Tenant ID to check
47
+ * @param {string} userEmail - User's email address
48
+ * @returns {Promise<boolean>} - Whether the user owns the tenant
49
+ */
50
+ export function verifyTenantOwnership(db: Object, tenantId: string, userEmail: string): Promise<boolean>;
51
+ /**
52
+ * Get tenant ID with ownership verification
53
+ * Throws 403 if user doesn't own the tenant
54
+ * @param {Object} db - D1 database instance
55
+ * @param {string} tenantId - Tenant ID from request
56
+ * @param {Object} user - User object with email
57
+ * @returns {Promise<string>} - Verified tenant ID
58
+ * @throws {Error} - If unauthorized
59
+ */
60
+ export function getVerifiedTenantId(db: Object, tenantId: string, user: Object): Promise<string>;
@@ -54,7 +54,7 @@ export function createSessionCookie(token, isProduction = true) {
54
54
  "Path=/",
55
55
  `Max-Age=${SESSION_DURATION_SECONDS}`,
56
56
  "HttpOnly",
57
- "SameSite=Lax",
57
+ "SameSite=Strict",
58
58
  ];
59
59
 
60
60
  if (isProduction) {
@@ -69,7 +69,7 @@ export function createSessionCookie(token, isProduction = true) {
69
69
  * @returns {string} - Cookie header value
70
70
  */
71
71
  export function clearSessionCookie() {
72
- return `${SESSION_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`;
72
+ return `${SESSION_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict`;
73
73
  }
74
74
 
75
75
  /**
@@ -103,3 +103,65 @@ export function isAllowedAdmin(email, allowedList) {
103
103
  const allowed = allowedList.split(",").map((e) => e.trim().toLowerCase());
104
104
  return allowed.includes(email.toLowerCase());
105
105
  }
106
+
107
+ /**
108
+ * Verify that a user owns/has access to a tenant
109
+ * @param {Object} db - D1 database instance
110
+ * @param {string} tenantId - Tenant ID to check
111
+ * @param {string} userEmail - User's email address
112
+ * @returns {Promise<boolean>} - Whether the user owns the tenant
113
+ */
114
+ export async function verifyTenantOwnership(db, tenantId, userEmail) {
115
+ if (!tenantId || !userEmail) {
116
+ return false;
117
+ }
118
+
119
+ try {
120
+ const tenant = await db
121
+ .prepare("SELECT email FROM tenants WHERE id = ?")
122
+ .bind(tenantId)
123
+ .first();
124
+
125
+ if (!tenant) {
126
+ return false;
127
+ }
128
+
129
+ // Check if user email matches tenant owner email
130
+ return tenant.email.toLowerCase() === userEmail.toLowerCase();
131
+ } catch (error) {
132
+ console.error("Error verifying tenant ownership:", error);
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get tenant ID with ownership verification
139
+ * Throws 403 if user doesn't own the tenant
140
+ * @param {Object} db - D1 database instance
141
+ * @param {string} tenantId - Tenant ID from request
142
+ * @param {Object} user - User object with email
143
+ * @returns {Promise<string>} - Verified tenant ID
144
+ * @throws {Error} - If unauthorized
145
+ */
146
+ export async function getVerifiedTenantId(db, tenantId, user) {
147
+ if (!tenantId) {
148
+ const err = new Error("Tenant ID required");
149
+ err.status = 400;
150
+ throw err;
151
+ }
152
+
153
+ if (!user?.email) {
154
+ const err = new Error("Unauthorized");
155
+ err.status = 401;
156
+ throw err;
157
+ }
158
+
159
+ const isOwner = await verifyTenantOwnership(db, tenantId, user.email);
160
+ if (!isOwner) {
161
+ const err = new Error("Access denied - you do not own this tenant");
162
+ err.status = 403;
163
+ throw err;
164
+ }
165
+
166
+ return tenantId;
167
+ }
@@ -76,13 +76,16 @@
76
76
 
77
77
  /**
78
78
  * Calculate positions based on anchor locations, with collision detection
79
+ * Uses getBoundingClientRect() for accurate positioning regardless of offset parent chains
79
80
  */
80
81
  async function updatePositions() {
81
82
  if (!gutterElement || !contentBodyElement) return;
82
83
 
83
84
  await tick(); // Wait for DOM to update
84
85
 
85
- const gutterTop = gutterElement.offsetTop;
86
+ // Use getBoundingClientRect for accurate relative positioning
87
+ // This works regardless of offset parent chains and CSS transforms
88
+ const gutterRect = gutterElement.getBoundingClientRect();
86
89
 
87
90
  let lastBottom = 0; // Track the bottom edge of the last positioned item
88
91
  const newOverflowingAnchors = [];
@@ -94,20 +97,24 @@
94
97
  if (!el && import.meta.env.DEV) {
95
98
  console.warn(`Anchor element not found for: ${anchor}`);
96
99
  }
100
+ // Use getBoundingClientRect for consistent positioning
101
+ const elRect = el ? el.getBoundingClientRect() : null;
97
102
  return {
98
103
  anchor,
99
104
  key: getKey(anchor),
100
105
  element: el,
101
- top: el ? el.offsetTop : Infinity
106
+ elementRect: elRect,
107
+ top: elRect ? elRect.top : Infinity
102
108
  };
103
109
  }).sort((a, b) => a.top - b.top);
104
110
 
105
- anchorPositions.forEach(({ anchor, key, element }) => {
111
+ anchorPositions.forEach(({ anchor, key, element, elementRect }) => {
106
112
  const groupEl = anchorGroupElements[key];
107
113
 
108
- if (element && groupEl) {
109
- // Desired position (aligned with anchor element)
110
- let desiredTop = element.offsetTop - gutterTop;
114
+ if (element && elementRect && groupEl) {
115
+ // Calculate position relative to the gutter element's top
116
+ // This accounts for any content above the content-body (headers, etc.)
117
+ let desiredTop = elementRect.top - gutterRect.top;
111
118
 
112
119
  // Get the height of this gutter group
113
120
  const groupHeight = groupEl.offsetHeight;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autumnsgrove/groveengine",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Multi-tenant blog engine for Grove Platform. Features gutter annotations, markdown editing, magic code auth, and Cloudflare Workers deployment.",
5
5
  "author": "AutumnsGrove",
6
6
  "license": "MIT",
@@ -129,25 +129,6 @@
129
129
  "dist",
130
130
  "!dist/**/*.test.*"
131
131
  ],
132
- "scripts": {
133
- "dev": "vite dev",
134
- "dev:wrangler": "wrangler pages dev -- vite dev",
135
- "build": "vite build",
136
- "build:package": "svelte-kit sync && svelte-package -o dist",
137
- "package": "svelte-kit sync && svelte-package -o dist",
138
- "prepublishOnly": "npm run package",
139
- "preview": "vite preview",
140
- "audit": "npm audit --audit-level=moderate",
141
- "audit:fix": "npm audit fix",
142
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
143
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
144
- "test": "vitest",
145
- "test:ui": "vitest --ui",
146
- "test:run": "vitest run",
147
- "test:security": "vitest run tests/security",
148
- "test:coverage": "vitest run --coverage",
149
- "test:watch": "vitest watch"
150
- },
151
132
  "peerDependencies": {
152
133
  "@sveltejs/kit": "^2.0.0",
153
134
  "svelte": "^5.0.0",
@@ -187,5 +168,23 @@
187
168
  "sonner": "^2.0.7",
188
169
  "svelte-sonner": "^1.0.7",
189
170
  "tailwind-merge": "^3.4.0"
171
+ },
172
+ "scripts": {
173
+ "dev": "vite dev",
174
+ "dev:wrangler": "wrangler pages dev -- vite dev",
175
+ "build": "vite build",
176
+ "build:package": "svelte-kit sync && svelte-package -o dist",
177
+ "package": "svelte-kit sync && svelte-package -o dist",
178
+ "preview": "vite preview",
179
+ "audit": "pnpm audit --audit-level=moderate",
180
+ "audit:fix": "pnpm audit --fix",
181
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
182
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
183
+ "test": "vitest",
184
+ "test:ui": "vitest --ui",
185
+ "test:run": "vitest run",
186
+ "test:security": "vitest run tests/security",
187
+ "test:coverage": "vitest run --coverage",
188
+ "test:watch": "vitest watch"
190
189
  }
191
- }
190
+ }