@stacksjs/socials 0.70.44 → 0.70.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/dist/abstract.d.ts +3 -0
- package/dist/drivers/apple.d.ts +56 -0
- package/dist/drivers/github.d.ts +1 -0
- package/dist/drivers/index.d.ts +4 -0
- package/dist/drivers/instagram.d.ts +51 -0
- package/dist/drivers/linkedin.d.ts +62 -0
- package/dist/drivers/threads.d.ts +52 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -1
- package/dist/token.d.ts +7 -0
- package/dist/types.d.ts +38 -1
- package/package.json +3 -3
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/abstract.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare abstract class AbstractProvider implements ProviderInterface {
|
|
|
14
14
|
protected scopeSeparator: string;
|
|
15
15
|
protected _stateless: boolean;
|
|
16
16
|
protected _usesPKCE: boolean;
|
|
17
|
+
protected _state: string | null;
|
|
17
18
|
protected user: SocialUser | null;
|
|
18
19
|
constructor(config: ProviderConfig);
|
|
19
20
|
abstract getAuthUrl(): Promise<string>;
|
|
@@ -27,6 +28,8 @@ export declare abstract class AbstractProvider implements ProviderInterface {
|
|
|
27
28
|
setScopes(scopes: string | string[]): this;
|
|
28
29
|
getScopes(): string[];
|
|
29
30
|
setRedirectUrl(url: string): this;
|
|
31
|
+
withState(state: string): this;
|
|
32
|
+
protected resolveState(): string;
|
|
30
33
|
protected usesState(): boolean;
|
|
31
34
|
validateState(expected: string | null | undefined, actual: string | null | undefined): boolean;
|
|
32
35
|
protected isStateless(): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AbstractProvider } from '../abstract';
|
|
2
|
+
import type { AppleIdTokenClaims, ProviderInterface, SocialUser } from '../types';
|
|
3
|
+
import type { ProviderConfig } from '../abstract';
|
|
4
|
+
/**
|
|
5
|
+
* Apple replaces the static client secret with a signed JWT, so its
|
|
6
|
+
* provider config carries the signing inputs instead of clientSecret
|
|
7
|
+
* (which may be left '').
|
|
8
|
+
*/
|
|
9
|
+
export declare interface AppleProviderConfig extends ProviderConfig {
|
|
10
|
+
teamId?: string
|
|
11
|
+
keyId?: string
|
|
12
|
+
privateKey?: string
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sign in with Apple (OAuth2 / OIDC).
|
|
16
|
+
*
|
|
17
|
+
* Apple deviates from the other providers in three ways, all handled
|
|
18
|
+
* here so callers keep the same getAuthUrl/getAccessToken/getUserByToken
|
|
19
|
+
* contract:
|
|
20
|
+
*
|
|
21
|
+
* 1. There is no static client secret. Apple requires a short-lived JWT
|
|
22
|
+
* signed with an ES256 private key (.p8) issued in the developer
|
|
23
|
+
* portal, scoped by team ID and key ID. `generateClientSecret()`
|
|
24
|
+
* builds one per token exchange.
|
|
25
|
+
* 2. There is no userinfo endpoint. Identity comes from the `id_token`
|
|
26
|
+
* returned by the token endpoint, so `getAccessToken()` returns the
|
|
27
|
+
* id_token (not the access token — there is nothing to spend it on),
|
|
28
|
+
* and `getUserByToken()` decodes its claims. The id_token arrives
|
|
29
|
+
* straight from Apple's token endpoint over TLS, which OIDC Core
|
|
30
|
+
* 3.1.3.7 accepts in place of local signature verification; iss, aud
|
|
31
|
+
* and exp are still validated.
|
|
32
|
+
* 3. When scopes are requested (they are by default: name + email),
|
|
33
|
+
* Apple mandates `response_mode=form_post` — the callback arrives as
|
|
34
|
+
* a cross-site POST, not a GET. Applications must register a POST
|
|
35
|
+
* callback route and use a cookie jar that survives cross-site POSTs
|
|
36
|
+
* (SameSite=None) if they carry state in cookies.
|
|
37
|
+
*
|
|
38
|
+
* Note that Apple only transmits the user's name (and only on the very
|
|
39
|
+
* first authorization) as a `user` JSON field in the form_post body —
|
|
40
|
+
* it is never part of the id_token. Reading it is the application's
|
|
41
|
+
* job; `SocialUser.name` from this driver is therefore always ''.
|
|
42
|
+
*/
|
|
43
|
+
export declare class AppleProvider extends AbstractProvider implements ProviderInterface {
|
|
44
|
+
protected baseUrl: string;
|
|
45
|
+
protected teamId: string;
|
|
46
|
+
protected keyId: string;
|
|
47
|
+
protected privateKey: string;
|
|
48
|
+
constructor(providerConfig: AppleProviderConfig);
|
|
49
|
+
getAuthUrl(): Promise<string>;
|
|
50
|
+
getAccessToken(code: string): Promise<string>;
|
|
51
|
+
getUserByToken(token: string): Promise<SocialUser>;
|
|
52
|
+
protected generateClientSecret(): string;
|
|
53
|
+
protected decodeIdToken(idToken: string): AppleIdTokenClaims;
|
|
54
|
+
protected validateConfig(): void;
|
|
55
|
+
protected getTokenUrl(): string;
|
|
56
|
+
}
|
package/dist/drivers/github.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare class GitHubProvider extends AbstractProvider implements Provider
|
|
|
6
6
|
getAuthUrl(): Promise<string>;
|
|
7
7
|
getAccessToken(code: string): Promise<string>;
|
|
8
8
|
getUserByToken(token: string): Promise<SocialUser>;
|
|
9
|
+
protected pickEmail(emails: GitHubEmail[]): GitHubEmail | null;
|
|
9
10
|
protected getEmail(emails: GitHubEmail[]): string | null;
|
|
10
11
|
protected validateConfig(): void;
|
|
11
12
|
protected getTokenUrl(): string;
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PublishedPost, PublishPostInput, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
|
|
2
|
+
export declare interface InstagramDriverOptions {
|
|
3
|
+
graphVersion?: string
|
|
4
|
+
authBase?: string
|
|
5
|
+
graphBase?: string
|
|
6
|
+
}
|
|
7
|
+
export declare interface InstagramAuthUrlInput {
|
|
8
|
+
clientId: string
|
|
9
|
+
redirectUrl: string
|
|
10
|
+
scopes: string[]
|
|
11
|
+
state: string
|
|
12
|
+
}
|
|
13
|
+
export declare interface InstagramTokenExchangeInput {
|
|
14
|
+
clientId: string
|
|
15
|
+
clientSecret: string
|
|
16
|
+
redirectUrl: string
|
|
17
|
+
code: string
|
|
18
|
+
}
|
|
19
|
+
export declare interface InstagramAccount {
|
|
20
|
+
igUserId: string
|
|
21
|
+
username?: string
|
|
22
|
+
pageAccessToken: string
|
|
23
|
+
}
|
|
24
|
+
export declare class InstagramApiError extends Error {
|
|
25
|
+
public status: number;
|
|
26
|
+
public body: string;
|
|
27
|
+
constructor(message: string, status: number, body: string);
|
|
28
|
+
get isAuthError(): boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Publishing driver for Instagram Business/Creator accounts via the Facebook
|
|
32
|
+
* Graph API. Auth is Facebook Login (OAuth 2.0). Publishing is the documented
|
|
33
|
+
* two-step flow: create a media container, then publish it.
|
|
34
|
+
*
|
|
35
|
+
* Instagram does not allow text-only posts — every post requires an image (or
|
|
36
|
+
* video) reachable at a public URL, supplied via `post.media`.
|
|
37
|
+
*/
|
|
38
|
+
export declare class InstagramPublishingDriver implements SocialPublishingDriver {
|
|
39
|
+
readonly provider: 'instagram';
|
|
40
|
+
characterLimit: number;
|
|
41
|
+
protected graphVersion: string;
|
|
42
|
+
protected authBase: string;
|
|
43
|
+
protected graphBase: string;
|
|
44
|
+
constructor(options?: InstagramDriverOptions);
|
|
45
|
+
getAuthUrl(input: InstagramAuthUrlInput): string;
|
|
46
|
+
exchangeCode(input: InstagramTokenExchangeInput): Promise<{ accessToken: string, expiresIn?: number }>;
|
|
47
|
+
resolveAccount(accessToken: string): Promise<InstagramAccount>;
|
|
48
|
+
publish(identity: SocialIdentityCredentials, post: PublishPostInput): Promise<PublishedPost>;
|
|
49
|
+
timeline(_identity: SocialIdentityCredentials, _query?: TimelineQuery): Promise<TimelineResult>;
|
|
50
|
+
protected graph<T>(path: string, init: RequestInit): Promise<T>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PublishedPost, PublishPostInput, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Escape the reserved characters of LinkedIn's "little text" commentary format
|
|
4
|
+
* so the literal text renders as typed. Without this, characters like `(` `)`
|
|
5
|
+
* `@` `#` cause the share to be rejected with a 400.
|
|
6
|
+
*/
|
|
7
|
+
export declare function escapeLinkedInText(text: string): string;
|
|
8
|
+
export declare interface LinkedInDriverOptions {
|
|
9
|
+
apiVersion?: string
|
|
10
|
+
authBase?: string
|
|
11
|
+
apiBase?: string
|
|
12
|
+
}
|
|
13
|
+
export declare interface LinkedInAuthUrlInput {
|
|
14
|
+
clientId: string
|
|
15
|
+
redirectUrl: string
|
|
16
|
+
scopes: string[]
|
|
17
|
+
state: string
|
|
18
|
+
}
|
|
19
|
+
export declare interface LinkedInTokenExchangeInput {
|
|
20
|
+
clientId: string
|
|
21
|
+
clientSecret: string
|
|
22
|
+
redirectUrl: string
|
|
23
|
+
code: string
|
|
24
|
+
}
|
|
25
|
+
export declare interface LinkedInTokenResponse {
|
|
26
|
+
accessToken: string
|
|
27
|
+
expiresIn?: number
|
|
28
|
+
scope?: string
|
|
29
|
+
}
|
|
30
|
+
export declare interface LinkedInProfile {
|
|
31
|
+
sub: string
|
|
32
|
+
name?: string
|
|
33
|
+
picture?: string
|
|
34
|
+
}
|
|
35
|
+
export declare class LinkedInApiError extends Error {
|
|
36
|
+
public status: number;
|
|
37
|
+
public body: string;
|
|
38
|
+
constructor(message: string, status: number, body: string);
|
|
39
|
+
get isAuthError(): boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Publishing driver for LinkedIn member shares.
|
|
43
|
+
*
|
|
44
|
+
* Auth is OAuth 2.0 (authorization code). Posting uses the versioned REST
|
|
45
|
+
* `/rest/posts` endpoint with the `w_member_social` scope. Unlike Bluesky,
|
|
46
|
+
* LinkedIn has no app-password, so a token is always obtained via OAuth (or
|
|
47
|
+
* pasted in from a prior OAuth grant).
|
|
48
|
+
*/
|
|
49
|
+
export declare class LinkedInPublishingDriver implements SocialPublishingDriver {
|
|
50
|
+
readonly provider: 'linkedin';
|
|
51
|
+
characterLimit: number;
|
|
52
|
+
protected apiVersion: string;
|
|
53
|
+
protected authBase: string;
|
|
54
|
+
protected apiBase: string;
|
|
55
|
+
constructor(options?: LinkedInDriverOptions);
|
|
56
|
+
getAuthUrl(input: LinkedInAuthUrlInput): string;
|
|
57
|
+
exchangeCode(input: LinkedInTokenExchangeInput): Promise<LinkedInTokenResponse>;
|
|
58
|
+
getProfile(accessToken: string): Promise<LinkedInProfile>;
|
|
59
|
+
publish(identity: SocialIdentityCredentials, post: PublishPostInput): Promise<PublishedPost>;
|
|
60
|
+
timeline(_identity: SocialIdentityCredentials, _query?: TimelineQuery): Promise<TimelineResult>;
|
|
61
|
+
protected request<T>(url: string, init: RequestInit): Promise<T>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PublishedPost, PublishPostInput, SocialIdentityCredentials, SocialPublishingDriver, TimelineQuery, TimelineResult } from '../types';
|
|
2
|
+
export declare interface ThreadsDriverOptions {
|
|
3
|
+
graphVersion?: string
|
|
4
|
+
authBase?: string
|
|
5
|
+
graphBase?: string
|
|
6
|
+
}
|
|
7
|
+
export declare interface ThreadsAuthUrlInput {
|
|
8
|
+
clientId: string
|
|
9
|
+
redirectUrl: string
|
|
10
|
+
scopes: string[]
|
|
11
|
+
state: string
|
|
12
|
+
}
|
|
13
|
+
export declare interface ThreadsTokenExchangeInput {
|
|
14
|
+
clientId: string
|
|
15
|
+
clientSecret: string
|
|
16
|
+
redirectUrl: string
|
|
17
|
+
code: string
|
|
18
|
+
}
|
|
19
|
+
export declare interface ThreadsAccount {
|
|
20
|
+
threadsUserId: string
|
|
21
|
+
username?: string
|
|
22
|
+
accessToken: string
|
|
23
|
+
}
|
|
24
|
+
export declare class ThreadsApiError extends Error {
|
|
25
|
+
public status: number;
|
|
26
|
+
public body: string;
|
|
27
|
+
constructor(message: string, status: number, body: string);
|
|
28
|
+
get isAuthError(): boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Publishing driver for Threads (Meta) via the Threads Graph API. Auth is the
|
|
32
|
+
* Threads OAuth flow (`threads.net/oauth/authorize`, scopes `threads_basic` +
|
|
33
|
+
* `threads_content_publish`). Publishing is the documented two-step flow:
|
|
34
|
+
* create a media container, then publish it.
|
|
35
|
+
*
|
|
36
|
+
* Unlike Instagram, Threads allows text-only posts — `post.media` is optional
|
|
37
|
+
* and, when present, the container is created as an `IMAGE` instead of `TEXT`.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ThreadsPublishingDriver implements SocialPublishingDriver {
|
|
40
|
+
readonly provider: 'threads';
|
|
41
|
+
characterLimit: number;
|
|
42
|
+
protected graphVersion: string;
|
|
43
|
+
protected authBase: string;
|
|
44
|
+
protected graphBase: string;
|
|
45
|
+
constructor(options?: ThreadsDriverOptions);
|
|
46
|
+
getAuthUrl(input: ThreadsAuthUrlInput): string;
|
|
47
|
+
exchangeCode(input: ThreadsTokenExchangeInput): Promise<{ accessToken: string, userId?: string, expiresIn?: number }>;
|
|
48
|
+
resolveAccount(accessToken: string): Promise<ThreadsAccount>;
|
|
49
|
+
publish(identity: SocialIdentityCredentials, post: PublishPostInput): Promise<PublishedPost>;
|
|
50
|
+
timeline(_identity: SocialIdentityCredentials, _query?: TimelineQuery): Promise<TimelineResult>;
|
|
51
|
+
protected graph<T>(path: string, init: RequestInit): Promise<T>;
|
|
52
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var o=import.meta.require;class d extends Error{status;body;constructor(r,t,s){super(r);this.status=t;this.body=s;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}class S{provider="bluesky";characterLimit=300;service;constructor(r={}){this.service=r.service||"https://bsky.social"}async createSession(r){let t=r.identifier.trim(),s=r.password.trim();if(!t)throw Error("Bluesky identifier is required.");if(!s)throw Error("Bluesky app password is required.");let i=await this.post("/xrpc/com.atproto.server.createSession",{identifier:t,password:s}),a=await this.getProfile({did:i.did,handle:i.handle,accessToken:i.accessJwt,refreshToken:i.refreshJwt}).catch(()=>{return});return{did:i.did,handle:i.handle,displayName:a?.displayName,accessJwt:i.accessJwt,refreshJwt:i.refreshJwt}}async refreshSession(r){if(!r)throw Error("Bluesky refresh token is required.");let t=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${r}`});return{did:t.did,handle:t.handle,accessJwt:t.accessJwt,refreshJwt:t.refreshJwt}}async publish(r,t){let s=r.did||r.handle;if(!r.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!s)throw Error("Bluesky identity DID or handle is required.");if(t.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let i={$type:"app.bsky.feed.post",text:t.text,createdAt:t.scheduledAt||new Date().toISOString()};if(t.langs?.length)i.langs=t.langs;if(t.external)i.embed={$type:"app.bsky.embed.external",external:{uri:t.external.uri,title:t.external.title,description:t.external.description||""}};let a=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:s,collection:"app.bsky.feed.post",record:i},{authorization:`Bearer ${r.accessToken}`});return{provider:this.provider,uri:a.uri,cid:a.cid,url:this.toPostUrl(r.handle,a.uri)}}async timeline(r,t={}){if(!r.accessToken)throw Error("Bluesky access token is missing for this identity.");let s=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(s.searchParams.set("limit",String(Math.min(Math.max(t.limit||30,1),100))),t.cursor)s.searchParams.set("cursor",t.cursor);let i=await this.request(s,{headers:{authorization:`Bearer ${r.accessToken}`}});return{cursor:i.cursor,items:(i.feed||[]).flatMap((a)=>{let h=a.post;if(!h?.uri||!h.author?.handle)return[];return[{uri:h.uri,authorHandle:h.author.handle,authorName:h.author.displayName,body:h.record?.text||"",postedAt:h.record?.createdAt||new Date().toISOString(),likeCount:h.likeCount||0,repostCount:h.repostCount||0,replyCount:h.replyCount||0}]})}}async getProfile(r){if(!r.accessToken)throw Error("Bluesky access token is missing for this identity.");let t=r.did||r.handle;if(!t)throw Error("Bluesky identity DID or handle is required.");let s=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return s.searchParams.set("actor",t),await this.request(s,{headers:{authorization:`Bearer ${r.accessToken}`}})}async post(r,t,s={}){return await this.request(new URL(`${this.service}${r}`),{method:"POST",headers:{...t===void 0?{}:{"content-type":"application/json"},...s},...t===void 0?{}:{body:JSON.stringify(t)}})}async request(r,t){let s=await fetch(r,t),i=await s.text();if(!s.ok)throw new d(`Bluesky API failed (${s.status}): ${i||s.statusText}`,s.status,i);return i?JSON.parse(i):{}}toPostUrl(r,t){let s=t.split("/").pop();return`https://bsky.app/profile/${r}/post/${s}`}}import{fetcher as P}from"@stacksjs/api";import{config as w}from"@stacksjs/config";class n{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;user=null;constructor(r){this.clientId=r.clientId,this.clientSecret=r.clientSecret,this.redirectUrl=r.redirectUrl}getCodeFields(r=null){let t={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())t.state=r;if(this.usesPKCE())t.code_challenge=this.getCodeChallenge(),t.code_challenge_method=this.getCodeChallengeMethod();return{...t,...this.parameters}}formatScopes(r,t){return r.join(t)}async userFromToken(r){return{...await this.getUserByToken(r),token:r}}scopes(r){let t=Array.isArray(r)?r:[r];return this._scopes=[...new Set([...this._scopes,...t])],this}setScopes(r){let t=Array.isArray(r)?r:[r];return this._scopes=[...new Set(t)],this}getScopes(){return this._scopes}setRedirectUrl(r){if(typeof r!=="string"||r.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let t;try{t=new URL(r)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${r}`)}if(t.protocol!=="https:"&&t.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${t.protocol}`);return this.redirectUrl=r,this}usesState(){return!this._stateless}validateState(r,t){if(typeof r!=="string"||typeof t!=="string")return!1;if(r.length===0||t.length===0)return!1;if(r.length!==t.length)return!1;try{let{timingSafeEqual:s}=o("crypto");return s(Buffer.from(r,"utf8"),Buffer.from(t,"utf8"))}catch{let s=0;for(let i=0;i<r.length;i++)s|=r.charCodeAt(i)^t.charCodeAt(i);return s===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let r=new Uint8Array(32);return crypto.getRandomValues(r),Array.from(r).map((t)=>t.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let r=new Uint8Array(48);return crypto.getRandomValues(r),Array.from(r).map((t)=>t.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let t=new TextEncoder().encode(this.getCodeVerifier()),s=await crypto.subtle.digest("SHA-256",t),{Buffer:i}=await import("buffer");return i.from(new Uint8Array(s)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(r){return this.parameters=r,this}buildAuthUrlFromBase(r,t){let s=new URLSearchParams(this.getCodeFields(t));return`${r}?${s.toString()}`}}class u extends Error{constructor(r){super(r);this.name="ConfigException"}}class k extends n{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let r={clientId:w.services.facebook?.clientId??"",clientSecret:w.services.facebook?.clientSecret??"",redirectUrl:w.services.facebook?.redirectUrl??"",scopes:w.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(r.scopes),r}async getAuthUrl(){let r=this.getState(),{clientId:t,redirectUrl:s,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:t,redirect_uri:s,scope:i.join(","),state:r,response_type:"code"}).toString()}`}async getAccessToken(r){let{clientId:t,clientSecret:s,redirectUrl:i}=this.getConfig();this.validateConfig();let a=await P.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:t,client_secret:s,redirect_uri:i,code:r}).toString()}`);if(a.data.error)throw Error(`Facebook OAuth error: ${a.data.error.message}`);return a.data.access_token}async getUserByToken(r){let t=await P.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:r,fields:"id,name,email,picture"}).toString()}`);return{id:t.data.id,nickname:null,name:t.data.name,email:t.data.email??null,avatar:t.data.picture?.data.url??null,token:r,raw:t.data}}validateConfig(){let{clientId:r,clientSecret:t,redirectUrl:s}=this.getConfig();if(!r)throw new u("Facebook client ID not provided");if(!t)throw new u("Facebook client secret not provided");if(!s)throw new u("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as y}from"@stacksjs/api";import{config as e}from"@stacksjs/config";class $ extends n{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let r={clientId:e.services.github?.clientId??"",clientSecret:e.services.github?.clientSecret??"",redirectUrl:e.services.github?.redirectUrl??"",scopes:e.services.github?.scopes??["read:user","user:email"]};return this.setScopes(r.scopes),r}async getAuthUrl(){let r=this.getState(),{clientId:t,redirectUrl:s,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:t,redirect_uri:s,scope:i.join(" "),state:r,response_type:"code"}).toString()}`}async getAccessToken(r){let{clientId:t,clientSecret:s,redirectUrl:i}=this.getConfig();this.validateConfig();let a=await y.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:t,client_secret:s,code:r,redirect_uri:i});if(a.data.error)throw Error(`GitHub OAuth error: ${a.data.error_description}`);return a.data.access_token}async getUserByToken(r){let[t,s]=await Promise.all([y.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${r}`}).get(`${this.apiUrl}/user`),y.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${r}`}).get(`${this.apiUrl}/user/emails`)]);return{id:t.data.id.toString(),nickname:t.data.login,name:t.data.name??t.data.login,email:this.getEmail(s.data)??t.data.email??null,avatar:t.data.avatar_url,token:r,raw:t.data}}getEmail(r){if(!Array.isArray(r)||r.length===0)return null;let t=r.find((i)=>i.primary&&i.verified),s=r.find((i)=>i.verified);return(t||s||r.find((i)=>i.primary)||r[0])?.email??null}validateConfig(){let{clientId:r,clientSecret:t,redirectUrl:s}=this.getConfig();if(!r)throw new u("GitHub client ID not provided");if(!t)throw new u("GitHub client secret not provided");if(!s)throw new u("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as T}from"@stacksjs/api";import{config as l}from"@stacksjs/config";class J extends n{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let r={clientId:l.services.google?.clientId??"",clientSecret:l.services.google?.clientSecret??"",redirectUrl:l.services.google?.redirectUrl??"",scopes:l.services.google?.scopes??["openid","email"]};return this.setScopes(r.scopes),r}async getAuthUrl(){let r=this.getState(),{clientId:t,redirectUrl:s,scopes:i}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:t,redirect_uri:s,scope:i.join(" "),state:r,response_type:"code",access_type:"offline",prompt:"consent"}).toString()}`}async getAccessToken(r){let{clientId:t,clientSecret:s,redirectUrl:i}=this.getConfig();this.validateConfig();let a=await T.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:t,client_secret:s,code:r,redirect_uri:i,grant_type:"authorization_code"});if(a.data.error)throw Error(`Google OAuth error: ${a.data.error_description}`);return a.data.access_token}async getUserByToken(r){let t=await T.withHeaders({Authorization:`Bearer ${r}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:t.data.id,nickname:t.data.given_name,name:t.data.name,email:t.data.email,avatar:t.data.picture,token:r,raw:t.data}}validateConfig(){let{clientId:r,clientSecret:t,redirectUrl:s}=this.getConfig();if(!r)throw new u("Google client ID not provided");if(!t)throw new u("Google client secret not provided");if(!s)throw new u("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}import{Buffer as D}from"buffer";import{createHash as x,randomBytes as N}from"crypto";import{fetcher as B}from"@stacksjs/api";import{config as m}from"@stacksjs/config";class b extends n{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let r={clientId:m.services.twitter?.clientId??"",clientSecret:m.services.twitter?.clientSecret??"",redirectUrl:m.services.twitter?.redirectUrl??"",scopes:m.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(r.scopes),r}generateCodeVerifier(){return N(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(r){return x("sha256").update(r).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let r=this.getState(),{clientId:t,redirectUrl:s,scopes:i}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let a=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:t,redirect_uri:s,scope:i.join(" "),state:r,response_type:"code",code_challenge:a,code_challenge_method:"S256"}).toString()}`}async getAccessToken(r){let{clientId:t,clientSecret:s,redirectUrl:i}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let a=D.from(`${t}:${s}`).toString("base64"),h=await B.withHeaders({Authorization:`Basic ${a}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:r,grant_type:"authorization_code",redirect_uri:i,code_verifier:this.codeVerifier});if(h.data.error)throw Error(`Twitter OAuth error: ${h.data.error_description}`);return h.data.access_token}async getUserByToken(r){let t=await B.withHeaders({Authorization:`Bearer ${r}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:t.data.id,nickname:t.data.username,name:t.data.name,email:t.data.email??null,avatar:t.data.profile_image_url??null,token:r,raw:t.data}}validateConfig(){let{clientId:r,clientSecret:t,redirectUrl:s}=this.getConfig();if(!r)throw new u("Twitter client ID not provided");if(!t)throw new u("Twitter client secret not provided");if(!s)throw new u("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}export{b as TwitterProvider,J as GoogleProvider,$ as GitHubProvider,k as FacebookProvider,S as BlueskyPublishingDriver,d as BlueskyApiError};
|
|
2
|
+
var q=import.meta.require;class N{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;_state=null;user=null;constructor(w){this.clientId=w.clientId,this.clientSecret=w.clientSecret,this.redirectUrl=w.redirectUrl}getCodeFields(w=null){let h={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())h.state=w;if(this.usesPKCE())h.code_challenge=this.getCodeChallenge(),h.code_challenge_method=this.getCodeChallengeMethod();return{...h,...this.parameters}}formatScopes(w,h){return w.join(h)}async userFromToken(w){return{...await this.getUserByToken(w),token:w}}scopes(w){let h=Array.isArray(w)?w:[w];return this._scopes=[...new Set([...this._scopes,...h])],this}setScopes(w){let h=Array.isArray(w)?w:[w];return this._scopes=[...new Set(h)],this}getScopes(){return this._scopes}setRedirectUrl(w){if(typeof w!=="string"||w.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let h;try{h=new URL(w)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${w}`)}if(h.protocol!=="https:"&&h.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${h.protocol}`);return this.redirectUrl=w,this}withState(w){if(typeof w!=="string"||w.length===0)throw Error("[socials] withState requires a non-empty string");return this._state=w,this}resolveState(){return this._state??this.getState()}usesState(){return!this._stateless}validateState(w,h){if(typeof w!=="string"||typeof h!=="string")return!1;if(w.length===0||h.length===0)return!1;if(w.length!==h.length)return!1;try{let{timingSafeEqual:P}=q("crypto");return P(Buffer.from(w,"utf8"),Buffer.from(h,"utf8"))}catch{let P=0;for(let S=0;S<w.length;S++)P|=w.charCodeAt(S)^h.charCodeAt(S);return P===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let w=new Uint8Array(32);return crypto.getRandomValues(w),Array.from(w).map((h)=>h.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let w=new Uint8Array(48);return crypto.getRandomValues(w),Array.from(w).map((h)=>h.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let h=new TextEncoder().encode(this.getCodeVerifier()),P=await crypto.subtle.digest("SHA-256",h),{Buffer:S}=await import("buffer");return S.from(new Uint8Array(P)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(w){return this.parameters=w,this}buildAuthUrlFromBase(w,h){let P=new URLSearchParams(this.getCodeFields(h));return`${w}?${P.toString()}`}}import{Buffer as Y}from"buffer";import{createPrivateKey as V,sign as x}from"crypto";import{config as u}from"@stacksjs/config";class L extends Error{constructor(w="Invalid state"){super(w);this.name="InvalidStateException"}}class B extends Error{constructor(w){super(w);this.name="ConfigException"}}class v extends N{baseUrl="https://appleid.apple.com";teamId="";keyId="";privateKey="";constructor(w){super(w);this.teamId=w.teamId??"",this.keyId=w.keyId??"",this.privateKey=w.privateKey??""}getConfig(){let w={clientId:this.clientId||(u.services.apple?.clientId??""),teamId:this.teamId||(u.services.apple?.teamId??""),keyId:this.keyId||(u.services.apple?.keyId??""),privateKey:(this.privateKey||(u.services.apple?.privateKey??"")).replace(/\\n/g,`
|
|
3
|
+
`),redirectUrl:this.redirectUrl||(u.services.apple?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:u.services.apple?.scopes??["name","email"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.resolveState(),{clientId:h,redirectUrl:P,scopes:S}=this.getConfig();this.validateConfig();let $={client_id:h,redirect_uri:P,scope:S.join(" "),state:w,response_type:"code",...this.parameters};if(S.length>0)$.response_mode="form_post";return`${this.baseUrl}/auth/authorize?${new URLSearchParams($).toString()}`}async getAccessToken(w){let{clientId:h,redirectUrl:P}=this.getConfig();this.validateConfig();let S=await fetch(`${this.baseUrl}/auth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:w,redirect_uri:P,client_id:h,client_secret:this.generateClientSecret()})}),$=await S.json();if(!S.ok||$.error)throw Error(`Apple OAuth error: ${$.error_description??$.error??`HTTP ${S.status}`}`);if(!$.id_token)throw Error("Apple OAuth error: token response contained no id_token");return $.id_token}async getUserByToken(w){let{clientId:h}=this.getConfig(),P=this.decodeIdToken(w),S=P.iss===this.baseUrl,$=Array.isArray(P.aud)?P.aud.includes(h):P.aud===h,J=typeof P.exp==="number"&&P.exp*1000>Date.now();if(!S||!$||!J)throw Error("Apple OAuth error: id_token claims failed validation (iss/aud/exp)");if(!P.sub)throw Error("Apple OAuth error: id_token has no subject");let D=typeof P.email==="string"?P.email:null,R=null;if(P.email_verified===!0||P.email_verified==="true")R=!0;else if(P.email_verified===!1||P.email_verified==="false")R=!1;return{id:String(P.sub),nickname:null,name:"",email:D,emailVerified:R,avatar:null,token:w,raw:P}}generateClientSecret(){let{clientId:w,teamId:h,keyId:P,privateKey:S}=this.getConfig(),$=Math.floor(Date.now()/1000),J={alg:"ES256",kid:P,typ:"JWT"},D={iss:h,iat:$,exp:$+3600,aud:this.baseUrl,sub:w},R=`${this.base64urlJson(J)}.${this.base64urlJson(D)}`,_;try{_=V(S)}catch(X){throw new B(`Apple private key could not be parsed: ${X instanceof Error?X.message:String(X)}`)}let m=x("sha256",Y.from(R),{key:_,dsaEncoding:"ieee-p1363"});return`${R}.${m.toString("base64url")}`}decodeIdToken(w){let h=w.split(".");if(h.length!==3)throw Error("Apple OAuth error: malformed id_token");return JSON.parse(Y.from(h[1],"base64url").toString("utf8"))}base64urlJson(w){return Y.from(JSON.stringify(w)).toString("base64url")}validateConfig(){let{clientId:w,teamId:h,keyId:P,privateKey:S,redirectUrl:$}=this.getConfig();if(!w)throw new B("Apple client ID (Service ID) not provided");if(!h)throw new B("Apple team ID not provided");if(!P)throw new B("Apple key ID not provided");if(!S)throw new B("Apple private key not provided");if(!$)throw new B("Apple redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/auth/token`}}class T extends Error{status;body;constructor(w,h,P){super(w);this.status=h;this.body=P;this.name="BlueskyApiError"}get isAuthError(){return this.status===400||this.status===401||this.status===403}}class U{provider="bluesky";characterLimit=300;service;constructor(w={}){this.service=w.service||"https://bsky.social"}async createSession(w){let h=w.identifier.trim(),P=w.password.trim();if(!h)throw Error("Bluesky identifier is required.");if(!P)throw Error("Bluesky app password is required.");let S=await this.post("/xrpc/com.atproto.server.createSession",{identifier:h,password:P}),$=await this.getProfile({did:S.did,handle:S.handle,accessToken:S.accessJwt,refreshToken:S.refreshJwt}).catch(()=>{return});return{did:S.did,handle:S.handle,displayName:$?.displayName,accessJwt:S.accessJwt,refreshJwt:S.refreshJwt}}async refreshSession(w){if(!w)throw Error("Bluesky refresh token is required.");let h=await this.post("/xrpc/com.atproto.server.refreshSession",void 0,{authorization:`Bearer ${w}`});return{did:h.did,handle:h.handle,accessJwt:h.accessJwt,refreshJwt:h.refreshJwt}}async publish(w,h){let P=w.did||w.handle;if(!w.accessToken)throw Error("Bluesky access token is missing for this identity.");if(!P)throw Error("Bluesky identity DID or handle is required.");if(h.text.length>this.characterLimit)throw Error(`Bluesky posts must be ${this.characterLimit} characters or fewer.`);let S={$type:"app.bsky.feed.post",text:h.text,createdAt:h.scheduledAt||new Date().toISOString()};if(h.langs?.length)S.langs=h.langs;if(h.external)S.embed={$type:"app.bsky.embed.external",external:{uri:h.external.uri,title:h.external.title,description:h.external.description||""}};let $=await this.post("/xrpc/com.atproto.repo.createRecord",{repo:P,collection:"app.bsky.feed.post",record:S},{authorization:`Bearer ${w.accessToken}`});return{provider:this.provider,uri:$.uri,cid:$.cid,url:this.toPostUrl(w.handle,$.uri)}}async timeline(w,h={}){if(!w.accessToken)throw Error("Bluesky access token is missing for this identity.");let P=new URL(`${this.service}/xrpc/app.bsky.feed.getTimeline`);if(P.searchParams.set("limit",String(Math.min(Math.max(h.limit||30,1),100))),h.cursor)P.searchParams.set("cursor",h.cursor);let S=await this.request(P,{headers:{authorization:`Bearer ${w.accessToken}`}});return{cursor:S.cursor,items:(S.feed||[]).flatMap(($)=>{let J=$.post;if(!J?.uri||!J.author?.handle)return[];return[{uri:J.uri,authorHandle:J.author.handle,authorName:J.author.displayName,authorAvatar:J.author.avatar,postUrl:this.toPostUrl(J.author.handle,J.uri),body:J.record?.text||"",postedAt:J.record?.createdAt||new Date().toISOString(),likeCount:J.likeCount||0,repostCount:J.repostCount||0,replyCount:J.replyCount||0}]})}}async getProfile(w){if(!w.accessToken)throw Error("Bluesky access token is missing for this identity.");let h=w.did||w.handle;if(!h)throw Error("Bluesky identity DID or handle is required.");let P=new URL(`${this.service}/xrpc/app.bsky.actor.getProfile`);return P.searchParams.set("actor",h),await this.request(P,{headers:{authorization:`Bearer ${w.accessToken}`}})}async post(w,h,P={}){return await this.request(new URL(`${this.service}${w}`),{method:"POST",headers:{...h===void 0?{}:{"content-type":"application/json"},...P},...h===void 0?{}:{body:JSON.stringify(h)}})}async request(w,h){let P=await fetch(w,h),S=await P.text();if(!P.ok)throw new T(`Bluesky API failed (${P.status}): ${S||P.statusText}`,P.status,S);return S?JSON.parse(S):{}}toPostUrl(w,h){let P=h.split("/").pop();return`https://bsky.app/profile/${w}/post/${P}`}}import{fetcher as W}from"@stacksjs/api";import{config as Q}from"@stacksjs/config";class l extends N{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let w={clientId:Q.services.facebook?.clientId??"",clientSecret:Q.services.facebook?.clientSecret??"",redirectUrl:Q.services.facebook?.redirectUrl??"",scopes:Q.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.getState(),{clientId:h,redirectUrl:P,scopes:S}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:h,redirect_uri:P,scope:S.join(","),state:w,response_type:"code"}).toString()}`}async getAccessToken(w){let{clientId:h,clientSecret:P,redirectUrl:S}=this.getConfig();this.validateConfig();let $=await W.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:h,client_secret:P,redirect_uri:S,code:w}).toString()}`);if($.data.error)throw Error(`Facebook OAuth error: ${$.data.error.message}`);return $.data.access_token}async getUserByToken(w){let h=await W.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:w,fields:"id,name,email,picture"}).toString()}`);return{id:h.data.id,nickname:null,name:h.data.name,email:h.data.email??null,avatar:h.data.picture?.data.url??null,token:w,raw:h.data}}validateConfig(){let{clientId:w,clientSecret:h,redirectUrl:P}=this.getConfig();if(!w)throw new B("Facebook client ID not provided");if(!h)throw new B("Facebook client secret not provided");if(!P)throw new B("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as Z}from"@stacksjs/api";import{config as F}from"@stacksjs/config";class C extends N{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let w={clientId:this.clientId||(F.services.github?.clientId??""),clientSecret:this.clientSecret||(F.services.github?.clientSecret??""),redirectUrl:this.redirectUrl||(F.services.github?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:F.services.github?.scopes??["read:user","user:email"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.resolveState(),{clientId:h,redirectUrl:P,scopes:S}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:h,redirect_uri:P,scope:S.join(" "),state:w,response_type:"code",...this.parameters}).toString()}`}async getAccessToken(w){let{clientId:h,clientSecret:P,redirectUrl:S}=this.getConfig();this.validateConfig();let $=await Z.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:h,client_secret:P,code:w,redirect_uri:S});if($.data.error)throw Error(`GitHub OAuth error: ${$.data.error_description}`);return $.data.access_token}async getUserByToken(w){let[h,P]=await Promise.all([Z.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${w}`}).get(`${this.apiUrl}/user`),Z.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${w}`}).get(`${this.apiUrl}/user/emails`)]),S=this.pickEmail(P.data);return{id:h.data.id.toString(),nickname:h.data.login,name:h.data.name??h.data.login,email:S?.email??h.data.email??null,emailVerified:S?S.verified:null,avatar:h.data.avatar_url,token:w,raw:h.data}}pickEmail(w){if(!Array.isArray(w)||w.length===0)return null;let h=w.find((S)=>S.primary&&S.verified),P=w.find((S)=>S.verified);return h??P??w.find((S)=>S.primary)??w[0]??null}getEmail(w){return this.pickEmail(w)?.email??null}validateConfig(){let{clientId:w,clientSecret:h,redirectUrl:P}=this.getConfig();if(!w)throw new B("GitHub client ID not provided");if(!h)throw new B("GitHub client secret not provided");if(!P)throw new B("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as K}from"@stacksjs/api";import{config as G}from"@stacksjs/config";class k extends N{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let w={clientId:this.clientId||(G.services.google?.clientId??""),clientSecret:this.clientSecret||(G.services.google?.clientSecret??""),redirectUrl:this.redirectUrl||(G.services.google?.redirectUrl??""),scopes:this._scopes.length>0?this._scopes:G.services.google?.scopes??["openid","email"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.resolveState(),{clientId:h,redirectUrl:P,scopes:S}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:h,redirect_uri:P,scope:S.join(" "),state:w,response_type:"code",access_type:"offline",prompt:"consent",...this.parameters}).toString()}`}async getAccessToken(w){let{clientId:h,clientSecret:P,redirectUrl:S}=this.getConfig();this.validateConfig();let $=await K.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:h,client_secret:P,code:w,redirect_uri:S,grant_type:"authorization_code"});if($.data.error)throw Error(`Google OAuth error: ${$.data.error_description}`);return $.data.access_token}async getUserByToken(w){let h=await K.withHeaders({Authorization:`Bearer ${w}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:h.data.id,nickname:h.data.given_name,name:h.data.name,email:h.data.email,emailVerified:typeof h.data.verified_email==="boolean"?h.data.verified_email:null,avatar:h.data.picture,token:w,raw:h.data}}validateConfig(){let{clientId:w,clientSecret:h,redirectUrl:P}=this.getConfig();if(!w)throw new B("Google client ID not provided");if(!h)throw new B("Google client secret not provided");if(!P)throw new B("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}class O extends Error{status;body;constructor(w,h,P){super(w);this.status=h;this.body=P;this.name="InstagramApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class A{provider="instagram";characterLimit=2200;graphVersion;authBase;graphBase;constructor(w={}){this.graphVersion=w.graphVersion||"v21.0",this.authBase=w.authBase||"https://www.facebook.com",this.graphBase=w.graphBase||"https://graph.facebook.com"}getAuthUrl(w){let h=new URLSearchParams({client_id:w.clientId,redirect_uri:w.redirectUrl,scope:w.scopes.join(","),state:w.state,response_type:"code"});return`${this.authBase}/${this.graphVersion}/dialog/oauth?${h.toString()}`}async exchangeCode(w){let h=new URLSearchParams({client_id:w.clientId,client_secret:w.clientSecret,redirect_uri:w.redirectUrl,code:w.code}),P=await this.graph(`/oauth/access_token?${h.toString()}`,{method:"GET"});if(!P.access_token)throw new O("Facebook did not return an access token.",400,JSON.stringify(P));return{accessToken:P.access_token,expiresIn:P.expires_in}}async resolveAccount(w){let h=new URLSearchParams({fields:"name,access_token,instagram_business_account{id,username}",access_token:w}),P=await this.graph(`/me/accounts?${h.toString()}`,{method:"GET"}),S=(P.data||[]).find((J)=>J.instagram_business_account?.id),$=S?.instagram_business_account;if(!$?.id||!S?.access_token)throw new O("No Instagram Business account is linked to your Facebook Pages.",400,JSON.stringify(P));return{igUserId:$.id,username:$.username,pageAccessToken:S.access_token}}async publish(w,h){if(!w.accessToken)throw Error("Instagram access token is missing for this identity.");let P=w.did;if(!P)throw Error("Instagram account id is required to publish.");let S=h.media?.[0];if(!S?.url)throw Error("Instagram requires an image to post.");if(h.text.length>this.characterLimit)throw Error(`Instagram captions must be ${this.characterLimit} characters or fewer.`);let $=await this.graph(`/${P}/media`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({image_url:S.url,caption:h.text,access_token:w.accessToken}).toString()});if(!$.id)throw new O("Instagram did not return a media container id.",400,JSON.stringify($));let J=await this.graph(`/${P}/media_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:$.id,access_token:w.accessToken}).toString()}),D=await this.graph(`/${J.id}?fields=permalink&access_token=${encodeURIComponent(w.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:J.id,url:D?.permalink}}async timeline(w,h={}){return{items:[]}}async graph(w,h){let P=await fetch(`${this.graphBase}/${this.graphVersion}${w}`,h),S=await P.text(),$={};try{$=S?JSON.parse(S):{}}catch{$={}}if(!P.ok||$?.error){let J=$?.error?.message||S||P.statusText;throw new O(`Instagram API failed (${P.status}): ${J}`,P.status,S)}return $}}class M extends Error{status;body;constructor(w,h,P){super(w);this.status=h;this.body=P;this.name="LinkedInApiError"}get isAuthError(){return this.status===401||this.status===403}}class E{provider="linkedin";characterLimit=3000;apiVersion;authBase;apiBase;constructor(w={}){this.apiVersion=w.apiVersion||"202405",this.authBase=w.authBase||"https://www.linkedin.com",this.apiBase=w.apiBase||"https://api.linkedin.com"}getAuthUrl(w){let h=new URLSearchParams({response_type:"code",client_id:w.clientId,redirect_uri:w.redirectUrl,scope:w.scopes.join(" "),state:w.state});return`${this.authBase}/oauth/v2/authorization?${h.toString()}`}async exchangeCode(w){let h=new URLSearchParams({grant_type:"authorization_code",code:w.code,redirect_uri:w.redirectUrl,client_id:w.clientId,client_secret:w.clientSecret}),P=await this.request(`${this.authBase}/oauth/v2/accessToken`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:h.toString()});if(!P.access_token)throw new M("LinkedIn did not return an access token.",400,JSON.stringify(P));return{accessToken:P.access_token,expiresIn:P.expires_in,scope:P.scope}}async getProfile(w){if(!w)throw Error("LinkedIn access token is required.");let h=await this.request(`${this.apiBase}/v2/userinfo`,{headers:{authorization:`Bearer ${w}`}});if(!h.sub)throw new M("LinkedIn profile is missing a subject id.",400,JSON.stringify(h));return{sub:h.sub,name:h.name,picture:h.picture}}async publish(w,h){if(!w.accessToken)throw Error("LinkedIn access token is missing for this identity.");let P=w.did;if(!P)throw Error("LinkedIn member URN is required to publish.");if(h.text.length>this.characterLimit)throw Error(`LinkedIn posts must be ${this.characterLimit} characters or fewer.`);let S={author:P,commentary:j(h.text),visibility:"PUBLIC",distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1};if(h.external)S.content={article:{source:h.external.uri,title:h.external.title,description:h.external.description||""}};let $=await fetch(`${this.apiBase}/rest/posts`,{method:"POST",headers:{authorization:`Bearer ${w.accessToken}`,"content-type":"application/json","linkedin-version":this.apiVersion,"x-restli-protocol-version":"2.0.0"},body:JSON.stringify(S)}),J=await $.text();if(!$.ok)throw new M(`LinkedIn API failed (${$.status}): ${J||$.statusText}`,$.status,J);let D=$.headers.get("x-restli-id")||$.headers.get("x-linkedin-id")||"";return{provider:this.provider,uri:D,url:D?`https://www.linkedin.com/feed/update/${D}`:void 0}}async timeline(w,h={}){return{items:[]}}async request(w,h){let P=await fetch(w,h),S=await P.text();if(!P.ok)throw new M(`LinkedIn API failed (${P.status}): ${S||P.statusText}`,P.status,S);return S?JSON.parse(S):{}}}function j(w){return w.replace(/[\\|{}@[\]()<>#*_~]/g,"\\$&")}class z extends Error{status;body;constructor(w,h,P){super(w);this.status=h;this.body=P;this.name="ThreadsApiError"}get isAuthError(){return this.status===401||this.status===403||this.status===190}}class I{provider="threads";characterLimit=500;graphVersion;authBase;graphBase;constructor(w={}){this.graphVersion=w.graphVersion||"v1.0",this.authBase=w.authBase||"https://threads.net",this.graphBase=w.graphBase||"https://graph.threads.net"}getAuthUrl(w){let h=new URLSearchParams({client_id:w.clientId,redirect_uri:w.redirectUrl,scope:w.scopes.join(","),response_type:"code",state:w.state});return`${this.authBase}/oauth/authorize?${h.toString()}`}async exchangeCode(w){let h=await fetch(`${this.graphBase}/oauth/access_token`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:w.clientId,client_secret:w.clientSecret,grant_type:"authorization_code",redirect_uri:w.redirectUrl,code:w.code}).toString()}),P=await h.text(),S={};try{S=P?JSON.parse(P):{}}catch{S={}}if(!h.ok||S?.error||!S?.access_token){let $=S?.error_message||S?.error?.message||P||h.statusText;throw new z(`Threads token exchange failed (${h.status}): ${$}`,h.status,P)}return{accessToken:S.access_token,userId:S.user_id!=null?String(S.user_id):void 0,expiresIn:S.expires_in}}async resolveAccount(w){let h=new URLSearchParams({fields:"id,username",access_token:w}),P=await this.graph(`/me?${h.toString()}`,{method:"GET"});if(!P.id)throw new z("Could not resolve the Threads account for this token.",400,JSON.stringify(P));return{threadsUserId:P.id,username:P.username,accessToken:w}}async publish(w,h){if(!w.accessToken)throw Error("Threads access token is missing for this identity.");let P=w.did;if(!P)throw Error("Threads account id is required to publish.");if(h.text.length>this.characterLimit)throw Error(`Threads posts must be ${this.characterLimit} characters or fewer.`);let S=h.media?.[0],$=new URLSearchParams({text:h.text,access_token:w.accessToken});if(S?.url)$.set("media_type","IMAGE"),$.set("image_url",S.url);else $.set("media_type","TEXT");let J=await this.graph(`/${P}/threads`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:$.toString()});if(!J.id)throw new z("Threads did not return a media container id.",400,JSON.stringify(J));let D=await this.graph(`/${P}/threads_publish`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({creation_id:J.id,access_token:w.accessToken}).toString()});if(!D.id)throw new z("Threads did not return a published post id.",400,JSON.stringify(D));let R=await this.graph(`/${D.id}?fields=permalink&access_token=${encodeURIComponent(w.accessToken)}`,{method:"GET"}).catch(()=>{return});return{provider:this.provider,uri:D.id,url:R?.permalink}}async timeline(w,h={}){return{items:[]}}async graph(w,h){let P=await fetch(`${this.graphBase}/${this.graphVersion}${w}`,h),S=await P.text(),$={};try{$=S?JSON.parse(S):{}}catch{$={}}if(!P.ok||$?.error){let J=$?.error?.message||S||P.statusText;throw new z(`Threads API failed (${P.status}): ${J}`,P.status,S)}return $}}import{Buffer as y}from"buffer";import{createHash as r,randomBytes as g}from"crypto";import{fetcher as b}from"@stacksjs/api";import{config as H}from"@stacksjs/config";class f extends N{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let w={clientId:H.services.twitter?.clientId??"",clientSecret:H.services.twitter?.clientSecret??"",redirectUrl:H.services.twitter?.redirectUrl??"",scopes:H.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(w.scopes),w}generateCodeVerifier(){return g(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(w){return r("sha256").update(w).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let w=this.getState(),{clientId:h,redirectUrl:P,scopes:S}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let $=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:h,redirect_uri:P,scope:S.join(" "),state:w,response_type:"code",code_challenge:$,code_challenge_method:"S256"}).toString()}`}async getAccessToken(w){let{clientId:h,clientSecret:P,redirectUrl:S}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let $=y.from(`${h}:${P}`).toString("base64"),J=await b.withHeaders({Authorization:`Basic ${$}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:w,grant_type:"authorization_code",redirect_uri:S,code_verifier:this.codeVerifier});if(J.data.error)throw Error(`Twitter OAuth error: ${J.data.error_description}`);return J.data.access_token}async getUserByToken(w){let h=await b.withHeaders({Authorization:`Bearer ${w}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:h.data.id,nickname:h.data.username,name:h.data.name,email:h.data.email??null,avatar:h.data.profile_image_url??null,token:w,raw:h.data}}validateConfig(){let{clientId:w,clientSecret:h,redirectUrl:P}=this.getConfig();if(!w)throw new B("Twitter client ID not provided");if(!h)throw new B("Twitter client secret not provided");if(!P)throw new B("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}class a{accessToken;refreshToken;expiresIn;approvedScopes;constructor(w,h=null,P=null,S=[]){this.accessToken=w;this.refreshToken=h;this.expiresIn=P;this.approvedScopes=S}}export{j as escapeLinkedInText,f as TwitterProvider,a as Token,I as ThreadsPublishingDriver,z as ThreadsApiError,E as LinkedInPublishingDriver,M as LinkedInApiError,L as InvalidStateException,A as InstagramPublishingDriver,O as InstagramApiError,k as GoogleProvider,C as GitHubProvider,l as FacebookProvider,B as ConfigException,U as BlueskyPublishingDriver,T as BlueskyApiError,v as AppleProvider,N as AbstractProvider};
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class Token {
|
|
2
|
+
public accessToken: string;
|
|
3
|
+
public refreshToken?: string | null;
|
|
4
|
+
public expiresIn?: number | null;
|
|
5
|
+
public approvedScopes?: string[];
|
|
6
|
+
constructor(accessToken: string, refreshToken?: string | null, expiresIn?: number | null, approvedScopes?: string[]);
|
|
7
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare interface SocialUser {
|
|
|
7
7
|
nickname: string | null
|
|
8
8
|
name: string
|
|
9
9
|
email: string | null
|
|
10
|
+
emailVerified?: boolean | null
|
|
10
11
|
avatar: string | null
|
|
11
12
|
token: string
|
|
12
13
|
raw?: any
|
|
@@ -57,6 +58,35 @@ export declare interface TwitterUser {
|
|
|
57
58
|
email?: string
|
|
58
59
|
profile_image_url?: string
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Apple-specific token response from https://appleid.apple.com/auth/token
|
|
63
|
+
*/
|
|
64
|
+
export declare interface AppleTokenResponse {
|
|
65
|
+
access_token: string
|
|
66
|
+
token_type: string
|
|
67
|
+
expires_in: number
|
|
68
|
+
refresh_token?: string
|
|
69
|
+
id_token: string
|
|
70
|
+
error?: string
|
|
71
|
+
error_description?: string
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Claims Apple places in the id_token. `email_verified` and
|
|
75
|
+
* `is_private_email` arrive as booleans or the strings 'true'/'false'
|
|
76
|
+
* depending on the API era.
|
|
77
|
+
*/
|
|
78
|
+
export declare interface AppleIdTokenClaims {
|
|
79
|
+
iss: string
|
|
80
|
+
aud: string | string[]
|
|
81
|
+
exp: number
|
|
82
|
+
iat: number
|
|
83
|
+
sub: string
|
|
84
|
+
nonce?: string
|
|
85
|
+
email?: string
|
|
86
|
+
email_verified?: boolean | 'true' | 'false'
|
|
87
|
+
is_private_email?: boolean | 'true' | 'false'
|
|
88
|
+
[key: string]: any
|
|
89
|
+
}
|
|
60
90
|
export declare interface BlueskySessionCredentials {
|
|
61
91
|
identifier: string
|
|
62
92
|
password: string
|
|
@@ -83,6 +113,10 @@ export declare interface PublishPostInput {
|
|
|
83
113
|
title: string
|
|
84
114
|
description?: string
|
|
85
115
|
}
|
|
116
|
+
media?: Array<{
|
|
117
|
+
url: string
|
|
118
|
+
altText?: string
|
|
119
|
+
}>
|
|
86
120
|
}
|
|
87
121
|
export declare interface PublishedPost {
|
|
88
122
|
provider: SocialPublishingProvider
|
|
@@ -100,6 +134,8 @@ export declare interface TimelineResult {
|
|
|
100
134
|
uri: string
|
|
101
135
|
authorHandle: string
|
|
102
136
|
authorName?: string
|
|
137
|
+
authorAvatar?: string
|
|
138
|
+
postUrl?: string
|
|
103
139
|
body: string
|
|
104
140
|
postedAt: string
|
|
105
141
|
likeCount: number
|
|
@@ -119,4 +155,5 @@ export type SocialPublishingProvider = | 'bluesky'
|
|
|
119
155
|
| 'facebook'
|
|
120
156
|
| 'instagram'
|
|
121
157
|
| 'tiktok'
|
|
122
|
-
| 'linkedin'
|
|
158
|
+
| 'linkedin'
|
|
159
|
+
| 'threads';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/socials",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.53",
|
|
5
5
|
"description": "A simple and elegant social authentication package for Stacks.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"better-dx": "^0.2.12",
|
|
51
|
-
"@stacksjs/error-handling": "
|
|
52
|
-
"@stacksjs/router": "
|
|
51
|
+
"@stacksjs/error-handling": "0.70.53",
|
|
52
|
+
"@stacksjs/router": "0.70.53"
|
|
53
53
|
}
|
|
54
54
|
}
|