@stacksjs/socials 0.75.90 → 0.75.91

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.
@@ -0,0 +1,101 @@
1
+ import type { SocialCredentialPlatform } from '@stacksjs/types';
2
+ export declare function isAuthorizablePlatform(platform: SocialCredentialPlatform): platform is AuthorizablePlatform;
3
+ /**
4
+ * The redirect URI, which has to match the one registered with the platform
5
+ * exactly, character for character.
6
+ *
7
+ * `127.0.0.1` rather than `localhost`: X requires the literal loopback
8
+ * address for a native app's redirect, and the two are not interchangeable to
9
+ * a URI string comparison even though they resolve to the same host.
10
+ */
11
+ export declare function callbackUri(port: number): string;
12
+ /** An unguessable `state`, which is what ties a redirect to this run. */
13
+ export declare function newState(): string;
14
+ /**
15
+ * What came back on the redirect.
16
+ *
17
+ * `state` is checked before the code is used, and a mismatch is refused
18
+ * rather than logged: the state is the only thing distinguishing the redirect
19
+ * this run started from one somebody else aimed at the listener, and this
20
+ * listener is on a loopback port any local process can reach.
21
+ */
22
+ export declare function readCallback(url: string, expectedState: string): CallbackReading;
23
+ /**
24
+ * The lines to put in `.env`, in the order they are most usefully pasted.
25
+ *
26
+ * Printed rather than written. `.env` may be encrypted, `buddy env:set` owns
27
+ * writing to it, and a token passed as a shell argument lands in shell
28
+ * history, which is a worse place for it than a gitignored file.
29
+ */
30
+ export declare function envLines(identity: string, platform: SocialCredentialPlatform, values: Record<string, string | undefined>): string[];
31
+ /**
32
+ * Finish the flow: code to tokens to the account they belong to.
33
+ *
34
+ * Separate from the listener so the exchange can be driven by anything that
35
+ * has a code, a dashboard route included.
36
+ */
37
+ export declare function completeTwitterAuthorization(input: AuthorizeTwitterInput): Promise<AuthorizedIdentity>;
38
+ /** The consent URL to open, and the verifier the exchange needs back. */
39
+ export declare function startTwitterAuthorization(input: {
40
+ clientId: string
41
+ redirectUrl: string
42
+ state: string
43
+ }): Promise<{ url: string, codeVerifier: string }>;
44
+ /** What the browser is shown once the redirect lands, so it does not look broken. */
45
+ export declare function callbackPage(title: string, detail: string): string;
46
+ /**
47
+ * The scopes X needs to post as the authorized account.
48
+ *
49
+ * `offline.access` is the one that is easy to miss and expensive to miss:
50
+ * without it X issues no refresh token, so the access token expires in two
51
+ * hours and the identity has to be re-authorized by hand. `users.read` is
52
+ * what makes `getProfile` work, which is how the flow can report WHICH
53
+ * account was just authorized rather than only that one was.
54
+ */
55
+ export declare const TWITTER_PUBLISH_SCOPES: readonly ['tweet.read', 'tweet.write', 'users.read', 'offline.access'];
56
+ /** Loopback, so the redirect never leaves the machine running the flow. */
57
+ export declare const AUTHORIZE_HOST: '127.0.0.1';
58
+ export declare const AUTHORIZE_PATH: '/callback';
59
+ export declare interface AuthorizedIdentity {
60
+ handle: string
61
+ accountId: string
62
+ accessToken: string
63
+ refreshToken?: string
64
+ expiresIn?: number
65
+ scope?: string
66
+ renewable: boolean
67
+ }
68
+ export declare interface AuthorizeTwitterInput {
69
+ clientId: string
70
+ clientSecret?: string
71
+ redirectUrl: string
72
+ code: string
73
+ codeVerifier: string
74
+ }
75
+ /**
76
+ * Running a consent flow from a terminal, so an identity can be authorized
77
+ * without a dashboard.
78
+ *
79
+ * X is the reason this exists. Bluesky's app password and Mastodon's manually
80
+ * generated token are long-lived values an operator can paste into `.env`
81
+ * complete; X posts with a user-context OAuth 2.0 token, so the access and
82
+ * refresh tokens come out of a consent flow and cannot be prepared ahead of
83
+ * time (stacksjs/stacks#2873). Declaring the platform without them leaves an
84
+ * identity that reports it is not authorized, which is honest and not
85
+ * useful.
86
+ *
87
+ * The driver already had every piece: `createAuthorization` with PKCE,
88
+ * `exchangeCode`, `refreshAccessToken` and `getProfile`. What was missing is
89
+ * the half a human is part of: somewhere for the browser to come back to, and
90
+ * the values printed in a form that can be put in `.env`.
91
+ *
92
+ * LinkedIn, Instagram and Threads have the same property and their own
93
+ * `getAuthUrl` / `exchangeCode` pairs. They are NOT wired up here: each
94
+ * takes a different input shape and needs its own app registration, and
95
+ * claiming a generic flow that has only ever run against one platform would
96
+ * be a worse starting point than an honest single one.
97
+ */
98
+ /** The platforms this flow can run. */
99
+ export type AuthorizablePlatform = 'twitter';
100
+ export type CallbackReading = | { ok: true, code: string }
101
+ | { ok: false, reason: 'not-the-callback' | 'declined' | 'state-mismatch' | 'no-code', message: string }
@@ -1,4 +1,4 @@
1
- import type { BlueskyCredentialOptions, SocialCredentialPlatform, SocialCredentialsByPlatform, SocialIdentityOptions, TwitterCredentialOptions } from '@stacksjs/types';
1
+ import type { BlueskyCredentialOptions, InstagramCredentialOptions, LinkedInCredentialOptions, MastodonCredentialOptions, SocialCredentialPlatform, SocialCredentialsByPlatform, SocialIdentityOptions, ThreadsCredentialOptions, TwitterCredentialOptions } from '@stacksjs/types';
2
2
  /**
3
3
  * The environment segment for an identity name.
4
4
  *
@@ -11,6 +11,16 @@ import type { BlueskyCredentialOptions, SocialCredentialPlatform, SocialCredenti
11
11
  export declare function envSegment(identity: string): string;
12
12
  /** `SOCIALS_STACKS_BLUESKY_PASSWORD`, for the error messages and the docs. */
13
13
  export declare function envName(identity: string, platform: SocialCredentialPlatform, field: string): string;
14
+ /**
15
+ * This application's `socials` configuration, after the overrides have
16
+ * merged.
17
+ *
18
+ * Exported so the publishing layer reads it through the same barrier rather
19
+ * than reaching for `config` itself: an early read answers with framework
20
+ * defaults, and a credential resolved from a default is an empty string that
21
+ * fails much later and somewhere else (stacksjs/stacks#2333).
22
+ */
23
+ export declare function socialsConfig(): Promise<SocialsShape>;
14
24
  /**
15
25
  * Which identity a call acts as, given the configuration.
16
26
  *
@@ -23,6 +33,17 @@ export declare function envName(identity: string, platform: SocialCredentialPlat
23
33
  * ambiguity is reported at the call instead of resolved by position.
24
34
  */
25
35
  export declare function pickIdentity(socials: SocialsShape, identity?: string): string;
36
+ /**
37
+ * Whatever is configured for one identity on one platform, without judging it.
38
+ *
39
+ * Inline `credentials` win; otherwise the environment convention applies.
40
+ *
41
+ * Separate from {@link pickCredentials} because a consent flow needs exactly
42
+ * this: it reads `clientId` and `clientSecret` in order to GO AND GET the
43
+ * access token, so the validator that insists on an access token would refuse
44
+ * the one command that can produce one.
45
+ */
46
+ export declare function readCredentials<P extends SocialCredentialPlatform>(socials: SocialsShape, platform: P, identity: string, env?: Record<string, string | undefined>): Partial<SocialCredentialsByPlatform[P]>;
26
47
  /**
27
48
  * The credentials for one identity on one platform, given the configuration.
28
49
  *
@@ -38,6 +59,46 @@ export declare function resolveSocialCredentials<P extends SocialCredentialPlatf
38
59
  /** Typed shorthands, so a caller does not restate the platform twice. */
39
60
  export declare function resolveBlueskyCredentials(identity?: string, env?: Record<string, string | undefined>): Promise<BlueskyCredentialOptions>;
40
61
  export declare function resolveTwitterCredentials(identity?: string, env?: Record<string, string | undefined>): Promise<TwitterCredentialOptions>;
