feat(coding): add bundled plugin package selection

This commit is contained in:
2026-08-27 15:08:28 +08:00
parent 2ab1c51a24
commit 422150d4fa
14 changed files with 2188 additions and 15 deletions

View File

@@ -0,0 +1,281 @@
{
"schemaVersion": 1,
"pluginId": "makelore.data-service",
"contractVersion": 1,
"scope": "project",
"adapterId": "data-service",
"requiresBackend": true,
"display": {
"name": "开发数据服务",
"description": "为当前项目提供受控的开发期 JSON 数据存储。"
},
"skills": [
{
"id": "data-service",
"entry": "../skills/data-service/SKILL.md",
"grants": [
"data-service.control",
"data-service.documents"
]
}
],
"tools": [
{
"name": "data_service_configure",
"label": "Data Service configure",
"description": "Configure collections for the active project.",
"capabilityId": "data-service.control",
"operation": "configure",
"roles": ["parent"],
"mutation": "write",
"projectWriteLease": true,
"permissions": ["project.data.configure"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["collections"],
"properties": {
"collections": {
"type": "array",
"maxItems": 20,
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,47}$"
}
}
}
}
},
{
"name": "data_service_inspect",
"label": "Data Service inspect",
"description": "Inspect the active project data instance.",
"capabilityId": "data-service.control",
"operation": "inspect",
"roles": ["parent"],
"mutation": "read",
"projectWriteLease": false,
"permissions": ["project.data.read"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
},
{
"name": "data_service_list_projects",
"label": "Data Service list projects",
"description": "List the authenticated projects with Data Service instances.",
"capabilityId": "data-service.control",
"operation": "list_projects",
"roles": ["parent"],
"mutation": "read",
"projectWriteLease": false,
"permissions": ["project.data.read"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
},
{
"name": "data_service_get_document",
"label": "Data Service get document",
"description": "Read a document from the active project.",
"capabilityId": "data-service.documents",
"operation": "get_document",
"roles": ["parent"],
"mutation": "read",
"projectWriteLease": false,
"permissions": ["project.data.read"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["collection", "document_id"],
"properties": {
"collection": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,47}$"
},
"document_id": {
"type": "string",
"pattern": "^[A-Za-z0-9._~-]{1,128}$"
}
}
}
},
{
"name": "data_service_list_documents",
"label": "Data Service list documents",
"description": "List documents from a collection in the active project.",
"capabilityId": "data-service.documents",
"operation": "list_documents",
"roles": ["parent"],
"mutation": "read",
"projectWriteLease": false,
"permissions": ["project.data.read"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["collection"],
"properties": {
"collection": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,47}$"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"cursor": {
"type": "string",
"minLength": 1,
"maxLength": 1024
}
}
}
},
{
"name": "data_service_put_document",
"label": "Data Service put document",
"description": "Create or replace a document in the active project.",
"capabilityId": "data-service.documents",
"operation": "put_document",
"roles": ["parent"],
"mutation": "write",
"projectWriteLease": true,
"permissions": ["project.data.write"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["collection", "document_id", "data"],
"properties": {
"collection": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,47}$"
},
"document_id": {
"type": "string",
"pattern": "^[A-Za-z0-9._~-]{1,128}$"
},
"data": {
"type": "object"
},
"if_revision": {
"type": "integer",
"minimum": 1
}
}
}
},
{
"name": "data_service_delete_document",
"label": "Data Service delete document",
"description": "Delete a document after explicit confirmation.",
"capabilityId": "data-service.documents",
"operation": "delete_document",
"roles": ["parent"],
"mutation": "destructive",
"projectWriteLease": true,
"permissions": ["project.data.write"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["collection", "document_id", "confirmed"],
"properties": {
"collection": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,47}$"
},
"document_id": {
"type": "string",
"pattern": "^[A-Za-z0-9._~-]{1,128}$"
},
"if_revision": {
"type": "integer",
"minimum": 1
},
"confirmed": {
"type": "boolean",
"const": true
}
}
}
},
{
"name": "data_service_remove_collection",
"label": "Data Service remove collection",
"description": "Remove a collection after explicit confirmation.",
"capabilityId": "data-service.control",
"operation": "remove_collection",
"roles": ["parent"],
"mutation": "destructive",
"projectWriteLease": true,
"permissions": ["project.data.admin"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["collection", "confirmed"],
"properties": {
"collection": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{0,47}$"
},
"confirmed": {
"type": "boolean",
"const": true
}
}
}
},
{
"name": "data_service_reset",
"label": "Data Service reset",
"description": "Reset all active project data after explicit confirmation.",
"capabilityId": "data-service.control",
"operation": "reset",
"roles": ["parent"],
"mutation": "destructive",
"projectWriteLease": true,
"permissions": ["project.data.admin"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["confirmed"],
"properties": {
"confirmed": {
"type": "boolean",
"const": true
}
}
}
},
{
"name": "data_service_remove_project",
"label": "Data Service remove project",
"description": "Remove the active project data instance after explicit confirmation.",
"capabilityId": "data-service.control",
"operation": "remove_project",
"roles": ["parent"],
"mutation": "destructive",
"projectWriteLease": true,
"permissions": ["project.data.admin"],
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["confirmed"],
"properties": {
"confirmed": {
"type": "boolean",
"const": true
}
}
}
}
],
"surfaces": {
"projectSettings": "data-service",
"previewRuntime": "data-service-v1"
}
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "makelore.data-service",
"version": "1.0.0",
"description": "Project-scoped development data storage",
"author": {
"name": "MakeLore"
},
"extensions": {
"com.makelore": {
"capabilityManifest": "./com.makelore/capability.json"
}
}
}

