@floegence/redevplugin-ui 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capability-client.d.ts +40 -0
- package/dist/capability-client.js +501 -0
- package/dist/contracts.gen.d.ts +47 -11
- package/dist/contracts.gen.js +53 -11
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +1 -0
- package/dist/platform.d.ts +81 -8
- package/dist/plugin.d.ts +4 -1
- package/dist/plugin.js +2 -0
- package/dist/surface.d.ts +15 -3
- package/dist/surface.js +234 -29
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PluginBridgeError } from "./errors.js";
|
|
2
|
+
import type { PluginBridgeClient, PluginJSONObject, PluginStreamEvent as PluginRawStreamEvent, PluginStreamTerminalStatus } from "./surface.js";
|
|
3
|
+
export type PluginCapabilitySchema = Readonly<Record<string, unknown>>;
|
|
4
|
+
export type PluginCapabilityBusinessErrorSpec = {
|
|
5
|
+
detail_schema_sha256: string;
|
|
6
|
+
schema: PluginCapabilitySchema | null;
|
|
7
|
+
};
|
|
8
|
+
export type PluginOperation<T, Cancelable extends boolean = true> = {
|
|
9
|
+
data: T;
|
|
10
|
+
operation_id: string;
|
|
11
|
+
} & (Cancelable extends true ? {
|
|
12
|
+
cancel(reason?: string): Promise<void>;
|
|
13
|
+
} : Record<never, never>);
|
|
14
|
+
export type PluginCapabilityStreamEvent<Event> = Omit<PluginRawStreamEvent, "data" | "error"> & {
|
|
15
|
+
data: Event;
|
|
16
|
+
};
|
|
17
|
+
export type PluginCapabilityStreamReadResult<Event> = {
|
|
18
|
+
events: PluginCapabilityStreamEvent<Event>[];
|
|
19
|
+
done: false;
|
|
20
|
+
retry_after_ms: number;
|
|
21
|
+
} | {
|
|
22
|
+
events: PluginCapabilityStreamEvent<Event>[];
|
|
23
|
+
done: true;
|
|
24
|
+
terminal_status: PluginStreamTerminalStatus;
|
|
25
|
+
retry_after_ms: 0;
|
|
26
|
+
};
|
|
27
|
+
export type PluginStream<Initial, Event> = {
|
|
28
|
+
data: Initial;
|
|
29
|
+
operation_id: string;
|
|
30
|
+
stream_handle: string;
|
|
31
|
+
read(): Promise<PluginCapabilityStreamReadResult<Event>>;
|
|
32
|
+
cancel(reason?: string): Promise<void>;
|
|
33
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<PluginCapabilityStreamEvent<Event>>;
|
|
34
|
+
};
|
|
35
|
+
export declare function callCapabilitySync<Request extends object, Response>(bridge: PluginBridgeClient, method: string, request: Request, requestSchema: PluginCapabilitySchema, responseSchema: PluginCapabilitySchema): Promise<Response>;
|
|
36
|
+
export declare function callCapabilityOperation<Request extends object, Response, Cancelable extends boolean = true>(bridge: PluginBridgeClient, method: string, request: Request, requestSchema: PluginCapabilitySchema, responseSchema: PluginCapabilitySchema, cancelable?: Cancelable): Promise<PluginOperation<Response, Cancelable>>;
|
|
37
|
+
export declare function callCapabilityStream<Request extends object, Response, Event>(bridge: PluginBridgeClient, method: string, request: Request, requestSchema: PluginCapabilitySchema, responseSchema: PluginCapabilitySchema, eventTypeName: string, eventSchema: PluginCapabilitySchema): Promise<PluginStream<Response, Event>>;
|
|
38
|
+
export declare function isCapabilityBusinessError(error: unknown, capabilityID: string, capabilityVersion: string, detailSchemas: Readonly<Record<string, PluginCapabilityBusinessErrorSpec>>): error is PluginBridgeError & {
|
|
39
|
+
readonly details: PluginJSONObject;
|
|
40
|
+
};
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { PluginBridgeError } from "./errors.js";
|
|
2
|
+
import { decodePluginStreamText } from "./surface.js";
|
|
3
|
+
export async function callCapabilitySync(bridge, method, request, requestSchema, responseSchema) {
|
|
4
|
+
const params = validateRequest(request, requestSchema);
|
|
5
|
+
const result = parseSyncResult(method, await bridge.call(method, params));
|
|
6
|
+
return validateResponse(method, result.data, responseSchema);
|
|
7
|
+
}
|
|
8
|
+
export async function callCapabilityOperation(bridge, method, request, requestSchema, responseSchema, cancelable = true) {
|
|
9
|
+
const params = validateRequest(request, requestSchema);
|
|
10
|
+
const result = parseOperationResult(method, await bridge.call(method, params));
|
|
11
|
+
let data;
|
|
12
|
+
try {
|
|
13
|
+
data = validateResponse(method, result.data, responseSchema);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (cancelable)
|
|
17
|
+
await cancelAfterResponseMismatch(bridge, result.operation_id, error);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
data: data,
|
|
22
|
+
operation_id: result.operation_id,
|
|
23
|
+
...(cancelable ? { cancel: (reason) => bridge.cancelOperation(result.operation_id, reason) } : {}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function callCapabilityStream(bridge, method, request, requestSchema, responseSchema, eventTypeName, eventSchema) {
|
|
27
|
+
const params = validateRequest(request, requestSchema);
|
|
28
|
+
const result = parseStreamResult(method, await bridge.call(method, params));
|
|
29
|
+
let data;
|
|
30
|
+
try {
|
|
31
|
+
data = validateResponse(method, result.data, responseSchema);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
await cancelAfterResponseMismatch(bridge, result.operation_id, error);
|
|
35
|
+
}
|
|
36
|
+
let settled = false;
|
|
37
|
+
const read = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const batch = decodeCapabilityStreamBatch(method, await bridge.readStream(result.stream_handle), eventTypeName, eventSchema);
|
|
40
|
+
if (batch.done)
|
|
41
|
+
settled = true;
|
|
42
|
+
return batch;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
settled = true;
|
|
46
|
+
return cancelAfterStreamMismatch(bridge, result.operation_id, error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const cancel = async (reason) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
await bridge.cancelOperation(result.operation_id, reason);
|
|
53
|
+
settled = true;
|
|
54
|
+
};
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
data: data,
|
|
57
|
+
operation_id: result.operation_id,
|
|
58
|
+
stream_handle: result.stream_handle,
|
|
59
|
+
read,
|
|
60
|
+
cancel,
|
|
61
|
+
async *[Symbol.asyncIterator]() {
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
const batch = await read();
|
|
65
|
+
for (const event of batch.events)
|
|
66
|
+
yield event;
|
|
67
|
+
if (batch.done) {
|
|
68
|
+
if (batch.terminal_status === "closed")
|
|
69
|
+
return;
|
|
70
|
+
throw pluginStreamTerminalError(batch.terminal_status);
|
|
71
|
+
}
|
|
72
|
+
if (batch.events.length === 0 && batch.retry_after_ms > 0) {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, batch.retry_after_ms));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (!settled)
|
|
79
|
+
await cancel("stream_iterator_closed");
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function decodeCapabilityStreamBatch(method, batch, eventTypeName, eventSchema) {
|
|
85
|
+
const events = batch.events.map((event) => decodeCapabilityStreamEvent(method, event, eventTypeName, eventSchema));
|
|
86
|
+
if (batch.done) {
|
|
87
|
+
return { events, done: true, terminal_status: batch.terminal_status, retry_after_ms: 0 };
|
|
88
|
+
}
|
|
89
|
+
return { events, done: false, retry_after_ms: batch.retry_after_ms };
|
|
90
|
+
}
|
|
91
|
+
function decodeCapabilityStreamEvent(method, event, eventTypeName, eventSchema) {
|
|
92
|
+
if (event.kind !== eventTypeName || event.error !== undefined || event.data === undefined) {
|
|
93
|
+
throw contractMismatch(method, "stream event envelope does not match its published contract");
|
|
94
|
+
}
|
|
95
|
+
let value;
|
|
96
|
+
try {
|
|
97
|
+
value = JSON.parse(decodePluginStreamText(event));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw contractMismatch(method, "stream event is not canonical JSON");
|
|
101
|
+
}
|
|
102
|
+
if (!validateValue(value, eventSchema, new Set())) {
|
|
103
|
+
throw contractMismatch(method, "stream event does not match its published contract");
|
|
104
|
+
}
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
sequence: event.sequence,
|
|
107
|
+
kind: event.kind,
|
|
108
|
+
data: value,
|
|
109
|
+
at: event.at,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function parseSyncResult(method, value) {
|
|
113
|
+
if (!hasExactKeys(value, ["data"]))
|
|
114
|
+
throw contractMismatch(method, "returned an invalid sync result envelope");
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function parseOperationResult(method, value) {
|
|
118
|
+
if (!hasExactKeys(value, ["data", "operation_id"]) || !validOpaqueIdentifier(value.operation_id, "operation")) {
|
|
119
|
+
throw contractMismatch(method, "returned an invalid operation result envelope");
|
|
120
|
+
}
|
|
121
|
+
return { data: value.data, operation_id: value.operation_id };
|
|
122
|
+
}
|
|
123
|
+
function parseStreamResult(method, value) {
|
|
124
|
+
if (!hasExactKeys(value, ["data", "operation_id", "stream_handle"]) ||
|
|
125
|
+
!validOpaqueIdentifier(value.operation_id, "operation") || !validOpaqueIdentifier(value.stream_handle, "stream")) {
|
|
126
|
+
throw contractMismatch(method, "returned an invalid subscription result envelope");
|
|
127
|
+
}
|
|
128
|
+
return { data: value.data, operation_id: value.operation_id, stream_handle: value.stream_handle };
|
|
129
|
+
}
|
|
130
|
+
async function cancelAfterResponseMismatch(bridge, operationID, mismatch) {
|
|
131
|
+
try {
|
|
132
|
+
await bridge.cancelOperation(operationID, "response_contract_mismatch");
|
|
133
|
+
}
|
|
134
|
+
catch (cleanupError) {
|
|
135
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Capability response failed validation and its live operation could not be cancelled", undefined, {
|
|
136
|
+
operation_id: operationID,
|
|
137
|
+
cleanup_error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
throw mismatch;
|
|
141
|
+
}
|
|
142
|
+
async function cancelAfterStreamMismatch(bridge, operationID, mismatch) {
|
|
143
|
+
try {
|
|
144
|
+
await bridge.cancelOperation(operationID, "stream_contract_mismatch");
|
|
145
|
+
}
|
|
146
|
+
catch (cleanupError) {
|
|
147
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Capability stream failed validation and its live operation could not be cancelled", undefined, {
|
|
148
|
+
operation_id: operationID,
|
|
149
|
+
cleanup_error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
throw mismatch;
|
|
153
|
+
}
|
|
154
|
+
export function isCapabilityBusinessError(error, capabilityID, capabilityVersion, detailSchemas) {
|
|
155
|
+
if (!(error instanceof PluginBridgeError) || error.errorCode !== "PLUGIN_CAPABILITY_ERROR" || !isRecord(error.details)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (!Object.keys(error.details).every((key) => key === "capability_id" || key === "capability_version" || key === "detail_schema_sha256" ||
|
|
159
|
+
key === "business_error_code" || key === "business_error_details")) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (error.details.capability_id !== capabilityID || error.details.capability_version !== capabilityVersion)
|
|
163
|
+
return false;
|
|
164
|
+
const code = error.details.business_error_code;
|
|
165
|
+
if (typeof code !== "string" || !Object.hasOwn(detailSchemas, code))
|
|
166
|
+
return false;
|
|
167
|
+
const specification = detailSchemas[code];
|
|
168
|
+
if (error.details.detail_schema_sha256 !== specification.detail_schema_sha256)
|
|
169
|
+
return false;
|
|
170
|
+
const schema = specification.schema;
|
|
171
|
+
if (schema === null)
|
|
172
|
+
return error.details.business_error_details === undefined;
|
|
173
|
+
return isRecord(schema) && validateValue(error.details.business_error_details, schema, new Set());
|
|
174
|
+
}
|
|
175
|
+
function validateRequest(request, schema) {
|
|
176
|
+
if (!validateValue(request, schema, new Set())) {
|
|
177
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Capability request does not match its published contract");
|
|
178
|
+
}
|
|
179
|
+
return request;
|
|
180
|
+
}
|
|
181
|
+
function validateResponse(method, value, schema) {
|
|
182
|
+
if (!validateValue(value, schema, new Set())) {
|
|
183
|
+
throw contractMismatch(method, "response does not match its published contract");
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function validateValue(value, schema, seen) {
|
|
188
|
+
if (Array.isArray(schema.oneOf)) {
|
|
189
|
+
let matches = 0;
|
|
190
|
+
for (const branch of schema.oneOf) {
|
|
191
|
+
if (!isRecord(branch))
|
|
192
|
+
return false;
|
|
193
|
+
if (validateValue(value, branch, seen))
|
|
194
|
+
matches += 1;
|
|
195
|
+
}
|
|
196
|
+
return matches === 1;
|
|
197
|
+
}
|
|
198
|
+
const type = schema.type;
|
|
199
|
+
if (typeof type !== "string")
|
|
200
|
+
return false;
|
|
201
|
+
if (value !== null && typeof value === "object") {
|
|
202
|
+
if (seen.has(value))
|
|
203
|
+
return false;
|
|
204
|
+
seen.add(value);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
switch (type) {
|
|
208
|
+
case "object":
|
|
209
|
+
return validateObject(value, schema, seen);
|
|
210
|
+
case "array":
|
|
211
|
+
return validateArray(value, schema, seen);
|
|
212
|
+
case "string":
|
|
213
|
+
return validateString(value, schema);
|
|
214
|
+
case "integer":
|
|
215
|
+
return typeof value === "number" && Number.isSafeInteger(value) && validateNumber(value, schema);
|
|
216
|
+
case "number":
|
|
217
|
+
return typeof value === "number" && Number.isFinite(value) && validateNumber(value, schema);
|
|
218
|
+
case "boolean":
|
|
219
|
+
return typeof value === "boolean" && validateConst(value, schema);
|
|
220
|
+
case "null":
|
|
221
|
+
return value === null;
|
|
222
|
+
default:
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (value !== null && typeof value === "object")
|
|
228
|
+
seen.delete(value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function validateObject(value, schema, seen) {
|
|
232
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
233
|
+
return false;
|
|
234
|
+
if (schema.additionalProperties !== false || (schema.properties !== undefined && !isRecord(schema.properties)))
|
|
235
|
+
return false;
|
|
236
|
+
const object = value;
|
|
237
|
+
const properties = (schema.properties ?? {});
|
|
238
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
239
|
+
if (required.some((name) => typeof name !== "string" || !Object.hasOwn(object, name)))
|
|
240
|
+
return false;
|
|
241
|
+
const keys = Object.keys(object);
|
|
242
|
+
if (keys.some((key) => prototypeSensitivePropertyNames.has(key)) ||
|
|
243
|
+
Object.keys(properties).some((key) => prototypeSensitivePropertyNames.has(key)))
|
|
244
|
+
return false;
|
|
245
|
+
if (!integerBound(keys.length, schema.minProperties, schema.maxProperties))
|
|
246
|
+
return false;
|
|
247
|
+
for (const key of keys) {
|
|
248
|
+
const child = properties[key];
|
|
249
|
+
if (!isRecord(child) || !validateValue(object[key], child, seen))
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
function validateArray(value, schema, seen) {
|
|
255
|
+
if (!Array.isArray(value) || !isRecord(schema.items))
|
|
256
|
+
return false;
|
|
257
|
+
if (!integerBound(value.length, schema.minItems, schema.maxItems))
|
|
258
|
+
return false;
|
|
259
|
+
if (schema.uniqueItems === true) {
|
|
260
|
+
const canonical = value.map(canonicalJSONKey);
|
|
261
|
+
if (new Set(canonical).size !== canonical.length)
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return value.every((item) => validateValue(item, schema.items, seen));
|
|
265
|
+
}
|
|
266
|
+
function validateString(value, schema) {
|
|
267
|
+
if (typeof value !== "string" || !integerBound(Array.from(value).length, schema.minLength, schema.maxLength) || !validateConst(value, schema)) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
271
|
+
return false;
|
|
272
|
+
if (typeof schema.pattern === "string") {
|
|
273
|
+
try {
|
|
274
|
+
if (!isPortablePattern(schema.pattern))
|
|
275
|
+
return false;
|
|
276
|
+
const match = new RegExp(schema.pattern, "u").exec(value);
|
|
277
|
+
if (match === null || match.index !== 0 || match[0].length !== value.length)
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (typeof schema.format === "string" && !validateStringFormat(value, schema.format))
|
|
285
|
+
return false;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
function validateStringFormat(value, format) {
|
|
289
|
+
switch (format) {
|
|
290
|
+
case "date-time":
|
|
291
|
+
return validateDateTime(value);
|
|
292
|
+
case "uuid":
|
|
293
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
294
|
+
case "hostname":
|
|
295
|
+
return validateHostname(value);
|
|
296
|
+
case "ipv4":
|
|
297
|
+
return validateIPv4(value);
|
|
298
|
+
case "ipv6":
|
|
299
|
+
return validateIPv6(value);
|
|
300
|
+
default:
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function validateDateTime(value) {
|
|
305
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
306
|
+
if (match === null || Number(match[4]) > 23 || Number(match[5]) > 59 || Number(match[6]) > 59)
|
|
307
|
+
return false;
|
|
308
|
+
const month = Number(match[2]);
|
|
309
|
+
const day = Number(match[3]);
|
|
310
|
+
const offset = match[7];
|
|
311
|
+
if (offset !== "Z") {
|
|
312
|
+
const offsetHours = Number(offset.slice(1, 3));
|
|
313
|
+
const offsetMinutes = Number(offset.slice(4, 6));
|
|
314
|
+
if (offsetHours > 23 || offsetMinutes > 59)
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return Number.isFinite(Date.parse(value)) && month >= 1 && month <= 12 && day >= 1 && day <= new Date(Date.UTC(Number(match[1]), month, 0)).getUTCDate();
|
|
318
|
+
}
|
|
319
|
+
function validateHostname(value) {
|
|
320
|
+
if (value.length === 0 || value.length > 253 || value.endsWith("."))
|
|
321
|
+
return false;
|
|
322
|
+
return value.split(".").every((label) => label.length >= 1 && label.length <= 63 && /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
|
|
323
|
+
}
|
|
324
|
+
function validateIPv4(value) {
|
|
325
|
+
const parts = value.split(".");
|
|
326
|
+
return parts.length === 4 && parts.every((part) => /^(?:0|[1-9]\d{0,2})$/.test(part) && Number(part) <= 255);
|
|
327
|
+
}
|
|
328
|
+
function validateIPv6(value) {
|
|
329
|
+
if (value.length === 0 || value.includes(":::"))
|
|
330
|
+
return false;
|
|
331
|
+
const compression = value.indexOf("::");
|
|
332
|
+
if (compression !== value.lastIndexOf("::"))
|
|
333
|
+
return false;
|
|
334
|
+
const [leftRaw, rightRaw = ""] = compression >= 0 ? value.split("::") : [value, ""];
|
|
335
|
+
const left = leftRaw === "" ? [] : leftRaw.split(":");
|
|
336
|
+
const right = rightRaw === "" ? [] : rightRaw.split(":");
|
|
337
|
+
const parseSide = (parts, allowIPv4) => {
|
|
338
|
+
let groups = 0;
|
|
339
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
340
|
+
const part = parts[index];
|
|
341
|
+
if (part.includes(".")) {
|
|
342
|
+
if (!allowIPv4 || index !== parts.length - 1 || !validateIPv4(part))
|
|
343
|
+
return null;
|
|
344
|
+
groups += 2;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
if (!/^[0-9A-Fa-f]{1,4}$/.test(part))
|
|
348
|
+
return null;
|
|
349
|
+
groups += 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return groups;
|
|
353
|
+
};
|
|
354
|
+
const leftGroups = parseSide(left, right.length === 0);
|
|
355
|
+
const rightGroups = parseSide(right, true);
|
|
356
|
+
if (leftGroups === null || rightGroups === null)
|
|
357
|
+
return false;
|
|
358
|
+
const groups = leftGroups + rightGroups;
|
|
359
|
+
return compression >= 0 ? groups < 8 : groups === 8;
|
|
360
|
+
}
|
|
361
|
+
function validateNumber(value, schema) {
|
|
362
|
+
if (!validateConst(value, schema))
|
|
363
|
+
return false;
|
|
364
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
365
|
+
return false;
|
|
366
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
367
|
+
return false;
|
|
368
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
369
|
+
return false;
|
|
370
|
+
if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum)
|
|
371
|
+
return false;
|
|
372
|
+
if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum)
|
|
373
|
+
return false;
|
|
374
|
+
if (typeof schema.multipleOf === "number" && !isJSONMultipleOf(value, schema.multipleOf))
|
|
375
|
+
return false;
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
function isJSONMultipleOf(value, divisor) {
|
|
379
|
+
if (!Number.isFinite(divisor) || divisor <= 0)
|
|
380
|
+
return false;
|
|
381
|
+
const left = decimalCoefficient(value);
|
|
382
|
+
const right = decimalCoefficient(divisor);
|
|
383
|
+
if (left === null || right === null || right.coefficient === 0n)
|
|
384
|
+
return false;
|
|
385
|
+
let numerator = left.coefficient;
|
|
386
|
+
let denominator = right.coefficient;
|
|
387
|
+
const difference = right.scale - left.scale;
|
|
388
|
+
if (difference > 0)
|
|
389
|
+
numerator *= 10n ** BigInt(difference);
|
|
390
|
+
if (difference < 0)
|
|
391
|
+
denominator *= 10n ** BigInt(-difference);
|
|
392
|
+
return numerator % denominator === 0n;
|
|
393
|
+
}
|
|
394
|
+
function decimalCoefficient(value) {
|
|
395
|
+
if (!Number.isFinite(value))
|
|
396
|
+
return null;
|
|
397
|
+
let text = String(value).toLowerCase();
|
|
398
|
+
let sign = 1n;
|
|
399
|
+
if (text.startsWith("-")) {
|
|
400
|
+
sign = -1n;
|
|
401
|
+
text = text.slice(1);
|
|
402
|
+
}
|
|
403
|
+
let exponent = 0;
|
|
404
|
+
const exponentIndex = text.indexOf("e");
|
|
405
|
+
if (exponentIndex >= 0) {
|
|
406
|
+
exponent = Number(text.slice(exponentIndex + 1));
|
|
407
|
+
if (!Number.isSafeInteger(exponent))
|
|
408
|
+
return null;
|
|
409
|
+
text = text.slice(0, exponentIndex);
|
|
410
|
+
}
|
|
411
|
+
let scale = 0;
|
|
412
|
+
const decimalIndex = text.indexOf(".");
|
|
413
|
+
if (decimalIndex >= 0) {
|
|
414
|
+
scale = text.length - decimalIndex - 1;
|
|
415
|
+
text = text.slice(0, decimalIndex) + text.slice(decimalIndex + 1);
|
|
416
|
+
}
|
|
417
|
+
text = text.replace(/^0+/, "") || "0";
|
|
418
|
+
if (!/^[0-9]+$/.test(text))
|
|
419
|
+
return null;
|
|
420
|
+
return { coefficient: BigInt(text) * sign, scale: scale - exponent };
|
|
421
|
+
}
|
|
422
|
+
function isPortablePattern(pattern) {
|
|
423
|
+
if (!pattern.startsWith("^") || !pattern.endsWith("$") || pattern.length < 3)
|
|
424
|
+
return false;
|
|
425
|
+
const body = pattern.slice(1, -1);
|
|
426
|
+
let index = 0;
|
|
427
|
+
let atoms = 0;
|
|
428
|
+
while (index < body.length) {
|
|
429
|
+
const current = body[index];
|
|
430
|
+
if (current === "[") {
|
|
431
|
+
const end = body.indexOf("]", index + 1);
|
|
432
|
+
if (end < 0 || end === index + 1 || !/^[A-Za-z0-9._~:/-]+$/.test(body.slice(index + 1, end)))
|
|
433
|
+
return false;
|
|
434
|
+
index = end + 1;
|
|
435
|
+
}
|
|
436
|
+
else if (current === "\\") {
|
|
437
|
+
if (index + 1 >= body.length || !".\\-".includes(body[index + 1]))
|
|
438
|
+
return false;
|
|
439
|
+
index += 2;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
if (!/^[A-Za-z0-9_~:/-]$/.test(current))
|
|
443
|
+
return false;
|
|
444
|
+
index += 1;
|
|
445
|
+
}
|
|
446
|
+
atoms += 1;
|
|
447
|
+
if (index >= body.length)
|
|
448
|
+
continue;
|
|
449
|
+
if ("+*?".includes(body[index])) {
|
|
450
|
+
index += 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (body[index] === "{") {
|
|
454
|
+
const end = body.indexOf("}", index + 1);
|
|
455
|
+
if (end < 0 || !/^(?:0|[1-9][0-9]*)(?:,(?:0|[1-9][0-9]*)?)?$/.test(body.slice(index + 1, end)))
|
|
456
|
+
return false;
|
|
457
|
+
index = end + 1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return atoms > 0;
|
|
461
|
+
}
|
|
462
|
+
function canonicalJSONKey(value) {
|
|
463
|
+
if (value === null || typeof value !== "object")
|
|
464
|
+
return JSON.stringify(value);
|
|
465
|
+
if (Array.isArray(value))
|
|
466
|
+
return `[${value.map(canonicalJSONKey).join(",")}]`;
|
|
467
|
+
const record = value;
|
|
468
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJSONKey(record[key])}`).join(",")}}`;
|
|
469
|
+
}
|
|
470
|
+
function validateConst(value, schema) {
|
|
471
|
+
return !Object.hasOwn(schema, "const") || Object.is(value, schema.const);
|
|
472
|
+
}
|
|
473
|
+
function integerBound(value, minimum, maximum) {
|
|
474
|
+
if (typeof minimum === "number" && (!Number.isSafeInteger(minimum) || value < minimum))
|
|
475
|
+
return false;
|
|
476
|
+
if (typeof maximum === "number" && (!Number.isSafeInteger(maximum) || value > maximum))
|
|
477
|
+
return false;
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
function isRecord(value) {
|
|
481
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
482
|
+
}
|
|
483
|
+
function hasExactKeys(value, keys) {
|
|
484
|
+
if (!isRecord(value))
|
|
485
|
+
return false;
|
|
486
|
+
const actual = Object.keys(value).sort();
|
|
487
|
+
const expected = [...keys].sort();
|
|
488
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
489
|
+
}
|
|
490
|
+
function validOpaqueIdentifier(value, prefix) {
|
|
491
|
+
return typeof value === "string" && value.startsWith(`${prefix}_`) && value.length >= 8 && value.length <= 160 && /^[-A-Za-z0-9_]+$/.test(value);
|
|
492
|
+
}
|
|
493
|
+
function pluginStreamTerminalError(status) {
|
|
494
|
+
if (status === "failed")
|
|
495
|
+
return new PluginBridgeError("PLUGIN_STREAM_FAILED", "Plugin stream execution failed");
|
|
496
|
+
return new PluginBridgeError("PLUGIN_STREAM_CANCELLED", `Plugin stream ended with status ${status}`);
|
|
497
|
+
}
|
|
498
|
+
const prototypeSensitivePropertyNames = new Set(["__proto__", "constructor", "prototype"]);
|
|
499
|
+
function contractMismatch(method, detail) {
|
|
500
|
+
return new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", `Capability method ${method} ${detail}`);
|
|
501
|
+
}
|
package/dist/contracts.gen.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare const redevPluginContractVersions: {
|
|
|
23
23
|
readonly compatibility_schema_version: "compatibility-manifest-v2";
|
|
24
24
|
readonly release_manifest_schema_version: "release-manifest-v2";
|
|
25
25
|
readonly worker_invocation_schema_version: "worker-invocation-v1";
|
|
26
|
+
readonly host_capability_contract_schema_version: "host-capability-contract-v1";
|
|
27
|
+
readonly host_capability_pin_schema_version: "host-capability-pin-v1";
|
|
28
|
+
readonly host_capability_manifest_schema_version: "host-capability-manifest-v1";
|
|
29
|
+
readonly host_capability_compatibility_schema_version: "host-capability-compatibility-v1";
|
|
30
|
+
readonly host_capability_signature_schema_version: "host-capability-signature-v1";
|
|
31
|
+
readonly host_capability_notices_schema_version: "host-capability-notices-v1";
|
|
26
32
|
readonly error_codes_schema_version: "error-codes-v1";
|
|
27
33
|
readonly contract_registry_version: "contract-registry-v1";
|
|
28
34
|
};
|
|
@@ -36,12 +42,12 @@ export declare const redevPluginContractArtifacts: readonly [{
|
|
|
36
42
|
readonly id: "plugin-platform-openapi";
|
|
37
43
|
readonly path: "spec/openapi/plugin-platform-v2.yaml";
|
|
38
44
|
readonly version: "plugin-platform-v2";
|
|
39
|
-
readonly sha256: "
|
|
45
|
+
readonly sha256: "b7922ebd37be680060f067b66a439f3d1f9a7f22d730512763e3f7d347dcaf9a";
|
|
40
46
|
}, {
|
|
41
47
|
readonly id: "manifest-schema";
|
|
42
48
|
readonly path: "spec/plugin/manifest-v2.schema.json";
|
|
43
49
|
readonly version: "manifest-v2";
|
|
44
|
-
readonly sha256: "
|
|
50
|
+
readonly sha256: "f80279943bde0e304138ee0fdae2e7b9f2b0334412cf159d268cc8badc196759";
|
|
45
51
|
}, {
|
|
46
52
|
readonly id: "package-signature-schema";
|
|
47
53
|
readonly path: "spec/plugin/package-signature-v1.schema.json";
|
|
@@ -51,12 +57,12 @@ export declare const redevPluginContractArtifacts: readonly [{
|
|
|
51
57
|
readonly id: "release-metadata-schema";
|
|
52
58
|
readonly path: "spec/plugin/release-metadata-v2.schema.json";
|
|
53
59
|
readonly version: "release-metadata-v2";
|
|
54
|
-
readonly sha256: "
|
|
60
|
+
readonly sha256: "5f24cf61bea352715f24595c385745476e677affdaf8f60fd15e534424df60b6";
|
|
55
61
|
}, {
|
|
56
62
|
readonly id: "source-policy-schema";
|
|
57
63
|
readonly path: "spec/plugin/source-policy-v1.schema.json";
|
|
58
64
|
readonly version: "source-policy-v1";
|
|
59
|
-
readonly sha256: "
|
|
65
|
+
readonly sha256: "fb6a9c27e726378a28a9ff48ee47c40ede99fa95a524a308e2c762bebc4b9fe2";
|
|
60
66
|
}, {
|
|
61
67
|
readonly id: "source-revocations-schema";
|
|
62
68
|
readonly path: "spec/plugin/source-revocations-v1.schema.json";
|
|
@@ -66,12 +72,12 @@ export declare const redevPluginContractArtifacts: readonly [{
|
|
|
66
72
|
readonly id: "token-ticket-schema";
|
|
67
73
|
readonly path: "spec/plugin/token-ticket-v2.schema.json";
|
|
68
74
|
readonly version: "token-ticket-v2";
|
|
69
|
-
readonly sha256: "
|
|
75
|
+
readonly sha256: "f0342614f28b81045ac47d76d71753c8ee177f8143c14a711a0b1f5632e7835e";
|
|
70
76
|
}, {
|
|
71
77
|
readonly id: "iframe-bridge-schema";
|
|
72
78
|
readonly path: "spec/plugin/bridge-v2.schema.json";
|
|
73
79
|
readonly version: "bridge-v2";
|
|
74
|
-
readonly sha256: "
|
|
80
|
+
readonly sha256: "23d968113aaf93dab75df6acd96a350df07c0428fd2a00c66c015a0d875592d1";
|
|
75
81
|
}, {
|
|
76
82
|
readonly id: "opaque-surface-document-schema";
|
|
77
83
|
readonly path: "spec/plugin/opaque-surface-document-v1.schema.json";
|
|
@@ -86,7 +92,7 @@ export declare const redevPluginContractArtifacts: readonly [{
|
|
|
86
92
|
readonly id: "compatibility-manifest-schema";
|
|
87
93
|
readonly path: "spec/plugin/compatibility-manifest-v2.schema.json";
|
|
88
94
|
readonly version: "compatibility-manifest-v2";
|
|
89
|
-
readonly sha256: "
|
|
95
|
+
readonly sha256: "7e0789cc80f4674277c6d12f4e097198543081b198b7a1a94eefb3cd404c6eb0";
|
|
90
96
|
}, {
|
|
91
97
|
readonly id: "release-manifest-schema";
|
|
92
98
|
readonly path: "spec/plugin/release-manifest-v2.schema.json";
|
|
@@ -96,17 +102,47 @@ export declare const redevPluginContractArtifacts: readonly [{
|
|
|
96
102
|
readonly id: "worker-invocation-schema";
|
|
97
103
|
readonly path: "spec/plugin/worker-invocation-v1.schema.json";
|
|
98
104
|
readonly version: "worker-invocation-v1";
|
|
99
|
-
readonly sha256: "
|
|
105
|
+
readonly sha256: "9199dd0731aaf592e34c1574cd81c094412a3e6cd9b633853d3e60e89438bcb5";
|
|
106
|
+
}, {
|
|
107
|
+
readonly id: "host-capability-contract-schema";
|
|
108
|
+
readonly path: "spec/plugin/host-capability-contract-v1.schema.json";
|
|
109
|
+
readonly version: "host-capability-contract-v1";
|
|
110
|
+
readonly sha256: "a2318b01211aa6dbd8ca983f7ebca311619527aa4828a0ea0d201ebac7427292";
|
|
111
|
+
}, {
|
|
112
|
+
readonly id: "host-capability-pin-schema";
|
|
113
|
+
readonly path: "spec/plugin/host-capability-pin-v1.schema.json";
|
|
114
|
+
readonly version: "host-capability-pin-v1";
|
|
115
|
+
readonly sha256: "20526a5934f0d85a3db7882492487266f09de5bcd176e2c7936f05d0b7fe0338";
|
|
116
|
+
}, {
|
|
117
|
+
readonly id: "host-capability-manifest-schema";
|
|
118
|
+
readonly path: "spec/plugin/host-capability-manifest-v1.schema.json";
|
|
119
|
+
readonly version: "host-capability-manifest-v1";
|
|
120
|
+
readonly sha256: "5f13a8e5f918378b9cba9fdc133a9a75ba3a8311c30cb6a02ca0aba09035110e";
|
|
121
|
+
}, {
|
|
122
|
+
readonly id: "host-capability-compatibility-schema";
|
|
123
|
+
readonly path: "spec/plugin/host-capability-compatibility-v1.schema.json";
|
|
124
|
+
readonly version: "host-capability-compatibility-v1";
|
|
125
|
+
readonly sha256: "361ebbf7009aeb66c763ecde46427b17b6cf7d2307c01886c5987a1f4f1762de";
|
|
126
|
+
}, {
|
|
127
|
+
readonly id: "host-capability-signature-schema";
|
|
128
|
+
readonly path: "spec/plugin/host-capability-signature-v1.schema.json";
|
|
129
|
+
readonly version: "host-capability-signature-v1";
|
|
130
|
+
readonly sha256: "88cbc1d63afb7e289ca4f8b7c76f702b8d20156f7722c1b71f3ab9138a240a5e";
|
|
131
|
+
}, {
|
|
132
|
+
readonly id: "host-capability-notices-schema";
|
|
133
|
+
readonly path: "spec/plugin/host-capability-notices-v1.schema.json";
|
|
134
|
+
readonly version: "host-capability-notices-v1";
|
|
135
|
+
readonly sha256: "6affd5d0ae90239f6fd08cbeddae4eef393efa2838c9e4935874ba3dc0e05948";
|
|
100
136
|
}, {
|
|
101
137
|
readonly id: "error-codes-schema";
|
|
102
138
|
readonly path: "spec/plugin/error-codes-v1.schema.json";
|
|
103
139
|
readonly version: "error-codes-v1";
|
|
104
|
-
readonly sha256: "
|
|
140
|
+
readonly sha256: "254eb4c6d25bae3976c30fe51bdd49456e906e1a3f3bc76df092afd30bd4c948";
|
|
105
141
|
}, {
|
|
106
142
|
readonly id: "rust-ipc-schema";
|
|
107
143
|
readonly path: "spec/plugin/ipc-v1.schema.json";
|
|
108
144
|
readonly version: "rust-ipc-v1";
|
|
109
|
-
readonly sha256: "
|
|
145
|
+
readonly sha256: "78f1310a0b48db733301c3f163ada041a3c9daf2d09369860d758296cbe12e39";
|
|
110
146
|
}, {
|
|
111
147
|
readonly id: "wasm-worker-schema";
|
|
112
148
|
readonly path: "spec/plugin/wasm-worker-v1.schema.json";
|
|
@@ -126,5 +162,5 @@ export declare const redevPluginContractArtifacts: readonly [{
|
|
|
126
162
|
readonly id: "contract-registry";
|
|
127
163
|
readonly path: "spec/plugin/contract-registry-v1.json";
|
|
128
164
|
readonly version: "contract-registry-v1";
|
|
129
|
-
readonly sha256: "
|
|
165
|
+
readonly sha256: "42dd916922a6183b49d4abc765da2e2d086771a6e6ac1828f8e1dbebb262bba0";
|
|
130
166
|
}];
|