@publishq/sdk 0.0.1 → 0.0.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/dist/index.js CHANGED
@@ -816,7 +816,7 @@ var Posts = class {
816
816
  /**
817
817
  * List posts
818
818
  *
819
- * Returns a paginated list of posts.
819
+ * Returns a paginated list of the posts you created through PublishQ. Posts imported from a connected platform are excluded by default; pass includeImported=true to get both, or onlyImported=true for only the imported ones.
820
820
  */
821
821
  static listPosts(options) {
822
822
  return (options?.client ?? client).get({
@@ -978,7 +978,7 @@ var Media = class {
978
978
  /**
979
979
  * Upload media
980
980
  *
981
- * Upload one media file with multipart form data. The returned media ID can then be attached to posts.
981
+ * Upload a file smaller than 100 MB in a single request. It is the simplest way to upload: one call returns a media ID you can attach to posts. For bigger files, use Create media upload.
982
982
  */
983
983
  static uploadMedia(options) {
984
984
  return (options.client ?? client).post({
@@ -1004,6 +1004,39 @@ var Media = class {
1004
1004
  ...options
1005
1005
  });
1006
1006
  }
1007
+ /**
1008
+ * Create media upload
1009
+ *
1010
+ * Upload a file of up to 4 GB (500 MB on the Free plan) straight to storage:
1011
+ *
1012
+ * 1. Call this endpoint with `filename`, `contentType` and `fileSize`. You get back an `id` and an `uploadUrl`.
1013
+ * 2. `PUT` the file to `uploadUrl` with the same `Content-Type` header. Storage answers `200`, or `403` if the file is not exactly `fileSize` bytes.
1014
+ * 3. Optional: call Complete media upload to add the file to your media library now.
1015
+ * 4. Put the `id` in `mediaIds` when you create or update a post.
1016
+ */
1017
+ static createMediaUpload(options) {
1018
+ return (options.client ?? client).post({
1019
+ security: [{ scheme: "bearer", type: "http" }],
1020
+ url: "/media/uploads",
1021
+ ...options,
1022
+ headers: {
1023
+ "Content-Type": "application/json",
1024
+ ...options.headers
1025
+ }
1026
+ });
1027
+ }
1028
+ /**
1029
+ * Complete media upload
1030
+ *
1031
+ * Optional. Adds a file uploaded through Create media upload to your media library now and returns it. Without this call the file is added the first time you use its id in a post, and an upload nobody completes or uses within 1 day expires.
1032
+ */
1033
+ static completeMediaUpload(options) {
1034
+ return (options.client ?? client).post({
1035
+ security: [{ scheme: "bearer", type: "http" }],
1036
+ url: "/media/uploads/{id}/complete",
1037
+ ...options
1038
+ });
1039
+ }
1007
1040
  };
1008
1041
 
1009
1042
  // src/generated/zod.gen.ts
@@ -1023,6 +1056,7 @@ var zApiErrorCode = z.enum([
1023
1056
  "post_not_editable",
1024
1057
  "media_not_found",
1025
1058
  "media_in_use",
1059
+ "media_upload_incomplete",
1026
1060
  "storage_limit_exceeded",
1027
1061
  "free_media_limit_exceeded",
1028
1062
  "free_pending_media_limit_exceeded",
@@ -1033,6 +1067,9 @@ var zApiErrorCode = z.enum([
1033
1067
  "invalid_file_extension",
1034
1068
  "social_accounts_not_found",
1035
1069
  "social_accounts_expired",
1070
+ "social_account_limit_exceeded",
1071
+ "platform_temporarily_unavailable",
1072
+ "platform_permission_missing",
1036
1073
  "platform_validation_failed",
1037
1074
  "posting_limits_exceeded",
1038
1075
  "scheduled_at_required",
@@ -1042,10 +1079,12 @@ var zApiErrorCode = z.enum([
1042
1079
  "workspace_not_found",
1043
1080
  "workspace_forbidden",
1044
1081
  "workspace_owner_required",
1082
+ "workspace_limit_exceeded",
1045
1083
  "workspace_account_not_found",
1046
1084
  "workspace_account_conflict",
1047
1085
  "workspace_member_not_found",
1048
1086
  "workspace_member_conflict",
1087
+ "team_member_limit_exceeded",
1049
1088
  "workspace_invitation_invalid",
1050
1089
  "workspace_invitation_expired"
1051
1090
  ]);
@@ -1254,6 +1293,56 @@ var zTwitterPlatformSpecificSettings = z.object({
1254
1293
  }).register(z.globalRegistry, {
1255
1294
  description: "X (Twitter) specific post settings. All fields are optional."
1256
1295
  });
1296
+ var zPostSource = z.enum(["PUBLISHQ", "IMPORTED"]).register(z.globalRegistry, {
1297
+ description: "Where the post came from. PUBLISHQ: created in PublishQ. IMPORTED: published directly on the platform and discovered during an analytics sync, so the post history stays complete."
1298
+ });
1299
+ var zPostAnalytics = z.object({
1300
+ views: z.int().check(z.gte(0)).register(z.globalRegistry, {
1301
+ description: "Views the platform reports for this post."
1302
+ }),
1303
+ likes: z.int().check(z.gte(0)).register(z.globalRegistry, {
1304
+ description: "Likes the platform reports for this post."
1305
+ }),
1306
+ comments: z.int().check(z.gte(0)).register(z.globalRegistry, {
1307
+ description: "Comments the platform reports for this post."
1308
+ }),
1309
+ shares: z.int().check(z.gte(0)).register(z.globalRegistry, {
1310
+ description: "Shares the platform reports for this post. 0 on platforms that do not disclose it (including YouTube)."
1311
+ }),
1312
+ extraMetrics: z.record(z.string(), z.int().check(z.gte(0))).register(z.globalRegistry, {
1313
+ description: "Metrics only this platform reports, keyed by metric name \u2014 Bluesky bookmarks, for example. Every metric the platform reports is always present, 0 included, so no key has to be guessed. Empty on platforms that expose nothing beyond the fields above, and a metric that already has its own field is never repeated here."
1314
+ }),
1315
+ syncedAt: z.iso.datetime().register(z.globalRegistry, {
1316
+ description: "When these numbers were last recorded. Metrics are refreshed in the background roughly once a day, and only stored when they change."
1317
+ })
1318
+ });
1319
+ var zPublishFailure = z.object({
1320
+ kind: z.enum([
1321
+ "RECONNECT",
1322
+ "MEDIA",
1323
+ "ACCOUNT_STATE",
1324
+ "TRANSIENT",
1325
+ "MAYBE_PUBLISHED",
1326
+ "UNKNOWN"
1327
+ ]).register(z.globalRegistry, {
1328
+ description: "What kind of action this failure needs. `RECONNECT`: the account has to be reconnected. `MEDIA`: the platform refused the attached file. `ACCOUNT_STATE`: the platform is blocking this account, so the fix is on the platform. `TRANSIENT`: the platform was unavailable and the same request can succeed unchanged. `MAYBE_PUBLISHED`: the platform reported an error for a post that may have gone out anyway. `UNKNOWN`: unrecognised, so read `errorMessage`."
1329
+ }),
1330
+ title: z.string().register(z.globalRegistry, {
1331
+ description: "Short label for this failure."
1332
+ }),
1333
+ whatHappened: z.string().register(z.globalRegistry, {
1334
+ description: "One sentence about what the platform did."
1335
+ }),
1336
+ whatToDo: z.string().register(z.globalRegistry, {
1337
+ description: "One sentence naming the next action."
1338
+ }),
1339
+ canRetry: z.boolean().register(z.globalRegistry, {
1340
+ description: "Whether retrying can succeed. False when the platform is blocking the account, since the same request will be refused the same way."
1341
+ }),
1342
+ checkPlatformFirst: z.boolean().register(z.globalRegistry, {
1343
+ description: "Whether the post may already be live. A publish is not idempotent and no platform accepts a deduplication key, so when this is true a retry can create a second copy: check the profile before retrying."
1344
+ })
1345
+ });
1257
1346
  var zSocialPlatform = z.enum([
1258
1347
  "TWITTER",
1259
1348
  "INSTAGRAM",
@@ -1278,8 +1367,8 @@ var zPostOverrides = z.object({
1278
1367
  thumbnailTimestamp: z.optional(z.int().check(z.gte(0)).register(z.globalRegistry, {
1279
1368
  description: "Video thumbnail timestamp in milliseconds. Applies to Instagram and TikTok."
1280
1369
  })),
1281
- mediaIds: z.optional(z.array(z.uuid()).register(z.globalRegistry, {
1282
- description: "Replacement media for this account. Completely replaces the post-level media."
1370
+ mediaIds: z.optional(z.array(z.uuid()).check(z.maxLength(35)).register(z.globalRegistry, {
1371
+ description: "Replacement media for this account. Completely replaces the post-level media. Up to 35."
1283
1372
  }))
1284
1373
  });
1285
1374
  var zPlatformSpecificSettings = z.union([
@@ -1471,6 +1560,12 @@ var zPostAccount = z.object({
1471
1560
  errorMessage: z.optional(z.string().register(z.globalRegistry, {
1472
1561
  description: "Error message from the platform if publishing failed for this account."
1473
1562
  })),
1563
+ failure: z.optional(z.intersection(zPublishFailure, z.unknown().register(z.globalRegistry, {
1564
+ description: "The same failure read as an action: what happened and the one thing to do about it. Present whenever status is FAILED, including when the platform gave no reason. The wording is what the app and the failure email show, so a client does not have to interpret `errorMessage` itself."
1565
+ }))),
1566
+ analytics: z.optional(z.intersection(zPostAnalytics, z.unknown().register(z.globalRegistry, {
1567
+ description: "Latest metrics recorded for this delivery. Present only for published posts on platforms with analytics support, once a background sync has run."
1568
+ }))),
1474
1569
  postOverrides: z.optional(zPostOverrides),
1475
1570
  platformSpecificSettings: z.optional(zPlatformSpecificSettings)
1476
1571
  });
@@ -1482,6 +1577,7 @@ var zPost = z.object({
1482
1577
  description: "The shared text content of the post."
1483
1578
  }),
1484
1579
  status: zPostStatus,
1580
+ source: zPostSource,
1485
1581
  errorMessage: z.optional(z.string().register(z.globalRegistry, {
1486
1582
  description: "Human-readable error summary when one or more accounts failed."
1487
1583
  })),
@@ -1523,8 +1619,8 @@ var zCreatePostRequest = z.object({
1523
1619
  content: z._default(z.optional(z.string().register(z.globalRegistry, {
1524
1620
  description: "The text content of the post, shared across all accounts unless overridden. Optional when media is attached (platform-dependent)."
1525
1621
  })), ""),
1526
- mediaIds: z.optional(z.array(z.uuid()).register(z.globalRegistry, {
1527
- description: "Media to attach to the post. Shared across all accounts unless an account provides its own mediaIds in postOverrides."
1622
+ mediaIds: z.optional(z.array(z.uuid()).check(z.maxLength(35)).register(z.globalRegistry, {
1623
+ description: "Media to attach to the post, up to 35. Shared across all accounts unless an account provides its own mediaIds in postOverrides. Accepts IDs from Upload media and from Create media upload."
1528
1624
  })),
1529
1625
  thumbnailTimestamp: z.optional(z.int().check(z.gte(0)).register(z.globalRegistry, {
1530
1626
  description: "Default video thumbnail timestamp in milliseconds for Instagram and TikTok. Can be overridden per account."
@@ -1533,7 +1629,7 @@ var zCreatePostRequest = z.object({
1533
1629
  description: "Accounts to post to. Each entry can optionally include content overrides and platform-specific settings."
1534
1630
  }),
1535
1631
  scheduledAt: z.optional(z.iso.datetime().register(z.globalRegistry, {
1536
- description: "Schedule the post for a future date. If omitted and publishNow is false or absent, the post is saved as a draft."
1632
+ description: "Schedule the post for a future date. If omitted and publishNow is false or absent, the post is saved as a draft. Accepts UTC (2026-04-01T12:00:00Z) or an explicit UTC offset (2026-04-01T14:00:00+02:00); a time without zone information is rejected."
1537
1633
  })),
1538
1634
  publishNow: z.optional(z.boolean().register(z.globalRegistry, {
1539
1635
  description: "Publish immediately. Cannot be combined with scheduledAt."
@@ -1546,10 +1642,10 @@ var zUpdatePostRequest = z.object({
1546
1642
  description: "New shared text content for the post."
1547
1643
  })),
1548
1644
  scheduledAt: z.optional(z.iso.datetime().register(z.globalRegistry, {
1549
- description: "New scheduled date for the post. Must be at least 1 minute in the future."
1645
+ description: "New scheduled date for the post. Must be at least 1 minute in the future. Accepts UTC (2026-04-15T14:00:00Z) or an explicit UTC offset (2026-04-15T16:00:00+02:00); a time without zone information is rejected."
1550
1646
  })),
1551
- mediaIds: z.optional(z.array(z.uuid()).register(z.globalRegistry, {
1552
- description: "Full replacement for the post media. If omitted, the existing media stays unchanged. If provided, it replaces the current media across all accounts in the post. Send an empty array to remove all media from the post."
1647
+ mediaIds: z.optional(z.array(z.uuid()).check(z.maxLength(35)).register(z.globalRegistry, {
1648
+ description: "Full replacement for the post media, up to 35. If omitted, the existing media stays unchanged. If provided, it replaces the current media across all accounts in the post. Send an empty array to remove all media from the post."
1553
1649
  })),
1554
1650
  thumbnailTimestamp: z.optional(z.int().check(z.gte(0)).register(z.globalRegistry, {
1555
1651
  description: "New video thumbnail timestamp in milliseconds for Instagram and TikTok media. Does not change which media files are attached. Combine with mediaIds if you want to replace media and set the new thumbnail timestamp in the same request."
@@ -1658,6 +1754,28 @@ var zMediaListResponse = z.object({
1658
1754
  var zUploadMediaRequest = z.object({
1659
1755
  file: z.string()
1660
1756
  });
1757
+ var zCreateMediaUploadRequest = z.object({
1758
+ filename: z.string().check(z.minLength(1), z.maxLength(1024)).register(z.globalRegistry, {
1759
+ description: "Name of the file you are uploading."
1760
+ }),
1761
+ contentType: z.string().check(z.minLength(1)).register(z.globalRegistry, {
1762
+ description: "Content type of the file. One of: image/jpeg, image/png, image/gif, image/webp, video/mp4, video/webm, video/quicktime, video/x-msvideo."
1763
+ }),
1764
+ fileSize: z.int().check(z.gt(0)).register(z.globalRegistry, {
1765
+ description: "Exact size of the file in bytes. Your plan's file size limit and your storage are checked before you upload, and storage refuses a file of any other size. Each platform's own size limits are checked when you attach the media to a post."
1766
+ })
1767
+ });
1768
+ var zMediaUpload = z.object({
1769
+ id: z.uuid().register(z.globalRegistry, {
1770
+ description: "Media ID of the file. Attach it to posts through mediaIds once the file is uploaded."
1771
+ }),
1772
+ uploadUrl: z.url().register(z.globalRegistry, {
1773
+ description: "Signed storage URL. PUT the file to it with the same Content-Type. It goes to storage, not to the PublishQ API, so send no Authorization header."
1774
+ }),
1775
+ expiresAt: z.iso.datetime().register(z.globalRegistry, {
1776
+ description: "When uploadUrl stops accepting the file, 1 hour after it is created."
1777
+ })
1778
+ });
1661
1779
  var zTikTokCreatorInfoResponse = z.object({
1662
1780
  privacyLevelOptions: z.array(z.string()).register(z.globalRegistry, {
1663
1781
  description: "Privacy levels available for this account. Use one of these values as `privacyLevel` in `platformSpecificSettings`."
@@ -1690,7 +1808,13 @@ var zListPostsQuery = z.object({
1690
1808
  })), 1),
1691
1809
  limit: z._default(z.optional(z.int().check(z.gt(0), z.lte(100)).register(z.globalRegistry, {
1692
1810
  description: "Page size. Max 100."
1693
- })), 20)
1811
+ })), 20),
1812
+ includeImported: z._default(z.optional(z.boolean().register(z.globalRegistry, {
1813
+ description: "Also return posts imported from a connected platform (posts with source IMPORTED). Defaults to false: only posts created through PublishQ are returned."
1814
+ })), false),
1815
+ onlyImported: z._default(z.optional(z.boolean().register(z.globalRegistry, {
1816
+ description: "Return ONLY posts imported from a connected platform. Cannot be combined with includeImported."
1817
+ })), false)
1694
1818
  });
1695
1819
  var zListPostsResponse = zPostsResponse;
1696
1820
  var zCreatePostBody = zCreatePostRequest;
@@ -1761,6 +1885,12 @@ var zGetMediaPath = z.object({
1761
1885
  id: z.uuid()
1762
1886
  });
1763
1887
  var zGetMediaResponse = zMedia;
1888
+ var zCreateMediaUploadBody = zCreateMediaUploadRequest;
1889
+ var zCreateMediaUploadResponse = zMediaUpload;
1890
+ var zCompleteMediaUploadPath = z.object({
1891
+ id: z.uuid()
1892
+ });
1893
+ var zCompleteMediaUploadResponse = zMedia;
1764
1894
 
1765
1895
  // src/index.ts
1766
1896
  var rawErrorBodies = /* @__PURE__ */ new WeakMap();
@@ -1862,12 +1992,92 @@ function createWorkspacesNamespace(client2) {
1862
1992
  get: (id) => preserveApiError(Workspaces.getWorkspace({ client: client2, path: { id } }))
1863
1993
  };
1864
1994
  }
1995
+ var UPLOAD_MIME_TYPE_BY_EXTENSION = {
1996
+ jpg: "image/jpeg",
1997
+ jpeg: "image/jpeg",
1998
+ png: "image/png",
1999
+ gif: "image/gif",
2000
+ webp: "image/webp",
2001
+ mp4: "video/mp4",
2002
+ webm: "video/webm",
2003
+ mov: "video/quicktime",
2004
+ avi: "video/x-msvideo"
2005
+ };
2006
+ function isUninformativeType(type) {
2007
+ return type === "" || type === "application/octet-stream";
2008
+ }
2009
+ function withInferredContentType(file) {
2010
+ const name = "name" in file ? file.name : void 0;
2011
+ if (!name || !isUninformativeType(file.type)) return file;
2012
+ const extension = name.split(".").pop()?.toLowerCase();
2013
+ const type = extension ? UPLOAD_MIME_TYPE_BY_EXTENSION[extension] : void 0;
2014
+ if (!type) return file;
2015
+ return new File([file], name, { type });
2016
+ }
2017
+ var STORAGE_UPLOAD_THRESHOLD_BYTES = 90 * 1024 * 1024;
2018
+ async function uploadThroughStorage(client2, file) {
2019
+ const created = await preserveApiError(
2020
+ Media.createMediaUpload({
2021
+ client: client2,
2022
+ body: {
2023
+ filename: "name" in file && file.name ? file.name : "upload",
2024
+ contentType: file.type,
2025
+ fileSize: file.size
2026
+ }
2027
+ })
2028
+ );
2029
+ if (!created.data) return created;
2030
+ const response = await fetch(created.data.uploadUrl, {
2031
+ method: "PUT",
2032
+ headers: { "Content-Type": file.type },
2033
+ body: file
2034
+ });
2035
+ if (!response.ok) {
2036
+ return {
2037
+ data: void 0,
2038
+ error: {
2039
+ error: {
2040
+ status: response.status,
2041
+ message: "Storage refused the file upload",
2042
+ mediaId: created.data.id,
2043
+ storageResponse: await response.text()
2044
+ }
2045
+ },
2046
+ request: new Request(created.data.uploadUrl, { method: "PUT" }),
2047
+ response
2048
+ };
2049
+ }
2050
+ return preserveApiError(
2051
+ Media.completeMediaUpload({ client: client2, path: { id: created.data.id } })
2052
+ );
2053
+ }
1865
2054
  function createMediaNamespace(client2) {
1866
2055
  return {
1867
2056
  /** List your uploaded and generated media files. Filter by id, type, source. Wraps `Media.listMedia`. */
1868
2057
  list: (query) => preserveApiError(Media.listMedia({ client: client2, query })),
1869
- /** Upload one media file (multipart) and get back a media ID to attach to posts. Wraps `Media.uploadMedia`. */
1870
- upload: (body) => preserveApiError(Media.uploadMedia({ client: client2, body })),
2058
+ /**
2059
+ * Upload one media file and get back a media ID to attach to posts. Wraps
2060
+ * `Media.uploadMedia`, or for files over `STORAGE_UPLOAD_THRESHOLD_BYTES`
2061
+ * runs `createUpload`, sends the file to storage and returns the finished
2062
+ * media from `completeUpload`. Both return the same `Media` object.
2063
+ *
2064
+ * The file's content type is filled in from its name when it has none, so a
2065
+ * file read off disk with `new File([buffer], 'photo.jpg')` is not sent as
2066
+ * `application/octet-stream` and refused. See `withInferredContentType`.
2067
+ */
2068
+ upload: (body) => {
2069
+ const file = withInferredContentType(body.file);
2070
+ if (file.size > STORAGE_UPLOAD_THRESHOLD_BYTES) {
2071
+ return uploadThroughStorage(client2, file);
2072
+ }
2073
+ return preserveApiError(
2074
+ Media.uploadMedia({ client: client2, body: { ...body, file } })
2075
+ );
2076
+ },
2077
+ /** Get a storage upload URL for a file of up to 4 GB. Send the file there, then use the returned media ID in posts. Wraps `Media.createMediaUpload`. */
2078
+ createUpload: (body) => preserveApiError(Media.createMediaUpload({ client: client2, body })),
2079
+ /** Add a file sent to a `createUpload` URL to the media library now and return it. Optional: using the ID in a post does the same. Wraps `Media.completeMediaUpload`. */
2080
+ completeUpload: (id) => preserveApiError(Media.completeMediaUpload({ client: client2, path: { id } })),
1871
2081
  /** Get one media file by ID. Wraps `Media.getMedia`. */
1872
2082
  get: (id) => preserveApiError(Media.getMedia({ client: client2, path: { id } }))
1873
2083
  };
@@ -1878,14 +2088,21 @@ export {
1878
2088
  Media as Media2,
1879
2089
  Posts,
1880
2090
  PublishQ,
2091
+ STORAGE_UPLOAD_THRESHOLD_BYTES,
1881
2092
  Workspaces,
1882
2093
  index_default as default,
2094
+ withInferredContentType,
1883
2095
  zAccount,
1884
2096
  zAccountsResponse,
1885
2097
  zApiError,
1886
2098
  zApiErrorCode,
1887
2099
  zApiKeyScope,
1888
2100
  zBlueskyPlatformSpecificSettings,
2101
+ zCompleteMediaUploadPath,
2102
+ zCompleteMediaUploadResponse,
2103
+ zCreateMediaUploadBody,
2104
+ zCreateMediaUploadRequest,
2105
+ zCreateMediaUploadResponse,
1889
2106
  zCreatePostAccount,
1890
2107
  zCreatePostBody,
1891
2108
  zCreatePostRequest,
@@ -1918,13 +2135,17 @@ export {
1918
2135
  zMediaListResponse,
1919
2136
  zMediaSource,
1920
2137
  zMediaType,
2138
+ zMediaUpload,
1921
2139
  zPagination,
1922
2140
  zPlatformSpecificSettings,
1923
2141
  zPost,
1924
2142
  zPostAccount,
2143
+ zPostAnalytics,
1925
2144
  zPostOverrides,
2145
+ zPostSource,
1926
2146
  zPostStatus,
1927
2147
  zPostsResponse,
2148
+ zPublishFailure,
1928
2149
  zRemovePostAccountPath,
1929
2150
  zRemovePostAccountResponse,
1930
2151
  zSocialPlatform,