Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { NoopFlagProvider, UnleashFlagProvider, createFlagProvider, } from "./fl
export { LEGAL_BASE_URL, LEGAL_CONTACTS, LEGAL_FOOTER_LINKS, LEGAL_LINKS, LEGAL_URLS, } from "./legal";
export { NoopNotificationProvider, createNotificationProvider, } from "./notifications";
export { SECURITY_HEADERS } from "./server";
export { GatekeeperSocialProvider, createSocialProvider, } from "./social";
export { NoopStorageProvider, S3StorageProvider, createStorageProvider, } from "./storage";
export { TtlCache } from "./util/cache";
export { CircuitBreaker } from "./util/circuitBreaker";
Expand All @@ -19,6 +20,7 @@ export type { FetchPublicCatalogOptions, PublicBundle, PublicCatalog, PublicConn
export type { EmitResult, EventActor, EventInput, EventsProvider, EventsProviderConfig, EventsProviderStatus, HttpEventsProviderConfig, IggyEventsProviderConfig, NoopEventsProviderConfig, RegisteredSchema, SchemaRegistration, SubscriptionCreated, SubscriptionInput, } from "./events";
export type { FlagContext, FlagProvider, FlagProviderConfig, NoopFlagProviderConfig, UnleashFlagProviderConfig, } from "./flags";
export type { EmailParams, EmailResult, NoopNotificationProviderConfig, NotificationProvider, NotificationProviderConfig, } from "./notifications";
export type { GatekeeperSocialProviderConfig, SocialConnectionDTO, SocialProvider, } from "./social";
export type { NoopStorageProviderConfig, PresignedParams, PresignedResult, S3StorageProviderConfig, StorageProvider, StorageProviderConfig, UploadParams, UploadResult, } from "./storage";
export type { TtlCacheConfig } from "./util/cache";
export type { CircuitBreakerConfig } from "./util/circuitBreaker";
45 changes: 45 additions & 0 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80084,6 +80084,49 @@ var SECURITY_HEADERS = {
"Referrer-Policy": "strict-origin-when-cross-origin"
};
var headers_default = SECURITY_HEADERS;
// src/social/gatekeeper.ts
var REQUEST_TIMEOUT_MS9 = 5000;

class GatekeeperSocialProvider {
config;
constructor(config) {
this.config = {
...config,
timeoutMs: config.timeoutMs ?? REQUEST_TIMEOUT_MS9
};
}
async getConnections(userId) {
try {
const url = `${this.config.baseUrl}/social/connections/read?userId=${encodeURIComponent(userId)}`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.config.serviceKey}`,
Accept: "application/json"
},
signal: AbortSignal.timeout(this.config.timeoutMs)
});
if (!response.ok) {
throw new Error(`social connections read failed: ${response.status}`);
}
const raw = await response.json();
return Array.isArray(raw) ? raw : raw.connections ?? [];
} catch (error) {
log("error", "social", "connections read failed", {
userId,
error: error instanceof Error ? error.message : String(error)
});
return [];
}
}
async getConnection(userId, platform) {
const connections = await this.getConnections(userId);
return connections.find((c) => c.platform === platform) ?? null;
}
}

// src/social/index.ts
var createSocialProvider = (config) => new GatekeeperSocialProvider(config);
// src/storage/noop.ts
var NOOP_BASE_URL = "https://noop.example.com";

Expand Down Expand Up @@ -80260,6 +80303,7 @@ export {
eventMeta,
ensureFreshAccessToken,
createStorageProvider,
createSocialProvider,
createOmniOAuthConfig,
createOidcClient,
createNotificationProvider,
Expand Down Expand Up @@ -80291,6 +80335,7 @@ export {
LEGAL_BASE_URL,
IggyEventsProvider,
HttpEventsProvider,
GatekeeperSocialProvider,
GatekeeperOrgError,
GatekeeperOrgClient,
GatekeeperApiKeyProvider,
Expand Down
25 changes: 25 additions & 0 deletions build/social/gatekeeper.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { SocialConnectionDTO, SocialProvider } from "./interface";
type GatekeeperSocialProviderConfig = {
/** Gatekeeper base URL (no trailing slash) */
baseUrl: string;
/** Service key used for server-to-server auth */
serviceKey: string;
/** Request timeout in milliseconds */
timeoutMs?: number;
};
/**
* Gatekeeper social connections provider.
* Reads a user's connected social handles from the Gatekeeper social API.
*
* Fail-open: on any network error, non-2xx, or timeout the reads degrade to
* "no connections" (`[]` / `null`) rather than throwing, so a consumer never
* crashes a request just because Gatekeeper is briefly unreachable.
*/
declare class GatekeeperSocialProvider implements SocialProvider {
private readonly config;
constructor(config: GatekeeperSocialProviderConfig);
getConnections(userId: string): Promise<SocialConnectionDTO[]>;
getConnection(userId: string, platform: string): Promise<SocialConnectionDTO | null>;
}
export { GatekeeperSocialProvider };
export type { GatekeeperSocialProviderConfig };
12 changes: 12 additions & 0 deletions build/social/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { GatekeeperSocialProviderConfig } from "./gatekeeper";
import type { SocialProvider } from "./interface";
/**
* Create a social connections provider backed by Gatekeeper.
* @param config - Gatekeeper base URL and service key
* @returns Configured social provider instance
*/
declare const createSocialProvider: (config: GatekeeperSocialProviderConfig) => SocialProvider;
export { createSocialProvider };
export { GatekeeperSocialProvider } from "./gatekeeper";
export type { GatekeeperSocialProviderConfig } from "./gatekeeper";
export type { SocialConnectionDTO, SocialProvider } from "./interface";
65 changes: 65 additions & 0 deletions build/social/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// src/util/log.ts
function log(level, module, message, data) {
const entry = {
level,
module,
message,
...data,
timestamp: new Date().toISOString()
};
if (level === "error") {
console.error(JSON.stringify(entry));
} else if (level === "warn") {
console.warn(JSON.stringify(entry));
} else {
console.info(JSON.stringify(entry));
}
}

// src/social/gatekeeper.ts
var REQUEST_TIMEOUT_MS = 5000;

class GatekeeperSocialProvider {
config;
constructor(config) {
this.config = {
...config,
timeoutMs: config.timeoutMs ?? REQUEST_TIMEOUT_MS
};
}
async getConnections(userId) {
try {
const url = `${this.config.baseUrl}/social/connections/read?userId=${encodeURIComponent(userId)}`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.config.serviceKey}`,
Accept: "application/json"
},
signal: AbortSignal.timeout(this.config.timeoutMs)
});
if (!response.ok) {
throw new Error(`social connections read failed: ${response.status}`);
}
const raw = await response.json();
return Array.isArray(raw) ? raw : raw.connections ?? [];
} catch (error) {
log("error", "social", "connections read failed", {
userId,
error: error instanceof Error ? error.message : String(error)
});
return [];
}
}
async getConnection(userId, platform) {
const connections = await this.getConnections(userId);
return connections.find((c) => c.platform === platform) ?? null;
}
}

