Skip to content

Add WebSocket collaboration schemas and RLS integration to permission protocol#386

Merged
hotlong merged 2 commits intomainfrom
copilot/add-websocket-collaboration-features
Jan 30, 2026
Merged

Add WebSocket collaboration schemas and RLS integration to permission protocol#386
hotlong merged 2 commits intomainfrom
copilot/add-websocket-collaboration-features

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 30, 2026

Adds simplified WebSocket collaboration APIs and row-level security capabilities to the permission system. Complements existing comprehensive protocols with lightweight alternatives for common use cases.

Changes

WebSocket Protocol (api/websocket.zod.ts)

Added simplified schemas alongside existing comprehensive protocol:

  • WebSocketEventSchema - Basic event protocol (subscribe, data-change, presence-update, cursor-update)
  • SimplePresenceStateSchema - User presence tracking
  • SimpleCursorPositionSchema - Collaborative cursor position
  • WebSocketServerConfigSchema - Server-side feature toggles
// Subscribe to real-time changes
WebSocketEventSchema.parse({
  type: 'subscribe',
  channel: 'record.account.123',
  payload: { events: ['created', 'updated'] },
  timestamp: Date.now()
});

Permission Protocol (permission/permission.zod.ts)

Extended PermissionSetSchema with row-level security:

  • RLSRuleSchema - Field-level access filters with context variable support
  • rls field - Array of RLS rules per permission set
  • contextVariables field - Runtime evaluation context
// Permission set with tenant isolation
PermissionSetSchema.parse({
  name: 'standard_user',
  objects: { account: { allowRead: true } },
  rls: [{
    name: 'tenant_isolation',
    objectName: 'account',
    operation: 'read',
    filter: {
      field: 'tenant_id',
      operator: 'eq',
      value: { contextVariable: 'current_user.tenant_id' }
    }
  }],
  contextVariables: { access_level: 2 }
});

Implementation Notes

  • WebSocket schemas use Unix timestamps (ms) vs ISO 8601 for performance
  • RLS rules support 8 filter operators (eq, ne, in, nin, gt, gte, lt, lte)
  • Context variables enable dynamic filtering based on user session state
  • All schemas follow ObjectStack conventions (camelCase config, snake_case identifiers)
  • Generated JSON schemas and TypeScript types for IDE/tooling integration
Original prompt
  1. API Protocol Completeness / API协议完整性

4.1 WebSocket Enhancement

Current: packages/spec/src/api/websocket.zod.ts
Enhancement: Add collaboration features

import { z } from 'zod';

export const WebSocketEventSchema = z.object({
type: z.enum([
'subscribe',
'unsubscribe',
'data-change',
'presence-update',
'cursor-update',
'error',
]),
channel: z.string(),
payload: z.any(),
timestamp: z.number(),
});

export const PresenceStateSchema = z.object({
userId: z.string(),
userName: z.string(),
status: z.enum(['online', 'away', 'offline']),
lastSeen: z.number(),
metadata: z.record(z.any()).optional(),
});

export const CursorPositionSchema = z.object({
userId: z.string(),
recordId: z.string(),
fieldName: z.string(),
position: z.number(),
selection: z.object({
start: z.number(),
end: z.number(),
}).optional(),
});

export const WebSocketConfigSchema = z.object({
enabled: z.boolean().default(false),
path: z.string().default('/ws'),
heartbeatInterval: z.number().default(30000),
reconnectAttempts: z.number().default(5),
presence: z.boolean().default(false),
cursorSharing: z.boolean().default(false),
});

export type WebSocketConfig = z.infer;
5. Security Protocol Framework / 安全协议框架

5.1 Row-Level Security Enhancement

Current: packages/spec/src/auth/permission.zod.ts
Enhancement: Add dynamic RLS rules

import { z } from 'zod';

