export type DataServiceDocument = Record> = { id: string; data: T; revision: number; created_at: string; updated_at: string; }; export type DataServiceDocumentPage = Record> = { items: Array>; next_cursor: string | null; limit: number; }; export type DataServiceListOptions = { limit?: number; cursor?: string; }; export type DataServiceWriteOptions = { ifRevision?: number; }; export type DataServiceErrorContext = { resource?: 'instances' | 'collections' | 'documents' | 'bytes'; limit?: number; current?: number; attempted?: number; actual?: number; allowed?: number; current_revision?: number; retry_after_seconds?: number; }; export type DataServiceErrorCode = | 'runtime_unavailable' | 'upstream_invalid_response' | 'invalid_request' | 'invalid_content_type' | 'invalid_revision' | 'invalid_cursor' | 'cursor_expired' | 'invalid_collection_name' | 'invalid_document_id' | 'invalid_document_data' | 'invalid_project_id' | 'authentication_required' | 'origin_not_allowed' | 'method_not_allowed' | 'route_not_found' | 'instance_not_found' | 'collection_not_found' | 'document_not_found' | 'revision_conflict' | 'document_too_large' | 'quota_exceeded' | 'rate_limited' | 'data_service_unavailable' | 'request_too_large'; declare global { var __MAKELORE_DATA__: unknown; } type FetchResponse = { status: number; headers?: { get(name: string): string | null }; json(): Promise; }; type FetchOptions = { method: 'GET' | 'PUT' | 'DELETE'; headers: Record; body?: string; }; type RuntimeBinding = { endpoint: string; token: string; contractVersion: 1; }; type RuntimeGlobal = typeof globalThis & { __MAKELORE_DATA__?: unknown; fetch?: (input: string, init: FetchOptions) => Promise; crypto?: { randomUUID?: () => string }; }; const runtimeGlobal = globalThis as RuntimeGlobal; const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/; const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/; const MAX_CURSOR_LENGTH = 1_024; const MAX_ERROR_MESSAGE_LENGTH = 256; const MAX_RETRY_AFTER_SECONDS = 86_400; const ERROR_STATUS: Readonly> = { runtime_unavailable: 503, upstream_invalid_response: 502, invalid_request: [400, 422], invalid_content_type: 415, invalid_revision: 422, invalid_cursor: 400, cursor_expired: 410, invalid_collection_name: 422, invalid_document_id: 422, invalid_document_data: 422, invalid_project_id: 422, authentication_required: 401, origin_not_allowed: 403, method_not_allowed: 405, route_not_found: 404, instance_not_found: 404, collection_not_found: 404, document_not_found: 404, revision_conflict: 409, document_too_large: 413, quota_exceeded: 409, rate_limited: 429, data_service_unavailable: 503, request_too_large: 413, }; const ERROR_MESSAGE: Readonly> = { runtime_unavailable: 'Preview data runtime is unavailable', upstream_invalid_response: 'Data Service returned an invalid response', invalid_request: 'Data Service request is invalid', invalid_content_type: 'Data Service request content type is invalid', invalid_revision: 'Data Service document revision is invalid', invalid_cursor: 'Data Service cursor is invalid', cursor_expired: 'Data Service cursor has expired', invalid_collection_name: 'Data Service collection is invalid', invalid_document_id: 'Data Service document ID is invalid', invalid_document_data: 'Data Service document data is invalid', invalid_project_id: 'Data Service project ID is invalid', authentication_required: 'Preview data authorization is required', origin_not_allowed: 'Preview data Origin is not allowed', method_not_allowed: 'Preview data method is not allowed', route_not_found: 'Preview data route was not found', instance_not_found: 'Data Service project instance was not found', collection_not_found: 'Data Service collection was not found', document_not_found: 'Data Service document was not found', revision_conflict: 'Data Service document revision conflicts', document_too_large: 'Data Service document is too large', quota_exceeded: 'Data Service quota exceeded', rate_limited: 'Preview data request rate limit exceeded', data_service_unavailable: 'Data Service is temporarily unavailable', request_too_large: 'Preview data request is too large', }; const CONTEXT_KEYS = new Set([ 'resource', 'limit', 'current', 'attempted', 'actual', 'allowed', 'current_revision', 'retry_after_seconds', ]); const CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']); function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function positiveInteger(value: unknown): value is number { return Number.isSafeInteger(value) && (value as number) > 0; } function boundedText(value: unknown, maximum: number): value is string { return typeof value === 'string' && value.length > 0 && value.length <= maximum; } function boundedTimestamp(value: unknown): value is string { return boundedText(value, 64) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/.test(value) && Number.isFinite(Date.parse(value)); } function hasOnlyKeys(value: Record, required: readonly string[], optional: readonly string[] = []): boolean { const allowed = new Set([...required, ...optional]); return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => allowed.has(key)); } function statusMatches(code: DataServiceErrorCode, status: number): boolean { const expected = ERROR_STATUS[code]; return Array.isArray(expected) ? expected.includes(status) : expected === status; } function frozenContext(value: DataServiceErrorContext | undefined): DataServiceErrorContext | undefined { return value === undefined ? undefined : Object.freeze({ ...value }); } export class DataServiceError extends Error { readonly code: DataServiceErrorCode; readonly status: number; readonly retryable: boolean; readonly retryAfterSeconds?: number; readonly context?: DataServiceErrorContext; constructor( code: DataServiceErrorCode, status: number, retryable: boolean, retryAfterSeconds?: number, context?: DataServiceErrorContext, ) { super(ERROR_MESSAGE[code]); Object.defineProperty(this, 'name', { value: 'DataServiceError', enumerable: false }); this.code = code; this.status = status; this.retryable = retryable; if (retryAfterSeconds !== undefined) this.retryAfterSeconds = retryAfterSeconds; const safeContext = frozenContext(context); if (safeContext !== undefined) this.context = safeContext; Object.freeze(this); } } function runtimeUnavailable(): DataServiceError { return new DataServiceError('runtime_unavailable', 503, true); } function invalidRequest(): DataServiceError { return new DataServiceError('invalid_request', 422, false); } function upstreamInvalidResponse(): DataServiceError { return new DataServiceError('upstream_invalid_response', 502, false); } function readRuntime(): RuntimeBinding { const candidate = globalThis.__MAKELORE_DATA__; if (!isRecord(candidate) || candidate.contractVersion !== 1 || !boundedText(candidate.token, 256) || !/^[A-Za-z0-9_-]+$/.test(candidate.token) || !boundedText(candidate.endpoint, 256)) { throw runtimeUnavailable(); } let endpoint: URL; try { endpoint = new URL(candidate.endpoint); } catch { throw runtimeUnavailable(); } if (endpoint.protocol !== 'http:' || endpoint.hostname !== '127.0.0.1' || !endpoint.port || endpoint.username || endpoint.password || endpoint.pathname !== '/api/runtime/data/v1' || endpoint.search || endpoint.hash) { throw runtimeUnavailable(); } return { endpoint: `${endpoint.origin}${endpoint.pathname}`, token: candidate.token, contractVersion: 1, }; } function requireCollection(value: string): string { if (typeof value !== 'string' || !COLLECTION_PATTERN.test(value)) throw invalidRequest(); return value; } function requireDocumentId(value: string): string { if (typeof value !== 'string' || value === '.' || value === '..' || !DOCUMENT_ID_PATTERN.test(value)) { throw invalidRequest(); } return value; } function requireData(value: Record): Record { if (!isRecord(value)) throw invalidRequest(); try { if (JSON.stringify(value) === undefined) throw new Error('data is not JSON'); } catch { throw invalidRequest(); } return value; } function requireOptions(value: unknown, allowed: string[]): Record { if (value === undefined) return {}; if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw invalidRequest(); return value; } function readIfRevision(value: unknown): number | undefined { if (value === undefined) return undefined; if (!positiveInteger(value)) throw invalidRequest(); return value; } function buildDocumentPath(endpoint: string, collection: string, documentId: string): string { return `${endpoint}/collections/${encodeURIComponent(collection)}/documents/${encodeURIComponent(documentId)}`; } function parseContext(value: unknown): DataServiceErrorContext | undefined { if (value === undefined) return undefined; if (!isRecord(value)) throw upstreamInvalidResponse(); const context: DataServiceErrorContext = {}; for (const [key, item] of Object.entries(value)) { if (!CONTEXT_KEYS.has(key)) throw upstreamInvalidResponse(); if (key === 'resource') { if (!boundedText(item, 16) || !CONTEXT_RESOURCES.has(item)) throw upstreamInvalidResponse(); context.resource = item as DataServiceErrorContext['resource']; } else { if (!positiveInteger(item) && item !== 0) throw upstreamInvalidResponse(); context[key as Exclude] = item as never; } } return Object.keys(context).length === 0 ? undefined : context; } function readRetryAfter(response: FetchResponse): number | undefined { const raw = response.headers?.get('retry-after')?.trim(); if (!raw || !/^\d+$/.test(raw)) return undefined; const seconds = Number(raw); return Number.isSafeInteger(seconds) && seconds <= MAX_RETRY_AFTER_SECONDS ? seconds : undefined; } async function parseError(response: FetchResponse): Promise { try { const payload = await response.json(); if (!isRecord(payload) || !isRecord(payload.detail)) throw upstreamInvalidResponse(); const detail = payload.detail; if (!hasOnlyKeys(detail, ['code', 'message', 'retryable'], ['context']) || !boundedText(detail.code, 64) || !Object.prototype.hasOwnProperty.call(ERROR_STATUS, detail.code) || !boundedText(detail.message, MAX_ERROR_MESSAGE_LENGTH) || typeof detail.retryable !== 'boolean' || !statusMatches(detail.code as DataServiceErrorCode, response.status)) { throw upstreamInvalidResponse(); } const context = parseContext(detail.context); return new DataServiceError( detail.code as DataServiceErrorCode, response.status, detail.retryable, readRetryAfter(response), context, ); } catch (error: unknown) { if (error instanceof DataServiceError) throw error; throw upstreamInvalidResponse(); } } async function parseJson(response: FetchResponse): Promise { try { return await response.json(); } catch { throw upstreamInvalidResponse(); } } function parseDocument(value: unknown): DataServiceDocument { if (!isRecord(value) || !hasOnlyKeys(value, ['id', 'data', 'revision', 'created_at', 'updated_at']) || typeof value.id !== 'string' || value.id === '.' || value.id === '..' || !DOCUMENT_ID_PATTERN.test(value.id) || !isRecord(value.data) || !positiveInteger(value.revision) || !boundedTimestamp(value.created_at) || !boundedTimestamp(value.updated_at)) { throw upstreamInvalidResponse(); } return { id: value.id, data: value.data, revision: value.revision, created_at: value.created_at, updated_at: value.updated_at, }; } function parsePage(value: unknown): DataServiceDocumentPage { if (!isRecord(value) || !hasOnlyKeys(value, ['items', 'next_cursor', 'limit']) || !Array.isArray(value.items) || value.items.length > 100 || !positiveInteger(value.limit) || value.limit > 100 || (value.next_cursor !== null && !boundedText(value.next_cursor, MAX_CURSOR_LENGTH))) { throw upstreamInvalidResponse(); } const items = value.items.map(parseDocument); if (new Set(items.map((item) => item.id)).size !== items.length) throw upstreamInvalidResponse(); return { items, next_cursor: value.next_cursor === null ? null : value.next_cursor, limit: value.limit, }; } async function request( method: FetchOptions['method'], url: string, expectedStatus: number, parse: (value: unknown) => T, body?: Record, ifRevision?: number, ): Promise { const runtime = readRuntime(); const fetchImpl = runtimeGlobal.fetch; if (typeof fetchImpl !== 'function') throw runtimeUnavailable(); const headers: Record = { Accept: 'application/json', Authorization: `Bearer ${runtime.token}`, }; const serializedBody = body === undefined ? undefined : (() => { try { const encoded = JSON.stringify(body); return encoded === undefined ? null : encoded; } catch { return null; } })(); if (serializedBody === null) throw invalidRequest(); if (serializedBody !== undefined) headers['Content-Type'] = 'application/json'; if (ifRevision !== undefined) headers['If-Match'] = `"${ifRevision}"`; let response: FetchResponse; try { response = await fetchImpl(url, { method, headers, ...(serializedBody === undefined ? {} : { body: serializedBody }), }); } catch { throw runtimeUnavailable(); } if (!response || !Number.isSafeInteger(response.status)) throw upstreamInvalidResponse(); if (response.status !== expectedStatus) throw await parseError(response); if (expectedStatus === 204) return undefined as T; return parse(await parseJson(response)); } function listPath(endpoint: string, collection: string, options: DataServiceListOptions): string { const params = new URLSearchParams(); if (options.limit !== undefined) params.set('limit', String(options.limit)); if (options.cursor !== undefined) params.set('cursor', options.cursor); const query = params.toString(); return `${endpoint}/collections/${encodeURIComponent(collection)}/documents${query ? `?${query}` : ''}`; } async function getDocument(collection: string, documentId: string): Promise { const runtime = readRuntime(); const safeCollection = requireCollection(collection); const safeDocumentId = requireDocumentId(documentId); return await request( 'GET', buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId), 200, parseDocument, ); } async function listDocuments( collection: string, input: DataServiceListOptions = {}, ): Promise { const runtime = readRuntime(); const safeCollection = requireCollection(collection); const options = requireOptions(input, ['limit', 'cursor']); const limit = options.limit; if (limit !== undefined && (!positiveInteger(limit) || limit > 100)) throw invalidRequest(); const cursor = options.cursor; if (cursor !== undefined && (!boundedText(cursor, MAX_CURSOR_LENGTH))) throw invalidRequest(); return await request( 'GET', listPath(runtime.endpoint, safeCollection, { ...(limit === undefined ? {} : { limit }), ...(cursor === undefined ? {} : { cursor }), }), 200, parsePage, ); } async function putDocument( collection: string, documentId: string, value: Record, input: DataServiceWriteOptions = {}, ): Promise { const runtime = readRuntime(); const safeCollection = requireCollection(collection); const safeDocumentId = requireDocumentId(documentId); const data = requireData(value); const options = requireOptions(input, ['ifRevision']); const ifRevision = readIfRevision(options.ifRevision); return await request( 'PUT', buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId), 200, parseDocument, { data }, ifRevision, ); } async function deleteDocument( collection: string, documentId: string, input: DataServiceWriteOptions = {}, ): Promise { const runtime = readRuntime(); const safeCollection = requireCollection(collection); const safeDocumentId = requireDocumentId(documentId); const options = requireOptions(input, ['ifRevision']); const ifRevision = readIfRevision(options.ifRevision); await request( 'DELETE', buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId), 204, () => undefined, undefined, ifRevision, ); } async function addDocument( collection: string, value: Record, ): Promise { readRuntime(); requireCollection(collection); requireData(value); const randomUuid = runtimeGlobal.crypto?.randomUUID; if (typeof randomUuid !== 'function') throw runtimeUnavailable(); const id = randomUuid(); if (!boundedText(id, 128) || !DOCUMENT_ID_PATTERN.test(id)) throw runtimeUnavailable(); return await putDocument(collection, id, value); } export const data = Object.freeze({ get: getDocument, list: listDocuments, put: putDocument, delete: deleteDocument, add: addDocument, }); export default data;