@bubltec/mycota-social 0.1.1-next.18
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/LICENSE +21 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/capabilities.spec.d.ts +2 -0
- package/dist/capabilities.spec.d.ts.map +1 -0
- package/dist/capabilities.spec.js +12 -0
- package/dist/capabilities.spec.js.map +1 -0
- package/dist/fake.adapter.d.ts +9 -0
- package/dist/fake.adapter.d.ts.map +1 -0
- package/dist/fake.adapter.js +21 -0
- package/dist/fake.adapter.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/meta.adapter.d.ts +27 -0
- package/dist/meta.adapter.d.ts.map +1 -0
- package/dist/meta.adapter.js +84 -0
- package/dist/meta.adapter.js.map +1 -0
- package/dist/meta.adapter.spec.d.ts +2 -0
- package/dist/meta.adapter.spec.d.ts.map +1 -0
- package/dist/meta.adapter.spec.js +45 -0
- package/dist/meta.adapter.spec.js.map +1 -0
- package/dist/ports.d.ts +64 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/registry.d.ts +18 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +38 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.spec.d.ts +2 -0
- package/dist/registry.spec.d.ts.map +1 -0
- package/dist/registry.spec.js +40 -0
- package/dist/registry.spec.js.map +1 -0
- package/dist/tiktok.adapter.d.ts +28 -0
- package/dist/tiktok.adapter.d.ts.map +1 -0
- package/dist/tiktok.adapter.js +71 -0
- package/dist/tiktok.adapter.js.map +1 -0
- package/dist/x.adapter.d.ts +21 -0
- package/dist/x.adapter.d.ts.map +1 -0
- package/dist/x.adapter.js +59 -0
- package/dist/x.adapter.js.map +1 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Genome
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SocialCapability, SocialPlatform } from './ports.js';
|
|
2
|
+
/**
|
|
3
|
+
* Honest capability map for official APIs as of 2026. This is the contract
|
|
4
|
+
* the product UI should render — not "we post to TikTok" when the adapter
|
|
5
|
+
* can only drop a draft.
|
|
6
|
+
*
|
|
7
|
+
* Sources: Meta Graph / Instagram content publishing docs (100 posts / 24h),
|
|
8
|
+
* TikTok Content Posting API (direct post vs drafts; production partnership
|
|
9
|
+
* gated), X API v2 (paid write access, per-post pricing on some endpoints).
|
|
10
|
+
*/
|
|
11
|
+
export declare const PLATFORM_CAPABILITIES: Record<SocialPlatform, SocialCapability[]>;
|
|
12
|
+
export declare const INSTAGRAM_DAILY_PUBLISH_LIMIT = 100;
|
|
13
|
+
export declare function assertSupported(platform: SocialPlatform, kind: SocialCapability): void;
|
|
14
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEnE;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,cAAc,EAAE,gBAAgB,EAAE,CAK5E,CAAC;AAEF,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAKtF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honest capability map for official APIs as of 2026. This is the contract
|
|
3
|
+
* the product UI should render — not "we post to TikTok" when the adapter
|
|
4
|
+
* can only drop a draft.
|
|
5
|
+
*
|
|
6
|
+
* Sources: Meta Graph / Instagram content publishing docs (100 posts / 24h),
|
|
7
|
+
* TikTok Content Posting API (direct post vs drafts; production partnership
|
|
8
|
+
* gated), X API v2 (paid write access, per-post pricing on some endpoints).
|
|
9
|
+
*/
|
|
10
|
+
export const PLATFORM_CAPABILITIES = {
|
|
11
|
+
instagram: ['feed', 'reel', 'carousel'],
|
|
12
|
+
facebook: ['feed', 'reel'],
|
|
13
|
+
tiktok: ['draft_only'],
|
|
14
|
+
x: ['feed'],
|
|
15
|
+
};
|
|
16
|
+
export const INSTAGRAM_DAILY_PUBLISH_LIMIT = 100;
|
|
17
|
+
export function assertSupported(platform, kind) {
|
|
18
|
+
const allowed = PLATFORM_CAPABILITIES[platform];
|
|
19
|
+
if (!allowed.includes(kind)) {
|
|
20
|
+
throw new Error(`${platform} cannot ${kind} via the official API (supported: ${allowed.join(', ')})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAA+C;IAC/E,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC;IACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,CAAC,YAAY,CAAC;IACtB,CAAC,EAAE,CAAC,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AAEjD,MAAM,UAAU,eAAe,CAAC,QAAwB,EAAE,IAAsB;IAC9E,MAAM,OAAO,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,WAAW,IAAI,qCAAqC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxG,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.spec.d.ts","sourceRoot":"","sources":["../src/capabilities.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { assertSupported, PLATFORM_CAPABILITIES } from './capabilities.js';
|
|
3
|
+
describe('PLATFORM_CAPABILITIES', () => {
|
|
4
|
+
it('does not advertise Instagram Stories or TikTok direct post by default', () => {
|
|
5
|
+
expect(PLATFORM_CAPABILITIES.instagram).not.toContain('story');
|
|
6
|
+
expect(PLATFORM_CAPABILITIES.tiktok).toEqual(['draft_only']);
|
|
7
|
+
});
|
|
8
|
+
it('rejects unsupported kinds before any HTTP call', () => {
|
|
9
|
+
expect(() => assertSupported('x', 'reel')).toThrow(/x cannot reel/);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
//# sourceMappingURL=capabilities.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.spec.js","sourceRoot":"","sources":["../src/capabilities.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC/D,MAAM,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlatformPost, PublishResult, SocialCapability, SocialPlatform, SocialPublisher } from './ports.js';
|
|
2
|
+
export declare class FakeSocialPublisher implements SocialPublisher {
|
|
3
|
+
readonly platform: SocialPlatform;
|
|
4
|
+
readonly published: PlatformPost[];
|
|
5
|
+
constructor(platform: SocialPlatform);
|
|
6
|
+
capabilities(): SocialCapability[];
|
|
7
|
+
publish(post: PlatformPost): Promise<PublishResult>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=fake.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.adapter.d.ts","sourceRoot":"","sources":["../src/fake.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,qBAAa,mBAAoB,YAAW,eAAe;IAG7C,QAAQ,CAAC,QAAQ,EAAE,cAAc;IAF7C,QAAQ,CAAC,SAAS,EAAE,YAAY,EAAE,CAAM;gBAEnB,QAAQ,EAAE,cAAc;IAE7C,YAAY,IAAI,gBAAgB,EAAE;IAI5B,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;CAS1D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PLATFORM_CAPABILITIES } from './capabilities.js';
|
|
2
|
+
export class FakeSocialPublisher {
|
|
3
|
+
platform;
|
|
4
|
+
published = [];
|
|
5
|
+
constructor(platform) {
|
|
6
|
+
this.platform = platform;
|
|
7
|
+
}
|
|
8
|
+
capabilities() {
|
|
9
|
+
return PLATFORM_CAPABILITIES[this.platform];
|
|
10
|
+
}
|
|
11
|
+
async publish(post) {
|
|
12
|
+
this.published.push(post);
|
|
13
|
+
const draft = post.kind === 'draft_only' || this.platform === 'tiktok';
|
|
14
|
+
return {
|
|
15
|
+
platform: this.platform,
|
|
16
|
+
status: draft ? 'draft' : 'published',
|
|
17
|
+
remoteId: `${this.platform}_${this.published.length}`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=fake.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.adapter.js","sourceRoot":"","sources":["../src/fake.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAS1D,MAAM,OAAO,mBAAmB;IAGT;IAFZ,SAAS,GAAmB,EAAE,CAAC;IAExC,YAAqB,QAAwB;QAAxB,aAAQ,GAAR,QAAQ,CAAgB;IAAG,CAAC;IAEjD,YAAY;QACV,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAkB;QAC9B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC;QACvE,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW;YACrC,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE;SACtD,CAAC;IACJ,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { ConnectionHealth, FetchLike, MediaAsset, OAuthAppConfig, PlatformPost, PublishFailure, PublishResult, SocialAccount, SocialAuth, SocialCapability, SocialPlatform, SocialPublisher, TokenSet, } from './ports.js';
|
|
2
|
+
export { INSTAGRAM_DAILY_PUBLISH_LIMIT, PLATFORM_CAPABILITIES, assertSupported, } from './capabilities.js';
|
|
3
|
+
export { SocialPublisherRegistry, UnsupportedPlatformError } from './registry.js';
|
|
4
|
+
export { FakeSocialPublisher } from './fake.adapter.js';
|
|
5
|
+
export { MetaSocialPublisher, metaAuthorizationUrl, type MetaPublisherOptions } from './meta.adapter.js';
|
|
6
|
+
export { TikTokSocialPublisher, tiktokAuthorizationUrl, type TikTokPublisherOptions, } from './tiktok.adapter.js';
|
|
7
|
+
export { XSocialPublisher, xAuthorizationUrl, type XPublisherOptions } from './x.adapter.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,cAAc,EACd,YAAY,EACZ,cAAc,EACd,aAAa,EACb,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,QAAQ,GACT,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,6BAA6B,EAC7B,qBAAqB,EACrB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzG,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { INSTAGRAM_DAILY_PUBLISH_LIMIT, PLATFORM_CAPABILITIES, assertSupported, } from './capabilities.js';
|
|
2
|
+
export { SocialPublisherRegistry, UnsupportedPlatformError } from './registry.js';
|
|
3
|
+
export { FakeSocialPublisher } from './fake.adapter.js';
|
|
4
|
+
export { MetaSocialPublisher, metaAuthorizationUrl } from './meta.adapter.js';
|
|
5
|
+
export { TikTokSocialPublisher, tiktokAuthorizationUrl, } from './tiktok.adapter.js';
|
|
6
|
+
export { XSocialPublisher, xAuthorizationUrl } from './x.adapter.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,6BAA6B,EAC7B,qBAAqB,EACrB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAA6B,MAAM,mBAAmB,CAAC;AACzG,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GAEvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAA0B,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FetchLike, OAuthAppConfig, PlatformPost, PublishResult, SocialCapability, SocialPublisher, TokenSet } from './ports.js';
|
|
2
|
+
export interface MetaPublisherOptions {
|
|
3
|
+
fetch?: FetchLike;
|
|
4
|
+
graphBaseUrl?: string;
|
|
5
|
+
tokens: TokenSet;
|
|
6
|
+
/** Instagram professional account id, or Facebook page id. */
|
|
7
|
+
igUserId?: string;
|
|
8
|
+
pageId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Instagram + Facebook publishing via the Graph API. Stories are omitted on
|
|
12
|
+
* purpose: the Instagram content-publishing API still does not support them
|
|
13
|
+
* the way feed/reels/carousels are supported, which is a common failure mode
|
|
14
|
+
* in Buffer/Later/Zoho-class tools that advertise "Instagram" as a checkbox.
|
|
15
|
+
*/
|
|
16
|
+
export declare class MetaSocialPublisher implements SocialPublisher {
|
|
17
|
+
private readonly options;
|
|
18
|
+
readonly platform: 'instagram' | 'facebook';
|
|
19
|
+
private readonly fetch;
|
|
20
|
+
private readonly graphBaseUrl;
|
|
21
|
+
constructor(platform: 'instagram' | 'facebook', options: MetaPublisherOptions);
|
|
22
|
+
capabilities(): SocialCapability[];
|
|
23
|
+
publish(post: PlatformPost): Promise<PublishResult>;
|
|
24
|
+
private graphPost;
|
|
25
|
+
}
|
|
26
|
+
export declare function metaAuthorizationUrl(config: OAuthAppConfig, state: string): string;
|
|
27
|
+
//# sourceMappingURL=meta.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.adapter.d.ts","sourceRoot":"","sources":["../src/meta.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,QAAQ,CAAC;IACjB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IAOvD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,UAAU,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGpC,QAAQ,EAAE,WAAW,GAAG,UAAU,EACjB,OAAO,EAAE,oBAAoB;IAOhD,YAAY,IAAI,gBAAgB,EAAE;IAI5B,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;YAyC3C,SAAS;CAWxB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CASlF"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { PLATFORM_CAPABILITIES } from './capabilities.js';
|
|
2
|
+
/**
|
|
3
|
+
* Instagram + Facebook publishing via the Graph API. Stories are omitted on
|
|
4
|
+
* purpose: the Instagram content-publishing API still does not support them
|
|
5
|
+
* the way feed/reels/carousels are supported, which is a common failure mode
|
|
6
|
+
* in Buffer/Later/Zoho-class tools that advertise "Instagram" as a checkbox.
|
|
7
|
+
*/
|
|
8
|
+
export class MetaSocialPublisher {
|
|
9
|
+
options;
|
|
10
|
+
platform;
|
|
11
|
+
fetch;
|
|
12
|
+
graphBaseUrl;
|
|
13
|
+
constructor(platform, options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.platform = platform;
|
|
16
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
17
|
+
this.graphBaseUrl = options.graphBaseUrl ?? 'https://graph.facebook.com/v22.0';
|
|
18
|
+
}
|
|
19
|
+
capabilities() {
|
|
20
|
+
return PLATFORM_CAPABILITIES[this.platform];
|
|
21
|
+
}
|
|
22
|
+
async publish(post) {
|
|
23
|
+
const targetId = this.platform === 'instagram' ? this.options.igUserId : this.options.pageId;
|
|
24
|
+
if (!targetId) {
|
|
25
|
+
return {
|
|
26
|
+
platform: this.platform,
|
|
27
|
+
status: 'failed',
|
|
28
|
+
error: {
|
|
29
|
+
code: 'not_configured',
|
|
30
|
+
message: `${this.platform} account id missing`,
|
|
31
|
+
retryable: false,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const container = await this.graphPost(`/${targetId}/media`, {
|
|
37
|
+
caption: post.caption,
|
|
38
|
+
image_url: post.media[0]?.url,
|
|
39
|
+
media_type: post.kind === 'reel' ? 'REELS' : 'IMAGE',
|
|
40
|
+
access_token: this.options.tokens.accessToken,
|
|
41
|
+
});
|
|
42
|
+
const published = await this.graphPost(`/${targetId}/media_publish`, {
|
|
43
|
+
creation_id: container.id,
|
|
44
|
+
access_token: this.options.tokens.accessToken,
|
|
45
|
+
});
|
|
46
|
+
return { platform: this.platform, status: 'published', remoteId: String(published.id) };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : 'graph request failed';
|
|
50
|
+
const reauth = /session|oauth|token/i.test(message);
|
|
51
|
+
return {
|
|
52
|
+
platform: this.platform,
|
|
53
|
+
status: 'failed',
|
|
54
|
+
error: {
|
|
55
|
+
code: reauth ? 'needs_reauth' : 'publish_failed',
|
|
56
|
+
message,
|
|
57
|
+
retryable: !reauth,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async graphPost(path, body) {
|
|
63
|
+
const response = await this.fetch(`${this.graphBaseUrl}${path}`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/json' },
|
|
66
|
+
body: JSON.stringify(body),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`graph ${path} ${response.status}: ${await response.text()}`);
|
|
70
|
+
}
|
|
71
|
+
return (await response.json());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function metaAuthorizationUrl(config, state) {
|
|
75
|
+
const params = new URLSearchParams({
|
|
76
|
+
client_id: config.clientId,
|
|
77
|
+
redirect_uri: config.redirectUri,
|
|
78
|
+
state,
|
|
79
|
+
response_type: 'code',
|
|
80
|
+
scope: 'instagram_business_basic,instagram_business_content_publish,pages_show_list',
|
|
81
|
+
});
|
|
82
|
+
return `https://www.facebook.com/v22.0/dialog/oauth?${params.toString()}`;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=meta.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.adapter.js","sourceRoot":"","sources":["../src/meta.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAoB1D;;;;;GAKG;AACH,MAAM,OAAO,mBAAmB;IAOX;IANV,QAAQ,CAA2B;IAC3B,KAAK,CAAY;IACjB,YAAY,CAAS;IAEtC,YACE,QAAkC,EACjB,OAA6B;QAA7B,YAAO,GAAP,OAAO,CAAsB;QAE9C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,kCAAkC,CAAC;IACjF,CAAC;IAED,YAAY;QACV,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAkB;QAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QAC7F,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO;gBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE;oBACL,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,GAAG,IAAI,CAAC,QAAQ,qBAAqB;oBAC9C,SAAS,EAAE,KAAK;iBACjB;aACF,CAAC;QACJ,CAAC;QAED,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,QAAQ,QAAQ,EAAE;gBAC3D,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG;gBAC7B,UAAU,EAAE,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;gBACpD,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW;aAC9C,CAAC,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,QAAQ,gBAAgB,EAAE;gBACnE,WAAW,EAAE,SAAS,CAAC,EAAE;gBACzB,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW;aAC9C,CAAC,CAAC;YACH,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;YAC5E,MAAM,MAAM,GAAG,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACpD,OAAO;gBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,gBAAgB;oBAChD,OAAO;oBACP,SAAS,EAAE,CAAC,MAAM;iBACnB;aACF,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,IAA6B;QACjE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,YAAY,GAAG,IAAI,EAAE,EAAE;YAC/D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAChF,CAAC;QACD,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAmB,CAAC;IACnD,CAAC;CACF;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAsB,EAAE,KAAa;IACxE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,KAAK;QACL,aAAa,EAAE,MAAM;QACrB,KAAK,EAAE,6EAA6E;KACrF,CAAC,CAAC;IACH,OAAO,+CAA+C,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC5E,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.adapter.spec.d.ts","sourceRoot":"","sources":["../src/meta.adapter.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { MetaSocialPublisher } from './meta.adapter.js';
|
|
3
|
+
describe('MetaSocialPublisher', () => {
|
|
4
|
+
it('creates a container then publishes it', async () => {
|
|
5
|
+
const fetch = vi.fn(async (url) => {
|
|
6
|
+
if (String(url).endsWith('/media')) {
|
|
7
|
+
return new Response(JSON.stringify({ id: 'container_1' }), { status: 200 });
|
|
8
|
+
}
|
|
9
|
+
return new Response(JSON.stringify({ id: 'media_9' }), { status: 200 });
|
|
10
|
+
});
|
|
11
|
+
const publisher = new MetaSocialPublisher('instagram', {
|
|
12
|
+
fetch: fetch,
|
|
13
|
+
tokens: { accessToken: 'tok' },
|
|
14
|
+
igUserId: '1784',
|
|
15
|
+
});
|
|
16
|
+
const result = await publisher.publish({
|
|
17
|
+
platform: 'instagram',
|
|
18
|
+
accountId: '1784',
|
|
19
|
+
caption: 'Doors at 8',
|
|
20
|
+
media: [{ url: 'https://cdn.local/poster.jpg', mimeType: 'image/jpeg' }],
|
|
21
|
+
kind: 'feed',
|
|
22
|
+
});
|
|
23
|
+
expect(result).toEqual({ platform: 'instagram', status: 'published', remoteId: 'media_9' });
|
|
24
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
25
|
+
});
|
|
26
|
+
it('marks token errors as needs_reauth', async () => {
|
|
27
|
+
const fetch = vi.fn(async () => new Response('oauth exception: session invalidated', { status: 401 }));
|
|
28
|
+
const publisher = new MetaSocialPublisher('instagram', {
|
|
29
|
+
fetch: fetch,
|
|
30
|
+
tokens: { accessToken: 'expired' },
|
|
31
|
+
igUserId: '1784',
|
|
32
|
+
});
|
|
33
|
+
const result = await publisher.publish({
|
|
34
|
+
platform: 'instagram',
|
|
35
|
+
accountId: '1784',
|
|
36
|
+
caption: 'Doors at 8',
|
|
37
|
+
media: [{ url: 'https://cdn.local/poster.jpg', mimeType: 'image/jpeg' }],
|
|
38
|
+
kind: 'feed',
|
|
39
|
+
});
|
|
40
|
+
expect(result.status).toBe('failed');
|
|
41
|
+
expect(result.error?.code).toBe('needs_reauth');
|
|
42
|
+
expect(result.error?.retryable).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=meta.adapter.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.adapter.spec.js","sourceRoot":"","sources":["../src/meta.adapter.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE;YACxC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACnC,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YAC9E,CAAC;YACD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,mBAAmB,CAAC,WAAW,EAAE;YACrD,KAAK,EAAE,KAA2C;YAClD,MAAM,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;YAC9B,QAAQ,EAAE,MAAM;SACjB,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC;YACrC,QAAQ,EAAE,WAAW;YACrB,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,YAAY;YACrB,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;YACxE,IAAI,EAAE,MAAM;SACb,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;QAC5F,MAAM,CAAC,KAAK,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QAClD,MAAM,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,QAAQ,CAAC,sCAAsC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACvG,MAAM,SAAS,GAAG,IAAI,mBAAmB,CAAC,WAAW,EAAE;YACrD,KAAK,EAAE,KAA2C;YAClD,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE;YAClC,QAAQ,EAAE,MAAM;SACjB,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC;YACrC,QAAQ,EAAE,WAAW;YACrB,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,YAAY;YACrB,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;YACxE,IAAI,EAAE,MAAM;SACb,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type SocialPlatform = 'instagram' | 'facebook' | 'tiktok' | 'x';
|
|
2
|
+
/**
|
|
3
|
+
* What a given account is actually allowed to do through official APIs.
|
|
4
|
+
* Callers should branch on this instead of assuming "post everywhere".
|
|
5
|
+
*/
|
|
6
|
+
export type SocialCapability = 'feed' | 'reel' | 'story' | 'carousel' | 'direct_post' | 'draft_only';
|
|
7
|
+
export type ConnectionHealth = 'ok' | 'needs_reauth' | 'degraded';
|
|
8
|
+
export interface SocialAccount {
|
|
9
|
+
platform: SocialPlatform;
|
|
10
|
+
accountId: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
capabilities: SocialCapability[];
|
|
13
|
+
health: ConnectionHealth;
|
|
14
|
+
}
|
|
15
|
+
export interface MediaAsset {
|
|
16
|
+
url: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
/** e.g. '9:16', '1:1', '16:9' — adapters reject unsupported ratios. */
|
|
19
|
+
aspectRatio?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PlatformPost {
|
|
22
|
+
platform: SocialPlatform;
|
|
23
|
+
accountId: string;
|
|
24
|
+
caption: string;
|
|
25
|
+
media: MediaAsset[];
|
|
26
|
+
kind: SocialCapability;
|
|
27
|
+
/** ISO-8601. Adapters that cannot schedule natively return `status: 'scheduled'` for the app to retry later. */
|
|
28
|
+
scheduledAt?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PublishFailure {
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
retryable: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface PublishResult {
|
|
36
|
+
platform: SocialPlatform;
|
|
37
|
+
status: 'published' | 'draft' | 'failed' | 'queued';
|
|
38
|
+
remoteId?: string;
|
|
39
|
+
error?: PublishFailure;
|
|
40
|
+
}
|
|
41
|
+
export interface SocialPublisher {
|
|
42
|
+
readonly platform: SocialPlatform;
|
|
43
|
+
capabilities(): SocialCapability[];
|
|
44
|
+
publish(post: PlatformPost): Promise<PublishResult>;
|
|
45
|
+
}
|
|
46
|
+
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
47
|
+
export interface OAuthAppConfig {
|
|
48
|
+
clientId: string;
|
|
49
|
+
clientSecret: string;
|
|
50
|
+
redirectUri: string;
|
|
51
|
+
}
|
|
52
|
+
export interface TokenSet {
|
|
53
|
+
accessToken: string;
|
|
54
|
+
refreshToken?: string;
|
|
55
|
+
expiresAt?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface SocialAuth {
|
|
58
|
+
authorizationUrl(state: string, scopes?: string[]): string;
|
|
59
|
+
exchangeCode(code: string): Promise<{
|
|
60
|
+
account: SocialAccount;
|
|
61
|
+
tokens: TokenSet;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,GAAG,CAAC;AAEvE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,MAAM,GACN,OAAO,GACP,UAAU,GACV,aAAa,GACb,YAAY,CAAC;AAEjB,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,cAAc,GAAG,UAAU,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,gHAAgH;IAChH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,CAAC;IACzB,MAAM,EAAE,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,YAAY,IAAI,gBAAgB,EAAE,CAAC;IACnC,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACrD;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CACnF"}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PlatformPost, PublishResult, SocialPlatform, SocialPublisher } from './ports.js';
|
|
2
|
+
export declare class UnsupportedPlatformError extends Error {
|
|
3
|
+
readonly platform: SocialPlatform;
|
|
4
|
+
constructor(platform: SocialPlatform);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fan-out dispatcher. Use cases talk to this, not to Meta/TikTok/X clients.
|
|
8
|
+
* Missing adapters fail closed with a typed error instead of silently no-op.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SocialPublisherRegistry {
|
|
11
|
+
private readonly publishers;
|
|
12
|
+
register(publisher: SocialPublisher): this;
|
|
13
|
+
get(platform: SocialPlatform): SocialPublisher;
|
|
14
|
+
publish(post: PlatformPost): Promise<PublishResult>;
|
|
15
|
+
publishAll(posts: PlatformPost[]): Promise<PublishResult[]>;
|
|
16
|
+
supportedKinds(platform: SocialPlatform): string[];
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE/F,qBAAa,wBAAyB,SAAQ,KAAK;aACrB,QAAQ,EAAE,cAAc;gBAAxB,QAAQ,EAAE,cAAc;CAIrD;AAED;;;GAGG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8C;IAEzE,QAAQ,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI;IAK1C,GAAG,CAAC,QAAQ,EAAE,cAAc,GAAG,eAAe;IAMxC,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IAKnD,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAIjE,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,EAAE;CAInD"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { assertSupported, PLATFORM_CAPABILITIES } from './capabilities.js';
|
|
2
|
+
export class UnsupportedPlatformError extends Error {
|
|
3
|
+
platform;
|
|
4
|
+
constructor(platform) {
|
|
5
|
+
super(`no publisher registered for ${platform}`);
|
|
6
|
+
this.platform = platform;
|
|
7
|
+
this.name = 'UnsupportedPlatformError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fan-out dispatcher. Use cases talk to this, not to Meta/TikTok/X clients.
|
|
12
|
+
* Missing adapters fail closed with a typed error instead of silently no-op.
|
|
13
|
+
*/
|
|
14
|
+
export class SocialPublisherRegistry {
|
|
15
|
+
publishers = new Map();
|
|
16
|
+
register(publisher) {
|
|
17
|
+
this.publishers.set(publisher.platform, publisher);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
get(platform) {
|
|
21
|
+
const publisher = this.publishers.get(platform);
|
|
22
|
+
if (!publisher)
|
|
23
|
+
throw new UnsupportedPlatformError(platform);
|
|
24
|
+
return publisher;
|
|
25
|
+
}
|
|
26
|
+
async publish(post) {
|
|
27
|
+
assertSupported(post.platform, post.kind);
|
|
28
|
+
return this.get(post.platform).publish(post);
|
|
29
|
+
}
|
|
30
|
+
async publishAll(posts) {
|
|
31
|
+
return Promise.all(posts.map((post) => this.publish(post)));
|
|
32
|
+
}
|
|
33
|
+
supportedKinds(platform) {
|
|
34
|
+
const registered = this.publishers.get(platform);
|
|
35
|
+
return registered?.capabilities() ?? PLATFORM_CAPABILITIES[platform];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG3E,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACrB;IAA5B,YAA4B,QAAwB;QAClD,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;QADvB,aAAQ,GAAR,QAAQ,CAAgB;QAElD,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,uBAAuB;IACjB,UAAU,GAAG,IAAI,GAAG,EAAmC,CAAC;IAEzE,QAAQ,CAAC,SAA0B;QACjC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,QAAwB;QAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,wBAAwB,CAAC,QAAQ,CAAC,CAAC;QAC7D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAkB;QAC9B,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAAqB;QACpC,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,cAAc,CAAC,QAAwB;QACrC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjD,OAAO,UAAU,EAAE,YAAY,EAAE,IAAI,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACvE,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.spec.d.ts","sourceRoot":"","sources":["../src/registry.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { FakeSocialPublisher } from './fake.adapter.js';
|
|
3
|
+
import { SocialPublisherRegistry, UnsupportedPlatformError } from './registry.js';
|
|
4
|
+
describe('SocialPublisherRegistry', () => {
|
|
5
|
+
it('fans a campaign out per platform and keeps TikTok as a draft', async () => {
|
|
6
|
+
const ig = new FakeSocialPublisher('instagram');
|
|
7
|
+
const tt = new FakeSocialPublisher('tiktok');
|
|
8
|
+
const registry = new SocialPublisherRegistry().register(ig).register(tt);
|
|
9
|
+
const results = await registry.publishAll([
|
|
10
|
+
{
|
|
11
|
+
platform: 'instagram',
|
|
12
|
+
accountId: 'ig_1',
|
|
13
|
+
caption: 'Tickets on sale',
|
|
14
|
+
media: [{ url: 'https://cdn.local/poster.jpg', mimeType: 'image/jpeg' }],
|
|
15
|
+
kind: 'feed',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
platform: 'tiktok',
|
|
19
|
+
accountId: 'tt_1',
|
|
20
|
+
caption: 'Tickets on sale',
|
|
21
|
+
media: [{ url: 'https://cdn.local/clip.mp4', mimeType: 'video/mp4', aspectRatio: '9:16' }],
|
|
22
|
+
kind: 'draft_only',
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
expect(results.map((r) => r.status)).toEqual(['published', 'draft']);
|
|
26
|
+
expect(ig.published).toHaveLength(1);
|
|
27
|
+
expect(tt.published).toHaveLength(1);
|
|
28
|
+
});
|
|
29
|
+
it('fails closed when a platform has no adapter', async () => {
|
|
30
|
+
const registry = new SocialPublisherRegistry();
|
|
31
|
+
await expect(registry.publish({
|
|
32
|
+
platform: 'x',
|
|
33
|
+
accountId: 'x_1',
|
|
34
|
+
caption: 'hi',
|
|
35
|
+
media: [],
|
|
36
|
+
kind: 'feed',
|
|
37
|
+
})).rejects.toBeInstanceOf(UnsupportedPlatformError);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=registry.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.spec.js","sourceRoot":"","sources":["../src/registry.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAElF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,EAAE,GAAG,IAAI,mBAAmB,CAAC,WAAW,CAAC,CAAC;QAChD,MAAM,EAAE,GAAG,IAAI,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,IAAI,uBAAuB,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAEzE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC;YACxC;gBACE,QAAQ,EAAE,WAAW;gBACrB,SAAS,EAAE,MAAM;gBACjB,OAAO,EAAE,iBAAiB;gBAC1B,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;gBACxE,IAAI,EAAE,MAAM;aACb;YACD;gBACE,QAAQ,EAAE,QAAQ;gBAClB,SAAS,EAAE,MAAM;gBACjB,OAAO,EAAE,iBAAiB;gBAC1B,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,4BAA4B,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;gBAC1F,IAAI,EAAE,YAAY;aACnB;SACF,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QACrE,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,QAAQ,GAAG,IAAI,uBAAuB,EAAE,CAAC;QAC/C,MAAM,MAAM,CACV,QAAQ,CAAC,OAAO,CAAC;YACf,QAAQ,EAAE,GAAG;YACb,SAAS,EAAE,KAAK;YAChB,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,EAAE;YACT,IAAI,EAAE,MAAM;SACb,CAAC,CACH,CAAC,OAAO,CAAC,cAAc,CAAC,wBAAwB,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FetchLike, OAuthAppConfig, PlatformPost, PublishResult, SocialCapability, SocialPublisher, TokenSet } from './ports.js';
|
|
2
|
+
export interface TikTokPublisherOptions {
|
|
3
|
+
fetch?: FetchLike;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
tokens: TokenSet;
|
|
6
|
+
/**
|
|
7
|
+
* Direct Post requires TikTok production partnership approval. Until then
|
|
8
|
+
* the adapter uploads to the creator's draft tray — the honest default.
|
|
9
|
+
*/
|
|
10
|
+
allowDirectPost?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* TikTok Content Posting API. Production direct-post is gated; sandbox and
|
|
14
|
+
* most third-party tools only get drafts. Hootsuite-class outages in 2026
|
|
15
|
+
* also showed this API is a single point of failure — callers should treat
|
|
16
|
+
* `draft` as success, not a bug.
|
|
17
|
+
*/
|
|
18
|
+
export declare class TikTokSocialPublisher implements SocialPublisher {
|
|
19
|
+
private readonly options;
|
|
20
|
+
readonly platform: "tiktok";
|
|
21
|
+
private readonly fetch;
|
|
22
|
+
private readonly baseUrl;
|
|
23
|
+
constructor(options: TikTokPublisherOptions);
|
|
24
|
+
capabilities(): SocialCapability[];
|
|
25
|
+
publish(post: PlatformPost): Promise<PublishResult>;
|
|
26
|
+
}
|
|
27
|
+
export declare function tiktokAuthorizationUrl(config: OAuthAppConfig, state: string): string;
|
|
28
|
+
//# sourceMappingURL=tiktok.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tiktok.adapter.d.ts","sourceRoot":"","sources":["../src/tiktok.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IAK/C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,QAAQ,CAAC,QAAQ,EAAG,QAAQ,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAEJ,OAAO,EAAE,sBAAsB;IAK5D,YAAY,IAAI,gBAAgB,EAAE;IAM5B,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;CAoC1D;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CASpF"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { PLATFORM_CAPABILITIES } from './capabilities.js';
|
|
2
|
+
/**
|
|
3
|
+
* TikTok Content Posting API. Production direct-post is gated; sandbox and
|
|
4
|
+
* most third-party tools only get drafts. Hootsuite-class outages in 2026
|
|
5
|
+
* also showed this API is a single point of failure — callers should treat
|
|
6
|
+
* `draft` as success, not a bug.
|
|
7
|
+
*/
|
|
8
|
+
export class TikTokSocialPublisher {
|
|
9
|
+
options;
|
|
10
|
+
platform = 'tiktok';
|
|
11
|
+
fetch;
|
|
12
|
+
baseUrl;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
16
|
+
this.baseUrl = options.baseUrl ?? 'https://open.tiktokapis.com/v2';
|
|
17
|
+
}
|
|
18
|
+
capabilities() {
|
|
19
|
+
return this.options.allowDirectPost
|
|
20
|
+
? ['draft_only', 'direct_post']
|
|
21
|
+
: PLATFORM_CAPABILITIES.tiktok;
|
|
22
|
+
}
|
|
23
|
+
async publish(post) {
|
|
24
|
+
const direct = post.kind === 'direct_post' && this.options.allowDirectPost;
|
|
25
|
+
const path = direct ? '/post/publish/video/init/' : '/post/publish/inbox/video/init/';
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
authorization: `Bearer ${this.options.tokens.accessToken}`,
|
|
31
|
+
'content-type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
post_info: { title: post.caption },
|
|
35
|
+
source_info: { source: 'PULL_FROM_URL', video_url: post.media[0]?.url },
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`tiktok ${path} ${response.status}: ${await response.text()}`);
|
|
40
|
+
}
|
|
41
|
+
const body = (await response.json());
|
|
42
|
+
return {
|
|
43
|
+
platform: 'tiktok',
|
|
44
|
+
status: direct ? 'published' : 'draft',
|
|
45
|
+
remoteId: body.data?.publish_id,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return {
|
|
50
|
+
platform: 'tiktok',
|
|
51
|
+
status: 'failed',
|
|
52
|
+
error: {
|
|
53
|
+
code: 'publish_failed',
|
|
54
|
+
message: err instanceof Error ? err.message : 'tiktok request failed',
|
|
55
|
+
retryable: true,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function tiktokAuthorizationUrl(config, state) {
|
|
62
|
+
const params = new URLSearchParams({
|
|
63
|
+
client_key: config.clientId,
|
|
64
|
+
redirect_uri: config.redirectUri,
|
|
65
|
+
state,
|
|
66
|
+
response_type: 'code',
|
|
67
|
+
scope: 'video.upload,video.publish',
|
|
68
|
+
});
|
|
69
|
+
return `https://www.tiktok.com/v2/auth/authorize/?${params.toString()}`;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=tiktok.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tiktok.adapter.js","sourceRoot":"","sources":["../src/tiktok.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAsB1D;;;;;GAKG;AACH,MAAM,OAAO,qBAAqB;IAKH;IAJpB,QAAQ,GAAG,QAAiB,CAAC;IACrB,KAAK,CAAY;IACjB,OAAO,CAAS;IAEjC,YAA6B,OAA+B;QAA/B,YAAO,GAAP,OAAO,CAAwB;QAC1D,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gCAAgC,CAAC;IACrE,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe;YACjC,CAAC,CAAC,CAAC,YAAY,EAAE,aAAa,CAAC;YAC/B,CAAC,CAAC,qBAAqB,CAAC,MAAM,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAkB;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC;QAC3E,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,iCAAiC,CAAC;QACtF,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;oBAC1D,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE;oBAClC,WAAW,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE;iBACxE,CAAC;aACH,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,UAAU,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACjF,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuC,CAAC;YAC3E,OAAO;gBACL,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO;gBACtC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU;aAChC,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE;oBACL,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB;oBACrE,SAAS,EAAE,IAAI;iBAChB;aACF,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAsB,EAAE,KAAa;IAC1E,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,UAAU,EAAE,MAAM,CAAC,QAAQ;QAC3B,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,KAAK;QACL,aAAa,EAAE,MAAM;QACrB,KAAK,EAAE,4BAA4B;KACpC,CAAC,CAAC;IACH,OAAO,6CAA6C,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC1E,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FetchLike, OAuthAppConfig, PlatformPost, PublishResult, SocialCapability, SocialPublisher, TokenSet } from './ports.js';
|
|
2
|
+
export interface XPublisherOptions {
|
|
3
|
+
fetch?: FetchLike;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
tokens: TokenSet;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* X API v2 tweets. Write access is paid; some post types are metered. The
|
|
9
|
+
* adapter does not pretend to cover follows/likes (removed from self-serve).
|
|
10
|
+
*/
|
|
11
|
+
export declare class XSocialPublisher implements SocialPublisher {
|
|
12
|
+
private readonly options;
|
|
13
|
+
readonly platform: "x";
|
|
14
|
+
private readonly fetch;
|
|
15
|
+
private readonly baseUrl;
|
|
16
|
+
constructor(options: XPublisherOptions);
|
|
17
|
+
capabilities(): SocialCapability[];
|
|
18
|
+
publish(post: PlatformPost): Promise<PublishResult>;
|
|
19
|
+
}
|
|
20
|
+
export declare function xAuthorizationUrl(config: OAuthAppConfig, state: string): string;
|
|
21
|
+
//# sourceMappingURL=x.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"x.adapter.d.ts","sourceRoot":"","sources":["../src/x.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,eAAe;IAK1C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,QAAQ,CAAC,QAAQ,EAAG,GAAG,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAEJ,OAAO,EAAE,iBAAiB;IAKvD,YAAY,IAAI,gBAAgB,EAAE;IAI5B,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;CA2B1D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PLATFORM_CAPABILITIES } from './capabilities.js';
|
|
2
|
+
/**
|
|
3
|
+
* X API v2 tweets. Write access is paid; some post types are metered. The
|
|
4
|
+
* adapter does not pretend to cover follows/likes (removed from self-serve).
|
|
5
|
+
*/
|
|
6
|
+
export class XSocialPublisher {
|
|
7
|
+
options;
|
|
8
|
+
platform = 'x';
|
|
9
|
+
fetch;
|
|
10
|
+
baseUrl;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
14
|
+
this.baseUrl = options.baseUrl ?? 'https://api.x.com/2';
|
|
15
|
+
}
|
|
16
|
+
capabilities() {
|
|
17
|
+
return PLATFORM_CAPABILITIES.x;
|
|
18
|
+
}
|
|
19
|
+
async publish(post) {
|
|
20
|
+
try {
|
|
21
|
+
const response = await this.fetch(`${this.baseUrl}/tweets`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: `Bearer ${this.options.tokens.accessToken}`,
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ text: post.caption }),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`x /tweets ${response.status}: ${await response.text()}`);
|
|
31
|
+
}
|
|
32
|
+
const body = (await response.json());
|
|
33
|
+
return { platform: 'x', status: 'published', remoteId: body.data?.id };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return {
|
|
37
|
+
platform: 'x',
|
|
38
|
+
status: 'failed',
|
|
39
|
+
error: {
|
|
40
|
+
code: 'publish_failed',
|
|
41
|
+
message: err instanceof Error ? err.message : 'x request failed',
|
|
42
|
+
retryable: true,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function xAuthorizationUrl(config, state) {
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
client_id: config.clientId,
|
|
51
|
+
redirect_uri: config.redirectUri,
|
|
52
|
+
state,
|
|
53
|
+
response_type: 'code',
|
|
54
|
+
scope: 'tweet.read tweet.write users.read offline.access',
|
|
55
|
+
code_challenge_method: 'S256',
|
|
56
|
+
});
|
|
57
|
+
return `https://twitter.com/i/oauth2/authorize?${params.toString()}`;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=x.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"x.adapter.js","sourceRoot":"","sources":["../src/x.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAiB1D;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IAKE;IAJpB,QAAQ,GAAG,GAAY,CAAC;IAChB,KAAK,CAAY;IACjB,OAAO,CAAS;IAEjC,YAA6B,OAA0B;QAA1B,YAAO,GAAP,OAAO,CAAmB;QACrD,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,qBAAqB,CAAC;IAC1D,CAAC;IAED,YAAY;QACV,OAAO,qBAAqB,CAAC,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAkB;QAC9B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,SAAS,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;oBAC1D,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;aAC7C,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,aAAa,QAAQ,CAAC,MAAM,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC5E,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA+B,CAAC;YACnE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;QACzE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,QAAQ,EAAE,GAAG;gBACb,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE;oBACL,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;oBAChE,SAAS,EAAE,IAAI;iBAChB;aACF,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAsB,EAAE,KAAa;IACrE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,KAAK;QACL,aAAa,EAAE,MAAM;QACrB,KAAK,EAAE,kDAAkD;QACzD,qBAAqB,EAAE,MAAM;KAC9B,CAAC,CAAC;IACH,OAAO,0CAA0C,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AACvE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bubltec/mycota-social",
|
|
3
|
+
"version": "0.1.1-next.18",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.10.0",
|
|
21
|
+
"typescript": "^5.8.0",
|
|
22
|
+
"vitest": "^2.1.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -b",
|
|
26
|
+
"typecheck": "tsc -b --noEmit",
|
|
27
|
+
"lint": "oxlint -c ../../.oxlintrc.json src",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
}
|
|
30
|
+
}
|