export const RLSRuleSchema = z.object({
name: z.string(),
objectName: z.string(),
operation: z.enum(['read', 'create', 'update', 'delete']),
filter: z.object({
field: z.string(),
operator: z.enum(['eq', 'ne', 'in', 'nin', 'gt', 'gte', 'lt', 'lte']),
value: z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.any()),
z.object({ contextVariable: z.string() }),
]),
}),
enabled: z.boolean().default(true),
priority: z.number().default(0),
});

export const PermissionSchema = z.object({
// ... existing fields

// ✅ NEW: Row-level security
rls: z.array(RLSRuleSchema).optional(),

// ✅ NEW: Context-based access control
contextVariables: z.record(z.any()).optional(),
});


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jan 30, 2026 1:05pm

Request Review

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Enhance WebSocket with collaboration features Add WebSocket collaboration schemas and RLS integration to permission protocol Jan 30, 2026
Copilot AI requested a review from hotlong January 30, 2026 13:05
@hotlong hotlong marked this pull request as ready for review January 30, 2026 13:08
Copilot AI review requested due to automatic review settings January 30, 2026 13:08
@github-actions github-actions bot added documentation Improvements or additions to documentation size/l labels Jan 30, 2026
@hotlong hotlong merged commit 0305b08 into main Jan 30, 2026
14 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds simplified WebSocket collaboration schemas and row-level security (RLS) capabilities to the ObjectStack specification. The changes introduce lightweight alternatives to existing comprehensive protocols for common use cases, enabling easier integration for basic real-time collaboration and tenant isolation scenarios.

Changes:

  • Adds simplified WebSocket event schemas (WebSocketEventSchema, SimplePresenceStateSchema, SimpleCursorPositionSchema, WebSocketServerConfigSchema) with Unix timestamp support for performance
  • Extends PermissionSetSchema with embedded RLS rules using structured filter objects instead of SQL expressions
  • Generates corresponding JSON schemas and documentation for IDE/tooling integration

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/spec/src/api/websocket.zod.ts Adds simplified WebSocket collaboration API with basic event protocol, presence tracking, cursor sharing, and server configuration schemas
packages/spec/src/permission/permission.zod.ts Extends permission protocol with simplified RLS rules (RLSRuleSchema) and context variables for row-level filtering
packages/spec/json-schema/permission/RLSRule.json Generated JSON schema for RLS rule validation
packages/spec/json-schema/permission/PermissionSet.json Updated JSON schema including new rls and contextVariables fields
packages/spec/json-schema/api/WebSocketServerConfig.json Generated JSON schema for WebSocket server configuration
packages/spec/json-schema/api/WebSocketEvent.json Generated JSON schema for simplified WebSocket events
packages/spec/json-schema/api/SimplePresenceState.json Generated JSON schema for simplified presence tracking
packages/spec/json-schema/api/SimpleCursorPosition.json Generated JSON schema for simplified cursor positions
content/docs/references/permission/permission.mdx Updated documentation with RLSRule schema reference and property tables
content/docs/references/api/websocket.mdx Updated documentation with new simplified WebSocket schema references

Comment on lines +36 to +48
filter: z.object({
field: z.string().describe('Field name to filter on'),
operator: z.enum(['eq', 'ne', 'in', 'nin', 'gt', 'gte', 'lt', 'lte'])
.describe('Filter operator'),
value: z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.any()),
z.object({ contextVariable: z.string() })
.describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'),
]).describe('Filter value or context variable reference'),
}).describe('Filter condition for row-level access'),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RLS filter implementation lacks security documentation about preventing injection attacks. The comprehensive RLS protocol (../permission/rls.zod.ts, lines 267-275) includes important security notes about using parameterized queries to prevent SQL injection when evaluating RLS conditions. Consider adding similar security guidance in the documentation for the simplified RLS filter, explaining how filter values and context variables should be safely evaluated at runtime.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +35
operation: z.enum(['read', 'create', 'update', 'delete'])
.describe('Database operation this rule applies to'),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operation enum values in the simplified RLS schema differ from the comprehensive RLS protocol. The comprehensive protocol uses database operations (select, insert, update, delete, all) while the simplified version uses CRUD operations (read, create, update, delete). This inconsistency could cause confusion and integration issues.

