@stacksjs/socials 0.75.226 → 0.75.230

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.
@@ -36,6 +36,7 @@ export declare interface TwitterProfile {
36
36
  export declare class TwitterProvider extends AbstractProvider implements ProviderInterface {
37
37
  protected baseUrl: string;
38
38
  protected apiUrl: string;
39
+ protected _usesPKCE: boolean;
39
40
  getAuthUrl(): Promise<string>;
40
41
  getAccessToken(code: string): Promise<string>;
41
42
  getUserByToken(token: string): Promise<SocialUser>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Single-use, browser-bound OAuth flow state (login CSRF protection).
3
+ *
4
+ * A social callback that exchanges any `code` it is handed signs the browser
5
+ * into whichever account that code belongs to. An attacker starts a sign-in
6
+ * with their own provider account, stops at the callback, and sends the
7
+ * victim the link: the victim is now signed in as the attacker, and whatever
8
+ * they type lands in the attacker's account. The OAuth `state` parameter
9
+ * exists to stop this, but only when the callback checks it against
10
+ * something the attacker cannot plant in the victim's browser.
11
+ *
12
+ * {@link issueOAuthFlow} mints a random `state` and an independent browser
13
+ * secret. The state goes to the provider; the secret goes to the browser in
14
+ * an HttpOnly cookie; only SHA-256 digests of both are stored. On the
15
+ * callback, {@link consumeOAuthFlow} accepts the flow once, and only when the
16
+ * same browser presents the matching secret before the deadline. A leaked
17
+ * callback URL, a replayed state, or a state from another browser all
18
+ * resolve to `null`.
19
+ *
20
+ * The same store backs other one-use, browser-bound hand-offs (the session
21
+ * handoff in `@stacksjs/auth`), distinguished by `purpose`, so a state minted
22
+ * for one purpose can never be redeemed as another.
23
+ *
24
+ * Storage is the auth-owned `oauth_flows` table (`buddy migrate` creates it
25
+ * with the other auth tables). Tests and unusual deployments can swap it with
26
+ * {@link useOAuthFlowStore}.
27
+ */
28
+ /** Hex SHA-256, the only form a state or a browser secret is ever stored in. */
29
+ export declare function oauthFlowDigest(value: string): string;
30
+ /**
31
+ * Replace the flow store (pass `null` to restore the database store). Meant
32
+ * for tests and for apps that keep this state somewhere other than SQL.
33
+ */
34
+ export declare function useOAuthFlowStore(store: OAuthFlowStore | null): void;
35
+ /** The per-flow cookie name, so two sign-ins in two tabs do not overwrite each other. */
36
+ export declare function oauthFlowCookieName(state: string): string;
37
+ /**
38
+ * Start a flow. Returns the state for the provider and the cookie for the
39
+ * browser; send both, or neither.
40
+ */
41
+ export declare function issueOAuthFlow(options: IssueOAuthFlowOptions): Promise<IssuedOAuthFlow>;
42
+ /**
43
+ * Redeem a flow, once. Null when the state is unknown, already used, expired,
44
+ * issued for another purpose, or bound to a different browser.
45
+ *
46
+ * A mismatched purpose or browser does NOT consume the flow, so an attacker
47
+ * who learned a state cannot burn the legitimate browser's sign-in.
48
+ */
49
+ export declare function consumeOAuthFlow(state: unknown, options: ConsumeOAuthFlowOptions): Promise<ConsumedOAuthFlow | null>;
50
+ /** The browser secret for `state` from a `Cookie` request header, or ''. */
51
+ export declare function readOAuthFlowCookie(cookieHeader: string | null | undefined, state: string): string;
52
+ /** A `Set-Cookie` value that removes the flow's cookie. Send it on every callback outcome. */
53
+ export declare function clearOAuthFlowCookie(state: string, options?: OAuthFlowCookieOptions): Promise<string>;
54
+ /** An in-memory store, for tests and single-process tools. */
55
+ export declare function memoryOAuthFlowStore(): OAuthFlowStore & { records: Map<string, OAuthFlowRecord> };
56
+ /**
57
+ * The default store: the auth-owned `oauth_flows` table. The database graph
58
+ * is imported on first use, so importing `@stacksjs/socials` for its
59
+ * provider drivers alone stays light.
60
+ */
61
+ export declare function databaseOAuthFlowStore(): OAuthFlowStore;
62
+ /** One stored flow, as the store sees it. Never carries a raw state or secret. */
63
+ export declare interface OAuthFlowRecord {
64
+ stateDigest: string
65
+ purpose: string
66
+ browserDigest: string | null
67
+ userId: number | null
68
+ nonce: string | null
69
+ data: Record<string, unknown> | null
70
+ expiresAt: Date
71
+ }
72
+ /**
73
+ * Where flows live. `take` must be atomic: of two concurrent calls for the
74
+ * same digest, at most one may return the record.
75
+ */
76
+ export declare interface OAuthFlowStore {
77
+ insert: (record: OAuthFlowRecord) => Promise<void>
78
+ find: (stateDigest: string) => Promise<OAuthFlowRecord | undefined>
79
+ take: (stateDigest: string) => Promise<boolean>
80
+ prune: (now: Date) => Promise<void>
81
+ }
82
+ export declare interface IssueOAuthFlowOptions {
83
+ purpose: string
84
+ userId?: number | null
85
+ data?: Record<string, unknown>
86
+ ttlMs: number
87
+ bindBrowser?: boolean
88
+ challenge?: string
89
+ nonce?: boolean | string
90
+ cookie?: OAuthFlowCookieOptions
91
+ }
92
+ export declare interface OAuthFlowCookieOptions {
93
+ path?: string
94
+ sameSite?: 'Lax' | 'Strict' | 'None'
95
+ secure?: boolean
96
+ }
97
+ export declare interface IssuedOAuthFlow {
98
+ state: string
99
+ cookie: string | null
100
+ secret: string | null
101
+ nonce?: string
102
+ expiresAt: Date
103
+ }
104
+ export declare interface ConsumeOAuthFlowOptions {
105
+ purpose: string
106
+ browserSecret?: string | null
107
+ }
108
+ export declare interface ConsumedOAuthFlow {
109
+ purpose: string
110
+ userId: number | null
111
+ nonce: string | null
112
+ data: Record<string, unknown> | null
113
+ expiresAt: Date
114
+ }
package/dist/handoff.d.ts CHANGED
@@ -29,6 +29,18 @@ export declare function isSafeHandoffTarget(redirectTo: string, allowedHosts?: s
29
29
  * cached 302 would replay someone else's session to the next visitor.
30
30
  */
31
31
  export declare function socialHandoffRedirect(pack: SessionHandoffPack, options?: SocialHandoffOptions): Response;
32
+ /**
33
+ * The redirect that completes a social sign-in WITHOUT a credential in the URL.
34
+ *
35
+ * `code` comes from `createSessionHandoff()` in `@stacksjs/auth`: a 60-second,
36
+ * single-use code bound to this browser's cookie (or to a native app's
37
+ * verifier). `useAuth().completeSocialLogin()` reads it from the fragment and
38
+ * POSTs it to `/auth/handoff`, which answers with the session. This is what
39
+ * the framework's default callback sends; prefer it over
40
+ * {@link socialHandoffRedirect}, whose fragment carries the bearer and
41
+ * refresh tokens themselves (browser history keeps those).
42
+ */
43
+ export declare function socialHandoffCodeRedirect(code: string, options?: SocialHandoffOptions): Response;
32
44
  /**
33
45
  * The redirect for a sign-in that did not succeed.
34
46
  *
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './registry';
5
5
  export * from './token';
6
6
  export * from './types';
7
7
  export * from './handoff';
8
+ export * from './flows';
8
9
  export * from './identities';
9
10
  export * from './authorize';
10
11
  export * from './publish';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // @bun
2
- var ge=import.meta.require;import{Buffer as T}from"buffer";class g{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_codeVerifier=null;_state=null;user=null;constructor(e){this.clientId=e.clientId,this.clientSecret=e.clientSecret,this.redirectUrl=e.redirectUrl}async 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;return{...t,...await this.pkceFields(),...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}=ge("crypto");return r(T.from(e,"utf8"),T.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(){if(this._codeVerifier===null){let e=new Uint8Array(32);crypto.getRandomValues(e),this._codeVerifier=T.from(e).toString("base64url")}return this._codeVerifier}getPkceVerifier(){return this._codeVerifier}withCodeVerifier(e){if(!e)throw Error("A PKCE code verifier must be a non-empty string");return this._usesPKCE=!0,this._codeVerifier=e,this}async pkceFields(){if(!this.usesPKCE())return{};return{code_challenge:await this.getCodeChallenge(),code_challenge_method:this.getCodeChallengeMethod()}}pkceTokenFields(){if(!this.usesPKCE())return{};if(this._codeVerifier===null)throw Error("PKCE is enabled but no code verifier is known for this exchange. Pass the one from getPkceVerifier() back with withCodeVerifier().");return{code_verifier:this._codeVerifier}}async getCodeChallenge(){let t=new TextEncoder().encode(this.getCodeVerifier()),r=await crypto.subtle.digest("SHA-256",t);return T.from(new Uint8Array(r)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(e){return this.parameters=e,this}async buildAuthUrlFromBase(e,t){let r=new URLSearchParams(await this.getCodeFields(t));return`${e}?${r.toString()}`}}import{Buffer as ee}from"buffer";import{createPrivateKey as ke,sign as be}from"crypto";import{config as w}from"@stacksjs/config";class fe extends Error{constructor(e="Invalid state"){super(e);this.name="InvalidStateException"}}class d extends Error{constructor(e){super(e);this.name="ConfigException"}}import{Buffer as V}from"buffer";var Y=new Map,me=600000,ye=60000;async function Z(e,t){let r=await fetch(e);if(!r.ok)throw Error(`id_token verification failed: could not fetch signing keys from ${e} (HTTP ${r.status})`);let i=await r.json(),s=Array.isArray(i.keys)?i.keys:[];return Y.set(e,{keys:s,fetchedAt:t}),s}async function we(e,t,r){let i=Y.get(e),s=i!==void 0&&r-i.fetchedAt<me,n=s?i.keys:await Z(e,r),o=n.find((a)=>a.kid===t);if(!o&&s&&r-i.fetchedAt>=ye)n=await Z(e,r),o=n.find((a)=>a.kid===t);if(!o)throw Error(`id_token verification failed: no signing key matches kid "${t}"`);if(o.kty!=="RSA")throw Error(`id_token verification failed: signing key "${t}" is ${o.kty}, expected RSA`);return o}function X(e,t){try{return JSON.parse(V.from(e,"base64url").toString("utf8"))}catch{throw Error(`id_token verification failed: ${t} is not valid JSON`)}}async function x(e,t){let r=e.split(".");if(r.length!==3)throw Error("id_token verification failed: malformed token");let[i,s,n]=r,o=X(i,"header");if(o.alg!=="RS256")throw Error(`id_token verification failed: unsupported algorithm ${JSON.stringify(o.alg)}, expected RS256`);if(typeof o.kid!=="string"||o.kid==="")throw Error("id_token verification failed: header has no kid");let a=t.now??Date.now(),c=await we(t.jwksUrl,o.kid,a),u=await crypto.subtle.importKey("jwk",{kty:"RSA",n:c.n,e:c.e,alg:"RS256",ext:!0},{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["verify"]);if(!await crypto.subtle.verify("RSASSA-PKCS1-v1_5",u,V.from(n,"base64url"),V.from(`${i}.${s}`)))throw Error("id_token verification failed: signature does not match the issuer's key");let l=X(s,"payload"),L=(t.clockSkewSeconds??60)*1000,W=Array.isArray(t.issuer)?t.issuer:[t.issuer];if(!W.includes(l.iss))throw Error(`id_token verification failed: issuer is ${JSON.stringify(l.iss)}, expected ${W.map((D)=>JSON.stringify(D)).join(" or ")}`);let he=Array.isArray(t.audience)?t.audience:[t.audience];if(!(Array.isArray(l.aud)?l.aud:[l.aud]).some((D)=>he.includes(D)))throw Error("id_token verification failed: token is addressed to a different client");if(typeof l.exp!=="number"||l.exp*1000+L<=a)throw Error("id_token verification failed: token has expired");if(typeof l.iat==="number"&&l.iat*1000-L>a)throw Error("id_token verification failed: token was issued in the future");if(typeof l.nbf==="number"&&l.nbf*1000-L>a)throw Error("id_token verification failed: token is not valid yet");if(t.nonce!==void 0&&l.nonce!==t.nonce)throw Error("id_token verification failed: nonce does not match");if(typeof l.sub!=="string"||l.sub==="")throw Error("id_token verification failed: token has no subject");return l}class N extends g{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";native=!1;constructor(e){super(e);this.teamId=e.teamId??"",this.keyId=e.keyId??"",this.privateKey=e.privateKey??"",this.native=e.native===!0}getConfig(){let e={clientId:this.clientId||(w.services.apple?.clientId??""),teamId:this.teamId||(w.services.apple?.teamId??""),keyId:this.keyId||(w.services.apple?.keyId??""),privateKey:(this.privateKey||(w.services.apple?.privateKey??"")).replace(/\\n/g,`
3
- `),redirectUrl:this.native?"":this.redirectUrl||(w.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:w.services.apple?.scopes??["name","email"]};return this.setScopes(e.scopes),e}async getAuthUrl(){if(this.native)throw new d("A native Apple provider redeems the app's codes; the web flow needs the Service ID");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,...this.native?{}:{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,t={}){let{clientId:r}=this.getConfig(),i;try{i=await x(e,{jwksUrl:`${this.baseUrl}/auth/keys`,issuer:this.baseUrl,audience:r,nonce:t.nonce})}catch(o){throw Error(`Apple OAuth error: ${o instanceof Error?o.message:String(o)}`)}let s=typeof i.email==="string"?i.email:null,n=null;if(i.email_verified===!0||i.email_verified==="true")n=!0;else if(i.email_verified===!1||i.email_verified==="false")n=!1;return{id:String(i.sub),nickname:null,name:"",email:s,emailVerified:n,avatar:null,token:e,raw:i}}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=ke(i)}catch(p){throw new d(`Apple private key could not be parsed: ${p instanceof Error?p.message:String(p)}`)}let u=be("sha256",ee.from(a),{key:c,dsaEncoding:"ieee-p1363"});return`${a}.${u.toString("base64url")}`}base64urlJson(e){return ee.from(JSON.stringify(e)).toString("base64url")}validateConfig(){let{clientId:e,teamId:t,keyId:r,privateKey:i,redirectUrl:s}=this.getConfig();if(!e)throw new d(this.native?"Apple client ID (the app's bundle ID) not provided":"Apple client ID (Service ID) not provided");if(!t)throw new d("Apple team ID not provided");if(!r)throw new d("Apple key ID not provided");if(!i)throw new d("Apple private key not provided");if(!s&&!this.native)throw new d("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function Pe(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 te 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 Se=new TextEncoder;function m(e){return Se.encode(e).length}function ve(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 u=(o.index??0)+a.length,p=m(e.slice(0,u)),l=p+m(c);if(i(p,l))continue;t.push({byteStart:p,byteEnd:l,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 u=(o.index??0)+a.length,p=m(e.slice(0,u)),l=p+m(c);if(i(p,l))continue;t.push({byteStart:p,byteEnd:l,type:"mention",value:c.slice(1).replace(/\.+$/,"")})}return t.sort((o,a)=>o.byteStart-a.byteStart)}class _{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 u=await this.uploadBlob(e,c.bytes,c.mimeType||"image/jpeg");a.push({image:u,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 ve(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}=Pe(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 te(`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 re}from"@stacksjs/api";import{config as S}from"@stacksjs/config";var C="v24.0";class q extends g{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";get graphVersion(){return S.services.facebook?.graphVersion??C}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",...await this.pkceFields()}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await re.get(`${this.apiUrl}/${this.graphVersion}/oauth/access_token?${new URLSearchParams({client_id:t,client_secret:r,redirect_uri:i,code:e,...this.pkceTokenFields()}).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 re.withHeaders({Authorization:`Bearer ${e}`}).get(`${this.apiUrl}/${this.graphVersion}/me?${new URLSearchParams({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 d("Facebook client ID not provided");if(!t)throw new d("Facebook client secret not provided");if(!r)throw new d("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/${this.graphVersion}/oauth/access_token`}}import{fetcher as z}from"@stacksjs/api";import{config as U}from"@stacksjs/config";class H extends g{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let e={clientId:this.clientId||(U.services.github?.clientId??""),clientSecret:this.clientSecret||(U.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(U.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:U.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",...await this.pkceFields(),...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await z.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:t,client_secret:r,code:e,redirect_uri:i,...this.pkceTokenFields()});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([z.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${e}`}).get(`${this.apiUrl}/user`),z.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 d("GitHub client ID not provided");if(!t)throw new d("GitHub client secret not provided");if(!r)throw new d("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as ie}from"@stacksjs/api";import{config as R}from"@stacksjs/config";class J extends g{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";tokenUrl="https://oauth2.googleapis.com/token";tokenInfoUrl="https://oauth2.googleapis.com/tokeninfo";certsUrl="https://www.googleapis.com/oauth2/v3/certs";issuers=["https://accounts.google.com","accounts.google.com"];getConfig(){let e={clientId:this.clientId||(R.services.google?.clientId??""),clientSecret:this.clientSecret||(R.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(R.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:R.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",...await this.pkceFields(),...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await ie.post(this.tokenUrl,{client_id:t,client_secret:r,code:e,redirect_uri:i,grant_type:"authorization_code",...this.pkceTokenFields()});if(s.data.error)throw Error(`Google OAuth error: ${s.data.error_description}`);return s.data.access_token}async getUserByToken(e){await this.assertTokenWasIssuedToUs(e);let t=await ie.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}}async getUserByIdToken(e,t={}){let{clientId:r}=this.getConfig(),i;try{i=await x(e,{jwksUrl:this.certsUrl,issuer:this.issuers,audience:r,nonce:t.nonce})}catch(n){throw Error(`Google OAuth error: ${n instanceof Error?n.message:String(n)}`)}let s=typeof i.email==="string"?i.email:"";return{id:String(i.sub),nickname:typeof i.given_name==="string"?i.given_name:null,name:typeof i.name==="string"?i.name:"",email:s,emailVerified:typeof i.email_verified==="boolean"?i.email_verified:i.email_verified==="true"?!0:i.email_verified==="false"?!1:null,avatar:typeof i.picture==="string"?i.picture:null,token:e,raw:i}}async assertTokenWasIssuedToUs(e){let{clientId:t}=this.getConfig(),r=await fetch(`${this.tokenInfoUrl}?access_token=${encodeURIComponent(e)}`);if(!r.ok)throw Error(`Google OAuth error: access token was not accepted by tokeninfo (HTTP ${r.status})`);let i=await r.json();if(i.error)throw Error(`Google OAuth error: ${i.error_description??i.error}`);let s=i.aud??i.audience;if(!s)throw Error("Google OAuth error: tokeninfo did not report an audience for the access token");if(s!==t)throw Error("Google OAuth error: access token was issued to a different client")}validateConfig(){let{clientId:e,clientSecret:t,redirectUrl:r}=this.getConfig();if(!e)throw new d("Google client ID not provided");if(!t)throw new d("Google client secret not provided");if(!r)throw new d("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 j{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(e={}){this.graphVersion=e.graphVersion||C,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=await this.graph("/oauth/access_token",{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,redirect_uri:e.redirectUrl,code:e.code}).toString()});if(!t.access_token)throw new v("Facebook did not return an access token.",400,JSON.stringify(t));return{accessToken:t.access_token,expiresIn:t.expires_in}}async resolveAccount(e){let t=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}"}),r=await this.graph(`/me/accounts?${t.toString()}`,{method:"GET"},e),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}).toString()},e.accessToken);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}).toString()},e.accessToken),o=await this.graph(`/${n.id}?fields=permalink`,{method:"GET"},e.accessToken).catch(()=>{return});return{provider:this.provider,uri:n.id,url:o?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t,r){let i=new Headers(t.headers);if(r)i.set("authorization",`Bearer ${r}`);let s=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,{...t,headers:i}),n=await s.text(),o={};try{o=n?JSON.parse(n):{}}catch{o={}}if(!s.ok||o?.error){let a=o?.error?.message||n||s.statusText;throw new v(`Instagram API failed (${s.status}): ${a}`,s.status,n)}return o}}class f 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 G{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 f("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 f("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:Ie(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 f(`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 f&&(c.status===401||c.status===403))throw new f("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 f(`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 f(`LinkedIn API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function Ie(e){return e.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class se 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 Te(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 M{provider="mastodon";characterLimit=500;instanceOf(e){return Te(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:u}=a;if(!c?.length&&a.url){let p=await fetch(a.url);if(!p.ok)continue;c=new Uint8Array(await p.arrayBuffer()),u=u||p.headers.get("content-type")||"image/jpeg"}if(c?.length)s.push(await this.uploadMedia(e,c,u||"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||xe(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 se(`Mastodon API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function xe(e){return String(e||"").replace(/\/+$/,"").split("/").pop()||""}class k 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 F{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 k(`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"}),r=await this.graph(`/me?${t.toString()}`,{method:"GET"},e);if(!r.id)throw new k("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});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()},e.accessToken);if(!n.id)throw new k("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}).toString()},e.accessToken);if(!o.id)throw new k("Threads did not return a published post id.",400,JSON.stringify(o));let a=await this.graph(`/${o.id}?fields=permalink`,{method:"GET"},e.accessToken).catch(()=>{return});return{provider:this.provider,uri:o.id,url:a?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t,r){let i=new Headers(t.headers);if(r)i.set("authorization",`Bearer ${r}`);let s=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,{...t,headers:i}),n=await s.text(),o={};try{o=n?JSON.parse(n):{}}catch{o={}}if(!s.ok||o?.error){let a=o?.error?.message||n||s.statusText;throw new k(`Threads API failed (${s.status}): ${a}`,s.status,n)}return o}}import{Buffer as _e}from"buffer";import{createHash as Ce,randomBytes as Ue}from"crypto";import{fetcher as ne}from"@stacksjs/api";import{config as A}from"@stacksjs/config";class K extends g{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let e={clientId:A.services.twitter?.clientId??"",clientSecret:A.services.twitter?.clientSecret??"",redirectUrl:A.services.twitter?.redirectUrl??"",scopes:A.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(e.scopes),e}generateCodeVerifier(){return Ue(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(e){return Ce("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=_e.from(`${t}:${r}`).toString("base64"),n=await ne.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 ne.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 d("Twitter client ID not provided");if(!t)throw new d("Twitter client secret not provided");if(!r)throw new d("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class y 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 oe(e){let t="";for(let r of e)t+=String.fromCharCode(r);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class b{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=oe(crypto.getRandomValues(new Uint8Array(32))),r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)),i=oe(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 y("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 y("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 u=await fetch(i.url);if(u.ok)a=new Uint8Array(await u.arrayBuffer()),c=c||u.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 y("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 y(`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 y("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 y(`Twitter API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}import{config as Re}from"@stacksjs/config";var E=["clientId","clientSecret","redirectUrl"],I=Object.freeze({google:{name:"google",label:"Google",driver:J,required:E,postCallback:!1},github:{name:"github",label:"GitHub",driver:H,required:E,postCallback:!1},facebook:{name:"facebook",label:"Facebook",driver:q,required:E,postCallback:!1},twitter:{name:"twitter",label:"X",driver:K,required:E,postCallback:!1},apple:{name:"apple",label:"Apple",driver:N,required:["clientId","teamId","keyId","privateKey","redirectUrl"],postCallback:!0}});function ae(e){return Re?.services?.[e]}function $e(e){return typeof e==="string"&&e in I}function ce(e){if(!$e(e))return!1;let t=ae(e);if(!t)return!1;return I[e].required.every((r)=>Boolean(t[r]))}function qt(){return Object.keys(I).filter(ce).map((e)=>I[e])}function zt(e){if(!ce(e))return null;let t=I[e],r=ae(e)??{};return new t.driver({clientSecret:"",...r,clientId:String(r.clientId??""),redirectUrl:String(r.redirectUrl??"")})}class Ae{accessToken;refreshToken;expiresIn;approvedScopes;constructor(e,t=null,r=null,i=[]){this.accessToken=e;this.refreshToken=t;this.expiresIn=r;this.approvedScopes=i}}function jt(e){return typeof e?.deletePost==="function"}function Gt(e){return typeof e?.listAuthoredPosts==="function"}import{buildSessionHandoffUrl as Ee}from"@stacksjs/composables";function de(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 Kt(e,t={}){let r=t.redirectTo??"/";if(!de(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:Ee(r,e),"Cache-Control":"no-store","Referrer-Policy":"no-referrer"}})}function Qt(e,t={}){let r=t.redirectTo??"/login";if(!de(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 Oe={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"]},Be={bluesky:["identifier","password"],twitter:["clientId","clientSecret","accessToken"],linkedin:["accessToken","memberUrn"],mastodon:["accessToken","instanceUrl"],instagram:["accessToken","accountId"],threads:["accessToken","userId"]};function Le(e){return e.toUpperCase().replace(/[^A-Z0-9]/g,"")}function O(e,t,r){let i=r.replace(/[A-Z]/g,(s)=>`_${s}`).toUpperCase();return`SOCIALS_${Le(e)}_${t.toUpperCase()}_${i}`}class h extends Error{constructor(e){super(e);this.name="SocialIdentityError"}}async function B(){let{config:e,overridesReady:t}=await import("@stacksjs/config");return await t,e.socials??{}}function le(e,t){let r=Object.keys(e.identities??{});if(t){if(!r.includes(t))throw new h(`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 h(`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 h("config/socials.ts declares no identities, so there is no account to post as.");throw new h(`config/socials.ts declares ${r.length} identities (${r.join(", ")}) and no default, so a call has to name one.`)}function De(e,t,r,i=process.env){let n=(e.identities?.[r]??{}).credentials?.[t]??{},o={};for(let a of Oe[t]){let c=(n[a]??i[O(r,t,a)]??"").trim();if(c)o[a]=c}return o}function Q(e,t,r,i=process.env){let s=le(e,r),n=De(e,t,s,i),o=Be[t].filter((a)=>!n[a]);if(o.length){let a=o.map((c)=>O(s,t,c)).join(", ");throw new h(`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 ue(e){return le(await B(),e)}async function P(e,t,r){return Q(await B(),e,t,r)}function Zt(e,t){return P("bluesky",e,t)}function Xt(e,t){return P("twitter",e,t)}function Yt(e,t){return P("linkedin",e,t)}function er(e,t){return P("mastodon",e,t)}function tr(e,t){return P("instagram",e,t)}function rr(e,t){return P("threads",e,t)}import{randomBytes as Ve}from"crypto";function ar(e){return e==="twitter"}var Ne=["tweet.read","tweet.write","users.read","offline.access"],qe="127.0.0.1",pe="/callback";function cr(e){if(!Number.isInteger(e)||e<1||e>65535)throw new h(`${e} is not a port a redirect can listen on.`);return`http://${qe}:${e}${pe}`}function dr(){return Ve(16).toString("hex")}function lr(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!==pe)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 ur(e,t,r){let i=[];for(let[s,n]of Object.entries(r)){if(!n)continue;i.push(`${O(e,t,s)}=${n}`)}return i}async function pr(e){let t=new b,r=await t.exchangeCode(e);if(!r.accessToken)throw new h("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 hr(e){return new b().createAuthorization({clientId:e.clientId,redirectUrl:e.redirectUrl,scopes:[...Ne],state:e.state})}function gr(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 ze={bluesky:null,twitter:null,linkedin:"memberUrn",mastodon:"instanceUrl",instagram:"accountId",threads:"userId"};function He(e,t){if(e==="bluesky")return t.service?{service:t.service}:{};if(e==="linkedin")return t.apiVersion?{apiVersion:t.apiVersion}:{};return{}}function Je(e,t={}){switch(e){case"bluesky":return new _(t);case"linkedin":return new G(t);case"twitter":return new b;case"mastodon":return new M;case"instagram":return new j;case"threads":return new F;default:throw new h(`No publishing driver for '${String(e)}'.`)}}function je(e,t,r){let i=ze[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 Ge(e,t,r){if(!r)return;if(r.includes(t))return;throw new h(`Identity '${e}' is not set up for ${t}. config/socials.ts lists: ${r.length?r.join(", "):"none"}.`)}async function Me(e,t,r){let i=await B(),s=await ue(t),n=i.identities?.[s]??{};Ge(s,e,n.platforms);let o=Q(i,e,s,r),a=He(e,o);if(e==="bluesky"){let c=await new _(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:je(e,n.handle??"",o),options:a}}async function wr(e,t,r,i){let s=await Me(e,r,i);return Je(s.platform,s.options).publish(s.identity,t)}export{qe as AUTHORIZE_HOST,pe as AUTHORIZE_PATH,g as AbstractProvider,N as AppleProvider,te as BlueskyApiError,_ as BlueskyPublishingDriver,d as ConfigException,ze as DRIVER_DID_FIELD,q as FacebookProvider,H as GitHubProvider,J as GoogleProvider,v as InstagramApiError,j as InstagramPublishingDriver,fe as InvalidStateException,f as LinkedInApiError,G as LinkedInPublishingDriver,C as META_GRAPH_VERSION,se as MastodonApiError,M as MastodonPublishingDriver,Be as REQUIRED_SOCIAL_CREDENTIALS,Oe as SOCIAL_CREDENTIAL_FIELDS,I as SOCIAL_PROVIDERS,h as SocialIdentityError,Ne as TWITTER_PUBLISH_SCOPES,k as ThreadsApiError,F as ThreadsPublishingDriver,Ae as Token,y as TwitterApiError,K as TwitterProvider,b as TwitterPublishingDriver,Ge as assertPlatformDeclared,gr as callbackPage,cr as callbackUri,pr as completeTwitterAuthorization,qt as configuredSocialProviders,ve as detectFacetCandidates,He as driverOptions,ur as envLines,O as envName,Le as envSegment,Ie as escapeLinkedInText,ar as isAuthorizablePlatform,de as isSafeHandoffTarget,ce as isSocialProviderConfigured,$e as isSocialProviderName,dr as newState,Te as normalizeInstance,Pe as parseAtUri,Q as pickCredentials,le as pickIdentity,wr as publishAs,Je as publishingDriver,lr as readCallback,De as readCredentials,Zt as resolveBlueskyCredentials,ue as resolveIdentityName,tr as resolveInstagramCredentials,Yt as resolveLinkedInCredentials,er as resolveMastodonCredentials,P as resolveSocialCredentials,rr as resolveThreadsCredentials,Xt as resolveTwitterCredentials,Qt as socialHandoffFailureRedirect,Kt as socialHandoffRedirect,zt as socialProvider,Me as socialPublishTarget,B as socialsConfig,hr as startTwitterAuthorization,jt as supportsDeletion,Gt as supportsEnumeration,je as toDriverIdentity};
2
+ var ve=import.meta.require;import{Buffer as x}from"buffer";class g{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_codeVerifier=null;_state=null;user=null;constructor(e){this.clientId=e.clientId,this.clientSecret=e.clientSecret,this.redirectUrl=e.redirectUrl}async 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;return{...t,...await this.pkceFields(),...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}=ve("crypto");return r(x.from(e,"utf8"),x.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(){if(this._codeVerifier===null){let e=new Uint8Array(32);crypto.getRandomValues(e),this._codeVerifier=x.from(e).toString("base64url")}return this._codeVerifier}getPkceVerifier(){return this._codeVerifier}withCodeVerifier(e){if(!e)throw Error("A PKCE code verifier must be a non-empty string");return this._usesPKCE=!0,this._codeVerifier=e,this}async pkceFields(){if(!this.usesPKCE())return{};return{code_challenge:await this.getCodeChallenge(),code_challenge_method:this.getCodeChallengeMethod()}}pkceTokenFields(){if(!this.usesPKCE())return{};if(this._codeVerifier===null)throw Error("PKCE is enabled but no code verifier is known for this exchange. Pass the one from getPkceVerifier() back with withCodeVerifier().");return{code_verifier:this._codeVerifier}}async getCodeChallenge(){let t=new TextEncoder().encode(this.getCodeVerifier()),r=await crypto.subtle.digest("SHA-256",t);return x.from(new Uint8Array(r)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(e){return this.parameters=e,this}async buildAuthUrlFromBase(e,t){let r=new URLSearchParams(await this.getCodeFields(t));return`${e}?${r.toString()}`}}import{Buffer as ne}from"buffer";import{createPrivateKey as Ce,sign as Ae}from"crypto";import{config as y}from"@stacksjs/config";class Ie extends Error{constructor(e="Invalid state"){super(e);this.name="InvalidStateException"}}class l extends Error{constructor(e){super(e);this.name="ConfigException"}}import{Buffer as q}from"buffer";var se=new Map,Te=600000,xe=60000;async function re(e,t){let r=await fetch(e);if(!r.ok)throw Error(`id_token verification failed: could not fetch signing keys from ${e} (HTTP ${r.status})`);let i=await r.json(),s=Array.isArray(i.keys)?i.keys:[];return se.set(e,{keys:s,fetchedAt:t}),s}async function _e(e,t,r){let i=se.get(e),s=i!==void 0&&r-i.fetchedAt<Te,n=s?i.keys:await re(e,r),o=n.find((a)=>a.kid===t);if(!o&&s&&r-i.fetchedAt>=xe)n=await re(e,r),o=n.find((a)=>a.kid===t);if(!o)throw Error(`id_token verification failed: no signing key matches kid "${t}"`);if(o.kty!=="RSA")throw Error(`id_token verification failed: signing key "${t}" is ${o.kty}, expected RSA`);return o}function ie(e,t){try{return JSON.parse(q.from(e,"base64url").toString("utf8"))}catch{throw Error(`id_token verification failed: ${t} is not valid JSON`)}}async function _(e,t){let r=e.split(".");if(r.length!==3)throw Error("id_token verification failed: malformed token");let[i,s,n]=r,o=ie(i,"header");if(o.alg!=="RS256")throw Error(`id_token verification failed: unsupported algorithm ${JSON.stringify(o.alg)}, expected RS256`);if(typeof o.kid!=="string"||o.kid==="")throw Error("id_token verification failed: header has no kid");let a=t.now??Date.now(),c=await _e(t.jwksUrl,o.kid,a),u=await crypto.subtle.importKey("jwk",{kty:"RSA",n:c.n,e:c.e,alg:"RS256",ext:!0},{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["verify"]);if(!await crypto.subtle.verify("RSASSA-PKCS1-v1_5",u,q.from(n,"base64url"),q.from(`${i}.${s}`)))throw Error("id_token verification failed: signature does not match the issuer's key");let d=ie(s,"payload"),F=(t.clockSkewSeconds??60)*1000,te=Array.isArray(t.issuer)?t.issuer:[t.issuer];if(!te.includes(d.iss))throw Error(`id_token verification failed: issuer is ${JSON.stringify(d.iss)}, expected ${te.map((N)=>JSON.stringify(N)).join(" or ")}`);let Pe=Array.isArray(t.audience)?t.audience:[t.audience];if(!(Array.isArray(d.aud)?d.aud:[d.aud]).some((N)=>Pe.includes(N)))throw Error("id_token verification failed: token is addressed to a different client");if(typeof d.exp!=="number"||d.exp*1000+F<=a)throw Error("id_token verification failed: token has expired");if(typeof d.iat==="number"&&d.iat*1000-F>a)throw Error("id_token verification failed: token was issued in the future");if(typeof d.nbf==="number"&&d.nbf*1000-F>a)throw Error("id_token verification failed: token is not valid yet");if(t.nonce!==void 0&&d.nonce!==t.nonce)throw Error("id_token verification failed: nonce does not match");if(typeof d.sub!=="string"||d.sub==="")throw Error("id_token verification failed: token has no subject");return d}class H extends g{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";native=!1;constructor(e){super(e);this.teamId=e.teamId??"",this.keyId=e.keyId??"",this.privateKey=e.privateKey??"",this.native=e.native===!0}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.native?"":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(){if(this.native)throw new l("A native Apple provider redeems the app's codes; the web flow needs the Service ID");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,...this.native?{}:{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,t={}){let{clientId:r}=this.getConfig(),i;try{i=await _(e,{jwksUrl:`${this.baseUrl}/auth/keys`,issuer:this.baseUrl,audience:r,nonce:t.nonce})}catch(o){throw Error(`Apple OAuth error: ${o instanceof Error?o.message:String(o)}`)}let s=typeof i.email==="string"?i.email:null,n=null;if(i.email_verified===!0||i.email_verified==="true")n=!0;else if(i.email_verified===!1||i.email_verified==="false")n=!1;return{id:String(i.sub),nickname:null,name:"",email:s,emailVerified:n,avatar:null,token:e,raw:i}}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=Ce(i)}catch(p){throw new l(`Apple private key could not be parsed: ${p instanceof Error?p.message:String(p)}`)}let u=Ae("sha256",ne.from(a),{key:c,dsaEncoding:"ieee-p1363"});return`${a}.${u.toString("base64url")}`}base64urlJson(e){return ne.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(this.native?"Apple client ID (the app's bundle ID) not provided":"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&&!this.native)throw new l("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function Ue(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 oe 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 Re=new TextEncoder;function m(e){return Re.encode(e).length}function $e(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 u=(o.index??0)+a.length,p=m(e.slice(0,u)),d=p+m(c);if(i(p,d))continue;t.push({byteStart:p,byteEnd:d,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 u=(o.index??0)+a.length,p=m(e.slice(0,u)),d=p+m(c);if(i(p,d))continue;t.push({byteStart:p,byteEnd:d,type:"mention",value:c.slice(1).replace(/\.+$/,"")})}return t.sort((o,a)=>o.byteStart-a.byteStart)}class C{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 u=await this.uploadBlob(e,c.bytes,c.mimeType||"image/jpeg");a.push({image:u,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 $e(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}=Ue(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 oe(`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 ae}from"@stacksjs/api";import{config as P}from"@stacksjs/config";var A="v24.0";class V extends g{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";get graphVersion(){return P.services.facebook?.graphVersion??A}getConfig(){let e={clientId:this.clientId||(P.services.facebook?.clientId??""),clientSecret:this.clientSecret||(P.services.facebook?.clientSecret??""),redirectUrl:this.redirectUrl||(P.services.facebook?.redirectUrl??""),scopes:P.services.facebook?.scopes??["email","public_profile"]};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}/${this.graphVersion}/dialog/oauth?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(","),response_type:"code",...await this.pkceFields(),...this.parameters,state:e}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await ae.get(`${this.apiUrl}/${this.graphVersion}/oauth/access_token?${new URLSearchParams({client_id:t,client_secret:r,redirect_uri:i,code:e,...this.pkceTokenFields()}).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 ae.withHeaders({Authorization:`Bearer ${e}`}).get(`${this.apiUrl}/${this.graphVersion}/me?${new URLSearchParams({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 z}from"@stacksjs/api";import{config as U}from"@stacksjs/config";class M extends g{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let e={clientId:this.clientId||(U.services.github?.clientId??""),clientSecret:this.clientSecret||(U.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(U.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:U.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",...await this.pkceFields(),...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await z.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:t,client_secret:r,code:e,redirect_uri:i,...this.pkceTokenFields()});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([z.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${e}`}).get(`${this.apiUrl}/user`),z.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 ce}from"@stacksjs/api";import{config as R}from"@stacksjs/config";class J extends g{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";tokenUrl="https://oauth2.googleapis.com/token";tokenInfoUrl="https://oauth2.googleapis.com/tokeninfo";certsUrl="https://www.googleapis.com/oauth2/v3/certs";issuers=["https://accounts.google.com","accounts.google.com"];getConfig(){let e={clientId:this.clientId||(R.services.google?.clientId??""),clientSecret:this.clientSecret||(R.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(R.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:R.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",...await this.pkceFields(),...this.parameters}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=await ce.post(this.tokenUrl,{client_id:t,client_secret:r,code:e,redirect_uri:i,grant_type:"authorization_code",...this.pkceTokenFields()});if(s.data.error)throw Error(`Google OAuth error: ${s.data.error_description}`);return s.data.access_token}async getUserByToken(e){await this.assertTokenWasIssuedToUs(e);let t=await ce.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}}async getUserByIdToken(e,t={}){let{clientId:r}=this.getConfig(),i;try{i=await _(e,{jwksUrl:this.certsUrl,issuer:this.issuers,audience:r,nonce:t.nonce})}catch(n){throw Error(`Google OAuth error: ${n instanceof Error?n.message:String(n)}`)}let s=typeof i.email==="string"?i.email:"";return{id:String(i.sub),nickname:typeof i.given_name==="string"?i.given_name:null,name:typeof i.name==="string"?i.name:"",email:s,emailVerified:typeof i.email_verified==="boolean"?i.email_verified:i.email_verified==="true"?!0:i.email_verified==="false"?!1:null,avatar:typeof i.picture==="string"?i.picture:null,token:e,raw:i}}async assertTokenWasIssuedToUs(e){let{clientId:t}=this.getConfig(),r=await fetch(`${this.tokenInfoUrl}?access_token=${encodeURIComponent(e)}`);if(!r.ok)throw Error(`Google OAuth error: access token was not accepted by tokeninfo (HTTP ${r.status})`);let i=await r.json();if(i.error)throw Error(`Google OAuth error: ${i.error_description??i.error}`);let s=i.aud??i.audience;if(!s)throw Error("Google OAuth error: tokeninfo did not report an audience for the access token");if(s!==t)throw Error("Google OAuth error: access token was issued to a different client")}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 j{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(e={}){this.graphVersion=e.graphVersion||A,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=await this.graph("/oauth/access_token",{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,redirect_uri:e.redirectUrl,code:e.code}).toString()});if(!t.access_token)throw new v("Facebook did not return an access token.",400,JSON.stringify(t));return{accessToken:t.access_token,expiresIn:t.expires_in}}async resolveAccount(e){let t=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}"}),r=await this.graph(`/me/accounts?${t.toString()}`,{method:"GET"},e),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}).toString()},e.accessToken);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}).toString()},e.accessToken),o=await this.graph(`/${n.id}?fields=permalink`,{method:"GET"},e.accessToken).catch(()=>{return});return{provider:this.provider,uri:n.id,url:o?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t,r){let i=new Headers(t.headers);if(r)i.set("authorization",`Bearer ${r}`);let s=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,{...t,headers:i}),n=await s.text(),o={};try{o=n?JSON.parse(n):{}}catch{o={}}if(!s.ok||o?.error){let a=o?.error?.message||n||s.statusText;throw new v(`Instagram API failed (${s.status}): ${a}`,s.status,n)}return o}}class f 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 G{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 f("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 f("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:Oe(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 f(`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 f&&(c.status===401||c.status===403))throw new f("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 f(`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 f(`LinkedIn API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function Oe(e){return e.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class le 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 Ee(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 K{provider="mastodon";characterLimit=500;instanceOf(e){return Ee(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:u}=a;if(!c?.length&&a.url){let p=await fetch(a.url);if(!p.ok)continue;c=new Uint8Array(await p.arrayBuffer()),u=u||p.headers.get("content-type")||"image/jpeg"}if(c?.length)s.push(await this.uploadMedia(e,c,u||"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||Be(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 le(`Mastodon API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}function Be(e){return String(e||"").replace(/\/+$/,"").split("/").pop()||""}class k 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 Q{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 k(`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"}),r=await this.graph(`/me?${t.toString()}`,{method:"GET"},e);if(!r.id)throw new k("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});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()},e.accessToken);if(!n.id)throw new k("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}).toString()},e.accessToken);if(!o.id)throw new k("Threads did not return a published post id.",400,JSON.stringify(o));let a=await this.graph(`/${o.id}?fields=permalink`,{method:"GET"},e.accessToken).catch(()=>{return});return{provider:this.provider,uri:o.id,url:a?.permalink}}async timeline(e,t={}){return{items:[]}}async graph(e,t,r){let i=new Headers(t.headers);if(r)i.set("authorization",`Bearer ${r}`);let s=await fetch(`${this.graphBase}/${this.graphVersion}${e}`,{...t,headers:i}),n=await s.text(),o={};try{o=n?JSON.parse(n):{}}catch{o={}}if(!s.ok||o?.error){let a=o?.error?.message||n||s.statusText;throw new k(`Threads API failed (${s.status}): ${a}`,s.status,n)}return o}}import{Buffer as De}from"buffer";import{fetcher as de}from"@stacksjs/api";import{config as O}from"@stacksjs/config";class W extends g{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";_usesPKCE=!0;getConfig(){let e={clientId:this.clientId||(O.services.twitter?.clientId??""),clientSecret:this.clientSecret||(O.services.twitter?.clientSecret??""),redirectUrl:this.redirectUrl||(O.services.twitter?.redirectUrl??""),scopes:O.services.twitter?.scopes??["users.read","tweet.read"]};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}/i/oauth2/authorize?${new URLSearchParams({client_id:t,redirect_uri:r,scope:i.join(" "),response_type:"code",...this.parameters,...await this.pkceFields(),state:e}).toString()}`}async getAccessToken(e){let{clientId:t,clientSecret:r,redirectUrl:i}=this.getConfig();this.validateConfig();let s=this.pkceTokenFields(),n=De.from(`${t}:${r}`).toString("base64"),o=await de.withHeaders({Authorization:`Basic ${n}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:e,grant_type:"authorization_code",redirect_uri:i,...s});if(o.data.error)throw Error(`Twitter OAuth error: ${o.data.error_description}`);return o.data.access_token}async getUserByToken(e){let t=await de.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 w 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 ue(e){let t="";for(let r of e)t+=String.fromCharCode(r);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class b{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=ue(crypto.getRandomValues(new Uint8Array(32))),r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)),i=ue(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 w("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 w("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 u=await fetch(i.url);if(u.ok)a=new Uint8Array(await u.arrayBuffer()),c=c||u.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 w("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 w(`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 w("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 w(`Twitter API failed (${r.status}): ${i||r.statusText}`,r.status,i);return i?JSON.parse(i):{}}}import{config as Le}from"@stacksjs/config";var E=["clientId","clientSecret","redirectUrl"],I=Object.freeze({google:{name:"google",label:"Google",driver:J,required:E,postCallback:!1,pkce:!1,nonce:!1},github:{name:"github",label:"GitHub",driver:M,required:E,postCallback:!1,pkce:!1,nonce:!1},facebook:{name:"facebook",label:"Facebook",driver:V,required:E,postCallback:!1,pkce:!1,nonce:!1},twitter:{name:"twitter",label:"X",driver:W,required:E,postCallback:!1,pkce:!0,nonce:!1},apple:{name:"apple",label:"Apple",driver:H,required:["clientId","teamId","keyId","privateKey","redirectUrl"],postCallback:!0,pkce:!1,nonce:!0}});function pe(e){return Le?.services?.[e]}function Fe(e){return typeof e==="string"&&e in I}function he(e){if(!Fe(e))return!1;let t=pe(e);if(!t)return!1;return I[e].required.every((r)=>Boolean(t[r]))}function ir(){return Object.keys(I).filter(he).map((e)=>I[e])}function sr(e){if(!he(e))return null;let t=I[e],r=pe(e)??{};return new t.driver({clientSecret:"",...r,clientId:String(r.clientId??""),redirectUrl:String(r.redirectUrl??"")})}class Ne{accessToken;refreshToken;expiresIn;approvedScopes;constructor(e,t=null,r=null,i=[]){this.accessToken=e;this.refreshToken=t;this.expiresIn=r;this.approvedScopes=i}}function ar(e){return typeof e?.deletePost==="function"}function cr(e){return typeof e?.listAuthoredPosts==="function"}import{buildSessionHandoffCodeUrl as qe,buildSessionHandoffUrl as He}from"@stacksjs/composables";function X(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 ur(e,t={}){let r=t.redirectTo??"/";if(!X(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:He(r,e),"Cache-Control":"no-store","Referrer-Policy":"no-referrer"}})}function pr(e,t={}){let r=t.redirectTo??"/";if(!X(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:qe(r,e),"Cache-Control":"no-store","Referrer-Policy":"no-referrer"}})}function hr(e,t={}){let r=t.redirectTo??"/login";if(!X(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","Referrer-Policy":"no-referrer"}})}import{Buffer as ge}from"buffer";import{createHash as Ve,randomBytes as ze,timingSafeEqual as Me}from"crypto";function T(e){return Ve("sha256").update(e).digest("hex")}var Je=/^[a-f0-9]{64}$/,je=/^[a-f0-9]{64}$/,Ge=/^[\w.:-]{1,64}$/,Ke=3600000,fe=null;function wr(e){fe=e}function me(){return fe??Xe()}function Z(e=32){return ze(e).toString("hex")}function Qe(e,t){if(e.length!==t.length)return!1;return Me(ge.from(e,"utf8"),ge.from(t,"utf8"))}function Y(e){return`stacks_oauth_${T(e).slice(0,24)}`}async function We(){try{let{config:e}=await import("@stacksjs/config");return String(e.app?.url??"").startsWith("https:")}catch{return!1}}function we(e,t,r,i){return[`${e}=${t}`,`Path=${i.path}`,"HttpOnly",`SameSite=${i.sameSite}`,`Max-Age=${Math.max(0,Math.floor(r))}`,...i.secure?["Secure"]:[]].join("; ")}async function ye(e={}){let t=e.sameSite??"Lax",r=t==="None"?!0:e.secure??await We();return{path:e.path??"/",sameSite:t,secure:r}}async function yr(e){if(!Ge.test(e.purpose??""))throw TypeError("[socials] issueOAuthFlow needs a purpose of 1-64 word characters, dots, colons or dashes.");if(!Number.isFinite(e.ttlMs)||e.ttlMs<=0)throw TypeError("[socials] issueOAuthFlow needs a positive ttlMs.");if(e.challenge!==void 0&&!je.test(e.challenge))throw TypeError("[socials] issueOAuthFlow challenge must be the lowercase hex SHA-256 of the verifier.");if(e.userId!=null&&(!Number.isSafeInteger(e.userId)||e.userId<=0))throw TypeError("[socials] issueOAuthFlow userId must be a positive integer.");let t=Math.min(e.ttlMs,Ke),r=Z(),s=(e.challenge?!1:e.bindBrowser!==!1)?Z():null,n=e.nonce===!0?Z():typeof e.nonce==="string"&&e.nonce?e.nonce:void 0,o=new Date,a=new Date(o.getTime()+t),c=me();try{await c.prune(o)}catch{}await c.insert({stateDigest:T(r),purpose:e.purpose,browserDigest:e.challenge??(s?T(s):null),userId:e.userId??null,nonce:n??null,data:e.data??null,expiresAt:a});let u=s?we(Y(r),s,t/1000,await ye(e.cookie)):null;return{state:r,cookie:u,secret:s,...n!==void 0&&{nonce:n},expiresAt:a}}async function kr(e,t){if(typeof e!=="string"||!Je.test(e))return null;let r=me(),i=T(e),s=await r.find(i);if(!s)return null;if(s.expiresAt.getTime()<=Date.now())return await r.take(i).catch(()=>!1),null;if(s.purpose!==t.purpose)return null;if(s.browserDigest!==null){let n=t.browserSecret;if(typeof n!=="string"||n.length===0||n.length>512)return null;if(!Qe(T(n),s.browserDigest))return null}if(!await r.take(i))return null;return{purpose:s.purpose,userId:s.userId,nonce:s.nonce,data:s.data,expiresAt:s.expiresAt}}function br(e,t){if(!e||!t)return"";let r=Y(t);for(let i of e.split(";")){let s=i.trim();if(s.startsWith(`${r}=`))return s.slice(r.length+1)}return""}async function Sr(e,t={}){return we(Y(e),"",0,await ye(t))}function Pr(){let e=new Map;return{records:e,async insert(t){e.set(t.stateDigest,{...t})},async find(t){let r=e.get(t);return r?{...r}:void 0},async take(t){return e.delete(t)},async prune(t){for(let[r,i]of e)if(i.expiresAt.getTime()<=t.getTime())e.delete(r)}}}var B="oauth_flows";function Xe(){let e=()=>import("@stacksjs/database/runtime");return{async insert(t){let{db:r,sqlDateTime:i}=await e();await r.insertInto(B).values({state_digest:t.stateDigest,purpose:t.purpose,browser_digest:t.browserDigest,user_id:t.userId,nonce:t.nonce,data:t.data?JSON.stringify(t.data):null,expires_at:i(t.expiresAt),created_at:i(new Date)}).execute()},async find(t){let{db:r,parseSqlDateTime:i}=await e(),s=await r.primary.selectFrom(B).where("state_digest","=",t).selectAll().executeTakeFirst();if(!s)return;let n=null;if(typeof s.data==="string"&&s.data)try{let o=JSON.parse(s.data);n=o&&typeof o==="object"?o:null}catch{n=null}return{stateDigest:String(s.state_digest),purpose:String(s.purpose),browserDigest:typeof s.browser_digest==="string"&&s.browser_digest?s.browser_digest:null,userId:s.user_id==null?null:Number(s.user_id),nonce:typeof s.nonce==="string"&&s.nonce?s.nonce:null,data:n,expiresAt:i(s.expires_at)??new Date(0)}},async take(t){let{db:r,mutationCount:i}=await e(),s=await r.deleteFrom(B).where("state_digest","=",t).execute();return i(s)===1},async prune(t){let{db:r,sqlDateTime:i}=await e();await r.deleteFrom(B).where("expires_at","<",i(t)).execute()}}}var Ze={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"]},Ye={bluesky:["identifier","password"],twitter:["clientId","clientSecret","accessToken"],linkedin:["accessToken","memberUrn"],mastodon:["accessToken","instanceUrl"],instagram:["accessToken","accountId"],threads:["accessToken","userId"]};function et(e){return e.toUpperCase().replace(/[^A-Z0-9]/g,"")}function D(e,t,r){let i=r.replace(/[A-Z]/g,(s)=>`_${s}`).toUpperCase();return`SOCIALS_${et(e)}_${t.toUpperCase()}_${i}`}class h extends Error{constructor(e){super(e);this.name="SocialIdentityError"}}async function L(){let{config:e,overridesReady:t}=await import("@stacksjs/config");return await t,e.socials??{}}function ke(e,t){let r=Object.keys(e.identities??{});if(t){if(!r.includes(t))throw new h(`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 h(`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 h("config/socials.ts declares no identities, so there is no account to post as.");throw new h(`config/socials.ts declares ${r.length} identities (${r.join(", ")}) and no default, so a call has to name one.`)}function tt(e,t,r,i=process.env){let n=(e.identities?.[r]??{}).credentials?.[t]??{},o={};for(let a of Ze[t]){let c=(n[a]??i[D(r,t,a)]??"").trim();if(c)o[a]=c}return o}function ee(e,t,r,i=process.env){let s=ke(e,r),n=tt(e,t,s,i),o=Ye[t].filter((a)=>!n[a]);if(o.length){let a=o.map((c)=>D(s,t,c)).join(", ");throw new h(`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 be(e){return ke(await L(),e)}async function S(e,t,r){return ee(await L(),e,t,r)}function Ir(e,t){return S("bluesky",e,t)}function Tr(e,t){return S("twitter",e,t)}function xr(e,t){return S("linkedin",e,t)}function _r(e,t){return S("mastodon",e,t)}function Cr(e,t){return S("instagram",e,t)}function Ar(e,t){return S("threads",e,t)}import{randomBytes as rt}from"crypto";function Er(e){return e==="twitter"}var it=["tweet.read","tweet.write","users.read","offline.access"],st="127.0.0.1",Se="/callback";function Br(e){if(!Number.isInteger(e)||e<1||e>65535)throw new h(`${e} is not a port a redirect can listen on.`);return`http://${st}:${e}${Se}`}function Dr(){return rt(16).toString("hex")}function Lr(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!==Se)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 Fr(e,t,r){let i=[];for(let[s,n]of Object.entries(r)){if(!n)continue;i.push(`${D(e,t,s)}=${n}`)}return i}async function Nr(e){let t=new b,r=await t.exchangeCode(e);if(!r.accessToken)throw new h("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 qr(e){return new b().createAuthorization({clientId:e.clientId,redirectUrl:e.redirectUrl,scopes:[...it],state:e.state})}function Hr(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 nt={bluesky:null,twitter:null,linkedin:"memberUrn",mastodon:"instanceUrl",instagram:"accountId",threads:"userId"};function ot(e,t){if(e==="bluesky")return t.service?{service:t.service}:{};if(e==="linkedin")return t.apiVersion?{apiVersion:t.apiVersion}:{};return{}}function at(e,t={}){switch(e){case"bluesky":return new C(t);case"linkedin":return new G(t);case"twitter":return new b;case"mastodon":return new K;case"instagram":return new j;case"threads":return new Q;default:throw new h(`No publishing driver for '${String(e)}'.`)}}function ct(e,t,r){let i=nt[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 lt(e,t,r){if(!r)return;if(r.includes(t))return;throw new h(`Identity '${e}' is not set up for ${t}. config/socials.ts lists: ${r.length?r.join(", "):"none"}.`)}async function dt(e,t,r){let i=await L(),s=await be(t),n=i.identities?.[s]??{};lt(s,e,n.platforms);let o=ee(i,e,s,r),a=ot(e,o);if(e==="bluesky"){let c=await new C(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:ct(e,n.handle??"",o),options:a}}async function Jr(e,t,r,i){let s=await dt(e,r,i);return at(s.platform,s.options).publish(s.identity,t)}export{st as AUTHORIZE_HOST,Se as AUTHORIZE_PATH,g as AbstractProvider,H as AppleProvider,oe as BlueskyApiError,C as BlueskyPublishingDriver,l as ConfigException,nt as DRIVER_DID_FIELD,V as FacebookProvider,M as GitHubProvider,J as GoogleProvider,v as InstagramApiError,j as InstagramPublishingDriver,Ie as InvalidStateException,f as LinkedInApiError,G as LinkedInPublishingDriver,A as META_GRAPH_VERSION,le as MastodonApiError,K as MastodonPublishingDriver,Ye as REQUIRED_SOCIAL_CREDENTIALS,Ze as SOCIAL_CREDENTIAL_FIELDS,I as SOCIAL_PROVIDERS,h as SocialIdentityError,it as TWITTER_PUBLISH_SCOPES,k as ThreadsApiError,Q as ThreadsPublishingDriver,Ne as Token,w as TwitterApiError,W as TwitterProvider,b as TwitterPublishingDriver,lt as assertPlatformDeclared,Hr as callbackPage,Br as callbackUri,Sr as clearOAuthFlowCookie,Nr as completeTwitterAuthorization,ir as configuredSocialProviders,kr as consumeOAuthFlow,Xe as databaseOAuthFlowStore,$e as detectFacetCandidates,ot as driverOptions,Fr as envLines,D as envName,et as envSegment,Oe as escapeLinkedInText,Er as isAuthorizablePlatform,X as isSafeHandoffTarget,he as isSocialProviderConfigured,Fe as isSocialProviderName,yr as issueOAuthFlow,Pr as memoryOAuthFlowStore,Dr as newState,Ee as normalizeInstance,Y as oauthFlowCookieName,T as oauthFlowDigest,Ue as parseAtUri,ee as pickCredentials,ke as pickIdentity,Jr as publishAs,at as publishingDriver,Lr as readCallback,tt as readCredentials,br as readOAuthFlowCookie,Ir as resolveBlueskyCredentials,be as resolveIdentityName,Cr as resolveInstagramCredentials,xr as resolveLinkedInCredentials,_r as resolveMastodonCredentials,S as resolveSocialCredentials,Ar as resolveThreadsCredentials,Tr as resolveTwitterCredentials,pr as socialHandoffCodeRedirect,hr as socialHandoffFailureRedirect,ur as socialHandoffRedirect,sr as socialProvider,dt as socialPublishTarget,L as socialsConfig,qr as startTwitterAuthorization,ar as supportsDeletion,cr as supportsEnumeration,ct as toDriverIdentity,wr as useOAuthFlowStore};
@@ -33,6 +33,8 @@ export declare interface SocialProviderDescriptor {
33
33
  driver: new (providerConfig: any) => ProviderInterface
34
34
  required: readonly string[]
35
35
  postCallback: boolean
36
+ pkce: boolean
37
+ nonce: boolean
36
38
  }
37
39
  /**
38
40
  * Which social providers are usable in this application right now.
package/dist/types.d.ts CHANGED
@@ -45,7 +45,12 @@ export declare interface GitHubTokenResponse {
45
45
  export declare interface ProviderInterface {
46
46
  getAuthUrl: () => Promise<string>
47
47
  getAccessToken: (code: string) => Promise<string>
48
- getUserByToken: (token: string) => Promise<SocialUser>
48
+ getUserByToken: (token: string, options?: { nonce?: string }) => Promise<SocialUser>
49
+ withState: (state: string) => this
50
+ with: (parameters: Record<string, any>) => this
51
+ setRedirectUrl: (url: string) => this
52
+ getPkceVerifier: () => string | null
53
+ withCodeVerifier: (verifier: string) => this
49
54
  }
50
55
  export declare interface TwitterTokenResponse {
51
56
  access_token: string
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.226",
5
+ "version": "0.75.230",
6
6
  "description": "A simple and elegant social authentication package for Stacks.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -57,13 +57,14 @@
57
57
  "prepublishOnly": "bun run build"
58
58
  },
59
59
  "dependencies": {
60
- "@stacksjs/api": "0.75.226",
61
- "@stacksjs/composables": "0.75.226",
62
- "@stacksjs/config": "0.75.226"
60
+ "@stacksjs/api": "0.75.230",
61
+ "@stacksjs/composables": "0.75.230",
62
+ "@stacksjs/config": "0.75.230",
63
+ "@stacksjs/database": "0.75.230"
63
64
  },
64
65
  "devDependencies": {
65
- "@stacksjs/error-handling": "0.75.226",
66
- "@stacksjs/router": "0.75.226",
66
+ "@stacksjs/error-handling": "0.75.230",
67
+ "@stacksjs/router": "0.75.230",
67
68
  "better-dx": "^0.2.24"
68
69
  }
69
70
  }