pubky-app-specs 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.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Pubky App Specs · `pubky-app-specs`
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pubky-app-specs)](https://www.npmjs.com/package/pubky-app-specs)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
3
6
  A WASM library for building and validating structured JSON models compatible with Pubky.App social powered by [`@synonymdev/pubky`](https://www.npmjs.com/package/@synonymdev/pubky). It handles domain objects like **Users**, **Posts**, **Feeds**, **Bookmarks**, **Tags**, and more. Each object is:
4
7
 
5
8
  - **Sanitized** and **Validated** via Rust logic.
@@ -42,7 +45,7 @@ yarn add pubky-app-specs
42
45
  import { PubkySpecsBuilder } from "pubky-app-specs";
43
46
 
44
47
  // OR CommonJS
45
- const { PubkySpecsBuilder } = require("pubky-app-specs/index.cjs");
48
+ const { PubkySpecsBuilder } = require("pubky-app-specs");
46
49
 
47
50
  function loadSpecs(pubkyId) {
48
51
  // Create a specs builder instance - WASM is already initialized
@@ -83,7 +86,7 @@ async function createUser(pubkyId) {
83
86
  const userJson = user.toJson();
84
87
 
85
88
  // Store in homeserver via pubky
86
- const response = await client.fetch(userResult.meta.url, {
89
+ const response = await client.fetch(meta.url, {
87
90
  method: "PUT",
88
91
  body: JSON.stringify(userJson),
89
92
  credentials: "include",
@@ -93,8 +96,8 @@ async function createUser(pubkyId) {
93
96
  throw new Error(`Failed to store user: ${response.statusText}`);
94
97
  }
95
98
 
96
- console.log("User stored at:", userResult.meta.url);
97
- return userResult;
99
+ console.log("User stored at:", meta.url);
100
+ return { user, meta };
98
101
  }
99
102
  ```
100
103
 
@@ -105,17 +108,16 @@ import { Client } from "@synonymdev/pubky";
105
108
  import { PubkySpecsBuilder, PubkyAppPostKind } from "pubky-app-specs";
106
109
 
107
110
  async function createPost(pubkyId, content) {
108
- // fileData can be a File (browser) or a raw Blob/Buffer (Node).
109
111
  const client = new Client();
110
112
  const specs = new PubkySpecsBuilder(pubkyId);
111
113
 
112
- // Create the Post object referencing your (optional) attachment
114
+ // Create the Post object
113
115
  const {post, meta} = specs.createPost(
114
116
  content,
115
117
  PubkyAppPostKind.Short,
116
- null, // parent post
117
- null, // embed
118
- null // attachments list of urls
118
+ null, // parent post URI (for replies)
119
+ null, // embed object (for reposts)
120
+ null // attachments (array of file URLs, max 3)
119
121
  );
120
122
 
121
123
  // Store the post
@@ -130,7 +132,39 @@ async function createPost(pubkyId, content) {
130
132
  }
131
133
  ```
132
134
 
133
- ### 3) Following a User
135
+ ### 3) Creating a Post with Attachments
136
+
137
+ ```js
138
+ import { Client } from "@synonymdev/pubky";
139
+ import { PubkySpecsBuilder, PubkyAppPostKind } from "pubky-app-specs";
140
+
141
+ async function createPostWithAttachments(pubkyId, content, fileUrls) {
142
+ const client = new Client();
143
+ const specs = new PubkySpecsBuilder(pubkyId);
144
+
145
+ // Create post with attachments (max 3 allowed)
146
+ const {post, meta} = specs.createPost(
147
+ content,
148
+ PubkyAppPostKind.Image,
149
+ null, // parent
150
+ null, // embed
151
+ fileUrls // e.g. ["pubky://user/pub/pubky.app/files/abc123"]
152
+ );
153
+
154
+ const postJson = post.toJson();
155
+ console.log("Attachments:", postJson.attachments);
156
+
157
+ await client.fetch(meta.url, {
158
+ method: "PUT",
159
+ body: JSON.stringify(postJson),
160
+ });
161
+
162
+ console.log("Post with attachments stored at:", meta.url);
163
+ return {post, meta};
164
+ }
165
+ ```
166
+
167
+ ### 4) Following a User
134
168
 
135
169
  ```js
136
170
  import { Client } from "@synonymdev/pubky";
@@ -164,9 +198,109 @@ This library supports many more domain objects beyond `User` and `Post`. Here ar
164
198
  - **Mutes**: `createMute(...)`
165
199
  - **Follows**: `createFollow(...)`
166
200
  - **LastRead**: `createLastRead(...)`
201
+ - **Blobs**: `createBlob(...)`
202
+ - **Files**: `createFile(...)`
167
203
 
168
204
  Each has a `meta` field for storing relevant IDs/paths and a typed data object.
169
205
 
206
+ ### Creating a File with Blob
207
+
208
+ ```js
209
+ import { Client } from "@synonymdev/pubky";
210
+ import { PubkySpecsBuilder, getValidMimeTypes } from "pubky-app-specs";
211
+
212
+ async function uploadFile(pubkyId, fileData, fileName, contentType, fileSize) {
213
+ const client = new Client();
214
+ const specs = new PubkySpecsBuilder(pubkyId);
215
+
216
+ // First, create and store the blob (raw binary data)
217
+ const { blob, meta: blobMeta } = specs.createBlob(fileData);
218
+
219
+ await client.fetch(blobMeta.url, {
220
+ method: "PUT",
221
+ body: JSON.stringify(blob.toJson()),
222
+ });
223
+
224
+ // Then create the file metadata pointing to the blob
225
+ const { file, meta: fileMeta } = specs.createFile(
226
+ fileName, // e.g. "vacation-photo.jpg"
227
+ blobMeta.url, // Reference to the blob
228
+ contentType, // e.g. "image/jpeg"
229
+ fileSize // Size in bytes
230
+ );
231
+
232
+ await client.fetch(fileMeta.url, {
233
+ method: "PUT",
234
+ body: JSON.stringify(file.toJson()),
235
+ });
236
+
237
+ console.log("File stored at:", fileMeta.url);
238
+ return { file, meta: fileMeta };
239
+ }
240
+ ```
241
+
242
+ ---
243
+
244
+ ## ✅ Validating File MIME Types
245
+
246
+ Use `getValidMimeTypes()` to get the list of allowed MIME types for file attachments. This helps validate files before upload without duplicating the validation list.
247
+
248
+ ```js
249
+ import { getValidMimeTypes } from "pubky-app-specs";
250
+
251
+ // Get the list of valid MIME types
252
+ const validMimeTypes = getValidMimeTypes();
253
+ // Returns: ["application/javascript", "application/json", "application/pdf", "image/png", ...]
254
+
255
+ // Validate a file before upload
256
+ function isValidFileType(mimeType) {
257
+ return validMimeTypes.includes(mimeType);
258
+ }
259
+
260
+ // Example usage
261
+ if (isValidFileType(file.type)) {
262
+ // Proceed with upload
263
+ } else {
264
+ console.error(`Invalid file type: ${file.type}`);
265
+ }
266
+ ```
267
+
268
+ ## 🔗 URI Builder Utilities
269
+
270
+ These helper functions construct properly formatted Pubky URIs:
271
+
272
+ ```js
273
+ import {
274
+ userUriBuilder,
275
+ postUriBuilder,
276
+ bookmarkUriBuilder,
277
+ followUriBuilder,
278
+ tagUriBuilder,
279
+ muteUriBuilder,
280
+ lastReadUriBuilder,
281
+ blobUriBuilder,
282
+ fileUriBuilder,
283
+ feedUriBuilder,
284
+ } from "pubky-app-specs";
285
+
286
+ const userId = "8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto";
287
+ const targetUserId = "dzswkfy7ek3bqnoc89jxuqqfbzhjrj6mi8qthgbxxcqkdugm3rio";
288
+
289
+ // Build URIs for different resources
290
+ userUriBuilder(userId); // pubky://{userId}/pub/pubky.app/profile.json
291
+ postUriBuilder(userId, "0033SSE3B1FQ0"); // pubky://{userId}/pub/pubky.app/posts/{postId}
292
+ bookmarkUriBuilder(userId, "ABC123"); // pubky://{userId}/pub/pubky.app/bookmarks/{bookmarkId}
293
+ followUriBuilder(userId, targetUserId); // pubky://{userId}/pub/pubky.app/follows/{targetUserId}
294
+ tagUriBuilder(userId, "XYZ789"); // pubky://{userId}/pub/pubky.app/tags/{tagId}
295
+ muteUriBuilder(userId, targetUserId); // pubky://{userId}/pub/pubky.app/mutes/{targetUserId}
296
+ lastReadUriBuilder(userId); // pubky://{userId}/pub/pubky.app/last_read
297
+ blobUriBuilder(userId, "BLOB123"); // pubky://{userId}/pub/pubky.app/blobs/{blobId}
298
+ fileUriBuilder(userId, "FILE456"); // pubky://{userId}/pub/pubky.app/files/{fileId}
299
+ feedUriBuilder(userId, "FEED789"); // pubky://{userId}/pub/pubky.app/feeds/{feedId}
300
+ ```
301
+
302
+ ---
303
+
170
304
  ## 📌 Parsing a Pubky URI
171
305
 
172
306
  The `parse_uri()` function converts a Pubky URI string into a strongly typed object.
@@ -193,3 +327,9 @@ A `ParsedUriResult` object with:
193
327
  - **user_id:** The parsed user identifier.
194
328
  - **resource:** A string indicating the resource type.
195
329
  - **resource_id:** An optional resource identifier.
330
+
331
+ ---
332
+
333
+ ## 📄 License
334
+
335
+ MIT
package/example.js CHANGED
@@ -1,93 +1,295 @@
1
- import { PubkyAppPostKind, PubkySpecsBuilder, PubkyAppPostEmbed } from "./index.js";
1
+ import {
2
+ PubkyAppPostKind,
3
+ PubkySpecsBuilder,
4
+ PubkyAppPostEmbed,
5
+ userUriBuilder,
6
+ postUriBuilder,
7
+ bookmarkUriBuilder,
8
+ followUriBuilder,
9
+ tagUriBuilder,
10
+ muteUriBuilder,
11
+ lastReadUriBuilder,
12
+ blobUriBuilder,
13
+ fileUriBuilder,
14
+ feedUriBuilder,
15
+ getValidMimeTypes,
16
+ } from "./index.js";
2
17
 
18
+ // =============================================================================
19
+ // ANSI color helpers for pretty output
20
+ // =============================================================================
21
+ const c = {
22
+ reset: "\x1b[0m",
23
+ bright: "\x1b[1m",
24
+ dim: "\x1b[2m",
25
+ cyan: "\x1b[36m",
26
+ green: "\x1b[32m",
27
+ yellow: "\x1b[33m",
28
+ blue: "\x1b[34m",
29
+ magenta: "\x1b[35m",
30
+ gray: "\x1b[90m",
31
+ white: "\x1b[37m",
32
+ bgBlue: "\x1b[44m",
33
+ };
34
+
35
+ const divider = () => console.log(c.gray + "─".repeat(70) + c.reset);
36
+ const header = (title) => {
37
+ console.log();
38
+ console.log(`${c.bright}${c.blue}${title}${c.reset}`);
39
+ divider();
40
+ };
41
+ const field = (label, value) => {
42
+ console.log(` ${c.dim}${label.padEnd(12)}${c.reset} ${c.white}${value}${c.reset}`);
43
+ };
44
+
45
+ // =============================================================================
46
+ // Setup
47
+ // =============================================================================
3
48
  const OTTO = "8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto";
4
49
  const RIO = "dzswkfy7ek3bqnoc89jxuqqfbzhjrj6mi8qthgbxxcqkdugm3rio";
5
50
 
6
- // 👤 Create a user profile
7
- console.log("👤 Creating User Profile...");
51
+ console.log();
52
+ console.log(`${c.bgBlue}${c.white}${c.bright} ${c.reset}`);
53
+ console.log(`${c.bgBlue}${c.white}${c.bright} PUBKY APP SPECS - EXAMPLES ${c.reset}`);
54
+ console.log(`${c.bgBlue}${c.white}${c.bright} ${c.reset}`);
55
+ console.log();
56
+
8
57
  const specsBuilder = new PubkySpecsBuilder(OTTO);
9
- const { user, meta: userMeta } =
10
- specsBuilder.createUser("Alice Smith", "Software Developer", null, null, "active");
11
- console.log("User Profile URL:", userMeta.url);
12
- console.log("User Data:", JSON.stringify(user.toJson(), null, 2));
13
- console.log("-".repeat(60));
14
-
15
- // 📝 Create different posts
16
- console.log("📝 Creating First Post...");
17
- const { post, meta } = specsBuilder.createPost("Hello, Pubky world! This is my first post.", PubkyAppPostKind.Short, null, null, null);
18
- console.log("Post ID:", meta.id);
19
- console.log("Post URL:", meta.url);
20
- console.log("Post Data:", JSON.stringify(post.toJson(), null, 2));
21
- console.log("-".repeat(60));
22
-
23
- console.log("💬 Creating Reply Post...");
24
- const { post: replyPost, meta: replyMeta } = specsBuilder.createPost("This is a reply to the first post!", PubkyAppPostKind.Short, userMeta.url, null, null);
25
- console.log("Reply Post ID:", replyMeta.id);
26
- console.log("Reply Post URL:", replyMeta.url);
27
- console.log("Reply Data:", JSON.stringify(replyPost.toJson(), null, 2));
28
- console.log("-".repeat(60));
29
-
30
- console.log("🔄 Creating Repost with Embed...");
31
- let embeed = new PubkyAppPostEmbed(`pubky://${RIO}/pub/pubky.app/posts/0033SREKPC4N0`, PubkyAppPostKind.Video);
32
- const { post: repost, meta: repostMeta } = specsBuilder.createPost("This is a repost to random post!", PubkyAppPostKind.Short, null, embeed, null);
33
- console.log("Repost Post ID:", repostMeta.id);
34
- console.log("Repost Post URL:", repostMeta.url);
35
- console.log("Repost Data:", JSON.stringify(repost.toJson(), null, 2));
36
- console.log("-".repeat(60));
37
-
38
- console.log("🔖 Creating Bookmark...");
39
- let { bookmark, meta: bookmarkMeta } = specsBuilder.createBookmark(`pubky://${RIO}/pub/pubky.app/posts/0033SREKPC4N0`);
40
- console.log("Bookmark ID:", bookmarkMeta.id);
41
- console.log("Bookmark URL:", bookmarkMeta.url);
42
- console.log("Bookmark Data:", JSON.stringify(bookmark.toJson(), null, 2));
43
- console.log("-".repeat(60));
44
-
45
- console.log("👥 Creating Follow...");
46
- let {follow, meta: followMeta} = specsBuilder.createFollow(RIO);
47
- console.log("Follow ID:", followMeta.id);
48
- console.log("Follow URL:", followMeta.url);
49
- console.log("Follow Data:", JSON.stringify(follow.toJson(), null, 2));
50
- console.log("-".repeat(60));
51
-
52
- console.log("🏷️ Creating Tag...");
53
- let {tag, meta: tagMeta} = specsBuilder.createTag(`pubky://${OTTO}/pub/pubky.app/profile.json`, "otto");
54
- console.log("Tag ID:", tagMeta.id);
55
- console.log("Tag URL:", tagMeta.url);
56
- console.log("Tag Data:", JSON.stringify(tag.toJson(), null, 2));
57
- console.log("-".repeat(60));
58
-
59
- console.log("🔇 Creating Mute...");
60
- let {mute, meta: muteMeta} = specsBuilder.createMute(RIO);
61
- console.log("Mute ID:", muteMeta.id);
62
- console.log("Mute URL:", muteMeta.url);
63
- console.log("Mute Data:", JSON.stringify(mute.toJson(), null, 2));
64
- console.log("-".repeat(60));
65
-
66
- console.log("📖 Creating Last Read...");
67
- let {last_read, meta: lastReadMeta} = specsBuilder.createLastRead(RIO);
68
- console.log("LastRead Timestamp:", lastReadMeta.url);
69
- console.log("LastRead Data:", JSON.stringify(last_read.toJson(), null, 2));
70
- console.log("-".repeat(60));
71
-
72
- console.log("💾 Creating Blob...");
73
- let { blob, meta: blobMeta } = specsBuilder.createBlob(Array.from({length: 8}, () => Math.floor(Math.random() * 256)));
74
- console.log("Blob ID:", blobMeta.id);
75
- console.log("Blob URL:", blobMeta.url);
76
- console.log("Blob Data:", JSON.stringify(blob.toJson(), null, 2));
77
- console.log("-".repeat(60));
78
-
79
- console.log("📄 Creating File...");
80
- let { file, meta: fileMeta } = specsBuilder.createFile("My adventures", blobMeta.url, "application/pdf", 88);
81
- console.log("File ID:", fileMeta.id);
82
- console.log("File URL:", fileMeta.url);
83
- console.log("File Data:", JSON.stringify(file.toJson(), null, 2));
84
- console.log("-".repeat(60));
85
-
86
- console.log("📰 Creating Feed...");
87
- let { feed, meta: feedMeta } = specsBuilder.createFeed(["mountain","hike"], "all", "columns", "recent", "image", "nature");
88
- console.log("Feed ID:", feedMeta.id);
89
- console.log("Feed URL:", feedMeta.url);
90
- console.log("Feed Data:", JSON.stringify(feed.toJson(), null, 2));
91
- console.log("=".repeat(60));
92
- console.log("🎉 All Pubky App Specs examples completed successfully!");
93
- console.log("=".repeat(60));
58
+ console.log(`${c.dim}Using PubkyId: ${c.reset}${c.cyan}${OTTO}${c.reset}`);
59
+
60
+ // =============================================================================
61
+ // 1. User Profile
62
+ // =============================================================================
63
+ header("USER PROFILE");
64
+ const { user, meta: userMeta } = specsBuilder.createUser(
65
+ "Alice Smith",
66
+ "Software Developer",
67
+ null,
68
+ null,
69
+ "active"
70
+ );
71
+ field("URL", userMeta.url);
72
+ field("Name", user.toJson().name);
73
+ field("Bio", user.toJson().bio);
74
+ field("Status", user.toJson().status);
75
+
76
+ // =============================================================================
77
+ // 2. Posts
78
+ // =============================================================================
79
+ header("POSTS");
80
+
81
+ // Simple post
82
+ console.log(` ${c.yellow}▸ Simple Post${c.reset}`);
83
+ const { post, meta } = specsBuilder.createPost(
84
+ "Hello, Pubky world! This is my first post.",
85
+ PubkyAppPostKind.Short,
86
+ null,
87
+ null,
88
+ null
89
+ );
90
+ field("ID", meta.id);
91
+ field("URL", meta.url);
92
+ field("Content", post.toJson().content);
93
+ console.log();
94
+
95
+ // Reply post
96
+ console.log(` ${c.yellow}▸ Reply Post${c.reset}`);
97
+ const { post: replyPost, meta: replyMeta } = specsBuilder.createPost(
98
+ "This is a reply to the first post!",
99
+ PubkyAppPostKind.Short,
100
+ userMeta.url,
101
+ null,
102
+ null
103
+ );
104
+ field("ID", replyMeta.id);
105
+ field("Parent", replyPost.toJson().parent);
106
+ console.log();
107
+
108
+ // Repost with embed
109
+ console.log(` ${c.yellow}▸ Repost with Embed${c.reset}`);
110
+ const embed = new PubkyAppPostEmbed(
111
+ `pubky://${RIO}/pub/pubky.app/posts/0033SREKPC4N0`,
112
+ PubkyAppPostKind.Video
113
+ );
114
+ const { post: repost, meta: repostMeta } = specsBuilder.createPost(
115
+ "Check out this awesome video!",
116
+ PubkyAppPostKind.Short,
117
+ null,
118
+ embed,
119
+ null
120
+ );
121
+ field("ID", repostMeta.id);
122
+ field("Embed URI", repost.toJson().embed.uri);
123
+ field("Embed Kind", repost.toJson().embed.kind);
124
+ console.log();
125
+
126
+ // Post with attachments
127
+ console.log(` ${c.yellow}▸ Post with Attachments${c.reset}`);
128
+ const { post: postWithAttachments, meta: postWithAttachmentsMeta } = specsBuilder.createPost(
129
+ "Check out these photos from my trip!",
130
+ PubkyAppPostKind.Image,
131
+ null,
132
+ null,
133
+ [
134
+ `pubky://${OTTO}/pub/pubky.app/files/0034A0X7NJ52G`,
135
+ `pubky://${OTTO}/pub/pubky.app/files/0034A0X7NJ53H`,
136
+ ]
137
+ );
138
+ field("ID", postWithAttachmentsMeta.id);
139
+ field("Attachments", `${postWithAttachments.toJson().attachments.length} files`);
140
+
141
+ // =============================================================================
142
+ // 3. Social Actions
143
+ // =============================================================================
144
+ header("SOCIAL ACTIONS");
145
+
146
+ // Bookmark
147
+ console.log(` ${c.yellow}▸ Bookmark${c.reset}`);
148
+ const { bookmark, meta: bookmarkMeta } = specsBuilder.createBookmark(
149
+ `pubky://${RIO}/pub/pubky.app/posts/0033SREKPC4N0`
150
+ );
151
+ field("ID", bookmarkMeta.id);
152
+ field("URI", bookmark.toJson().uri);
153
+ console.log();
154
+
155
+ // Follow
156
+ console.log(` ${c.yellow}▸ Follow${c.reset}`);
157
+ const { follow, meta: followMeta } = specsBuilder.createFollow(RIO);
158
+ field("ID", followMeta.id);
159
+ field("URL", followMeta.url);
160
+ console.log();
161
+
162
+ // Tag
163
+ console.log(` ${c.yellow}▸ Tag${c.reset}`);
164
+ const { tag, meta: tagMeta } = specsBuilder.createTag(
165
+ `pubky://${OTTO}/pub/pubky.app/profile.json`,
166
+ "developer"
167
+ );
168
+ field("ID", tagMeta.id);
169
+ field("Label", tag.toJson().label);
170
+ field("URI", tag.toJson().uri);
171
+ console.log();
172
+
173
+ // Mute
174
+ console.log(` ${c.yellow}▸ Mute${c.reset}`);
175
+ const { mute, meta: muteMeta } = specsBuilder.createMute(RIO);
176
+ field("ID", muteMeta.id);
177
+ field("URL", muteMeta.url);
178
+
179
+ // =============================================================================
180
+ // 4. Files & Blobs
181
+ // =============================================================================
182
+ header("FILES & BLOBS");
183
+
184
+ // Blob
185
+ console.log(` ${c.yellow}▸ Blob (raw data)${c.reset}`);
186
+ const blobData = Array.from({ length: 8 }, () => Math.floor(Math.random() * 256));
187
+ const { blob, meta: blobMeta } = specsBuilder.createBlob(blobData);
188
+ field("ID", blobMeta.id);
189
+ field("URL", blobMeta.url);
190
+ field("Size", `${blobData.length} bytes`);
191
+ console.log();
192
+
193
+ // File
194
+ console.log(` ${c.yellow}▸ File (metadata)${c.reset}`);
195
+ const { file, meta: fileMeta } = specsBuilder.createFile(
196
+ "vacation-photos.pdf",
197
+ blobMeta.url,
198
+ "application/pdf",
199
+ 1024
200
+ );
201
+ field("ID", fileMeta.id);
202
+ field("Name", file.toJson().name);
203
+ field("Type", file.toJson().content_type);
204
+ field("Size", `${file.toJson().size} bytes`);
205
+ field("Source", file.toJson().src);
206
+
207
+ // =============================================================================
208
+ // 5. Feeds & LastRead
209
+ // =============================================================================
210
+ header("FEEDS & LAST READ");
211
+
212
+ // Feed
213
+ console.log(` ${c.yellow}▸ Custom Feed${c.reset}`);
214
+ const { feed, meta: feedMeta } = specsBuilder.createFeed(
215
+ ["mountain", "hiking", "nature"],
216
+ "all",
217
+ "columns",
218
+ "recent",
219
+ "image",
220
+ "Outdoor Adventures"
221
+ );
222
+ field("ID", feedMeta.id);
223
+ field("Name", feed.toJson().name);
224
+ field("Tags", feed.toJson().feed.tags.join(", "));
225
+ field("Layout", feed.toJson().feed.layout);
226
+ field("Sort", feed.toJson().feed.sort);
227
+ console.log();
228
+
229
+ // LastRead
230
+ console.log(` ${c.yellow}▸ Last Read Marker${c.reset}`);
231
+ const { last_read, meta: lastReadMeta } = specsBuilder.createLastRead();
232
+ field("URL", lastReadMeta.url);
233
+ field("Timestamp", new Date(last_read.toJson().timestamp / 1000).toISOString());
234
+
235
+ // =============================================================================
236
+ // 6. URI Builders
237
+ // =============================================================================
238
+ header("URI BUILDERS");
239
+ const uris = [
240
+ ["User", userUriBuilder(OTTO)],
241
+ ["Post", postUriBuilder(OTTO, meta.id)],
242
+ ["Bookmark", bookmarkUriBuilder(OTTO, bookmarkMeta.id)],
243
+ ["Follow", followUriBuilder(OTTO, RIO)],
244
+ ["Tag", tagUriBuilder(OTTO, tagMeta.id)],
245
+ ["Mute", muteUriBuilder(OTTO, RIO)],
246
+ ["LastRead", lastReadUriBuilder(OTTO)],
247
+ ["Blob", blobUriBuilder(OTTO, blobMeta.id)],
248
+ ["File", fileUriBuilder(OTTO, fileMeta.id)],
249
+ ["Feed", feedUriBuilder(OTTO, feedMeta.id)],
250
+ ];
251
+ uris.forEach(([name, uri]) => {
252
+ console.log(` ${c.dim}${name.padEnd(10)}${c.reset} ${c.cyan}${uri}${c.reset}`);
253
+ });
254
+
255
+ // =============================================================================
256
+ // 7. Valid MIME Types
257
+ // =============================================================================
258
+ header("VALID MIME TYPES");
259
+ const validMimeTypes = getValidMimeTypes();
260
+ console.log(` ${c.dim}Total types:${c.reset} ${c.bright}${validMimeTypes.length}${c.reset}`);
261
+ console.log();
262
+
263
+ // Group by category
264
+ const categories = {
265
+ "Images": validMimeTypes.filter(t => t.startsWith("image/")),
266
+ "Videos": validMimeTypes.filter(t => t.startsWith("video/")),
267
+ "Audio": validMimeTypes.filter(t => t.startsWith("audio/")),
268
+ "Documents": validMimeTypes.filter(t => t.startsWith("application/") || t.startsWith("text/")),
269
+ };
270
+
271
+ Object.entries(categories).forEach(([category, types]) => {
272
+ if (types.length > 0) {
273
+ console.log(` ${c.yellow}${category}:${c.reset}`);
274
+ types.forEach(type => console.log(` ${c.dim}-${c.reset} ${type}`));
275
+ console.log();
276
+ }
277
+ });
278
+
279
+ // Validation example
280
+ console.log(` ${c.yellow}Validation Example:${c.reset}`);
281
+ const testTypes = ["image/png", "video/mp4", "application/x-executable"];
282
+ testTypes.forEach(type => {
283
+ const isValid = validMimeTypes.includes(type);
284
+ const icon = isValid ? `${c.green}[ok]${c.reset}` : `${c.magenta}[x]${c.reset}`;
285
+ console.log(` ${icon} ${type}`);
286
+ });
287
+
288
+ // =============================================================================
289
+ // Done!
290
+ // =============================================================================
291
+ console.log();
292
+ console.log(`${c.bgBlue}${c.white}${c.bright} ${c.reset}`);
293
+ console.log(`${c.bgBlue}${c.white}${c.bright} ALL EXAMPLES COMPLETED SUCCESSFULLY! ${c.reset}`);
294
+ console.log(`${c.bgBlue}${c.white}${c.bright} ${c.reset}`);
295
+ console.log();
package/index.cjs CHANGED
@@ -446,6 +446,32 @@ module.exports.lastReadUriBuilder = function(author_id) {
446
446
  }
447
447
  };
448
448
 
449
+ /**
450
+ * Returns the list of valid MIME types for file attachments.
451
+ *
452
+ * This allows JavaScript consumers to validate file types before submission
453
+ * without having to duplicate the list.
454
+ *
455
+ * # Example (TypeScript)
456
+ *
457
+ * ```typescript
458
+ * import { get_valid_mime_types } from "pubky-app-specs";
459
+ *
460
+ * const validTypes = get_valid_mime_types();
461
+ * const fileType = "image/png";
462
+ * if (validTypes.includes(fileType)) {
463
+ * console.log("Valid file type!");
464
+ * }
465
+ * ```
466
+ * @returns {any[]}
467
+ */
468
+ module.exports.getValidMimeTypes = function() {
469
+ const ret = wasm.getValidMimeTypes();
470
+ var v1 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
471
+ wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
472
+ return v1;
473
+ };
474
+
449
475
  /**
450
476
  * Parses a Pubky URI and returns a strongly typed `ParsedUriResult`.
451
477
  *