Consider either:

  1. Aligning with the comprehensive protocol by using select and insert instead of read and create, OR
  2. Adding explicit documentation explaining why different operation names are used and how they map between the two protocols

Copilot uses AI. Check for mistakes.
Comment on lines +465 to +571
export const WebSocketEventSchema = z.object({
type: z.enum([
'subscribe', // Client subscribes to channel
'unsubscribe', // Client unsubscribes from channel
'data-change', // Data modification event
'presence-update', // User presence change
'cursor-update', // Cursor position change (collaborative editing)
'error', // Error message
]).describe('Event type'),
channel: z.string().describe('Channel identifier (e.g., "record.account.123", "user.456")'),
payload: z.any().describe('Event payload data'),
timestamp: z.number().describe('Unix timestamp in milliseconds'),
});

export type WebSocketEvent = z.infer<typeof WebSocketEventSchema>;

/**
* Simplified Presence State Schema
*
* A simplified presence schema for basic user presence tracking.
* Complements the comprehensive PresenceStateSchema for simpler integrations.
*
* Use this for basic presence features. For advanced features like device tracking,
* custom status, and session management, use the comprehensive PresenceStateSchema above.
*
* @example User online
* ```typescript
* {
* userId: 'user123',
* userName: 'John Doe',
* status: 'online',
* lastSeen: Date.now(),
* metadata: { currentPage: '/dashboard' }
* }
* ```
*/
export const SimplePresenceStateSchema = z.object({
userId: z.string().describe('User identifier'),
userName: z.string().describe('User display name'),
status: z.enum(['online', 'away', 'offline']).describe('User presence status'),
lastSeen: z.number().describe('Unix timestamp of last activity in milliseconds'),
metadata: z.record(z.any()).optional().describe('Additional presence metadata (e.g., current page, custom status)'),
});

export type SimplePresenceState = z.infer<typeof SimplePresenceStateSchema>;

/**
* Simplified Cursor Position Schema
*
* A simplified cursor position schema for basic collaborative editing.
* Complements the comprehensive CursorPositionSchema for simpler use cases.
*
* Use this for basic cursor sharing. For advanced features like selections,
* color coding, and document versioning, use the comprehensive CursorPositionSchema above.
*
* @example Cursor in text field
* ```typescript
* {
* userId: 'user123',
* recordId: 'account_456',
* fieldName: 'description',
* position: 42,
* selection: { start: 42, end: 57 }
* }
* ```
*/
export const SimpleCursorPositionSchema = z.object({
userId: z.string().describe('User identifier'),
recordId: z.string().describe('Record identifier being edited'),
fieldName: z.string().describe('Field name being edited'),
position: z.number().describe('Cursor position (character offset from start)'),
selection: z.object({
start: z.number().describe('Selection start position'),
end: z.number().describe('Selection end position'),
}).optional().describe('Text selection range (if text is selected)'),
});

export type SimpleCursorPosition = z.infer<typeof SimpleCursorPositionSchema>;

/**
* WebSocket Server Configuration Schema
*
* Server-side configuration for WebSocket services.
* Controls features like presence tracking, cursor sharing, and connection management.
*
* @example Production configuration
* ```typescript
* {
* enabled: true,
* path: '/ws',
* heartbeatInterval: 30000,
* reconnectAttempts: 5,
* presence: true,
* cursorSharing: true
* }
* ```
*/
export const WebSocketServerConfigSchema = z.object({
enabled: z.boolean().default(false).describe('Enable WebSocket server'),
path: z.string().default('/ws').describe('WebSocket endpoint path'),
heartbeatInterval: z.number().default(30000).describe('Heartbeat interval in milliseconds'),
reconnectAttempts: z.number().default(5).describe('Maximum reconnection attempts for clients'),
presence: z.boolean().default(false).describe('Enable presence tracking'),
cursorSharing: z.boolean().default(false).describe('Enable collaborative cursor sharing'),
});

