@singi-labs/sifa-sdk 0.19.53 → 0.19.55
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/dist/badge/index.cjs +18 -2
- package/dist/badge/index.cjs.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{network-map-C-p-k3gB.d.cts → network-map-CdYpUy-t.d.cts} +2 -2
- package/dist/{network-map-CnVlBE0G.d.ts → network-map-CiWrQ1fF.d.ts} +2 -2
- package/dist/{org-CFzRg61Q.d.cts → org-fmpG85L0.d.cts} +32 -27
- package/dist/{org-Bp4mNT8K.d.ts → org-hxHnoBfg.d.ts} +32 -27
- package/dist/{profile-summary-C_wdHGj6.d.cts → profile-summary-5T1hDw6Q.d.cts} +1 -1
- package/dist/{profile-summary-Qtfk-52i.d.ts → profile-summary-Bj7ufSDK.d.ts} +1 -1
- package/dist/query/client.js +26 -6
- package/dist/query/client.js.map +1 -1
- package/dist/query/fetchers/destructive.js.map +1 -1
- package/dist/query/fetchers/external-accounts.js.map +1 -1
- package/dist/query/fetchers/follow.js.map +1 -1
- package/dist/query/fetchers/index.cjs +26 -6
- package/dist/query/fetchers/index.cjs.map +1 -1
- package/dist/query/fetchers/index.d.cts +5 -5
- package/dist/query/fetchers/index.d.ts +5 -5
- package/dist/query/fetchers/org.js.map +1 -1
- package/dist/query/fetchers/profile-mutations.js.map +1 -1
- package/dist/query/fetchers/publications.js.map +1 -1
- package/dist/query/fetchers/repo-inventory.js.map +1 -1
- package/dist/query/fetchers/skills.js.map +1 -1
- package/dist/query/hooks/index.cjs +26 -6
- package/dist/query/hooks/index.cjs.map +1 -1
- package/dist/query/hooks/index.d.cts +3 -3
- package/dist/query/hooks/index.d.ts +3 -3
- package/dist/query/hooks/use-org-claim.js.map +1 -1
- package/dist/query/hooks/use-org-domains.js.map +1 -1
- package/dist/query/hooks/use-org-notification-emails.js.map +1 -1
- package/dist/query/hooks/use-org-profile-settings.js.map +1 -1
- package/dist/query/hooks/use-skill-mutations.js.map +1 -1
- package/dist/query/index.cjs +26 -6
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +8 -8
- package/dist/query/index.d.ts +8 -8
- package/dist/roadmap/index.js +1 -1
- package/dist/roadmap/index.js.map +1 -1
- package/dist/schemas/write/external-account.js +2 -1
- package/dist/schemas/write/external-account.js.map +1 -1
- package/dist/schemas/write/index.cjs +2 -1
- package/dist/schemas/write/index.cjs.map +1 -1
- package/dist/schemas/write/index.d.cts +1 -1
- package/dist/schemas/write/index.d.ts +1 -1
- package/dist/{types-DAGqE0b9.d.cts → types-DHT7jcZc.d.cts} +36 -8
- package/dist/{types-CK8KgdqA.d.ts → types-jB67EFpw.d.ts} +36 -8
- package/dist/{use-org-notification-emails-CJ0hKqco.d.ts → use-org-notification-emails-CWLCWkmb.d.ts} +15 -15
- package/dist/{use-org-notification-emails-Dk084Rrw.d.cts → use-org-notification-emails-Cj8IDI_7.d.cts} +15 -15
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/query/fetchers/destructive.ts"],"sourcesContent":["import {\n apiFetch,\n apiWrite,\n type ApiFetchOptions,\n type SifaApiConfig,\n type WriteResult,\n} from '../client.js';\n\n/**\n * What the server managed to remove from the user's PDS.\n *\n * The wipe runs per collection, so it can partly succeed: `success: true` means\n * the account action completed, NOT that the PDS is clean. A UI that ignores\n * this can tell someone their data is gone while it is still on their data\n * server, so treat a non-empty `remaining` as \"not deleted\".\n *\n * Absent when the PDS was not touched (`deletePdsData: false`).\n */\nexport interface PdsWipeOutcome {\n /** Collections whose records were removed. */\n deleted: string[];\n /** Collections whose records are still on the PDS. Non-empty means not done. */\n remaining: string[];\n /**\n * The server could not enumerate the repo, so it does not know what survived.\n * Without this, that case is indistinguishable from \"nothing to delete\".\n */\n unknown: boolean;\n}\n\n/**\n * What a PDS wipe could not remove with the grant the session holds today.\n *\n * Read this BEFORE the destructive step. Deleting an account destroys the\n * session, so a missing scope cannot be granted afterwards -- there is nobody\n * left to ask. A non-empty `needsScopeFor` means the wipe would strand those\n * records on the user's data server.\n */\nexport interface WipePreview {\n /** id.sifa.* collections the current grant cannot delete. */\n needsScopeFor: string[];\n /**\n * The server could not enumerate the repo, so the gap list is not\n * authoritative. Distinct from an empty list, which means \"nothing to ask for\".\n */\n unknown?: boolean;\n}\n\n/**\n * Ask which id.sifa.* collections the current grant cannot delete.\n *\n * Unlike most read fetchers in this file's neighbourhood, a failure is NOT\n * flattened into an empty result: an empty gap list reads as \"a wipe will be\n * clean\", and a caller must not promise that on the strength of a request that\n * never arrived. Let it throw and warn.\n */\nexport function fetchWipePreview(\n config: SifaApiConfig,\n options: ApiFetchOptions = {},\n): Promise<WipePreview> {\n return apiFetch<WipePreview>(config, '/api/profile/wipe-preview', {\n credentials: 'include',\n ...options,\n });\n}\n\n/** Extended write result for {@link resetProfile}. */\nexport
|
|
1
|
+
{"version":3,"sources":["../../../src/query/fetchers/destructive.ts"],"sourcesContent":["import {\n apiFetch,\n apiWrite,\n type ApiFetchOptions,\n type SifaApiConfig,\n type WriteResult,\n} from '../client.js';\n\n/**\n * What the server managed to remove from the user's PDS.\n *\n * The wipe runs per collection, so it can partly succeed: `success: true` means\n * the account action completed, NOT that the PDS is clean. A UI that ignores\n * this can tell someone their data is gone while it is still on their data\n * server, so treat a non-empty `remaining` as \"not deleted\".\n *\n * Absent when the PDS was not touched (`deletePdsData: false`).\n */\nexport interface PdsWipeOutcome {\n /** Collections whose records were removed. */\n deleted: string[];\n /** Collections whose records are still on the PDS. Non-empty means not done. */\n remaining: string[];\n /**\n * The server could not enumerate the repo, so it does not know what survived.\n * Without this, that case is indistinguishable from \"nothing to delete\".\n */\n unknown: boolean;\n}\n\n/**\n * What a PDS wipe could not remove with the grant the session holds today.\n *\n * Read this BEFORE the destructive step. Deleting an account destroys the\n * session, so a missing scope cannot be granted afterwards -- there is nobody\n * left to ask. A non-empty `needsScopeFor` means the wipe would strand those\n * records on the user's data server.\n */\nexport interface WipePreview {\n /** id.sifa.* collections the current grant cannot delete. */\n needsScopeFor: string[];\n /**\n * The server could not enumerate the repo, so the gap list is not\n * authoritative. Distinct from an empty list, which means \"nothing to ask for\".\n */\n unknown?: boolean;\n}\n\n/**\n * Ask which id.sifa.* collections the current grant cannot delete.\n *\n * Unlike most read fetchers in this file's neighbourhood, a failure is NOT\n * flattened into an empty result: an empty gap list reads as \"a wipe will be\n * clean\", and a caller must not promise that on the strength of a request that\n * never arrived. Let it throw and warn.\n */\nexport function fetchWipePreview(\n config: SifaApiConfig,\n options: ApiFetchOptions = {},\n): Promise<WipePreview> {\n return apiFetch<WipePreview>(config, '/api/profile/wipe-preview', {\n credentials: 'include',\n ...options,\n });\n}\n\n/** Extended write result for {@link resetProfile}. */\nexport type ResetProfileResult = WriteResult<{\n /** Present when `deletePdsData: true`. See {@link PdsWipeOutcome}. */\n pds?: PdsWipeOutcome;\n}>;\n\n/** Extended write result for {@link deleteAccount}. */\nexport type DeleteAccountResult = WriteResult<{\n /** The deleted handle, returned by the server for confirmation UIs. */\n handle?: string;\n /** Present when `deletePdsData: true`. See {@link PdsWipeOutcome}. */\n pds?: PdsWipeOutcome;\n}>;\n\n/**\n * Reset the authenticated user's Sifa profile.\n *\n * `deletePdsData: true` also deletes the corresponding records on the\n * user's PDS. `deletePdsData: false` only removes the AppView's\n * indexed state -- the records on the PDS are left intact and could be\n * re-indexed later.\n *\n * Destructive. Server enforces session check + attestation; the SDK\n * does not gate on additional confirmation. Wrap call sites in your\n * own modal if you want a UX confirmation step.\n */\nexport function resetProfile(\n config: SifaApiConfig,\n deletePdsData: boolean,\n options: ApiFetchOptions = {},\n): Promise<ResetProfileResult> {\n return apiWrite<{ ok?: boolean; pds?: PdsWipeOutcome }>(config, '/api/profile/reset', 'DELETE', {\n body: { deletePdsData },\n ...options,\n });\n}\n\n/**\n * Delete the authenticated user's account. Returns the deleted handle\n * on success (used by the post-delete confirmation screen).\n *\n * `deletePdsData: true` also deletes the corresponding records on the\n * user's PDS; `false` leaves the PDS records intact.\n *\n * Destructive. Same caveat as {@link resetProfile}.\n */\nexport function deleteAccount(\n config: SifaApiConfig,\n deletePdsData: boolean,\n options: ApiFetchOptions = {},\n): Promise<DeleteAccountResult> {\n return apiWrite<{ ok?: boolean; handle?: string; pds?: PdsWipeOutcome }>(\n config,\n '/api/profile/account',\n 'DELETE',\n {\n body: { deletePdsData },\n ...options,\n },\n );\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAkDA,SAAS,iBACd,QACA,UAA2B,CAAC,GACN;AACtB,SAAO,SAAsB,QAAQ,6BAA6B;AAAA,IAChE,aAAa;AAAA,IACb,GAAG;AAAA,EACL,CAAC;AACH;AA4BO,SAAS,aACd,QACA,eACA,UAA2B,CAAC,GACC;AAC7B,SAAO,SAAiD,QAAQ,sBAAsB,UAAU;AAAA,IAC9F,MAAM,EAAE,cAAc;AAAA,IACtB,GAAG;AAAA,EACL,CAAC;AACH;AAWO,SAAS,cACd,QACA,eACA,UAA2B,CAAC,GACE;AAC9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAM,EAAE,cAAc;AAAA,MACtB,GAAG;AAAA,IACL;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/query/fetchers/external-accounts.ts"],"sourcesContent":["import type { ExternalAccount } from '../../types/index.js';\nimport {\n apiFetch,\n encodeIdentifier,\n apiWrite,\n type ApiFetchOptions,\n type SifaApiConfig,\n type WriteResult,\n} from '../client.js';\n\n/** Body accepted by {@link createExternalAccount} / {@link updateExternalAccount}. */\nexport interface ExternalAccountInput {\n platform: string;\n url: string;\n label?: string;\n feedUrl?: string;\n}\n\n/** Extended create result for {@link createExternalAccount}. */\nexport
|
|
1
|
+
{"version":3,"sources":["../../../src/query/fetchers/external-accounts.ts"],"sourcesContent":["import type { ExternalAccount } from '../../types/index.js';\nimport {\n apiFetch,\n encodeIdentifier,\n apiWrite,\n type ApiFetchOptions,\n type SifaApiConfig,\n type WriteResult,\n} from '../client.js';\n\n/** Body accepted by {@link createExternalAccount} / {@link updateExternalAccount}. */\nexport interface ExternalAccountInput {\n platform: string;\n url: string;\n label?: string;\n feedUrl?: string;\n}\n\n/** Extended create result for {@link createExternalAccount}. */\nexport type CreateExternalAccountResult = WriteResult<{\n rkey?: string;\n feedUrl?: string | null;\n}>;\n\n/** Extended write result for {@link verifyExternalAccount}. */\nexport type VerifyExternalAccountResult = WriteResult<{\n verified?: boolean;\n verifiedVia?: string;\n}>;\n\n/** List external accounts attached to a profile. Returns `[]` on error. */\nexport async function fetchExternalAccounts(\n config: SifaApiConfig,\n handleOrDid: string,\n options: ApiFetchOptions = {},\n): Promise<ExternalAccount[]> {\n const path = `/api/profile/${encodeIdentifier(handleOrDid)}/external-accounts`;\n try {\n const data = await apiFetch<{ accounts?: ExternalAccount[] }>(config, path, {\n credentials: 'include',\n ...options,\n });\n return data.accounts ?? [];\n } catch {\n return [];\n }\n}\n\n/**\n * Create a new external account record. Returns the newly-created `rkey`\n * and the server-resolved `feedUrl` (sifa-api inspects the target for\n * RSS feeds on platforms that publish them).\n */\nexport function createExternalAccount(\n config: SifaApiConfig,\n data: ExternalAccountInput,\n options: ApiFetchOptions = {},\n): Promise<CreateExternalAccountResult> {\n return apiWrite<{ rkey?: string; feedUrl?: string | null }>(\n config,\n '/api/profile/external-accounts',\n 'POST',\n { body: data, ...options },\n );\n}\n\n/** Update an existing external account by `rkey`. */\nexport function updateExternalAccount(\n config: SifaApiConfig,\n rkey: string,\n data: ExternalAccountInput,\n options: ApiFetchOptions = {},\n): Promise<WriteResult> {\n return apiWrite(config, `/api/profile/external-accounts/${encodeURIComponent(rkey)}`, 'PUT', {\n body: data,\n ...options,\n });\n}\n\n/** Delete an external account by `rkey`. */\nexport function deleteExternalAccount(\n config: SifaApiConfig,\n rkey: string,\n options: ApiFetchOptions = {},\n): Promise<WriteResult> {\n return apiWrite(\n config,\n `/api/profile/external-accounts/${encodeURIComponent(rkey)}`,\n 'DELETE',\n options,\n );\n}\n\n/** Mark an external account as the user's primary. */\nexport function setExternalAccountPrimary(\n config: SifaApiConfig,\n rkey: string,\n options: ApiFetchOptions = {},\n): Promise<WriteResult> {\n return apiWrite(\n config,\n `/api/profile/external-accounts/${encodeURIComponent(rkey)}/primary`,\n 'PUT',\n options,\n );\n}\n\n/** Clear the \"primary\" flag on an external account. */\nexport function unsetExternalAccountPrimary(\n config: SifaApiConfig,\n rkey: string,\n options: ApiFetchOptions = {},\n): Promise<WriteResult> {\n return apiWrite(\n config,\n `/api/profile/external-accounts/${encodeURIComponent(rkey)}/primary`,\n 'DELETE',\n options,\n );\n}\n\n/**\n * Run server-side verification on an external account (e.g. inspect\n * the target for a keytrace claim). Returns `{ verified, verifiedVia }`\n * on success.\n */\nexport function verifyExternalAccount(\n config: SifaApiConfig,\n rkey: string,\n options: ApiFetchOptions = {},\n): Promise<VerifyExternalAccountResult> {\n return apiWrite<{ verified?: boolean; verifiedVia?: string }>(\n config,\n `/api/profile/external-accounts/${encodeURIComponent(rkey)}/verify`,\n 'POST',\n { body: {}, ...options },\n );\n}\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAuBP,eAAsB,sBACpB,QACA,aACA,UAA2B,CAAC,GACA;AAC5B,QAAM,OAAO,gBAAgB,iBAAiB,WAAW,CAAC;AAC1D,MAAI;AACF,UAAM,OAAO,MAAM,SAA2C,QAAQ,MAAM;AAAA,MAC1E,aAAa;AAAA,MACb,GAAG;AAAA,IACL,CAAC;AACD,WAAO,KAAK,YAAY,CAAC;AAAA,EAC3B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAOO,SAAS,sBACd,QACA,MACA,UAA2B,CAAC,GACU;AACtC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,EAAE,MAAM,MAAM,GAAG,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,sBACd,QACA,MACA,MACA,UAA2B,CAAC,GACN;AACtB,SAAO,SAAS,QAAQ,kCAAkC,mBAAmB,IAAI,CAAC,IAAI,OAAO;AAAA,IAC3F,MAAM;AAAA,IACN,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,sBACd,QACA,MACA,UAA2B,CAAC,GACN;AACtB,SAAO;AAAA,IACL;AAAA,IACA,kCAAkC,mBAAmB,IAAI,CAAC;AAAA,IAC1D;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,0BACd,QACA,MACA,UAA2B,CAAC,GACN;AACtB,SAAO;AAAA,IACL;AAAA,IACA,kCAAkC,mBAAmB,IAAI,CAAC;AAAA,IAC1D;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,4BACd,QACA,MACA,UAA2B,CAAC,GACN;AACtB,SAAO;AAAA,IACL;AAAA,IACA,kCAAkC,mBAAmB,IAAI,CAAC;AAAA,IAC1D;AAAA,IACA;AAAA,EACF;AACF;AAOO,SAAS,sBACd,QACA,MACA,UAA2B,CAAC,GACU;AACtC,SAAO;AAAA,IACL;AAAA,IACA,kCAAkC,mBAAmB,IAAI,CAAC;AAAA,IAC1D;AAAA,IACA,EAAE,MAAM,CAAC,GAAG,GAAG,QAAQ;AAAA,EACzB;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/query/fetchers/follow.ts"],"sourcesContent":["import {\n apiFetch,\n encodeIdentifier,\n apiWrite,\n type ApiFetchOptions,\n type SifaApiConfig,\n type WriteResult,\n} from '../client.js';\nimport type { ActivityFeedResponse } from './activity.js';\n\nexport interface FollowProfile {\n did: string;\n handle: string;\n displayName?: string;\n headline?: string;\n avatarUrl?: string;\n source: string;\n claimed: boolean;\n followedAt: string;\n blueskyVerified?: boolean;\n blueskyVerifiedAt?: string | null;\n}\n\nexport interface FollowingResponse {\n follows: FollowProfile[];\n cursor?: string;\n}\n\n/** People the authenticated user follows. Empty on error. */\nexport async function fetchFollowing(\n config: SifaApiConfig,\n opts: { source?: string; cursor?: string; limit?: number } & ApiFetchOptions = {},\n): Promise<FollowingResponse> {\n const params = new URLSearchParams();\n if (opts.source) params.set('source', opts.source);\n if (opts.cursor) params.set('cursor', opts.cursor);\n if (opts.limit) params.set('limit', String(opts.limit));\n const qs = params.toString();\n\n try {\n return await apiFetch<FollowingResponse>(config, `/api/following${qs ? `?${qs}` : ''}`, {\n credentials: 'include',\n cache: 'no-store',\n ...opts,\n });\n } catch {\n return { follows: [] };\n }\n}\n\n/**\n * Result of {@link followUser}. Extends {@link WriteResult} with the\n * follow `rkey` returned by sifa-api on success. Self-follow + invalid\n * handle surface as `success: false` with the server-provided message;\n * dup-follow is idempotent (sifa-api E7) and resolves as `success: true`.\n */\nexport interface FollowUserResult extends WriteResult {\n rkey?: string;\n /** DID of the followed subject (server-resolved from the handle). */\n subjectDid?: string;\n}\n\n/**\n * Create an `id.sifa.graph.follow` record on the caller's PDS via the\n * AppView. Idempotent on duplicate (server catches the unique-violation\n * and returns 200, per sifa-api#673 E7).\n */\nexport function followUser(\n config: SifaApiConfig,\n handle: string,\n opts: { note?: string } & Omit<ApiFetchOptions, 'method' | 'body'> = {},\n): Promise<FollowUserResult> {\n const { note, ...rest } = opts;\n return apiWrite<{ rkey?: string; subjectDid?: string }>(\n config,\n `/api/follow/${encodeIdentifier(handle)}`,\n 'POST',\n {\n body: note !== undefined ? { note } : undefined,\n ...rest,\n },\n );\n}\n\n/** Delete the authenticated viewer's `id.sifa.graph.follow` for `handle`. */\nexport function unfollowUser(\n config: SifaApiConfig,\n handle: string,\n opts: Omit<ApiFetchOptions, 'method' | 'body'> = {},\n): Promise<WriteResult> {\n return apiWrite(config, `/api/follow/${encodeIdentifier(handle)}`, 'DELETE', opts);\n}\n\nexport interface FollowListPage {\n follows: FollowProfile[];\n cursor: string | null;\n}\n\nexport interface FetchFollowListOptions extends ApiFetchOptions {\n cursor?: string;\n limit?: number;\n /**\n * Pass the caller's `Cookie` header on Next.js RSC server-side calls\n * (mirrors {@link FetchActivityFeedOptions}; required for authenticated\n * server fetches because `credentials: 'include'` does not propagate\n * cookies from RSC).\n */\n cookieHeader?: string;\n}\n\nfunction buildListPath(prefix: string, opts: FetchFollowListOptions): string {\n const params = new URLSearchParams();\n if (opts.cursor) params.set('cursor', opts.cursor);\n if (opts.limit) params.set('limit', String(opts.limit));\n const qs = params.toString();\n return `${prefix}${qs ? `?${qs}` : ''}`;\n}\n\n/**\n * Paginated list of `handle`'s followers. Returns an empty page on error\n * so the UI can render a graceful \"no followers yet\" state.\n */\nexport async function getFollowers(\n config: SifaApiConfig,\n handle: string,\n opts: FetchFollowListOptions = {},\n): Promise<FollowListPage> {\n const headers: Record<string, string> = { ...(opts.headers ?? {}) };\n if (opts.cookieHeader) headers.cookie = opts.cookieHeader;\n try {\n const res = await apiFetch<{ follows: FollowProfile[]; cursor?: string | null }>(\n config,\n buildListPath(`/api/profile/${encodeIdentifier(handle)}/followers`, opts),\n { credentials: 'include', cache: 'no-store', ...opts, headers },\n );\n return { follows: res.follows, cursor: res.cursor ?? null };\n } catch {\n return { follows: [], cursor: null };\n }\n}\n\n/** Paginated list of who `handle` follows. */\nexport async function getFollowing(\n config: SifaApiConfig,\n handle: string,\n opts: FetchFollowListOptions = {},\n): Promise<FollowListPage> {\n const headers: Record<string, string> = { ...(opts.headers ?? {}) };\n if (opts.cookieHeader) headers.cookie = opts.cookieHeader;\n try {\n const res = await apiFetch<{ follows: FollowProfile[]; cursor?: string | null }>(\n config,\n buildListPath(`/api/profile/${encodeIdentifier(handle)}/following`, opts),\n { credentials: 'include', cache: 'no-store', ...opts, headers },\n );\n return { follows: res.follows, cursor: res.cursor ?? null };\n } catch {\n return { follows: [], cursor: null };\n }\n}\n\n/** The method binding for the following-feed service-auth token; MUST match the\n * sifa-api endpoint's `lxm`. */\nexport const FOLLOWING_FEED_LXM = 'id.sifa.feed.getFollowingFeed';\n\n/** One app the viewer's network is active on, for the feed's source tabs. */\nexport interface FollowingFeedApp {\n /** Registry app id, e.g. `tangled`. Pass back as {@link FetchFollowingFeedOptions.app}. */\n id: string;\n name: string;\n /** Summed recent activity across the network; the tab ordering. */\n count: number;\n}\n\n/**\n * Why a following-feed page came back empty. Lets a client say something true\n * per case instead of one catch-all sentence, and point at the right next step.\n */\nexport type FollowingFeedEmptyReason =\n | 'no_follows'\n | 'no_visible_follows'\n | 'no_cross_app_activity'\n | 'no_recent_records'\n | 'all_hidden'\n | 'nothing_renderable'\n | 'hydration_failed'\n /** The feed is still being built. Nothing is wrong: ask again shortly. */\n | 'building';\n\n/** Options for {@link fetchFollowingFeed}. */\nexport interface FetchFollowingFeedOptions extends ApiFetchOptions {\n limit?: number;\n /**\n * Opt in to the viewer's Bluesky activity too. Off by default: the point of\n * the feed is what connections do on OTHER apps.\n *\n * Ignored when {@link app} is set, since a source tab selects one app.\n */\n includeBluesky?: boolean;\n /**\n * Restrict the page to one app (a registry id from the response's `apps`).\n * This is what a source tab selects. `app: 'bluesky'` selects Bluesky even\n * though it is excluded from the default mixed view.\n */\n app?: string;\n /** Forward a `Cookie` header on Next.js RSC server-side calls (web). */\n cookieHeader?: string;\n}\n\n/** The following feed's response: an activity feed plus its tab metadata. */\nexport interface FollowingFeedResponse extends ActivityFeedResponse {\n /**\n * Apps the viewer's network is actually active on, most active first.\n * Present on empty pages too, so a client's tabs survive a quiet tab.\n */\n apps?: FollowingFeedApp[];\n /** Set only when `items` is empty. */\n reason?: FollowingFeedEmptyReason;\n /** When this feed was built (ISO 8601). Absent on a `building` answer. */\n builtAt?: string;\n /**\n * True when this is an older build, served while a newer one is built in\n * the background. Poll {@link fetchFollowingFeedVersion} to learn when the\n * newer one is ready.\n */\n stale?: boolean;\n}\n\n/** Query string and auth headers shared by the feed and its version check. */\nasync function followingFeedRequest(config: SifaApiConfig, opts: FetchFollowingFeedOptions) {\n const params = new URLSearchParams();\n if (opts.limit) params.set('limit', String(opts.limit));\n // A selected tab decides Bluesky on its own, so the flag is not also sent.\n if (opts.app) params.set('app', opts.app);\n else if (opts.includeBluesky) params.set('includeBluesky', 'true');\n const qs = params.toString();\n\n const headers: Record<string, string> = { ...(opts.headers ?? {}) };\n if (opts.cookieHeader) headers.cookie = opts.cookieHeader;\n if (config.getAuthToken) {\n const token = await config.getAuthToken(FOLLOWING_FEED_LXM);\n if (token) headers.Authorization = `Bearer ${token}`;\n }\n return { query: qs ? `?${qs}` : '', headers };\n}\n\n/**\n * The viewer's cross-app following feed: what OTHER apps their connections use\n * (Tangled, WhiteWind, Smoke Signal, ...) — Bluesky is excluded server-side by\n * default, as it is the least interesting part and other apps already surface\n * it. Pass `includeBluesky: true` to bring it back in.\n *\n * Auth is identity-only server-side, so the NATIVE app supplies\n * `config.getAuthToken` to mint a service-auth Bearer, while WEB relies on its\n * session cookie (`credentials: 'include'`). Returns `null` on error. v1 is a\n * finite first page (no cursor yet).\n */\nexport async function fetchFollowingFeed(\n config: SifaApiConfig,\n opts: FetchFollowingFeedOptions = {},\n): Promise<FollowingFeedResponse | null> {\n const { query, headers } = await followingFeedRequest(config, opts);\n try {\n return await apiFetch<FollowingFeedResponse>(config, `/api/following/feed${query}`, {\n credentials: 'include',\n cache: 'no-store',\n ...opts,\n headers,\n });\n } catch {\n return null;\n }\n}\n\n/** When the cached following feed for a query was built. */\nexport interface FollowingFeedVersion {\n /** ISO 8601, or null when nothing is cached for this query. */\n builtAt: string | null;\n}\n\n/**\n * When the cached following feed for the same query was built. Cheap: it never\n * builds a feed. A client showing a `stale` feed polls this and offers the\n * newer one once {@link isNewerFollowingFeed} says so. Returns `null` on error.\n */\nexport async function fetchFollowingFeedVersion(\n config: SifaApiConfig,\n opts: FetchFollowingFeedOptions = {},\n): Promise<FollowingFeedVersion | null> {\n const { query, headers } = await followingFeedRequest(config, opts);\n try {\n return await apiFetch<FollowingFeedVersion>(config, `/api/following/feed/version${query}`, {\n credentials: 'include',\n cache: 'no-store',\n ...opts,\n headers,\n });\n } catch {\n return null;\n }\n}\n\n/** True when `latestBuiltAt` is a newer build than the one shown (`shownBuiltAt`). */\nexport function isNewerFollowingFeed(\n shownBuiltAt: string | null | undefined,\n latestBuiltAt: string | null | undefined,\n): boolean {\n if (!shownBuiltAt || !latestBuiltAt) return false;\n return Date.parse(latestBuiltAt) > Date.parse(shownBuiltAt);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/query/fetchers/follow.ts"],"sourcesContent":["import {\n apiFetch,\n encodeIdentifier,\n apiWrite,\n type ApiFetchOptions,\n type SifaApiConfig,\n type WriteResult,\n} from '../client.js';\nimport type { ActivityFeedResponse } from './activity.js';\n\nexport interface FollowProfile {\n did: string;\n handle: string;\n displayName?: string;\n headline?: string;\n avatarUrl?: string;\n source: string;\n claimed: boolean;\n followedAt: string;\n blueskyVerified?: boolean;\n blueskyVerifiedAt?: string | null;\n}\n\nexport interface FollowingResponse {\n follows: FollowProfile[];\n cursor?: string;\n}\n\n/** People the authenticated user follows. Empty on error. */\nexport async function fetchFollowing(\n config: SifaApiConfig,\n opts: { source?: string; cursor?: string; limit?: number } & ApiFetchOptions = {},\n): Promise<FollowingResponse> {\n const params = new URLSearchParams();\n if (opts.source) params.set('source', opts.source);\n if (opts.cursor) params.set('cursor', opts.cursor);\n if (opts.limit) params.set('limit', String(opts.limit));\n const qs = params.toString();\n\n try {\n return await apiFetch<FollowingResponse>(config, `/api/following${qs ? `?${qs}` : ''}`, {\n credentials: 'include',\n cache: 'no-store',\n ...opts,\n });\n } catch {\n return { follows: [] };\n }\n}\n\n/**\n * Result of {@link followUser}. Extends {@link WriteResult} with the\n * follow `rkey` returned by sifa-api on success. Self-follow + invalid\n * handle surface as `success: false` with the server-provided message;\n * dup-follow is idempotent (sifa-api E7) and resolves as `success: true`.\n */\nexport type FollowUserResult = WriteResult<{\n rkey?: string;\n /** DID of the followed subject (server-resolved from the handle). */\n subjectDid?: string;\n}>;\n\n/**\n * Create an `id.sifa.graph.follow` record on the caller's PDS via the\n * AppView. Idempotent on duplicate (server catches the unique-violation\n * and returns 200, per sifa-api#673 E7).\n */\nexport function followUser(\n config: SifaApiConfig,\n handle: string,\n opts: { note?: string } & Omit<ApiFetchOptions, 'method' | 'body'> = {},\n): Promise<FollowUserResult> {\n const { note, ...rest } = opts;\n return apiWrite<{ rkey?: string; subjectDid?: string }>(\n config,\n `/api/follow/${encodeIdentifier(handle)}`,\n 'POST',\n {\n body: note !== undefined ? { note } : undefined,\n ...rest,\n },\n );\n}\n\n/** Delete the authenticated viewer's `id.sifa.graph.follow` for `handle`. */\nexport function unfollowUser(\n config: SifaApiConfig,\n handle: string,\n opts: Omit<ApiFetchOptions, 'method' | 'body'> = {},\n): Promise<WriteResult> {\n return apiWrite(config, `/api/follow/${encodeIdentifier(handle)}`, 'DELETE', opts);\n}\n\nexport interface FollowListPage {\n follows: FollowProfile[];\n cursor: string | null;\n}\n\nexport interface FetchFollowListOptions extends ApiFetchOptions {\n cursor?: string;\n limit?: number;\n /**\n * Pass the caller's `Cookie` header on Next.js RSC server-side calls\n * (mirrors {@link FetchActivityFeedOptions}; required for authenticated\n * server fetches because `credentials: 'include'` does not propagate\n * cookies from RSC).\n */\n cookieHeader?: string;\n}\n\nfunction buildListPath(prefix: string, opts: FetchFollowListOptions): string {\n const params = new URLSearchParams();\n if (opts.cursor) params.set('cursor', opts.cursor);\n if (opts.limit) params.set('limit', String(opts.limit));\n const qs = params.toString();\n return `${prefix}${qs ? `?${qs}` : ''}`;\n}\n\n/**\n * Paginated list of `handle`'s followers. Returns an empty page on error\n * so the UI can render a graceful \"no followers yet\" state.\n */\nexport async function getFollowers(\n config: SifaApiConfig,\n handle: string,\n opts: FetchFollowListOptions = {},\n): Promise<FollowListPage> {\n const headers: Record<string, string> = { ...(opts.headers ?? {}) };\n if (opts.cookieHeader) headers.cookie = opts.cookieHeader;\n try {\n const res = await apiFetch<{ follows: FollowProfile[]; cursor?: string | null }>(\n config,\n buildListPath(`/api/profile/${encodeIdentifier(handle)}/followers`, opts),\n { credentials: 'include', cache: 'no-store', ...opts, headers },\n );\n return { follows: res.follows, cursor: res.cursor ?? null };\n } catch {\n return { follows: [], cursor: null };\n }\n}\n\n/** Paginated list of who `handle` follows. */\nexport async function getFollowing(\n config: SifaApiConfig,\n handle: string,\n opts: FetchFollowListOptions = {},\n): Promise<FollowListPage> {\n const headers: Record<string, string> = { ...(opts.headers ?? {}) };\n if (opts.cookieHeader) headers.cookie = opts.cookieHeader;\n try {\n const res = await apiFetch<{ follows: FollowProfile[]; cursor?: string | null }>(\n config,\n buildListPath(`/api/profile/${encodeIdentifier(handle)}/following`, opts),\n { credentials: 'include', cache: 'no-store', ...opts, headers },\n );\n return { follows: res.follows, cursor: res.cursor ?? null };\n } catch {\n return { follows: [], cursor: null };\n }\n}\n\n/** The method binding for the following-feed service-auth token; MUST match the\n * sifa-api endpoint's `lxm`. */\nexport const FOLLOWING_FEED_LXM = 'id.sifa.feed.getFollowingFeed';\n\n/** One app the viewer's network is active on, for the feed's source tabs. */\nexport interface FollowingFeedApp {\n /** Registry app id, e.g. `tangled`. Pass back as {@link FetchFollowingFeedOptions.app}. */\n id: string;\n name: string;\n /** Summed recent activity across the network; the tab ordering. */\n count: number;\n}\n\n/**\n * Why a following-feed page came back empty. Lets a client say something true\n * per case instead of one catch-all sentence, and point at the right next step.\n */\nexport type FollowingFeedEmptyReason =\n | 'no_follows'\n | 'no_visible_follows'\n | 'no_cross_app_activity'\n | 'no_recent_records'\n | 'all_hidden'\n | 'nothing_renderable'\n | 'hydration_failed'\n /** The feed is still being built. Nothing is wrong: ask again shortly. */\n | 'building';\n\n/** Options for {@link fetchFollowingFeed}. */\nexport interface FetchFollowingFeedOptions extends ApiFetchOptions {\n limit?: number;\n /**\n * Opt in to the viewer's Bluesky activity too. Off by default: the point of\n * the feed is what connections do on OTHER apps.\n *\n * Ignored when {@link app} is set, since a source tab selects one app.\n */\n includeBluesky?: boolean;\n /**\n * Restrict the page to one app (a registry id from the response's `apps`).\n * This is what a source tab selects. `app: 'bluesky'` selects Bluesky even\n * though it is excluded from the default mixed view.\n */\n app?: string;\n /** Forward a `Cookie` header on Next.js RSC server-side calls (web). */\n cookieHeader?: string;\n}\n\n/** The following feed's response: an activity feed plus its tab metadata. */\nexport interface FollowingFeedResponse extends ActivityFeedResponse {\n /**\n * Apps the viewer's network is actually active on, most active first.\n * Present on empty pages too, so a client's tabs survive a quiet tab.\n */\n apps?: FollowingFeedApp[];\n /** Set only when `items` is empty. */\n reason?: FollowingFeedEmptyReason;\n /** When this feed was built (ISO 8601). Absent on a `building` answer. */\n builtAt?: string;\n /**\n * True when this is an older build, served while a newer one is built in\n * the background. Poll {@link fetchFollowingFeedVersion} to learn when the\n * newer one is ready.\n */\n stale?: boolean;\n}\n\n/** Query string and auth headers shared by the feed and its version check. */\nasync function followingFeedRequest(config: SifaApiConfig, opts: FetchFollowingFeedOptions) {\n const params = new URLSearchParams();\n if (opts.limit) params.set('limit', String(opts.limit));\n // A selected tab decides Bluesky on its own, so the flag is not also sent.\n if (opts.app) params.set('app', opts.app);\n else if (opts.includeBluesky) params.set('includeBluesky', 'true');\n const qs = params.toString();\n\n const headers: Record<string, string> = { ...(opts.headers ?? {}) };\n if (opts.cookieHeader) headers.cookie = opts.cookieHeader;\n if (config.getAuthToken) {\n const token = await config.getAuthToken(FOLLOWING_FEED_LXM);\n if (token) headers.Authorization = `Bearer ${token}`;\n }\n return { query: qs ? `?${qs}` : '', headers };\n}\n\n/**\n * The viewer's cross-app following feed: what OTHER apps their connections use\n * (Tangled, WhiteWind, Smoke Signal, ...) — Bluesky is excluded server-side by\n * default, as it is the least interesting part and other apps already surface\n * it. Pass `includeBluesky: true` to bring it back in.\n *\n * Auth is identity-only server-side, so the NATIVE app supplies\n * `config.getAuthToken` to mint a service-auth Bearer, while WEB relies on its\n * session cookie (`credentials: 'include'`). Returns `null` on error. v1 is a\n * finite first page (no cursor yet).\n */\nexport async function fetchFollowingFeed(\n config: SifaApiConfig,\n opts: FetchFollowingFeedOptions = {},\n): Promise<FollowingFeedResponse | null> {\n const { query, headers } = await followingFeedRequest(config, opts);\n try {\n return await apiFetch<FollowingFeedResponse>(config, `/api/following/feed${query}`, {\n credentials: 'include',\n cache: 'no-store',\n ...opts,\n headers,\n });\n } catch {\n return null;\n }\n}\n\n/** When the cached following feed for a query was built. */\nexport interface FollowingFeedVersion {\n /** ISO 8601, or null when nothing is cached for this query. */\n builtAt: string | null;\n}\n\n/**\n * When the cached following feed for the same query was built. Cheap: it never\n * builds a feed. A client showing a `stale` feed polls this and offers the\n * newer one once {@link isNewerFollowingFeed} says so. Returns `null` on error.\n */\nexport async function fetchFollowingFeedVersion(\n config: SifaApiConfig,\n opts: FetchFollowingFeedOptions = {},\n): Promise<FollowingFeedVersion | null> {\n const { query, headers } = await followingFeedRequest(config, opts);\n try {\n return await apiFetch<FollowingFeedVersion>(config, `/api/following/feed/version${query}`, {\n credentials: 'include',\n cache: 'no-store',\n ...opts,\n headers,\n });\n } catch {\n return null;\n }\n}\n\n/** True when `latestBuiltAt` is a newer build than the one shown (`shownBuiltAt`). */\nexport function isNewerFollowingFeed(\n shownBuiltAt: string | null | undefined,\n latestBuiltAt: string | null | undefined,\n): boolean {\n if (!shownBuiltAt || !latestBuiltAt) return false;\n return Date.parse(latestBuiltAt) > Date.parse(shownBuiltAt);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,16 +6,27 @@ var zod = require('zod');
|
|
|
6
6
|
var ApiError = class extends Error {
|
|
7
7
|
status;
|
|
8
8
|
body;
|
|
9
|
-
|
|
9
|
+
/** Seconds until a rate limit resets, read from the headers of a 429. */
|
|
10
|
+
retryAfterSeconds;
|
|
11
|
+
constructor(message, status, body, retryAfterSeconds) {
|
|
10
12
|
super(message);
|
|
11
13
|
this.name = "ApiError";
|
|
12
14
|
this.status = status;
|
|
13
15
|
this.body = body;
|
|
16
|
+
if (retryAfterSeconds !== void 0) this.retryAfterSeconds = retryAfterSeconds;
|
|
14
17
|
}
|
|
15
18
|
};
|
|
16
19
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
17
20
|
var MAX_RATE_LIMIT_RETRIES = 3;
|
|
18
21
|
var RATE_LIMIT_RETRY_CAP_SECONDS = 3;
|
|
22
|
+
var RATE_LIMIT_RESET_HEADERS = ["retry-after", "x-ratelimit-reset"];
|
|
23
|
+
function parseRetryAfterSeconds(headers) {
|
|
24
|
+
for (const name of RATE_LIMIT_RESET_HEADERS) {
|
|
25
|
+
const seconds = Number.parseInt(headers.get(name) ?? "", 10);
|
|
26
|
+
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
27
|
+
}
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
19
30
|
function encodeIdentifier(value) {
|
|
20
31
|
let decoded = value;
|
|
21
32
|
try {
|
|
@@ -67,7 +78,12 @@ async function apiFetch(config, path, options = {}) {
|
|
|
67
78
|
errBody = void 0;
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
|
-
throw new ApiError(
|
|
81
|
+
throw new ApiError(
|
|
82
|
+
`Sifa API ${res.status} on ${path}`,
|
|
83
|
+
res.status,
|
|
84
|
+
errBody,
|
|
85
|
+
res.status === 429 ? parseRetryAfterSeconds(res.headers) : void 0
|
|
86
|
+
);
|
|
71
87
|
}
|
|
72
88
|
if (res.status === 204 || res.status === 205) {
|
|
73
89
|
return void 0;
|
|
@@ -84,10 +100,14 @@ async function apiFetchOrNull(config, path, options = {}) {
|
|
|
84
100
|
throw e;
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
|
-
function extractWriteError(
|
|
88
|
-
const body =
|
|
103
|
+
function extractWriteError(e) {
|
|
104
|
+
const body = e.body && typeof e.body === "object" ? e.body : {};
|
|
89
105
|
return {
|
|
90
|
-
|
|
106
|
+
// Fastify puts the detail in `message` and the status phrase in `error`;
|
|
107
|
+
// sifa-api's own handlers (and the rate limiter) only set `error`.
|
|
108
|
+
error: body.message ?? body.error ?? `Request failed (${e.status})`,
|
|
109
|
+
status: e.status,
|
|
110
|
+
...e.retryAfterSeconds !== void 0 ? { retryAfterSeconds: e.retryAfterSeconds } : {},
|
|
91
111
|
...body.pdsHost ? { pdsHost: body.pdsHost } : {}
|
|
92
112
|
};
|
|
93
113
|
}
|
|
@@ -101,7 +121,7 @@ async function apiWrite(config, path, method, options = {}) {
|
|
|
101
121
|
return { success: true, ...data ?? {} };
|
|
102
122
|
} catch (e) {
|
|
103
123
|
if (e instanceof ApiError) {
|
|
104
|
-
return { success: false, ...extractWriteError(e
|
|
124
|
+
return { success: false, ...extractWriteError(e) };
|
|
105
125
|
}
|
|
106
126
|
return { success: false, error: "Network error" };
|
|
107
127
|
}
|