@stacksjs/socials 0.70.293 → 0.70.296
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/dist/handoff.d.ts +62 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -32,6 +32,65 @@ const authUrl = await github.redirect()
|
|
|
32
32
|
const user = await github.user()
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
### Validate the OAuth `state`
|
|
36
|
+
|
|
37
|
+
Do not hand-roll this. The driver already ships a constant-time check, and an
|
|
38
|
+
HMAC written next to it is a CSRF hole waiting to be got subtly wrong:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { Socials } from '@stacksjs/socials'
|
|
42
|
+
|
|
43
|
+
// On the redirect: mint a state, stash it, embed it.
|
|
44
|
+
const state = crypto.randomUUID()
|
|
45
|
+
const github = Socials.driver('github').withState(state)
|
|
46
|
+
// persist `state` in the session/cookie, then redirect to:
|
|
47
|
+
const authUrl = await github.redirect()
|
|
48
|
+
|
|
49
|
+
// On the callback: compare what came back against what you stashed.
|
|
50
|
+
if (!github.validateState(stashedState, url.searchParams.get('state')))
|
|
51
|
+
throw new HttpError(400, 'Invalid OAuth state')
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`validateState()` is timing-safe, so response time cannot leak a prefix match.
|
|
55
|
+
Without `withState()` the driver mints a state you cannot recover, and there is
|
|
56
|
+
nothing to compare against on the way back.
|
|
57
|
+
|
|
58
|
+
### Sign the browser in
|
|
59
|
+
|
|
60
|
+
The provider hands you a user; `@stacksjs/auth` turns that into a session. Set
|
|
61
|
+
the auth cookie and redirect — no HTML, no inline script, and nothing secret in
|
|
62
|
+
the URL:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { Auth, authCookie } from '@stacksjs/auth'
|
|
66
|
+
|
|
67
|
+
const session = await Auth.loginUsingId(localUser.id)
|
|
68
|
+
|
|
69
|
+
return new Response(null, {
|
|
70
|
+
status: 303,
|
|
71
|
+
headers: {
|
|
72
|
+
'Location': '/account',
|
|
73
|
+
'Set-Cookie': authCookie(String(session.token)),
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`authCookie()` writes an `HttpOnly`, `SameSite=Lax`, `Secure`-outside-local
|
|
79
|
+
cookie under the same name every framework reader looks for, so the next
|
|
80
|
+
request is authenticated by the Auth middleware with nothing further to do.
|
|
81
|
+
|
|
82
|
+
On the page you land on, hydrate the client session:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const { completeSocialLogin } = useAuth()
|
|
86
|
+
const user = await completeSocialLogin()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Do NOT write `token` / `refresh_token` / `user` into `localStorage` yourself.
|
|
90
|
+
Those keys are `useStorage`-encoded (JSON-stringified on write), and
|
|
91
|
+
re-deriving that encoding by hand is how you end up storing the string
|
|
92
|
+
`[object Object]` for a user (stacksjs/stacks#2236).
|
|
93
|
+
|
|
35
94
|
Learn more in the docs.
|
|
36
95
|
|
|
37
96
|
## 🧪 Testing
|
|
@@ -44,6 +103,67 @@ bun test
|
|
|
44
103
|
|
|
45
104
|
Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently.
|
|
46
105
|
|
|
106
|
+
## Completing a sign-in
|
|
107
|
+
|
|
108
|
+
The driver stops at the provider user. These two steps take it the rest of the
|
|
109
|
+
way, and both already exist — do not hand-roll either.
|
|
110
|
+
|
|
111
|
+
### CSRF state
|
|
112
|
+
|
|
113
|
+
`withState()` / `getState()` / `validateState()` ship on the abstract driver
|
|
114
|
+
(`src/abstract.ts`). `validateState()` compares in constant time. There is no
|
|
115
|
+
reason to write your own HMAC.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const url = driver.withState(await driver.redirectUrl())
|
|
119
|
+
// …provider redirects back…
|
|
120
|
+
if (!driver.validateState(request.get('state')))
|
|
121
|
+
return socialHandoffFailureRedirect('invalid_state', { redirectTo: '/login' })
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Handing the session to the browser
|
|
125
|
+
|
|
126
|
+
Do **not** return an HTML page whose inline script writes `localStorage`. The
|
|
127
|
+
session format is the framework's, and re-deriving it is how apps ended up
|
|
128
|
+
double-stringifying tokens and storing the literal `[object Object]` as the
|
|
129
|
+
user (stacksjs/stacks#2236). An inline script also has to escape provider text
|
|
130
|
+
— a display name containing a closing script tag terminates the block.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { socialHandoffRedirect } from '@stacksjs/socials'
|
|
134
|
+
|
|
135
|
+
const result = await Auth.loginUsingId(user.id)
|
|
136
|
+
|
|
137
|
+
return socialHandoffRedirect({
|
|
138
|
+
token: result.token,
|
|
139
|
+
refreshToken: result.refreshToken,
|
|
140
|
+
user: { id: user.id, email: user.email, name: user.name },
|
|
141
|
+
expiresIn: result.expiresIn,
|
|
142
|
+
}, { redirectTo: '/account' })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
That is a plain 302 — no HTML, no script, nothing to escape. The pack travels
|
|
146
|
+
in the URL fragment, which is never sent to a server, so it stays out of access
|
|
147
|
+
logs, `Referer` and any proxy in between.
|
|
148
|
+
|
|
149
|
+
On the landing page:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const { completeSocialLogin } = useAuth()
|
|
153
|
+
|
|
154
|
+
// Safe on every load: resolves null when there was no handoff to apply.
|
|
155
|
+
const user = await completeSocialLogin()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
That writes through the same storage refs an ordinary `login()` uses, so the
|
|
159
|
+
encoding cannot be got wrong, and strips the fragment from the URL and from
|
|
160
|
+
history. It then confirms the session against `/api/me`, which is also what
|
|
161
|
+
picks up the cookie handoff below — there the browser holds no tokens at all
|
|
162
|
+
and there is nothing in the fragment to apply.
|
|
163
|
+
|
|
164
|
+
An absolute `redirectTo` is refused unless its host is in `allowedHosts` — the
|
|
165
|
+
redirect carries a token pack, and the target often comes from user input.
|
|
166
|
+
|
|
47
167
|
## 🚜 Contributing
|
|
48
168
|
|
|
49
169
|
Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SessionHandoffPack } from '@stacksjs/composables';
|
|
2
|
+
/**
|
|
3
|
+
* Whether `redirectTo` is safe to send a token pack to.
|
|
4
|
+
*
|
|
5
|
+
* Relative paths are fine. Absolute URLs must match an allowed host. Anything
|
|
6
|
+
* that fails to parse, or uses a scheme other than http(s), is refused —
|
|
7
|
+
* `javascript:` and `data:` targets are how a redirect becomes script
|
|
8
|
+
* execution.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSafeHandoffTarget(redirectTo: string, allowedHosts?: string[]): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The redirect that completes a social sign-in.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const result = await Auth.loginUsingId(user.id)
|
|
16
|
+
* return socialHandoffRedirect({
|
|
17
|
+
* token: result.token,
|
|
18
|
+
* refreshToken: result.refreshToken,
|
|
19
|
+
* user: { id: user.id, email: user.email, name: user.name },
|
|
20
|
+
* expiresIn: result.expiresIn,
|
|
21
|
+
* }, { redirectTo: '/account' })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The client completes it with `useAuth().completeSocialLogin()`, which reads
|
|
25
|
+
* the fragment, writes through the storage refs — so the encoding is applied
|
|
26
|
+
* by the same code path as an ordinary login — and strips the fragment.
|
|
27
|
+
*
|
|
28
|
+
* `Cache-Control: no-store` because the Location header carries the pack; a
|
|
29
|
+
* cached 302 would replay someone else's session to the next visitor.
|
|
30
|
+
*/
|
|
31
|
+
export declare function socialHandoffRedirect(pack: SessionHandoffPack, options?: SocialHandoffOptions): Response;
|
|
32
|
+
/**
|
|
33
|
+
* The redirect for a sign-in that did not succeed.
|
|
34
|
+
*
|
|
35
|
+
* Ships alongside the success path because the failure path was the other
|
|
36
|
+
* hand-built inline script in the app that reported this — the same escaping
|
|
37
|
+
* hazard, for a message that often contains provider text.
|
|
38
|
+
*
|
|
39
|
+
* The reason travels as an ordinary query parameter: it carries no credential,
|
|
40
|
+
* and a query is what the destination page can read server-side to render an
|
|
41
|
+
* error.
|
|
42
|
+
*/
|
|
43
|
+
export declare function socialHandoffFailureRedirect(reason: string, options?: SocialHandoffOptions): Response;
|
|
44
|
+
/**
|
|
45
|
+
* Hand a server-minted session to the browser at the end of a redirect flow
|
|
46
|
+
* (stacksjs/stacks#2236).
|
|
47
|
+
*
|
|
48
|
+
* The package covered the OAuth exchange and stopped at the provider user;
|
|
49
|
+
* `@stacksjs/auth` covered `loginUsingId()`. Nothing bridged them, so apps
|
|
50
|
+
* ended their callback action by returning an HTML page with an inline script
|
|
51
|
+
* that wrote the framework's own storage keys by hand — re-deriving the
|
|
52
|
+
* session format, and needing hand-written escaping so a display name
|
|
53
|
+
* containing a closing script tag could not terminate the block.
|
|
54
|
+
*
|
|
55
|
+
* This returns a plain 302 instead. No HTML, no inline script, so the escaping
|
|
56
|
+
* hazard does not exist and the tokens never appear in a response body that a
|
|
57
|
+
* proxy or a log might retain.
|
|
58
|
+
*/
|
|
59
|
+
export declare interface SocialHandoffOptions {
|
|
60
|
+
redirectTo?: string
|
|
61
|
+
allowedHosts?: string[]
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
3
|
-
`),redirectUrl:this.redirectUrl||(B.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:B.services.apple?.scopes??["name","email"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.resolveState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();this.validateConfig();let $={client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",...this.parameters};if(Z.length>0)$.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams($).toString()}`}async getAccessToken(J){let{clientId:X,redirectUrl:Y}=this.getConfig();this.validateConfig();let Z=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:J,redirect_uri:Y,client_id:X,client_secret:this.generateClientSecret()})}),$=await Z.json();if(!Z.ok||$.error)throw Error(`Apple OAuth error: ${$.error_description??$.error??`HTTP ${Z.status}`}`);if(!$.id_token)throw Error("Apple OAuth error: token response contained no id_token");return $.id_token}async getUserByToken(J){let{clientId:X}=this.getConfig(),Y=this.decodeIdToken(J),Z=Y.iss===this.baseUrl,$=Array.isArray(Y.aud)?Y.aud.includes(X):Y.aud===X,z=typeof Y.exp==="number"&&Y.exp*1000>Date.now();if(!Z||!$||!z)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!Y.sub)throw Error("Apple OAuth error: id_token has no subject");let N=typeof Y.email==="string"?Y.email:null,G=null;if(Y.email_verified===!0||Y.email_verified==="true")G=!0;else if(Y.email_verified===!1||Y.email_verified==="false")G=!1;return{id:String(Y.sub),nickname:null,name:"",email:N,emailVerified:G,avatar:null,token:J,raw:Y}}generateClientSecret(){let{clientId:J,teamId:X,keyId:Y,privateKey:Z}=this.getConfig(),$=Math.floor(Date.now()/1000),z={alg:"ES256",kid:Y,typ:"JWT"},N={iss:X,iat:$,exp:$+3600,aud:this.baseUrl,sub:J},G=`${this.base64urlJson(z)}.${this.base64urlJson(N)}`,W;try{W=c(Z)}catch(Q){throw new F(`Apple private key could not be parsed: ${Q instanceof Error?Q.message:String(Q)}`)}let K=p("sha256",q.from(G),{key:W,dsaEncoding:"ieee-p1363"});return`${G}.${K.toString("base64url")}`}decodeIdToken(J){let X=J.split(".");if(X.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(q.from(X[1],"base64url").toString("utf8"))}base64urlJson(J){return q.from(JSON.stringify(J)).toString("base64url")}validateConfig(){let{clientId:J,teamId:X,keyId:Y,privateKey:Z,redirectUrl:$}=this.getConfig();if(!J)throw new F("Apple client ID (Service ID) not provided");if(!X)throw new F("Apple team ID not provided");if(!Y)throw new F("Apple key ID not provided");if(!Z)throw new F("Apple private key not provided");if(!$)throw new F("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function d(J){let X=/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(String(J||"").trim());if(!X)throw Error(`"${J}" is not a Bluesky post URI.`);return{repo:X[1],collection:X[2],rkey:X[3]}}class b extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}var a=new TextEncoder;function D(J){return a.encode(J).length}function o(J){let X=[],Y=/https?:\/\/[^\s<>"']+/g;for(let N of J.matchAll(Y)){let G=N[0].replace(/[),.;!?]+$/,"");X.push({byteStart:D(J.slice(0,N.index)),byteEnd:D(J.slice(0,N.index))+D(G),type:"link",value:G})}let Z=(N,G)=>X.some((W)=>W.type==="link"&&N<W.byteEnd&&G>W.byteStart),$=/(^|\s)(#[A-Za-z0-9_]+)/g;for(let N of J.matchAll($)){let G=N[1],W=N[2];if(G===void 0||W===void 0)continue;if(/^#\d+$/.test(W))continue;let K=(N.index??0)+G.length,Q=D(J.slice(0,K)),V=Q+D(W);if(Z(Q,V))continue;X.push({byteStart:Q,byteEnd:V,type:"tag",value:W.slice(1)})}let z=/(^|\s)(@[a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;for(let N of J.matchAll(z)){let G=N[1],W=N[2];if(G===void 0||W===void 0)continue;let K=(N.index??0)+G.length,Q=D(J.slice(0,K)),V=Q+D(W);if(Z(Q,V))continue;X.push({byteStart:Q,byteEnd:V,type:"mention",value:W.slice(1).replace(/\.+$/,"")})}return X.sort((N,G)=>N.byteStart-G.byteStart)}class s{provider="bluesky";characterLimit=300;service;constructor(J={}){this.service=J.service||"https://bsky.social"}async createSession(J){let X=J.identifier.trim(),Y=J.password.trim();if(!X)throw Error("Bluesky identifier is required.");if(!Y)throw Error("Bluesky app password is required.");let Z=await this.post("/xrpc/com.atproto.server.createSession",{identifier:X,password:Y}),$=await this.getProfile({did:Z.did,handle:Z.handle,accessToken:Z.accessJwt,refreshToken:Z.refreshJwt}).catch(()=>{return});return{did:Z.did,handle:Z.handle,displayName:$?.displayName,accessJwt:Z.accessJwt,refreshJwt:Z.refreshJwt}}async refreshSession(J){if(!J)throw Error("Bluesky refresh token is required.");let X=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${J}`});return{did:X.did,handle:X.handle,accessJwt:X.accessJwt,refreshJwt:X.refreshJwt}}async publish(J,X){let Y=J.did||J.handle;if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!Y)throw Error("Bluesky identity DID or handle is required.");if(X.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let Z={$type:"app.bsky.feed.post",text:X.text,createdAt:X.scheduledAt||new Date().toISOString()};if(X.langs?.length)Z.langs=X.langs;if(X.reply)Z.reply=X.reply;let $=X.facets??await this.buildFacets(X.text);if($.length)Z.facets=$;if(X.external)Z.embed={$type:"app.bsky.embed.external",external:{uri:X.external.uri,title:X.external.title,description:X.external.description||""}};let z=(X.media||[]).filter((G)=>G.bytes?.length).slice(0,4);if(z.length){let G=[];for(let W of z){let K=await this.uploadBlob(J,W.bytes,W.mimeType||"image/jpeg");G.push({image:K,alt:W.altText||""})}Z.embed={$type:"app.bsky.embed.images",images:G}}let N=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:Y,collection:"app.bsky.feed.post",record:Z},{authorization:`Bearer ${J.accessToken}`});return{provider:this.provider,uri:N.uri,cid:N.cid,url:this.toPostUrl(J.handle,N.uri)}}async postMetrics(J,X){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(X.length===0)return[];let Y=new URL(`${this.service}/xrpc/app.bsky.feed.getPosts`);for(let $ of X.slice(0,25))Y.searchParams.append("uris",$);return((await this.request(Y,{headers:{authorization:`Bearer ${J.accessToken}`}})).posts||[]).map(($)=>({uri:$.uri,likeCount:$.likeCount||0,repostCount:$.repostCount||0,replyCount:$.replyCount||0}))}async timeline(J,X={}){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");let Y=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(Y.searchParams.set("limit",String(Math.min(Math.max(X.limit||30,1),100))),X.cursor)Y.searchParams.set("cursor",X.cursor);let Z=await this.request(Y,{headers:{authorization:`Bearer ${J.accessToken}`}});return{cursor:Z.cursor,items:(Z.feed||[]).flatMap(($)=>{let z=$.post;if(!z?.uri||!z.author?.handle)return[];return[{uri:z.uri,authorHandle:z.author.handle,authorName:z.author.displayName,authorAvatar:z.author.avatar,postUrl:this.toPostUrl(z.author.handle,z.uri),body:z.record?.text||"",postedAt:z.record?.createdAt||new Date().toISOString(),likeCount:z.likeCount||0,repostCount:z.repostCount||0,replyCount:z.replyCount||0}]})}}async getProfile(J){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");let X=J.did||J.handle;if(!X)throw Error("Bluesky identity DID or handle is required.");let Y=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return Y.searchParams.set("actor",X),await this.request(Y,{headers:{authorization:`Bearer ${J.accessToken}`}})}async buildFacets(J){let X=[];for(let Y of o(J)){let Z=null;if(Y.type==="link")Z={$type:"app.bsky.richtext.facet#link",uri:Y.value};else if(Y.type==="tag")Z={$type:"app.bsky.richtext.facet#tag",tag:Y.value};else if(Y.type==="mention"){let $=await this.resolveHandle(Y.value);if($)Z={$type:"app.bsky.richtext.facet#mention",did:$}}if(Z)X.push({index:{byteStart:Y.byteStart,byteEnd:Y.byteEnd},features:[Z]})}return X}async resolveHandle(J){try{let X=new URL(`${this.service}/xrpc/com.atproto.identity.resolveHandle`);return X.searchParams.set("handle",J),(await this.request(X,{})).did||null}catch{return null}}async uploadBlob(J,X,Y){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(X.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":Y,authorization:`Bearer ${J.accessToken}`},body:new Uint8Array(X)})).blob}async listAuthoredPosts(J,X={}){let Y=J.did||J.handle;if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!Y)throw Error("Bluesky identity DID or handle is required.");let Z=new URL(`${this.service}/xrpc/com.atproto.repo.listRecords`);if(Z.searchParams.set("repo",Y),Z.searchParams.set("collection","app.bsky.feed.post"),Z.searchParams.set("limit",String(Math.min(Math.max(X.limit||100,1),100))),X.cursor)Z.searchParams.set("cursor",X.cursor);let $=await this.request(Z,{headers:{authorization:`Bearer ${J.accessToken}`}});return{cursor:$.cursor,posts:($.records||[]).filter((z)=>z?.uri).map((z)=>({uri:z.uri,cid:z.cid,text:z.value?.text,postedAt:z.value?.createdAt,url:J.handle?this.toPostUrl(J.handle,z.uri):void 0}))}}async deletePost(J,X){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");let{repo:Y,collection:Z,rkey:$}=d(X.uri);await this.post("/xrpc/com.atproto.repo.deleteRecord",{repo:Y,collection:Z,rkey:$},{authorization:`Bearer ${J.accessToken}`})}async post(J,X,Y={}){return await this.request(new URL(`${this.service}${J}`),{method:"POST",headers:{...X===void 0?{}:{"content-type":"application/json"},...Y},...X===void 0?{}:{body:JSON.stringify(X)}})}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new b(`Bluesky API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}toPostUrl(J,X){let Y=X.split("/").pop();return`https://bsky.app/profile/${J}/post/${Y}`}}import{fetcher as k}from"@stacksjs/api";import{config as j}from"@stacksjs/config";class v extends M{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let J={clientId:j.services.facebook?.clientId??"",clientSecret:j.services.facebook?.clientSecret??"",redirectUrl:j.services.facebook?.redirectUrl??"",scopes:j.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.getState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(","),state:J,response_type:"code"}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();this.validateConfig();let $=await k.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:X,client_secret:Y,redirect_uri:Z,code:J}).toString()}`);if($.data.error)throw Error(`Facebook OAuth error: ${$.data.error.message}`);return $.data.access_token}async getUserByToken(J){let X=await k.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:J,fields:"id,name,email,picture"}).toString()}`);return{id:X.data.id,nickname:null,name:X.data.name,email:X.data.email??null,avatar:X.data.picture?.data.url??null,token:J,raw:X.data}}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("Facebook client ID not provided");if(!X)throw new F("Facebook client secret not provided");if(!Y)throw new F("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as T}from"@stacksjs/api";import{config as L}from"@stacksjs/config";class A extends M{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let J={clientId:this.clientId||(L.services.github?.clientId??""),clientSecret:this.clientSecret||(L.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(L.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:L.services.github?.scopes??["read:user","user:email"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.resolveState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();this.validateConfig();let $=await T.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:X,client_secret:Y,code:J,redirect_uri:Z});if($.data.error)throw Error(`GitHub OAuth error: ${$.data.error_description}`);return $.data.access_token}async getUserByToken(J){let[X,Y]=await Promise.all([T.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${J}`}).get(`${this.apiUrl}/user`),T.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${J}`}).get(`${this.apiUrl}/user/emails`)]),Z=this.pickEmail(Y.data);return{id:X.data.id.toString(),nickname:X.data.login,name:X.data.name??X.data.login,email:Z?.email??X.data.email??null,emailVerified:Z?Z.verified:null,avatar:X.data.avatar_url,token:J,raw:X.data}}pickEmail(J){if(!Array.isArray(J)||J.length===0)return null;let X=J.find((Z)=>Z.primary&&Z.verified),Y=J.find((Z)=>Z.verified);return X??Y??J.find((Z)=>Z.primary)??J[0]??null}getEmail(J){return this.pickEmail(J)?.email??null}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("GitHub client ID not provided");if(!X)throw new F("GitHub client secret not provided");if(!Y)throw new F("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as h}from"@stacksjs/api";import{config as w}from"@stacksjs/config";class I extends M{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let J={clientId:this.clientId||(w.services.google?.clientId??""),clientSecret:this.clientSecret||(w.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(w.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:w.services.google?.scopes??["openid","email"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.resolveState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();this.validateConfig();let $=await h.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:X,client_secret:Y,code:J,redirect_uri:Z,grant_type:"authorization_code"});if($.data.error)throw Error(`Google OAuth error: ${$.data.error_description}`);return $.data.access_token}async getUserByToken(J){let X=await h.withHeaders({Authorization:`Bearer ${J}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:X.data.id,nickname:X.data.given_name,name:X.data.name,email:X.data.email,emailVerified:typeof X.data.verified_email==="boolean"?X.data.verified_email:null,avatar:X.data.picture,token:J,raw:X.data}}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("Google client ID not provided");if(!X)throw new F("Google client secret not provided");if(!Y)throw new F("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}class P extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class r{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(J={}){this.graphVersion=J.graphVersion||"v21.0",this.authBase=J.authBase||"https://www.facebook.com",this.graphBase=J.graphBase||"https://graph.facebook.com"}getAuthUrl(J){let X=new URLSearchParams({client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(","),state:J.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${X.toString()}`}async exchangeCode(J){let X=new URLSearchParams({client_id:J.clientId,client_secret:J.clientSecret,redirect_uri:J.redirectUrl,code:J.code}),Y=await this.graph(`/oauth/access_token?${X.toString()}`,{method:"GET"});if(!Y.access_token)throw new P("Facebook did not return an access token.",400,JSON.stringify(Y));return{accessToken:Y.access_token,expiresIn:Y.expires_in}}async resolveAccount(J){let X=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:J}),Y=await this.graph(`/me/accounts?${X.toString()}`,{method:"GET"}),Z=(Y.data||[]).find((z)=>z.instagram_business_account?.id),$=Z?.instagram_business_account;if(!$?.id||!Z?.access_token)throw new P("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(Y));return{igUserId:$.id,username:$.username,pageAccessToken:Z.access_token}}async publish(J,X){if(!J.accessToken)throw Error("Instagram access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("Instagram account id is required to publish.");let Z=X.media?.[0];if(!Z?.url)throw Error("Instagram requires an image to post.");if(X.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let $=await this.graph(`/${Y}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:Z.url,caption:X.text,access_token:J.accessToken}).toString()});if(!$.id)throw new P("Instagram did not return a media container id.",400,JSON.stringify($));let z=await this.graph(`/${Y}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:$.id,access_token:J.accessToken}).toString()}),N=await this.graph(`/${z.id}?fields=permalink&access_token=${encodeURIComponent(J.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:z.id,url:N?.permalink}}async timeline(J,X={}){return{items:[]}}async graph(J,X){let Y=await fetch(`${this.graphBase}/${this.graphVersion}${J}`,X),Z=await Y.text(),$={};try{$=Z?JSON.parse(Z):{}}catch{$={}}if(!Y.ok||$?.error){let z=$?.error?.message||Z||Y.statusText;throw new P(`Instagram API failed (${Y.status}): ${z}`,Y.status,Z)}return $}}class H extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class n{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(J={}){this.apiVersion=J.apiVersion||"202405",this.authBase=J.authBase||"https://www.linkedin.com",this.apiBase=J.apiBase||"https://api.linkedin.com"}getAuthUrl(J){let X=new URLSearchParams({response_type:"code",client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(" "),state:J.state});return`${this.authBase}/oauth/v2/authorization?${X.toString()}`}async exchangeCode(J){let X=new URLSearchParams({grant_type:"authorization_code",code:J.code,redirect_uri:J.redirectUrl,client_id:J.clientId,client_secret:J.clientSecret}),Y=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:X.toString()});if(!Y.access_token)throw new H("LinkedIn did not return an access token.",400,JSON.stringify(Y));return{accessToken:Y.access_token,expiresIn:Y.expires_in,scope:Y.scope}}async getProfile(J){if(!J)throw Error("LinkedIn access token is required.");let X=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${J}`}});if(!X.sub)throw new H("LinkedIn profile is missing a subject id.",400,JSON.stringify(X));return{sub:X.sub,name:X.name,picture:X.picture}}async publish(J,X){if(!J.accessToken)throw Error("LinkedIn access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("LinkedIn member URN is required to publish.");if(X.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let Z={author:Y,commentary:i(X.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(X.external)Z.content={article:{source:X.external.uri,title:X.external.title,description:X.external.description||""}};let $=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${J.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(Z)}),z=await $.text();if(!$.ok)throw new H(`LinkedIn API failed (${$.status}): ${z||$.statusText}`,$.status,z);let N=$.headers.get("x-restli-id")||$.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:N,url:N?`https://www.linkedin.com/feed/update/${N}`:void 0}}async timeline(J,X={}){return{items:[]}}async listAuthoredPosts(J,X={}){if(!J.accessToken)throw Error("LinkedIn access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("LinkedIn member URN is required to list posts.");let Z=Math.min(Math.max(X.limit||50,1),100),$=Number(X.cursor||0)||0,z=new URL(`${this.apiBase}/rest/posts`);z.searchParams.set("q","author"),z.searchParams.set("author",Y),z.searchParams.set("count",String(Z)),z.searchParams.set("start",String($));let N;try{N=await this.request(z.toString(),{headers:{authorization:`Bearer ${J.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}})}catch(W){if(W instanceof H&&(W.status===401||W.status===403))throw new H("LinkedIn will not list this account's posts \u2014 the Posts author finder needs the r_member_social permission, which this app does not hold.",W.status,W.body);throw W}let G=(N.elements||[]).filter((W)=>W?.id).map((W)=>({uri:String(W.id),text:W.commentary,postedAt:W.createdAt?new Date(W.createdAt).toISOString():void 0,url:`https://www.linkedin.com/feed/update/${W.id}`}));return{cursor:G.length===Z?String($+Z):void 0,posts:G}}async deletePost(J,X){if(!J.accessToken)throw Error("LinkedIn access token is missing for this identity.");let Y=String(X.uri||"").trim();if(!Y)throw Error("A LinkedIn post URN is required to delete a post.");let Z=await fetch(`${this.apiBase}/rest/posts/${encodeURIComponent(Y)}`,{method:"DELETE",headers:{authorization:`Bearer ${J.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}});if(!Z.ok&&Z.status!==404){let $=await Z.text().catch(()=>"");throw new H(`LinkedIn API failed (${Z.status}): ${$||Z.statusText}`,Z.status,$)}}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new H(`LinkedIn API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}}function i(J){return J.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class g extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="MastodonApiError"}get isAuthError(){return this.status===401||this.status===403}}function t(J){let X=String(J||"").trim().replace(/\/+$/,"");if(!X)throw Error("Mastodon instance URL is required.");let Y=/^https?:\/\//i.test(X)?X:`https://${X}`;try{let Z=new URL(Y);return`${Z.protocol}//${Z.host}`}catch{throw Error("Mastodon instance URL is invalid.")}}class e{provider="mastodon";characterLimit=500;instanceOf(J){return t(J.did||"")}tokenOf(J){if(!J.accessToken)throw Error("Mastodon access token is missing for this identity.");return J.accessToken}async verifyCredentials(J){let X=await this.request(`${this.instanceOf(J)}/api/v1/accounts/verify_credentials`,{headers:{authorization:`Bearer ${this.tokenOf(J)}`}});return{accountId:X.id,username:X.username,displayName:X.display_name||void 0,url:X.url}}async uploadMedia(J,X,Y,Z){let $=new FormData;if($.set("file",new Blob([new Uint8Array(X)],{type:Y||"image/jpeg"}),"upload"),Z)$.set("description",Z);return(await this.request(`${this.instanceOf(J)}/api/v2/media`,{method:"POST",headers:{authorization:`Bearer ${this.tokenOf(J)}`},body:$})).id}async publish(J,X){let Y=this.instanceOf(J),Z=this.tokenOf(J);if(X.text.length>this.characterLimit)throw Error(`Mastodon posts must be ${this.characterLimit} characters or fewer.`);let $=[];for(let G of(X.media||[]).slice(0,4)){let{bytes:W,mimeType:K}=G;if(!W?.length&&G.url){let Q=await fetch(G.url);if(!Q.ok)continue;W=new Uint8Array(await Q.arrayBuffer()),K=K||Q.headers.get("content-type")||"image/jpeg"}if(W?.length)$.push(await this.uploadMedia(J,W,K||"image/jpeg",G.altText))}let z={status:X.text,visibility:"public"};if($.length)z.media_ids=$;if(X.reply?.parent?.uri)z.in_reply_to_id=X.reply.parent.uri;let N=await this.request(`${Y}/api/v1/statuses`,{method:"POST",headers:{authorization:`Bearer ${Z}`,"content-type":"application/json"},body:JSON.stringify(z)});return{provider:this.provider,uri:N.id,cid:N.id,url:N.url||N.uri}}async timeline(J,X={}){return{items:[]}}async listAuthoredPosts(J,X={}){let Y=this.instanceOf(J),{accountId:Z}=await this.verifyCredentials(J),$=new URL(`${Y}/api/v1/accounts/${encodeURIComponent(Z)}/statuses`);if($.searchParams.set("limit",String(Math.min(Math.max(X.limit||40,1),40))),$.searchParams.set("exclude_reblogs","true"),X.cursor)$.searchParams.set("max_id",X.cursor);let N=(await this.request($.toString(),{headers:{authorization:`Bearer ${this.tokenOf(J)}`}})||[]).filter((G)=>G?.id).map((G)=>({uri:G.id,cid:G.id,text:G.content,postedAt:G.created_at,url:G.url}));return{cursor:N.length?N[N.length-1]?.uri:void 0,posts:N}}async deletePost(J,X){let Y=String(X.cid||JJ(X.uri)||"").trim();if(!Y)throw Error("A status id is required to delete a post.");await this.request(`${this.instanceOf(J)}/api/v1/statuses/${encodeURIComponent(Y)}`,{method:"DELETE",headers:{authorization:`Bearer ${this.tokenOf(J)}`}})}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new g(`Mastodon API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}}function JJ(J){return String(J||"").replace(/\/+$/,"").split("/").pop()||""}class O extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class XJ{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(J={}){this.graphVersion=J.graphVersion||"v1.0",this.authBase=J.authBase||"https://threads.net",this.graphBase=J.graphBase||"https://graph.threads.net"}getAuthUrl(J){let X=new URLSearchParams({client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(","),response_type:"code",state:J.state});return`${this.authBase}/oauth/authorize?${X.toString()}`}async exchangeCode(J){let X=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:J.clientId,client_secret:J.clientSecret,grant_type:"authorization_code",redirect_uri:J.redirectUrl,code:J.code}).toString()}),Y=await X.text(),Z={};try{Z=Y?JSON.parse(Y):{}}catch{Z={}}if(!X.ok||Z?.error||!Z?.access_token){let $=Z?.error_message||Z?.error?.message||Y||X.statusText;throw new O(`Threads token exchange failed (${X.status}): ${$}`,X.status,Y)}return{accessToken:Z.access_token,userId:Z.user_id!=null?String(Z.user_id):void 0,expiresIn:Z.expires_in}}async resolveAccount(J){let X=new URLSearchParams({fields:"id,username",access_token:J}),Y=await this.graph(`/me?${X.toString()}`,{method:"GET"});if(!Y.id)throw new O("Could not resolve the Threads account for this token.",400,JSON.stringify(Y));return{threadsUserId:Y.id,username:Y.username,accessToken:J}}async publish(J,X){if(!J.accessToken)throw Error("Threads access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("Threads account id is required to publish.");if(X.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let Z=X.media?.[0],$=new URLSearchParams({text:X.text,access_token:J.accessToken});if(Z?.url)$.set("media_type","IMAGE"),$.set("image_url",Z.url);else $.set("media_type","TEXT");let z=await this.graph(`/${Y}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:$.toString()});if(!z.id)throw new O("Threads did not return a media container id.",400,JSON.stringify(z));let N=await this.graph(`/${Y}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:z.id,access_token:J.accessToken}).toString()});if(!N.id)throw new O("Threads did not return a published post id.",400,JSON.stringify(N));let G=await this.graph(`/${N.id}?fields=permalink&access_token=${encodeURIComponent(J.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:N.id,url:G?.permalink}}async timeline(J,X={}){return{items:[]}}async graph(J,X){let Y=await fetch(`${this.graphBase}/${this.graphVersion}${J}`,X),Z=await Y.text(),$={};try{$=Z?JSON.parse(Z):{}}catch{$={}}if(!Y.ok||$?.error){let z=$?.error?.message||Z||Y.statusText;throw new O(`Threads API failed (${Y.status}): ${z}`,Y.status,Z)}return $}}import{Buffer as YJ}from"buffer";import{createHash as ZJ,randomBytes as $J}from"crypto";import{fetcher as f}from"@stacksjs/api";import{config as S}from"@stacksjs/config";class E extends M{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let J={clientId:S.services.twitter?.clientId??"",clientSecret:S.services.twitter?.clientSecret??"",redirectUrl:S.services.twitter?.redirectUrl??"",scopes:S.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(J.scopes),J}generateCodeVerifier(){return $J(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(J){return ZJ("sha256").update(J).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let J=this.getState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let $=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",code_challenge:$,code_challenge_method:"S256"}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let $=YJ.from(`${X}:${Y}`).toString("base64"),z=await f.withHeaders({Authorization:`Basic ${$}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:J,grant_type:"authorization_code",redirect_uri:Z,code_verifier:this.codeVerifier});if(z.data.error)throw Error(`Twitter OAuth error: ${z.data.error_description}`);return z.data.access_token}async getUserByToken(J){let X=await f.withHeaders({Authorization:`Bearer ${J}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:X.data.id,nickname:X.data.username,name:X.data.name,email:X.data.email??null,avatar:X.data.profile_image_url??null,token:J,raw:X.data}}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("Twitter client ID not provided");if(!X)throw new F("Twitter client secret not provided");if(!Y)throw new F("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class _ extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="TwitterApiError"}get isAuthError(){return this.status===401||this.status===403}}function m(J){let X="";for(let Y of J)X+=String.fromCharCode(Y);return btoa(X).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class zJ{provider="twitter";characterLimit=280;apiBase;authorizeBase;constructor(J={}){this.apiBase=J.apiBase||"https://api.twitter.com",this.authorizeBase=J.authorizeBase||"https://twitter.com"}async createAuthorization(J){let X=m(crypto.getRandomValues(new Uint8Array(32))),Y=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(X)),Z=m(new Uint8Array(Y)),$=new URLSearchParams({response_type:"code",client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(" "),state:J.state,code_challenge:Z,code_challenge_method:"S256"});return{url:`${this.authorizeBase}/i/oauth2/authorize?${$.toString()}`,codeVerifier:X}}async exchangeCode(J){return this.tokenRequest(new URLSearchParams({grant_type:"authorization_code",code:J.code,redirect_uri:J.redirectUrl,code_verifier:J.codeVerifier,client_id:J.clientId}),J.clientId,J.clientSecret)}async refreshAccessToken(J){return this.tokenRequest(new URLSearchParams({grant_type:"refresh_token",refresh_token:J.refreshToken,client_id:J.clientId}),J.clientId,J.clientSecret)}async getProfile(J){let X=await this.request(`${this.apiBase}/2/users/me?user.fields=username,name`,{headers:{authorization:`Bearer ${J}`}});if(!X.data?.id||!X.data.username)throw new _("Twitter did not return the authenticated user.",400,JSON.stringify(X));return{id:X.data.id,username:X.data.username,name:X.data.name}}async uploadMedia(J,X,Y){let Z=new FormData;Z.set("media",new Blob([new Uint8Array(X)],{type:Y||"image/jpeg"})),Z.set("media_category","tweet_image");let $=await this.request(`${this.apiBase}/2/media/upload`,{method:"POST",headers:{authorization:`Bearer ${J}`},body:Z}),z=$.data?.id||$.media_id_string||$.id;if(!z)throw new _("Twitter did not return a media id.",400,JSON.stringify($));return z}async publish(J,X){if(!J.accessToken)throw Error("Twitter access token is missing for this identity.");if(X.text.length>this.characterLimit)throw Error(`Twitter posts must be ${this.characterLimit} characters or fewer.`);let Y=[],Z=X.media?.[0];if(Z){let{bytes:G,mimeType:W}=Z;if(!G?.length&&Z.url){let K=await fetch(Z.url);if(K.ok)G=new Uint8Array(await K.arrayBuffer()),W=W||K.headers.get("content-type")||"image/jpeg"}if(G?.length)Y.push(await this.uploadMedia(J.accessToken,G,W||"image/jpeg"))}let $={text:X.text};if(Y.length)$.media={media_ids:Y};if(X.reply?.parent?.uri)$.reply={in_reply_to_tweet_id:X.reply.parent.uri};let z=await this.request(`${this.apiBase}/2/tweets`,{method:"POST",headers:{authorization:`Bearer ${J.accessToken}`,"content-type":"application/json"},body:JSON.stringify($)}),N=z.data?.id;if(!N)throw new _("Twitter did not return a tweet id.",400,JSON.stringify(z));return{provider:this.provider,uri:N,cid:N,url:J.handle?`https://x.com/${J.handle}/status/${N}`:`https://x.com/i/web/status/${N}`}}async timeline(J,X={}){return{items:[]}}async listAuthoredPosts(J,X={}){if(!J.accessToken)throw Error("Twitter access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("Twitter user id is required to list posts.");let Z=new URL(`${this.apiBase}/2/users/${encodeURIComponent(Y)}/tweets`);if(Z.searchParams.set("max_results",String(Math.min(Math.max(X.limit||100,5),100))),Z.searchParams.set("tweet.fields","created_at"),X.cursor)Z.searchParams.set("pagination_token",X.cursor);let $=await this.request(Z.toString(),{headers:{authorization:`Bearer ${J.accessToken}`}});return{cursor:$.meta?.next_token,posts:($.data||[]).filter((z)=>z?.id).map((z)=>({uri:z.id,cid:z.id,text:z.text,postedAt:z.created_at,url:`https://x.com/i/web/status/${z.id}`}))}}async deletePost(J,X){if(!J.accessToken)throw Error("Twitter access token is missing for this identity.");let Y=String(X.uri||"").trim();if(!Y)throw Error("A tweet id is required to delete a post.");let Z=await this.request(`${this.apiBase}/2/tweets/${encodeURIComponent(Y)}`,{method:"DELETE",headers:{authorization:`Bearer ${J.accessToken}`}});if(Z.data&&Z.data.deleted===!1)throw new _(`X refused to delete tweet ${Y}.`,400,JSON.stringify(Z))}async tokenRequest(J,X,Y){let Z={"content-type":"application/x-www-form-urlencoded"};if(Y)Z.authorization=`Basic ${btoa(`${X}:${Y}`)}`;let $=await this.request(`${this.apiBase}/2/oauth2/token`,{method:"POST",headers:Z,body:J.toString()});if(!$.access_token)throw new _("Twitter did not return an access token.",400,JSON.stringify($));return{accessToken:$.access_token,refreshToken:$.refresh_token,expiresIn:$.expires_in,scope:$.scope}}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new _(`Twitter API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}}import{config as NJ}from"@stacksjs/config";var U=["clientId","clientSecret","redirectUrl"],R=Object.freeze({google:{name:"google",label:"Google",driver:I,required:U,postCallback:!1},github:{name:"github",label:"GitHub",driver:A,required:U,postCallback:!1},facebook:{name:"facebook",label:"Facebook",driver:v,required:U,postCallback:!1},twitter:{name:"twitter",label:"X",driver:E,required:U,postCallback:!1},apple:{name:"apple",label:"Apple",driver:C,required:["clientId","teamId","keyId","privateKey","redirectUrl"],postCallback:!0}});function l(J){return NJ?.services?.[J]}function GJ(J){return typeof J==="string"&&J in R}function u(J){if(!GJ(J))return!1;let X=l(J);if(!X)return!1;return R[J].required.every((Y)=>Boolean(X[Y]))}function oJ(){return Object.keys(R).filter(u).map((J)=>R[J])}function sJ(J){if(!u(J))return null;let X=R[J],Y=l(J)??{};return new X.driver({clientSecret:"",...Y,clientId:String(Y.clientId??""),redirectUrl:String(Y.redirectUrl??"")})}class WJ{accessToken;refreshToken;expiresIn;approvedScopes;constructor(J,X=null,Y=null,Z=[]){this.accessToken=J;this.refreshToken=X;this.expiresIn=Y;this.approvedScopes=Z}}function iJ(J){return typeof J?.deletePost==="function"}function tJ(J){return typeof J?.listAuthoredPosts==="function"}export{tJ as supportsEnumeration,iJ as supportsDeletion,sJ as socialProvider,d as parseAtUri,t as normalizeInstance,GJ as isSocialProviderName,u as isSocialProviderConfigured,i as escapeLinkedInText,o as detectFacetCandidates,oJ as configuredSocialProviders,zJ as TwitterPublishingDriver,E as TwitterProvider,_ as TwitterApiError,WJ as Token,XJ as ThreadsPublishingDriver,O as ThreadsApiError,R as SOCIAL_PROVIDERS,e as MastodonPublishingDriver,g as MastodonApiError,n as LinkedInPublishingDriver,H as LinkedInApiError,y as InvalidStateException,r as InstagramPublishingDriver,P as InstagramApiError,I as GoogleProvider,A as GitHubProvider,v as FacebookProvider,F as ConfigException,s as BlueskyPublishingDriver,b as BlueskyApiError,C as AppleProvider,M as AbstractProvider};
|
|
2
|
+
var T=import.meta.require;class M{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_state=null;user=null;constructor(J){this.clientId=J.clientId,this.clientSecret=J.clientSecret,this.redirectUrl=J.redirectUrl}getCodeFields(J=null){let X={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())X.state=J;if(this.usesPKCE())X.code_challenge=this.getCodeChallenge(),X.code_challenge_method=this.getCodeChallengeMethod();return{...X,...this.parameters}}formatScopes(J,X){return J.join(X)}async userFromToken(J){return{...await this.getUserByToken(J),token:J}}scopes(J){let X=Array.isArray(J)?J:[J];return this._scopes=[...new Set([...this._scopes,...X])],this}setScopes(J){let X=Array.isArray(J)?J:[J];return this._scopes=[...new Set(X)],this}getScopes(){return this._scopes}setRedirectUrl(J){if(typeof J!=="string"||J.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let X;try{X=new URL(J)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${J}`)}if(X.protocol!=="https:"&&X.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${X.protocol}`);return this.redirectUrl=J,this}withState(J){if(typeof J!=="string"||J.length===0)throw Error("[socials] withState requires a non-empty string");return this._state=J,this}resolveState(){return this._state??this.getState()}usesState(){return!this._stateless}validateState(J,X){if(typeof J!=="string"||typeof X!=="string")return!1;if(J.length===0||X.length===0)return!1;if(J.length!==X.length)return!1;try{let{timingSafeEqual:Y}=T("crypto");return Y(Buffer.from(J,"utf8"),Buffer.from(X,"utf8"))}catch{let Y=0;for(let Z=0;Z<J.length;Z++)Y|=J.charCodeAt(Z)^X.charCodeAt(Z);return Y===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let J=new Uint8Array(32);return crypto.getRandomValues(J),Array.from(J).map((X)=>X.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let J=new Uint8Array(48);return crypto.getRandomValues(J),Array.from(J).map((X)=>X.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let X=new TextEncoder().encode(this.getCodeVerifier()),Y=await crypto.subtle.digest("SHA-256",X),{Buffer:Z}=await import("buffer");return Z.from(new Uint8Array(Y)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(J){return this.parameters=J,this}buildAuthUrlFromBase(J,X){let Y=new URLSearchParams(this.getCodeFields(X));return`${J}?${Y.toString()}`}}import{Buffer as S}from"buffer";import{createPrivateKey as p,sign as d}from"crypto";import{config as B}from"@stacksjs/config";class c extends Error{constructor(J="Invalid state"){super(J);this.name="InvalidStateException"}}class F extends Error{constructor(J){super(J);this.name="ConfigException"}}class C extends M{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";constructor(J){super(J);this.teamId=J.teamId??"",this.keyId=J.keyId??"",this.privateKey=J.privateKey??""}getConfig(){let J={clientId:this.clientId||(B.services.apple?.clientId??""),teamId:this.teamId||(B.services.apple?.teamId??""),keyId:this.keyId||(B.services.apple?.keyId??""),privateKey:(this.privateKey||(B.services.apple?.privateKey??"")).replace(/\\n/g,`
|
|
3
|
+
`),redirectUrl:this.redirectUrl||(B.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:B.services.apple?.scopes??["name","email"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.resolveState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();this.validateConfig();let $={client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",...this.parameters};if(Z.length>0)$.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams($).toString()}`}async getAccessToken(J){let{clientId:X,redirectUrl:Y}=this.getConfig();this.validateConfig();let Z=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:J,redirect_uri:Y,client_id:X,client_secret:this.generateClientSecret()})}),$=await Z.json();if(!Z.ok||$.error)throw Error(`Apple OAuth error: ${$.error_description??$.error??`HTTP ${Z.status}`}`);if(!$.id_token)throw Error("Apple OAuth error: token response contained no id_token");return $.id_token}async getUserByToken(J){let{clientId:X}=this.getConfig(),Y=this.decodeIdToken(J),Z=Y.iss===this.baseUrl,$=Array.isArray(Y.aud)?Y.aud.includes(X):Y.aud===X,N=typeof Y.exp==="number"&&Y.exp*1000>Date.now();if(!Z||!$||!N)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!Y.sub)throw Error("Apple OAuth error: id_token has no subject");let z=typeof Y.email==="string"?Y.email:null,G=null;if(Y.email_verified===!0||Y.email_verified==="true")G=!0;else if(Y.email_verified===!1||Y.email_verified==="false")G=!1;return{id:String(Y.sub),nickname:null,name:"",email:z,emailVerified:G,avatar:null,token:J,raw:Y}}generateClientSecret(){let{clientId:J,teamId:X,keyId:Y,privateKey:Z}=this.getConfig(),$=Math.floor(Date.now()/1000),N={alg:"ES256",kid:Y,typ:"JWT"},z={iss:X,iat:$,exp:$+3600,aud:this.baseUrl,sub:J},G=`${this.base64urlJson(N)}.${this.base64urlJson(z)}`,W;try{W=p(Z)}catch(Q){throw new F(`Apple private key could not be parsed: ${Q instanceof Error?Q.message:String(Q)}`)}let K=d("sha256",S.from(G),{key:W,dsaEncoding:"ieee-p1363"});return`${G}.${K.toString("base64url")}`}decodeIdToken(J){let X=J.split(".");if(X.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(S.from(X[1],"base64url").toString("utf8"))}base64urlJson(J){return S.from(JSON.stringify(J)).toString("base64url")}validateConfig(){let{clientId:J,teamId:X,keyId:Y,privateKey:Z,redirectUrl:$}=this.getConfig();if(!J)throw new F("Apple client ID (Service ID) not provided");if(!X)throw new F("Apple team ID not provided");if(!Y)throw new F("Apple key ID not provided");if(!Z)throw new F("Apple private key not provided");if(!$)throw new F("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}function a(J){let X=/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(String(J||"").trim());if(!X)throw Error(`"${J}" is not a Bluesky post URI.`);return{repo:X[1],collection:X[2],rkey:X[3]}}class b extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}var o=new TextEncoder;function _(J){return o.encode(J).length}function n(J){let X=[],Y=/https?:\/\/[^\s<>"']+/g;for(let z of J.matchAll(Y)){let G=z[0].replace(/[),.;!?]+$/,"");X.push({byteStart:_(J.slice(0,z.index)),byteEnd:_(J.slice(0,z.index))+_(G),type:"link",value:G})}let Z=(z,G)=>X.some((W)=>W.type==="link"&&z<W.byteEnd&&G>W.byteStart),$=/(^|\s)(#[A-Za-z0-9_]+)/g;for(let z of J.matchAll($)){let G=z[1],W=z[2];if(G===void 0||W===void 0)continue;if(/^#\d+$/.test(W))continue;let K=(z.index??0)+G.length,Q=_(J.slice(0,K)),V=Q+_(W);if(Z(Q,V))continue;X.push({byteStart:Q,byteEnd:V,type:"tag",value:W.slice(1)})}let N=/(^|\s)(@[a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;for(let z of J.matchAll(N)){let G=z[1],W=z[2];if(G===void 0||W===void 0)continue;let K=(z.index??0)+G.length,Q=_(J.slice(0,K)),V=Q+_(W);if(Z(Q,V))continue;X.push({byteStart:Q,byteEnd:V,type:"mention",value:W.slice(1).replace(/\.+$/,"")})}return X.sort((z,G)=>z.byteStart-G.byteStart)}class s{provider="bluesky";characterLimit=300;service;constructor(J={}){this.service=J.service||"https://bsky.social"}async createSession(J){let X=J.identifier.trim(),Y=J.password.trim();if(!X)throw Error("Bluesky identifier is required.");if(!Y)throw Error("Bluesky app password is required.");let Z=await this.post("/xrpc/com.atproto.server.createSession",{identifier:X,password:Y}),$=await this.getProfile({did:Z.did,handle:Z.handle,accessToken:Z.accessJwt,refreshToken:Z.refreshJwt}).catch(()=>{return});return{did:Z.did,handle:Z.handle,displayName:$?.displayName,accessJwt:Z.accessJwt,refreshJwt:Z.refreshJwt}}async refreshSession(J){if(!J)throw Error("Bluesky refresh token is required.");let X=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${J}`});return{did:X.did,handle:X.handle,accessJwt:X.accessJwt,refreshJwt:X.refreshJwt}}async publish(J,X){let Y=J.did||J.handle;if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!Y)throw Error("Bluesky identity DID or handle is required.");if(X.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let Z={$type:"app.bsky.feed.post",text:X.text,createdAt:X.scheduledAt||new Date().toISOString()};if(X.langs?.length)Z.langs=X.langs;if(X.reply)Z.reply=X.reply;let $=X.facets??await this.buildFacets(X.text);if($.length)Z.facets=$;if(X.external)Z.embed={$type:"app.bsky.embed.external",external:{uri:X.external.uri,title:X.external.title,description:X.external.description||""}};let N=(X.media||[]).filter((G)=>G.bytes?.length).slice(0,4);if(N.length){let G=[];for(let W of N){let K=await this.uploadBlob(J,W.bytes,W.mimeType||"image/jpeg");G.push({image:K,alt:W.altText||""})}Z.embed={$type:"app.bsky.embed.images",images:G}}let z=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:Y,collection:"app.bsky.feed.post",record:Z},{authorization:`Bearer ${J.accessToken}`});return{provider:this.provider,uri:z.uri,cid:z.cid,url:this.toPostUrl(J.handle,z.uri)}}async postMetrics(J,X){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(X.length===0)return[];let Y=new URL(`${this.service}/xrpc/app.bsky.feed.getPosts`);for(let $ of X.slice(0,25))Y.searchParams.append("uris",$);return((await this.request(Y,{headers:{authorization:`Bearer ${J.accessToken}`}})).posts||[]).map(($)=>({uri:$.uri,likeCount:$.likeCount||0,repostCount:$.repostCount||0,replyCount:$.replyCount||0}))}async timeline(J,X={}){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");let Y=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(Y.searchParams.set("limit",String(Math.min(Math.max(X.limit||30,1),100))),X.cursor)Y.searchParams.set("cursor",X.cursor);let Z=await this.request(Y,{headers:{authorization:`Bearer ${J.accessToken}`}});return{cursor:Z.cursor,items:(Z.feed||[]).flatMap(($)=>{let N=$.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(J){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");let X=J.did||J.handle;if(!X)throw Error("Bluesky identity DID or handle is required.");let Y=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return Y.searchParams.set("actor",X),await this.request(Y,{headers:{authorization:`Bearer ${J.accessToken}`}})}async buildFacets(J){let X=[];for(let Y of n(J)){let Z=null;if(Y.type==="link")Z={$type:"app.bsky.richtext.facet#link",uri:Y.value};else if(Y.type==="tag")Z={$type:"app.bsky.richtext.facet#tag",tag:Y.value};else if(Y.type==="mention"){let $=await this.resolveHandle(Y.value);if($)Z={$type:"app.bsky.richtext.facet#mention",did:$}}if(Z)X.push({index:{byteStart:Y.byteStart,byteEnd:Y.byteEnd},features:[Z]})}return X}async resolveHandle(J){try{let X=new URL(`${this.service}/xrpc/com.atproto.identity.resolveHandle`);return X.searchParams.set("handle",J),(await this.request(X,{})).did||null}catch{return null}}async uploadBlob(J,X,Y){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(X.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":Y,authorization:`Bearer ${J.accessToken}`},body:new Uint8Array(X)})).blob}async listAuthoredPosts(J,X={}){let Y=J.did||J.handle;if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!Y)throw Error("Bluesky identity DID or handle is required.");let Z=new URL(`${this.service}/xrpc/com.atproto.repo.listRecords`);if(Z.searchParams.set("repo",Y),Z.searchParams.set("collection","app.bsky.feed.post"),Z.searchParams.set("limit",String(Math.min(Math.max(X.limit||100,1),100))),X.cursor)Z.searchParams.set("cursor",X.cursor);let $=await this.request(Z,{headers:{authorization:`Bearer ${J.accessToken}`}});return{cursor:$.cursor,posts:($.records||[]).filter((N)=>N?.uri).map((N)=>({uri:N.uri,cid:N.cid,text:N.value?.text,postedAt:N.value?.createdAt,url:J.handle?this.toPostUrl(J.handle,N.uri):void 0}))}}async deletePost(J,X){if(!J.accessToken)throw Error("Bluesky access token is missing for this identity.");let{repo:Y,collection:Z,rkey:$}=a(X.uri);await this.post("/xrpc/com.atproto.repo.deleteRecord",{repo:Y,collection:Z,rkey:$},{authorization:`Bearer ${J.accessToken}`})}async post(J,X,Y={}){return await this.request(new URL(`${this.service}${J}`),{method:"POST",headers:{...X===void 0?{}:{"content-type":"application/json"},...Y},...X===void 0?{}:{body:JSON.stringify(X)}})}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new b(`Bluesky API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}toPostUrl(J,X){let Y=X.split("/").pop();return`https://bsky.app/profile/${J}/post/${Y}`}}import{fetcher as k}from"@stacksjs/api";import{config as j}from"@stacksjs/config";class v extends M{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let J={clientId:j.services.facebook?.clientId??"",clientSecret:j.services.facebook?.clientSecret??"",redirectUrl:j.services.facebook?.redirectUrl??"",scopes:j.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.getState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(","),state:J,response_type:"code"}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();this.validateConfig();let $=await k.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:X,client_secret:Y,redirect_uri:Z,code:J}).toString()}`);if($.data.error)throw Error(`Facebook OAuth error: ${$.data.error.message}`);return $.data.access_token}async getUserByToken(J){let X=await k.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:J,fields:"id,name,email,picture"}).toString()}`);return{id:X.data.id,nickname:null,name:X.data.name,email:X.data.email??null,avatar:X.data.picture?.data.url??null,token:J,raw:X.data}}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("Facebook client ID not provided");if(!X)throw new F("Facebook client secret not provided");if(!Y)throw new F("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as A}from"@stacksjs/api";import{config as L}from"@stacksjs/config";class I extends M{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let J={clientId:this.clientId||(L.services.github?.clientId??""),clientSecret:this.clientSecret||(L.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(L.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:L.services.github?.scopes??["read:user","user:email"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.resolveState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();this.validateConfig();let $=await A.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:X,client_secret:Y,code:J,redirect_uri:Z});if($.data.error)throw Error(`GitHub OAuth error: ${$.data.error_description}`);return $.data.access_token}async getUserByToken(J){let[X,Y]=await Promise.all([A.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${J}`}).get(`${this.apiUrl}/user`),A.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${J}`}).get(`${this.apiUrl}/user/emails`)]),Z=this.pickEmail(Y.data);return{id:X.data.id.toString(),nickname:X.data.login,name:X.data.name??X.data.login,email:Z?.email??X.data.email??null,emailVerified:Z?Z.verified:null,avatar:X.data.avatar_url,token:J,raw:X.data}}pickEmail(J){if(!Array.isArray(J)||J.length===0)return null;let X=J.find((Z)=>Z.primary&&Z.verified),Y=J.find((Z)=>Z.verified);return X??Y??J.find((Z)=>Z.primary)??J[0]??null}getEmail(J){return this.pickEmail(J)?.email??null}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("GitHub client ID not provided");if(!X)throw new F("GitHub client secret not provided");if(!Y)throw new F("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as h}from"@stacksjs/api";import{config as U}from"@stacksjs/config";class E extends M{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let J={clientId:this.clientId||(U.services.google?.clientId??""),clientSecret:this.clientSecret||(U.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(U.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:U.services.google?.scopes??["openid","email"]};return this.setScopes(J.scopes),J}async getAuthUrl(){let J=this.resolveState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();this.validateConfig();let $=await h.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:X,client_secret:Y,code:J,redirect_uri:Z,grant_type:"authorization_code"});if($.data.error)throw Error(`Google OAuth error: ${$.data.error_description}`);return $.data.access_token}async getUserByToken(J){let X=await h.withHeaders({Authorization:`Bearer ${J}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:X.data.id,nickname:X.data.given_name,name:X.data.name,email:X.data.email,emailVerified:typeof X.data.verified_email==="boolean"?X.data.verified_email:null,avatar:X.data.picture,token:J,raw:X.data}}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("Google client ID not provided");if(!X)throw new F("Google client secret not provided");if(!Y)throw new F("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}class P extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class r{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(J={}){this.graphVersion=J.graphVersion||"v21.0",this.authBase=J.authBase||"https://www.facebook.com",this.graphBase=J.graphBase||"https://graph.facebook.com"}getAuthUrl(J){let X=new URLSearchParams({client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(","),state:J.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${X.toString()}`}async exchangeCode(J){let X=new URLSearchParams({client_id:J.clientId,client_secret:J.clientSecret,redirect_uri:J.redirectUrl,code:J.code}),Y=await this.graph(`/oauth/access_token?${X.toString()}`,{method:"GET"});if(!Y.access_token)throw new P("Facebook did not return an access token.",400,JSON.stringify(Y));return{accessToken:Y.access_token,expiresIn:Y.expires_in}}async resolveAccount(J){let X=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:J}),Y=await this.graph(`/me/accounts?${X.toString()}`,{method:"GET"}),Z=(Y.data||[]).find((N)=>N.instagram_business_account?.id),$=Z?.instagram_business_account;if(!$?.id||!Z?.access_token)throw new P("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(Y));return{igUserId:$.id,username:$.username,pageAccessToken:Z.access_token}}async publish(J,X){if(!J.accessToken)throw Error("Instagram access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("Instagram account id is required to publish.");let Z=X.media?.[0];if(!Z?.url)throw Error("Instagram requires an image to post.");if(X.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let $=await this.graph(`/${Y}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:Z.url,caption:X.text,access_token:J.accessToken}).toString()});if(!$.id)throw new P("Instagram did not return a media container id.",400,JSON.stringify($));let N=await this.graph(`/${Y}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:$.id,access_token:J.accessToken}).toString()}),z=await this.graph(`/${N.id}?fields=permalink&access_token=${encodeURIComponent(J.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:N.id,url:z?.permalink}}async timeline(J,X={}){return{items:[]}}async graph(J,X){let Y=await fetch(`${this.graphBase}/${this.graphVersion}${J}`,X),Z=await Y.text(),$={};try{$=Z?JSON.parse(Z):{}}catch{$={}}if(!Y.ok||$?.error){let N=$?.error?.message||Z||Y.statusText;throw new P(`Instagram API failed (${Y.status}): ${N}`,Y.status,Z)}return $}}class D extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class i{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(J={}){this.apiVersion=J.apiVersion||"202405",this.authBase=J.authBase||"https://www.linkedin.com",this.apiBase=J.apiBase||"https://api.linkedin.com"}getAuthUrl(J){let X=new URLSearchParams({response_type:"code",client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(" "),state:J.state});return`${this.authBase}/oauth/v2/authorization?${X.toString()}`}async exchangeCode(J){let X=new URLSearchParams({grant_type:"authorization_code",code:J.code,redirect_uri:J.redirectUrl,client_id:J.clientId,client_secret:J.clientSecret}),Y=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:X.toString()});if(!Y.access_token)throw new D("LinkedIn did not return an access token.",400,JSON.stringify(Y));return{accessToken:Y.access_token,expiresIn:Y.expires_in,scope:Y.scope}}async getProfile(J){if(!J)throw Error("LinkedIn access token is required.");let X=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${J}`}});if(!X.sub)throw new D("LinkedIn profile is missing a subject id.",400,JSON.stringify(X));return{sub:X.sub,name:X.name,picture:X.picture}}async publish(J,X){if(!J.accessToken)throw Error("LinkedIn access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("LinkedIn member URN is required to publish.");if(X.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let Z={author:Y,commentary:t(X.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(X.external)Z.content={article:{source:X.external.uri,title:X.external.title,description:X.external.description||""}};let $=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${J.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(Z)}),N=await $.text();if(!$.ok)throw new D(`LinkedIn API failed (${$.status}): ${N||$.statusText}`,$.status,N);let z=$.headers.get("x-restli-id")||$.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:z,url:z?`https://www.linkedin.com/feed/update/${z}`:void 0}}async timeline(J,X={}){return{items:[]}}async listAuthoredPosts(J,X={}){if(!J.accessToken)throw Error("LinkedIn access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("LinkedIn member URN is required to list posts.");let Z=Math.min(Math.max(X.limit||50,1),100),$=Number(X.cursor||0)||0,N=new URL(`${this.apiBase}/rest/posts`);N.searchParams.set("q","author"),N.searchParams.set("author",Y),N.searchParams.set("count",String(Z)),N.searchParams.set("start",String($));let z;try{z=await this.request(N.toString(),{headers:{authorization:`Bearer ${J.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}})}catch(W){if(W instanceof D&&(W.status===401||W.status===403))throw new D("LinkedIn will not list this account's posts \u2014 the Posts author finder needs the r_member_social permission, which this app does not hold.",W.status,W.body);throw W}let G=(z.elements||[]).filter((W)=>W?.id).map((W)=>({uri:String(W.id),text:W.commentary,postedAt:W.createdAt?new Date(W.createdAt).toISOString():void 0,url:`https://www.linkedin.com/feed/update/${W.id}`}));return{cursor:G.length===Z?String($+Z):void 0,posts:G}}async deletePost(J,X){if(!J.accessToken)throw Error("LinkedIn access token is missing for this identity.");let Y=String(X.uri||"").trim();if(!Y)throw Error("A LinkedIn post URN is required to delete a post.");let Z=await fetch(`${this.apiBase}/rest/posts/${encodeURIComponent(Y)}`,{method:"DELETE",headers:{authorization:`Bearer ${J.accessToken}`,"linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"}});if(!Z.ok&&Z.status!==404){let $=await Z.text().catch(()=>"");throw new D(`LinkedIn API failed (${Z.status}): ${$||Z.statusText}`,Z.status,$)}}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new D(`LinkedIn API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}}function t(J){return J.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class g extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="MastodonApiError"}get isAuthError(){return this.status===401||this.status===403}}function e(J){let X=String(J||"").trim().replace(/\/+$/,"");if(!X)throw Error("Mastodon instance URL is required.");let Y=/^https?:\/\//i.test(X)?X:`https://${X}`;try{let Z=new URL(Y);return`${Z.protocol}//${Z.host}`}catch{throw Error("Mastodon instance URL is invalid.")}}class JJ{provider="mastodon";characterLimit=500;instanceOf(J){return e(J.did||"")}tokenOf(J){if(!J.accessToken)throw Error("Mastodon access token is missing for this identity.");return J.accessToken}async verifyCredentials(J){let X=await this.request(`${this.instanceOf(J)}/api/v1/accounts/verify_credentials`,{headers:{authorization:`Bearer ${this.tokenOf(J)}`}});return{accountId:X.id,username:X.username,displayName:X.display_name||void 0,url:X.url}}async uploadMedia(J,X,Y,Z){let $=new FormData;if($.set("file",new Blob([new Uint8Array(X)],{type:Y||"image/jpeg"}),"upload"),Z)$.set("description",Z);return(await this.request(`${this.instanceOf(J)}/api/v2/media`,{method:"POST",headers:{authorization:`Bearer ${this.tokenOf(J)}`},body:$})).id}async publish(J,X){let Y=this.instanceOf(J),Z=this.tokenOf(J);if(X.text.length>this.characterLimit)throw Error(`Mastodon posts must be ${this.characterLimit} characters or fewer.`);let $=[];for(let G of(X.media||[]).slice(0,4)){let{bytes:W,mimeType:K}=G;if(!W?.length&&G.url){let Q=await fetch(G.url);if(!Q.ok)continue;W=new Uint8Array(await Q.arrayBuffer()),K=K||Q.headers.get("content-type")||"image/jpeg"}if(W?.length)$.push(await this.uploadMedia(J,W,K||"image/jpeg",G.altText))}let N={status:X.text,visibility:"public"};if($.length)N.media_ids=$;if(X.reply?.parent?.uri)N.in_reply_to_id=X.reply.parent.uri;let z=await this.request(`${Y}/api/v1/statuses`,{method:"POST",headers:{authorization:`Bearer ${Z}`,"content-type":"application/json"},body:JSON.stringify(N)});return{provider:this.provider,uri:z.id,cid:z.id,url:z.url||z.uri}}async timeline(J,X={}){return{items:[]}}async listAuthoredPosts(J,X={}){let Y=this.instanceOf(J),{accountId:Z}=await this.verifyCredentials(J),$=new URL(`${Y}/api/v1/accounts/${encodeURIComponent(Z)}/statuses`);if($.searchParams.set("limit",String(Math.min(Math.max(X.limit||40,1),40))),$.searchParams.set("exclude_reblogs","true"),X.cursor)$.searchParams.set("max_id",X.cursor);let z=(await this.request($.toString(),{headers:{authorization:`Bearer ${this.tokenOf(J)}`}})||[]).filter((G)=>G?.id).map((G)=>({uri:G.id,cid:G.id,text:G.content,postedAt:G.created_at,url:G.url}));return{cursor:z.length?z[z.length-1]?.uri:void 0,posts:z}}async deletePost(J,X){let Y=String(X.cid||XJ(X.uri)||"").trim();if(!Y)throw Error("A status id is required to delete a post.");await this.request(`${this.instanceOf(J)}/api/v1/statuses/${encodeURIComponent(Y)}`,{method:"DELETE",headers:{authorization:`Bearer ${this.tokenOf(J)}`}})}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new g(`Mastodon API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}}function XJ(J){return String(J||"").replace(/\/+$/,"").split("/").pop()||""}class O extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class YJ{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(J={}){this.graphVersion=J.graphVersion||"v1.0",this.authBase=J.authBase||"https://threads.net",this.graphBase=J.graphBase||"https://graph.threads.net"}getAuthUrl(J){let X=new URLSearchParams({client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(","),response_type:"code",state:J.state});return`${this.authBase}/oauth/authorize?${X.toString()}`}async exchangeCode(J){let X=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:J.clientId,client_secret:J.clientSecret,grant_type:"authorization_code",redirect_uri:J.redirectUrl,code:J.code}).toString()}),Y=await X.text(),Z={};try{Z=Y?JSON.parse(Y):{}}catch{Z={}}if(!X.ok||Z?.error||!Z?.access_token){let $=Z?.error_message||Z?.error?.message||Y||X.statusText;throw new O(`Threads token exchange failed (${X.status}): ${$}`,X.status,Y)}return{accessToken:Z.access_token,userId:Z.user_id!=null?String(Z.user_id):void 0,expiresIn:Z.expires_in}}async resolveAccount(J){let X=new URLSearchParams({fields:"id,username",access_token:J}),Y=await this.graph(`/me?${X.toString()}`,{method:"GET"});if(!Y.id)throw new O("Could not resolve the Threads account for this token.",400,JSON.stringify(Y));return{threadsUserId:Y.id,username:Y.username,accessToken:J}}async publish(J,X){if(!J.accessToken)throw Error("Threads access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("Threads account id is required to publish.");if(X.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let Z=X.media?.[0],$=new URLSearchParams({text:X.text,access_token:J.accessToken});if(Z?.url)$.set("media_type","IMAGE"),$.set("image_url",Z.url);else $.set("media_type","TEXT");let N=await this.graph(`/${Y}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:$.toString()});if(!N.id)throw new O("Threads did not return a media container id.",400,JSON.stringify(N));let z=await this.graph(`/${Y}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:N.id,access_token:J.accessToken}).toString()});if(!z.id)throw new O("Threads did not return a published post id.",400,JSON.stringify(z));let G=await this.graph(`/${z.id}?fields=permalink&access_token=${encodeURIComponent(J.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:z.id,url:G?.permalink}}async timeline(J,X={}){return{items:[]}}async graph(J,X){let Y=await fetch(`${this.graphBase}/${this.graphVersion}${J}`,X),Z=await Y.text(),$={};try{$=Z?JSON.parse(Z):{}}catch{$={}}if(!Y.ok||$?.error){let N=$?.error?.message||Z||Y.statusText;throw new O(`Threads API failed (${Y.status}): ${N}`,Y.status,Z)}return $}}import{Buffer as ZJ}from"buffer";import{createHash as $J,randomBytes as NJ}from"crypto";import{fetcher as f}from"@stacksjs/api";import{config as q}from"@stacksjs/config";class x extends M{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let J={clientId:q.services.twitter?.clientId??"",clientSecret:q.services.twitter?.clientSecret??"",redirectUrl:q.services.twitter?.redirectUrl??"",scopes:q.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(J.scopes),J}generateCodeVerifier(){return NJ(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(J){return $J("sha256").update(J).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let J=this.getState(),{clientId:X,redirectUrl:Y,scopes:Z}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let $=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:X,redirect_uri:Y,scope:Z.join(" "),state:J,response_type:"code",code_challenge:$,code_challenge_method:"S256"}).toString()}`}async getAccessToken(J){let{clientId:X,clientSecret:Y,redirectUrl:Z}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let $=ZJ.from(`${X}:${Y}`).toString("base64"),N=await f.withHeaders({Authorization:`Basic ${$}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:J,grant_type:"authorization_code",redirect_uri:Z,code_verifier:this.codeVerifier});if(N.data.error)throw Error(`Twitter OAuth error: ${N.data.error_description}`);return N.data.access_token}async getUserByToken(J){let X=await f.withHeaders({Authorization:`Bearer ${J}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:X.data.id,nickname:X.data.username,name:X.data.name,email:X.data.email??null,avatar:X.data.profile_image_url??null,token:J,raw:X.data}}validateConfig(){let{clientId:J,clientSecret:X,redirectUrl:Y}=this.getConfig();if(!J)throw new F("Twitter client ID not provided");if(!X)throw new F("Twitter client secret not provided");if(!Y)throw new F("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class H extends Error{status;body;constructor(J,X,Y){super(J);this.status=X;this.body=Y;this.name="TwitterApiError"}get isAuthError(){return this.status===401||this.status===403}}function m(J){let X="";for(let Y of J)X+=String.fromCharCode(Y);return btoa(X).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}class zJ{provider="twitter";characterLimit=280;apiBase;authorizeBase;constructor(J={}){this.apiBase=J.apiBase||"https://api.twitter.com",this.authorizeBase=J.authorizeBase||"https://twitter.com"}async createAuthorization(J){let X=m(crypto.getRandomValues(new Uint8Array(32))),Y=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(X)),Z=m(new Uint8Array(Y)),$=new URLSearchParams({response_type:"code",client_id:J.clientId,redirect_uri:J.redirectUrl,scope:J.scopes.join(" "),state:J.state,code_challenge:Z,code_challenge_method:"S256"});return{url:`${this.authorizeBase}/i/oauth2/authorize?${$.toString()}`,codeVerifier:X}}async exchangeCode(J){return this.tokenRequest(new URLSearchParams({grant_type:"authorization_code",code:J.code,redirect_uri:J.redirectUrl,code_verifier:J.codeVerifier,client_id:J.clientId}),J.clientId,J.clientSecret)}async refreshAccessToken(J){return this.tokenRequest(new URLSearchParams({grant_type:"refresh_token",refresh_token:J.refreshToken,client_id:J.clientId}),J.clientId,J.clientSecret)}async getProfile(J){let X=await this.request(`${this.apiBase}/2/users/me?user.fields=username,name`,{headers:{authorization:`Bearer ${J}`}});if(!X.data?.id||!X.data.username)throw new H("Twitter did not return the authenticated user.",400,JSON.stringify(X));return{id:X.data.id,username:X.data.username,name:X.data.name}}async uploadMedia(J,X,Y){let Z=new FormData;Z.set("media",new Blob([new Uint8Array(X)],{type:Y||"image/jpeg"})),Z.set("media_category","tweet_image");let $=await this.request(`${this.apiBase}/2/media/upload`,{method:"POST",headers:{authorization:`Bearer ${J}`},body:Z}),N=$.data?.id||$.media_id_string||$.id;if(!N)throw new H("Twitter did not return a media id.",400,JSON.stringify($));return N}async publish(J,X){if(!J.accessToken)throw Error("Twitter access token is missing for this identity.");if(X.text.length>this.characterLimit)throw Error(`Twitter posts must be ${this.characterLimit} characters or fewer.`);let Y=[],Z=X.media?.[0];if(Z){let{bytes:G,mimeType:W}=Z;if(!G?.length&&Z.url){let K=await fetch(Z.url);if(K.ok)G=new Uint8Array(await K.arrayBuffer()),W=W||K.headers.get("content-type")||"image/jpeg"}if(G?.length)Y.push(await this.uploadMedia(J.accessToken,G,W||"image/jpeg"))}let $={text:X.text};if(Y.length)$.media={media_ids:Y};if(X.reply?.parent?.uri)$.reply={in_reply_to_tweet_id:X.reply.parent.uri};let N=await this.request(`${this.apiBase}/2/tweets`,{method:"POST",headers:{authorization:`Bearer ${J.accessToken}`,"content-type":"application/json"},body:JSON.stringify($)}),z=N.data?.id;if(!z)throw new H("Twitter did not return a tweet id.",400,JSON.stringify(N));return{provider:this.provider,uri:z,cid:z,url:J.handle?`https://x.com/${J.handle}/status/${z}`:`https://x.com/i/web/status/${z}`}}async timeline(J,X={}){return{items:[]}}async listAuthoredPosts(J,X={}){if(!J.accessToken)throw Error("Twitter access token is missing for this identity.");let Y=J.did;if(!Y)throw Error("Twitter user id is required to list posts.");let Z=new URL(`${this.apiBase}/2/users/${encodeURIComponent(Y)}/tweets`);if(Z.searchParams.set("max_results",String(Math.min(Math.max(X.limit||100,5),100))),Z.searchParams.set("tweet.fields","created_at"),X.cursor)Z.searchParams.set("pagination_token",X.cursor);let $=await this.request(Z.toString(),{headers:{authorization:`Bearer ${J.accessToken}`}});return{cursor:$.meta?.next_token,posts:($.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(J,X){if(!J.accessToken)throw Error("Twitter access token is missing for this identity.");let Y=String(X.uri||"").trim();if(!Y)throw Error("A tweet id is required to delete a post.");let Z=await this.request(`${this.apiBase}/2/tweets/${encodeURIComponent(Y)}`,{method:"DELETE",headers:{authorization:`Bearer ${J.accessToken}`}});if(Z.data&&Z.data.deleted===!1)throw new H(`X refused to delete tweet ${Y}.`,400,JSON.stringify(Z))}async tokenRequest(J,X,Y){let Z={"content-type":"application/x-www-form-urlencoded"};if(Y)Z.authorization=`Basic ${btoa(`${X}:${Y}`)}`;let $=await this.request(`${this.apiBase}/2/oauth2/token`,{method:"POST",headers:Z,body:J.toString()});if(!$.access_token)throw new H("Twitter did not return an access token.",400,JSON.stringify($));return{accessToken:$.access_token,refreshToken:$.refresh_token,expiresIn:$.expires_in,scope:$.scope}}async request(J,X){let Y=await fetch(J,X),Z=await Y.text();if(!Y.ok)throw new H(`Twitter API failed (${Y.status}): ${Z||Y.statusText}`,Y.status,Z);return Z?JSON.parse(Z):{}}}import{config as GJ}from"@stacksjs/config";var w=["clientId","clientSecret","redirectUrl"],R=Object.freeze({google:{name:"google",label:"Google",driver:E,required:w,postCallback:!1},github:{name:"github",label:"GitHub",driver:I,required:w,postCallback:!1},facebook:{name:"facebook",label:"Facebook",driver:v,required:w,postCallback:!1},twitter:{name:"twitter",label:"X",driver:x,required:w,postCallback:!1},apple:{name:"apple",label:"Apple",driver:C,required:["clientId","teamId","keyId","privateKey","redirectUrl"],postCallback:!0}});function u(J){return GJ?.services?.[J]}function WJ(J){return typeof J==="string"&&J in R}function l(J){if(!WJ(J))return!1;let X=u(J);if(!X)return!1;return R[J].required.every((Y)=>Boolean(X[Y]))}function sJ(){return Object.keys(R).filter(l).map((J)=>R[J])}function rJ(J){if(!l(J))return null;let X=R[J],Y=u(J)??{};return new X.driver({clientSecret:"",...Y,clientId:String(Y.clientId??""),redirectUrl:String(Y.redirectUrl??"")})}class FJ{accessToken;refreshToken;expiresIn;approvedScopes;constructor(J,X=null,Y=null,Z=[]){this.accessToken=J;this.refreshToken=X;this.expiresIn=Y;this.approvedScopes=Z}}function eJ(J){return typeof J?.deletePost==="function"}function JX(J){return typeof J?.listAuthoredPosts==="function"}import{buildSessionHandoffUrl as KJ}from"@stacksjs/composables";function y(J,X=[]){if(!J)return!1;if(J.startsWith("//"))return!1;if(J.startsWith("/"))return!0;try{let Y=new URL(J);if(Y.protocol!=="http:"&&Y.protocol!=="https:")return!1;return X.includes(Y.host)}catch{return!1}}function ZX(J,X={}){let Y=X.redirectTo??"/";if(!y(Y,X.allowedHosts??[]))throw Error(`[socials] refusing to hand a session to ${Y}: relative paths are always allowed; an absolute URL needs its host in allowedHosts.`);return new Response(null,{status:302,headers:{Location:KJ(Y,J),"Cache-Control":"no-store","Referrer-Policy":"no-referrer"}})}function $X(J,X={}){let Y=X.redirectTo??"/login";if(!y(Y,X.allowedHosts??[]))throw Error(`[socials] refusing to redirect to ${Y}`);let Z=Y.includes("?")?"&":"?",$=`${Y}${Z}social_error=${encodeURIComponent(J)}`;return new Response(null,{status:302,headers:{Location:$,"Cache-Control":"no-store"}})}export{JX as supportsEnumeration,eJ as supportsDeletion,rJ as socialProvider,ZX as socialHandoffRedirect,$X as socialHandoffFailureRedirect,a as parseAtUri,e as normalizeInstance,WJ as isSocialProviderName,l as isSocialProviderConfigured,y as isSafeHandoffTarget,t as escapeLinkedInText,n as detectFacetCandidates,sJ as configuredSocialProviders,zJ as TwitterPublishingDriver,x as TwitterProvider,H as TwitterApiError,FJ as Token,YJ as ThreadsPublishingDriver,O as ThreadsApiError,R as SOCIAL_PROVIDERS,JJ as MastodonPublishingDriver,g as MastodonApiError,i as LinkedInPublishingDriver,D as LinkedInApiError,c as InvalidStateException,r as InstagramPublishingDriver,P as InstagramApiError,E as GoogleProvider,I as GitHubProvider,v as FacebookProvider,F as ConfigException,s as BlueskyPublishingDriver,b as BlueskyApiError,C as AppleProvider,M as AbstractProvider};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/socials",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.296",
|
|
6
6
|
"description": "A simple and elegant social authentication package for Stacks.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -58,7 +58,10 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"better-dx": "^0.2.17",
|
|
61
|
-
"@stacksjs/error-handling": "0.70.
|
|
62
|
-
"@stacksjs/router": "0.70.
|
|
61
|
+
"@stacksjs/error-handling": "0.70.296",
|
|
62
|
+
"@stacksjs/router": "0.70.296"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@stacksjs/composables": "0.70.296"
|
|
63
66
|
}
|
|
64
67
|
}
|