export type WebSocketServerConfig = z.infer<typeof WebSocketServerConfigSchema>;
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly introduced WebSocketEventSchema, SimplePresenceStateSchema, SimpleCursorPositionSchema, and WebSocketServerConfigSchema lack test coverage. The codebase follows a pattern of comprehensive testing for all schemas (e.g., WebSocketConfigSchema has tests in websocket.test.ts). Add test cases to verify schema validation, defaults, and edge cases for these new schemas.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +53
export const RLSRuleSchema = z.object({
name: z.string()
.regex(/^[a-z_][a-z0-9_]*$/)
.describe('Rule unique identifier (snake_case)'),
objectName: z.string().describe('Target object name'),
operation: z.enum(['read', 'create', 'update', 'delete'])
.describe('Database operation this rule applies to'),
filter: z.object({
field: z.string().describe('Field name to filter on'),
operator: z.enum(['eq', 'ne', 'in', 'nin', 'gt', 'gte', 'lt', 'lte'])
.describe('Filter operator'),
value: z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.any()),
z.object({ contextVariable: z.string() })
.describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'),
]).describe('Filter value or context variable reference'),
}).describe('Filter condition for row-level access'),
enabled: z.boolean().default(true).describe('Whether this rule is active'),
priority: z.number().default(0).describe('Rule evaluation priority (higher = evaluated first)'),
});

export type RLSRule = z.infer<typeof RLSRuleSchema>;
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly introduced RLSRuleSchema lacks test coverage. The codebase follows a pattern of comprehensive testing for all schemas (e.g., PermissionSetSchema has extensive tests in permission.test.ts). Add test cases to validate the RLS rule schema, including filter operators, context variable references, and edge cases such as invalid snake_case names or unsupported operators.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +31
name: z.string()
.regex(/^[a-z_][a-z0-9_]*$/)
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RLSRuleSchema duplicates the snake_case validation pattern instead of using the existing SnakeCaseIdentifierSchema imported at line 2. For consistency with the codebase (e.g., PermissionSetSchema at line 129), use SnakeCaseIdentifierSchema for the name field instead of the inline regex pattern.

Suggested change
name: z.string()
.regex(/^[a-z_][a-z0-9_]*$/)
name: SnakeCaseIdentifierSchema

Copilot uses AI. Check for mistakes.
name: z.string()
.regex(/^[a-z_][a-z0-9_]*$/)
.describe('Rule unique identifier (snake_case)'),
objectName: z.string().describe('Target object name'),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplified RLS schema uses objectName while the comprehensive RLS protocol (../permission/rls.zod.ts, line 240) uses object for the same field. This naming inconsistency between the two protocols could confuse users when migrating from the simplified to the comprehensive version. Consider using object to maintain consistency with the comprehensive protocol.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +46
z.object({ contextVariable: z.string() })
.describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contextVariable field accepts any string without validation. Consider adding pattern validation to ensure the context variable follows the expected format (e.g., 'current_user.tenant_id', 'current_user.department'). This would provide better developer experience through early validation and prevent runtime errors.

Suggested change
z.object({ contextVariable: z.string() })
.describe('Reference to context variable (e.g., { contextVariable: "current_user.tenant_id" })'),
z.object({
contextVariable: z.string()
.regex(/^current_user\.[a-z_][a-z0-9_]*$/)
.describe('Reference to context variable (current_user.<snake_case_field>, e.g., "current_user.tenant_id")'),
}),

Copilot uses AI. Check for mistakes.
'cursor-update', // Cursor position change (collaborative editing)
'error', // Error message
]).describe('Event type'),
channel: z.string().describe('Channel identifier (e.g., "record.account.123", "user.456")'),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The channel field accepts any string without validation. Consider adding pattern validation or documentation about expected channel identifier formats (e.g., 'record.{object}.{id}', 'user.{id}'). The examples in the documentation show specific patterns like 'record.account.123' and 'user.456', but these patterns aren't enforced by the schema.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants