@venturekit-pro/social 0.0.0-dev.20260602192622

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 (53) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +134 -0
  3. package/dist/adapter.d.ts +46 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +15 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/adapters/facebook.d.ts +25 -0
  8. package/dist/adapters/facebook.d.ts.map +1 -0
  9. package/dist/adapters/facebook.js +95 -0
  10. package/dist/adapters/facebook.js.map +1 -0
  11. package/dist/adapters/index.d.ts +23 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +19 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/instagram.d.ts +25 -0
  16. package/dist/adapters/instagram.d.ts.map +1 -0
  17. package/dist/adapters/instagram.js +113 -0
  18. package/dist/adapters/instagram.js.map +1 -0
  19. package/dist/adapters/linkedin.d.ts +28 -0
  20. package/dist/adapters/linkedin.d.ts.map +1 -0
  21. package/dist/adapters/linkedin.js +121 -0
  22. package/dist/adapters/linkedin.js.map +1 -0
  23. package/dist/adapters/x.d.ts +21 -0
  24. package/dist/adapters/x.d.ts.map +1 -0
  25. package/dist/adapters/x.js +80 -0
  26. package/dist/adapters/x.js.map +1 -0
  27. package/dist/http.d.ts +40 -0
  28. package/dist/http.d.ts.map +1 -0
  29. package/dist/http.js +127 -0
  30. package/dist/http.js.map +1 -0
  31. package/dist/index.d.ts +28 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +27 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/migrations/vk_social_0001_init.sql +121 -0
  36. package/dist/path.d.ts +9 -0
  37. package/dist/path.d.ts.map +1 -0
  38. package/dist/path.js +17 -0
  39. package/dist/path.js.map +1 -0
  40. package/dist/registry.d.ts +32 -0
  41. package/dist/registry.d.ts.map +1 -0
  42. package/dist/registry.js +42 -0
  43. package/dist/registry.js.map +1 -0
  44. package/dist/types.d.ts +229 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +57 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/validation.d.ts +17 -0
  49. package/dist/validation.d.ts.map +1 -0
  50. package/dist/validation.js +157 -0
  51. package/dist/validation.js.map +1 -0
  52. package/package.json +78 -0
  53. package/src/migrations/vk_social_0001_init.sql +121 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Generic post-validation. Adapters compose this with their per-