View File

@@ -0,0 +1,93 @@
---
name: data-service
description: 当用户明确要求为当前 MakeLore 项目添加持久化开发数据、集合或数据读写示例时使用;只在本地预览中验证,不用于已发布作品。
---
# MakeLore 开发数据
为当前项目添加开发数据时,使用本 Skill 的顺序和完成条件。数据能力是
显式 opt-in必须得到 explicit user intent读取项目、创建项目、打开预览或复制项目本身都不会配置服务、
安装 SDK 或编辑源码。
## 1. Inspect
先 inspect 当前项目的实际目录、`package.json``tsconfig.json`(如果存在)、
已有入口文件和数据需求。识别完成当前功能所需的最小集合名称及每个集合的
最小文档形状;按实际源文件而不是 `projectType` 标签判断 TypeScript 或
JavaScript。
完成条件:已经列出实际布局、候选集合和最小示例,并且还没有修改应用源码、
安装 SDK 或调用 `data_service_configure`
## 2. Explain and wait
向用户说明候选集合、示例文档、将要写入的目标文件和预览验证动作。等待用户
明确同意这个具体集合方案;推测用户意图、提前配置或先写模板都不算同意。
完成条件:用户明确同意本次集合方案;若用户拒绝或改变需求,回到 Inspect
不要产生源码编辑。
## 3. Configure once
在明确同意后,用 `data_service_configure` 对这一个最终集合列表调用一次,且
只传实际项目上下文。不要因为响应慢、限流或不确定结果重复调用;配置响应是
后续步骤的唯一门槛。
完成条件:一次调用明确成功并返回配置/实例 DTO。若失败或返回
`project_identity_required`,立即停止,保持应用源码零编辑,并如实报告结果;
不要安装模板、创建示例或声称服务已配置。
## 4. Install the canonical SDK
配置成功后,根据实际布局选择唯一目标:
1.`src/` 时,若源代码/`tsconfig.json` 证明项目使用 TypeScript目标是
`src/lib/makelore-data.ts`,否则是 `src/lib/makelore-data.js`
2. 没有 `src/` 时,若已验证 TypeScript 工具链,目标是项目根的
`makelore-data.ts`,否则是根目录 `makelore-data.js`
从本 Skill 的 `assets/makelore-data.ts``assets/makelore-data.js` 逐字复制
选中的文件;不要让模型从说明重写 transport。目标不存在时创建它目标文本
已与选中的 asset 完全相同则保持精确文本 no-op。目标存在但文本不同先展示
实际路径和冲突事实,明确询问“替换 canonical 文件”或“保留并由用户自行
适配”;没有明确选择时不覆盖、不继续报告成功。
在最小有用的现有应用文件中加入指向该精确目标的相对 import并添加最小
示例调用。只编辑完成示例所需的 import/应用代码;不要加入 cloud URL、账号
凭据、缓存、重试、离线同步、订阅、schema 或 policy 层。SDK 的 DELETE 是
普通程序操作,不接受 `confirmed`
完成条件配置成功后canonical asset 已按实际布局逐字落到唯一目标;重复
运行会得到字节级 no-op修改过的目标会先产生清晰冲突询问最小应用编辑
只引用该目标且没有凭据或远端地址。
## 5. Preview verification
`agent_browser``open` 输入 `{ action: "open", url: "<本地预览地址>", injectProjectData: true }`
打开当前项目的 data-enabled preview不要在外部浏览器、发布运行时或
通用 Host 路径中寻找替代能力。实际运行最小示例,先执行一次真实 `put`,再
用返回的文档标识执行 `get`,读取并直接比较返回的文档数据与刚写入的数据。
完成条件:同一次数据预览会话中的真实 `put` 已成功,随后真实 `get` 的数据
逐项匹配且带有服务端返回的 revision。没有匹配的 read-back、没有预览注入、
或出现 `runtime_unavailable` 时,报告阻塞事实,不报告配置成功或配额状态。
## 6. report
只有 read-back 匹配后,才可用 `data_service_inspect` 读取并报告实际配置的
集合和响应中的 quota/usage 状态。报告只引用本次工具响应观察到的字段,不猜
测实例、owner、project、路径或剩余配额如果 inspect 失败,报告验证失败而
不是补造状态。
完成条件:报告明确区分配置响应、真实 put/get 结果和观察到的 quota/usage
并没有暴露凭据、云端地址、绝对路径或未观察到的服务端状态。
## Fixed SDK surface
程序只使用 `assets/makelore-data.ts``assets/makelore-data.js` 提供的
`data.get``data.list``data.put``data.delete` 和可选 `data.add`。SDK 每次
调用读取 `globalThis.__MAKELORE_DATA__`,只接受 `contractVersion === 1`,将
集合/文档路径片段用 `encodeURIComponent` 编码,并把 `ifRevision` 转成一个
strong `If-Match`。它严格解析直接的文档/page/error DTO缺失注入时返回稳定
`runtime_unavailable` 且不发网络请求。SDK 不持有凭据,不访问云端,不重试,
不缓存也不实现离线、订阅、schema 或 policy。

View File

@@ -0,0 +1,369 @@
const runtimeGlobal = globalThis;
const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
const MAX_CURSOR_LENGTH = 1024;
const MAX_ERROR_MESSAGE_LENGTH = 256;
const MAX_RETRY_AFTER_SECONDS = 86400;
const ERROR_STATUS = {
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 = {
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 = /* @__PURE__ */ new Set([
"resource",
"limit",
"current",
"attempted",
"actual",
"allowed",
"current_revision",
"retry_after_seconds"
]);
const CONTEXT_RESOURCES = /* @__PURE__ */ new Set(["instances", "collections", "documents", "bytes"]);
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function positiveInteger(value) {
return Number.isSafeInteger(value) && value > 0;
}
function boundedText(value, maximum) {
return typeof value === "string" && value.length > 0 && value.length <= maximum;
}
function boundedTimestamp(value) {
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, required, optional = []) {
const allowed = /* @__PURE__ */ 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, status) {
const expected = ERROR_STATUS[code];
return Array.isArray(expected) ? expected.includes(status) : expected === status;
}
function frozenContext(value) {
return value === void 0 ? void 0 : Object.freeze({ ...value });
}
class DataServiceError extends Error {
code;
status;
retryable;
retryAfterSeconds;
context;
constructor(code, status, retryable, retryAfterSeconds, context) {
super(ERROR_MESSAGE[code]);
Object.defineProperty(this, "name", { value: "DataServiceError", enumerable: false });
this.code = code;
this.status = status;
this.retryable = retryable;
if (retryAfterSeconds !== void 0) this.retryAfterSeconds = retryAfterSeconds;
const safeContext = frozenContext(context);
if (safeContext !== void 0) this.context = safeContext;
Object.freeze(this);
}
}
function runtimeUnavailable() {
return new DataServiceError("runtime_unavailable", 503, true);
}
function invalidRequest() {
return new DataServiceError("invalid_request", 422, false);
}
function upstreamInvalidResponse() {
return new DataServiceError("upstream_invalid_response", 502, false);
}
function readRuntime() {
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;
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) {
if (typeof value !== "string" || !COLLECTION_PATTERN.test(value)) throw invalidRequest();
return value;
}
function requireDocumentId(value) {
if (typeof value !== "string" || value === "." || value === ".." || !DOCUMENT_ID_PATTERN.test(value)) {
throw invalidRequest();
}
return value;
}
function requireData(value) {
if (!isRecord(value)) throw invalidRequest();
try {
if (JSON.stringify(value) === void 0) throw new Error("data is not JSON");
} catch {
throw invalidRequest();
}
return value;
}
function requireOptions(value, allowed) {
if (value === void 0) return {};
if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw invalidRequest();
return value;
}
function readIfRevision(value) {
if (value === void 0) return void 0;
if (!positiveInteger(value)) throw invalidRequest();
return value;
}
function buildDocumentPath(endpoint, collection, documentId) {
return `${endpoint}/collections/${encodeURIComponent(collection)}/documents/${encodeURIComponent(documentId)}`;
}
function parseContext(value) {
if (value === void 0) return void 0;
if (!isRecord(value)) throw upstreamInvalidResponse();
const context = {};
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;
} else {
if (!positiveInteger(item) && item !== 0) throw upstreamInvalidResponse();
context[key] = item;
}
}
return Object.keys(context).length === 0 ? void 0 : context;
}
function readRetryAfter(response) {
const raw = response.headers?.get("retry-after")?.trim();
if (!raw || !/^\d+$/.test(raw)) return void 0;
const seconds = Number(raw);
return Number.isSafeInteger(seconds) && seconds <= MAX_RETRY_AFTER_SECONDS ? seconds : void 0;
}
async function parseError(response) {
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, response.status)) {
throw upstreamInvalidResponse();
}
const context = parseContext(detail.context);
return new DataServiceError(
detail.code,
response.status,
detail.retryable,
readRetryAfter(response),
context
);
} catch (error) {
if (error instanceof DataServiceError) throw error;
throw upstreamInvalidResponse();
}
}
async function parseJson(response) {
try {
return await response.json();
} catch {
throw upstreamInvalidResponse();
}
}
function parseDocument(value) {
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) {
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, url, expectedStatus, parse, body, ifRevision) {
const runtime = readRuntime();
const fetchImpl = runtimeGlobal.fetch;
if (typeof fetchImpl !== "function") throw runtimeUnavailable();
const headers = {
Accept: "application/json",
Authorization: `Bearer ${runtime.token}`
};
const serializedBody = body === void 0 ? void 0 : (() => {
try {
const encoded = JSON.stringify(body);
return encoded === void 0 ? null : encoded;
} catch {
return null;
}
})();
if (serializedBody === null) throw invalidRequest();
if (serializedBody !== void 0) headers["Content-Type"] = "application/json";
if (ifRevision !== void 0) headers["If-Match"] = `"${ifRevision}"`;
let response;
try {
response = await fetchImpl(url, {
method,
headers,
...serializedBody === void 0 ? {} : { 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 void 0;
return parse(await parseJson(response));
}
function listPath(endpoint, collection, options) {
const params = new URLSearchParams();
if (options.limit !== void 0) params.set("limit", String(options.limit));
if (options.cursor !== void 0) params.set("cursor", options.cursor);
const query = params.toString();
return `${endpoint}/collections/${encodeURIComponent(collection)}/documents${query ? `?${query}` : ""}`;
}
async function getDocument(collection, documentId) {
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, input = {}) {
const runtime = readRuntime();
const safeCollection = requireCollection(collection);
const options = requireOptions(input, ["limit", "cursor"]);
const limit = options.limit;
if (limit !== void 0 && (!positiveInteger(limit) || limit > 100)) throw invalidRequest();
const cursor = options.cursor;
if (cursor !== void 0 && !boundedText(cursor, MAX_CURSOR_LENGTH)) throw invalidRequest();
return await request(
"GET",
listPath(runtime.endpoint, safeCollection, {
...limit === void 0 ? {} : { limit },
...cursor === void 0 ? {} : { cursor }
}),
200,
parsePage
);
}
async function putDocument(collection, documentId, value, input = {}) {
const runtime = readRuntime();
const safeCollection = requireCollection(collection);
const safeDocumentId = requireDocumentId(documentId);
const data2 = requireData(value);
const options = requireOptions(input, ["ifRevision"]);
const ifRevision = readIfRevision(options.ifRevision);
return await request(
"PUT",
buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId),
200,
parseDocument,
{ data: data2 },
ifRevision
);
}
async function deleteDocument(collection, documentId, input = {}) {
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,
() => void 0,
void 0,
ifRevision
);
}
async function addDocument(collection, value) {
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);
}
const data = Object.freeze({
get: getDocument,
list: listDocuments,
put: putDocument,
delete: deleteDocument,
add: addDocument
});
var makelore_data_default = data;
export {
DataServiceError,
data,
makelore_data_default as default
};

View File

@@ -0,0 +1,550 @@
export type DataServiceDocument<T extends Record<string, unknown> = Record<string, unknown>> = {
id: string;
data: T;
revision: number;
created_at: string;
updated_at: string;
};
export type DataServiceDocumentPage<T extends Record<string, unknown> = Record<string, unknown>> = {
items: Array<DataServiceDocument<T>>;
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<unknown>;
};
type FetchOptions = {
method: 'GET' | 'PUT' | 'DELETE';
headers: Record<string, string>;
body?: string;
};
type RuntimeBinding = {
endpoint: string;
token: string;
contractVersion: 1;
};
type RuntimeGlobal = typeof globalThis & {
__MAKELORE_DATA__?: unknown;
fetch?: (input: string, init: FetchOptions) => Promise<FetchResponse>;
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<Record<DataServiceErrorCode, number | readonly number[]>> = {
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<Record<DataServiceErrorCode, string>> = {
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<string, unknown> {
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<string, unknown>, 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<string, unknown>): Record<string, unknown> {
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<string, unknown> {
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<keyof DataServiceErrorContext, 'resource'>] = 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<DataServiceError> {
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<unknown> {
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<T>(
method: FetchOptions['method'],
url: string,
expectedStatus: number,
parse: (value: unknown) => T,
body?: Record<string, unknown>,
ifRevision?: number,
): Promise<T> {
const runtime = readRuntime();
const fetchImpl = runtimeGlobal.fetch;
if (typeof fetchImpl !== 'function') throw runtimeUnavailable();
const headers: Record<string, string> = {
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<DataServiceDocument> {
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<DataServiceDocumentPage> {
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<string, unknown>,
input: DataServiceWriteOptions = {},
): Promise<DataServiceDocument> {
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<void> {
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<string, unknown>,
): Promise<DataServiceDocument> {
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;