@commonpub/layer 0.18.2 → 0.18.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.
@@ -25,7 +25,12 @@ interface AuthResponse {
25
25
  session: ClientAuthSession | null;
26
26
  }
27
27
 
28
- /** Type-safe POST fetch that avoids Nuxt $fetch TS2589 deep instantiation */
28
+ // `$fetch<T>(url, options)` instantiates Nuxt's NitroFetchRequest generic
29
+ // with an excessively deep type graph, which fails TS2589 on this
30
+ // specific call shape. The cast below narrows `$fetch` to a concrete
31
+ // signature the compiler can check without recursing. Verified session
32
+ // 133: removing the cast immediately reintroduces the error. Cleanup is
33
+ // upstream — wait for Nuxt to simplify the $fetch type.
29
34
  async function authPost(url: string, body: Record<string, unknown>): Promise<AuthResponse | null> {
30
35
  return ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(url, {
31
36
  method: 'POST',
@@ -35,6 +40,12 @@ async function authPost(url: string, body: Record<string, unknown>): Promise<Aut
35
40
  });
36
41
  }
37
42
 
43
+ async function authGet(url: string): Promise<AuthResponse | null> {
44
+ return ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(url, {
45
+ credentials: 'include',
46
+ });
47
+ }
48
+
38
49
  export function useAuth() {
39
50
  const user = useState<ClientAuthUser | null>('auth-user', () => null);
40
51
  const session = useState<ClientAuthSession | null>('auth-session', () => null);
@@ -68,9 +79,7 @@ export function useAuth() {
68
79
  async function refreshSession(): Promise<void> {
69
80
  if (import.meta.server) return;
70
81
  try {
71
- const data = await ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(
72
- '/api/me', { credentials: 'include' },
73
- );
82
+ const data = await authGet('/api/me');
74
83
  user.value = data?.user ?? null;
75
84
  session.value = data?.session ?? null;
76
85
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -54,10 +54,10 @@
54
54
  "vue-router": "^4.3.0",
55
55
  "zod": "^4.3.6",
56
56
  "@commonpub/auth": "0.5.1",
57
- "@commonpub/docs": "0.6.2",
58
57
  "@commonpub/config": "0.11.0",
59
- "@commonpub/learning": "0.5.2",
58
+ "@commonpub/docs": "0.6.2",
60
59
  "@commonpub/editor": "0.7.9",
60
+ "@commonpub/learning": "0.5.2",
61
61
  "@commonpub/ui": "0.8.5",
62
62
  "@commonpub/protocol": "0.9.9"
63
63
  },
@@ -357,4 +357,47 @@ const activeDifficultyFilter = ref('');
357
357
  .cpub-empty-icon { font-size: 32px; color: var(--text-faint); margin-bottom: 12px; }
358
358
  .cpub-empty-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
359
359
  .cpub-empty-sub { font-size: 12px; color: var(--text-dim); }
360
+
361
+ /* MOBILE (≤ 768px) — stack sidebar, collapse multi-column grids,
362
+ shrink outer padding so content gets the full viewport. */
363
+ @media (max-width: 768px) {
364
+ .cpub-learn-hero { padding: 24px 16px 18px; }
365
+ .cpub-hero-title { font-size: 22px; }
366
+ .cpub-hero-sub { font-size: 13px; margin-bottom: 18px; }
367
+ .cpub-hero-stats { gap: 16px; margin-top: 18px; padding-top: 16px; }
368
+
369
+ .cpub-shell { flex-direction: column; min-height: auto; }
370
+ .cpub-page { padding: 20px 16px; }
371
+ .cpub-sidebar {
372
+ width: 100%;
373
+ border-left: none;
374
+ border-top: var(--border-width-default) solid var(--border);
375
+ padding: 16px 0;
376
+ }
377
+
378
+ /* In-progress cards: already scroll horizontally on mobile via cpub-ip-row */
379
+
380
+ /* Path cards: stack vertically so description + aside don't crush */
381
+ .cpub-path-card { flex-direction: column; gap: 14px; padding: 16px; }
382
+ .cpub-path-aside {
383
+ flex-direction: row;
384
+ align-items: center;
385
+ justify-content: flex-start;
386
+ width: 100%;
387
+ }
388
+
389
+ /* My-path rows: put status/meta under title instead of side-by-side */
390
+ .cpub-my-path-row {
391
+ flex-direction: column;
392
+ align-items: flex-start;
393
+ gap: 6px;
394
+ padding: 12px;
395
+ }
396
+ .cpub-my-path-meta { gap: 10px; flex-wrap: wrap; }
397
+
398
+ /* Course + explainer grids (not currently rendered on this page but
399
+ safe to include in case the template adds them back) */
400
+ .cpub-course-grid { grid-template-columns: 1fr; }
401
+ .cpub-explainer-grid { grid-template-columns: 1fr; }
402
+ }
360
403
  </style>
@@ -227,4 +227,18 @@ const authorInitial = computed(() => {
227
227
  .cpub-link:hover {
228
228
  text-decoration: underline;
229
229
  }
230
+
231
+ /* MOBILE (≤ 768px) — let meta items wrap instead of overflowing, shrink
232
+ title + info padding so content gets the full viewport width. */
233
+ @media (max-width: 768px) {
234
+ .cpub-video-player { margin-bottom: 16px; }
235
+ .cpub-video-info { padding: 16px; }
236
+ .cpub-video-title { font-size: 18px; }
237
+ .cpub-video-meta {
238
+ flex-wrap: wrap;
239
+ gap: 10px;
240
+ row-gap: 6px;
241
+ }
242
+ .cpub-video-desc { font-size: 13px; margin-bottom: 16px; }
243
+ }
230
244
  </style>
@@ -324,4 +324,23 @@ function formatDate(dateStr: string): string {
324
324
  .cpub-empty-icon { font-size: 32px; color: var(--text-faint); margin-bottom: 12px; }
325
325
  .cpub-empty-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
326
326
  .cpub-empty-sub { font-size: 12px; color: var(--text-dim); }
327
+
328
+ /* MOBILE (≤ 768px) — collapse 2-track grid to single column, stack
329
+ sidebar under main content, shrink outer padding, and single-col
330
+ the thumbnail grid so cards get full viewport width. */
331
+ @media (max-width: 768px) {
332
+ .cpub-video-hero { padding: 24px 16px 18px; }
333
+ .cpub-hero-row { flex-wrap: wrap; gap: 10px; }
334
+ .cpub-hero-title { font-size: 22px; }
335
+ .cpub-hero-sub { font-size: 12px; margin-bottom: 14px; }
336
+
337
+ .cpub-filter-bar { padding: 0 16px; }
338
+
339
+ .cpub-page-wrap { padding: 20px 16px; }
340
+ .cpub-main-grid { grid-template-columns: 1fr; gap: 20px; }
341
+
342
+ .cpub-video-grid { grid-template-columns: 1fr; gap: 14px; }
343
+
344
+ .cpub-featured-title { font-size: 15px; }
345
+ }
327
346
  </style>
@@ -1,6 +1,36 @@
1
1
  // Security middleware — rate limiting + security headers + CSP
2
2
  import { checkRateLimit, createRateLimitStore, createRedisFailOpenLogger, shouldSkipRateLimit, getSecurityHeaders, buildCspHeader, buildCspDirectives } from '@commonpub/server';
3
3
 
4
+ // Structured JSON sink for fail-open events. Emits one JSON line per event
5
+ // to stdout so Docker logs / Loki / Datadog / CloudWatch can parse without
6
+ // regex-scraping. Duplicated from packages/infra/structuredLogger.ts
7
+ // because layers/base doesn't depend on @commonpub/infra directly and the
8
+ // symbol isn't re-exported via @commonpub/server (which pins to the npm
9
+ // registry, not the workspace, in apps/reference). Keep this helper in
10
+ // sync with the one in infra if the event shape changes.
11
+ function jsonLog(component: string) {
12
+ return (message: string, meta?: Record<string, unknown>) => {
13
+ try {
14
+ const event: Record<string, unknown> = {
15
+ ts: new Date().toISOString(),
16
+ level: 'warn',
17
+ component,
18
+ message,
19
+ };
20
+ if (meta) {
21
+ for (const [k, v] of Object.entries(meta)) {
22
+ if (k === 'ts' || k === 'level' || k === 'component' || k === 'message') continue;
23
+ event[k] = v;
24
+ }
25
+ }
26
+ process.stdout.write(JSON.stringify(event) + '\n');
27
+ } catch {
28
+ // Circular meta; fall through to plain console so the event isn't lost.
29
+ console.warn(`[${component}] ${message}`, meta);
30
+ }
31
+ };
32
+ }
33
+
4
34
  // Selects a Redis-backed store when NUXT_REDIS_URL is set, otherwise the
5
35
  // in-process memory store. Unset env = byte-identical behavior to pre-0.6.
6
36
  // `onRedisError` is rate-limited: first event logs immediately, subsequent
@@ -8,7 +38,10 @@ import { checkRateLimit, createRateLimitStore, createRedisFailOpenLogger, should
8
38
  // flood the log at real traffic.
9
39
  const store = createRateLimitStore({
10
40
  redisUrl: process.env.NUXT_REDIS_URL,
11
- onRedisError: createRedisFailOpenLogger({ scope: 'ratelimit:ip' }),
41
+ onRedisError: createRedisFailOpenLogger({
42
+ scope: 'ratelimit:ip',
43
+ sink: jsonLog('ratelimit-ip'),
44
+ }),
12
45
  });
13
46
  const isDev = process.env.NODE_ENV !== 'production';
14
47