@stacksjs/socials 0.70.230 → 0.70.232

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.
@@ -1,4 +1,9 @@
1
- import type { BlueskySession, BlueskySessionCredentials, PublishedPost, PublishPostInput, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
1
+ import type { AuthoredPostPage, BlueskySession, BlueskySessionCredentials, PublishedPost, PublishPostInput, RemotePostRef, SocialDeletionDriver, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
2
+ /**
3
+ * Split `at://<repo>/<collection>/<rkey>` into its parts. Deletes are keyed by
4
+ * repo + collection + rkey, not by the AT-URI itself.
5
+ */
6
+ export declare function parseAtUri(uri: string): { repo: string, collection: string, rkey: string };
2
7
  /**
3
8
  * Find link/hashtag/mention spans in post text with UTF-8 byte offsets
4
9
  * (ATProto facet ranges are byte-indexed, not character-indexed).
@@ -20,7 +25,7 @@ export declare class BlueskyApiError extends Error {
20
25
  constructor(message: string, status: number, body: string);
21
26
  get isAuthError(): boolean;
22
27
  }
23
- export declare class BlueskyPublishingDriver implements SocialPublishingDriver {
28
+ export declare class BlueskyPublishingDriver implements SocialPublishingDriver, SocialDeletionDriver {
24
29
  readonly provider: 'bluesky';
25
30
  characterLimit: number;
26
31
  protected service: string;
@@ -34,6 +39,8 @@ export declare class BlueskyPublishingDriver implements SocialPublishingDriver {
34
39
  protected buildFacets(text: string): Promise<Array<Record<string, unknown>>>;
35
40
  protected resolveHandle(handle: string): Promise<string | null>;
36
41
  uploadBlob(identity: SocialIdentityCredentials, bytes: Uint8Array, mimeType: string): Promise<unknown>;
42
+ listAuthoredPosts(identity: SocialIdentityCredentials, query?: TimelineQuery): Promise<AuthoredPostPage>;
43
+ deletePost(identity: SocialIdentityCredentials, ref: RemotePostRef): Promise<void>;
37
44
  protected post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
38
45
  protected request<T>(url: URL, init: RequestInit): Promise<T>;
39
46
  protected toPostUrl(handle: string, uri: string): string;
@@ -5,5 +5,6 @@ export * from './github';
5
5
  export * from './google';
6
6
  export * from './instagram';
7
7
  export * from './linkedin';
8
+ export * from './mastodon';
8
9
  export * from './threads';
9
10
  export * from './twitter';
@@ -1,4 +1,4 @@
1
- import type { PublishedPost, PublishPostInput, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
1
+ import type { AuthoredPostPage, PublishedPost, PublishPostInput, RemotePostRef, SocialDeletionDriver, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
2
2
  /**
3
3
  * Escape the reserved characters of LinkedIn's "little text" commentary format
4
4
  * so the literal text renders as typed. Without this, characters like `(` `)`
@@ -46,7 +46,7 @@ export declare class LinkedInApiError extends Error {
46
46
  * LinkedIn has no app-password, so a token is always obtained via OAuth (or
47
47
  * pasted in from a prior OAuth grant).
48
48
  */
49
- export declare class LinkedInPublishingDriver implements SocialPublishingDriver {
49
+ export declare class LinkedInPublishingDriver implements SocialPublishingDriver, SocialDeletionDriver {
50
50
  readonly provider: 'linkedin';
51
51
  characterLimit: number;
52
52
  protected apiVersion: string;
@@ -58,5 +58,7 @@ export declare class LinkedInPublishingDriver implements SocialPublishingDriver
58
58
  getProfile(accessToken: string): Promise<LinkedInProfile>;
59
59
  publish(identity: SocialIdentityCredentials, post: PublishPostInput): Promise<PublishedPost>;
60
60
  timeline(_identity: SocialIdentityCredentials, _query?: TimelineQuery): Promise<TimelineResult>;
61
+ listAuthoredPosts(identity: SocialIdentityCredentials, query?: TimelineQuery): Promise<AuthoredPostPage>;
62
+ deletePost(identity: SocialIdentityCredentials, ref: RemotePostRef): Promise<void>;
61
63
  protected request<T>(url: string, init: RequestInit): Promise<T>;
62
64
  }
@@ -0,0 +1,39 @@
1
+ import type { AuthoredPostPage, PublishedPost, PublishPostInput, RemotePostRef, SocialDeletionDriver, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
2
+ /** Normalize "mastodon.social" / "https://mastodon.social/" → "https://mastodon.social". */
3
+ export declare function normalizeInstance(value: string): string;
4
+ export declare interface MastodonAccount {
5
+ accountId: string
6
+ username: string
7
+ displayName?: string
8
+ url: string
9
+ }
10
+ export declare class MastodonApiError extends Error {
11
+ public status: number;
12
+ public body: string;
13
+ constructor(message: string, status: number, body: string);
14
+ get isAuthError(): boolean;
15
+ }
16
+ /**
17
+ * Publishing driver for Mastodon (and API-compatible instances).
18
+ *
19
+ * Mastodon is token-based rather than OAuth-redirect based: the user creates
20
+ * an access token in their instance's Preferences → Development, so there is
21
+ * no consent URL or code exchange to model.
22
+ *
23
+ * Because every instance is a different host, `identity.did` carries the
24
+ * instance base URL — the same slot LinkedIn uses for the member URN and
25
+ * Instagram for the account id.
26
+ */
27
+ export declare class MastodonPublishingDriver implements SocialPublishingDriver, SocialDeletionDriver {
28
+ readonly provider: 'mastodon';
29
+ characterLimit: number;
30
+ protected instanceOf(identity: SocialIdentityCredentials): string;
31
+ protected tokenOf(identity: SocialIdentityCredentials): string;
32
+ verifyCredentials(identity: SocialIdentityCredentials): Promise<MastodonAccount>;
33
+ uploadMedia(identity: SocialIdentityCredentials, bytes: Uint8Array, mimeType: string, altText?: string): Promise<string>;
34
+ publish(identity: SocialIdentityCredentials, post: PublishPostInput): Promise<PublishedPost>;
35
+ timeline(_identity: SocialIdentityCredentials, _query?: TimelineQuery): Promise<TimelineResult>;
36
+ listAuthoredPosts(identity: SocialIdentityCredentials, query?: TimelineQuery): Promise<AuthoredPostPage>;
37
+ deletePost(identity: SocialIdentityCredentials, ref: RemotePostRef): Promise<void>;
38
+ protected request<T>(url: string, init: RequestInit): Promise<T>;
39
+ }
@@ -1,5 +1,38 @@
1
1
  import { AbstractProvider } from '../abstract';
2
- import type { ProviderInterface, SocialUser } from '../types';
2
+ import type { AuthoredPostPage, ProviderInterface, PublishedPost, PublishPostInput, RemotePostRef, SocialDeletionDriver, SocialIdentityCredentials, SocialPublishingDriver, SocialUser, TimelineQuery, TimelineResult } from '../types';
3
+ export declare interface TwitterDriverOptions {
4
+ apiBase?: string
5
+ authorizeBase?: string
6
+ }
7
+ export declare interface TwitterAuthUrlInput {
8
+ clientId: string
9
+ redirectUrl: string
10
+ scopes: string[]
11
+ state: string
12
+ }
13
+ export declare interface TwitterTokenExchangeInput {
14
+ clientId: string
15
+ clientSecret?: string
16
+ redirectUrl: string
17
+ code: string
18
+ codeVerifier: string
19
+ }
20
+ export declare interface TwitterRefreshInput {
21
+ clientId: string
22
+ clientSecret?: string
23
+ refreshToken: string
24
+ }
25
+ export declare interface TwitterToken {
26
+ accessToken: string
27
+ refreshToken?: string
28
+ expiresIn?: number
29
+ scope?: string
30
+ }
31
+ export declare interface TwitterProfile {
32
+ id: string
33
+ username: string
34
+ name?: string
35
+ }
3
36
  export declare class TwitterProvider extends AbstractProvider implements ProviderInterface {
4
37
  protected baseUrl: string;
5
38
  protected apiUrl: string;
@@ -9,3 +42,38 @@ export declare class TwitterProvider extends AbstractProvider implements Provide
9
42
  protected validateConfig(): void;
10
43
  protected getTokenUrl(): string;
11
44
  }
45
+ /**
46
+ * Publishing driver for X/Twitter.
47
+ *
48
+ * Separate from `TwitterProvider` above, which only signs users in. Auth here
49
+ * is OAuth 2.0 with PKCE (X's user-context flow); posting is `POST /2/tweets`,
50
+ * images upload first via `POST /2/media/upload` and attach by media id, and
51
+ * threads chain through `reply.in_reply_to_tweet_id`.
52
+ *
53
+ * Posting on X requires a paid API tier, so this is exercised against mocked
54
+ * endpoints; the request shapes follow X's documented v2 API.
55
+ */
56
+ export declare class TwitterApiError extends Error {
57
+ public status: number;
58
+ public body: string;
59
+ constructor(message: string, status: number, body: string);
60
+ get isAuthError(): boolean;
61
+ }
62
+ export declare class TwitterPublishingDriver implements SocialPublishingDriver, SocialDeletionDriver {
63
+ readonly provider: 'twitter';
64
+ characterLimit: number;
65
+ protected apiBase: string;
66
+ protected authorizeBase: string;
67
+ constructor(options?: TwitterDriverOptions);
68
+ createAuthorization(input: TwitterAuthUrlInput): Promise<{ url: string, codeVerifier: string }>;
69
+ exchangeCode(input: TwitterTokenExchangeInput): Promise<TwitterToken>;
70
+ refreshAccessToken(input: TwitterRefreshInput): Promise<TwitterToken>;
71
+ getProfile(accessToken: string): Promise<TwitterProfile>;
72
+ uploadMedia(accessToken: string, bytes: Uint8Array, mimeType: string): Promise<string>;
73
+ publish(identity: SocialIdentityCredentials, post: PublishPostInput): Promise<PublishedPost>;
74
+ timeline(_identity: SocialIdentityCredentials, _query?: TimelineQuery): Promise<TimelineResult>;
75
+ listAuthoredPosts(identity: SocialIdentityCredentials, query?: TimelineQuery): Promise<AuthoredPostPage>;
76
+ deletePost(identity: SocialIdentityCredentials, ref: RemotePostRef): Promise<void>;
77
+ protected tokenRequest(body: URLSearchParams, clientId: string, clientSecret?: string): Promise<TwitterToken>;
78
+ protected request<T>(url: string, init: RequestInit): Promise<T>;
79
+ }
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // @bun
2
- var U=import.meta.require;class F{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_state=null;user=null;constructor(z){this.clientId=z.clientId,this.clientSecret=z.clientSecret,this.redirectUrl=z.redirectUrl}getCodeFields(z=null){let D={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())D.state=z;if(this.usesPKCE())D.code_challenge=this.getCodeChallenge(),D.code_challenge_method=this.getCodeChallengeMethod();return{...D,...this.parameters}}formatScopes(z,D){return z.join(D)}async userFromToken(z){return{...await this.getUserByToken(z),token:z}}scopes(z){let D=Array.isArray(z)?z:[z];return this._scopes=[...new Set([...this._scopes,...D])],this}setScopes(z){let D=Array.isArray(z)?z:[z];return this._scopes=[...new Set(D)],this}getScopes(){return this._scopes}setRedirectUrl(z){if(typeof z!=="string"||z.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let D;try{D=new URL(z)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${z}`)}if(D.protocol!=="https:"&&D.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${D.protocol}`);return this.redirectUrl=z,this}withState(z){if(typeof z!=="string"||z.length===0)throw Error("[socials] withState requires a non-empty string");return this._state=z,this}resolveState(){return this._state??this.getState()}usesState(){return!this._stateless}validateState(z,D){if(typeof z!=="string"||typeof D!=="string")return!1;if(z.length===0||D.length===0)return!1;if(z.length!==D.length)return!1;try{let{timingSafeEqual:G}=U("crypto");return G(Buffer.from(z,"utf8"),Buffer.from(D,"utf8"))}catch{let G=0;for(let J=0;J<z.length;J++)G|=z.charCodeAt(J)^D.charCodeAt(J);return G===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let z=new Uint8Array(32);return crypto.getRandomValues(z),Array.from(z).map((D)=>D.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let z=new Uint8Array(48);return crypto.getRandomValues(z),Array.from(z).map((D)=>D.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let D=new TextEncoder().encode(this.getCodeVerifier()),G=await crypto.subtle.digest("SHA-256",D),{Buffer:J}=await import("buffer");return J.from(new Uint8Array(G)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(z){return this.parameters=z,this}buildAuthUrlFromBase(z,D){let G=new URLSearchParams(this.getCodeFields(D));return`${z}?${G.toString()}`}}import{Buffer as S}from"buffer";import{createPrivateKey as I,sign as E}from"crypto";import{config as M}from"@stacksjs/config";class C extends Error{constructor(z="Invalid state"){super(z);this.name="InvalidStateException"}}class Z extends Error{constructor(z){super(z);this.name="ConfigException"}}class b extends F{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";constructor(z){super(z);this.teamId=z.teamId??"",this.keyId=z.keyId??"",this.privateKey=z.privateKey??""}getConfig(){let z={clientId:this.clientId||(M.services.apple?.clientId??""),teamId:this.teamId||(M.services.apple?.teamId??""),keyId:this.keyId||(M.services.apple?.keyId??""),privateKey:(this.privateKey||(M.services.apple?.privateKey??"")).replace(/\\n/g,`
3
- `),redirectUrl:this.redirectUrl||(M.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:M.services.apple?.scopes??["name","email"]};return this.setScopes(z.scopes),z}async getAuthUrl(){let z=this.resolveState(),{clientId:D,redirectUrl:G,scopes:J}=this.getConfig();this.validateConfig();let N={client_id:D,redirect_uri:G,scope:J.join(" "),state:z,response_type:"code",...this.parameters};if(J.length>0)N.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams(N).toString()}`}async getAccessToken(z){let{clientId:D,redirectUrl:G}=this.getConfig();this.validateConfig();let J=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:z,redirect_uri:G,client_id:D,client_secret:this.generateClientSecret()})}),N=await J.json();if(!J.ok||N.error)throw Error(`Apple OAuth error: ${N.error_description??N.error??`HTTP ${J.status}`}`);if(!N.id_token)throw Error("Apple OAuth error: token response contained no id_token");return N.id_token}async getUserByToken(z){let{clientId:D}=this.getConfig(),G=this.decodeIdToken(z),J=G.iss===this.baseUrl,N=Array.isArray(G.aud)?G.aud.includes(D):G.aud===D,Q=typeof G.exp==="number"&&G.exp*1000>Date.now();if(!J||!N||!Q)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!G.sub)throw Error("Apple OAuth error: id_token has no subject");let X=typeof G.email==="string"?G.email:null,Y=null;if(G.email_verified===!0||G.email_verified==="true")Y=!0;else if(G.email_verified===!1||G.email_verified==="false")Y=!1;return{id:String(G.sub),nickname:null,name:"",email:X,emailVerified:Y,avatar:null,token:z,raw:G}}generateClientSecret(){let{clientId:z,teamId:D,keyId:G,privateKey:J}=this.getConfig(),N=Math.floor(Date.now()/1000),Q={alg:"ES256",kid:G,typ:"JWT"},X={iss:D,iat:N,exp:N+3600,aud:this.baseUrl,sub:z},Y=`${this.base64urlJson(Q)}.${this.base64urlJson(X)}`,_;try{_=I(J)}catch($){throw new Z(`Apple private key could not be parsed: ${$ instanceof Error?$.message:String($)}`)}let H=E("sha256",S.from(Y),{key:_,dsaEncoding:"ieee-p1363"});return`${Y}.${H.toString("base64url")}`}decodeIdToken(z){let D=z.split(".");if(D.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(S.from(D[1],"base64url").toString("utf8"))}base64urlJson(z){return S.from(JSON.stringify(z)).toString("base64url")}validateConfig(){let{clientId:z,teamId:D,keyId:G,privateKey:J,redirectUrl:N}=this.getConfig();if(!z)throw new Z("Apple client ID (Service ID) not provided");if(!D)throw new Z("Apple team ID not provided");if(!G)throw new Z("Apple key ID not provided");if(!J)throw new Z("Apple private key not provided");if(!N)throw new Z("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}class T extends Error{status;body;constructor(z,D,G){super(z);this.status=D;this.body=G;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}var x=new TextEncoder;function W(z){return x.encode(z).length}function k(z){let D=[],G=/https?:\/\/[^\s<>"']+/g;for(let X of z.matchAll(G)){let Y=X[0].replace(/[),.;!?]+$/,"");D.push({byteStart:W(z.slice(0,X.index)),byteEnd:W(z.slice(0,X.index))+W(Y),type:"link",value:Y})}let J=(X,Y)=>D.some((_)=>_.type==="link"&&X<_.byteEnd&&Y>_.byteStart),N=/(^|\s)(#[A-Za-z0-9_]+)/g;for(let X of z.matchAll(N)){let Y=X[1],_=X[2];if(Y===void 0||_===void 0)continue;if(/^#\d+$/.test(_))continue;let H=(X.index??0)+Y.length,$=W(z.slice(0,H)),O=$+W(_);if(J($,O))continue;D.push({byteStart:$,byteEnd:O,type:"tag",value:_.slice(1)})}let Q=/(^|\s)(@[a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;for(let X of z.matchAll(Q)){let Y=X[1],_=X[2];if(Y===void 0||_===void 0)continue;let H=(X.index??0)+Y.length,$=W(z.slice(0,H)),O=$+W(_);if(J($,O))continue;D.push({byteStart:$,byteEnd:O,type:"mention",value:_.slice(1).replace(/\.+$/,"")})}return D.sort((X,Y)=>X.byteStart-Y.byteStart)}class h{provider="bluesky";characterLimit=300;service;constructor(z={}){this.service=z.service||"https://bsky.social"}async createSession(z){let D=z.identifier.trim(),G=z.password.trim();if(!D)throw Error("Bluesky identifier is required.");if(!G)throw Error("Bluesky app password is required.");let J=await this.post("/xrpc/com.atproto.server.createSession",{identifier:D,password:G}),N=await this.getProfile({did:J.did,handle:J.handle,accessToken:J.accessJwt,refreshToken:J.refreshJwt}).catch(()=>{return});return{did:J.did,handle:J.handle,displayName:N?.displayName,accessJwt:J.accessJwt,refreshJwt:J.refreshJwt}}async refreshSession(z){if(!z)throw Error("Bluesky refresh token is required.");let D=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${z}`});return{did:D.did,handle:D.handle,accessJwt:D.accessJwt,refreshJwt:D.refreshJwt}}async publish(z,D){let G=z.did||z.handle;if(!z.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!G)throw Error("Bluesky identity DID or handle is required.");if(D.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let J={$type:"app.bsky.feed.post",text:D.text,createdAt:D.scheduledAt||new Date().toISOString()};if(D.langs?.length)J.langs=D.langs;if(D.reply)J.reply=D.reply;let N=D.facets??await this.buildFacets(D.text);if(N.length)J.facets=N;if(D.external)J.embed={$type:"app.bsky.embed.external",external:{uri:D.external.uri,title:D.external.title,description:D.external.description||""}};let Q=(D.media||[]).filter((Y)=>Y.bytes?.length).slice(0,4);if(Q.length){let Y=[];for(let _ of Q){let H=await this.uploadBlob(z,_.bytes,_.mimeType||"image/jpeg");Y.push({image:H,alt:_.altText||""})}J.embed={$type:"app.bsky.embed.images",images:Y}}let X=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:G,collection:"app.bsky.feed.post",record:J},{authorization:`Bearer ${z.accessToken}`});return{provider:this.provider,uri:X.uri,cid:X.cid,url:this.toPostUrl(z.handle,X.uri)}}async postMetrics(z,D){if(!z.accessToken)throw Error("Bluesky access token is missing for this identity.");if(D.length===0)return[];let G=new URL(`${this.service}/xrpc/app.bsky.feed.getPosts`);for(let N of D.slice(0,25))G.searchParams.append("uris",N);return((await this.request(G,{headers:{authorization:`Bearer ${z.accessToken}`}})).posts||[]).map((N)=>({uri:N.uri,likeCount:N.likeCount||0,repostCount:N.repostCount||0,replyCount:N.replyCount||0}))}async timeline(z,D={}){if(!z.accessToken)throw Error("Bluesky access token is missing for this identity.");let G=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(G.searchParams.set("limit",String(Math.min(Math.max(D.limit||30,1),100))),D.cursor)G.searchParams.set("cursor",D.cursor);let J=await this.request(G,{headers:{authorization:`Bearer ${z.accessToken}`}});return{cursor:J.cursor,items:(J.feed||[]).flatMap((N)=>{let Q=N.post;if(!Q?.uri||!Q.author?.handle)return[];return[{uri:Q.uri,authorHandle:Q.author.handle,authorName:Q.author.displayName,authorAvatar:Q.author.avatar,postUrl:this.toPostUrl(Q.author.handle,Q.uri),body:Q.record?.text||"",postedAt:Q.record?.createdAt||new Date().toISOString(),likeCount:Q.likeCount||0,repostCount:Q.repostCount||0,replyCount:Q.replyCount||0}]})}}async getProfile(z){if(!z.accessToken)throw Error("Bluesky access token is missing for this identity.");let D=z.did||z.handle;if(!D)throw Error("Bluesky identity DID or handle is required.");let G=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return G.searchParams.set("actor",D),await this.request(G,{headers:{authorization:`Bearer ${z.accessToken}`}})}async buildFacets(z){let D=[];for(let G of k(z)){let J=null;if(G.type==="link")J={$type:"app.bsky.richtext.facet#link",uri:G.value};else if(G.type==="tag")J={$type:"app.bsky.richtext.facet#tag",tag:G.value};else if(G.type==="mention"){let N=await this.resolveHandle(G.value);if(N)J={$type:"app.bsky.richtext.facet#mention",did:N}}if(J)D.push({index:{byteStart:G.byteStart,byteEnd:G.byteEnd},features:[J]})}return D}async resolveHandle(z){try{let D=new URL(`${this.service}/xrpc/com.atproto.identity.resolveHandle`);return D.searchParams.set("handle",z),(await this.request(D,{})).did||null}catch{return null}}async uploadBlob(z,D,G){if(!z.accessToken)throw Error("Bluesky access token is missing for this identity.");if(D.length>1e6)throw Error("Bluesky images must be 1MB or smaller.");return(await this.request(new URL(`${this.service}/xrpc/com.atproto.repo.uploadBlob`),{method:"POST",headers:{"content-type":G,authorization:`Bearer ${z.accessToken}`},body:new Uint8Array(D)})).blob}async post(z,D,G={}){return await this.request(new URL(`${this.service}${z}`),{method:"POST",headers:{...D===void 0?{}:{"content-type":"application/json"},...G},...D===void 0?{}:{body:JSON.stringify(D)}})}async request(z,D){let G=await fetch(z,D),J=await G.text();if(!G.ok)throw new T(`Bluesky API failed (${G.status}): ${J||G.statusText}`,G.status,J);return J?JSON.parse(J):{}}toPostUrl(z,D){let G=D.split("/").pop();return`https://bsky.app/profile/${z}/post/${G}`}}import{fetcher as j}from"@stacksjs/api";import{config as P}from"@stacksjs/config";class m extends F{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let z={clientId:P.services.facebook?.clientId??"",clientSecret:P.services.facebook?.clientSecret??"",redirectUrl:P.services.facebook?.redirectUrl??"",scopes:P.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(z.scopes),z}async getAuthUrl(){let z=this.getState(),{clientId:D,redirectUrl:G,scopes:J}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:D,redirect_uri:G,scope:J.join(","),state:z,response_type:"code"}).toString()}`}async getAccessToken(z){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();this.validateConfig();let N=await j.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:D,client_secret:G,redirect_uri:J,code:z}).toString()}`);if(N.data.error)throw Error(`Facebook OAuth error: ${N.data.error.message}`);return N.data.access_token}async getUserByToken(z){let D=await j.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:z,fields:"id,name,email,picture"}).toString()}`);return{id:D.data.id,nickname:null,name:D.data.name,email:D.data.email??null,avatar:D.data.picture?.data.url??null,token:z,raw:D.data}}validateConfig(){let{clientId:z,clientSecret:D,redirectUrl:G}=this.getConfig();if(!z)throw new Z("Facebook client ID not provided");if(!D)throw new Z("Facebook client secret not provided");if(!G)throw new Z("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as L}from"@stacksjs/api";import{config as V}from"@stacksjs/config";class g extends F{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let z={clientId:this.clientId||(V.services.github?.clientId??""),clientSecret:this.clientSecret||(V.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(V.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:V.services.github?.scopes??["read:user","user:email"]};return this.setScopes(z.scopes),z}async getAuthUrl(){let z=this.resolveState(),{clientId:D,redirectUrl:G,scopes:J}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:D,redirect_uri:G,scope:J.join(" "),state:z,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(z){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();this.validateConfig();let N=await L.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:D,client_secret:G,code:z,redirect_uri:J});if(N.data.error)throw Error(`GitHub OAuth error: ${N.data.error_description}`);return N.data.access_token}async getUserByToken(z){let[D,G]=await Promise.all([L.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${z}`}).get(`${this.apiUrl}/user`),L.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${z}`}).get(`${this.apiUrl}/user/emails`)]),J=this.pickEmail(G.data);return{id:D.data.id.toString(),nickname:D.data.login,name:D.data.name??D.data.login,email:J?.email??D.data.email??null,emailVerified:J?J.verified:null,avatar:D.data.avatar_url,token:z,raw:D.data}}pickEmail(z){if(!Array.isArray(z)||z.length===0)return null;let D=z.find((J)=>J.primary&&J.verified),G=z.find((J)=>J.verified);return D??G??z.find((J)=>J.primary)??z[0]??null}getEmail(z){return this.pickEmail(z)?.email??null}validateConfig(){let{clientId:z,clientSecret:D,redirectUrl:G}=this.getConfig();if(!z)throw new Z("GitHub client ID not provided");if(!D)throw new Z("GitHub client secret not provided");if(!G)throw new Z("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as v}from"@stacksjs/api";import{config as q}from"@stacksjs/config";class f extends F{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let z={clientId:this.clientId||(q.services.google?.clientId??""),clientSecret:this.clientSecret||(q.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(q.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:q.services.google?.scopes??["openid","email"]};return this.setScopes(z.scopes),z}async getAuthUrl(){let z=this.resolveState(),{clientId:D,redirectUrl:G,scopes:J}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:D,redirect_uri:G,scope:J.join(" "),state:z,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(z){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();this.validateConfig();let N=await v.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:D,client_secret:G,code:z,redirect_uri:J,grant_type:"authorization_code"});if(N.data.error)throw Error(`Google OAuth error: ${N.data.error_description}`);return N.data.access_token}async getUserByToken(z){let D=await v.withHeaders({Authorization:`Bearer ${z}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:D.data.id,nickname:D.data.given_name,name:D.data.name,email:D.data.email,emailVerified:typeof D.data.verified_email==="boolean"?D.data.verified_email:null,avatar:D.data.picture,token:z,raw:D.data}}validateConfig(){let{clientId:z,clientSecret:D,redirectUrl:G}=this.getConfig();if(!z)throw new Z("Google client ID not provided");if(!D)throw new Z("Google client secret not provided");if(!G)throw new Z("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}class w extends Error{status;body;constructor(z,D,G){super(z);this.status=D;this.body=G;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class u{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(z={}){this.graphVersion=z.graphVersion||"v21.0",this.authBase=z.authBase||"https://www.facebook.com",this.graphBase=z.graphBase||"https://graph.facebook.com"}getAuthUrl(z){let D=new URLSearchParams({client_id:z.clientId,redirect_uri:z.redirectUrl,scope:z.scopes.join(","),state:z.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${D.toString()}`}async exchangeCode(z){let D=new URLSearchParams({client_id:z.clientId,client_secret:z.clientSecret,redirect_uri:z.redirectUrl,code:z.code}),G=await this.graph(`/oauth/access_token?${D.toString()}`,{method:"GET"});if(!G.access_token)throw new w("Facebook did not return an access token.",400,JSON.stringify(G));return{accessToken:G.access_token,expiresIn:G.expires_in}}async resolveAccount(z){let D=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:z}),G=await this.graph(`/me/accounts?${D.toString()}`,{method:"GET"}),J=(G.data||[]).find((Q)=>Q.instagram_business_account?.id),N=J?.instagram_business_account;if(!N?.id||!J?.access_token)throw new w("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(G));return{igUserId:N.id,username:N.username,pageAccessToken:J.access_token}}async publish(z,D){if(!z.accessToken)throw Error("Instagram access token is missing for this identity.");let G=z.did;if(!G)throw Error("Instagram account id is required to publish.");let J=D.media?.[0];if(!J?.url)throw Error("Instagram requires an image to post.");if(D.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let N=await this.graph(`/${G}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:J.url,caption:D.text,access_token:z.accessToken}).toString()});if(!N.id)throw new w("Instagram did not return a media container id.",400,JSON.stringify(N));let Q=await this.graph(`/${G}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:N.id,access_token:z.accessToken}).toString()}),X=await this.graph(`/${Q.id}?fields=permalink&access_token=${encodeURIComponent(z.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:Q.id,url:X?.permalink}}async timeline(z,D={}){return{items:[]}}async graph(z,D){let G=await fetch(`${this.graphBase}/${this.graphVersion}${z}`,D),J=await G.text(),N={};try{N=J?JSON.parse(J):{}}catch{N={}}if(!G.ok||N?.error){let Q=N?.error?.message||J||G.statusText;throw new w(`Instagram API failed (${G.status}): ${Q}`,G.status,J)}return N}}class R extends Error{status;body;constructor(z,D,G){super(z);this.status=D;this.body=G;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class y{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(z={}){this.apiVersion=z.apiVersion||"202405",this.authBase=z.authBase||"https://www.linkedin.com",this.apiBase=z.apiBase||"https://api.linkedin.com"}getAuthUrl(z){let D=new URLSearchParams({response_type:"code",client_id:z.clientId,redirect_uri:z.redirectUrl,scope:z.scopes.join(" "),state:z.state});return`${this.authBase}/oauth/v2/authorization?${D.toString()}`}async exchangeCode(z){let D=new URLSearchParams({grant_type:"authorization_code",code:z.code,redirect_uri:z.redirectUrl,client_id:z.clientId,client_secret:z.clientSecret}),G=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:D.toString()});if(!G.access_token)throw new R("LinkedIn did not return an access token.",400,JSON.stringify(G));return{accessToken:G.access_token,expiresIn:G.expires_in,scope:G.scope}}async getProfile(z){if(!z)throw Error("LinkedIn access token is required.");let D=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${z}`}});if(!D.sub)throw new R("LinkedIn profile is missing a subject id.",400,JSON.stringify(D));return{sub:D.sub,name:D.name,picture:D.picture}}async publish(z,D){if(!z.accessToken)throw Error("LinkedIn access token is missing for this identity.");let G=z.did;if(!G)throw Error("LinkedIn member URN is required to publish.");if(D.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let J={author:G,commentary:l(D.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(D.external)J.content={article:{source:D.external.uri,title:D.external.title,description:D.external.description||""}};let N=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${z.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(J)}),Q=await N.text();if(!N.ok)throw new R(`LinkedIn API failed (${N.status}): ${Q||N.statusText}`,N.status,Q);let X=N.headers.get("x-restli-id")||N.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:X,url:X?`https://www.linkedin.com/feed/update/${X}`:void 0}}async timeline(z,D={}){return{items:[]}}async request(z,D){let G=await fetch(z,D),J=await G.text();if(!G.ok)throw new R(`LinkedIn API failed (${G.status}): ${J||G.statusText}`,G.status,J);return J?JSON.parse(J):{}}}function l(z){return z.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class K extends Error{status;body;constructor(z,D,G){super(z);this.status=D;this.body=G;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class c{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(z={}){this.graphVersion=z.graphVersion||"v1.0",this.authBase=z.authBase||"https://threads.net",this.graphBase=z.graphBase||"https://graph.threads.net"}getAuthUrl(z){let D=new URLSearchParams({client_id:z.clientId,redirect_uri:z.redirectUrl,scope:z.scopes.join(","),response_type:"code",state:z.state});return`${this.authBase}/oauth/authorize?${D.toString()}`}async exchangeCode(z){let D=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:z.clientId,client_secret:z.clientSecret,grant_type:"authorization_code",redirect_uri:z.redirectUrl,code:z.code}).toString()}),G=await D.text(),J={};try{J=G?JSON.parse(G):{}}catch{J={}}if(!D.ok||J?.error||!J?.access_token){let N=J?.error_message||J?.error?.message||G||D.statusText;throw new K(`Threads token exchange failed (${D.status}): ${N}`,D.status,G)}return{accessToken:J.access_token,userId:J.user_id!=null?String(J.user_id):void 0,expiresIn:J.expires_in}}async resolveAccount(z){let D=new URLSearchParams({fields:"id,username",access_token:z}),G=await this.graph(`/me?${D.toString()}`,{method:"GET"});if(!G.id)throw new K("Could not resolve the Threads account for this token.",400,JSON.stringify(G));return{threadsUserId:G.id,username:G.username,accessToken:z}}async publish(z,D){if(!z.accessToken)throw Error("Threads access token is missing for this identity.");let G=z.did;if(!G)throw Error("Threads account id is required to publish.");if(D.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let J=D.media?.[0],N=new URLSearchParams({text:D.text,access_token:z.accessToken});if(J?.url)N.set("media_type","IMAGE"),N.set("image_url",J.url);else N.set("media_type","TEXT");let Q=await this.graph(`/${G}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:N.toString()});if(!Q.id)throw new K("Threads did not return a media container id.",400,JSON.stringify(Q));let X=await this.graph(`/${G}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:Q.id,access_token:z.accessToken}).toString()});if(!X.id)throw new K("Threads did not return a published post id.",400,JSON.stringify(X));let Y=await this.graph(`/${X.id}?fields=permalink&access_token=${encodeURIComponent(z.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:X.id,url:Y?.permalink}}async timeline(z,D={}){return{items:[]}}async graph(z,D){let G=await fetch(`${this.graphBase}/${this.graphVersion}${z}`,D),J=await G.text(),N={};try{N=J?JSON.parse(J):{}}catch{N={}}if(!G.ok||N?.error){let Q=N?.error?.message||J||G.statusText;throw new K(`Threads API failed (${G.status}): ${Q}`,G.status,J)}return N}}import{Buffer as d}from"buffer";import{createHash as p,randomBytes as a}from"crypto";import{fetcher as A}from"@stacksjs/api";import{config as B}from"@stacksjs/config";class r extends F{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let z={clientId:B.services.twitter?.clientId??"",clientSecret:B.services.twitter?.clientSecret??"",redirectUrl:B.services.twitter?.redirectUrl??"",scopes:B.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(z.scopes),z}generateCodeVerifier(){return a(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(z){return p("sha256").update(z).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let z=this.getState(),{clientId:D,redirectUrl:G,scopes:J}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let N=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:D,redirect_uri:G,scope:J.join(" "),state:z,response_type:"code",code_challenge:N,code_challenge_method:"S256"}).toString()}`}async getAccessToken(z){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let N=d.from(`${D}:${G}`).toString("base64"),Q=await A.withHeaders({Authorization:`Basic ${N}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:z,grant_type:"authorization_code",redirect_uri:J,code_verifier:this.codeVerifier});if(Q.data.error)throw Error(`Twitter OAuth error: ${Q.data.error_description}`);return Q.data.access_token}async getUserByToken(z){let D=await A.withHeaders({Authorization:`Bearer ${z}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:D.data.id,nickname:D.data.username,name:D.data.name,email:D.data.email??null,avatar:D.data.profile_image_url??null,token:z,raw:D.data}}validateConfig(){let{clientId:z,clientSecret:D,redirectUrl:G}=this.getConfig();if(!z)throw new Z("Twitter client ID not provided");if(!D)throw new Z("Twitter client secret not provided");if(!G)throw new Z("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class o{accessToken;refreshToken;expiresIn;approvedScopes;constructor(z,D=null,G=null,J=[]){this.accessToken=z;this.refreshToken=D;this.expiresIn=G;this.approvedScopes=J}}export{l as escapeLinkedInText,k as detectFacetCandidates,r as TwitterProvider,o as Token,c as ThreadsPublishingDriver,K as ThreadsApiError,y as LinkedInPublishingDriver,R as LinkedInApiError,C as InvalidStateException,u as InstagramPublishingDriver,w as InstagramApiError,f as GoogleProvider,g as GitHubProvider,m as FacebookProvider,Z as ConfigException,h as BlueskyPublishingDriver,T as BlueskyApiError,b as AppleProvider,F as AbstractProvider};
2
+ var q=import.meta.require;class _{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_state=null;user=null;constructor(D){this.clientId=D.clientId,this.clientSecret=D.clientSecret,this.redirectUrl=D.redirectUrl}getCodeFields(D=null){let G={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())G.state=D;if(this.usesPKCE())G.code_challenge=this.getCodeChallenge(),G.code_challenge_method=this.getCodeChallengeMethod();return{...G,...this.parameters}}formatScopes(D,G){return D.join(G)}async userFromToken(D){return{...await this.getUserByToken(D),token:D}}scopes(D){let G=Array.isArray(D)?D:[D];return this._scopes=[...new Set([...this._scopes,...G])],this}setScopes(D){let G=Array.isArray(D)?D:[D];return this._scopes=[...new Set(G)],this}getScopes(){return this._scopes}setRedirectUrl(D){if(typeof D!=="string"||D.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let G;try{G=new URL(D)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${D}`)}if(G.protocol!=="https:"&&G.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${G.protocol}`);return this.redirectUrl=D,this}withState(D){if(typeof D!=="string"||D.length===0)throw Error("[socials] withState requires a non-empty string");return this._state=D,this}resolveState(){return this._state??this.getState()}usesState(){return!this._stateless}validateState(D,G){if(typeof D!=="string"||typeof G!=="string")return!1;if(D.length===0||G.length===0)return!1;if(D.length!==G.length)return!1;try{let{timingSafeEqual:J}=q("crypto");return J(Buffer.from(D,"utf8"),Buffer.from(G,"utf8"))}catch{let J=0;for(let N=0;N<D.length;N++)J|=D.charCodeAt(N)^G.charCodeAt(N);return J===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let D=new Uint8Array(32);return crypto.getRandomValues(D),Array.from(D).map((G)=>G.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let D=new Uint8Array(48);return crypto.getRandomValues(D),Array.from(D).map((G)=>G.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let G=new TextEncoder().encode(this.getCodeVerifier()),J=await crypto.subtle.digest("SHA-256",G),{Buffer:N}=await import("buffer");return N.from(new Uint8Array(J)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(D){return this.parameters=D,this}buildAuthUrlFromBase(D,G){let J=new URLSearchParams(this.getCodeFields(G));return`${D}?${J.toString()}`}}import{Buffer as j}from"buffer";import{createPrivateKey as k,sign as b}from"crypto";import{config as M}from"@stacksjs/config";class x extends Error{constructor(D="Invalid state"){super(D);this.name="InvalidStateException"}}class z extends Error{constructor(D){super(D);this.name="ConfigException"}}class h extends _{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";constructor(D){super(D);this.teamId=D.teamId??"",this.keyId=D.keyId??"",this.privateKey=D.privateKey??""}getConfig(){let D={clientId:this.clientId||(M.services.apple?.clientId??""),teamId:this.teamId||(M.services.apple?.teamId??""),keyId:this.keyId||(M.services.apple?.keyId??""),privateKey:(this.privateKey||(M.services.apple?.privateKey??"")).replace(/\\n/g,`
3
+ `),redirectUrl:this.redirectUrl||(M.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:M.services.apple?.scopes??["name","email"]};return this.setScopes(D.scopes),D}async getAuthUrl(){let D=this.resolveState(),{clientId:G,redirectUrl:J,scopes:N}=this.getConfig();this.validateConfig();let Q={client_id:G,redirect_uri:J,scope:N.join(" "),state:D,response_type:"code",...this.parameters};if(N.length>0)Q.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams(Q).toString()}`}async getAccessToken(D){let{clientId:G,redirectUrl:J}=this.getConfig();this.validateConfig();let N=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:D,redirect_uri:J,client_id:G,client_secret:this.generateClientSecret()})}),Q=await N.json();if(!N.ok||Q.error)throw Error(`Apple OAuth error: ${Q.error_description??Q.error??`HTTP ${N.status}`}`);if(!Q.id_token)throw Error("Apple OAuth error: token response contained no id_token");return Q.id_token}async getUserByToken(D){let{clientId:G}=this.getConfig(),J=this.decodeIdToken(D),N=J.iss===this.baseUrl,Q=Array.isArray(J.aud)?J.aud.includes(G):J.aud===G,X=typeof J.exp==="number"&&J.exp*1000>Date.now();if(!N||!Q||!X)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!J.sub)throw Error("Apple OAuth error: id_token has no subject");let Y=typeof J.email==="string"?J.email:null,Z=null;if(J.email_verified===!0||J.email_verified==="true")Z=!0;else if(J.email_verified===!1||J.email_verified==="false")Z=!1;return{id:String(J.sub),nickname:null,name:"",email:Y,emailVerified:Z,avatar:null,token:D,raw:J}}generateClientSecret(){let{clientId:D,teamId:G,keyId:J,privateKey:N}=this.getConfig(),Q=Math.floor(Date.now()/1000),X={alg:"ES256",kid:J,typ:"JWT"},Y={iss:G,iat:Q,exp:Q+3600,aud:this.baseUrl,sub:D},Z=`${this.base64urlJson(X)}.${this.base64urlJson(Y)}`,$;try{$=k(N)}catch(H){throw new z(`Apple private key could not be parsed: ${H instanceof Error?H.message:String(H)}`)}let F=b("sha256",j.from(Z),{key:$,dsaEncoding:"ieee-p1363"});return`${Z}.${F.toString("base64url")}`}decodeIdToken(D){let G=D.split(".");if(G.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(j.from(G[1],"base64url").toString("utf8"))}base64urlJson(D){return j.from(JSON.stringify(D)).toString("base64url")}validateConfig(){let{clientId:D,teamId:G,keyId:J,privateKey:N,redirectUrl:Q}=this.getConfig();if(!D)throw new z("Apple client ID (Service ID) not provided");if(!G)throw new z("Apple team ID not provided");if(!J)throw new z("Apple key ID not provided");if(!N)throw new z("Apple private key not provided");if(!Q)throw new z("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function g(D){let G=/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(String(D||"").trim());if(!G)throw Error(`"${D}" is not a Bluesky post URI.`);return{repo:G[1],collection:G[2],rkey:G[3]}}class C extends Error{status;body;constructor(D,G,J){super(D);this.status=G;this.body=J;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}var f=new TextEncoder;function O(D){return f.encode(D).length}function m(D){let G=[],J=/https?:\/\/[^\s<>"']+/g;for(let Y of D.matchAll(J)){let Z=Y[0].replace(/[),.;!?]+$/,"");G.push({byteStart:O(D.slice(0,Y.index)),byteEnd:O(D.slice(0,Y.index))+O(Z),type:"link",value:Z})}let N=(Y,Z)=>G.some(($)=>$.type==="link"&&Y<$.byteEnd&&Z>$.byteStart),Q=/(^|\s)(#[A-Za-z0-9_]+)/g;for(let Y of D.matchAll(Q)){let Z=Y[1],$=Y[2];if(Z===void 0||$===void 0)continue;if(/^#\d+$/.test($))continue;let F=(Y.index??0)+Z.length,H=O(D.slice(0,F)),V=H+O($);if(N(H,V))continue;G.push({byteStart:H,byteEnd:V,type:"tag",value:$.slice(1)})}let X=/(^|\s)(@[a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;for(let Y of D.matchAll(X)){let Z=Y[1],$=Y[2];if(Z===void 0||$===void 0)continue;let F=(Y.index??0)+Z.length,H=O(D.slice(0,F)),V=H+O($);if(N(H,V))continue;G.push({byteStart:H,byteEnd:V,type:"mention",value:$.slice(1).replace(/\.+$/,"")})}return G.sort((Y,Z)=>Y.byteStart-Z.byteStart)}class l{provider="bluesky";characterLimit=300;service;constructor(D={}){this.service=D.service||"https://bsky.social"}async createSession(D){let G=D.identifier.trim(),J=D.password.trim();if(!G)throw Error("Bluesky identifier is required.");if(!J)throw Error("Bluesky app password is required.");let N=await this.post("/xrpc/com.atproto.server.createSession",{identifier:G,password:J}),Q=await this.getProfile({did:N.did,handle:N.handle,accessToken:N.accessJwt,refreshToken:N.refreshJwt}).catch(()=>{return});return{did:N.did,handle:N.handle,displayName:Q?.displayName,accessJwt:N.accessJwt,refreshJwt:N.refreshJwt}}async refreshSession(D){if(!D)throw Error("Bluesky refresh token is required.");let G=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${D}`});return{did:G.did,handle:G.handle,accessJwt:G.accessJwt,refreshJwt:G.refreshJwt}}async publish(D,G){let J=D.did||D.handle;if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!J)throw Error("Bluesky identity DID or handle is required.");if(G.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let N={$type:"app.bsky.feed.post",text:G.text,createdAt:G.scheduledAt||new Date().toISOString()};if(G.langs?.length)N.langs=G.langs;if(G.reply)N.reply=G.reply;let Q=G.facets??await this.buildFacets(G.text);if(Q.length)N.facets=Q;if(G.external)N.embed={$type:"app.bsky.embed.external",external:{uri:G.external.uri,title:G.external.title,description:G.external.description||""}};let X=(G.media||[]).filter((Z)=>Z.bytes?.length).slice(0,4);if(X.length){let Z=[];for(let $ of X){let F=await this.uploadBlob(D,$.bytes,$.mimeType||"image/jpeg");Z.push({image:F,alt:$.altText||""})}N.embed={$type:"app.bsky.embed.images",images:Z}}let Y=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:J,collection:"app.bsky.feed.post",record:N},{authorization:`Bearer ${D.accessToken}`});return{provider:this.provider,uri:Y.uri,cid:Y.cid,url:this.toPostUrl(D.handle,Y.uri)}}async postMetrics(D,G){if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");if(G.length===0)return[];let J=new URL(`${this.service}/xrpc/app.bsky.feed.getPosts`);for(let Q of G.slice(0,25))J.searchParams.append("uris",Q);return((await this.request(J,{headers:{authorization:`Bearer ${D.accessToken}`}})).posts||[]).map((Q)=>({uri:Q.uri,likeCount:Q.likeCount||0,repostCount:Q.repostCount||0,replyCount:Q.replyCount||0}))}async timeline(D,G={}){if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");let J=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(J.searchParams.set("limit",String(Math.min(Math.max(G.limit||30,1),100))),G.cursor)J.searchParams.set("cursor",G.cursor);let N=await this.request(J,{headers:{authorization:`Bearer ${D.accessToken}`}});return{cursor:N.cursor,items:(N.feed||[]).flatMap((Q)=>{let X=Q.post;if(!X?.uri||!X.author?.handle)return[];return[{uri:X.uri,authorHandle:X.author.handle,authorName:X.author.displayName,authorAvatar:X.author.avatar,postUrl:this.toPostUrl(X.author.handle,X.uri),body:X.record?.text||"",postedAt:X.record?.createdAt||new Date().toISOString(),likeCount:X.likeCount||0,repostCount:X.repostCount||0,replyCount:X.replyCount||0}]})}}async getProfile(D){if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");let G=D.did||D.handle;if(!G)throw Error("Bluesky identity DID or handle is required.");let J=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return J.searchParams.set("actor",G),await this.request(J,{headers:{authorization:`Bearer ${D.accessToken}`}})}async buildFacets(D){let G=[];for(let J of m(D)){let N=null;if(J.type==="link")N={$type:"app.bsky.richtext.facet#link",uri:J.value};else if(J.type==="tag")N={$type:"app.bsky.richtext.facet#tag",tag:J.value};else if(J.type==="mention"){let Q=await this.resolveHandle(J.value);if(Q)N={$type:"app.bsky.richtext.facet#mention",did:Q}}if(N)G.push({index:{byteStart:J.byteStart,byteEnd:J.byteEnd},features:[N]})}return G}async resolveHandle(D){try{let G=new URL(`${this.service}/xrpc/com.atproto.identity.resolveHandle`);return G.searchParams.set("handle",D),(await this.request(G,{})).did||null}catch{return null}}async uploadBlob(D,G,J){if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");if(G.length>1e6)throw Error("Bluesky images must be 1MB or smaller.");return(await this.request(new URL(`${this.service}/xrpc/com.atproto.repo.uploadBlob`),{method:"POST",headers:{"content-type":J,authorization:`Bearer ${D.accessToken}`},body:new Uint8Array(G)})).blob}async listAuthoredPosts(D,G={}){let J=D.did||D.handle;if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!J)throw Error("Bluesky identity DID or handle is required.");let N=new URL(`${this.service}/xrpc/com.atproto.repo.listRecords`);if(N.searchParams.set("repo",J),N.searchParams.set("collection","app.bsky.feed.post"),N.searchParams.set("limit",String(Math.min(Math.max(G.limit||100,1),100))),G.cursor)N.searchParams.set("cursor",G.cursor);let Q=await this.request(N,{headers:{authorization:`Bearer ${D.accessToken}`}});return{cursor:Q.cursor,posts:(Q.records||[]).filter((X)=>X?.uri).map((X)=>({uri:X.uri,cid:X.cid,text:X.value?.text,postedAt:X.value?.createdAt,url:D.handle?this.toPostUrl(D.handle,X.uri):void 0}))}}async deletePost(D,G){if(!D.accessToken)throw Error("Bluesky access token is missing for this identity.");let{repo:J,collection:N,rkey:Q}=g(G.uri);await this.post("/xrpc/com.atproto.repo.deleteRecord",{repo:J,collection:N,rkey:Q},{authorization:`Bearer ${D.accessToken}`})}async post(D,G,J={}){return await this.request(new URL(`${this.service}${D}`),{method:"POST",headers:{...G===void 0?{}:{"content-type":"application/json"},...J},...G===void 0?{}:{body:JSON.stringify(G)}})}async request(D,G){let J=await fetch(D,G),N=await J.text();if(!J.ok)throw new C(`Bluesky API failed (${J.status}): ${N||J.statusText}`,J.status,N);return N?JSON.parse(N):{}}toPostUrl(D,G){let J=G.split("/").pop();return`https://bsky.app/profile/${D}/post/${J}`}}import{fetcher as v}from"@stacksjs/api";import{config as P}from"@stacksjs/config";class u extends _{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let D={clientId:P.services.facebook?.clientId??"",clientSecret:P.services.facebook?.clientSecret??"",redirectUrl:P.services.facebook?.redirectUrl??"",scopes:P.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(D.scopes),D}async getAuthUrl(){let D=this.getState(),{clientId:G,redirectUrl:J,scopes:N}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:G,redirect_uri:J,scope:N.join(","),state:D,response_type:"code"}).toString()}`}async getAccessToken(D){let{clientId:G,clientSecret:J,redirectUrl:N}=this.getConfig();this.validateConfig();let Q=await v.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:G,client_secret:J,redirect_uri:N,code:D}).toString()}`);if(Q.data.error)throw Error(`Facebook OAuth error: ${Q.data.error.message}`);return Q.data.access_token}async getUserByToken(D){let G=await v.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:D,fields:"id,name,email,picture"}).toString()}`);return{id:G.data.id,nickname:null,name:G.data.name,email:G.data.email??null,avatar:G.data.picture?.data.url??null,token:D,raw:G.data}}validateConfig(){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();if(!D)throw new z("Facebook client ID not provided");if(!G)throw new z("Facebook client secret not provided");if(!J)throw new z("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as w}from"@stacksjs/api";import{config as S}from"@stacksjs/config";class y extends _{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let D={clientId:this.clientId||(S.services.github?.clientId??""),clientSecret:this.clientSecret||(S.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(S.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:S.services.github?.scopes??["read:user","user:email"]};return this.setScopes(D.scopes),D}async getAuthUrl(){let D=this.resolveState(),{clientId:G,redirectUrl:J,scopes:N}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:G,redirect_uri:J,scope:N.join(" "),state:D,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(D){let{clientId:G,clientSecret:J,redirectUrl:N}=this.getConfig();this.validateConfig();let Q=await w.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:G,client_secret:J,code:D,redirect_uri:N});if(Q.data.error)throw Error(`GitHub OAuth error: ${Q.data.error_description}`);return Q.data.access_token}async getUserByToken(D){let[G,J]=await Promise.all([w.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${D}`}).get(`${this.apiUrl}/user`),w.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${D}`}).get(`${this.apiUrl}/user/emails`)]),N=this.pickEmail(J.data);return{id:G.data.id.toString(),nickname:G.data.login,name:G.data.name??G.data.login,email:N?.email??G.data.email??null,emailVerified:N?N.verified:null,avatar:G.data.avatar_url,token:D,raw:G.data}}pickEmail(D){if(!Array.isArray(D)||D.length===0)return null;let G=D.find((N)=>N.primary&&N.verified),J=D.find((N)=>N.verified);return G??J??D.find((N)=>N.primary)??D[0]??null}getEmail(D){return this.pickEmail(D)?.email??null}validateConfig(){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();if(!D)throw new z("GitHub client ID not provided");if(!G)throw new z("GitHub client secret not provided");if(!J)throw new z("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as A}from"@stacksjs/api";import{config as U}from"@stacksjs/config";class c extends _{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let D={clientId:this.clientId||(U.services.google?.clientId??""),clientSecret:this.clientSecret||(U.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(U.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:U.services.google?.scopes??["openid","email"]};return this.setScopes(D.scopes),D}async getAuthUrl(){let D=this.resolveState(),{clientId:G,redirectUrl:J,scopes:N}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:G,redirect_uri:J,scope:N.join(" "),state:D,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(D){let{clientId:G,clientSecret:J,redirectUrl:N}=this.getConfig();this.validateConfig();let Q=await A.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:G,client_secret:J,code:D,redirect_uri:N,grant_type:"authorization_code"});if(Q.data.error)throw Error(`Google OAuth error: ${Q.data.error_description}`);return Q.data.access_token}async getUserByToken(D){let G=await A.withHeaders({Authorization:`Bearer ${D}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:G.data.id,nickname:G.data.given_name,name:G.data.name,email:G.data.email,emailVerified:typeof G.data.verified_email==="boolean"?G.data.verified_email:null,avatar:G.data.picture,token:D,raw:G.data}}validateConfig(){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();if(!D)throw new z("Google client ID not provided");if(!G)throw new z("Google client secret not provided");if(!J)throw new z("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}class B extends Error{status;body;constructor(D,G,J){super(D);this.status=G;this.body=J;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class p{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(D={}){this.graphVersion=D.graphVersion||"v21.0",this.authBase=D.authBase||"https://www.facebook.com",this.graphBase=D.graphBase||"https://graph.facebook.com"}getAuthUrl(D){let G=new URLSearchParams({client_id:D.clientId,redirect_uri:D.redirectUrl,scope:D.scopes.join(","),state:D.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${G.toString()}`}async exchangeCode(D){let G=new URLSearchParams({client_id:D.clientId,client_secret:D.clientSecret,redirect_uri:D.redirectUrl,code:D.code}),J=await this.graph(`/oauth/access_token?${G.toString()}`,{method:"GET"});if(!J.access_token)throw new B("Facebook did not return an access token.",400,JSON.stringify(J));return{accessToken:J.access_token,expiresIn:J.expires_in}}async resolveAccount(D){let G=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:D}),J=await this.graph(`/me/accounts?${G.toString()}`,{method:"GET"}),N=(J.data||[]).find((X)=>X.instagram_business_account?.id),Q=N?.instagram_business_account;if(!Q?.id||!N?.access_token)throw new B("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(J));return{igUserId:Q.id,username:Q.username,pageAccessToken:N.access_token}}async publish(D,G){if(!D.accessToken)throw Error("Instagram access token is missing for this identity.");let J=D.did;if(!J)throw Error("Instagram account id is required to publish.");let N=G.media?.[0];if(!N?.url)throw Error("Instagram requires an image to post.");if(G.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let Q=await this.graph(`/${J}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:N.url,caption:G.text,access_token:D.accessToken}).toString()});if(!Q.id)throw new B("Instagram did not return a media container id.",400,JSON.stringify(Q));let X=await this.graph(`/${J}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:Q.id,access_token:D.accessToken}).toString()}),Y=await this.graph(`/${X.id}?fields=permalink&access_token=${encodeURIComponent(D.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:X.id,url:Y?.permalink}}async timeline(D,G={}){return{items:[]}}async graph(D,G){let J=await fetch(`${this.graphBase}/${this.graphVersion}${D}`,G),N=await J.text(),Q={};try{Q=N?JSON.parse(N):{}}catch{Q={}}if(!J.ok||Q?.error){let X=Q?.error?.message||N||J.statusText;throw new B(`Instagram API failed (${J.status}): ${X}`,J.status,N)}return Q}}class W extends Error{status;body;constructor(D,G,J){super(D);this.status=G;this.body=J;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class d{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(D={}){this.apiVersion=D.apiVersion||"202405",this.authBase=D.authBase||"https://www.linkedin.com",this.apiBase=D.apiBase||"https://api.linkedin.com"}getAuthUrl(D){let G=new URLSearchParams({response_type:"code",client_id:D.clientId,redirect_uri:D.redirectUrl,scope:D.scopes.join(" "),state:D.state});return`${this.authBase}/oauth/v2/authorization?${G.toString()}`}async exchangeCode(D){let G=new URLSearchParams({grant_type:"authorization_code",code:D.code,redirect_uri:D.redirectUrl,client_id:D.clientId,client_secret:D.clientSecret}),J=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:G.toString()});if(!J.access_token)throw new W("LinkedIn did not return an access token.",400,JSON.stringify(J));return{accessToken:J.access_token,expiresIn:J.expires_in,scope:J.scope}}async getProfile(D){if(!D)throw Error("LinkedIn access token is required.");let G=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${D}`}});if(!G.sub)throw new W("LinkedIn profile is missing a subject id.",400,JSON.stringify(G));return{sub:G.sub,name:G.name,picture:G.picture}}async publish(D,G){if(!D.accessToken)throw Error("LinkedIn access token is missing for this identity.");let J=D.did;if(!J)throw Error("LinkedIn member URN is required to publish.");if(G.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let N={author:J,commentary:a(G.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(G.external)N.content={article:{source:G.external.uri,title:G.external.title,description:G.external.description||""}};let Q=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${D.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(N)}),X=await Q.text();if(!Q.ok)throw new W(`LinkedIn API failed (${Q.status}): ${X||Q.statusText}`,Q.status,X);let Y=Q.headers.get("x-restli-id")||Q.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:Y,url:Y?`https://www.linkedin.com/feed/update/${Y}`:void 0}}async timeline(D,G={}){return{items:[]}}async listAuthoredPosts(D,G={}){if(!D.accessToken)throw Error("LinkedIn access token is missing for this identity.");let J=D.did;if(!J)throw Error("LinkedIn member URN is required to list posts.");let N=Math.min(Math.max(G.limit||50,1),100),Q=Number(G.cursor||0)||0,X=new URL(`${this.apiBase}/rest/posts`);X.searchParams.set("q","author"),X.searchParams.set("author",J),X.searchParams.set("count",String(N)),X.searchParams.set("start",String(Q));let Y;try{Y=await this.request(X.toString(),{headers:{authorization:`Bearer ${D.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}})}catch($){if($ instanceof W&&($.status===401||$.status===403))throw new W("LinkedIn will not list this account's posts \u2014 the Posts author finder needs the r_member_social permission, which this app does not hold.",$.status,$.body);throw $}let Z=(Y.elements||[]).filter(($)=>$?.id).map(($)=>({uri:String($.id),text:$.commentary,postedAt:$.createdAt?new Date($.createdAt).toISOString():void 0,url:`https://www.linkedin.com/feed/update/${$.id}`}));return{cursor:Z.length===N?String(Q+N):void 0,posts:Z}}async deletePost(D,G){if(!D.accessToken)throw Error("LinkedIn access token is missing for this identity.");let J=String(G.uri||"").trim();if(!J)throw Error("A LinkedIn post URN is required to delete a post.");let N=await fetch(`${this.apiBase}/rest/posts/${encodeURIComponent(J)}`,{method:"DELETE",headers:{authorization:`Bearer ${D.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}});if(!N.ok&&N.status!==404){let Q=await N.text().catch(()=>"");throw new W(`LinkedIn API failed (${N.status}): ${Q||N.statusText}`,N.status,Q)}}async request(D,G){let J=await fetch(D,G),N=await J.text();if(!J.ok)throw new W(`LinkedIn API failed (${J.status}): ${N||J.statusText}`,J.status,N);return N?JSON.parse(N):{}}}function a(D){return D.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class T extends Error{status;body;constructor(D,G,J){super(D);this.status=G;this.body=J;this.name="MastodonApiError"}get isAuthError(){return this.status===401||this.status===403}}function o(D){let G=String(D||"").trim().replace(/\/+$/,"");if(!G)throw Error("Mastodon instance URL is required.");let J=/^https?:\/\//i.test(G)?G:`https://${G}`;try{let N=new URL(J);return`${N.protocol}//${N.host}`}catch{throw Error("Mastodon instance URL is invalid.")}}class n{provider="mastodon";characterLimit=500;instanceOf(D){return o(D.did||"")}tokenOf(D){if(!D.accessToken)throw Error("Mastodon access token is missing for this identity.");return D.accessToken}async verifyCredentials(D){let G=await this.request(`${this.instanceOf(D)}/api/v1/accounts/verify_credentials`,{headers:{authorization:`Bearer ${this.tokenOf(D)}`}});return{accountId:G.id,username:G.username,displayName:G.display_name||void 0,url:G.url}}async uploadMedia(D,G,J,N){let Q=new FormData;if(Q.set("file",new Blob([new Uint8Array(G)],{type:J||"image/jpeg"}),"upload"),N)Q.set("description",N);return(await this.request(`${this.instanceOf(D)}/api/v2/media`,{method:"POST",headers:{authorization:`Bearer ${this.tokenOf(D)}`},body:Q})).id}async publish(D,G){let J=this.instanceOf(D),N=this.tokenOf(D);if(G.text.length>this.characterLimit)throw Error(`Mastodon posts must be ${this.characterLimit} characters or fewer.`);let Q=[];for(let Z of(G.media||[]).slice(0,4)){let{bytes:$,mimeType:F}=Z;if(!$?.length&&Z.url){let H=await fetch(Z.url);if(!H.ok)continue;$=new Uint8Array(await H.arrayBuffer()),F=F||H.headers.get("content-type")||"image/jpeg"}if($?.length)Q.push(await this.uploadMedia(D,$,F||"image/jpeg",Z.altText))}let X={status:G.text,visibility:"public"};if(Q.length)X.media_ids=Q;if(G.reply?.parent?.uri)X.in_reply_to_id=G.reply.parent.uri;let Y=await this.request(`${J}/api/v1/statuses`,{method:"POST",headers:{authorization:`Bearer ${N}`,"content-type":"application/json"},body:JSON.stringify(X)});return{provider:this.provider,uri:Y.id,cid:Y.id,url:Y.url||Y.uri}}async timeline(D,G={}){return{items:[]}}async listAuthoredPosts(D,G={}){let J=this.instanceOf(D),{accountId:N}=await this.verifyCredentials(D),Q=new URL(`${J}/api/v1/accounts/${encodeURIComponent(N)}/statuses`);if(Q.searchParams.set("limit",String(Math.min(Math.max(G.limit||40,1),40))),Q.searchParams.set("exclude_reblogs","true"),G.cursor)Q.searchParams.set("max_id",G.cursor);let Y=(await this.request(Q.toString(),{headers:{authorization:`Bearer ${this.tokenOf(D)}`}})||[]).filter((Z)=>Z?.id).map((Z)=>({uri:Z.id,cid:Z.id,text:Z.content,postedAt:Z.created_at,url:Z.url}));return{cursor:Y.length?Y[Y.length-1]?.uri:void 0,posts:Y}}async deletePost(D,G){let J=String(G.cid||s(G.uri)||"").trim();if(!J)throw Error("A status id is required to delete a post.");await this.request(`${this.instanceOf(D)}/api/v1/statuses/${encodeURIComponent(J)}`,{method:"DELETE",headers:{authorization:`Bearer ${this.tokenOf(D)}`}})}async request(D,G){let J=await fetch(D,G),N=await J.text();if(!J.ok)throw new T(`Mastodon API failed (${J.status}): ${N||J.statusText}`,J.status,N);return N?JSON.parse(N):{}}}function s(D){return String(D||"").replace(/\/+$/,"").split("/").pop()||""}class R extends Error{status;body;constructor(D,G,J){super(D);this.status=G;this.body=J;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class r{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(D={}){this.graphVersion=D.graphVersion||"v1.0",this.authBase=D.authBase||"https://threads.net",this.graphBase=D.graphBase||"https://graph.threads.net"}getAuthUrl(D){let G=new URLSearchParams({client_id:D.clientId,redirect_uri:D.redirectUrl,scope:D.scopes.join(","),response_type:"code",state:D.state});return`${this.authBase}/oauth/authorize?${G.toString()}`}async exchangeCode(D){let G=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:D.clientId,client_secret:D.clientSecret,grant_type:"authorization_code",redirect_uri:D.redirectUrl,code:D.code}).toString()}),J=await G.text(),N={};try{N=J?JSON.parse(J):{}}catch{N={}}if(!G.ok||N?.error||!N?.access_token){let Q=N?.error_message||N?.error?.message||J||G.statusText;throw new R(`Threads token exchange failed (${G.status}): ${Q}`,G.status,J)}return{accessToken:N.access_token,userId:N.user_id!=null?String(N.user_id):void 0,expiresIn:N.expires_in}}async resolveAccount(D){let G=new URLSearchParams({fields:"id,username",access_token:D}),J=await this.graph(`/me?${G.toString()}`,{method:"GET"});if(!J.id)throw new R("Could not resolve the Threads account for this token.",400,JSON.stringify(J));return{threadsUserId:J.id,username:J.username,accessToken:D}}async publish(D,G){if(!D.accessToken)throw Error("Threads access token is missing for this identity.");let J=D.did;if(!J)throw Error("Threads account id is required to publish.");if(G.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let N=G.media?.[0],Q=new URLSearchParams({text:G.text,access_token:D.accessToken});if(N?.url)Q.set("media_type","IMAGE"),Q.set("image_url",N.url);else Q.set("media_type","TEXT");let X=await this.graph(`/${J}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:Q.toString()});if(!X.id)throw new R("Threads did not return a media container id.",400,JSON.stringify(X));let Y=await this.graph(`/${J}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:X.id,access_token:D.accessToken}).toString()});if(!Y.id)throw new R("Threads did not return a published post id.",400,JSON.stringify(Y));let Z=await this.graph(`/${Y.id}?fields=permalink&access_token=${encodeURIComponent(D.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:Y.id,url:Z?.permalink}}async timeline(D,G={}){return{items:[]}}async graph(D,G){let J=await fetch(`${this.graphBase}/${this.graphVersion}${D}`,G),N=await J.text(),Q={};try{Q=N?JSON.parse(N):{}}catch{Q={}}if(!J.ok||Q?.error){let X=Q?.error?.message||N||J.statusText;throw new R(`Threads API failed (${J.status}): ${X}`,J.status,N)}return Q}}import{Buffer as i}from"buffer";import{createHash as t,randomBytes as e}from"crypto";import{fetcher as I}from"@stacksjs/api";import{config as L}from"@stacksjs/config";class DD extends _{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let D={clientId:L.services.twitter?.clientId??"",clientSecret:L.services.twitter?.clientSecret??"",redirectUrl:L.services.twitter?.redirectUrl??"",scopes:L.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(D.scopes),D}generateCodeVerifier(){return e(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(D){return t("sha256").update(D).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let D=this.getState(),{clientId:G,redirectUrl:J,scopes:N}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let Q=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:G,redirect_uri:J,scope:N.join(" "),state:D,response_type:"code",code_challenge:Q,code_challenge_method:"S256"}).toString()}`}async getAccessToken(D){let{clientId:G,clientSecret:J,redirectUrl:N}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let Q=i.from(`${G}:${J}`).toString("base64"),X=await I.withHeaders({Authorization:`Basic ${Q}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:D,grant_type:"authorization_code",redirect_uri:N,code_verifier:this.codeVerifier});if(X.data.error)throw Error(`Twitter OAuth error: ${X.data.error_description}`);return X.data.access_token}async getUserByToken(D){let G=await I.withHeaders({Authorization:`Bearer ${D}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:G.data.id,nickname:G.data.username,name:G.data.name,email:G.data.email??null,avatar:G.data.profile_image_url??null,token:D,raw:G.data}}validateConfig(){let{clientId:D,clientSecret:G,redirectUrl:J}=this.getConfig();if(!D)throw new z("Twitter client ID not provided");if(!G)throw new z("Twitter client secret not provided");if(!J)throw new z("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class K extends Error{status;body;constructor(D,G,J){super(D);this.status=G;this.body=J;this.name="TwitterApiError"}get isAuthError(){return this.status===401||this.status===403}}function E(D){let G="";for(let J of D)G+=String.fromCharCode(J);return btoa(G).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class GD{provider="twitter";characterLimit=280;apiBase;authorizeBase;constructor(D={}){this.apiBase=D.apiBase||"https://api.twitter.com",this.authorizeBase=D.authorizeBase||"https://twitter.com"}async createAuthorization(D){let G=E(crypto.getRandomValues(new Uint8Array(32))),J=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(G)),N=E(new Uint8Array(J)),Q=new URLSearchParams({response_type:"code",client_id:D.clientId,redirect_uri:D.redirectUrl,scope:D.scopes.join(" "),state:D.state,code_challenge:N,code_challenge_method:"S256"});return{url:`${this.authorizeBase}/i/oauth2/authorize?${Q.toString()}`,codeVerifier:G}}async exchangeCode(D){return this.tokenRequest(new URLSearchParams({grant_type:"authorization_code",code:D.code,redirect_uri:D.redirectUrl,code_verifier:D.codeVerifier,client_id:D.clientId}),D.clientId,D.clientSecret)}async refreshAccessToken(D){return this.tokenRequest(new URLSearchParams({grant_type:"refresh_token",refresh_token:D.refreshToken,client_id:D.clientId}),D.clientId,D.clientSecret)}async getProfile(D){let G=await this.request(`${this.apiBase}/2/users/me?user.fields=username,name`,{headers:{authorization:`Bearer ${D}`}});if(!G.data?.id||!G.data.username)throw new K("Twitter did not return the authenticated user.",400,JSON.stringify(G));return{id:G.data.id,username:G.data.username,name:G.data.name}}async uploadMedia(D,G,J){let N=new FormData;N.set("media",new Blob([new Uint8Array(G)],{type:J||"image/jpeg"})),N.set("media_category","tweet_image");let Q=await this.request(`${this.apiBase}/2/media/upload`,{method:"POST",headers:{authorization:`Bearer ${D}`},body:N}),X=Q.data?.id||Q.media_id_string||Q.id;if(!X)throw new K("Twitter did not return a media id.",400,JSON.stringify(Q));return X}async publish(D,G){if(!D.accessToken)throw Error("Twitter access token is missing for this identity.");if(G.text.length>this.characterLimit)throw Error(`Twitter posts must be ${this.characterLimit} characters or fewer.`);let J=[],N=G.media?.[0];if(N){let{bytes:Z,mimeType:$}=N;if(!Z?.length&&N.url){let F=await fetch(N.url);if(F.ok)Z=new Uint8Array(await F.arrayBuffer()),$=$||F.headers.get("content-type")||"image/jpeg"}if(Z?.length)J.push(await this.uploadMedia(D.accessToken,Z,$||"image/jpeg"))}let Q={text:G.text};if(J.length)Q.media={media_ids:J};if(G.reply?.parent?.uri)Q.reply={in_reply_to_tweet_id:G.reply.parent.uri};let X=await this.request(`${this.apiBase}/2/tweets`,{method:"POST",headers:{authorization:`Bearer ${D.accessToken}`,"content-type":"application/json"},body:JSON.stringify(Q)}),Y=X.data?.id;if(!Y)throw new K("Twitter did not return a tweet id.",400,JSON.stringify(X));return{provider:this.provider,uri:Y,cid:Y,url:D.handle?`https://x.com/${D.handle}/status/${Y}`:`https://x.com/i/web/status/${Y}`}}async timeline(D,G={}){return{items:[]}}async listAuthoredPosts(D,G={}){if(!D.accessToken)throw Error("Twitter access token is missing for this identity.");let J=D.did;if(!J)throw Error("Twitter user id is required to list posts.");let N=new URL(`${this.apiBase}/2/users/${encodeURIComponent(J)}/tweets`);if(N.searchParams.set("max_results",String(Math.min(Math.max(G.limit||100,5),100))),N.searchParams.set("tweet.fields","created_at"),G.cursor)N.searchParams.set("pagination_token",G.cursor);let Q=await this.request(N.toString(),{headers:{authorization:`Bearer ${D.accessToken}`}});return{cursor:Q.meta?.next_token,posts:(Q.data||[]).filter((X)=>X?.id).map((X)=>({uri:X.id,cid:X.id,text:X.text,postedAt:X.created_at,url:`https://x.com/i/web/status/${X.id}`}))}}async deletePost(D,G){if(!D.accessToken)throw Error("Twitter access token is missing for this identity.");let J=String(G.uri||"").trim();if(!J)throw Error("A tweet id is required to delete a post.");let N=await this.request(`${this.apiBase}/2/tweets/${encodeURIComponent(J)}`,{method:"DELETE",headers:{authorization:`Bearer ${D.accessToken}`}});if(N.data&&N.data.deleted===!1)throw new K(`X refused to delete tweet ${J}.`,400,JSON.stringify(N))}async tokenRequest(D,G,J){let N={"content-type":"application/x-www-form-urlencoded"};if(J)N.authorization=`Basic ${btoa(`${G}:${J}`)}`;let Q=await this.request(`${this.apiBase}/2/oauth2/token`,{method:"POST",headers:N,body:D.toString()});if(!Q.access_token)throw new K("Twitter did not return an access token.",400,JSON.stringify(Q));return{accessToken:Q.access_token,refreshToken:Q.refresh_token,expiresIn:Q.expires_in,scope:Q.scope}}async request(D,G){let J=await fetch(D,G),N=await J.text();if(!J.ok)throw new K(`Twitter API failed (${J.status}): ${N||J.statusText}`,J.status,N);return N?JSON.parse(N):{}}}class JD{accessToken;refreshToken;expiresIn;approvedScopes;constructor(D,G=null,J=null,N=[]){this.accessToken=D;this.refreshToken=G;this.expiresIn=J;this.approvedScopes=N}}function lD(D){return typeof D?.deletePost==="function"}function uD(D){return typeof D?.listAuthoredPosts==="function"}export{uD as supportsEnumeration,lD as supportsDeletion,g as parseAtUri,o as normalizeInstance,a as escapeLinkedInText,m as detectFacetCandidates,GD as TwitterPublishingDriver,DD as TwitterProvider,K as TwitterApiError,JD as Token,r as ThreadsPublishingDriver,R as ThreadsApiError,n as MastodonPublishingDriver,T as MastodonApiError,d as LinkedInPublishingDriver,W as LinkedInApiError,x as InvalidStateException,p as InstagramPublishingDriver,B as InstagramApiError,c as GoogleProvider,y as GitHubProvider,u as FacebookProvider,z as ConfigException,l as BlueskyPublishingDriver,C as BlueskyApiError,h as AppleProvider,_ as AbstractProvider};
package/dist/types.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /** Whether a driver can delete posts through its provider's API. */
2
+ export declare function supportsDeletion(driver: unknown): driver is SocialDeletionDriver;
3
+ /** Whether a driver can walk the connected account's own post history. */
4
+ export declare function supportsEnumeration(driver: unknown): driver is Required<Pick<SocialDeletionDriver, 'listAuthoredPosts'>> & SocialDeletionDriver;
1
5
  /**
2
6
  * Normalized user type that all providers will map their responses to.
3
7
  * This ensures a consistent user structure regardless of the provider used.
@@ -156,6 +160,51 @@ export declare interface SocialPublishingDriver {
156
160
  publish: (identity: SocialIdentityCredentials, post: PublishPostInput) => Promise<PublishedPost>
157
161
  timeline: (identity: SocialIdentityCredentials, query?: TimelineQuery) => Promise<TimelineResult>
158
162
  }
163
+ /**
164
+ * One post the connected account authored, as returned when walking that
165
+ * account's own history. `uri` is whatever the provider's delete endpoint
166
+ * keys on — an AT-URI on Bluesky, a numeric id on X and Mastodon, a URN on
167
+ * LinkedIn — so it can be handed straight back to `deletePost`.
168
+ */
169
+ export declare interface AuthoredPost {
170
+ uri: string
171
+ cid?: string
172
+ text?: string
173
+ postedAt?: string
174
+ url?: string
175
+ }
176
+ /** A page of authored posts plus the cursor for the next page, if any. */
177
+ export declare interface AuthoredPostPage {
178
+ cursor?: string
179
+ posts: AuthoredPost[]
180
+ }
181
+ /**
182
+ * The minimum needed to identify one remote post for deletion. `cid` matters
183
+ * where the delete key differs from the stored URI — Mastodon records a public
184
+ * status URL as its URI but deletes by status id.
185
+ */
186
+ export declare interface RemotePostRef {
187
+ uri: string
188
+ cid?: string
189
+ }
190
+ /**
191
+ * Deletion capability, kept separate from `SocialPublishingDriver` because
192
+ * publishing and deleting are independently available: Instagram and Threads
193
+ * can publish but their APIs cannot delete a feed post at all, and LinkedIn
194
+ * can delete every post it published while needing an extra partner
195
+ * permission before it will enumerate history.
196
+ *
197
+ * A driver implements what it can. Callers should feature-detect rather than
198
+ * assume, which `supportsDeletion`/`supportsEnumeration` make cheap.
199
+ */
200
+ export declare interface SocialDeletionDriver {
201
+ provider: SocialPublishingProvider
202
+ deletePost: (identity: SocialIdentityCredentials, ref: RemotePostRef) => Promise<void>
203
+ listAuthoredPosts?: (
204
+ identity: SocialIdentityCredentials,
205
+ query?: TimelineQuery,
206
+ ) => Promise<AuthoredPostPage>
207
+ }
159
208
  export type SocialPublishingProvider = | 'bluesky'
160
209
  | 'twitter'
161
210
  | 'mastodon'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/socials",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.230",
5
+ "version": "0.70.232",
6
6
  "description": "A simple and elegant social authentication package for Stacks.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "better-dx": "^0.2.17",
56
- "@stacksjs/error-handling": "0.70.230",
57
- "@stacksjs/router": "0.70.230"
56
+ "@stacksjs/error-handling": "0.70.232",
57
+ "@stacksjs/router": "0.70.232"
58
58
  }
59
59
  }