@singi-labs/sifa-sdk 0.23.0 → 0.23.1

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.
Files changed (57) hide show
  1. package/dist/badge/index.cjs.map +1 -1
  2. package/dist/{follow-profile-DGZzB5iM.d.ts → follow-profile-CCL1tT1x.d.ts} +1 -1
  3. package/dist/{follow-profile-BG_6eZwW.d.cts → follow-profile-RijPgyp-.d.cts} +1 -1
  4. package/dist/{index-BR8LDBo1.d.ts → index-Bcy443Qi.d.ts} +1 -1
  5. package/dist/{index-BKvI1Bvg.d.cts → index-Ck5zdubP.d.cts} +1 -1
  6. package/dist/index.cjs +25 -6
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +9 -9
  9. package/dist/index.d.ts +9 -9
  10. package/dist/index.js +15 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/jsonld/index.d.cts +2 -2
  13. package/dist/jsonld/index.d.ts +2 -2
  14. package/dist/{network-map-CMyjk6jc.d.ts → network-map-BGO1feWW.d.ts} +4 -4
  15. package/dist/{network-map-CqLYThmY.d.cts → network-map-DBiloRce.d.cts} +4 -4
  16. package/dist/{profile-summary-Sv6rqQbx.d.ts → profile-summary-5jNVFUR9.d.ts} +1 -1
  17. package/dist/{profile-summary-CoTNGDpt.d.cts → profile-summary-DJVnLbFV.d.cts} +1 -1
  18. package/dist/publishing/index.cjs +15 -5
  19. package/dist/publishing/index.cjs.map +1 -1
  20. package/dist/query/client.js +20 -1
  21. package/dist/query/client.js.map +1 -1
  22. package/dist/query/fetchers/index.cjs +35 -6
  23. package/dist/query/fetchers/index.cjs.map +1 -1
  24. package/dist/query/fetchers/index.d.cts +8 -8
  25. package/dist/query/fetchers/index.d.ts +8 -8
  26. package/dist/query/hooks/index.cjs +35 -6
  27. package/dist/query/hooks/index.cjs.map +1 -1
  28. package/dist/query/hooks/index.d.cts +6 -6
  29. package/dist/query/hooks/index.d.ts +6 -6
  30. package/dist/query/index.cjs +20 -1
  31. package/dist/query/index.cjs.map +1 -1
  32. package/dist/query/index.d.cts +11 -11
  33. package/dist/query/index.d.ts +11 -11
  34. package/dist/query/index.js.map +1 -1
  35. package/dist/resume/index.d.cts +2 -2
  36. package/dist/resume/index.d.ts +2 -2
  37. package/dist/schemas/index.cjs +24 -5
  38. package/dist/schemas/index.cjs.map +1 -1
  39. package/dist/schemas/index.d.cts +2 -2
  40. package/dist/schemas/index.d.ts +2 -2
  41. package/dist/schemas/index.js +14 -0
  42. package/dist/schemas/index.js.map +1 -1
  43. package/dist/schemas/shared.js +18 -5
  44. package/dist/schemas/shared.js.map +1 -1
  45. package/dist/schemas/write/index.cjs +15 -5
  46. package/dist/schemas/write/index.cjs.map +1 -1
  47. package/dist/testing/index.d.cts +2 -2
  48. package/dist/testing/index.d.ts +2 -2
  49. package/dist/{types-CcVRIHcX.d.ts → types-4MZHuQif.d.cts} +15 -3
  50. package/dist/{types-BC4A9-W9.d.cts → types-DJrmm7p0.d.ts} +15 -3
  51. package/dist/{use-user-settings-CI9eZQpp.d.ts → use-user-settings-BfGwI4x3.d.ts} +4 -4
  52. package/dist/{use-user-settings-CfQKSBAC.d.cts → use-user-settings-g8ATc9zm.d.cts} +4 -4
  53. package/dist/{user-settings-C65eFsPR.d.ts → user-settings-C3_y8_8T.d.ts} +4 -4
  54. package/dist/{user-settings-BVFxLAOa.d.cts → user-settings-CuqhXuTE.d.cts} +4 -4
  55. package/dist/{verification-providers-DQLIOSCK.d.cts → verification-providers-Dq6ni6e-.d.cts} +52 -1
  56. package/dist/{verification-providers-DQLIOSCK.d.ts → verification-providers-Dq6ni6e-.d.ts} +52 -1
  57. package/package.json +4 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/query/client.ts"],"sourcesContent":["/**\n * Foundation HTTP client for talking to the Sifa AppView.\n *\n * Stateless. Consumers supply a {@link SifaApiConfig} per call (the React\n * hooks read it from context; non-React consumers pass it explicitly). No\n * singletons, no module-level state.\n */\n\n/** Configuration passed to every fetcher. */\nexport interface SifaApiConfig {\n /** Base URL of the sifa-api AppView, e.g. `https://api.sifa.id`. No trailing slash. */\n baseUrl: string;\n /**\n * Optional fetch implementation. Defaults to {@link globalThis.fetch}.\n * Lets Next.js consumers pass their cache-enhanced fetch; node/Expo\n * consumers can leave this unset.\n */\n fetch?: typeof fetch;\n /**\n * Optional per-request auth-token minter for auth-required reads. The NATIVE\n * app supplies this (it has no sifa-api session cookie): given a method NSID\n * (`lxm`), it mints a short-lived AT Protocol service-auth JWT on the user's\n * PDS (see the SDK's `/atproto` `mintServiceAuthToken`) and returns it; the\n * fetcher attaches it as `Authorization: Bearer`. Web omits this and relies on\n * its session cookie (`credentials: 'include'`). Return `null` to send no\n * token. See `sifa-api` `createIdentityMiddleware`.\n */\n getAuthToken?: (lxm: string) => Promise<string | null>;\n}\n\n/** Options accepted by {@link apiFetch}. */\nexport interface ApiFetchOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n /** Request body. Serialized to JSON automatically. */\n body?: unknown;\n /** AbortSignal. Defaults to `AbortSignal.timeout(timeoutMs)` if `timeoutMs` is set. */\n signal?: AbortSignal;\n /** Per-call timeout in milliseconds. Default: 10_000. Ignored if `signal` is provided. */\n timeoutMs?: number;\n /** Retry on HTTP 429 up to 3 times with the server's `Retry-After` delay (capped at 3s). */\n retryOn429?: boolean;\n /** Additional headers. `Content-Type: application/json` is set automatically when `body` is present. */\n headers?: Record<string, string>;\n credentials?: RequestCredentials;\n cache?: RequestCache;\n /**\n * Next.js-specific cache hints. Ignored on non-Next runtimes. Passed\n * through transparently as part of {@link RequestInit}.\n */\n next?: { revalidate?: number | false; tags?: string[] };\n}\n\n/** Error thrown by {@link apiFetch} on non-2xx responses. */\nexport class ApiError extends Error {\n readonly status: number;\n readonly body: unknown;\n /** Seconds until a rate limit resets, read from the headers of a 429. */\n readonly retryAfterSeconds?: number;\n\n constructor(message: string, status: number, body?: unknown, retryAfterSeconds?: number) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.body = body;\n if (retryAfterSeconds !== undefined) this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst MAX_RATE_LIMIT_RETRIES = 3;\nconst RATE_LIMIT_RETRY_CAP_SECONDS = 3;\n\n/**\n * Headers sifa-api (`@fastify/rate-limit`) sets on a 429, in preference order.\n * Both carry an integer count of seconds until the limit resets.\n */\nconst RATE_LIMIT_RESET_HEADERS = ['retry-after', 'x-ratelimit-reset'] as const;\n\n/**\n * Seconds to wait before retrying a rate-limited response, or `undefined` when\n * no header carries a positive integer (`Retry-After` may also be an HTTP\n * date, which sifa-api never sends).\n */\nfunction parseRetryAfterSeconds(headers: Headers): number | undefined {\n for (const name of RATE_LIMIT_RESET_HEADERS) {\n const seconds = Number.parseInt(headers.get(name) ?? '', 10);\n if (Number.isFinite(seconds) && seconds > 0) return seconds;\n }\n return undefined;\n}\n\n/**\n * Encode a handle or DID for safe interpolation into a request path,\n * idempotently.\n *\n * Plain {@link encodeURIComponent} double-encodes an already-encoded\n * identifier: Next.js hands route params to RSC pages percent-encoded on a\n * hard navigation (`did%3Aplc%3A...`) but decoded on client-side navigation\n * (`did:plc:...`). Encoding the former again turns `%` into `%25`, so the\n * AppView reads `did%3A...` as a literal handle, finds no match, and 404s —\n * which is why DID profile links broke on direct visits but worked when\n * clicked within the app.\n *\n * AT identifiers (handles are domain names, DIDs are `did:method:...`) never\n * contain a literal `%`, so decoding a decoded value is a no-op. A malformed\n * percent sequence (lone `%`) makes {@link decodeURIComponent} throw; in that\n * case we keep the original value so behaviour matches the prior bare\n * `encodeURIComponent`.\n */\nexport function encodeIdentifier(value: string): string {\n let decoded = value;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n // Malformed percent-encoding — fall back to the raw value.\n }\n return encodeURIComponent(decoded);\n}\n\n/**\n * Generic fetcher used by all SDK query and mutation functions.\n *\n * Returns parsed JSON typed as `T`. Throws {@link ApiError} on non-2xx\n * responses. Use {@link apiFetchOrNull} when 404 should resolve to `null`\n * instead.\n */\nexport async function apiFetch<T = unknown>(\n config: SifaApiConfig,\n path: string,\n options: ApiFetchOptions = {},\n): Promise<T> {\n const fetchFn = config.fetch ?? globalThis.fetch;\n const url = `${config.baseUrl}${path}`;\n const maxRetries = options.retryOn429 ? MAX_RATE_LIMIT_RETRIES : 0;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const signal = options.signal ?? AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n\n const headers: Record<string, string> = { ...(options.headers ?? {}) };\n let body: BodyInit | undefined;\n if (options.body !== undefined) {\n headers['Content-Type'] ??= 'application/json';\n body = JSON.stringify(options.body);\n }\n\n // `next` is a Next.js extension to RequestInit; cast through.\n const init = {\n method: options.method ?? 'GET',\n headers,\n body,\n signal,\n credentials: options.credentials,\n cache: options.cache,\n ...(options.next ? { next: options.next } : {}),\n } as RequestInit;\n\n const res = await fetchFn(url, init);\n\n if (res.status === 429 && attempt < maxRetries) {\n const retryAfterRaw = res.headers.get('retry-after');\n const retryAfter = retryAfterRaw ? Number.parseInt(retryAfterRaw, 10) : 2;\n const waitSeconds = Math.min(\n Number.isFinite(retryAfter) ? retryAfter : 2,\n RATE_LIMIT_RETRY_CAP_SECONDS,\n );\n await new Promise((r) => setTimeout(r, waitSeconds * 1000));\n continue;\n }\n\n if (!res.ok) {\n let errBody: unknown;\n try {\n errBody = (await res.json()) as unknown;\n } catch {\n try {\n errBody = await res.text();\n } catch {\n errBody = undefined;\n }\n }\n throw new ApiError(\n `Sifa API ${res.status} on ${path}`,\n res.status,\n errBody,\n res.status === 429 ? parseRetryAfterSeconds(res.headers) : undefined,\n );\n }\n\n // 204 and 205 are defined to carry no body, so res.json() throws on them.\n // apiWrite treats that throw as a failed write, which turned every\n // successful no-content response (roadmap unvote, remove reaction,\n // dismiss endorsement) into a reported error.\n if (res.status === 204 || res.status === 205) {\n return undefined as T;\n }\n\n return (await res.json()) as T;\n }\n\n throw new ApiError(`Sifa API exhausted retries on ${path}`, 429);\n}\n\n/**\n * Variant of {@link apiFetch} that resolves to `null` on HTTP 404 instead\n * of throwing. Useful for \"fetch by handle\" reads where missing is\n * expected (e.g. unknown profile).\n */\nexport async function apiFetchOrNull<T>(\n config: SifaApiConfig,\n path: string,\n options: ApiFetchOptions = {},\n): Promise<T | null> {\n try {\n return await apiFetch<T>(config, path, options);\n } catch (e) {\n if (e instanceof ApiError && e.status === 404) return null;\n throw e;\n }\n}\n\n/**\n * Failed branch of a {@link WriteResult}.\n *\n * Never throws -- writes against the user's PDS can fail in many ways\n * (network, PDS unreachable, rate limit) and the UI needs structured\n * results to render appropriate messages.\n */\nexport interface WriteFailure {\n success: false;\n error?: string;\n /**\n * PDS hostname returned by sifa-api when a write failed at the user's\n * Personal Data Server (issue #167). Lets the UI render\n * \"Your data server (eurosky.social) isn't responding\" instead of a\n * generic \"Request failed (500)\".\n */\n pdsHost?: string;\n /** HTTP status of a failed write. Absent on network errors. */\n status?: number;\n /**\n * Seconds until the rate limit resets, when a write was rejected with 429\n * and the server said how long to wait (sifa-workspace#533). Lets the UI say\n * \"try again in N seconds\" instead of a generic failure.\n */\n retryAfterSeconds?: number;\n /**\n * Stable, machine-readable error code from the response body (for example\n * `'CustomDomainRequired'`), when sifa-api sends one (sifa-workspace#736).\n * Branch on this instead of matching the `error` text, which can change.\n * Present only when the body carries a non-empty string `code`.\n */\n code?: string;\n}\n\n/**\n * Successful branch of a {@link WriteResult}: the response body fields\n * (`TExtra`) plus the optional failure fields. A body field that shares a name\n * with a failure field (e.g. a body `status`) keeps its body type here, so it\n * never collides with the HTTP status on the failure branch.\n */\nexport type WriteSuccess<TExtra extends object = Record<never, never>> = { success: true } & Omit<\n WriteFailure,\n 'success' | keyof TExtra\n> &\n TExtra;\n\n/**\n * Result returned by record-write mutations (create / update / delete),\n * discriminated on `success`. Narrow on `success` to read a body `status`\n * (success) or the HTTP `status` (failure). Other fields stay readable without\n * narrowing.\n */\nexport type WriteResult<TExtra extends object = Record<never, never>> =\n WriteSuccess<TExtra> | (WriteFailure & Partial<Omit<TExtra, keyof WriteFailure>>);\n\n/** Result returned by create mutations. Includes the newly created `rkey`. */\nexport type CreateResult = WriteResult<{ rkey?: string }>;\n\ninterface ApiErrorBody {\n message?: string;\n error?: string;\n pdsHost?: string;\n code?: unknown;\n}\n\nfunction extractWriteError(e: ApiError): Omit<WriteFailure, 'success'> {\n const body = (e.body && typeof e.body === 'object' ? e.body : {}) as ApiErrorBody;\n return {\n // Fastify puts the detail in `message` and the status phrase in `error`;\n // sifa-api's own handlers (and the rate limiter) only set `error`.\n error: body.message ?? body.error ?? `Request failed (${e.status})`,\n status: e.status,\n ...(e.retryAfterSeconds !== undefined ? { retryAfterSeconds: e.retryAfterSeconds } : {}),\n ...(body.pdsHost ? { pdsHost: body.pdsHost } : {}),\n ...(typeof body.code === 'string' && body.code !== '' ? { code: body.code } : {}),\n };\n}\n\n/**\n * Write mutation against the Sifa AppView. Wraps {@link apiFetch} with the\n * never-throws contract used by all sifa-web mutations: returns a\n * structured {@link WriteResult} on both success and failure, and\n * preserves the `pdsHost` field when the AppView reports a PDS-side\n * failure (issue #167) and the `code` field when the AppView sends a\n * stable error code.\n *\n * On success: returns `{ success: true, ...payload }` where `payload` is\n * whatever the server returned in JSON (or `{}` for 204).\n *\n * On failure: returns `{ success: false, error, status?, retryAfterSeconds?,\n * pdsHost?, code? }`. Never throws.\n *\n * Use {@link apiWriteCreate} when you specifically need the `rkey` from a\n * create response folded into the result shape.\n */\nexport async function apiWrite<TExtra extends object = Record<never, never>>(\n config: SifaApiConfig,\n path: string,\n method: 'POST' | 'PUT' | 'DELETE' | 'PATCH',\n options: Omit<ApiFetchOptions, 'method'> = {},\n): Promise<WriteResult<TExtra>> {\n try {\n const data = await apiFetch<TExtra>(config, path, {\n method,\n credentials: 'include',\n ...options,\n });\n return { success: true, ...(data ?? ({} as TExtra)) };\n } catch (e) {\n if (e instanceof ApiError) {\n return { success: false, ...extractWriteError(e) };\n }\n return { success: false, error: 'Network error' };\n }\n}\n\n/**\n * Write mutation that expects the server to return a record key (`rkey`)\n * in its response body. Wraps {@link apiWrite} and folds the `rkey` into\n * the result shape so consumers get `{ success, rkey?, error?, pdsHost? }`.\n *\n * If the server returns additional fields (e.g. `feedUrl` from external\n * account creation), pass them via the `TExtra` generic to keep them\n * typed.\n */\nexport function apiWriteCreate<TExtra extends object = Record<never, never>>(\n config: SifaApiConfig,\n path: string,\n body: unknown,\n options: Omit<ApiFetchOptions, 'method' | 'body'> = {},\n): Promise<WriteResult<{ rkey?: string } & TExtra>> {\n return apiWrite<{ rkey?: string } & TExtra>(config, path, 'POST', {\n body,\n ...options,\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/query/client.ts"],"sourcesContent":["/**\n * Foundation HTTP client for talking to the Sifa AppView.\n *\n * Stateless. Consumers supply a {@link SifaApiConfig} per call (the React\n * hooks read it from context; non-React consumers pass it explicitly). No\n * singletons, no module-level state.\n */\n\n/** Configuration passed to every fetcher. */\nexport interface SifaApiConfig {\n /** Base URL of the sifa-api AppView, e.g. `https://api.sifa.id`. No trailing slash. */\n baseUrl: string;\n /**\n * Optional fetch implementation. Defaults to {@link globalThis.fetch}.\n * Lets Next.js consumers pass their cache-enhanced fetch; node/Expo\n * consumers can leave this unset.\n */\n fetch?: typeof fetch;\n /**\n * Optional per-request auth-token minter for auth-required reads. The NATIVE\n * app supplies this (it has no sifa-api session cookie): given a method NSID\n * (`lxm`), it mints a short-lived AT Protocol service-auth JWT on the user's\n * PDS (see the SDK's `/atproto` `mintServiceAuthToken`) and returns it; the\n * fetcher attaches it as `Authorization: Bearer`. Web omits this and relies on\n * its session cookie (`credentials: 'include'`). Return `null` to send no\n * token. See `sifa-api` `createIdentityMiddleware`.\n */\n getAuthToken?: (lxm: string) => Promise<string | null>;\n}\n\n/** Options accepted by {@link apiFetch}. */\nexport interface ApiFetchOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n /** Request body. Serialized to JSON automatically. */\n body?: unknown;\n /** AbortSignal. Defaults to `AbortSignal.timeout(timeoutMs)` if `timeoutMs` is set. */\n signal?: AbortSignal;\n /** Per-call timeout in milliseconds. Default: 10_000. Ignored if `signal` is provided. */\n timeoutMs?: number;\n /** Retry on HTTP 429 up to 3 times with the server's `Retry-After` delay (capped at 3s). */\n retryOn429?: boolean;\n /** Additional headers. `Content-Type: application/json` is set automatically when `body` is present. */\n headers?: Record<string, string>;\n credentials?: RequestCredentials;\n cache?: RequestCache;\n /**\n * Next.js-specific cache hints. Ignored on non-Next runtimes. Passed\n * through transparently as part of {@link RequestInit}.\n */\n next?: { revalidate?: number | false; tags?: string[] };\n}\n\n/** Error thrown by {@link apiFetch} on non-2xx responses. */\nexport class ApiError extends Error {\n readonly status: number;\n readonly body: unknown;\n /** Seconds until a rate limit resets, read from the headers of a 429. */\n readonly retryAfterSeconds?: number;\n\n constructor(message: string, status: number, body?: unknown, retryAfterSeconds?: number) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.body = body;\n if (retryAfterSeconds !== undefined) this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst MAX_RATE_LIMIT_RETRIES = 3;\nconst RATE_LIMIT_RETRY_CAP_SECONDS = 3;\n\n/**\n * Headers sifa-api (`@fastify/rate-limit`) sets on a 429, in preference order.\n * Both carry an integer count of seconds until the limit resets.\n */\nconst RATE_LIMIT_RESET_HEADERS = ['retry-after', 'x-ratelimit-reset'] as const;\n\n/**\n * Seconds to wait before retrying a rate-limited response, or `undefined` when\n * no header carries a positive integer (`Retry-After` may also be an HTTP\n * date, which sifa-api never sends).\n */\nfunction parseRetryAfterSeconds(headers: Headers): number | undefined {\n for (const name of RATE_LIMIT_RESET_HEADERS) {\n const seconds = Number.parseInt(headers.get(name) ?? '', 10);\n if (Number.isFinite(seconds) && seconds > 0) return seconds;\n }\n return undefined;\n}\n\n/**\n * Encode a handle or DID for safe interpolation into a request path,\n * idempotently.\n *\n * Plain {@link encodeURIComponent} double-encodes an already-encoded\n * identifier: Next.js hands route params to RSC pages percent-encoded on a\n * hard navigation (`did%3Aplc%3A...`) but decoded on client-side navigation\n * (`did:plc:...`). Encoding the former again turns `%` into `%25`, so the\n * AppView reads `did%3A...` as a literal handle, finds no match, and 404s —\n * which is why DID profile links broke on direct visits but worked when\n * clicked within the app.\n *\n * AT identifiers (handles are domain names, DIDs are `did:method:...`) never\n * contain a literal `%`, so decoding a decoded value is a no-op. A malformed\n * percent sequence (lone `%`) makes {@link decodeURIComponent} throw; in that\n * case we keep the original value so behaviour matches the prior bare\n * `encodeURIComponent`.\n */\nexport function encodeIdentifier(value: string): string {\n let decoded = value;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n // Malformed percent-encoding — fall back to the raw value.\n }\n return encodeURIComponent(decoded);\n}\n\n/**\n * Generic fetcher used by all SDK query and mutation functions.\n *\n * Returns parsed JSON typed as `T`. Throws {@link ApiError} on non-2xx\n * responses. Use {@link apiFetchOrNull} when 404 should resolve to `null`\n * instead.\n */\nexport async function apiFetch<T = unknown>(\n config: SifaApiConfig,\n path: string,\n options: ApiFetchOptions = {},\n): Promise<T> {\n const fetchFn = config.fetch ?? globalThis.fetch;\n const url = `${config.baseUrl}${path}`;\n const maxRetries = options.retryOn429 ? MAX_RATE_LIMIT_RETRIES : 0;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const signal = options.signal ?? AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n\n const headers: Record<string, string> = { ...(options.headers ?? {}) };\n let body: BodyInit | undefined;\n if (options.body !== undefined) {\n headers['Content-Type'] ??= 'application/json';\n body = JSON.stringify(options.body);\n }\n\n // `next` is a Next.js extension to RequestInit; cast through.\n const init = {\n method: options.method ?? 'GET',\n headers,\n body,\n signal,\n credentials: options.credentials,\n cache: options.cache,\n ...(options.next ? { next: options.next } : {}),\n } as RequestInit;\n\n const res = await fetchFn(url, init);\n\n if (res.status === 429 && attempt < maxRetries) {\n const retryAfterRaw = res.headers.get('retry-after');\n const retryAfter = retryAfterRaw ? Number.parseInt(retryAfterRaw, 10) : 2;\n const waitSeconds = Math.min(\n Number.isFinite(retryAfter) ? retryAfter : 2,\n RATE_LIMIT_RETRY_CAP_SECONDS,\n );\n await new Promise((r) => setTimeout(r, waitSeconds * 1000));\n continue;\n }\n\n if (!res.ok) {\n let errBody: unknown;\n try {\n errBody = (await res.json()) as unknown;\n } catch {\n try {\n errBody = await res.text();\n } catch {\n errBody = undefined;\n }\n }\n throw new ApiError(\n `Sifa API ${res.status} on ${path}`,\n res.status,\n errBody,\n res.status === 429 ? parseRetryAfterSeconds(res.headers) : undefined,\n );\n }\n\n // 204 and 205 are defined to carry no body, so res.json() throws on them.\n // apiWrite treats that throw as a failed write, which turned every\n // successful no-content response (roadmap unvote, remove reaction,\n // dismiss endorsement) into a reported error.\n if (res.status === 204 || res.status === 205) {\n return undefined as T;\n }\n\n return (await res.json()) as T;\n }\n\n throw new ApiError(`Sifa API exhausted retries on ${path}`, 429);\n}\n\n/**\n * Variant of {@link apiFetch} that resolves to `null` on HTTP 404 instead\n * of throwing. Useful for \"fetch by handle\" reads where missing is\n * expected (e.g. unknown profile).\n */\nexport async function apiFetchOrNull<T>(\n config: SifaApiConfig,\n path: string,\n options: ApiFetchOptions = {},\n): Promise<T | null> {\n try {\n return await apiFetch<T>(config, path, options);\n } catch (e) {\n if (e instanceof ApiError && e.status === 404) return null;\n throw e;\n }\n}\n\n/**\n * Failed branch of a {@link WriteResult}.\n *\n * Never throws -- writes against the user's PDS can fail in many ways\n * (network, PDS unreachable, rate limit) and the UI needs structured\n * results to render appropriate messages.\n */\nexport interface WriteFailure {\n success: false;\n error?: string;\n /**\n * PDS hostname returned by sifa-api when a write failed at the user's\n * Personal Data Server (issue #167). Lets the UI render\n * \"Your data server (eurosky.social) isn't responding\" instead of a\n * generic \"Request failed (500)\".\n */\n pdsHost?: string;\n /** HTTP status of a failed write. Absent on network errors. */\n status?: number;\n /**\n * Seconds until the rate limit resets, when a write was rejected with 429\n * and the server said how long to wait (sifa-workspace#533). Lets the UI say\n * \"try again in N seconds\" instead of a generic failure.\n */\n retryAfterSeconds?: number;\n /**\n * Stable, machine-readable error code from the response body (for example\n * `'CustomDomainRequired'`), when sifa-api sends one (sifa-workspace#736).\n * Branch on this instead of matching the `error` text, which can change.\n * Present only when the body carries a non-empty string `code`.\n */\n code?: string;\n /**\n * Field-level validation problems from a 400 `ValidationError` body (the\n * Zod `error.issues` of sifa-api). Present only when the body carries an\n * array; malformed entries are dropped. `path` locates the field.\n */\n issues?: WriteIssue[];\n}\n\n/** One validation problem reported by sifa-api, shaped like a Zod issue. */\nexport interface WriteIssue {\n path: (string | number)[];\n message: string;\n code?: string;\n}\n\n/**\n * Successful branch of a {@link WriteResult}: the response body fields\n * (`TExtra`) plus the optional failure fields. A body field that shares a name\n * with a failure field (e.g. a body `status`) keeps its body type here, so it\n * never collides with the HTTP status on the failure branch.\n */\nexport type WriteSuccess<TExtra extends object = Record<never, never>> = { success: true } & Omit<\n WriteFailure,\n 'success' | keyof TExtra\n> &\n TExtra;\n\n/**\n * Result returned by record-write mutations (create / update / delete),\n * discriminated on `success`. Narrow on `success` to read a body `status`\n * (success) or the HTTP `status` (failure). Other fields stay readable without\n * narrowing.\n */\nexport type WriteResult<TExtra extends object = Record<never, never>> =\n WriteSuccess<TExtra> | (WriteFailure & Partial<Omit<TExtra, keyof WriteFailure>>);\n\n/** Result returned by create mutations. Includes the newly created `rkey`. */\nexport type CreateResult = WriteResult<{ rkey?: string }>;\n\ninterface ApiErrorBody {\n message?: string;\n error?: string;\n pdsHost?: string;\n code?: unknown;\n issues?: unknown;\n}\n\nfunction parseWriteIssues(raw: unknown): WriteIssue[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const issues: WriteIssue[] = [];\n for (const entry of raw as unknown[]) {\n if (!entry || typeof entry !== 'object') continue;\n const { path, message, code } = entry as { path?: unknown; message?: unknown; code?: unknown };\n if (typeof message !== 'string' || !Array.isArray(path)) continue;\n const segments: unknown[] = path;\n if (!segments.every((p) => typeof p === 'string' || typeof p === 'number')) continue;\n issues.push({\n path: segments,\n message,\n ...(typeof code === 'string' ? { code } : {}),\n });\n }\n return issues;\n}\n\nfunction extractWriteError(e: ApiError): Omit<WriteFailure, 'success'> {\n const body = (e.body && typeof e.body === 'object' ? e.body : {}) as ApiErrorBody;\n const issues = parseWriteIssues(body.issues);\n return {\n // Fastify puts the detail in `message` and the status phrase in `error`;\n // sifa-api's own handlers (and the rate limiter) only set `error`.\n error: body.message ?? body.error ?? `Request failed (${e.status})`,\n status: e.status,\n ...(e.retryAfterSeconds !== undefined ? { retryAfterSeconds: e.retryAfterSeconds } : {}),\n ...(body.pdsHost ? { pdsHost: body.pdsHost } : {}),\n ...(typeof body.code === 'string' && body.code !== '' ? { code: body.code } : {}),\n ...(issues ? { issues } : {}),\n };\n}\n\n/**\n * Write mutation against the Sifa AppView. Wraps {@link apiFetch} with the\n * never-throws contract used by all sifa-web mutations: returns a\n * structured {@link WriteResult} on both success and failure, and\n * preserves the `pdsHost` field when the AppView reports a PDS-side\n * failure (issue #167) and the `code` field when the AppView sends a\n * stable error code.\n *\n * On success: returns `{ success: true, ...payload }` where `payload` is\n * whatever the server returned in JSON (or `{}` for 204).\n *\n * On failure: returns `{ success: false, error, status?, retryAfterSeconds?,\n * pdsHost?, code? }`. Never throws.\n *\n * Use {@link apiWriteCreate} when you specifically need the `rkey` from a\n * create response folded into the result shape.\n */\nexport async function apiWrite<TExtra extends object = Record<never, never>>(\n config: SifaApiConfig,\n path: string,\n method: 'POST' | 'PUT' | 'DELETE' | 'PATCH',\n options: Omit<ApiFetchOptions, 'method'> = {},\n): Promise<WriteResult<TExtra>> {\n try {\n const data = await apiFetch<TExtra>(config, path, {\n method,\n credentials: 'include',\n ...options,\n });\n return { success: true, ...(data ?? ({} as TExtra)) };\n } catch (e) {\n if (e instanceof ApiError) {\n return { success: false, ...extractWriteError(e) };\n }\n return { success: false, error: 'Network error' };\n }\n}\n\n/**\n * Write mutation that expects the server to return a record key (`rkey`)\n * in its response body. Wraps {@link apiWrite} and folds the `rkey` into\n * the result shape so consumers get `{ success, rkey?, error?, pdsHost? }`.\n *\n * If the server returns additional fields (e.g. `feedUrl` from external\n * account creation), pass them via the `TExtra` generic to keep them\n * typed.\n */\nexport function apiWriteCreate<TExtra extends object = Record<never, never>>(\n config: SifaApiConfig,\n path: string,\n body: unknown,\n options: Omit<ApiFetchOptions, 'method' | 'body'> = {},\n): Promise<WriteResult<{ rkey?: string } & TExtra>> {\n return apiWrite<{ rkey?: string } & TExtra>(config, path, 'POST', {\n body,\n ...options,\n });\n}\n"],"mappings":"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","names":[]}
@@ -100,8 +100,26 @@ async function apiFetchOrNull(config, path, options = {}) {
100
100
  throw e;
101
101
  }
102
102
  }
103
+ function parseWriteIssues(raw) {
104
+ if (!Array.isArray(raw)) return void 0;
105
+ const issues = [];
106
+ for (const entry of raw) {
107
+ if (!entry || typeof entry !== "object") continue;
108
+ const { path, message, code } = entry;
109
+ if (typeof message !== "string" || !Array.isArray(path)) continue;
110
+ const segments = path;
111
+ if (!segments.every((p) => typeof p === "string" || typeof p === "number")) continue;
112
+ issues.push({
113
+ path: segments,
114
+ message,
115
+ ...typeof code === "string" ? { code } : {}
116
+ });
117
+ }
118
+ return issues;
119
+ }
103
120
  function extractWriteError(e) {
104
121
  const body = e.body && typeof e.body === "object" ? e.body : {};
122
+ const issues = parseWriteIssues(body.issues);
105
123
  return {
106
124
  // Fastify puts the detail in `message` and the status phrase in `error`;
107
125
  // sifa-api's own handlers (and the rate limiter) only set `error`.
@@ -109,7 +127,8 @@ function extractWriteError(e) {
109
127
  status: e.status,
110
128
  ...e.retryAfterSeconds !== void 0 ? { retryAfterSeconds: e.retryAfterSeconds } : {},
111
129
  ...body.pdsHost ? { pdsHost: body.pdsHost } : {},
112
- ...typeof body.code === "string" && body.code !== "" ? { code: body.code } : {}
130
+ ...typeof body.code === "string" && body.code !== "" ? { code: body.code } : {},
131
+ ...issues ? { issues } : {}
113
132
  };
114
133
  }
115
134
  async function apiWrite(config, path, method, options = {}) {
@@ -2205,11 +2224,21 @@ function lexiconString(graphemes, bytes) {
2205
2224
  message: "This text is too long. Emoji and some scripts take more space, so shorten it a little."
2206
2225
  });
2207
2226
  }
2208
- lexiconString(64, 640);
2209
- lexiconString(64, 640);
2210
- lexiconString(20, 200);
2211
- lexiconString(120, 1200);
2212
- lexiconString(5e3, 5e4);
2227
+ var PROFILE_TEXT_LIMITS = {
2228
+ displayName: { graphemes: 64, bytes: 640 },
2229
+ givenName: { graphemes: 64, bytes: 640 },
2230
+ pronouns: { graphemes: 20, bytes: 200 },
2231
+ headline: { graphemes: 120, bytes: 1200 },
2232
+ about: { graphemes: 5e3, bytes: 5e4 }
2233
+ };
2234
+ function profileText(limit) {
2235
+ return lexiconString(limit.graphemes, limit.bytes);
2236
+ }
2237
+ profileText(PROFILE_TEXT_LIMITS.displayName);
2238
+ profileText(PROFILE_TEXT_LIMITS.givenName);
2239
+ profileText(PROFILE_TEXT_LIMITS.pronouns);
2240
+ profileText(PROFILE_TEXT_LIMITS.headline);
2241
+ profileText(PROFILE_TEXT_LIMITS.about);
2213
2242
  var didSchema = zod.z.string().regex(/^did:[a-z]+:[a-zA-Z0-9._:%-]+$/, "Invalid DID");
2214
2243
  var datetimeSchema = zod.z.string().datetime({ offset: true });
2215
2244
  var partialDateSchema = zod.z.string().regex(/^\d{4}(-\d{2}(-\d{2}(T.+)?)?)?$/, "Expected YYYY, YYYY-MM, YYYY-MM-DD, or a datetime");