{"version":3,"file":"search-continuation-Bb2FYfDN.mjs","names":[],"sources":["../src/core/search-continuation.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { InvalidSearchContinuationError } from \"./errors.ts\";\nimport type { SearchRequestOptions } from \"./types.ts\";\n\ninterface SearchContinuationPayload {\n  readonly version: 1;\n  readonly provider: string;\n  readonly requestFingerprint: string;\n  readonly providerContinuation: string;\n}\n\ninterface SearchContinuationEnvelope {\n  readonly payload: string;\n  readonly checksum: string;\n}\n\nexport const MAX_SEARCH_CONTINUATION_LENGTH = 4_096;\nexport const MAX_PROVIDER_SEARCH_CONTINUATION_LENGTH = 2_048;\n\nexport function encodeSearchContinuation(\n  provider: string,\n  query: string,\n  options: Readonly<SearchRequestOptions>,\n  providerContinuation: string,\n): string {\n  if (\n    !providerContinuation ||\n    providerContinuation.length > MAX_PROVIDER_SEARCH_CONTINUATION_LENGTH\n  ) {\n    throw new InvalidSearchContinuationError();\n  }\n  const payload: SearchContinuationPayload = {\n    version: 1,\n    provider,\n    requestFingerprint: requestFingerprint(query, options),\n    providerContinuation,\n  };\n  const serializedPayload = JSON.stringify(payload);\n  const envelope: SearchContinuationEnvelope = {\n    payload: serializedPayload,\n    checksum: continuationChecksum(serializedPayload),\n  };\n  const token = Buffer.from(JSON.stringify(envelope)).toString(\"base64url\");\n  if (token.length > MAX_SEARCH_CONTINUATION_LENGTH) {\n    throw new InvalidSearchContinuationError();\n  }\n  return token;\n}\n\nexport function decodeSearchContinuation(\n  token: string,\n  query: string,\n  options: Readonly<SearchRequestOptions>,\n): SearchContinuationPayload {\n  const payload = decodePayload(token);\n  if (payload.requestFingerprint !== requestFingerprint(query, options)) {\n    throw new InvalidSearchContinuationError();\n  }\n  return payload;\n}\n\nfunction decodePayload(token: string): SearchContinuationPayload {\n  if (!token || token.length > MAX_SEARCH_CONTINUATION_LENGTH) {\n    throw new InvalidSearchContinuationError();\n  }\n  try {\n    const envelope: unknown = JSON.parse(Buffer.from(token, \"base64url\").toString(\"utf8\"));\n    if (!isSearchContinuationEnvelope(envelope)) throw new InvalidSearchContinuationError();\n    if (envelope.checksum !== continuationChecksum(envelope.payload)) {\n      throw new InvalidSearchContinuationError();\n    }\n    const payload: unknown = JSON.parse(envelope.payload);\n    if (!isSearchContinuationPayload(payload)) throw new InvalidSearchContinuationError();\n    return payload;\n  } catch (error) {\n    if (error instanceof InvalidSearchContinuationError) throw error;\n    throw new InvalidSearchContinuationError();\n  }\n}\n\nfunction requestFingerprint(query: string, options: Readonly<SearchRequestOptions>): string {\n  return fingerprint(\n    JSON.stringify([\n      query,\n      valueOr(options.maxResults, 10),\n      valueOr(options.highlights, null),\n      valueOr(options.summary, null),\n      valueOr(options.fullText, null),\n      valueOr(options.includeDomains, null),\n      valueOr(options.excludeDomains, null),\n      valueOr(options.sources, null),\n      valueOr(options.categories, null),\n      valueOr(options.startPublishedDate, null),\n      valueOr(options.endPublishedDate, null),\n      valueOr(options.category, null),\n    ]),\n  );\n}\n\nfunction valueOr<TValue, TFallback>(\n  value: TValue | undefined,\n  fallback: TFallback,\n): TValue | TFallback {\n  return value ?? fallback;\n}\n\n/**\n * Calculate the envelope's corruption checksum; this is not an authorization signature.\n * @param payload - Serialized continuation payload.\n * @returns {string} Base64url-encoded SHA-256 digest.\n */\nfunction continuationChecksum(payload: string): string {\n  return fingerprint(`@agntn/web/search-continuation/v1\\0${payload}`);\n}\n\nfunction fingerprint(value: string): string {\n  return createHash(\"sha256\").update(value).digest(\"base64url\");\n}\n\nfunction isSearchContinuationEnvelope(value: unknown): value is SearchContinuationEnvelope {\n  if (typeof value !== \"object\" || value === null) return false;\n  const envelope = value as Readonly<Record<string, unknown>>;\n  return typeof envelope.payload === \"string\" && typeof envelope.checksum === \"string\";\n}\n\nfunction isSearchContinuationPayload(value: unknown): value is SearchContinuationPayload {\n  if (typeof value !== \"object\" || value === null) return false;\n  const payload = value as Readonly<Record<string, unknown>>;\n  return (\n    payload.version === 1 &&\n    isContinuationProvider(payload.provider) &&\n    isNonemptyString(payload.requestFingerprint) &&\n    isProviderContinuation(payload.providerContinuation)\n  );\n}\n\nfunction isContinuationProvider(value: unknown): value is string {\n  return typeof value === \"string\" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value);\n}\n\nfunction isNonemptyString(value: unknown): value is string {\n  return typeof value === \"string\" && value.length > 0;\n}\n\nfunction isProviderContinuation(value: unknown): value is string {\n  return isNonemptyString(value) && value.length <= MAX_PROVIDER_SEARCH_CONTINUATION_LENGTH;\n}\n"],"mappings":"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"}