Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,42 @@ jobs:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI }}
run: twine upload dist/*

publish-express-middleware:
name: Publish Express Middleware
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/middleware-v')
defaults:
run:
working-directory: implementations/express-middleware
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

publish-registry:
name: Publish Registry CLI
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/registry-v')
defaults:
run:
working-directory: tools/registry
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
67 changes: 64 additions & 3 deletions implementations/express-middleware/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,35 @@ import { Router, Request, Response, NextFunction } from 'express';
import * as fs from 'fs/promises';
import * as path from 'path';

// =============================================================================
// Multi-modal Content Types
// =============================================================================

export interface TextContent {
type: 'text';
text: string;
}

export interface ImageUrl {
url: string;
detail?: 'auto' | 'low' | 'high';
}

export interface ImageContent {
type: 'image_url';
image_url: ImageUrl;
}

export type ContentPart = TextContent | ImageContent;
export type PromptContent = string | ContentPart[];

// =============================================================================
// Prompt Types
// =============================================================================

export interface PromptEnvelope {
id: string;
content: string;
content: PromptContent;
meta: Record<string, unknown>;
}

Expand Down Expand Up @@ -170,6 +196,40 @@ function authMiddleware(
};
}

/**
* Validate content part structure
*/
function isValidContentPart(part: unknown): part is ContentPart {
if (!part || typeof part !== 'object') return false;
const obj = part as Record<string, unknown>;

if (obj.type === 'text') {
return typeof obj.text === 'string';
}

if (obj.type === 'image_url') {
const imageUrl = obj.image_url as Record<string, unknown> | undefined;
return imageUrl !== null && typeof imageUrl === 'object' && typeof imageUrl.url === 'string';
}

return false;
}

/**
* Validate content: string or array of ContentParts
*/
function isValidContent(content: unknown): content is PromptContent {
if (typeof content === 'string') {
return true;
}

if (Array.isArray(content)) {
return content.length > 0 && content.every(isValidContentPart);
}

return false;
}

/**
* Validate prompt ID
* - Must contain only alphanumeric, /, _, -
Expand Down Expand Up @@ -285,7 +345,8 @@ export function plpMiddleware(options: PLPMiddlewareOptions): Router {

const { content, meta } = req.body as { content?: unknown; meta?: unknown };

if (typeof content !== 'string') {
// Validate content: must be string or array of ContentParts
if (!isValidContent(content)) {
return res
.status(400)
.json({ error: 'Invalid request - content field is required' });
Expand Down Expand Up @@ -337,4 +398,4 @@ export function plpMiddleware(options: PLPMiddlewareOptions): Router {
}

// Export types and utilities
export { FileStorage, validatePromptId, parsePromptPath, SEMVER_PATTERN };
export { FileStorage, validatePromptId, parsePromptPath, SEMVER_PATTERN, isValidContent, isValidContentPart };
2 changes: 1 addition & 1 deletion tools/registry/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@goreal-ai/plp-registry",
"version": "0.0.3",
"version": "1.0.0",
"description": "PLP Registry CLI - Manage PLP server registration, compliance testing, and listing on plp.pub",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
40 changes: 37 additions & 3 deletions tools/registry/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,40 @@ function buildHeaders(ctx: TestContext, contentType = false): Record<string, str
return headers;
}

/**
* Validate content part structure
*/
function isValidContentPart(part: unknown): boolean {
if (!part || typeof part !== 'object') return false;
const obj = part as Record<string, unknown>;

if (obj.type === 'text') {
return typeof obj.text === 'string';
}

if (obj.type === 'image_url') {
const imageUrl = obj.image_url as Record<string, unknown> | undefined;
return imageUrl !== null && typeof imageUrl === 'object' && typeof imageUrl.url === 'string';
}

return false;
}

/**
* Validate content: string or array of ContentParts
*/
function isValidContent(content: unknown): boolean {
if (typeof content === 'string') {
return true;
}

if (Array.isArray(content)) {
return content.length > 0 && content.every(isValidContentPart);
}

return false;
}

/**
* Validate envelope structure
*/
Expand All @@ -112,7 +146,7 @@ function isValidEnvelope(data: unknown): data is PromptEnvelope {
const obj = data as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.content === 'string' &&
isValidContent(obj.content) &&
obj.meta !== null &&
typeof obj.meta === 'object' &&
!Array.isArray(obj.meta)
Expand Down Expand Up @@ -530,8 +564,8 @@ const envelopeTests: TestDefinition[] = [
if (typeof envelope.id !== 'string') {
throw new Error('Envelope field "id" must be a string');
}
if (typeof envelope.content !== 'string') {
throw new Error('Envelope field "content" must be a string');
if (!isValidContent(envelope.content)) {
throw new Error('Envelope field "content" must be a string or array of ContentParts');
}
if (typeof envelope.meta !== 'object' || envelope.meta === null || Array.isArray(envelope.meta)) {
throw new Error('Envelope field "meta" must be an object');
Expand Down
21 changes: 20 additions & 1 deletion tools/registry/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,28 @@ export interface TestContext {
debug?: boolean;
}

// Multi-modal content types
export interface TextContent {
type: 'text';
text: string;
}

export interface ImageUrl {
url: string;
detail?: 'auto' | 'low' | 'high';
}

export interface ImageContent {
type: 'image_url';
image_url: ImageUrl;
}

export type ContentPart = TextContent | ImageContent;
export type PromptContent = string | ContentPart[];

export interface PromptEnvelope {
id: string;
content: string;
content: PromptContent;
meta: Record<string, unknown>;
}

Expand Down