3
+ * platform `SocialPlatformConstraints` to surface a uniform error
4
+ * shape without re-implementing every check four times.
5
+ *
6
+ * Returns BOTH errors AND warnings. Callers MUST block publish on
7
+ * `severity === 'error'`; warnings are informational ("you only
8
+ * have 1 hashtag, LinkedIn recommends 2-5").
9
+ */
10
+ const HASHTAG_PATTERN = /#[\w\u00C0-\u024F\u4E00-\u9FFF]+/g;
11
+ const URL_PATTERN = /^https?:\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*)$/;
12
+ export function validatePost(post, constraints) {
13
+ const issues = [];
14
+ // ─── Body length ──────────────────────────────────────────────────
15
+ const body = post.body ?? '';
16
+ const bodyLen = [...body].length; // grapheme-ish count via spread; better than .length for non-Latin
17
+ if (constraints.minBodyChars > 0 && bodyLen === 0) {
18
+ issues.push({
19
+ code: 'body_empty',
20
+ severity: 'error',
21
+ message: 'Post body is empty.',
22
+ });
23
+ }
24
+ else if (bodyLen < constraints.minBodyChars) {
25
+ issues.push({
26
+ code: 'body_too_short',
27
+ severity: 'error',
28
+ message: `Body too short (${bodyLen} chars; minimum ${constraints.minBodyChars}).`,
29
+ });
30
+ }
31
+ if (bodyLen > constraints.maxBodyChars) {
32
+ issues.push({
33
+ code: 'body_too_long',
34
+ severity: 'error',
35
+ message: `Body too long (${bodyLen} chars; max ${constraints.maxBodyChars}).`,
36
+ });
37
+ }
38
+ // ─── Hashtags ─────────────────────────────────────────────────────
39
+ const hashtags = body.match(HASHTAG_PATTERN) ?? [];
40
+ if (hashtags.length < constraints.minHashtags) {
41
+ issues.push({
42
+ code: 'hashtag_count_low',
43
+ severity: 'warning',
44
+ message: `Hashtag count below recommendation (${hashtags.length}; min ${constraints.minHashtags}).`,
45
+ });
46
+ }
47
+ if (hashtags.length > constraints.maxHashtags) {
48
+ issues.push({
49
+ code: 'hashtag_count_high',
50
+ severity: 'warning',
51
+ message: `Hashtag count above recommendation (${hashtags.length}; max ${constraints.maxHashtags}).`,
52
+ });
53
+ }
54
+ // ─── Media presence + count ──────────────────────────────────────
55
+ const media = post.media ?? [];
56
+ if (constraints.maxMediaCount === 0 && media.length > 0) {
57
+ issues.push({
58
+ code: 'media_count_high',
59
+ severity: 'error',
60
+ message: 'Platform does not accept media attachments.',
61
+ });
62
+ }
63
+ if (constraints.maxMediaCount > 0 &&
64
+ media.length > constraints.maxMediaCount) {
65
+ issues.push({
66
+ code: 'media_count_high',
67
+ severity: 'error',
68
+ message: `Too many media items (${media.length}; max ${constraints.maxMediaCount}).`,
69
+ });
70
+ }
71
+ // Some platforms (e.g. Instagram) require media on every post.
72
+ if (constraints.allowedMimeTypes.length > 0 &&
73
+ constraints.maxMediaCount > 0 &&
74
+ media.length === 0 &&
75
+ requiresMedia(constraints)) {
76
+ issues.push({
77
+ code: 'media_required',
78
+ severity: 'error',
79
+ message: 'Platform requires at least one media attachment.',
80
+ });
81
+ }
82
+ // ─── Per-media validation ────────────────────────────────────────
83
+ for (const m of media) {
84
+ if (constraints.allowedMimeTypes.length > 0 &&
85
+ !constraints.allowedMimeTypes.includes(m.mimeType)) {
86
+ issues.push({
87
+ code: 'media_mime_unsupported',
88
+ severity: 'error',
89
+ message: `Unsupported MIME type: ${m.mimeType}. Allowed: ${constraints.allowedMimeTypes.join(', ')}.`,
90
+ });
91
+ }
92
+ if (constraints.maxMediaSizeBytes > 0 &&
93
+ m.sizeBytes !== undefined &&
94
+ m.sizeBytes > constraints.maxMediaSizeBytes) {
95
+ issues.push({
96
+ code: 'media_too_large',
97
+ severity: 'error',
98
+ message: `Media too large (${m.sizeBytes} bytes; max ${constraints.maxMediaSizeBytes}).`,
99
+ });
100
+ }
101
+ if (constraints.allowedAspectRatios.length > 0 &&
102
+ m.width !== undefined &&
103
+ m.height !== undefined) {
104
+ const ratio = simplifyRatio(m.width, m.height);
105
+ if (!constraints.allowedAspectRatios.includes(ratio)) {
106
+ issues.push({
107
+ code: 'aspect_ratio_unsupported',
108
+ severity: 'warning',
109
+ message: `Aspect ratio ${ratio} not in allow-list (${constraints.allowedAspectRatios.join(', ')}).`,
110
+ });
111
+ }
112
+ }
113
+ }
114
+ // ─── Link ────────────────────────────────────────────────────────
115
+ if (post.link && !URL_PATTERN.test(post.link)) {
116
+ issues.push({
117
+ code: 'link_invalid',
118
+ severity: 'error',
119
+ message: `Link does not look like a valid URL: ${post.link}`,
120
+ });
121
+ }
122
+ const ok = !issues.some((i) => i.severity === 'error');
123
+ return { ok, issues };
124
+ }
125
+ // ─── Internal helpers ──────────────────────────────────────────────────
126
+ /**
127
+ * Whether the platform refuses text-only posts. Reads the explicit
128
+ * `requiresMedia` flag from the constraints (defaults to `false`).
129
+ *
130
+ * The flag is decoupled from `minBodyChars` because Instagram requires
131
+ * BOTH a non-empty caption AND a media item — a heuristic of "media
132
+ * required when minBodyChars === 0" would miss that case.
133
+ */
134
+ function requiresMedia(c) {
135
+ return c.requiresMedia === true;
136
+ }
137
+ /**
138
+ * Reduce `(width, height)` to a normalized `W:H` ratio string.
139
+ * Matches the format used in `allowedAspectRatios`.
140
+ */
141
+ export function simplifyRatio(width, height) {
142
+ if (width <= 0 || height <= 0)
143
+ return `${width}:${height}`;
144
+ const g = gcd(width, height);
145
+ return `${width / g}:${height / g}`;
146
+ }
147
+ function gcd(a, b) {
148
+ let x = Math.abs(Math.trunc(a));
149
+ let y = Math.abs(Math.trunc(b));
150
+ while (y) {
151
+ const t = y;
152
+ y = x % y;
153
+ x = t;
154
+ }
155
+ return x || 1;
156
+ }
157
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@venturekit-pro/social",
3
+ "version": "0.0.0-dev.20260602192622",
4
+ "description": "Social-platform publishing adapters + catalog for VentureKit applications",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src/migrations/*.sql"
11
+ ],
12
+ "vk": {
13
+ "migrations": "src/migrations"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/venturekit-dev/venturekit.private.git",
18
+ "directory": "packages/pro/social"
19
+ },
20
+ "publishConfig": {
21
+ "registry": "https://registry.npmjs.org",
22
+ "access": "public"
23
+ },
24
+ "license": "SEE LICENSE IN ../LICENSE",
25
+ "licenseHeader": "VentureKit Pro Commercial License — production use requires a valid VentureKit Pro license key. See https://venturekit.dev/pricing.",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/index.js",
29
+ "types": "./dist/index.d.ts"
30
+ },
31
+ "./adapters/linkedin": {
32
+ "import": "./dist/adapters/linkedin.js",
33
+ "types": "./dist/adapters/linkedin.d.ts"
34
+ },
35
+ "./adapters/x": {
36
+ "import": "./dist/adapters/x.js",
37
+ "types": "./dist/adapters/x.d.ts"
38
+ },
39
+ "./adapters/facebook": {
40
+ "import": "./dist/adapters/facebook.js",
41
+ "types": "./dist/adapters/facebook.d.ts"
42
+ },
43
+ "./adapters/instagram": {
44
+ "import": "./dist/adapters/instagram.js",
45
+ "types": "./dist/adapters/instagram.d.ts"
46
+ }
47
+ },
48
+ "keywords": [
49
+ "venturekit",
50
+ "saas",
51
+ "social",
52
+ "linkedin",
53
+ "twitter",
54
+ "meta",
55
+ "publishing"
56
+ ],
57
+ "dependencies": {
58
+ "@venturekit/core": "0.0.0-dev.20260602192622"
59
+ },
60
+ "peerDependencies": {
61
+ "@venturekit/data": "0.0.0-dev.20260602192622"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@venturekit/data": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^25.6.0",
70
+ "@venturekit/data": "0.0.0-dev.20260602192622",
71
+ "typescript": "^5.3.0"
72
+ },
73
+ "scripts": {
74
+ "build": "tsc && node -e \"require('fs').cpSync('src/migrations','dist/migrations',{recursive:true})\"",
75
+ "dev": "tsc --watch",
76
+ "clean": "rm -rf dist"
77
+ }
78
+ }
@@ -0,0 +1,121 @@
1
+ -- @venturekit-pro/social — initial schema.
2
+ --
3
+ -- Two tables:
4
+ --
5
+ -- social_platforms — platform-admin-owned catalog of every
6
+ -- social network the package supports.
7
+ -- One row per (provider, adapter version).
8
+ -- The four v1 platforms are seeded by the
9
+ -- bottom of this migration.
10
+ --
11
+ -- tenant_social_platforms — per-tenant enable/disable + author-ref
12
+ -- pointer. NO cadence / fan-out / locale
13
+ -- fields here — those are calling-app
14
+ -- concerns. Apps that want per-platform
15
+ -- cadence settings model that in their
16
+ -- own schema.
17
+ --
18
+ -- Append-only fields are not used here; the rows are tenant-mutable
19
+ -- (toggle enabled, swap author_ref). Audit-worthy changes flow into
20
+ -- `@venturekit-pro/audit`'s `audit_events` separately.
21
+
22
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
23
+
24
+ -- ─── social_platforms (catalog) ─────────────────────────────────────
25
+
26
+ DO $$
27
+ BEGIN
28
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'vk_social_platform_status') THEN
29
+ CREATE TYPE vk_social_platform_status AS ENUM (
30
+ 'available',
31
+ 'deprecated',
32
+ 'disabled'
33
+ );
34
+ END IF;
35
+ END
36
+ $$;
37
+
38
+ CREATE TABLE IF NOT EXISTS social_platforms (
39
+ key varchar(64) PRIMARY KEY,
40
+ display_name varchar(255) NOT NULL,
41
+ -- Lifecycle: `available` rows render in pickers; `deprecated` ones
42
+ -- are still callable but the UI nudges a swap; `disabled` rows
43
+ -- refuse to publish at the adapter resolution boundary.
44
+ status vk_social_platform_status NOT NULL DEFAULT 'available',
45
+ -- Human-readable description for picker rows.
46
+ description text,
47
+ -- Free-form metadata (icon name, brand color, etc). Apps consume
48
+ -- this for UI rendering without the package needing to enumerate
49
+ -- every visual property.
50
+ meta jsonb NOT NULL DEFAULT '{}'::jsonb,
51
+ -- When the row was first introduced + lifecycle timestamps. Used
52
+ -- by the platform-admin UI to render badges.
53
+ introduced_at timestamptz NOT NULL DEFAULT now(),
54
+ deprecated_at timestamptz,
55
+ disabled_at timestamptz,
56
+ created_at timestamptz NOT NULL DEFAULT now(),
57
+ updated_at timestamptz NOT NULL DEFAULT now()
58
+ );
59
+
60
+ -- ─── tenant_social_platforms (per-tenant) ───────────────────────────
61
+ --
62
+ -- Apps that want per-platform-per-tenant settings (cadence, locales,
63
+ -- fan-out weeks) extend THIS table in their own migration. We keep
64
+ -- the package-owned columns to the strict minimum: enabled flag,
65
+ -- author ref (the platform-side id we publish into), and optional
66
+ -- vendor metadata.
67
+
68
+ CREATE TABLE IF NOT EXISTS tenant_social_platforms (
69
+ tenant_id uuid NOT NULL,
70
+ platform_key varchar(64) NOT NULL REFERENCES social_platforms(key) ON DELETE RESTRICT,
71
+ enabled boolean NOT NULL DEFAULT TRUE,
72
+ -- Platform-side identifier this tenant publishes to. URN / page id /
73
+ -- ig user id. Required when enabled = TRUE; the application enforces
74
+ -- that invariant at the service boundary (we don't put a CHECK here
75
+ -- because the empty string is sometimes used as a placeholder
76
+ -- during onboarding wizards).
77
+ author_ref varchar(512) NOT NULL DEFAULT '',
78
+ -- Free-form metadata stamped by the tenant ops flow (locale,
79
+ -- scopes, etc). Adapters ignore unless documented.
80
+ meta jsonb NOT NULL DEFAULT '{}'::jsonb,
81
+ created_at timestamptz NOT NULL DEFAULT now(),
82
+ updated_at timestamptz NOT NULL DEFAULT now(),
83
+ PRIMARY KEY (tenant_id, platform_key)
84
+ );
85
+
86
+ CREATE INDEX IF NOT EXISTS tenant_social_platforms_enabled_idx
87
+ ON tenant_social_platforms (tenant_id)
88
+ WHERE enabled;
89
+
90
+ -- ─── Catalog seed ───────────────────────────────────────────────────
91
+ --
92
+ -- Idempotent (ON CONFLICT DO NOTHING) so re-running the migration on
93
+ -- an already-seeded DB is a no-op. Tenants who want to add custom
94
+ -- adapters (e.g. Mastodon, Bluesky) insert their own row at app boot.
95
+
96
+ INSERT INTO social_platforms (key, display_name, description, meta) VALUES
97
+ (
98
+ 'linkedin',
99
+ 'LinkedIn',
100
+ 'Professional network. UGC Posts API (v2). Author URN required.',
101
+ jsonb_build_object('iconKey', 'linkedin', 'brandColor', '#0A66C2')
102
+ ),
103
+ (
104
+ 'x',
105
+ 'X',
106
+ 'Microblogging (formerly Twitter). v2 Tweets API. 280-char body.',
107
+ jsonb_build_object('iconKey', 'x', 'brandColor', '#000000')
108
+ ),
109
+ (
110
+ 'facebook',
111
+ 'Facebook',
112
+ 'Meta Graph API for Pages. Page access token required.',
113
+ jsonb_build_object('iconKey', 'facebook', 'brandColor', '#1877F2')
114
+ ),
115
+ (
116
+ 'instagram',
117
+ 'Instagram',
118
+ 'Meta Graph Content Publishing API (Business / Creator accounts).',
119
+ jsonb_build_object('iconKey', 'instagram', 'brandColor', '#E4405F')
120
+ )
121
+ ON CONFLICT (key) DO NOTHING;