62
+ export declare function resolveLinkedInCredentials(identity?: string, env?: Record<string, string | undefined>): Promise<LinkedInCredentialOptions>;
63
+ export declare function resolveMastodonCredentials(identity?: string, env?: Record<string, string | undefined>): Promise<MastodonCredentialOptions>;
64
+ export declare function resolveInstagramCredentials(identity?: string, env?: Record<string, string | undefined>): Promise<InstagramCredentialOptions>;
65
+ export declare function resolveThreadsCredentials(identity?: string, env?: Record<string, string | undefined>): Promise<ThreadsCredentialOptions>;
66
+ /**
67
+ * Where a social credential comes from.
68
+ *
69
+ * Six publishing drivers shipped before there was anywhere to declare their
70
+ * credentials, so `SocialIdentityCredentials` was passed in per call and every
71
+ * application invented its own convention (stacksjs/stacks#2873). This
72
+ * resolves them once, from one place, the way `initializeIntegration()` does
73
+ * for telemetry.
74
+ *
75
+ * Values are read lazily, inside the call. Reading `config` at module load
76
+ * returns framework defaults rather than the application's own values,
77
+ * because the override pass has not finished yet, and a credential resolved
78
+ * from a default is an empty string that fails much later and somewhere else.
79
+ */
80
+ /**
81
+ * The field names each platform's credentials are read from, in env order.
82
+ *
83
+ * Exported so a test can hold them against the drivers rather than against
84
+ * this file: these tables ARE the claim that a platform's credentials are
85
+ * complete, and a claim nothing checks is a comment.
86
+ */
87
+ export declare const SOCIAL_CREDENTIAL_FIELDS: Record<SocialCredentialPlatform, readonly string[]>;
88
+ /**
89
+ * The fields a platform cannot act without.
90
+ *
91
+ * Taken from what each driver's `publish()` dereferences and throws over, not
92
+ * from what its API documents. LinkedIn, Mastodon, Instagram and Threads each
93
+ * need a second value besides the token - the member URN, the instance, the
94
+ * account id, the user id - because a token alone does not say where or as
95
+ * whom to post, and each driver refuses without it.
96
+ *
97
+ * `clientId` and `clientSecret` are NOT required for those four: they are for
98
+ * refreshing, and an identity with a live token posts without them. Requiring
99
+ * them would refuse a working configuration.
100
+ */
101
+ export declare const REQUIRED_SOCIAL_CREDENTIALS: Record<SocialCredentialPlatform, readonly string[]>;
41
102
  export declare interface SocialsShape {
42
103
  default?: string
43
104
  identities?: Record<string, SocialIdentityOptions>
package/dist/index.d.ts CHANGED
@@ -6,4 +6,6 @@ export * from './token';
6
6
  export * from './types';
7
7
  export * from './handoff';
8
8
  export * from './identities';
9
+ export * from './authorize';
10
+ export * from './publish';
9
11
  export * from './meta-graph';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // @bun
2
- var K=import.meta.require;class p{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_state=null;user=null;constructor(e){this.clientId=e.clientId,this.clientSecret=e.clientSecret,this.redirectUrl=e.redirectUrl}getCodeFields(e=null){let t={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())t.state=e;if(this.usesPKCE())t.code_challenge=this.getCodeChallenge(),t.code_challenge_method=this.getCodeChallengeMethod();return{...t,...this.parameters}}formatScopes(e,t){return e.join(t)}async userFromToken(e){return{...await this.getUserByToken(e),token:e}}scopes(e){let t=Array.isArray(e)?e:[e];return this._scopes=[...new Set([...this._scopes,...t])],this}setScopes(e){let t=Array.isArray(e)?e:[e];return this._scopes=[...new Set(t)],this}getScopes(){return this._scopes}setRedirectUrl(e){if(typeof e!=="string"||e.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let t;try{t=new URL(e)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${e}`)}if(t.protocol!=="https:"&&t.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${t.protocol}`);return this.redirectUrl=e,this}withState(e){if(typeof e!=="string"||e.length===0)throw Error("[socials] withState requires a non-empty string");return this._state=e,this}resolveState(){return this._state??this.getState()}usesState(){return!this._stateless}validateState(e,t){if(typeof e!=="string"||typeof t!=="string")return!1;if(e.length===0||t.length===0)return!1;if(e.length!==t.length)return!1;try{let{timingSafeEqual:r}=K("crypto");return r(Buffer.from(e,"utf8"),Buffer.from(t,"utf8"))}catch{let r=0;for(let i=0;i<e.length;i++)r|=e.charCodeAt(i)^t.charCodeAt(i);return r===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let e=new Uint8Array(32);return crypto.getRandomValues(e),Array.from(e).map((t)=>t.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let e=new Uint8Array(48);return crypto.getRandomValues(e),Array.from(e).map((t)=>t.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let t=new TextEncoder().encode(this.getCodeVerifier()),r=await crypto.subtle.digest("SHA-256",t),{Buffer:i}=await import("buffer");return i.from(new Uint8Array(r)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(e){return this.parameters=e,this}buildAuthUrlFromBase(e,t){let r=new URLSearchParams(this.getCodeFields(t));return`${e}?${r.toString()}`}}import{Buffer as U}from"buffer";import{createPrivateKey as X,sign as Z}from"crypto";import{config as f}from"@stacksjs/config";class W extends Error{constructor(e="Invalid state"){super(e);this.name="InvalidStateException"}}class l extends Error{constructor(e){super(e);this.name="ConfigException"}}class C extends p{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";constructor(e){super(e);this.teamId=e.teamId??"",this.keyId=e.keyId??"",this.privateKey=e.privateKey??""}getConfig(){let e={clientId:this.clientId||(f.services.apple?.clientId??""),teamId:this.teamId||(f.services.apple?.teamId??""),keyId:this.keyId||(f.services.apple?.keyId??""),privateKey:(this.privateKey||(f.services.apple?.privateKey??"")).replace(/\\n/g,`
3
- `),redirectUrl:this.redirectUrl||(f.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:f.services.apple?.scopes??["name","email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.resolveState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();this.validateConfig();let s={client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",...this.parameters};if(i.length>0)s.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams(s).toString()}`}async getAccessToken(e){let{clientId:t,redirectUrl:r}=this.getConfig();this.validateConfig();let i=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:r,client_id:t,client_secret:this.generateClientSecret()})}),s=await i.json();if(!i.ok||s.error)throw Error(`Apple OAuth error: ${s.error_description??s.error??`HTTP ${i.status}`}`);if(!s.id_token)throw Error("Apple OAuth error: token response contained no id_token");return s.id_token}async getUserByToken(e){let{clientId:t}=this.getConfig(),r=this.decodeIdToken(e),i=r.iss===this.baseUrl,s=Array.isArray(r.aud)?r.aud.includes(t):r.aud===t,n=typeof r.exp==="number"&&r.exp*1000>Date.now();if(!i||!s||!n)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!r.sub)throw Error("Apple OAuth error: id_token has no subject");let o=typeof r.email==="string"?r.email:null,a=null;if(r.email_verified===!0||r.email_verified==="true")a=!0;else if(r.email_verified===!1||r.email_verified==="false")a=!1;return{id:String(r.sub),nickname:null,name:"",email:o,emailVerified:a,avatar:null,token:e,raw:r}}generateClientSecret(){let{clientId:e,teamId:t,keyId:r,privateKey:i}=this.getConfig(),s=Math.floor(Date.now()/1000),n={alg:"ES256",kid:r,typ:"JWT"},o={iss:t,iat:s,exp:s+3600,aud:this.baseUrl,sub:e},a=`${this.base64urlJson(n)}.${this.base64urlJson(o)}`,c;try{c=X(i)}catch(u){throw new l(`Apple private key could not be parsed: ${u instanceof Error?u.message:String(u)}`)}let d=Z("sha256",U.from(a),{key:c,dsaEncoding:"ieee-p1363"});return`${a}.${d.toString("base64url")}`}decodeIdToken(e){let t=e.split(".");if(t.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(U.from(t[1],"base64url").toString("utf8"))}base64urlJson(e){return U.from(JSON.stringify(e)).toString("base64url")}validateConfig(){let{clientId:e,teamId:t,keyId:r,privateKey:i,redirectUrl:s}=this.getConfig();if(!e)throw new l("Apple client ID (Service ID) not provided");if(!t)throw new l("Apple team ID not provided");if(!r)throw new l("Apple key ID not provided");if(!i)throw new l("Apple private key not provided");if(!s)throw new l("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function Y(e){let t=/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(String(e||"").trim());if(!t)throw Error(`"${e}" is not a Bluesky post URI.`);return{repo:t[1],collection:t[2],rkey:t[3]}}class O extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}var ee=new TextEncoder;function g(e){return ee.encode(e).length}function te(e){let t=[],r=/https?:\/\/[^\s<>"']+/g;for(let o of e.matchAll(r)){let a=o[0].replace(/[),.;!?]+$/,"");t.push({byteStart:g(e.slice(0,o.index)),byteEnd:g(e.slice(0,o.index))+g(a),type:"link",value:a})}let i=(o,a)=>t.some((c)=>c.type==="link"&&o<c.byteEnd&&a>c.byteStart),s=/(^|\s)(#[A-Za-z0-9_]+)/g;for(let o of e.matchAll(s)){let a=o[1],c=o[2];if(a===void 0||c===void 0)continue;if(/^#\d+$/.test(c))continue;let d=(o.index??0)+a.length,u=g(e.slice(0,d)),k=u+g(c);if(i(u,k))continue;t.push({byteStart:u,byteEnd:k,type:"tag",value:c.slice(1)})}let n=/(^|\s)(@[a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;for(let o of e.matchAll(n)){let a=o[1],c=o[2];if(a===void 0||c===void 0)continue;let d=(o.index??0)+a.length,u=g(e.slice(0,d)),k=u+g(c);if(i(u,k))continue;t.push({byteStart:u,byteEnd:k,type:"mention",value:c.slice(1).replace(/\.+$/,"")})}return t.sort((o,a)=>o.byteStart-a.byteStart)}class re{provider="bluesky";characterLimit=300;service;constructor(e={}){this.service=e.service||"https://bsky.social"}async createSession(e){let t=e.identifier.trim(),r=e.password.trim();if(!t)throw Error("Bluesky identifier is required.");if(!r)throw Error("Bluesky app password is required.");let i=await this.post("/xrpc/com.atproto.server.createSession",{identifier:t,password:r}),s=await this.getProfile({did:i.did,handle:i.handle,accessToken:i.accessJwt,refreshToken:i.refreshJwt}).catch(()=>{return});return{did:i.did,handle:i.handle,displayName:s?.displayName,accessJwt:i.accessJwt,refreshJwt:i.refreshJwt}}async refreshSession(e){if(!e)throw Error("Bluesky refresh token is required.");let t=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${e}`});return{did:t.did,handle:t.handle,accessJwt:t.accessJwt,refreshJwt:t.refreshJwt}}async publish(e,t){let r=e.did||e.handle;if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!r)throw Error("Bluesky identity DID or handle is required.");if(t.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let i={$type:"app.bsky.feed.post",text:t.text,createdAt:t.scheduledAt||new Date().toISOString()};if(t.langs?.length)i.langs=t.langs;if(t.reply)i.reply=t.reply;let s=t.facets??await this.buildFacets(t.text);if(s.length)i.facets=s;if(t.external)i.embed={$type:"app.bsky.embed.external",external:{uri:t.external.uri,title:t.external.title,description:t.external.description||""}};let n=(t.media||[]).filter((a)=>a.bytes?.length).slice(0,4);if(n.length){let a=[];for(let c of n){let d=await this.uploadBlob(e,c.bytes,c.mimeType||"image/jpeg");a.push({image:d,alt:c.altText||""})}i.embed={$type:"app.bsky.embed.images",images:a}}let o=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:r,collection:"app.bsky.feed.post",record:i},{authorization:`Bearer ${e.accessToken}`});return{provider:this.provider,uri:o.uri,cid:o.cid,url:this.toPostUrl(e.handle,o.uri)}}async postMetrics(e,t){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(t.length===0)return[];let r=new URL(`${this.service}/xrpc/app.bsky.feed.getPosts`);for(let s of t.slice(0,25))r.searchParams.append("uris",s);return((await this.request(r,{headers:{authorization:`Bearer ${e.accessToken}`}})).posts||[]).map((s)=>({uri:s.uri,likeCount:s.likeCount||0,repostCount:s.repostCount||0,replyCount:s.replyCount||0}))}async timeline(e,t={}){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");let r=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(r.searchParams.set("limit",String(Math.min(Math.max(t.limit||30,1),100))),t.cursor)r.searchParams.set("cursor",t.cursor);let i=await this.request(r,{headers:{authorization:`Bearer ${e.accessToken}`}});return{cursor:i.cursor,items:(i.feed||[]).flatMap((s)=>{let n=s.post;if(!n?.uri||!n.author?.handle)return[];return[{uri:n.uri,authorHandle:n.author.handle,authorName:n.author.displayName,authorAvatar:n.author.avatar,postUrl:this.toPostUrl(n.author.handle,n.uri),body:n.record?.text||"",postedAt:n.record?.createdAt||new Date().toISOString(),likeCount:n.likeCount||0,repostCount:n.repostCount||0,replyCount:n.replyCount||0}]})}}async getProfile(e){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");let t=e.did||e.handle;if(!t)throw Error("Bluesky identity DID or handle is required.");let r=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return r.searchParams.set("actor",t),await this.request(r,{headers:{authorization:`Bearer ${e.accessToken}`}})}async buildFacets(e){let t=[];for(let r of te(e)){let i=null;if(r.type==="link")i={$type:"app.bsky.richtext.facet#link",uri:r.value};else if(r.type==="tag")i={$type:"app.bsky.richtext.facet#tag",tag:r.value};else if(r.type==="mention"){let s=await this.resolveHandle(r.value);if(s)i={$type:"app.bsky.richtext.facet#mention",did:s}}if(i)t.push({index:{byteStart:r.byteStart,byteEnd:r.byteEnd},features:[i]})}return t}async resolveHandle(e){try{let t=new URL(`${this.service}/xrpc/com.atproto.identity.resolveHandle`);return t.searchParams.set("handle",e),(await this.request(t,{})).did||null}catch{return null}}async uploadBlob(e,t,r){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(t.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":r,authorization:`Bearer ${e.accessToken}`},body:new Uint8Array(t)})).blob}async listAuthoredPosts(e,t={}){let r=e.did||e.handle;if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!r)throw Error("Bluesky identity DID or handle is required.");let i=new URL(`${this.service}/xrpc/com.atproto.repo.listRecords`);if(i.searchParams.set("repo",r),i.searchParams.set("collection","app.bsky.feed.post"),i.searchParams.set("limit",String(Math.min(Math.max(t.limit||100,1),100))),t.cursor)i.searchParams.set("cursor",t.cursor);let s=await this.request(i,{headers:{authorization:`Bearer ${e.accessToken}`}});return{cursor:s.cursor,posts:(s.records||[]).filter((n)=>n?.uri).map((n)=>({uri:n.uri,cid:n.cid,text:n.value?.text,postedAt:n.value?.createdAt,url:e.handle?this.toPostUrl(e.handle,n.uri):void 0}))}}async deletePost(e,t){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");let{repo:r,collection:i,rkey:s}=Y(t.uri);await this.post("/xrpc/com.atproto.repo.deleteRecord",{repo:r,collection:i,rkey:s},{authorization:`Bearer ${e.accessToken}`})}async post(e,t,r={}){return await this.request(new URL(`${this.service}${e}`),{method:"POST",headers:{...t===void 0?{}:{"content-type":"application/json"},...r},...t===void 0?{}:{body:JSON.stringify(t)}})}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new O(`Bluesky API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}toPostUrl(e,t){let r=t.split("/").pop();return`https://bsky.app/profile/${e}/post/${r}`}}import{fetcher as D}from"@stacksjs/api";import{config as b}from"@stacksjs/config";var v="v24.0";class A extends p{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";get graphVersion(){return b.services.facebook?.graphVersion??v}getConfig(){let e={clientId:b.services.facebook?.clientId??"",clientSecret:b.services.facebook?.clientSecret??"",redirectUrl:b.services.facebook?.redirectUrl??"",scopes:b.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.getState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/${this.graphVersion}/dialog/oauth?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(","),state:e,response_type:"code"}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await D.get(`${this.apiUrl}/${this.graphVersion}/oauth/access_token?${new URLSearchParams({client_id:t,client_secret:r,redirect_uri:i,code:e}).toString()}`);if(s.data.error)throw Error(`Facebook OAuth error: ${s.data.error.message}`);return s.data.access_token}async getUserByToken(e){let t=await D.get(`${this.apiUrl}/${this.graphVersion}/me?${new URLSearchParams({access_token:e,fields:"id,name,email,picture"}).toString()}`);return{id:t.data.id,nickname:null,name:t.data.name,email:t.data.email??null,avatar:t.data.picture?.data.url??null,token:e,raw:t.data}}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("Facebook client ID not provided");if(!t)throw new l("Facebook client secret not provided");if(!r)throw new l("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/${this.graphVersion}/oauth/access_token`}}import{fetcher as R}from"@stacksjs/api";import{config as I}from"@stacksjs/config";class E extends p{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let e={clientId:this.clientId||(I.services.github?.clientId??""),clientSecret:this.clientSecret||(I.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(I.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:I.services.github?.scopes??["read:user","user:email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.resolveState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await R.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:t,client_secret:r,code:e,redirect_uri:i});if(s.data.error)throw Error(`GitHub OAuth error: ${s.data.error_description}`);return s.data.access_token}async getUserByToken(e){let[t,r]=await Promise.all([R.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${e}`}).get(`${this.apiUrl}/user`),R.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${e}`}).get(`${this.apiUrl}/user/emails`)]),i=this.pickEmail(r.data);return{id:t.data.id.toString(),nickname:t.data.login,name:t.data.name??t.data.login,email:i?.email??t.data.email??null,emailVerified:i?i.verified:null,avatar:t.data.avatar_url,token:e,raw:t.data}}pickEmail(e){if(!Array.isArray(e)||e.length===0)return null;let t=e.find((i)=>i.primary&&i.verified),r=e.find((i)=>i.verified);return t??r??e.find((i)=>i.primary)??e[0]??null}getEmail(e){return this.pickEmail(e)?.email??null}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("GitHub client ID not provided");if(!t)throw new l("GitHub client secret not provided");if(!r)throw new l("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as q}from"@stacksjs/api";import{config as T}from"@stacksjs/config";class B extends p{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";tokenUrl="https://oauth2.googleapis.com/token";getConfig(){let e={clientId:this.clientId||(T.services.google?.clientId??""),clientSecret:this.clientSecret||(T.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(T.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:T.services.google?.scopes??["openid","email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.resolveState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await q.post(this.tokenUrl,{client_id:t,client_secret:r,code:e,redirect_uri:i,grant_type:"authorization_code"});if(s.data.error)throw Error(`Google OAuth error: ${s.data.error_description}`);return s.data.access_token}async getUserByToken(e){let t=await q.withHeaders({Authorization:`Bearer ${e}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:t.data.id,nickname:t.data.given_name,name:t.data.name,email:t.data.email,emailVerified:typeof t.data.verified_email==="boolean"?t.data.verified_email:null,avatar:t.data.picture,token:e,raw:t.data}}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("Google client ID not provided");if(!t)throw new l("Google client secret not provided");if(!r)throw new l("Google redirect URL not provided")}getTokenUrl(){return this.tokenUrl}}class P extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class ie{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(e={}){this.graphVersion=e.graphVersion||v,this.authBase=e.authBase||"https://www.facebook.com",this.graphBase=e.graphBase||"https://graph.facebook.com"}getAuthUrl(e){let t=new URLSearchParams({client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(","),state:e.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${t.toString()}`}async exchangeCode(e){let t=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,redirect_uri:e.redirectUrl,code:e.code}),r=await this.graph(`/oauth/access_token?${t.toString()}`,{method:"GET"});if(!r.access_token)throw new P("Facebook did not return an access token.",400,JSON.stringify(r));return{accessToken:r.access_token,expiresIn:r.expires_in}}async resolveAccount(e){let t=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:e}),r=await this.graph(`/me/accounts?${t.toString()}`,{method:"GET"}),i=(r.data||[]).find((n)=>n.instagram_business_account?.id),s=i?.instagram_business_account;if(!s?.id||!i?.access_token)throw new P("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(r));return{igUserId:s.id,username:s.username,pageAccessToken:i.access_token}}async publish(e,t){if(!e.accessToken)throw Error("Instagram access token is missing for this identity.");let r=e.did;if(!r)throw Error("Instagram account id is required to publish.");let i=t.media?.[0];if(!i?.url)throw Error("Instagram requires an image to post.");if(t.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let s=await this.graph(`/${r}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:i.url,caption:t.text,access_token:e.accessToken}).toString()});if(!s.id)throw new P("Instagram did not return a media container id.",400,JSON.stringify(s));let n=await this.graph(`/${r}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:s.id,access_token:e.accessToken}).toString()}),o=await this.graph(`/${n.id}?fields=permalink&access_token=${encodeURIComponent(e.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:n.id,url:o?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t){let r=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,t),i=await r.text(),s={};try{s=i?JSON.parse(i):{}}catch{s={}}if(!r.ok||s?.error){let n=s?.error?.message||i||r.statusText;throw new P(`Instagram API failed (${r.status}): ${n}`,r.status,i)}return s}}class h extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class se{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(e={}){this.apiVersion=e.apiVersion||"202405",this.authBase=e.authBase||"https://www.linkedin.com",this.apiBase=e.apiBase||"https://api.linkedin.com"}getAuthUrl(e){let t=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(" "),state:e.state});return`${this.authBase}/oauth/v2/authorization?${t.toString()}`}async exchangeCode(e){let t=new URLSearchParams({grant_type:"authorization_code",code:e.code,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret}),r=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:t.toString()});if(!r.access_token)throw new h("LinkedIn did not return an access token.",400,JSON.stringify(r));return{accessToken:r.access_token,expiresIn:r.expires_in,scope:r.scope}}async getProfile(e){if(!e)throw Error("LinkedIn access token is required.");let t=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${e}`}});if(!t.sub)throw new h("LinkedIn profile is missing a subject id.",400,JSON.stringify(t));return{sub:t.sub,name:t.name,picture:t.picture}}async publish(e,t){if(!e.accessToken)throw Error("LinkedIn access token is missing for this identity.");let r=e.did;if(!r)throw Error("LinkedIn member URN is required to publish.");if(t.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let i={author:r,commentary:ne(t.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(t.external)i.content={article:{source:t.external.uri,title:t.external.title,description:t.external.description||""}};let s=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${e.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(i)}),n=await s.text();if(!s.ok)throw new h(`LinkedIn API failed (${s.status}): ${n||s.statusText}`,s.status,n);let o=s.headers.get("x-restli-id")||s.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:o,url:o?`https://www.linkedin.com/feed/update/${o}`:void 0}}async timeline(e,t={}){return{items:[]}}async listAuthoredPosts(e,t={}){if(!e.accessToken)throw Error("LinkedIn access token is missing for this identity.");let r=e.did;if(!r)throw Error("LinkedIn member URN is required to list posts.");let i=Math.min(Math.max(t.limit||50,1),100),s=Number(t.cursor||0)||0,n=new URL(`${this.apiBase}/rest/posts`);n.searchParams.set("q","author"),n.searchParams.set("author",r),n.searchParams.set("count",String(i)),n.searchParams.set("start",String(s));let o;try{o=await this.request(n.toString(),{headers:{authorization:`Bearer ${e.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}})}catch(c){if(c instanceof h&&(c.status===401||c.status===403))throw new h("LinkedIn will not list this account's posts - the Posts author finder needs the r_member_social permission, which this app does not hold.",c.status,c.body);throw c}let a=(o.elements||[]).filter((c)=>c?.id).map((c)=>({uri:String(c.id),text:c.commentary,postedAt:c.createdAt?new Date(c.createdAt).toISOString():void 0,url:`https://www.linkedin.com/feed/update/${c.id}`}));return{cursor:a.length===i?String(s+i):void 0,posts:a}}async deletePost(e,t){if(!e.accessToken)throw Error("LinkedIn access token is missing for this identity.");let r=String(t.uri||"").trim();if(!r)throw Error("A LinkedIn post URN is required to delete a post.");let i=await fetch(`${this.apiBase}/rest/posts/${encodeURIComponent(r)}`,{method:"DELETE",headers:{authorization:`Bearer ${e.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}});if(!i.ok&&i.status!==404){let s=await i.text().catch(()=>"");throw new h(`LinkedIn API failed (${i.status}): ${s||i.statusText}`,i.status,s)}}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new h(`LinkedIn API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function ne(e){return e.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class N extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="MastodonApiError"}get isAuthError(){return this.status===401||this.status===403}}function oe(e){let t=String(e||"").trim().replace(/\/+$/,"");if(!t)throw Error("Mastodon instance URL is required.");let r=/^https?:\/\//i.test(t)?t:`https://${t}`;try{let i=new URL(r);return`${i.protocol}//${i.host}`}catch{throw Error("Mastodon instance URL is invalid.")}}class ae{provider="mastodon";characterLimit=500;instanceOf(e){return oe(e.did||"")}tokenOf(e){if(!e.accessToken)throw Error("Mastodon access token is missing for this identity.");return e.accessToken}async verifyCredentials(e){let t=await this.request(`${this.instanceOf(e)}/api/v1/accounts/verify_credentials`,{headers:{authorization:`Bearer ${this.tokenOf(e)}`}});return{accountId:t.id,username:t.username,displayName:t.display_name||void 0,url:t.url}}async uploadMedia(e,t,r,i){let s=new FormData;if(s.set("file",new Blob([new Uint8Array(t)],{type:r||"image/jpeg"}),"upload"),i)s.set("description",i);return(await this.request(`${this.instanceOf(e)}/api/v2/media`,{method:"POST",headers:{authorization:`Bearer ${this.tokenOf(e)}`},body:s})).id}async publish(e,t){let r=this.instanceOf(e),i=this.tokenOf(e);if(t.text.length>this.characterLimit)throw Error(`Mastodon posts must be ${this.characterLimit} characters or fewer.`);let s=[];for(let a of(t.media||[]).slice(0,4)){let{bytes:c,mimeType:d}=a;if(!c?.length&&a.url){let u=await fetch(a.url);if(!u.ok)continue;c=new Uint8Array(await u.arrayBuffer()),d=d||u.headers.get("content-type")||"image/jpeg"}if(c?.length)s.push(await this.uploadMedia(e,c,d||"image/jpeg",a.altText))}let n={status:t.text,visibility:"public"};if(s.length)n.media_ids=s;if(t.reply?.parent?.uri)n.in_reply_to_id=t.reply.parent.uri;let o=await this.request(`${r}/api/v1/statuses`,{method:"POST",headers:{authorization:`Bearer ${i}`,"content-type":"application/json"},body:JSON.stringify(n)});return{provider:this.provider,uri:o.id,cid:o.id,url:o.url||o.uri}}async timeline(e,t={}){return{items:[]}}async listAuthoredPosts(e,t={}){let r=this.instanceOf(e),{accountId:i}=await this.verifyCredentials(e),s=new URL(`${r}/api/v1/accounts/${encodeURIComponent(i)}/statuses`);if(s.searchParams.set("limit",String(Math.min(Math.max(t.limit||40,1),40))),s.searchParams.set("exclude_reblogs","true"),t.cursor)s.searchParams.set("max_id",t.cursor);let o=(await this.request(s.toString(),{headers:{authorization:`Bearer ${this.tokenOf(e)}`}})||[]).filter((a)=>a?.id).map((a)=>({uri:a.id,cid:a.id,text:a.content,postedAt:a.created_at,url:a.url}));return{cursor:o.length?o[o.length-1]?.uri:void 0,posts:o}}async deletePost(e,t){let r=String(t.cid||ce(t.uri)||"").trim();if(!r)throw Error("A status id is required to delete a post.");await this.request(`${this.instanceOf(e)}/api/v1/statuses/${encodeURIComponent(r)}`,{method:"DELETE",headers:{authorization:`Bearer ${this.tokenOf(e)}`}})}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new N(`Mastodon API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function ce(e){return String(e||"").replace(/\/+$/,"").split("/").pop()||""}class y extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class le{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(e={}){this.graphVersion=e.graphVersion||"v1.0",this.authBase=e.authBase||"https://threads.net",this.graphBase=e.graphBase||"https://graph.threads.net"}getAuthUrl(e){let t=new URLSearchParams({client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(","),response_type:"code",state:e.state});return`${this.authBase}/oauth/authorize?${t.toString()}`}async exchangeCode(e){let t=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"authorization_code",redirect_uri:e.redirectUrl,code:e.code}).toString()}),r=await t.text(),i={};try{i=r?JSON.parse(r):{}}catch{i={}}if(!t.ok||i?.error||!i?.access_token){let s=i?.error_message||i?.error?.message||r||t.statusText;throw new y(`Threads token exchange failed (${t.status}): ${s}`,t.status,r)}return{accessToken:i.access_token,userId:i.user_id!=null?String(i.user_id):void 0,expiresIn:i.expires_in}}async resolveAccount(e){let t=new URLSearchParams({fields:"id,username",access_token:e}),r=await this.graph(`/me?${t.toString()}`,{method:"GET"});if(!r.id)throw new y("Could not resolve the Threads account for this token.",400,JSON.stringify(r));return{threadsUserId:r.id,username:r.username,accessToken:e}}async publish(e,t){if(!e.accessToken)throw Error("Threads access token is missing for this identity.");let r=e.did;if(!r)throw Error("Threads account id is required to publish.");if(t.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let i=t.media?.[0],s=new URLSearchParams({text:t.text,access_token:e.accessToken});if(i?.url)s.set("media_type","IMAGE"),s.set("image_url",i.url);else s.set("media_type","TEXT");let n=await this.graph(`/${r}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:s.toString()});if(!n.id)throw new y("Threads did not return a media container id.",400,JSON.stringify(n));let o=await this.graph(`/${r}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:n.id,access_token:e.accessToken}).toString()});if(!o.id)throw new y("Threads did not return a published post id.",400,JSON.stringify(o));let a=await this.graph(`/${o.id}?fields=permalink&access_token=${encodeURIComponent(e.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:o.id,url:a?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t){let r=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,t),i=await r.text(),s={};try{s=i?JSON.parse(i):{}}catch{s={}}if(!r.ok||s?.error){let n=s?.error?.message||i||r.statusText;throw new y(`Threads API failed (${r.status}): ${n}`,r.status,i)}return s}}import{Buffer as de}from"buffer";import{createHash as ue,randomBytes as pe}from"crypto";import{fetcher as z}from"@stacksjs/api";import{config as _}from"@stacksjs/config";class L extends p{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let e={clientId:_.services.twitter?.clientId??"",clientSecret:_.services.twitter?.clientSecret??"",redirectUrl:_.services.twitter?.redirectUrl??"",scopes:_.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(e.scopes),e}generateCodeVerifier(){return pe(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(e){return ue("sha256").update(e).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let e=this.getState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let s=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",code_challenge:s,code_challenge_method:"S256"}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let s=de.from(`${t}:${r}`).toString("base64"),n=await z.withHeaders({Authorization:`Basic ${s}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:e,grant_type:"authorization_code",redirect_uri:i,code_verifier:this.codeVerifier});if(n.data.error)throw Error(`Twitter OAuth error: ${n.data.error_description}`);return n.data.access_token}async getUserByToken(e){let t=await z.withHeaders({Authorization:`Bearer ${e}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:t.data.id,nickname:t.data.username,name:t.data.name,email:t.data.email??null,avatar:t.data.profile_image_url??null,token:e,raw:t.data}}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("Twitter client ID not provided");if(!t)throw new l("Twitter client secret not provided");if(!r)throw new l("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class m extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="TwitterApiError"}get isAuthError(){return this.status===401||this.status===403}}function H(e){let t="";for(let r of e)t+=String.fromCharCode(r);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class he{provider="twitter";characterLimit=280;apiBase;authorizeBase;constructor(e={}){this.apiBase=e.apiBase||"https://api.twitter.com",this.authorizeBase=e.authorizeBase||"https://twitter.com"}async createAuthorization(e){let t=H(crypto.getRandomValues(new Uint8Array(32))),r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)),i=H(new Uint8Array(r)),s=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(" "),state:e.state,code_challenge:i,code_challenge_method:"S256"});return{url:`${this.authorizeBase}/i/oauth2/authorize?${s.toString()}`,codeVerifier:t}}async exchangeCode(e){return this.tokenRequest(new URLSearchParams({grant_type:"authorization_code",code:e.code,redirect_uri:e.redirectUrl,code_verifier:e.codeVerifier,client_id:e.clientId}),e.clientId,e.clientSecret)}async refreshAccessToken(e){return this.tokenRequest(new URLSearchParams({grant_type:"refresh_token",refresh_token:e.refreshToken,client_id:e.clientId}),e.clientId,e.clientSecret)}async getProfile(e){let t=await this.request(`${this.apiBase}/2/users/me?user.fields=username,name`,{headers:{authorization:`Bearer ${e}`}});if(!t.data?.id||!t.data.username)throw new m("Twitter did not return the authenticated user.",400,JSON.stringify(t));return{id:t.data.id,username:t.data.username,name:t.data.name}}async uploadMedia(e,t,r){let i=new FormData;i.set("media",new Blob([new Uint8Array(t)],{type:r||"image/jpeg"})),i.set("media_category","tweet_image");let s=await this.request(`${this.apiBase}/2/media/upload`,{method:"POST",headers:{authorization:`Bearer ${e}`},body:i}),n=s.data?.id||s.media_id_string||s.id;if(!n)throw new m("Twitter did not return a media id.",400,JSON.stringify(s));return n}async publish(e,t){if(!e.accessToken)throw Error("Twitter access token is missing for this identity.");if(t.text.length>this.characterLimit)throw Error(`Twitter posts must be ${this.characterLimit} characters or fewer.`);let r=[],i=t.media?.[0];if(i){let{bytes:a,mimeType:c}=i;if(!a?.length&&i.url){let d=await fetch(i.url);if(d.ok)a=new Uint8Array(await d.arrayBuffer()),c=c||d.headers.get("content-type")||"image/jpeg"}if(a?.length)r.push(await this.uploadMedia(e.accessToken,a,c||"image/jpeg"))}let s={text:t.text};if(r.length)s.media={media_ids:r};if(t.reply?.parent?.uri)s.reply={in_reply_to_tweet_id:t.reply.parent.uri};let n=await this.request(`${this.apiBase}/2/tweets`,{method:"POST",headers:{authorization:`Bearer ${e.accessToken}`,"content-type":"application/json"},body:JSON.stringify(s)}),o=n.data?.id;if(!o)throw new m("Twitter did not return a tweet id.",400,JSON.stringify(n));return{provider:this.provider,uri:o,cid:o,url:e.handle?`https://x.com/${e.handle}/status/${o}`:`https://x.com/i/web/status/${o}`}}async timeline(e,t={}){return{items:[]}}async listAuthoredPosts(e,t={}){if(!e.accessToken)throw Error("Twitter access token is missing for this identity.");let r=e.did;if(!r)throw Error("Twitter user id is required to list posts.");let i=new URL(`${this.apiBase}/2/users/${encodeURIComponent(r)}/tweets`);if(i.searchParams.set("max_results",String(Math.min(Math.max(t.limit||100,5),100))),i.searchParams.set("tweet.fields","created_at"),t.cursor)i.searchParams.set("pagination_token",t.cursor);let s=await this.request(i.toString(),{headers:{authorization:`Bearer ${e.accessToken}`}});return{cursor:s.meta?.next_token,posts:(s.data||[]).filter((n)=>n?.id).map((n)=>({uri:n.id,cid:n.id,text:n.text,postedAt:n.created_at,url:`https://x.com/i/web/status/${n.id}`}))}}async deletePost(e,t){if(!e.accessToken)throw Error("Twitter access token is missing for this identity.");let r=String(t.uri||"").trim();if(!r)throw Error("A tweet id is required to delete a post.");let i=await this.request(`${this.apiBase}/2/tweets/${encodeURIComponent(r)}`,{method:"DELETE",headers:{authorization:`Bearer ${e.accessToken}`}});if(i.data&&i.data.deleted===!1)throw new m(`X refused to delete tweet ${r}.`,400,JSON.stringify(i))}async tokenRequest(e,t,r){let i={"content-type":"application/x-www-form-urlencoded"};if(r)i.authorization=`Basic ${btoa(`${t}:${r}`)}`;let s=await this.request(`${this.apiBase}/2/oauth2/token`,{method:"POST",headers:i,body:e.toString()});if(!s.access_token)throw new m("Twitter did not return an access token.",400,JSON.stringify(s));return{accessToken:s.access_token,refreshToken:s.refresh_token,expiresIn:s.expires_in,scope:s.scope}}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new m(`Twitter API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}import{config as ge}from"@stacksjs/config";var x=["clientId","clientSecret","redirectUrl"],S=Object.freeze({google:{name:"google",label:"Google",driver:B,required:x,postCallback:!1},github:{name:"github",label:"GitHub",driver:E,required:x,postCallback:!1},facebook:{name:"facebook",label:"Facebook",driver:A,required:x,postCallback:!1},twitter:{name:"twitter",label:"X",driver:L,required:x,postCallback:!1},apple:{name:"apple",label:"Apple",driver:C,required:["clientId","teamId","keyId","privateKey","redirectUrl"],postCallback:!0}});function V(e){return ge?.services?.[e]}function me(e){return typeof e==="string"&&e in S}function J(e){if(!me(e))return!1;let t=V(e);if(!t)return!1;return S[e].required.every((r)=>Boolean(t[r]))}function lt(){return Object.keys(S).filter(J).map((e)=>S[e])}function dt(e){if(!J(e))return null;let t=S[e],r=V(e)??{};return new t.driver({clientSecret:"",...r,clientId:String(r.clientId??""),redirectUrl:String(r.redirectUrl??"")})}class fe{accessToken;refreshToken;expiresIn;approvedScopes;constructor(e,t=null,r=null,i=[]){this.accessToken=e;this.refreshToken=t;this.expiresIn=r;this.approvedScopes=i}}function ht(e){return typeof e?.deletePost==="function"}function gt(e){return typeof e?.listAuthoredPosts==="function"}import{buildSessionHandoffUrl as ye}from"@stacksjs/composables";function j(e,t=[]){if(!e)return!1;if(e.startsWith("//"))return!1;if(e.startsWith("/"))return!0;try{let r=new URL(e);if(r.protocol!=="http:"&&r.protocol!=="https:")return!1;return t.includes(r.host)}catch{return!1}}function yt(e,t={}){let r=t.redirectTo??"/";if(!j(r,t.allowedHosts??[]))throw Error(`[socials] refusing to hand a session to ${r}: relative paths are always allowed; an absolute URL needs its host in allowedHosts.`);return new Response(null,{status:302,headers:{Location:ye(r,e),"Cache-Control":"no-store","Referrer-Policy":"no-referrer"}})}function wt(e,t={}){let r=t.redirectTo??"/login";if(!j(r,t.allowedHosts??[]))throw Error(`[socials] refusing to redirect to ${r}`);let i=r.includes("?")?"&":"?",s=`${r}${i}social_error=${encodeURIComponent(e)}`;return new Response(null,{status:302,headers:{Location:s,"Cache-Control":"no-store"}})}var we={bluesky:["identifier","password","service"],twitter:["clientId","clientSecret","accessToken","refreshToken"]},ke={bluesky:["identifier","password"],twitter:["clientId","clientSecret","accessToken"]};function be(e){return e.toUpperCase().replace(/[^A-Z0-9]/g,"")}function G(e,t,r){let i=r.replace(/[A-Z]/g,(s)=>`_${s}`).toUpperCase();return`SOCIALS_${be(e)}_${t.toUpperCase()}_${i}`}class w extends Error{constructor(e){super(e);this.name="SocialIdentityError"}}async function M(){let{config:e,overridesReady:t}=await import("@stacksjs/config");return await t,e.socials??{}}function F(e,t){let r=Object.keys(e.identities??{});if(t){if(!r.includes(t))throw new w(`Unknown social identity '${t}'. config/socials.ts declares: ${r.length?r.join(", "):"none"}.`);return t}if(e.default){if(!r.includes(e.default))throw new w(`config/socials.ts sets default: '${e.default}', which is not one of its identities: ${r.length?r.join(", "):"none"}.`);return e.default}if(r.length===1)return r[0];if(r.length===0)throw new w("config/socials.ts declares no identities, so there is no account to post as.");throw new w(`config/socials.ts declares ${r.length} identities (${r.join(", ")}) and no default, so a call has to name one.`)}function Pe(e,t,r,i=process.env){let s=F(e,r),o=(e.identities?.[s]??{}).credentials?.[t]??{},a={};for(let d of we[t]){let u=(o[d]??i[G(s,t,d)]??"").trim();if(u)a[d]=u}let c=ke[t].filter((d)=>!a[d]);if(c.length){let d=c.map((u)=>G(s,t,u)).join(", ");throw new w(`Identity '${s}' is not configured for ${t}: ${c.join(", ")} ${c.length===1?"is":"are"} missing. Set ${d}, or supply them inline in config/socials.ts.`)}return a}async function bt(e){return F(await M(),e)}async function Q(e,t,r){return Pe(await M(),e,t,r)}function Pt(e,t){return Q("bluesky",e,t)}function St(e,t){return Q("twitter",e,t)}export{p as AbstractProvider,C as AppleProvider,O as BlueskyApiError,re as BlueskyPublishingDriver,l as ConfigException,A as FacebookProvider,E as GitHubProvider,B as GoogleProvider,P as InstagramApiError,ie as InstagramPublishingDriver,W as InvalidStateException,h as LinkedInApiError,se as LinkedInPublishingDriver,v as META_GRAPH_VERSION,N as MastodonApiError,ae as MastodonPublishingDriver,S as SOCIAL_PROVIDERS,w as SocialIdentityError,y as ThreadsApiError,le as ThreadsPublishingDriver,fe as Token,m as TwitterApiError,L as TwitterProvider,he as TwitterPublishingDriver,lt as configuredSocialProviders,te as detectFacetCandidates,G as envName,be as envSegment,ne as escapeLinkedInText,j as isSafeHandoffTarget,J as isSocialProviderConfigured,me as isSocialProviderName,oe as normalizeInstance,Y as parseAtUri,Pe as pickCredentials,F as pickIdentity,Pt as resolveBlueskyCredentials,bt as resolveIdentityName,Q as resolveSocialCredentials,St as resolveTwitterCredentials,wt as socialHandoffFailureRedirect,yt as socialHandoffRedirect,dt as socialProvider,ht as supportsDeletion,gt as supportsEnumeration};
2
+ var se=import.meta.require;class h{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_state=null;user=null;constructor(e){this.clientId=e.clientId,this.clientSecret=e.clientSecret,this.redirectUrl=e.redirectUrl}getCodeFields(e=null){let t={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())t.state=e;if(this.usesPKCE())t.code_challenge=this.getCodeChallenge(),t.code_challenge_method=this.getCodeChallengeMethod();return{...t,...this.parameters}}formatScopes(e,t){return e.join(t)}async userFromToken(e){return{...await this.getUserByToken(e),token:e}}scopes(e){let t=Array.isArray(e)?e:[e];return this._scopes=[...new Set([...this._scopes,...t])],this}setScopes(e){let t=Array.isArray(e)?e:[e];return this._scopes=[...new Set(t)],this}getScopes(){return this._scopes}setRedirectUrl(e){if(typeof e!=="string"||e.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let t;try{t=new URL(e)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${e}`)}if(t.protocol!=="https:"&&t.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${t.protocol}`);return this.redirectUrl=e,this}withState(e){if(typeof e!=="string"||e.length===0)throw Error("[socials] withState requires a non-empty string");return this._state=e,this}resolveState(){return this._state??this.getState()}usesState(){return!this._stateless}validateState(e,t){if(typeof e!=="string"||typeof t!=="string")return!1;if(e.length===0||t.length===0)return!1;if(e.length!==t.length)return!1;try{let{timingSafeEqual:r}=se("crypto");return r(Buffer.from(e,"utf8"),Buffer.from(t,"utf8"))}catch{let r=0;for(let i=0;i<e.length;i++)r|=e.charCodeAt(i)^t.charCodeAt(i);return r===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let e=new Uint8Array(32);return crypto.getRandomValues(e),Array.from(e).map((t)=>t.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let e=new Uint8Array(48);return crypto.getRandomValues(e),Array.from(e).map((t)=>t.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let t=new TextEncoder().encode(this.getCodeVerifier()),r=await crypto.subtle.digest("SHA-256",t),{Buffer:i}=await import("buffer");return i.from(new Uint8Array(r)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(e){return this.parameters=e,this}buildAuthUrlFromBase(e,t){let r=new URLSearchParams(this.getCodeFields(t));return`${e}?${r.toString()}`}}import{Buffer as B}from"buffer";import{createPrivateKey as oe,sign as ae}from"crypto";import{config as y}from"@stacksjs/config";class ne extends Error{constructor(e="Invalid state"){super(e);this.name="InvalidStateException"}}class l extends Error{constructor(e){super(e);this.name="ConfigException"}}class O extends h{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";constructor(e){super(e);this.teamId=e.teamId??"",this.keyId=e.keyId??"",this.privateKey=e.privateKey??""}getConfig(){let e={clientId:this.clientId||(y.services.apple?.clientId??""),teamId:this.teamId||(y.services.apple?.teamId??""),keyId:this.keyId||(y.services.apple?.keyId??""),privateKey:(this.privateKey||(y.services.apple?.privateKey??"")).replace(/\\n/g,`
3
+ `),redirectUrl:this.redirectUrl||(y.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:y.services.apple?.scopes??["name","email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.resolveState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();this.validateConfig();let s={client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",...this.parameters};if(i.length>0)s.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams(s).toString()}`}async getAccessToken(e){let{clientId:t,redirectUrl:r}=this.getConfig();this.validateConfig();let i=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:r,client_id:t,client_secret:this.generateClientSecret()})}),s=await i.json();if(!i.ok||s.error)throw Error(`Apple OAuth error: ${s.error_description??s.error??`HTTP ${i.status}`}`);if(!s.id_token)throw Error("Apple OAuth error: token response contained no id_token");return s.id_token}async getUserByToken(e){let{clientId:t}=this.getConfig(),r=this.decodeIdToken(e),i=r.iss===this.baseUrl,s=Array.isArray(r.aud)?r.aud.includes(t):r.aud===t,n=typeof r.exp==="number"&&r.exp*1000>Date.now();if(!i||!s||!n)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!r.sub)throw Error("Apple OAuth error: id_token has no subject");let o=typeof r.email==="string"?r.email:null,a=null;if(r.email_verified===!0||r.email_verified==="true")a=!0;else if(r.email_verified===!1||r.email_verified==="false")a=!1;return{id:String(r.sub),nickname:null,name:"",email:o,emailVerified:a,avatar:null,token:e,raw:r}}generateClientSecret(){let{clientId:e,teamId:t,keyId:r,privateKey:i}=this.getConfig(),s=Math.floor(Date.now()/1000),n={alg:"ES256",kid:r,typ:"JWT"},o={iss:t,iat:s,exp:s+3600,aud:this.baseUrl,sub:e},a=`${this.base64urlJson(n)}.${this.base64urlJson(o)}`,c;try{c=oe(i)}catch(u){throw new l(`Apple private key could not be parsed: ${u instanceof Error?u.message:String(u)}`)}let d=ae("sha256",B.from(a),{key:c,dsaEncoding:"ieee-p1363"});return`${a}.${d.toString("base64url")}`}decodeIdToken(e){let t=e.split(".");if(t.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(B.from(t[1],"base64url").toString("utf8"))}base64urlJson(e){return B.from(JSON.stringify(e)).toString("base64url")}validateConfig(){let{clientId:e,teamId:t,keyId:r,privateKey:i,redirectUrl:s}=this.getConfig();if(!e)throw new l("Apple client ID (Service ID) not provided");if(!t)throw new l("Apple team ID not provided");if(!r)throw new l("Apple key ID not provided");if(!i)throw new l("Apple private key not provided");if(!s)throw new l("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function ce(e){let t=/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(String(e||"").trim());if(!t)throw Error(`"${e}" is not a Bluesky post URI.`);return{repo:t[1],collection:t[2],rkey:t[3]}}class M extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}var le=new TextEncoder;function m(e){return le.encode(e).length}function de(e){let t=[],r=/https?:\/\/[^\s<>"']+/g;for(let o of e.matchAll(r)){let a=o[0].replace(/[),.;!?]+$/,"");t.push({byteStart:m(e.slice(0,o.index)),byteEnd:m(e.slice(0,o.index))+m(a),type:"link",value:a})}let i=(o,a)=>t.some((c)=>c.type==="link"&&o<c.byteEnd&&a>c.byteStart),s=/(^|\s)(#[A-Za-z0-9_]+)/g;for(let o of e.matchAll(s)){let a=o[1],c=o[2];if(a===void 0||c===void 0)continue;if(/^#\d+$/.test(c))continue;let d=(o.index??0)+a.length,u=m(e.slice(0,d)),P=u+m(c);if(i(u,P))continue;t.push({byteStart:u,byteEnd:P,type:"tag",value:c.slice(1)})}let n=/(^|\s)(@[a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;for(let o of e.matchAll(n)){let a=o[1],c=o[2];if(a===void 0||c===void 0)continue;let d=(o.index??0)+a.length,u=m(e.slice(0,d)),P=u+m(c);if(i(u,P))continue;t.push({byteStart:u,byteEnd:P,type:"mention",value:c.slice(1).replace(/\.+$/,"")})}return t.sort((o,a)=>o.byteStart-a.byteStart)}class T{provider="bluesky";characterLimit=300;service;constructor(e={}){this.service=e.service||"https://bsky.social"}async createSession(e){let t=e.identifier.trim(),r=e.password.trim();if(!t)throw Error("Bluesky identifier is required.");if(!r)throw Error("Bluesky app password is required.");let i=await this.post("/xrpc/com.atproto.server.createSession",{identifier:t,password:r}),s=await this.getProfile({did:i.did,handle:i.handle,accessToken:i.accessJwt,refreshToken:i.refreshJwt}).catch(()=>{return});return{did:i.did,handle:i.handle,displayName:s?.displayName,accessJwt:i.accessJwt,refreshJwt:i.refreshJwt}}async refreshSession(e){if(!e)throw Error("Bluesky refresh token is required.");let t=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${e}`});return{did:t.did,handle:t.handle,accessJwt:t.accessJwt,refreshJwt:t.refreshJwt}}async publish(e,t){let r=e.did||e.handle;if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!r)throw Error("Bluesky identity DID or handle is required.");if(t.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let i={$type:"app.bsky.feed.post",text:t.text,createdAt:t.scheduledAt||new Date().toISOString()};if(t.langs?.length)i.langs=t.langs;if(t.reply)i.reply=t.reply;let s=t.facets??await this.buildFacets(t.text);if(s.length)i.facets=s;if(t.external)i.embed={$type:"app.bsky.embed.external",external:{uri:t.external.uri,title:t.external.title,description:t.external.description||""}};let n=(t.media||[]).filter((a)=>a.bytes?.length).slice(0,4);if(n.length){let a=[];for(let c of n){let d=await this.uploadBlob(e,c.bytes,c.mimeType||"image/jpeg");a.push({image:d,alt:c.altText||""})}i.embed={$type:"app.bsky.embed.images",images:a}}let o=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:r,collection:"app.bsky.feed.post",record:i},{authorization:`Bearer ${e.accessToken}`});return{provider:this.provider,uri:o.uri,cid:o.cid,url:this.toPostUrl(e.handle,o.uri)}}async postMetrics(e,t){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(t.length===0)return[];let r=new URL(`${this.service}/xrpc/app.bsky.feed.getPosts`);for(let s of t.slice(0,25))r.searchParams.append("uris",s);return((await this.request(r,{headers:{authorization:`Bearer ${e.accessToken}`}})).posts||[]).map((s)=>({uri:s.uri,likeCount:s.likeCount||0,repostCount:s.repostCount||0,replyCount:s.replyCount||0}))}async timeline(e,t={}){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");let r=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(r.searchParams.set("limit",String(Math.min(Math.max(t.limit||30,1),100))),t.cursor)r.searchParams.set("cursor",t.cursor);let i=await this.request(r,{headers:{authorization:`Bearer ${e.accessToken}`}});return{cursor:i.cursor,items:(i.feed||[]).flatMap((s)=>{let n=s.post;if(!n?.uri||!n.author?.handle)return[];return[{uri:n.uri,authorHandle:n.author.handle,authorName:n.author.displayName,authorAvatar:n.author.avatar,postUrl:this.toPostUrl(n.author.handle,n.uri),body:n.record?.text||"",postedAt:n.record?.createdAt||new Date().toISOString(),likeCount:n.likeCount||0,repostCount:n.repostCount||0,replyCount:n.replyCount||0}]})}}async getProfile(e){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");let t=e.did||e.handle;if(!t)throw Error("Bluesky identity DID or handle is required.");let r=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return r.searchParams.set("actor",t),await this.request(r,{headers:{authorization:`Bearer ${e.accessToken}`}})}async buildFacets(e){let t=[];for(let r of de(e)){let i=null;if(r.type==="link")i={$type:"app.bsky.richtext.facet#link",uri:r.value};else if(r.type==="tag")i={$type:"app.bsky.richtext.facet#tag",tag:r.value};else if(r.type==="mention"){let s=await this.resolveHandle(r.value);if(s)i={$type:"app.bsky.richtext.facet#mention",did:s}}if(i)t.push({index:{byteStart:r.byteStart,byteEnd:r.byteEnd},features:[i]})}return t}async resolveHandle(e){try{let t=new URL(`${this.service}/xrpc/com.atproto.identity.resolveHandle`);return t.searchParams.set("handle",e),(await this.request(t,{})).did||null}catch{return null}}async uploadBlob(e,t,r){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(t.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":r,authorization:`Bearer ${e.accessToken}`},body:new Uint8Array(t)})).blob}async listAuthoredPosts(e,t={}){let r=e.did||e.handle;if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!r)throw Error("Bluesky identity DID or handle is required.");let i=new URL(`${this.service}/xrpc/com.atproto.repo.listRecords`);if(i.searchParams.set("repo",r),i.searchParams.set("collection","app.bsky.feed.post"),i.searchParams.set("limit",String(Math.min(Math.max(t.limit||100,1),100))),t.cursor)i.searchParams.set("cursor",t.cursor);let s=await this.request(i,{headers:{authorization:`Bearer ${e.accessToken}`}});return{cursor:s.cursor,posts:(s.records||[]).filter((n)=>n?.uri).map((n)=>({uri:n.uri,cid:n.cid,text:n.value?.text,postedAt:n.value?.createdAt,url:e.handle?this.toPostUrl(e.handle,n.uri):void 0}))}}async deletePost(e,t){if(!e.accessToken)throw Error("Bluesky access token is missing for this identity.");let{repo:r,collection:i,rkey:s}=ce(t.uri);await this.post("/xrpc/com.atproto.repo.deleteRecord",{repo:r,collection:i,rkey:s},{authorization:`Bearer ${e.accessToken}`})}async post(e,t,r={}){return await this.request(new URL(`${this.service}${e}`),{method:"POST",headers:{...t===void 0?{}:{"content-type":"application/json"},...r},...t===void 0?{}:{body:JSON.stringify(t)}})}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new M(`Bluesky API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}toPostUrl(e,t){let r=t.split("/").pop();return`https://bsky.app/profile/${e}/post/${r}`}}import{fetcher as F}from"@stacksjs/api";import{config as S}from"@stacksjs/config";var x="v24.0";class L extends h{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";get graphVersion(){return S.services.facebook?.graphVersion??x}getConfig(){let e={clientId:S.services.facebook?.clientId??"",clientSecret:S.services.facebook?.clientSecret??"",redirectUrl:S.services.facebook?.redirectUrl??"",scopes:S.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.getState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/${this.graphVersion}/dialog/oauth?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(","),state:e,response_type:"code"}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await F.get(`${this.apiUrl}/${this.graphVersion}/oauth/access_token?${new URLSearchParams({client_id:t,client_secret:r,redirect_uri:i,code:e}).toString()}`);if(s.data.error)throw Error(`Facebook OAuth error: ${s.data.error.message}`);return s.data.access_token}async getUserByToken(e){let t=await F.get(`${this.apiUrl}/${this.graphVersion}/me?${new URLSearchParams({access_token:e,fields:"id,name,email,picture"}).toString()}`);return{id:t.data.id,nickname:null,name:t.data.name,email:t.data.email??null,avatar:t.data.picture?.data.url??null,token:e,raw:t.data}}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("Facebook client ID not provided");if(!t)throw new l("Facebook client secret not provided");if(!r)throw new l("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/${this.graphVersion}/oauth/access_token`}}import{fetcher as D}from"@stacksjs/api";import{config as _}from"@stacksjs/config";class q extends h{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let e={clientId:this.clientId||(_.services.github?.clientId??""),clientSecret:this.clientSecret||(_.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(_.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:_.services.github?.scopes??["read:user","user:email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.resolveState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await D.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:t,client_secret:r,code:e,redirect_uri:i});if(s.data.error)throw Error(`GitHub OAuth error: ${s.data.error_description}`);return s.data.access_token}async getUserByToken(e){let[t,r]=await Promise.all([D.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${e}`}).get(`${this.apiUrl}/user`),D.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${e}`}).get(`${this.apiUrl}/user/emails`)]),i=this.pickEmail(r.data);return{id:t.data.id.toString(),nickname:t.data.login,name:t.data.name??t.data.login,email:i?.email??t.data.email??null,emailVerified:i?i.verified:null,avatar:t.data.avatar_url,token:e,raw:t.data}}pickEmail(e){if(!Array.isArray(e)||e.length===0)return null;let t=e.find((i)=>i.primary&&i.verified),r=e.find((i)=>i.verified);return t??r??e.find((i)=>i.primary)??e[0]??null}getEmail(e){return this.pickEmail(e)?.email??null}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("GitHub client ID not provided");if(!t)throw new l("GitHub client secret not provided");if(!r)throw new l("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as Q}from"@stacksjs/api";import{config as C}from"@stacksjs/config";class z extends h{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";tokenUrl="https://oauth2.googleapis.com/token";getConfig(){let e={clientId:this.clientId||(C.services.google?.clientId??""),clientSecret:this.clientSecret||(C.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(C.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:C.services.google?.scopes??["openid","email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){let e=this.resolveState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await Q.post(this.tokenUrl,{client_id:t,client_secret:r,code:e,redirect_uri:i,grant_type:"authorization_code"});if(s.data.error)throw Error(`Google OAuth error: ${s.data.error_description}`);return s.data.access_token}async getUserByToken(e){let t=await Q.withHeaders({Authorization:`Bearer ${e}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:t.data.id,nickname:t.data.given_name,name:t.data.name,email:t.data.email,emailVerified:typeof t.data.verified_email==="boolean"?t.data.verified_email:null,avatar:t.data.picture,token:e,raw:t.data}}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("Google client ID not provided");if(!t)throw new l("Google client secret not provided");if(!r)throw new l("Google redirect URL not provided")}getTokenUrl(){return this.tokenUrl}}class v extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class N{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(e={}){this.graphVersion=e.graphVersion||x,this.authBase=e.authBase||"https://www.facebook.com",this.graphBase=e.graphBase||"https://graph.facebook.com"}getAuthUrl(e){let t=new URLSearchParams({client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(","),state:e.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${t.toString()}`}async exchangeCode(e){let t=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,redirect_uri:e.redirectUrl,code:e.code}),r=await this.graph(`/oauth/access_token?${t.toString()}`,{method:"GET"});if(!r.access_token)throw new v("Facebook did not return an access token.",400,JSON.stringify(r));return{accessToken:r.access_token,expiresIn:r.expires_in}}async resolveAccount(e){let t=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:e}),r=await this.graph(`/me/accounts?${t.toString()}`,{method:"GET"}),i=(r.data||[]).find((n)=>n.instagram_business_account?.id),s=i?.instagram_business_account;if(!s?.id||!i?.access_token)throw new v("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(r));return{igUserId:s.id,username:s.username,pageAccessToken:i.access_token}}async publish(e,t){if(!e.accessToken)throw Error("Instagram access token is missing for this identity.");let r=e.did;if(!r)throw Error("Instagram account id is required to publish.");let i=t.media?.[0];if(!i?.url)throw Error("Instagram requires an image to post.");if(t.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let s=await this.graph(`/${r}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:i.url,caption:t.text,access_token:e.accessToken}).toString()});if(!s.id)throw new v("Instagram did not return a media container id.",400,JSON.stringify(s));let n=await this.graph(`/${r}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:s.id,access_token:e.accessToken}).toString()}),o=await this.graph(`/${n.id}?fields=permalink&access_token=${encodeURIComponent(e.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:n.id,url:o?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t){let r=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,t),i=await r.text(),s={};try{s=i?JSON.parse(i):{}}catch{s={}}if(!r.ok||s?.error){let n=s?.error?.message||i||r.statusText;throw new v(`Instagram API failed (${r.status}): ${n}`,r.status,i)}return s}}class g extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class V{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(e={}){this.apiVersion=e.apiVersion||"202405",this.authBase=e.authBase||"https://www.linkedin.com",this.apiBase=e.apiBase||"https://api.linkedin.com"}getAuthUrl(e){let t=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(" "),state:e.state});return`${this.authBase}/oauth/v2/authorization?${t.toString()}`}async exchangeCode(e){let t=new URLSearchParams({grant_type:"authorization_code",code:e.code,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret}),r=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:t.toString()});if(!r.access_token)throw new g("LinkedIn did not return an access token.",400,JSON.stringify(r));return{accessToken:r.access_token,expiresIn:r.expires_in,scope:r.scope}}async getProfile(e){if(!e)throw Error("LinkedIn access token is required.");let t=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${e}`}});if(!t.sub)throw new g("LinkedIn profile is missing a subject id.",400,JSON.stringify(t));return{sub:t.sub,name:t.name,picture:t.picture}}async publish(e,t){if(!e.accessToken)throw Error("LinkedIn access token is missing for this identity.");let r=e.did;if(!r)throw Error("LinkedIn member URN is required to publish.");if(t.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let i={author:r,commentary:ue(t.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(t.external)i.content={article:{source:t.external.uri,title:t.external.title,description:t.external.description||""}};let s=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${e.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(i)}),n=await s.text();if(!s.ok)throw new g(`LinkedIn API failed (${s.status}): ${n||s.statusText}`,s.status,n);let o=s.headers.get("x-restli-id")||s.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:o,url:o?`https://www.linkedin.com/feed/update/${o}`:void 0}}async timeline(e,t={}){return{items:[]}}async listAuthoredPosts(e,t={}){if(!e.accessToken)throw Error("LinkedIn access token is missing for this identity.");let r=e.did;if(!r)throw Error("LinkedIn member URN is required to list posts.");let i=Math.min(Math.max(t.limit||50,1),100),s=Number(t.cursor||0)||0,n=new URL(`${this.apiBase}/rest/posts`);n.searchParams.set("q","author"),n.searchParams.set("author",r),n.searchParams.set("count",String(i)),n.searchParams.set("start",String(s));let o;try{o=await this.request(n.toString(),{headers:{authorization:`Bearer ${e.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}})}catch(c){if(c instanceof g&&(c.status===401||c.status===403))throw new g("LinkedIn will not list this account's posts - the Posts author finder needs the r_member_social permission, which this app does not hold.",c.status,c.body);throw c}let a=(o.elements||[]).filter((c)=>c?.id).map((c)=>({uri:String(c.id),text:c.commentary,postedAt:c.createdAt?new Date(c.createdAt).toISOString():void 0,url:`https://www.linkedin.com/feed/update/${c.id}`}));return{cursor:a.length===i?String(s+i):void 0,posts:a}}async deletePost(e,t){if(!e.accessToken)throw Error("LinkedIn access token is missing for this identity.");let r=String(t.uri||"").trim();if(!r)throw Error("A LinkedIn post URN is required to delete a post.");let i=await fetch(`${this.apiBase}/rest/posts/${encodeURIComponent(r)}`,{method:"DELETE",headers:{authorization:`Bearer ${e.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}});if(!i.ok&&i.status!==404){let s=await i.text().catch(()=>"");throw new g(`LinkedIn API failed (${i.status}): ${s||i.statusText}`,i.status,s)}}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new g(`LinkedIn API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function ue(e){return e.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class K extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="MastodonApiError"}get isAuthError(){return this.status===401||this.status===403}}function pe(e){let t=String(e||"").trim().replace(/\/+$/,"");if(!t)throw Error("Mastodon instance URL is required.");let r=/^https?:\/\//i.test(t)?t:`https://${t}`;try{let i=new URL(r);return`${i.protocol}//${i.host}`}catch{throw Error("Mastodon instance URL is invalid.")}}class H{provider="mastodon";characterLimit=500;instanceOf(e){return pe(e.did||"")}tokenOf(e){if(!e.accessToken)throw Error("Mastodon access token is missing for this identity.");return e.accessToken}async verifyCredentials(e){let t=await this.request(`${this.instanceOf(e)}/api/v1/accounts/verify_credentials`,{headers:{authorization:`Bearer ${this.tokenOf(e)}`}});return{accountId:t.id,username:t.username,displayName:t.display_name||void 0,url:t.url}}async uploadMedia(e,t,r,i){let s=new FormData;if(s.set("file",new Blob([new Uint8Array(t)],{type:r||"image/jpeg"}),"upload"),i)s.set("description",i);return(await this.request(`${this.instanceOf(e)}/api/v2/media`,{method:"POST",headers:{authorization:`Bearer ${this.tokenOf(e)}`},body:s})).id}async publish(e,t){let r=this.instanceOf(e),i=this.tokenOf(e);if(t.text.length>this.characterLimit)throw Error(`Mastodon posts must be ${this.characterLimit} characters or fewer.`);let s=[];for(let a of(t.media||[]).slice(0,4)){let{bytes:c,mimeType:d}=a;if(!c?.length&&a.url){let u=await fetch(a.url);if(!u.ok)continue;c=new Uint8Array(await u.arrayBuffer()),d=d||u.headers.get("content-type")||"image/jpeg"}if(c?.length)s.push(await this.uploadMedia(e,c,d||"image/jpeg",a.altText))}let n={status:t.text,visibility:"public"};if(s.length)n.media_ids=s;if(t.reply?.parent?.uri)n.in_reply_to_id=t.reply.parent.uri;let o=await this.request(`${r}/api/v1/statuses`,{method:"POST",headers:{authorization:`Bearer ${i}`,"content-type":"application/json"},body:JSON.stringify(n)});return{provider:this.provider,uri:o.id,cid:o.id,url:o.url||o.uri}}async timeline(e,t={}){return{items:[]}}async listAuthoredPosts(e,t={}){let r=this.instanceOf(e),{accountId:i}=await this.verifyCredentials(e),s=new URL(`${r}/api/v1/accounts/${encodeURIComponent(i)}/statuses`);if(s.searchParams.set("limit",String(Math.min(Math.max(t.limit||40,1),40))),s.searchParams.set("exclude_reblogs","true"),t.cursor)s.searchParams.set("max_id",t.cursor);let o=(await this.request(s.toString(),{headers:{authorization:`Bearer ${this.tokenOf(e)}`}})||[]).filter((a)=>a?.id).map((a)=>({uri:a.id,cid:a.id,text:a.content,postedAt:a.created_at,url:a.url}));return{cursor:o.length?o[o.length-1]?.uri:void 0,posts:o}}async deletePost(e,t){let r=String(t.cid||he(t.uri)||"").trim();if(!r)throw Error("A status id is required to delete a post.");await this.request(`${this.instanceOf(e)}/api/v1/statuses/${encodeURIComponent(r)}`,{method:"DELETE",headers:{authorization:`Bearer ${this.tokenOf(e)}`}})}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new K(`Mastodon API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function he(e){return String(e||"").replace(/\/+$/,"").split("/").pop()||""}class w extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class J{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(e={}){this.graphVersion=e.graphVersion||"v1.0",this.authBase=e.authBase||"https://threads.net",this.graphBase=e.graphBase||"https://graph.threads.net"}getAuthUrl(e){let t=new URLSearchParams({client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(","),response_type:"code",state:e.state});return`${this.authBase}/oauth/authorize?${t.toString()}`}async exchangeCode(e){let t=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,grant_type:"authorization_code",redirect_uri:e.redirectUrl,code:e.code}).toString()}),r=await t.text(),i={};try{i=r?JSON.parse(r):{}}catch{i={}}if(!t.ok||i?.error||!i?.access_token){let s=i?.error_message||i?.error?.message||r||t.statusText;throw new w(`Threads token exchange failed (${t.status}): ${s}`,t.status,r)}return{accessToken:i.access_token,userId:i.user_id!=null?String(i.user_id):void 0,expiresIn:i.expires_in}}async resolveAccount(e){let t=new URLSearchParams({fields:"id,username",access_token:e}),r=await this.graph(`/me?${t.toString()}`,{method:"GET"});if(!r.id)throw new w("Could not resolve the Threads account for this token.",400,JSON.stringify(r));return{threadsUserId:r.id,username:r.username,accessToken:e}}async publish(e,t){if(!e.accessToken)throw Error("Threads access token is missing for this identity.");let r=e.did;if(!r)throw Error("Threads account id is required to publish.");if(t.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let i=t.media?.[0],s=new URLSearchParams({text:t.text,access_token:e.accessToken});if(i?.url)s.set("media_type","IMAGE"),s.set("image_url",i.url);else s.set("media_type","TEXT");let n=await this.graph(`/${r}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:s.toString()});if(!n.id)throw new w("Threads did not return a media container id.",400,JSON.stringify(n));let o=await this.graph(`/${r}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:n.id,access_token:e.accessToken}).toString()});if(!o.id)throw new w("Threads did not return a published post id.",400,JSON.stringify(o));let a=await this.graph(`/${o.id}?fields=permalink&access_token=${encodeURIComponent(e.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:o.id,url:a?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t){let r=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,t),i=await r.text(),s={};try{s=i?JSON.parse(i):{}}catch{s={}}if(!r.ok||s?.error){let n=s?.error?.message||i||r.statusText;throw new w(`Threads API failed (${r.status}): ${n}`,r.status,i)}return s}}import{Buffer as ge}from"buffer";import{createHash as me,randomBytes as fe}from"crypto";import{fetcher as Z}from"@stacksjs/api";import{config as U}from"@stacksjs/config";class j extends h{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let e={clientId:U.services.twitter?.clientId??"",clientSecret:U.services.twitter?.clientSecret??"",redirectUrl:U.services.twitter?.redirectUrl??"",scopes:U.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(e.scopes),e}generateCodeVerifier(){return fe(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(e){return me("sha256").update(e).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let e=this.getState(),{clientId:t,redirectUrl:r,scopes:i}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let s=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),state:e,response_type:"code",code_challenge:s,code_challenge_method:"S256"}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let s=ge.from(`${t}:${r}`).toString("base64"),n=await Z.withHeaders({Authorization:`Basic ${s}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:e,grant_type:"authorization_code",redirect_uri:i,code_verifier:this.codeVerifier});if(n.data.error)throw Error(`Twitter OAuth error: ${n.data.error_description}`);return n.data.access_token}async getUserByToken(e){let t=await Z.withHeaders({Authorization:`Bearer ${e}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:t.data.id,nickname:t.data.username,name:t.data.name,email:t.data.email??null,avatar:t.data.profile_image_url??null,token:e,raw:t.data}}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new l("Twitter client ID not provided");if(!t)throw new l("Twitter client secret not provided");if(!r)throw new l("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class f extends Error{status;body;constructor(e,t,r){super(e);this.status=t;this.body=r;this.name="TwitterApiError"}get isAuthError(){return this.status===401||this.status===403}}function W(e){let t="";for(let r of e)t+=String.fromCharCode(r);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class k{provider="twitter";characterLimit=280;apiBase;authorizeBase;constructor(e={}){this.apiBase=e.apiBase||"https://api.twitter.com",this.authorizeBase=e.authorizeBase||"https://twitter.com"}async createAuthorization(e){let t=W(crypto.getRandomValues(new Uint8Array(32))),r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)),i=W(new Uint8Array(r)),s=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUrl,scope:e.scopes.join(" "),state:e.state,code_challenge:i,code_challenge_method:"S256"});return{url:`${this.authorizeBase}/i/oauth2/authorize?${s.toString()}`,codeVerifier:t}}async exchangeCode(e){return this.tokenRequest(new URLSearchParams({grant_type:"authorization_code",code:e.code,redirect_uri:e.redirectUrl,code_verifier:e.codeVerifier,client_id:e.clientId}),e.clientId,e.clientSecret)}async refreshAccessToken(e){return this.tokenRequest(new URLSearchParams({grant_type:"refresh_token",refresh_token:e.refreshToken,client_id:e.clientId}),e.clientId,e.clientSecret)}async getProfile(e){let t=await this.request(`${this.apiBase}/2/users/me?user.fields=username,name`,{headers:{authorization:`Bearer ${e}`}});if(!t.data?.id||!t.data.username)throw new f("Twitter did not return the authenticated user.",400,JSON.stringify(t));return{id:t.data.id,username:t.data.username,name:t.data.name}}async uploadMedia(e,t,r){let i=new FormData;i.set("media",new Blob([new Uint8Array(t)],{type:r||"image/jpeg"})),i.set("media_category","tweet_image");let s=await this.request(`${this.apiBase}/2/media/upload`,{method:"POST",headers:{authorization:`Bearer ${e}`},body:i}),n=s.data?.id||s.media_id_string||s.id;if(!n)throw new f("Twitter did not return a media id.",400,JSON.stringify(s));return n}async publish(e,t){if(!e.accessToken)throw Error("Twitter access token is missing for this identity.");if(t.text.length>this.characterLimit)throw Error(`Twitter posts must be ${this.characterLimit} characters or fewer.`);let r=[],i=t.media?.[0];if(i){let{bytes:a,mimeType:c}=i;if(!a?.length&&i.url){let d=await fetch(i.url);if(d.ok)a=new Uint8Array(await d.arrayBuffer()),c=c||d.headers.get("content-type")||"image/jpeg"}if(a?.length)r.push(await this.uploadMedia(e.accessToken,a,c||"image/jpeg"))}let s={text:t.text};if(r.length)s.media={media_ids:r};if(t.reply?.parent?.uri)s.reply={in_reply_to_tweet_id:t.reply.parent.uri};let n=await this.request(`${this.apiBase}/2/tweets`,{method:"POST",headers:{authorization:`Bearer ${e.accessToken}`,"content-type":"application/json"},body:JSON.stringify(s)}),o=n.data?.id;if(!o)throw new f("Twitter did not return a tweet id.",400,JSON.stringify(n));return{provider:this.provider,uri:o,cid:o,url:e.handle?`https://x.com/${e.handle}/status/${o}`:`https://x.com/i/web/status/${o}`}}async timeline(e,t={}){return{items:[]}}async listAuthoredPosts(e,t={}){if(!e.accessToken)throw Error("Twitter access token is missing for this identity.");let r=e.did;if(!r)throw Error("Twitter user id is required to list posts.");let i=new URL(`${this.apiBase}/2/users/${encodeURIComponent(r)}/tweets`);if(i.searchParams.set("max_results",String(Math.min(Math.max(t.limit||100,5),100))),i.searchParams.set("tweet.fields","created_at"),t.cursor)i.searchParams.set("pagination_token",t.cursor);let s=await this.request(i.toString(),{headers:{authorization:`Bearer ${e.accessToken}`}});return{cursor:s.meta?.next_token,posts:(s.data||[]).filter((n)=>n?.id).map((n)=>({uri:n.id,cid:n.id,text:n.text,postedAt:n.created_at,url:`https://x.com/i/web/status/${n.id}`}))}}async deletePost(e,t){if(!e.accessToken)throw Error("Twitter access token is missing for this identity.");let r=String(t.uri||"").trim();if(!r)throw Error("A tweet id is required to delete a post.");let i=await this.request(`${this.apiBase}/2/tweets/${encodeURIComponent(r)}`,{method:"DELETE",headers:{authorization:`Bearer ${e.accessToken}`}});if(i.data&&i.data.deleted===!1)throw new f(`X refused to delete tweet ${r}.`,400,JSON.stringify(i))}async tokenRequest(e,t,r){let i={"content-type":"application/x-www-form-urlencoded"};if(r)i.authorization=`Basic ${btoa(`${t}:${r}`)}`;let s=await this.request(`${this.apiBase}/2/oauth2/token`,{method:"POST",headers:i,body:e.toString()});if(!s.access_token)throw new f("Twitter did not return an access token.",400,JSON.stringify(s));return{accessToken:s.access_token,refreshToken:s.refresh_token,expiresIn:s.expires_in,scope:s.scope}}async request(e,t){let r=await fetch(e,t),i=await r.text();if(!r.ok)throw new f(`Twitter API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}import{config as ye}from"@stacksjs/config";var R=["clientId","clientSecret","redirectUrl"],I=Object.freeze({google:{name:"google",label:"Google",driver:z,required:R,postCallback:!1},github:{name:"github",label:"GitHub",driver:q,required:R,postCallback:!1},facebook:{name:"facebook",label:"Facebook",driver:L,required:R,postCallback:!1},twitter:{name:"twitter",label:"X",driver:j,required:R,postCallback:!1},apple:{name:"apple",label:"Apple",driver:O,required:["clientId","teamId","keyId","privateKey","redirectUrl"],postCallback:!0}});function X(e){return ye?.services?.[e]}function we(e){return typeof e==="string"&&e in I}function Y(e){if(!we(e))return!1;let t=X(e);if(!t)return!1;return I[e].required.every((r)=>Boolean(t[r]))}function Pt(){return Object.keys(I).filter(Y).map((e)=>I[e])}function St(e){if(!Y(e))return null;let t=I[e],r=X(e)??{};return new t.driver({clientSecret:"",...r,clientId:String(r.clientId??""),redirectUrl:String(r.redirectUrl??"")})}class ke{accessToken;refreshToken;expiresIn;approvedScopes;constructor(e,t=null,r=null,i=[]){this.accessToken=e;this.refreshToken=t;this.expiresIn=r;this.approvedScopes=i}}function Tt(e){return typeof e?.deletePost==="function"}function xt(e){return typeof e?.listAuthoredPosts==="function"}import{buildSessionHandoffUrl as be}from"@stacksjs/composables";function ee(e,t=[]){if(!e)return!1;if(e.startsWith("//"))return!1;if(e.startsWith("/"))return!0;try{let r=new URL(e);if(r.protocol!=="http:"&&r.protocol!=="https:")return!1;return t.includes(r.host)}catch{return!1}}function Ut(e,t={}){let r=t.redirectTo??"/";if(!ee(r,t.allowedHosts??[]))throw Error(`[socials] refusing to hand a session to ${r}: relative paths are always allowed; an absolute URL needs its host in allowedHosts.`);return new Response(null,{status:302,headers:{Location:be(r,e),"Cache-Control":"no-store","Referrer-Policy":"no-referrer"}})}function $t(e,t={}){let r=t.redirectTo??"/login";if(!ee(r,t.allowedHosts??[]))throw Error(`[socials] refusing to redirect to ${r}`);let i=r.includes("?")?"&":"?",s=`${r}${i}social_error=${encodeURIComponent(e)}`;return new Response(null,{status:302,headers:{Location:s,"Cache-Control":"no-store"}})}var Pe={bluesky:["identifier","password","service"],twitter:["clientId","clientSecret","accessToken","refreshToken"],linkedin:["accessToken","memberUrn","clientId","clientSecret","apiVersion"],mastodon:["accessToken","instanceUrl"],instagram:["accessToken","accountId","clientId","clientSecret"],threads:["accessToken","userId","clientId","clientSecret"]},Se={bluesky:["identifier","password"],twitter:["clientId","clientSecret","accessToken"],linkedin:["accessToken","memberUrn"],mastodon:["accessToken","instanceUrl"],instagram:["accessToken","accountId"],threads:["accessToken","userId"]};function ve(e){return e.toUpperCase().replace(/[^A-Z0-9]/g,"")}function A(e,t,r){let i=r.replace(/[A-Z]/g,(s)=>`_${s}`).toUpperCase();return`SOCIALS_${ve(e)}_${t.toUpperCase()}_${i}`}class p extends Error{constructor(e){super(e);this.name="SocialIdentityError"}}async function E(){let{config:e,overridesReady:t}=await import("@stacksjs/config");return await t,e.socials??{}}function te(e,t){let r=Object.keys(e.identities??{});if(t){if(!r.includes(t))throw new p(`Unknown social identity '${t}'. config/socials.ts declares: ${r.length?r.join(", "):"none"}.`);return t}if(e.default){if(!r.includes(e.default))throw new p(`config/socials.ts sets default: '${e.default}', which is not one of its identities: ${r.length?r.join(", "):"none"}.`);return e.default}if(r.length===1)return r[0];if(r.length===0)throw new p("config/socials.ts declares no identities, so there is no account to post as.");throw new p(`config/socials.ts declares ${r.length} identities (${r.join(", ")}) and no default, so a call has to name one.`)}function Ie(e,t,r,i=process.env){let n=(e.identities?.[r]??{}).credentials?.[t]??{},o={};for(let a of Pe[t]){let c=(n[a]??i[A(r,t,a)]??"").trim();if(c)o[a]=c}return o}function G(e,t,r,i=process.env){let s=te(e,r),n=Ie(e,t,s,i),o=Se[t].filter((a)=>!n[a]);if(o.length){let a=o.map((c)=>A(s,t,c)).join(", ");throw new p(`Identity '${s}' is not configured for ${t}: ${o.join(", ")} ${o.length===1?"is":"are"} missing. Set ${a}, or supply them inline in config/socials.ts.`)}return n}async function re(e){return te(await E(),e)}async function b(e,t,r){return G(await E(),e,t,r)}function At(e,t){return b("bluesky",e,t)}function Et(e,t){return b("twitter",e,t)}function Bt(e,t){return b("linkedin",e,t)}function Ot(e,t){return b("mastodon",e,t)}function Lt(e,t){return b("instagram",e,t)}function Dt(e,t){return b("threads",e,t)}import{randomBytes as Te}from"crypto";function Ht(e){return e==="twitter"}var xe=["tweet.read","tweet.write","users.read","offline.access"],_e="127.0.0.1",ie="/callback";function Jt(e){if(!Number.isInteger(e)||e<1||e>65535)throw new p(`${e} is not a port a redirect can listen on.`);return`http://${_e}:${e}${ie}`}function jt(){return Te(16).toString("hex")}function Gt(e,t){let r;try{r=new URL(e)}catch{return{ok:!1,reason:"not-the-callback",message:`Not a URL: ${e}`}}if(r.pathname!==ie)return{ok:!1,reason:"not-the-callback",message:`Ignoring a request for ${r.pathname}.`};let i=r.searchParams.get("error");if(i){let o=r.searchParams.get("error_description");return{ok:!1,reason:"declined",message:o?`${i}: ${o}`:i}}let s=r.searchParams.get("state")??"";if(!t||s!==t)return{ok:!1,reason:"state-mismatch",message:"The redirect carried the wrong state, so it did not come from this run."};let n=r.searchParams.get("code")??"";if(!n)return{ok:!1,reason:"no-code",message:"The redirect carried no authorization code."};return{ok:!0,code:n}}function Mt(e,t,r){let i=[];for(let[s,n]of Object.entries(r)){if(!n)continue;i.push(`${A(e,t,s)}=${n}`)}return i}async function Ft(e){let t=new k,r=await t.exchangeCode(e);if(!r.accessToken)throw new p("X returned no access token for that code.");let i=Boolean(r.refreshToken),s=await t.getProfile(r.accessToken);return{handle:s.username,accountId:s.id,accessToken:r.accessToken,refreshToken:r.refreshToken,expiresIn:r.expiresIn,scope:r.scope,renewable:i}}async function Qt(e){return new k().createAuthorization({clientId:e.clientId,redirectUrl:e.redirectUrl,scopes:[...xe],state:e.state})}function Kt(e,t){let r=(i)=>i.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");return`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${r(e)}</title><style>body{font:16px/1.5 system-ui,sans-serif;margin:0;display:grid;place-items:center;min-height:100vh;color:#111}main{max-width:28rem;padding:2rem;text-align:center}h1{font-size:1.25rem;margin:0 0 .5rem}p{margin:0;color:#555}@media(prefers-color-scheme:dark){body{background:#111;color:#eee}p{color:#aaa}}</style></head><body><main><h1>${r(e)}</h1><p>${r(t)}</p></main></body></html>`}var Ce={bluesky:null,twitter:null,linkedin:"memberUrn",mastodon:"instanceUrl",instagram:"accountId",threads:"userId"};function Ue(e,t){if(e==="bluesky")return t.service?{service:t.service}:{};if(e==="linkedin")return t.apiVersion?{apiVersion:t.apiVersion}:{};return{}}function $e(e,t={}){switch(e){case"bluesky":return new T(t);case"linkedin":return new V(t);case"twitter":return new k;case"mastodon":return new H;case"instagram":return new N;case"threads":return new J;default:throw new p(`No publishing driver for '${String(e)}'.`)}}function Re(e,t,r){let i=Ce[e],s={handle:(t??"").trim()};if(r.accessToken)s.accessToken=r.accessToken;if(r.refreshToken)s.refreshToken=r.refreshToken;if(i&&r[i])s.did=r[i];return s}function Ae(e,t,r){if(!r)return;if(r.includes(t))return;throw new p(`Identity '${e}' is not set up for ${t}. config/socials.ts lists: ${r.length?r.join(", "):"none"}.`)}async function Ee(e,t,r){let i=await E(),s=await re(t),n=i.identities?.[s]??{};Ae(s,e,n.platforms);let o=G(i,e,s,r),a=Ue(e,o);if(e==="bluesky"){let c=await new T(a).createSession({identifier:o.identifier,password:o.password});return{name:s,platform:e,identity:{handle:c.handle,did:c.did,accessToken:c.accessJwt,refreshToken:c.refreshJwt},options:a}}return{name:s,platform:e,identity:Re(e,n.handle??"",o),options:a}}async function Yt(e,t,r,i){let s=await Ee(e,r,i);return $e(s.platform,s.options).publish(s.identity,t)}export{_e as AUTHORIZE_HOST,ie as AUTHORIZE_PATH,h as AbstractProvider,O as AppleProvider,M as BlueskyApiError,T as BlueskyPublishingDriver,l as ConfigException,Ce as DRIVER_DID_FIELD,L as FacebookProvider,q as GitHubProvider,z as GoogleProvider,v as InstagramApiError,N as InstagramPublishingDriver,ne as InvalidStateException,g as LinkedInApiError,V as LinkedInPublishingDriver,x as META_GRAPH_VERSION,K as MastodonApiError,H as MastodonPublishingDriver,Se as REQUIRED_SOCIAL_CREDENTIALS,Pe as SOCIAL_CREDENTIAL_FIELDS,I as SOCIAL_PROVIDERS,p as SocialIdentityError,xe as TWITTER_PUBLISH_SCOPES,w as ThreadsApiError,J as ThreadsPublishingDriver,ke as Token,f as TwitterApiError,j as TwitterProvider,k as TwitterPublishingDriver,Ae as assertPlatformDeclared,Kt as callbackPage,Jt as callbackUri,Ft as completeTwitterAuthorization,Pt as configuredSocialProviders,de as detectFacetCandidates,Ue as driverOptions,Mt as envLines,A as envName,ve as envSegment,ue as escapeLinkedInText,Ht as isAuthorizablePlatform,ee as isSafeHandoffTarget,Y as isSocialProviderConfigured,we as isSocialProviderName,jt as newState,pe as normalizeInstance,ce as parseAtUri,G as pickCredentials,te as pickIdentity,Yt as publishAs,$e as publishingDriver,Gt as readCallback,Ie as readCredentials,At as resolveBlueskyCredentials,re as resolveIdentityName,Lt as resolveInstagramCredentials,Bt as resolveLinkedInCredentials,Ot as resolveMastodonCredentials,b as resolveSocialCredentials,Dt as resolveThreadsCredentials,Et as resolveTwitterCredentials,$t as socialHandoffFailureRedirect,Ut as socialHandoffRedirect,St as socialProvider,Ee as socialPublishTarget,E as socialsConfig,Qt as startTwitterAuthorization,Tt as supportsDeletion,xt as supportsEnumeration,Re as toDriverIdentity};
@@ -0,0 +1,91 @@
1
+ import type { PublishedPost, PublishPostInput, SocialIdentityCredentials, SocialPublishingDriver } from './types';
2
+ import type { SocialCredentialPlatform } from '@stacksjs/types';
3
+ /**
4
+ * The two driver options that are per identity rather than per platform.
5
+ *
6
+ * Pure and separate from the identity, because they go to a constructor while
7
+ * the identity goes to `publish()`. Resolved from the same credentials, so an
8
+ * app on a self-hosted PDS or an older LinkedIn tenant configures it in one
9
+ * place.
10
+ */
11
+ export declare function driverOptions(platform: SocialCredentialPlatform, credentials: Record<string, string | undefined>): SocialDriverOptions;
12
+ /** The driver for a platform. */
13
+ export declare function publishingDriver(platform: SocialCredentialPlatform, options?: SocialDriverOptions): SocialPublishingDriver;
14
+ /**
15
+ * Resolved credentials, in the shape a driver reads.
16
+ *
17
+ * Pure, so the mapping is testable without a network: this is the function
18
+ * that decides a LinkedIn post's author and a Mastodon post's instance, and
19
+ * getting either wrong posts to the wrong place rather than failing.
20
+ *
21
+ * Bluesky is the exception and is handled by {@link socialPublishTarget},
22
+ * because its `accessToken` comes from a session mint rather than from
23
+ * configuration.
24
+ */
25
+ export declare function toDriverIdentity(platform: SocialCredentialPlatform, handle: string, credentials: Record<string, string | undefined>): SocialIdentityCredentials;
26
+ /**
27
+ * Refuse a platform the identity does not list.
28
+ *
29
+ * Declaring `platforms` in `config/socials.ts` is what turns a missing
30
+ * credential into a configuration error rather than a silent no-op, so an
31
+ * undeclared platform must not quietly work off stray environment variables.
32
+ * An identity that declares no platforms at all is unconstrained, which is
33
+ * what an app with one identity and no list means.
34
+ *
35
+ * Pure, because it is the one check standing between "this identity speaks on
36
+ * these networks" and posting somewhere nobody said it could.
37
+ */
38
+ export declare function assertPlatformDeclared(name: string, platform: SocialCredentialPlatform, platforms?: readonly SocialCredentialPlatform[]): void;
39
+ /**
40
+ * Resolve one identity on one platform, ready to post.
41
+ *
42
+ * Bluesky mints a session here, which is a network call: its app password is
43
+ * long-lived and the token it buys is not, so there is nothing to store
44
+ * between posts and nothing to refresh.
45
+ */
46
+ export declare function socialPublishTarget(platform: SocialCredentialPlatform, identity?: string, env?: Record<string, string | undefined>): Promise<SocialPublishTarget>;
47
+ /**
48
+ * Post as a configured identity.
49
+ *
50
+ * The one call a caller needs: `publishAs('bluesky', { text }, 'home-lang')`.
51
+ * Nothing about which field is a DID, which driver takes which options, or
52
+ * where a credential lives reaches the call site.
53
+ */
54
+ export declare function publishAs(platform: SocialCredentialPlatform, post: PublishPostInput, identity?: string, env?: Record<string, string | undefined>): Promise<PublishedPost>;
55
+ /**
56
+ * Posting as a configured identity.
57
+ *
58
+ * The drivers and the credential resolver both existed and nothing joined
59
+ * them: `config/socials.ts` resolves to `{ accessToken, memberUrn }` while a
60
+ * driver reads `{ handle, did, accessToken }`, so every caller had to know
61
+ * which config field goes in the `did` slot for which platform
62
+ * (stacksjs/stacks#2873). That knowledge belongs here, once.
63
+ */
64
+ /**
65
+ * The resolved credential field each driver reads out of `identity.did`.
66
+ *
67
+ * `did` is an ATProto name and the slot is reused by every driver for "where
68
+ * or as whom", which is the one thing a token does not say. Four platforms
69
+ * need it and refuse without it; this table is what keeps the mapping out of
70
+ * the call sites.
71
+ *
72
+ * Null means the driver does not read the slot:
73
+ *
74
+ * - `bluesky` fills it itself. `createSession` returns the account's real DID,
75
+ * so taking one from configuration would be a second source for a value the
76
+ * network already answers.
77
+ * - `twitter` never reads it. Posting is scoped by the token alone.
78
+ */
79
+ export declare const DRIVER_DID_FIELD: Record<SocialCredentialPlatform, string | null>;
80
+ /** Driver constructor options that come from a credential rather than a default. */
81
+ export declare interface SocialDriverOptions {
82
+ service?: string
83
+ apiVersion?: string
84
+ }
85
+ /** Everything needed to post as one identity on one platform. */
86
+ export declare interface SocialPublishTarget {
87
+ name: string
88
+ platform: SocialCredentialPlatform
89
+ identity: SocialIdentityCredentials
90
+ options: SocialDriverOptions
91
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/socials",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.90",
5
+ "version": "0.75.91",
6
6
  "description": "A simple and elegant social authentication package for Stacks.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -57,13 +57,13 @@
57
57
  "prepublishOnly": "bun run build"
58
58
  },
59
59
  "dependencies": {
60
- "@stacksjs/api": "0.75.90",
61
- "@stacksjs/composables": "0.75.90",
62
- "@stacksjs/config": "0.75.90"
60
+ "@stacksjs/api": "0.75.91",
61
+ "@stacksjs/composables": "0.75.91",
62
+ "@stacksjs/config": "0.75.91"
63
63
  },
64
64
  "devDependencies": {
65
- "@stacksjs/error-handling": "0.75.90",
66
- "@stacksjs/router": "0.75.90",
65
+ "@stacksjs/error-handling": "0.75.91",
66
+ "@stacksjs/router": "0.75.91",
67
67
  "better-dx": "^0.2.24"
68
68
  }
69
69
  }