// src/social/index.ts
var createSocialProvider = (config) => new GatekeeperSocialProvider(config);
export {
createSocialProvider,
GatekeeperSocialProvider
};
39 changes: 39 additions & 0 deletions build/social/interface.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* A user's connected social account, as read from Gatekeeper.
*/
type SocialConnectionDTO = {
/** Platform slug, e.g. "threads" or "bluesky" */
platform: string;
/** Public handle on the platform, null until resolved */
handle: string | null;
/** Canonical profile URL, null until resolved */
profileUrl: string | null;
/** Avatar image URL, null until resolved */
avatarUrl: string | null;
/** Connection status, e.g. "connected" or "pending" */
status: string;
/** Whether the platform verified account ownership */
verified: boolean;
/** ISO timestamp of when the connection was established */
connectedAt: string;
};
/**
* Social connections provider interface.
* Reads a user's connected social handles from Gatekeeper.
*/
interface SocialProvider {
/**
* Read all social connections for a user.
* @param userId - The Gatekeeper user id
* @returns The user's connections, or [] on any error (fail-open)
*/
getConnections(userId: string): Promise<SocialConnectionDTO[]>;
/**
* Read a single social connection for a user by platform.
* @param userId - The Gatekeeper user id
* @param platform - The platform slug to filter by
* @returns The matching connection, or null if absent or on error (fail-open)
*/
getConnection(userId: string, platform: string): Promise<SocialConnectionDTO | null>;
}
export type { SocialConnectionDTO, SocialProvider };
1 change: 1 addition & 0 deletions build/social/social.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"import": "./build/server/index.js",
"types": "./build/server/index.d.ts"
},
"./social": {
"import": "./build/social/index.js",
"types": "./build/social/index.d.ts"
},
"./storage": {
"import": "./build/storage/index.js",
"types": "./build/storage/index.d.ts"
Expand Down
7 changes: 7 additions & 0 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ const entries = [
target: "node" as const,
external: [] as string[],
},
{
entrypoint: "./src/social/index.ts",
outdir: "./build/social",
// Fetch-only Gatekeeper client; safe in both browser and server consumers
target: "browser" as const,
external: [] as string[],
},
{
entrypoint: "./src/storage/index.ts",
outdir: "./build/storage",
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export {
} from "./notifications";
// Server
export { SECURITY_HEADERS } from "./server";
// Social
export {
GatekeeperSocialProvider,
createSocialProvider,
} from "./social";
// Storage
export {
NoopStorageProvider,
Expand Down Expand Up @@ -184,6 +189,11 @@ export type {
NotificationProvider,
NotificationProviderConfig,
} from "./notifications";
export type {
GatekeeperSocialProviderConfig,
SocialConnectionDTO,
SocialProvider,
} from "./social";
export type {
NoopStorageProviderConfig,
PresignedParams,
Expand Down
87 changes: 87 additions & 0 deletions src/social/gatekeeper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { log } from "../util/log";

import type { SocialConnectionDTO, SocialProvider } from "./interface";

/** Request timeout in milliseconds */
const REQUEST_TIMEOUT_MS = 5000;

type GatekeeperSocialProviderConfig = {
/** Gatekeeper base URL (no trailing slash) */
baseUrl: string;
/** Service key used for server-to-server auth */
serviceKey: string;
/** Request timeout in milliseconds */
timeoutMs?: number;
};

/**
* Raw read response shape from Gatekeeper. The endpoint returns either a bare
* array or an object wrapping the array under `connections`, so accept both.
*/
type GatekeeperConnectionsResponse =
| SocialConnectionDTO[]
| { connections?: SocialConnectionDTO[] };

/**
* Gatekeeper social connections provider.
* Reads a user's connected social handles from the Gatekeeper social API.
*
* Fail-open: on any network error, non-2xx, or timeout the reads degrade to
* "no connections" (`[]` / `null`) rather than throwing, so a consumer never
* crashes a request just because Gatekeeper is briefly unreachable.
*/
class GatekeeperSocialProvider implements SocialProvider {
private readonly config: Required<GatekeeperSocialProviderConfig>;

constructor(config: GatekeeperSocialProviderConfig) {
this.config = {
...config,
timeoutMs: config.timeoutMs ?? REQUEST_TIMEOUT_MS,
};
}

async getConnections(userId: string): Promise<SocialConnectionDTO[]> {
try {
const url = `${this.config.baseUrl}/social/connections/read?userId=${encodeURIComponent(userId)}`;

const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${this.config.serviceKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(this.config.timeoutMs),
});

if (!response.ok) {
throw new Error(`social connections read failed: ${response.status}`);
}

const raw = (await response.json()) as GatekeeperConnectionsResponse;

return Array.isArray(raw) ? raw : (raw.connections ?? []);
} catch (error) {
// Fail-open: degrade to "no connections" rather than throwing
log("error", "social", "connections read failed", {
userId,
error: error instanceof Error ? error.message : String(error),
});

return [];
}
}

async getConnection(
userId: string,
platform: string,
): Promise<SocialConnectionDTO | null> {
// Gatekeeper has no per-platform read endpoint, so fetch all and filter
const connections = await this.getConnections(userId);

return connections.find((c) => c.platform === platform) ?? null;
}
}

export { GatekeeperSocialProvider };

export type { GatekeeperSocialProviderConfig };
20 changes: 20 additions & 0 deletions src/social/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GatekeeperSocialProvider } from "./gatekeeper";

import type { GatekeeperSocialProviderConfig } from "./gatekeeper";
import type { SocialProvider } from "./interface";

/**
* Create a social connections provider backed by Gatekeeper.
* @param config - Gatekeeper base URL and service key
* @returns Configured social provider instance
*/
const createSocialProvider = (
config: GatekeeperSocialProviderConfig,
): SocialProvider => new GatekeeperSocialProvider(config);

export { createSocialProvider };

export { GatekeeperSocialProvider } from "./gatekeeper";

export type { GatekeeperSocialProviderConfig } from "./gatekeeper";
export type { SocialConnectionDTO, SocialProvider } from "./interface";
Loading
Loading