@commonpub/layer 0.21.2 → 0.21.3

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.
@@ -44,7 +44,13 @@ const hasChildren = computed(() => children.value.length > 0);
44
44
  <span v-if="time" class="cpub-step-time"><i class="fa-regular fa-clock"></i> {{ time }}</span>
45
45
  </div>
46
46
  <div v-if="hasChildren" class="cpub-step-body">
47
- <BlockContentRenderer :blocks="children" />
47
+ <!-- Nuxt pathPrefix auto-imports components/blocks/BlockContentRenderer.vue
48
+ under the name BlocksBlockContentRenderer (the Blocks dir prefix only
49
+ de-dups when the filename starts with it). The bare tag would not
50
+ resolve and would silently blank nested step content. A static import
51
+ is not an option here: BlockContentRenderer and BlockBuildStepView are
52
+ mutually recursive (ESM cycle). Use the auto-import name. -->
53
+ <BlocksBlockContentRenderer :blocks="children" />
48
54
  </div>
49
55
  </div>
50
56
  </template>
@@ -509,7 +509,10 @@ async function handleBuild(): Promise<void> {
509
509
  <span v-if="step.time" class="cpub-build-step-time"><i class="fa-regular fa-clock"></i> {{ step.time }}</span>
510
510
  </div>
511
511
  <div v-if="step.children.length > 0" class="cpub-build-step-body">
512
- <BlockContentRenderer :blocks="step.children" />
512
+ <!-- Auto-import name (same as the body renderer above); the
513
+ bare BlockContentRenderer tag does not resolve under
514
+ Nuxt pathPrefix and silently blanks nested content. -->
515
+ <BlocksBlockContentRenderer :blocks="step.children" />
513
516
  </div>
514
517
  </div>
515
518
  </div>
@@ -1,5 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import type { NavItem } from '@commonpub/server';
3
+ // Explicit import: Nuxt's pathPrefix auto-import names this component
4
+ // `<NavMobileNavRenderer>` (the `nav/` dir prefix only de-duplicates when
5
+ // the filename starts with `Nav`, which `MobileNavRenderer` does not).
6
+ // Referencing `<MobileNavRenderer>` below would otherwise silently fail to
7
+ // resolve, leaving the mobile hamburger menu empty.
8
+ import MobileNavRenderer from '../components/nav/MobileNavRenderer.vue';
3
9
 
4
10
  const { user, isAuthenticated, isAdmin, signOut, refreshSession } = useAuth();
5
11
  const { count: unreadCount, connect: connectNotifications, disconnect: disconnectNotifications } = useNotifications();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.21.2",
3
+ "version": "0.21.3",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -50,16 +50,16 @@
50
50
  "vue": "^3.4.0",
51
51
  "vue-router": "^4.3.0",
52
52
  "zod": "^4.3.6",
53
- "@commonpub/config": "0.12.0",
53
+ "@commonpub/auth": "0.6.0",
54
+ "@commonpub/docs": "0.6.3",
55
+ "@commonpub/learning": "0.5.2",
54
56
  "@commonpub/editor": "0.7.9",
55
57
  "@commonpub/explainer": "0.7.12",
56
- "@commonpub/learning": "0.5.2",
57
- "@commonpub/server": "2.51.0",
58
- "@commonpub/auth": "0.6.0",
59
- "@commonpub/ui": "0.8.5",
60
- "@commonpub/protocol": "0.9.9",
61
58
  "@commonpub/schema": "0.16.0",
62
- "@commonpub/docs": "0.6.2"
59
+ "@commonpub/ui": "0.8.5",
60
+ "@commonpub/protocol": "0.9.10",
61
+ "@commonpub/server": "2.53.0",
62
+ "@commonpub/config": "0.12.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@testing-library/jest-dom": "^6.9.1",
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { createStorageFromEnv, generateStorageKey, validateUpload, ALLOWED_IMAGE_TYPES } from '@commonpub/server';
2
+ import { createStorageFromEnv, generateStorageKey, ALLOWED_IMAGE_TYPES, safeFetchBinary } from '@commonpub/server';
3
3
 
4
4
  const schema = z.object({
5
5
  url: z.string().url(),
@@ -7,62 +7,36 @@ const schema = z.object({
7
7
  });
8
8
 
9
9
  export default defineEventHandler(async (event) => {
10
- const user = requireAuth(event);
10
+ requireAuth(event);
11
11
  const { url, purpose } = await parseBody(event, schema);
12
12
 
13
- // SSRF protection block private/internal IPs
14
- const parsed = new URL(url);
15
- const hostname = parsed.hostname.toLowerCase();
16
- const h = hostname.replace(/^\[|\]$/g, '');
17
- if (
18
- h === 'localhost' ||
19
- h === 'localhost.localdomain' ||
20
- h === 'metadata.google.internal' ||
21
- h.endsWith('.local') ||
22
- /^127\./.test(h) ||
23
- /^10\./.test(h) ||
24
- /^172\.(1[6-9]|2\d|3[01])\./.test(h) ||
25
- /^192\.168\./.test(h) ||
26
- /^169\.254\./.test(h) ||
27
- /^0\./.test(h) ||
28
- h === '::1' ||
29
- /^f[cd]/i.test(h) ||
30
- /^fe80/i.test(h)
31
- ) {
32
- throw createError({ statusCode: 400, statusMessage: 'Cannot fetch from private/local addresses' });
33
- }
34
-
35
- // Download the remote image
36
- const controller = new AbortController();
37
- const timeout = setTimeout(() => controller.abort(), 10_000); // 10s timeout
38
-
39
- let response: Response;
13
+ // SSRF-safe fetch: blocks private/reserved/numeric hosts and non-HTTP(S)
14
+ // schemes, re-validates every redirect hop, and enforces a 10 MB streaming
15
+ // cap + deadline. Replaces a hand-rolled denylist that missed redirect
16
+ // re-validation, IPv4-mapped IPv6, and numeric-IP encodings.
17
+ let buffer: Buffer;
18
+ let contentType: string;
40
19
  try {
41
- response = await fetch(url, {
42
- signal: controller.signal,
43
- headers: { 'User-Agent': 'devEco.io Image Fetcher' },
44
- });
20
+ ({ buffer, contentType } = await safeFetchBinary(url, {
21
+ accept: 'image/*',
22
+ userAgent: 'CommonPub/1.0 (image-upload)',
23
+ timeoutMs: 10_000,
24
+ }));
45
25
  } catch (err) {
26
+ const msg = err instanceof Error ? err.message : '';
27
+ if (msg.includes('private or reserved')) {
28
+ throw createError({ statusCode: 400, statusMessage: 'Cannot fetch from private/local addresses' });
29
+ }
30
+ if (msg === 'Response too large') {
31
+ throw createError({ statusCode: 400, statusMessage: 'Image too large (max 10MB)' });
32
+ }
46
33
  throw createError({ statusCode: 400, statusMessage: 'Failed to fetch remote image' });
47
- } finally {
48
- clearTimeout(timeout);
49
34
  }
50
35
 
51
- if (!response.ok) {
52
- throw createError({ statusCode: 400, statusMessage: `Remote server returned ${response.status}` });
53
- }
54
-
55
- const contentType = response.headers.get('content-type') || '';
56
36
  if (![...ALLOWED_IMAGE_TYPES].some((t: string) => contentType.startsWith(t))) {
57
37
  throw createError({ statusCode: 400, statusMessage: `Unsupported image type: ${contentType}` });
58
38
  }
59
39
 
60
- const buffer = Buffer.from(await response.arrayBuffer());
61
- const maxSize = 10 * 1024 * 1024; // 10MB
62
- if (buffer.length > maxSize) {
63
- throw createError({ statusCode: 400, statusMessage: 'Image too large (max 10MB)' });
64
- }
65
-
66
40
  // Upload to storage
67
41
  const storage = createStorageFromEnv();
68
42
  const ext = contentType.split('/')[1] || 'jpg';
@@ -113,6 +113,7 @@ export default defineEventHandler(async (event) => {
113
113
  // Handle auth API routes — skip custom routes that Nitro handles directly
114
114
  const isCustomAuthRoute = pathname.startsWith('/api/auth/federated/')
115
115
  || pathname.startsWith('/api/auth/oauth2/')
116
+ || pathname.startsWith('/api/auth/mastodon/')
116
117
  || pathname === '/api/auth/sign-in-username'
117
118
  || pathname === '/api/auth/delete-user'
118
119
  || pathname === '/api/auth/export-data';