uri.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /** Canonical session URI and inline mention encoding. */
  2. import { SessionId, type SessionId as SessionIdType } from '@deepseek-ai/dsh-session'
  3. import { SessionReferenceError } from './config.ts'
  4. import type { SessionReferenceInput } from './types.ts'
  5. /** URI scheme reserved for DeepSeek Harness session snapshots. */
  6. export const SESSION_REFERENCE_SCHEME = 'dsh-session:'
  7. /**
  8. * Encode any JavaScript session-id string as a canonical lossless URI.
  9. * @param sessionId - opaque session id to serialize.
  10. * @returns canonical `dsh-session:` URI.
  11. */
  12. export function encodeSessionReferenceUri(sessionId: SessionIdType): string {
  13. const payload = Buffer.from(JSON.stringify(sessionId), 'utf8').toString('base64url')
  14. return `${SESSION_REFERENCE_SCHEME}${payload}`
  15. }
  16. /**
  17. * Decode and canonicalize one session-reference URI.
  18. * @param uri - complete canonical URI.
  19. * @returns decoded session id.
  20. */
  21. export function decodeSessionReferenceUri(uri: string): SessionIdType {
  22. if (!uri.startsWith(SESSION_REFERENCE_SCHEME)) {
  23. throw invalidUri(uri)
  24. }
  25. const payload = uri.slice(SESSION_REFERENCE_SCHEME.length)
  26. if (!/^[A-Za-z0-9_-]+$/.test(payload)) throw invalidUri(uri)
  27. try {
  28. const parsed: unknown = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))
  29. if (typeof parsed !== 'string') throw new TypeError('decoded session id is not a string')
  30. const sessionId = SessionId(parsed)
  31. if (encodeSessionReferenceUri(sessionId) !== uri) throw new TypeError('URI is not canonical')
  32. return sessionId
  33. } catch (error: unknown) {
  34. throw invalidUri(uri, error)
  35. }
  36. }
  37. /**
  38. * Render a host-neutral Markdown mention carrying the canonical URI.
  39. * @param reference - structured id and optional display label.
  40. * @returns escaped `@[label](uri)` mention.
  41. */
  42. export function formatSessionReferenceMention(reference: SessionReferenceInput): string {
  43. const label = escapeLabel(reference.label ?? reference.sessionId)
  44. return `@[${label}](${encodeSessionReferenceUri(reference.sessionId)})`
  45. }
  46. /** Result of extracting canonical mentions from plain text. */
  47. export interface ParsedSessionReferenceText {
  48. /** Text with opaque tokens replaced by readable `@label` spans. */
  49. text: string
  50. /** Structured references in first-appearance order, before service deduplication. */
  51. references: SessionReferenceInput[]
  52. }
  53. /**
  54. * Extract Markdown mentions and bare canonical URIs from one text value.
  55. * Explicit Markdown mentions fail on any malformed URI. Bare text is treated
  56. * as a reference only when it has a non-empty base64url-shaped payload, then
  57. * still fails if that candidate is not canonical.
  58. * @param text - host text to normalize.
  59. * @returns readable text and structured references in appearance order.
  60. */
  61. export function parseSessionReferenceText(text: string): ParsedSessionReferenceText {
  62. const references: SessionReferenceInput[] = []
  63. const pattern = /@\[((?:\\.|[^\\\]])*)\]\((dsh-session:[^\s)]*)\)|(dsh-session:[A-Za-z0-9_-]+)/gu
  64. const rendered = text.replace(pattern, (
  65. _match,
  66. rawLabel: string | undefined,
  67. markdownUri: string | undefined,
  68. bareUri: string | undefined,
  69. ) => {
  70. const uri = markdownUri ?? bareUri
  71. /* v8 ignore next -- the two-alternative regex always captures exactly one URI group. */
  72. if (uri === undefined) throw new SessionReferenceError('session reference URI is missing', 'SESSION_REFERENCE_INVALID_REFERENCE')
  73. const sessionId = decodeSessionReferenceUri(uri)
  74. const label = rawLabel === undefined ? sessionId : unescapeLabel(rawLabel)
  75. references.push({ sessionId, label })
  76. return `@${label}`
  77. })
  78. return { text: rendered, references }
  79. }
  80. function escapeLabel(label: string): string {
  81. return label.replace(/[\\\]]/gu, match => `\\${match}`)
  82. }
  83. function unescapeLabel(label: string): string {
  84. return label.replace(/\\(.)/gu, '$1')
  85. }
  86. function invalidUri(uri: string, cause?: unknown): SessionReferenceError {
  87. return new SessionReferenceError(
  88. `invalid session reference URI ${JSON.stringify(uri)}`,
  89. 'SESSION_REFERENCE_INVALID_REFERENCE',
  90. cause === undefined ? undefined : { cause },
  91. )
  92. }