11import type { CredentialValidationResult } from "../../core/types.ts" ;
22import type { ProviderFetch , ProviderRuntimeHandler } from "../provider-runtime.ts" ;
33
4- import { compactObject , optionalInteger , optionalRecord , optionalString , requiredString } from "../../core/cast.ts" ;
5- import { assertPublicHttpUrl } from "../../core/request.ts" ;
4+ import {
5+ compactObject ,
6+ optionalInteger ,
7+ optionalRawString ,
8+ optionalRecord ,
9+ optionalString ,
10+ requiredString ,
11+ } from "../../core/cast.ts" ;
12+ import { assertPublicHttpUrl , encodePathSegment } from "../../core/request.ts" ;
613import {
714 createProviderTimeout ,
815 isAbortSignalError ,
@@ -11,11 +18,11 @@ import {
1118 providerUserAgent ,
1219 readProviderTextBody ,
1320} from "../provider-runtime.ts" ;
21+ import { qdrantUuidPattern } from "./actions.ts" ;
1422
1523const service = "qdrant" ;
1624const requestTimeoutMs = 30_000 ;
1725const qdrantHostnameSuffix = ".cloud.qdrant.io" ;
18- const qdrantPort = "6333" ;
1926
2027type QdrantRequestPhase = "validate" | "execute" ;
2128type QdrantHttpMethod = "GET" | "POST" | "PUT" ;
@@ -39,7 +46,7 @@ export const qdrantActionHandlers: Record<string, ProviderRuntimeHandler<QdrantC
3946 const collectionName = readCollectionName ( input ) ;
4047 const payload = await requestQdrantJson (
4148 context ,
42- `/collections/${ encodeURIComponent ( collectionName ) } ` ,
49+ `/collections/${ encodePathSegment ( collectionName ) } ` ,
4350 "GET" ,
4451 undefined ,
4552 "execute" ,
@@ -56,7 +63,7 @@ export const qdrantActionHandlers: Record<string, ProviderRuntimeHandler<QdrantC
5663 }
5764 const payload = await requestQdrantJson (
5865 context ,
59- `/collections/${ encodeURIComponent ( collectionName ) } ` ,
66+ `/collections/${ encodePathSegment ( collectionName ) } ` ,
6067 "PUT" ,
6168 { vectors : { size : vectorSize , distance } } ,
6269 "execute" ,
@@ -69,7 +76,7 @@ export const qdrantActionHandlers: Record<string, ProviderRuntimeHandler<QdrantC
6976 const points = readPoints ( input . points ) ;
7077 const payload = await requestQdrantJson (
7178 context ,
72- `/collections/${ encodeURIComponent ( collectionName ) } /points?wait=true` ,
79+ `/collections/${ encodePathSegment ( collectionName ) } /points?wait=true` ,
7380 "PUT" ,
7481 { points } ,
7582 "execute" ,
@@ -86,12 +93,16 @@ export const qdrantActionHandlers: Record<string, ProviderRuntimeHandler<QdrantC
8693 const id = readPointId ( input . id ) ;
8794 const payload = await requestQdrantJson (
8895 context ,
89- `/collections/${ encodeURIComponent ( collectionName ) } /points/${ encodeURIComponent ( String ( id ) ) } ` ,
96+ `/collections/${ encodePathSegment ( collectionName ) } /points/${ encodePathSegment ( id ) } ` ,
9097 "GET" ,
9198 undefined ,
9299 "execute" ,
93100 ) ;
94- return { point : requireObjectResult ( payload , "Qdrant point response" ) } ;
101+ const point = requireObjectResult ( payload , "Qdrant point response" ) ;
102+ if ( ! isPointId ( point . id ) ) {
103+ throw providerResponseError ( "Qdrant returned an invalid point ID" ) ;
104+ }
105+ return { point } ;
95106 } ,
96107
97108 async query_points ( input , context ) {
@@ -107,13 +118,13 @@ export const qdrantActionHandlers: Record<string, ProviderRuntimeHandler<QdrantC
107118 } ;
108119 const payload = await requestQdrantJson (
109120 context ,
110- `/collections/${ encodeURIComponent ( collectionName ) } /points/query` ,
121+ `/collections/${ encodePathSegment ( collectionName ) } /points/query` ,
111122 "POST" ,
112123 compactObject ( body ) ,
113124 "execute" ,
114125 ) ;
115126 const result = requireObjectResult ( payload , "Qdrant query response" ) ;
116- return { points : Array . isArray ( result . points ) ? result . points : [ ] } ;
127+ return { points : readPointRecords ( result . points , "query" ) } ;
117128 } ,
118129
119130 async scroll_points ( input , context ) {
@@ -128,15 +139,15 @@ export const qdrantActionHandlers: Record<string, ProviderRuntimeHandler<QdrantC
128139 } ;
129140 const payload = await requestQdrantJson (
130141 context ,
131- `/collections/${ encodeURIComponent ( collectionName ) } /points/scroll` ,
142+ `/collections/${ encodePathSegment ( collectionName ) } /points/scroll` ,
132143 "POST" ,
133144 compactObject ( body ) ,
134145 "execute" ,
135146 ) ;
136147 const result = requireObjectResult ( payload , "Qdrant scroll response" ) ;
137148 const nextOffset = readNullablePointId ( result . next_page_offset ) ;
138149 return {
139- points : Array . isArray ( result . points ) ? result . points : [ ] ,
150+ points : readPointRecords ( result . points , "scroll" ) ,
140151 nextOffset,
141152 complete : nextOffset === null ,
142153 } ;
@@ -241,8 +252,8 @@ function normalizeQdrantClusterUrl(value: string | undefined): URL {
241252 if ( url . username || url . password ) {
242253 throw providerInputError ( "clusterUrl must not include credentials" ) ;
243254 }
244- if ( url . port !== qdrantPort ) {
245- throw providerInputError ( "clusterUrl must use port 6333" ) ;
255+ if ( url . port !== "" && url . port !== "6333" ) {
256+ throw providerInputError ( "clusterUrl must use port 443 or 6333" ) ;
246257 }
247258 if ( url . pathname !== "/" || url . search || url . hash ) {
248259 throw providerInputError ( "clusterUrl must not include a path, query, or fragment" ) ;
@@ -284,7 +295,14 @@ function createQdrantError(status: number, payload: unknown, phase: QdrantReques
284295}
285296
286297function readCollectionName ( input : Record < string , unknown > ) : string {
287- return requiredString ( input . collectionName , "collectionName" , providerInputError ) ;
298+ const collectionName = optionalRawString ( input . collectionName ) ;
299+ if ( collectionName === undefined || collectionName . length === 0 ) {
300+ throw providerInputError ( "collectionName is required" ) ;
301+ }
302+ if ( collectionName === "." || collectionName === ".." ) {
303+ throw providerInputError ( "collectionName must not be . or .." ) ;
304+ }
305+ return collectionName ;
288306}
289307
290308function readPoints ( value : unknown ) : Record < string , unknown > [ ] {
@@ -315,17 +333,35 @@ function readPoints(value: unknown): Record<string, unknown>[] {
315333}
316334
317335function readPointId ( value : unknown ) : number | string {
318- if ( typeof value === "number" && Number . isSafeInteger ( value ) && value >= 0 ) {
336+ if ( isPointId ( value ) ) {
319337 return value ;
320338 }
321- if ( typeof value === "string" && uuidPattern . test ( value ) ) {
339+ throw providerInputError ( "id must be a non-negative safe integer or UUID" ) ;
340+ }
341+
342+ function readNullablePointId ( value : unknown ) : number | string | null {
343+ if ( value === null || value === undefined ) {
344+ return null ;
345+ }
346+ if ( isPointId ( value ) ) {
322347 return value ;
323348 }
324- throw providerInputError ( "id must be a non-negative integer or UUID ") ;
349+ throw providerResponseError ( "Qdrant returned an invalid next_page_offset ") ;
325350}
326351
327- function readNullablePointId ( value : unknown ) : number | string | null {
328- return value === null || value === undefined ? null : readPointId ( value ) ;
352+ function isPointId ( value : unknown ) : value is number | string {
353+ return (
354+ ( typeof value === "number" && Number . isSafeInteger ( value ) && value >= 0 ) ||
355+ ( typeof value === "string" && uuidPattern . test ( value ) )
356+ ) ;
357+ }
358+
359+ function readPointRecords ( value : unknown , operation : string ) : unknown [ ] {
360+ const points = Array . isArray ( value ) ? value : [ ] ;
361+ if ( points . some ( ( point ) => ! isPointId ( optionalRecord ( point ) ?. id ) ) ) {
362+ throw providerResponseError ( `Qdrant returned an invalid ${ operation } point ID` ) ;
363+ }
364+ return points ;
329365}
330366
331367function readVector ( value : unknown , fieldName : string ) : number [ ] {
@@ -370,7 +406,9 @@ function readOptionalNumber(value: unknown, fieldName: string): number | undefin
370406function readNullableInteger ( value : unknown , fieldName : string ) : number | null {
371407 if ( value === null || value === undefined ) return null ;
372408 const integer = optionalInteger ( value ) ;
373- if ( integer === undefined || integer < 0 ) throw providerResponseError ( `Qdrant returned an invalid ${ fieldName } ` ) ;
409+ if ( integer === undefined || ! Number . isSafeInteger ( integer ) || integer < 0 ) {
410+ throw providerResponseError ( `Qdrant returned an invalid ${ fieldName } ` ) ;
411+ }
374412 return integer ;
375413}
376414
@@ -394,7 +432,7 @@ function isDistance(value: string): value is "Cosine" | "Euclid" | "Dot" | "Manh
394432 return value === "Cosine" || value === "Euclid" || value === "Dot" || value === "Manhattan" ;
395433}
396434
397- const uuidPattern = / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 1 - 5 ] [ 0 - 9 a - f ] { 3 } - [ 8 9 a b ] [ 0 - 9 a - f ] { 3 } - [ 0 - 9 a - f ] { 12 } $ / i ;
435+ const uuidPattern = new RegExp ( qdrantUuidPattern ) ;
398436
399437function providerInputError ( message : string ) : ProviderRequestError {
400438 return new ProviderRequestError ( 400 , message ) ;
0 commit comments