@m5kdev/backend 0.1.1 → 0.1.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.
Files changed (113) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/src/lib/posthog.js +7 -0
  4. package/dist/src/lib/sentry.js +9 -0
  5. package/dist/src/modules/access/access.repository.js +32 -0
  6. package/dist/src/modules/access/access.service.js +51 -0
  7. package/dist/src/modules/access/access.test.js +182 -0
  8. package/dist/src/modules/access/access.utils.js +20 -0
  9. package/dist/src/modules/ai/ai.db.js +39 -0
  10. package/dist/src/modules/ai/ai.prompt.js +30 -0
  11. package/dist/src/modules/ai/ai.repository.js +26 -0
  12. package/dist/src/modules/ai/ai.router.js +132 -0
  13. package/dist/src/modules/ai/ai.service.js +207 -0
  14. package/dist/src/modules/ai/ai.trpc.d.ts +5 -5
  15. package/dist/src/modules/ai/ai.trpc.js +20 -0
  16. package/dist/src/modules/ai/ideogram/ideogram.constants.js +167 -0
  17. package/dist/src/modules/ai/ideogram/ideogram.dto.js +49 -0
  18. package/dist/src/modules/ai/ideogram/ideogram.prompt.js +860 -0
  19. package/dist/src/modules/ai/ideogram/ideogram.repository.js +46 -0
  20. package/dist/src/modules/ai/ideogram/ideogram.service.js +11 -0
  21. package/dist/src/modules/auth/auth.db.js +215 -0
  22. package/dist/src/modules/auth/auth.dto.js +38 -0
  23. package/dist/src/modules/auth/auth.lib.d.ts +4 -4
  24. package/dist/src/modules/auth/auth.lib.js +284 -0
  25. package/dist/src/modules/auth/auth.middleware.js +52 -0
  26. package/dist/src/modules/auth/auth.repository.js +541 -0
  27. package/dist/src/modules/auth/auth.service.js +201 -0
  28. package/dist/src/modules/auth/auth.trpc.d.ts +18 -18
  29. package/dist/src/modules/auth/auth.trpc.js +157 -0
  30. package/dist/src/modules/auth/auth.utils.js +97 -0
  31. package/dist/src/modules/base/base.abstract.js +53 -0
  32. package/dist/src/modules/base/base.dto.js +112 -0
  33. package/dist/src/modules/base/base.grants.js +123 -0
  34. package/dist/src/modules/base/base.grants.test.js +668 -0
  35. package/dist/src/modules/base/base.repository.js +307 -0
  36. package/dist/src/modules/base/base.service.js +109 -0
  37. package/dist/src/modules/base/base.types.js +2 -0
  38. package/dist/src/modules/billing/billing.db.js +29 -0
  39. package/dist/src/modules/billing/billing.repository.js +235 -0
  40. package/dist/src/modules/billing/billing.router.js +56 -0
  41. package/dist/src/modules/billing/billing.service.js +147 -0
  42. package/dist/src/modules/billing/billing.trpc.d.ts +5 -5
  43. package/dist/src/modules/billing/billing.trpc.js +17 -0
  44. package/dist/src/modules/clay/clay.repository.js +26 -0
  45. package/dist/src/modules/clay/clay.service.js +24 -0
  46. package/dist/src/modules/connect/connect.db.js +30 -0
  47. package/dist/src/modules/connect/connect.dto.js +36 -0
  48. package/dist/src/modules/connect/connect.linkedin.js +53 -0
  49. package/dist/src/modules/connect/connect.oauth.js +198 -0
  50. package/dist/src/modules/connect/connect.repository.d.ts +7 -7
  51. package/dist/src/modules/connect/connect.repository.js +54 -0
  52. package/dist/src/modules/connect/connect.router.js +54 -0
  53. package/dist/src/modules/connect/connect.service.d.ts +14 -14
  54. package/dist/src/modules/connect/connect.service.js +114 -0
  55. package/dist/src/modules/connect/connect.trpc.d.ts +10 -10
  56. package/dist/src/modules/connect/connect.trpc.js +21 -0
  57. package/dist/src/modules/connect/connect.types.js +2 -0
  58. package/dist/src/modules/crypto/crypto.db.js +17 -0
  59. package/dist/src/modules/crypto/crypto.repository.js +10 -0
  60. package/dist/src/modules/crypto/crypto.service.js +52 -0
  61. package/dist/src/modules/email/email.service.js +107 -0
  62. package/dist/src/modules/file/file.repository.js +79 -0
  63. package/dist/src/modules/file/file.router.js +99 -0
  64. package/dist/src/modules/file/file.service.js +150 -0
  65. package/dist/src/modules/recurrence/recurrence.db.js +66 -0
  66. package/dist/src/modules/recurrence/recurrence.repository.js +39 -0
  67. package/dist/src/modules/recurrence/recurrence.service.js +70 -0
  68. package/dist/src/modules/recurrence/recurrence.trpc.d.ts +15 -15
  69. package/dist/src/modules/recurrence/recurrence.trpc.js +65 -0
  70. package/dist/src/modules/social/social.dto.js +18 -0
  71. package/dist/src/modules/social/social.linkedin.js +427 -0
  72. package/dist/src/modules/social/social.linkedin.test.js +235 -0
  73. package/dist/src/modules/social/social.service.js +76 -0
  74. package/dist/src/modules/social/social.types.js +2 -0
  75. package/dist/src/modules/tag/tag.db.js +42 -0
  76. package/dist/src/modules/tag/tag.dto.js +9 -0
  77. package/dist/src/modules/tag/tag.repository.js +154 -0
  78. package/dist/src/modules/tag/tag.service.js +31 -0
  79. package/dist/src/modules/tag/tag.trpc.d.ts +5 -5
  80. package/dist/src/modules/tag/tag.trpc.js +47 -0
  81. package/dist/src/modules/utils/applyPagination.js +16 -0
  82. package/dist/src/modules/utils/applySorting.js +18 -0
  83. package/dist/src/modules/utils/getConditionsFromFilters.js +200 -0
  84. package/dist/src/modules/video/video.service.js +84 -0
  85. package/dist/src/modules/webhook/webhook.constants.js +10 -0
  86. package/dist/src/modules/webhook/webhook.db.js +17 -0
  87. package/dist/src/modules/webhook/webhook.dto.js +7 -0
  88. package/dist/src/modules/webhook/webhook.repository.js +56 -0
  89. package/dist/src/modules/webhook/webhook.router.js +30 -0
  90. package/dist/src/modules/webhook/webhook.service.js +68 -0
  91. package/dist/src/modules/workflow/workflow.db.js +30 -0
  92. package/dist/src/modules/workflow/workflow.repository.js +105 -0
  93. package/dist/src/modules/workflow/workflow.service.js +37 -0
  94. package/dist/src/modules/workflow/workflow.trpc.d.ts +5 -5
  95. package/dist/src/modules/workflow/workflow.trpc.js +21 -0
  96. package/dist/src/modules/workflow/workflow.types.js +2 -0
  97. package/dist/src/modules/workflow/workflow.utils.js +173 -0
  98. package/dist/src/test/stubs/utils.js +5 -0
  99. package/dist/src/trpc/context.d.ts +5 -5
  100. package/dist/src/trpc/context.js +17 -0
  101. package/dist/src/trpc/index.js +6 -0
  102. package/dist/src/trpc/procedures.d.ts +56 -56
  103. package/dist/src/trpc/procedures.js +32 -0
  104. package/dist/src/trpc/utils.js +20 -0
  105. package/dist/src/types.d.ts +33 -33
  106. package/dist/src/types.js +13 -0
  107. package/dist/src/utils/errors.js +104 -0
  108. package/dist/src/utils/logger.js +11 -0
  109. package/dist/src/utils/posthog.js +31 -0
  110. package/dist/src/utils/types.js +2 -0
  111. package/dist/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +3 -3
  113. package/tsconfig.json +2 -0
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLinkedInSocialProvider = createLinkedInSocialProvider;
4
+ exports.escapeLinkedInText = escapeLinkedInText;
5
+ const node_fs_1 = require("node:fs");
6
+ const promises_1 = require("node:fs/promises");
7
+ const LINKEDIN_API_BASE = "https://api.linkedin.com/rest";
8
+ const LINKEDIN_VERSION = "202601";
9
+ const IMAGES_URL = `${LINKEDIN_API_BASE}/images?action=initializeUpload`;
10
+ const VIDEOS_URL = `${LINKEDIN_API_BASE}/videos?action=initializeUpload`;
11
+ const VIDEOS_FINALIZE_URL = `${LINKEDIN_API_BASE}/videos?action=finalizeUpload`;
12
+ const DOCUMENTS_URL = `${LINKEDIN_API_BASE}/documents?action=initializeUpload`;
13
+ const POSTS_URL = `${LINKEDIN_API_BASE}/posts`;
14
+ function getApiHeaders(accessToken) {
15
+ return {
16
+ Authorization: `Bearer ${accessToken}`,
17
+ "Content-Type": "application/json",
18
+ "Linkedin-Version": LINKEDIN_VERSION,
19
+ "X-Restli-Protocol-Version": "2.0.0",
20
+ };
21
+ }
22
+ function createLinkedInSocialProvider() {
23
+ return {
24
+ id: "linkedin",
25
+ async post({ deps, context, payload }) {
26
+ const personUrn = resolveAuthorUrn(context.connection);
27
+ const mediaDescriptors = payload.media ?? [];
28
+ let uploadedAssets = [];
29
+ if (mediaDescriptors.length > 0) {
30
+ uploadedAssets = await uploadMediaAssets({
31
+ accessToken: context.accessToken,
32
+ fileService: deps.fileService,
33
+ mediaDescriptors,
34
+ personUrn,
35
+ });
36
+ }
37
+ const response = await publishPost({
38
+ accessToken: context.accessToken,
39
+ personUrn,
40
+ payload,
41
+ uploadedAssets,
42
+ });
43
+ return {
44
+ shareUrn: response.postUrn,
45
+ rawResponse: response.raw,
46
+ };
47
+ },
48
+ };
49
+ }
50
+ function resolveAuthorUrn(connection) {
51
+ if (connection.metadataJson) {
52
+ try {
53
+ const metadata = connection.metadataJson;
54
+ if (metadata.linkedInUrn && typeof metadata.linkedInUrn === "string") {
55
+ return metadata.linkedInUrn;
56
+ }
57
+ }
58
+ catch {
59
+ throw new Error("Failed to parse LinkedIn connection metadata");
60
+ }
61
+ }
62
+ if (connection.providerAccountId) {
63
+ return `urn:li:person:${connection.providerAccountId}`;
64
+ }
65
+ throw new Error("LinkedIn connection is missing a person URN");
66
+ }
67
+ async function uploadMediaAssets(params) {
68
+ const results = [];
69
+ for (const descriptor of params.mediaDescriptors) {
70
+ const download = await params.fileService.downloadS3ToFile(descriptor.s3Path);
71
+ if (download.isErr()) {
72
+ throw download.error;
73
+ }
74
+ const localPath = download.value;
75
+ try {
76
+ const mediaType = determineMediaType(descriptor.mediaType, localPath);
77
+ let assetUrn;
78
+ switch (mediaType) {
79
+ case "image":
80
+ assetUrn = await uploadImage({
81
+ accessToken: params.accessToken,
82
+ personUrn: params.personUrn,
83
+ localPath,
84
+ });
85
+ break;
86
+ case "video":
87
+ assetUrn = await uploadVideo({
88
+ accessToken: params.accessToken,
89
+ personUrn: params.personUrn,
90
+ localPath,
91
+ });
92
+ break;
93
+ case "document":
94
+ assetUrn = await uploadDocument({
95
+ accessToken: params.accessToken,
96
+ personUrn: params.personUrn,
97
+ localPath,
98
+ });
99
+ break;
100
+ }
101
+ results.push({
102
+ assetUrn,
103
+ mediaType,
104
+ title: descriptor.title,
105
+ description: descriptor.description,
106
+ });
107
+ }
108
+ finally {
109
+ await (0, promises_1.unlink)(localPath).catch(() => undefined);
110
+ }
111
+ }
112
+ return results;
113
+ }
114
+ function determineMediaType(explicitType, localPath) {
115
+ if (explicitType) {
116
+ return explicitType;
117
+ }
118
+ const extension = localPath.split(".").pop()?.toLowerCase();
119
+ if (!extension) {
120
+ throw new Error("Unable to determine media type from file extension");
121
+ }
122
+ const imageExtensions = new Set(["jpg", "jpeg", "png", "gif", "webp"]);
123
+ const videoExtensions = new Set(["mp4", "mov", "m4v", "avi", "mkv", "webm"]);
124
+ const documentExtensions = new Set(["pdf", "ppt", "pptx", "doc", "docx"]);
125
+ if (imageExtensions.has(extension)) {
126
+ return "image";
127
+ }
128
+ if (videoExtensions.has(extension)) {
129
+ return "video";
130
+ }
131
+ if (documentExtensions.has(extension)) {
132
+ return "document";
133
+ }
134
+ throw new Error(`Unsupported media extension: ${extension}`);
135
+ }
136
+ async function uploadImage(params) {
137
+ const initResponse = await fetch(IMAGES_URL, {
138
+ method: "POST",
139
+ headers: getApiHeaders(params.accessToken),
140
+ body: JSON.stringify({
141
+ initializeUploadRequest: {
142
+ owner: params.personUrn,
143
+ },
144
+ }),
145
+ });
146
+ if (!initResponse.ok) {
147
+ const errorText = await initResponse.text().catch(() => "");
148
+ throw new Error(`LinkedIn image upload initialization failed: ${initResponse.status} ${errorText}`);
149
+ }
150
+ const initJson = (await initResponse.json());
151
+ const { uploadUrl, image } = initJson.value;
152
+ if (!uploadUrl || !image) {
153
+ throw new Error("LinkedIn image initialization response missing required fields");
154
+ }
155
+ await uploadBinaryToLinkedIn({
156
+ uploadUrl,
157
+ localPath: params.localPath,
158
+ mimeType: inferImageMime(params.localPath),
159
+ });
160
+ return image;
161
+ }
162
+ async function uploadVideo(params) {
163
+ const fileSizeBytes = await getFileSize(params.localPath);
164
+ const initResponse = await fetch(VIDEOS_URL, {
165
+ method: "POST",
166
+ headers: getApiHeaders(params.accessToken),
167
+ body: JSON.stringify({
168
+ initializeUploadRequest: {
169
+ owner: params.personUrn,
170
+ fileSizeBytes,
171
+ uploadCaptions: false,
172
+ uploadThumbnail: false,
173
+ },
174
+ }),
175
+ });
176
+ if (!initResponse.ok) {
177
+ const errorText = await initResponse.text().catch(() => "");
178
+ throw new Error(`LinkedIn video upload initialization failed: ${initResponse.status} ${errorText}`);
179
+ }
180
+ const initJson = (await initResponse.json());
181
+ const { uploadInstructions, video, uploadToken } = initJson.value;
182
+ if (!uploadInstructions || !video || !uploadToken) {
183
+ throw new Error("LinkedIn video initialization response missing required fields");
184
+ }
185
+ const uploadedPartIds = await uploadVideoMultipart({
186
+ localPath: params.localPath,
187
+ uploadInstructions,
188
+ });
189
+ await finalizeVideoUpload({
190
+ accessToken: params.accessToken,
191
+ video,
192
+ uploadToken,
193
+ uploadedPartIds,
194
+ });
195
+ return video;
196
+ }
197
+ async function uploadVideoMultipart(params) {
198
+ const uploadedPartIds = [];
199
+ for (const instruction of params.uploadInstructions) {
200
+ const chunkStream = (0, node_fs_1.createReadStream)(params.localPath, {
201
+ start: instruction.firstByte,
202
+ end: instruction.lastByte,
203
+ });
204
+ const uploadResponse = await fetch(instruction.uploadUrl, {
205
+ method: "PUT",
206
+ headers: {
207
+ "Content-Type": "application/octet-stream",
208
+ },
209
+ body: chunkStream,
210
+ duplex: "half",
211
+ });
212
+ if (!uploadResponse.ok) {
213
+ const errorText = await uploadResponse.text().catch(() => "");
214
+ throw new Error(`LinkedIn video part upload failed: ${uploadResponse.status} ${errorText}`);
215
+ }
216
+ const etag = uploadResponse.headers.get("etag");
217
+ if (!etag) {
218
+ throw new Error("LinkedIn video part upload response missing ETag header");
219
+ }
220
+ uploadedPartIds.push(etag.replace(/"/g, ""));
221
+ }
222
+ return uploadedPartIds;
223
+ }
224
+ async function finalizeVideoUpload(params) {
225
+ const response = await fetch(VIDEOS_FINALIZE_URL, {
226
+ method: "POST",
227
+ headers: getApiHeaders(params.accessToken),
228
+ body: JSON.stringify({
229
+ finalizeUploadRequest: {
230
+ video: params.video,
231
+ uploadToken: params.uploadToken,
232
+ uploadedPartIds: params.uploadedPartIds,
233
+ },
234
+ }),
235
+ });
236
+ if (!response.ok) {
237
+ const errorText = await response.text().catch(() => "");
238
+ throw new Error(`LinkedIn video finalize failed: ${response.status} ${errorText}`);
239
+ }
240
+ }
241
+ async function uploadDocument(params) {
242
+ const initResponse = await fetch(DOCUMENTS_URL, {
243
+ method: "POST",
244
+ headers: getApiHeaders(params.accessToken),
245
+ body: JSON.stringify({
246
+ initializeUploadRequest: {
247
+ owner: params.personUrn,
248
+ },
249
+ }),
250
+ });
251
+ if (!initResponse.ok) {
252
+ const errorText = await initResponse.text().catch(() => "");
253
+ throw new Error(`LinkedIn document upload initialization failed: ${initResponse.status} ${errorText}`);
254
+ }
255
+ const initJson = (await initResponse.json());
256
+ const { uploadUrl, document } = initJson.value;
257
+ if (!uploadUrl || !document) {
258
+ throw new Error("LinkedIn document initialization response missing required fields");
259
+ }
260
+ await uploadBinaryToLinkedIn({
261
+ uploadUrl,
262
+ localPath: params.localPath,
263
+ mimeType: inferDocumentMime(params.localPath),
264
+ });
265
+ return document;
266
+ }
267
+ async function uploadBinaryToLinkedIn(params) {
268
+ const uploadResponse = await fetch(params.uploadUrl, {
269
+ method: "PUT",
270
+ headers: {
271
+ "Content-Type": params.mimeType,
272
+ },
273
+ body: (0, node_fs_1.createReadStream)(params.localPath),
274
+ duplex: "half",
275
+ });
276
+ if (!uploadResponse.ok) {
277
+ const errorText = await uploadResponse.text().catch(() => "");
278
+ throw new Error(`LinkedIn media upload failed: ${uploadResponse.status} ${errorText}`);
279
+ }
280
+ }
281
+ async function getFileSize(filePath) {
282
+ const stats = await (0, promises_1.stat)(filePath);
283
+ return stats.size;
284
+ }
285
+ function inferImageMime(localPath) {
286
+ const extension = localPath.split(".").pop()?.toLowerCase();
287
+ switch (extension) {
288
+ case "png":
289
+ return "image/png";
290
+ case "webp":
291
+ return "image/webp";
292
+ case "gif":
293
+ return "image/gif";
294
+ case "jpg":
295
+ case "jpeg":
296
+ return "image/jpeg";
297
+ default:
298
+ return "application/octet-stream";
299
+ }
300
+ }
301
+ function inferDocumentMime(localPath) {
302
+ const extension = localPath.split(".").pop()?.toLowerCase();
303
+ switch (extension) {
304
+ case "pdf":
305
+ return "application/pdf";
306
+ case "doc":
307
+ return "application/msword";
308
+ case "docx":
309
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
310
+ case "ppt":
311
+ return "application/vnd.ms-powerpoint";
312
+ case "pptx":
313
+ return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
314
+ default:
315
+ return "application/octet-stream";
316
+ }
317
+ }
318
+ async function publishPost(params) {
319
+ const body = buildPostBody(params);
320
+ const response = await fetch(POSTS_URL, {
321
+ method: "POST",
322
+ headers: getApiHeaders(params.accessToken),
323
+ body: JSON.stringify(body),
324
+ });
325
+ if (!response.ok) {
326
+ const errorText = await response.text().catch(() => "");
327
+ throw new Error(`LinkedIn post failed: ${response.status} ${errorText}`);
328
+ }
329
+ const postUrn = response.headers.get("x-restli-id") ?? undefined;
330
+ const raw = await safeJson(response);
331
+ return { postUrn, raw };
332
+ }
333
+ /**
334
+ * Escapes special characters in text for LinkedIn's post commentary field.
335
+ * LinkedIn requires certain characters to be backslash-escaped.
336
+ * Preserves mentions (@[Name](urn:li:...)) and hashtag templates ({hashtag|#|tag}).
337
+ * Converts simple hashtags (#tag) to the template format.
338
+ * @see https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/little-text-format
339
+ */
340
+ function escapeLinkedInText(text) {
341
+ // Patterns for LinkedIn elements that should not be escaped
342
+ // MentionElement: @[FallbackText](urn:li:...)
343
+ const mentionPattern = /@\[[^\]]*\]\(urn:li:[^)]+\)/g;
344
+ // HashtagTemplate: {hashtag|#|text} or {hashtag|#|text} (with optional escaped #)
345
+ const hashtagTemplatePattern = /\{hashtag\|\\?[##]\|[^}]+\}/g;
346
+ // Simple hashtag: #word (must start with a letter, not digit-only)
347
+ const simpleHashtagPattern = /#([a-zA-Z\u00C0-\u024F\u1E00-\u1EFF][\w\u00C0-\u024F\u1E00-\u1EFF]*)/g;
348
+ // Store matches in array, use index-based placeholders with null character delimiters
349
+ const preserved = [];
350
+ const createPlaceholder = (content) => {
351
+ const index = preserved.length;
352
+ preserved.push(content);
353
+ return `\x00${index}\x00`;
354
+ };
355
+ // Replace mentions with placeholders (preserve as-is)
356
+ let result = text.replace(mentionPattern, (match) => createPlaceholder(match));
357
+ // Replace hashtag templates with placeholders (preserve as-is)
358
+ result = result.replace(hashtagTemplatePattern, (match) => createPlaceholder(match));
359
+ // Convert simple hashtags to template format with escaped #
360
+ result = result.replace(simpleHashtagPattern, (_match, tag) => createPlaceholder(`{hashtag|\\#|${tag}}`));
361
+ // Escape remaining special characters (order matters: backslash first)
362
+ result = result
363
+ .replace(/\\/g, "\\\\")
364
+ .replace(/\|/g, "\\|")
365
+ .replace(/\{/g, "\\{")
366
+ .replace(/\}/g, "\\}")
367
+ .replace(/@/g, "\\@")
368
+ .replace(/\[/g, "\\[")
369
+ .replace(/\]/g, "\\]")
370
+ .replace(/\(/g, "\\(")
371
+ .replace(/\)/g, "\\)")
372
+ .replace(/</g, "\\<")
373
+ .replace(/>/g, "\\>")
374
+ .replace(/#/g, "\\#")
375
+ .replace(/\*/g, "\\*")
376
+ .replace(/_/g, "\\_")
377
+ .replace(/~/g, "\\~");
378
+ // Restore placeholders with preserved/transformed content
379
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: null chars are intentional placeholders
380
+ result = result.replace(/\x00(\d+)\x00/g, (_match, index) => preserved[Number(index)]);
381
+ return result;
382
+ }
383
+ function buildPostBody(params) {
384
+ const { personUrn, payload, uploadedAssets } = params;
385
+ const basePost = {
386
+ author: personUrn,
387
+ commentary: escapeLinkedInText(payload.text),
388
+ visibility: payload.visibility,
389
+ distribution: {
390
+ feedDistribution: "MAIN_FEED",
391
+ targetEntities: [],
392
+ thirdPartyDistributionChannels: [],
393
+ },
394
+ lifecycleState: "PUBLISHED",
395
+ isReshareDisabledByAuthor: false,
396
+ };
397
+ if (uploadedAssets.length === 0) {
398
+ return basePost;
399
+ }
400
+ if (uploadedAssets.length > 1) {
401
+ throw new Error("LinkedIn Posts API currently supports only a single media asset per post");
402
+ }
403
+ const asset = uploadedAssets[0];
404
+ const mediaContent = {
405
+ id: asset.assetUrn,
406
+ };
407
+ if (asset.title) {
408
+ mediaContent.title = asset.title;
409
+ }
410
+ if (asset.description && asset.mediaType === "image") {
411
+ mediaContent.altText = asset.description;
412
+ }
413
+ return {
414
+ ...basePost,
415
+ content: {
416
+ media: mediaContent,
417
+ },
418
+ };
419
+ }
420
+ async function safeJson(response) {
421
+ try {
422
+ return await response.json();
423
+ }
424
+ catch {
425
+ return undefined;
426
+ }
427
+ }
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const social_linkedin_1 = require("./social.linkedin");
4
+ describe("escapeLinkedInText", () => {
5
+ describe("special character escaping", () => {
6
+ it("escapes backslash", () => {
7
+ expect((0, social_linkedin_1.escapeLinkedInText)("path\\to\\file")).toBe("path\\\\to\\\\file");
8
+ });
9
+ it("escapes pipe", () => {
10
+ expect((0, social_linkedin_1.escapeLinkedInText)("a|b")).toBe("a\\|b");
11
+ });
12
+ it("escapes curly braces", () => {
13
+ expect((0, social_linkedin_1.escapeLinkedInText)("{value}")).toBe("\\{value\\}");
14
+ });
15
+ it("escapes at symbol", () => {
16
+ expect((0, social_linkedin_1.escapeLinkedInText)("email@example.com")).toBe("email\\@example.com");
17
+ });
18
+ it("escapes square brackets", () => {
19
+ expect((0, social_linkedin_1.escapeLinkedInText)("[item]")).toBe("\\[item\\]");
20
+ });
21
+ it("escapes parentheses", () => {
22
+ expect((0, social_linkedin_1.escapeLinkedInText)("(note)")).toBe("\\(note\\)");
23
+ });
24
+ it("escapes angle brackets", () => {
25
+ expect((0, social_linkedin_1.escapeLinkedInText)("<html>")).toBe("\\<html\\>");
26
+ });
27
+ it("escapes hash when not a hashtag", () => {
28
+ expect((0, social_linkedin_1.escapeLinkedInText)("item #1")).toBe("item \\#1");
29
+ });
30
+ it("escapes asterisk", () => {
31
+ expect((0, social_linkedin_1.escapeLinkedInText)("*bold*")).toBe("\\*bold\\*");
32
+ });
33
+ it("escapes underscore", () => {
34
+ expect((0, social_linkedin_1.escapeLinkedInText)("_italic_")).toBe("\\_italic\\_");
35
+ });
36
+ it("escapes tilde", () => {
37
+ expect((0, social_linkedin_1.escapeLinkedInText)("~strikethrough~")).toBe("\\~strikethrough\\~");
38
+ });
39
+ it("escapes multiple special characters together", () => {
40
+ expect((0, social_linkedin_1.escapeLinkedInText)("Check out (this) & [that]!")).toBe("Check out \\(this\\) & \\[that\\]!");
41
+ });
42
+ });
43
+ describe("mentions preservation", () => {
44
+ it("preserves person mention", () => {
45
+ const input = "Hello @[John Doe](urn:li:person:123456)!";
46
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
47
+ expect(result).toBe("Hello @[John Doe](urn:li:person:123456)!");
48
+ });
49
+ it("preserves organization mention", () => {
50
+ const input = "Check out @[DevtestCo](urn:li:organization:2414183)";
51
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
52
+ expect(result).toBe("Check out @[DevtestCo](urn:li:organization:2414183)");
53
+ });
54
+ it("preserves mention without fallback text", () => {
55
+ const input = "Mention @[](urn:li:person:123) here";
56
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
57
+ expect(result).toBe("Mention @[](urn:li:person:123) here");
58
+ });
59
+ it("preserves multiple mentions", () => {
60
+ const input = "Thanks @[Alice](urn:li:person:111) and @[Bob](urn:li:person:222)!";
61
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
62
+ expect(result).toBe("Thanks @[Alice](urn:li:person:111) and @[Bob](urn:li:person:222)!");
63
+ });
64
+ it("escapes text around mentions", () => {
65
+ const input = "(Hello) @[John](urn:li:person:123) [world]";
66
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
67
+ expect(result).toBe("\\(Hello\\) @[John](urn:li:person:123) \\[world\\]");
68
+ });
69
+ });
70
+ describe("hashtag template preservation", () => {
71
+ it("preserves hashtag template with escaped hash", () => {
72
+ const input = "Check {hashtag|\\#|MyTag}";
73
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
74
+ expect(result).toBe("Check {hashtag|\\#|MyTag}");
75
+ });
76
+ it("preserves hashtag template with unescaped hash", () => {
77
+ const input = "Check {hashtag|#|MyTag}";
78
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
79
+ expect(result).toBe("Check {hashtag|#|MyTag}");
80
+ });
81
+ it("preserves hashtag template with fullwidth hash", () => {
82
+ const input = "Check {hashtag|#|MyTag}";
83
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
84
+ expect(result).toBe("Check {hashtag|#|MyTag}");
85
+ });
86
+ it("preserves multiple hashtag templates", () => {
87
+ const input = "{hashtag|\\#|Tag1} and {hashtag|\\#|Tag2}";
88
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
89
+ expect(result).toBe("{hashtag|\\#|Tag1} and {hashtag|\\#|Tag2}");
90
+ });
91
+ });
92
+ describe("simple hashtag conversion", () => {
93
+ it("converts simple hashtag to template format", () => {
94
+ const input = "Check out #MyHashtag";
95
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
96
+ expect(result).toBe("Check out {hashtag|\\#|MyHashtag}");
97
+ });
98
+ it("converts multiple simple hashtags", () => {
99
+ const input = "#Hello #World";
100
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
101
+ expect(result).toBe("{hashtag|\\#|Hello} {hashtag|\\#|World}");
102
+ });
103
+ it("converts hashtag with accented characters", () => {
104
+ const input = "#Café #Naïve";
105
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
106
+ expect(result).toBe("{hashtag|\\#|Café} {hashtag|\\#|Naïve}");
107
+ });
108
+ it("handles hashtag at end of text", () => {
109
+ const input = "Great post #Amazing";
110
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
111
+ expect(result).toBe("Great post {hashtag|\\#|Amazing}");
112
+ });
113
+ it("does not convert hash followed by number only", () => {
114
+ const input = "Item #1 is great";
115
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
116
+ expect(result).toBe("Item \\#1 is great");
117
+ });
118
+ });
119
+ describe("mixed content", () => {
120
+ it("handles mentions, hashtags, and special chars together", () => {
121
+ const input = "Hello @[John](urn:li:person:123)! Check out #Tech & (amazing) *stuff*";
122
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
123
+ expect(result).toBe("Hello @[John](urn:li:person:123)! Check out {hashtag|\\#|Tech} & \\(amazing\\) \\*stuff\\*");
124
+ });
125
+ it("handles pre-formatted hashtag template with mentions", () => {
126
+ const input = "@[Company](urn:li:organization:123) is doing {hashtag|\\#|GreatThings}!";
127
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
128
+ expect(result).toBe("@[Company](urn:li:organization:123) is doing {hashtag|\\#|GreatThings}!");
129
+ });
130
+ it("preserves newlines and escapes special chars", () => {
131
+ const input = "Line 1 (note)\nLine 2 [item]";
132
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
133
+ expect(result).toBe("Line 1 \\(note\\)\nLine 2 \\[item\\]");
134
+ });
135
+ });
136
+ describe("edge cases", () => {
137
+ it("handles empty string", () => {
138
+ expect((0, social_linkedin_1.escapeLinkedInText)("")).toBe("");
139
+ });
140
+ it("handles plain text without special characters", () => {
141
+ const input = "Hello world this is plain text";
142
+ expect((0, social_linkedin_1.escapeLinkedInText)(input)).toBe("Hello world this is plain text");
143
+ });
144
+ it("handles text with only special characters", () => {
145
+ expect((0, social_linkedin_1.escapeLinkedInText)("@#$")).toBe("\\@\\#$");
146
+ });
147
+ it("does not double-escape already escaped backslashes in mentions", () => {
148
+ const input = "@[Test\\Name](urn:li:person:123)";
149
+ const result = (0, social_linkedin_1.escapeLinkedInText)(input);
150
+ // The mention is preserved as-is, including internal backslash
151
+ expect(result).toBe("@[Test\\Name](urn:li:person:123)");
152
+ });
153
+ });
154
+ describe("real-world posts", () => {
155
+ it("handles a full LinkedIn post with hashtags and special characters", () => {
156
+ const input = `I tried that prompt going around lately: "Draw a picture of how I'm treating you."
157
+
158
+
159
+ My result was just a happy little bot (picture included).
160
+
161
+
162
+ But when I scrolled through LinkedIn, I noticed something different. People were posting unhappy bots: overworked assistants and robots in chains. There was a lot of "you did it wrong" or "fix it" energy.
163
+
164
+
165
+ Here is the thing: I don't anthropomorphize AI. I don't chat with it about my day. I use it as a tool for specific tasks. Yet, I still find myself typing "please" and "thank you" in my prompts.
166
+
167
+
168
+ I am the same way with games. In RPGs, I almost always pick the "good guy" route. I tried to play the renegade path in Mass Effect once and couldn't even finish it. It just felt wrong.
169
+
170
+
171
+ Contrast that with a trend I've been seeing lately. There are papers and plenty of anecdotes suggesting that negative feedback, harsh wording, or even subtle "threats" can actually squeeze better answers out of these models.
172
+
173
+
174
+ That creates a weird tension for me:
175
+
176
+
177
+ Intellectually, I know adversarial prompting can work.
178
+ Practically, I can't bring myself to be mean to what is essentially linear algebra wrapped in a UI.
179
+ Personally, I don't think training myself to be rude, even to a tool, is a habit I want to cultivate.
180
+
181
+
182
+ Maybe the real optimization isn't about being harsh, but being clear. Well-scoped tasks, concrete constraints, and explicit feedback usually do the heavy lifting anyway.
183
+
184
+
185
+ I'm curious how others handle this, especially if you spend most of day in AI tools:
186
+
187
+
188
+ Do you consciously change your tone to get better results?
189
+ Have you actually seen consistent gains from theating or adversarial prompts?
190
+ Do you think how we talk to AI will eventually bleed into how we talk to humans?
191
+
192
+
193
+ #AI #LLM #PromptEngineering`;
194
+ const expected = `I tried that prompt going around lately: "Draw a picture of how I'm treating you."
195
+
196
+
197
+ My result was just a happy little bot \\(picture included\\).
198
+
199
+
200
+ But when I scrolled through LinkedIn, I noticed something different. People were posting unhappy bots: overworked assistants and robots in chains. There was a lot of "you did it wrong" or "fix it" energy.
201
+
202
+
203
+ Here is the thing: I don't anthropomorphize AI. I don't chat with it about my day. I use it as a tool for specific tasks. Yet, I still find myself typing "please" and "thank you" in my prompts.
204
+
205
+
206
+ I am the same way with games. In RPGs, I almost always pick the "good guy" route. I tried to play the renegade path in Mass Effect once and couldn't even finish it. It just felt wrong.
207
+
208
+
209
+ Contrast that with a trend I've been seeing lately. There are papers and plenty of anecdotes suggesting that negative feedback, harsh wording, or even subtle "threats" can actually squeeze better answers out of these models.
210
+
211
+
212
+ That creates a weird tension for me:
213
+
214
+
215
+ Intellectually, I know adversarial prompting can work.
216
+ Practically, I can't bring myself to be mean to what is essentially linear algebra wrapped in a UI.
217
+ Personally, I don't think training myself to be rude, even to a tool, is a habit I want to cultivate.
218
+
219
+
220
+ Maybe the real optimization isn't about being harsh, but being clear. Well-scoped tasks, concrete constraints, and explicit feedback usually do the heavy lifting anyway.
221
+
222
+
223
+ I'm curious how others handle this, especially if you spend most of day in AI tools:
224
+
225
+
226
+ Do you consciously change your tone to get better results?
227
+ Have you actually seen consistent gains from theating or adversarial prompts?
228
+ Do you think how we talk to AI will eventually bleed into how we talk to humans?
229
+
230
+
231
+ {hashtag|\\#|AI} {hashtag|\\#|LLM} {hashtag|\\#|PromptEngineering}`;
232
+ expect((0, social_linkedin_1.escapeLinkedInText)(input)).toBe(expected);
233
+ });
234
+ });
235
+ });