@commonpub/layer 0.5.5 → 0.5.6

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/nuxt.config.ts CHANGED
@@ -47,7 +47,11 @@ export default defineNuxtConfig({
47
47
  uiTheme('layouts.css'),
48
48
  uiTheme('forms.css'),
49
49
  uiTheme('editor-panels.css'),
50
- '@commonpub/explainer/vue/theme/explainer-themes.css',
50
+ // Explainer theme presets only (NOT base.css — it overrides site design system vars)
51
+ '@commonpub/explainer/vue/theme/dark-industrial.css',
52
+ '@commonpub/explainer/vue/theme/punk-zine.css',
53
+ '@commonpub/explainer/vue/theme/paper-teal.css',
54
+ '@commonpub/explainer/vue/theme/clean-light.css',
51
55
  ],
52
56
  runtimeConfig: {
53
57
  databaseUrl: '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
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.8.0",
54
- "@commonpub/editor": "0.5.0",
55
53
  "@commonpub/explainer": "0.7.0",
56
54
  "@commonpub/docs": "0.6.0",
55
+ "@commonpub/config": "0.8.0",
57
56
  "@commonpub/auth": "0.5.0",
58
- "@commonpub/learning": "0.5.0",
57
+ "@commonpub/editor": "0.5.0",
59
58
  "@commonpub/protocol": "0.9.6",
60
- "@commonpub/server": "2.25.0",
59
+ "@commonpub/schema": "0.8.17",
60
+ "@commonpub/learning": "0.5.0",
61
61
  "@commonpub/ui": "0.8.4",
62
- "@commonpub/schema": "0.8.17"
62
+ "@commonpub/server": "2.25.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@testing-library/jest-dom": "^6.9.1",
@@ -778,7 +778,7 @@ async function handleUrlImport(result: ImportedContent): Promise<void> {
778
778
  inset: 0;
779
779
  z-index: 9999;
780
780
  background: var(--bg);
781
- overflow: hidden;
781
+ overflow-y: auto;
782
782
  }
783
783
 
784
784
  .cpub-preview-close-btn {
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * /authorize_interaction — Standard ActivityPub remote interaction endpoint.
4
+ * When a user on another instance wants to follow/interact with content on this instance,
5
+ * their instance redirects them here with ?uri=<actor-or-object-uri>.
6
+ *
7
+ * If logged in: show the resource and offer to follow/interact.
8
+ * If not logged in: redirect to login first, then back here.
9
+ */
10
+ const route = useRoute();
11
+ const uri = computed(() => (route.query.uri as string) || '');
12
+
13
+ const { isAuthenticated } = useAuth();
14
+ const toast = useToast();
15
+
16
+ const loading = ref(false);
17
+ const resolved = ref<{ type: string; name: string; url: string } | null>(null);
18
+ const error = ref('');
19
+ const actionDone = ref(false);
20
+
21
+ onMounted(async () => {
22
+ if (!uri.value) {
23
+ error.value = 'No URI provided.';
24
+ return;
25
+ }
26
+
27
+ if (!isAuthenticated.value) {
28
+ // Redirect to login, then back here
29
+ navigateTo(`/auth/login?redirect=${encodeURIComponent(route.fullPath)}`);
30
+ return;
31
+ }
32
+
33
+ // Try to resolve what the URI points to
34
+ loading.value = true;
35
+ try {
36
+ // Check if it's a hub actor URI
37
+ const result = await $fetch<{ type: string; name: string; url: string }>('/api/federation/resolve-uri', {
38
+ method: 'POST',
39
+ body: { uri: uri.value },
40
+ }).catch(() => null);
41
+
42
+ if (result) {
43
+ resolved.value = result;
44
+ } else {
45
+ // Fallback: just show the URI and offer to open it
46
+ resolved.value = { type: 'unknown', name: uri.value, url: uri.value };
47
+ }
48
+ } catch {
49
+ error.value = 'Could not resolve the remote resource.';
50
+ } finally {
51
+ loading.value = false;
52
+ }
53
+ });
54
+
55
+ async function handleFollow(): Promise<void> {
56
+ if (!uri.value) return;
57
+ loading.value = true;
58
+ try {
59
+ // Try to follow as a remote actor (user or hub)
60
+ await $fetch('/api/federation/remote-follow', {
61
+ method: 'POST',
62
+ body: { uri: uri.value },
63
+ });
64
+ actionDone.value = true;
65
+ toast.success('Follow request sent');
66
+ } catch {
67
+ toast.error('Failed to send follow request');
68
+ } finally {
69
+ loading.value = false;
70
+ }
71
+ }
72
+
73
+ useSeoMeta({
74
+ title: 'Authorize Interaction',
75
+ robots: 'noindex',
76
+ });
77
+ </script>
78
+
79
+ <template>
80
+ <div class="cpub-authorize-page">
81
+ <div class="cpub-authorize-card">
82
+ <h1 class="cpub-authorize-title">Authorize Interaction</h1>
83
+
84
+ <div v-if="loading" class="cpub-authorize-loading">
85
+ <i class="fa-solid fa-spinner fa-spin"></i> Resolving...
86
+ </div>
87
+
88
+ <div v-else-if="error" class="cpub-authorize-error">
89
+ <i class="fa-solid fa-triangle-exclamation"></i> {{ error }}
90
+ </div>
91
+
92
+ <div v-else-if="resolved" class="cpub-authorize-content">
93
+ <p class="cpub-authorize-desc">
94
+ You are about to interact with a remote resource:
95
+ </p>
96
+ <div class="cpub-authorize-resource">
97
+ <strong>{{ resolved.name }}</strong>
98
+ <span v-if="resolved.type !== 'unknown'" class="cpub-authorize-type">{{ resolved.type }}</span>
99
+ </div>
100
+ <code class="cpub-authorize-uri">{{ uri }}</code>
101
+
102
+ <div v-if="actionDone" class="cpub-authorize-success">
103
+ <i class="fa-solid fa-check"></i> Follow request sent successfully.
104
+ </div>
105
+ <div v-else class="cpub-authorize-actions">
106
+ <button class="cpub-btn cpub-btn-primary" :disabled="loading" @click="handleFollow">
107
+ <i class="fa-solid fa-user-plus"></i> Follow
108
+ </button>
109
+ <a :href="uri" target="_blank" rel="noopener noreferrer" class="cpub-btn cpub-btn-sm">
110
+ <i class="fa-solid fa-arrow-up-right-from-square"></i> View Original
111
+ </a>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </template>
117
+
118
+ <style scoped>
119
+ .cpub-authorize-page {
120
+ display: flex;
121
+ justify-content: center;
122
+ align-items: flex-start;
123
+ padding: 60px 16px;
124
+ min-height: 60vh;
125
+ }
126
+ .cpub-authorize-card {
127
+ max-width: 520px;
128
+ width: 100%;
129
+ background: var(--surface);
130
+ border: var(--border-width-default) solid var(--border);
131
+ padding: 32px;
132
+ }
133
+ .cpub-authorize-title {
134
+ font-size: 18px;
135
+ font-weight: 700;
136
+ margin-bottom: 20px;
137
+ }
138
+ .cpub-authorize-loading {
139
+ color: var(--text-dim);
140
+ font-size: 14px;
141
+ }
142
+ .cpub-authorize-error {
143
+ color: var(--red, #ef4444);
144
+ font-size: 14px;
145
+ }
146
+ .cpub-authorize-desc {
147
+ font-size: 13px;
148
+ color: var(--text-dim);
149
+ margin-bottom: 16px;
150
+ }
151
+ .cpub-authorize-resource {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 8px;
155
+ margin-bottom: 8px;
156
+ }
157
+ .cpub-authorize-resource strong {
158
+ font-size: 15px;
159
+ }
160
+ .cpub-authorize-type {
161
+ font-family: var(--font-mono);
162
+ font-size: 10px;
163
+ text-transform: uppercase;
164
+ letter-spacing: 0.08em;
165
+ color: var(--accent);
166
+ background: var(--accent-bg);
167
+ padding: 2px 6px;
168
+ }
169
+ .cpub-authorize-uri {
170
+ display: block;
171
+ font-size: 11px;
172
+ color: var(--text-faint);
173
+ word-break: break-all;
174
+ margin-bottom: 20px;
175
+ padding: 8px;
176
+ background: var(--surface2);
177
+ border: 1px solid var(--border);
178
+ }
179
+ .cpub-authorize-success {
180
+ color: var(--green, #22c55e);
181
+ font-size: 14px;
182
+ font-weight: 600;
183
+ }
184
+ .cpub-authorize-actions {
185
+ display: flex;
186
+ gap: 8px;
187
+ }
188
+ </style>
@@ -206,6 +206,33 @@ async function handleDiscPost(): Promise<void> {
206
206
  const mirrorStatus = computed(() => hub.value?.followStatus ?? 'pending');
207
207
 
208
208
  const remoteFollowRef = ref<{ show: () => void } | null>(null);
209
+ const hubFollowing = ref(false);
210
+ const hubFollowStatus = ref('');
211
+
212
+ /** Follow the hub — if logged in, call API directly; otherwise show the remote follow modal */
213
+ async function handleJoinHub(): Promise<void> {
214
+ if (isAuthenticated.value && hub.value) {
215
+ // Logged-in user: call the hub-follow API directly
216
+ hubFollowing.value = true;
217
+ try {
218
+ const result = await $fetch<{ success: boolean; status: string }>('/api/federation/hub-follow', {
219
+ method: 'POST',
220
+ body: { federatedHubId: hub.value.id },
221
+ });
222
+ hubFollowStatus.value = result.status;
223
+ toast.success(result.status === 'accepted' ? 'Now following this hub' : 'Follow request sent');
224
+ await refreshHub();
225
+ } catch (err: unknown) {
226
+ const msg = err instanceof Error ? err.message : 'Failed to follow hub';
227
+ toast.error(msg);
228
+ } finally {
229
+ hubFollowing.value = false;
230
+ }
231
+ } else {
232
+ // Not logged in: show the remote follow modal
233
+ remoteFollowRef.value?.show();
234
+ }
235
+ }
209
236
 
210
237
  // --- Like state tracking ---
211
238
  const likedPostIds = ref<Set<string>>(new Set());
@@ -272,8 +299,8 @@ async function handlePostVote(postId: string): Promise<void> {
272
299
  <span v-if="mirrorStatus === 'accepted'" class="cpub-member-badge cpub-member-badge-mirrored">
273
300
  <i class="fa-solid fa-globe"></i> Mirrored
274
301
  </span>
275
- <button v-if="hub?.actorUri" class="cpub-btn cpub-btn-primary cpub-btn-sm" @click="remoteFollowRef?.show()">
276
- <i class="fa-solid fa-user-plus"></i> Join from your instance
302
+ <button v-if="hub?.actorUri" class="cpub-btn cpub-btn-primary cpub-btn-sm" :disabled="hubFollowing" @click="handleJoinHub">
303
+ <i class="fa-solid fa-user-plus"></i> {{ hubFollowing ? 'Following...' : 'Join from your instance' }}
277
304
  </button>
278
305
  <a v-if="hub?.url" :href="hub.url" target="_blank" rel="noopener noreferrer" class="cpub-btn cpub-btn-sm">
279
306
  <i class="fa-solid fa-arrow-up-right-from-square"></i> Visit original
@@ -0,0 +1,27 @@
1
+ import { sendFollow, resolveRemoteActor } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const schema = z.object({
5
+ uri: z.string().url(),
6
+ });
7
+
8
+ /**
9
+ * Follow a remote actor by URI. Used by /authorize_interaction.
10
+ * Resolves the actor, then sends an AP Follow activity.
11
+ */
12
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
13
+ requireFeature('federation');
14
+ const user = requireAuth(event);
15
+ const db = useDB();
16
+ const config = useConfig();
17
+ const { uri } = await parseBody(event, schema);
18
+
19
+ // Resolve actor to ensure it's cached
20
+ const actor = await resolveRemoteActor(db, uri);
21
+ if (!actor) {
22
+ throw createError({ statusCode: 404, statusMessage: 'Could not resolve remote actor' });
23
+ }
24
+
25
+ await sendFollow(db, user.id, uri, config.instance.domain);
26
+ return { success: true };
27
+ });
@@ -0,0 +1,32 @@
1
+ import { resolveRemoteActor } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const schema = z.object({
5
+ uri: z.string().url(),
6
+ });
7
+
8
+ /**
9
+ * Resolve a remote AP URI to a displayable resource.
10
+ * Used by /authorize_interaction to show what the user is about to follow.
11
+ */
12
+ export default defineEventHandler(async (event): Promise<{ type: string; name: string; url: string }> => {
13
+ requireFeature('federation');
14
+ requireAuth(event);
15
+ const db = useDB();
16
+ const { uri } = await parseBody(event, schema);
17
+
18
+ const actor = await resolveRemoteActor(db, uri);
19
+ if (actor) {
20
+ const actorType = (actor as Record<string, unknown>).type as string || 'Person';
21
+ const name = (actor as Record<string, unknown>).name as string
22
+ || (actor as Record<string, unknown>).preferredUsername as string
23
+ || uri;
24
+ return {
25
+ type: actorType === 'Group' ? 'Hub' : actorType,
26
+ name,
27
+ url: uri,
28
+ };
29
+ }
30
+
31
+ return { type: 'unknown', name: uri, url: uri };
32
+ });