@arcote.tech/arc-host 0.1.11 → 0.3.1
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/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4433 -425
- package/dist/index.js.map +16 -14
- package/dist/sqliteAdapter.d.ts +1 -1
- package/dist/sqliteAdapter.d.ts.map +1 -1
- package/dist/src/arc-host.d.ts +69 -0
- package/dist/src/arc-host.d.ts.map +1 -0
- package/dist/src/connection-manager.d.ts +50 -0
- package/dist/src/connection-manager.d.ts.map +1 -0
- package/dist/src/context-handler.d.ts +55 -0
- package/dist/src/context-handler.d.ts.map +1 -0
- package/dist/src/event-auth.d.ts +19 -0
- package/dist/src/event-auth.d.ts.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/types.d.ts +100 -0
- package/dist/src/types.d.ts.map +1 -0
- package/index.ts +18 -7
- package/package.json +2 -1
- package/sqliteAdapter.ts +4 -29
- package/src/arc-host.ts +646 -0
- package/src/connection-manager.ts +115 -0
- package/src/context-handler.ts +219 -0
- package/src/event-auth.ts +127 -0
- package/src/index.ts +24 -0
- package/src/types.ts +112 -0
- package/dist/host.d.ts +0 -45
- package/dist/host.d.ts.map +0 -1
- package/dist/postgresAdapter.d.ts +0 -3
- package/dist/postgresAdapter.d.ts.map +0 -1
- package/host.ts +0 -510
- package/postgresAdapter.ts +0 -50
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import type {
|
|
3
|
+
ConnectedClient,
|
|
4
|
+
HostToClientMessage,
|
|
5
|
+
TokenPayload,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages WebSocket connections for a host
|
|
10
|
+
*/
|
|
11
|
+
export class ConnectionManager {
|
|
12
|
+
private clients = new Map<string, ConnectedClient>();
|
|
13
|
+
private clientIdCounter = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a new client connection
|
|
17
|
+
*/
|
|
18
|
+
addClient(
|
|
19
|
+
ws: ServerWebSocket<{ clientId: string }>,
|
|
20
|
+
token: TokenPayload | null,
|
|
21
|
+
rawToken: string | null,
|
|
22
|
+
): ConnectedClient {
|
|
23
|
+
const clientId = `client_${++this.clientIdCounter}_${Date.now()}`;
|
|
24
|
+
|
|
25
|
+
const client: ConnectedClient = {
|
|
26
|
+
id: clientId,
|
|
27
|
+
token,
|
|
28
|
+
rawToken,
|
|
29
|
+
lastHostEventId: null,
|
|
30
|
+
ws,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.clients.set(clientId, client);
|
|
34
|
+
|
|
35
|
+
// Store clientId in WebSocket data for later lookup
|
|
36
|
+
ws.data = { clientId };
|
|
37
|
+
|
|
38
|
+
return client;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Remove a client connection
|
|
43
|
+
*/
|
|
44
|
+
removeClient(clientId: string): void {
|
|
45
|
+
this.clients.delete(clientId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get client by ID
|
|
50
|
+
*/
|
|
51
|
+
getClient(clientId: string): ConnectedClient | undefined {
|
|
52
|
+
return this.clients.get(clientId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get client by WebSocket
|
|
57
|
+
*/
|
|
58
|
+
getClientByWs(
|
|
59
|
+
ws: ServerWebSocket<{ clientId: string }>,
|
|
60
|
+
): ConnectedClient | undefined {
|
|
61
|
+
const clientId = ws.data?.clientId;
|
|
62
|
+
if (!clientId) return undefined;
|
|
63
|
+
return this.clients.get(clientId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update client's last synced event ID
|
|
68
|
+
*/
|
|
69
|
+
updateLastSyncedEventId(clientId: string, hostEventId: string): void {
|
|
70
|
+
const client = this.clients.get(clientId);
|
|
71
|
+
if (client) {
|
|
72
|
+
client.lastHostEventId = hostEventId;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all connected clients
|
|
78
|
+
*/
|
|
79
|
+
getAllClients(): ConnectedClient[] {
|
|
80
|
+
return Array.from(this.clients.values());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Send message to a specific client
|
|
85
|
+
*/
|
|
86
|
+
sendToClient(clientId: string, message: HostToClientMessage): boolean {
|
|
87
|
+
const client = this.clients.get(clientId);
|
|
88
|
+
if (!client) return false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
client.ws.send(JSON.stringify(message));
|
|
92
|
+
return true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`Failed to send message to client ${clientId}:`, error);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Broadcast message to all clients
|
|
101
|
+
*/
|
|
102
|
+
broadcast(message: HostToClientMessage, excludeClientId?: string): void {
|
|
103
|
+
for (const client of this.clients.values()) {
|
|
104
|
+
if (client.id === excludeClientId) continue;
|
|
105
|
+
this.sendToClient(client.id, message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get total number of connected clients
|
|
111
|
+
*/
|
|
112
|
+
get clientCount(): number {
|
|
113
|
+
return this.clients.size;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ArcContextAny,
|
|
3
|
+
type ArcEventAny,
|
|
4
|
+
AuthAdapter,
|
|
5
|
+
type DatabaseAdapter,
|
|
6
|
+
LocalEventPublisher,
|
|
7
|
+
MasterDataStorage,
|
|
8
|
+
Model,
|
|
9
|
+
mutationExecutor,
|
|
10
|
+
} from "@arcote.tech/arc";
|
|
11
|
+
import { canTokenEmitEvent, filterEventsForToken } from "./event-auth";
|
|
12
|
+
import type { SyncableEvent, TokenPayload } from "./types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handles a single context on the host
|
|
16
|
+
*/
|
|
17
|
+
export class ContextHandler {
|
|
18
|
+
private model: Model<ArcContextAny>;
|
|
19
|
+
private dataStorage: MasterDataStorage;
|
|
20
|
+
private eventPublisher: LocalEventPublisher;
|
|
21
|
+
private authAdapter: AuthAdapter;
|
|
22
|
+
private eventDefinitions = new Map<string, ArcEventAny>();
|
|
23
|
+
private hostEventIdCounter = 0;
|
|
24
|
+
private initialized = false;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
public readonly context: ArcContextAny,
|
|
28
|
+
dbAdapter: Promise<DatabaseAdapter>,
|
|
29
|
+
) {
|
|
30
|
+
this.dataStorage = new MasterDataStorage(dbAdapter);
|
|
31
|
+
this.eventPublisher = new LocalEventPublisher(this.dataStorage);
|
|
32
|
+
this.authAdapter = new AuthAdapter();
|
|
33
|
+
|
|
34
|
+
this.model = new Model(context, {
|
|
35
|
+
adapters: {
|
|
36
|
+
dataStorage: this.dataStorage,
|
|
37
|
+
eventPublisher: this.eventPublisher,
|
|
38
|
+
authAdapter: this.authAdapter,
|
|
39
|
+
},
|
|
40
|
+
environment: "server",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Build event definitions map
|
|
44
|
+
for (const element of context.elements) {
|
|
45
|
+
if ("tagFields" in element && "payload" in element) {
|
|
46
|
+
this.eventDefinitions.set(element.name, element as ArcEventAny);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the context handler
|
|
53
|
+
*/
|
|
54
|
+
async init(): Promise<void> {
|
|
55
|
+
if (this.initialized) return;
|
|
56
|
+
await this.model.init();
|
|
57
|
+
this.initialized = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute a command
|
|
62
|
+
*/
|
|
63
|
+
async executeCommand(
|
|
64
|
+
commandName: string,
|
|
65
|
+
params: any,
|
|
66
|
+
rawToken: string | null,
|
|
67
|
+
): Promise<any> {
|
|
68
|
+
const mutations = mutationExecutor(this.model);
|
|
69
|
+
const command = (mutations as any)[commandName];
|
|
70
|
+
|
|
71
|
+
if (!command) {
|
|
72
|
+
throw new Error(`Command '${commandName}' not found`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Set auth token on adapter so commands can access $auth.params
|
|
76
|
+
// AuthAdapter decodes the JWT internally
|
|
77
|
+
this.authAdapter.setToken(rawToken);
|
|
78
|
+
|
|
79
|
+
// TODO: Check command protection with token
|
|
80
|
+
|
|
81
|
+
return await command(params);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Persist events from a client and return with host IDs
|
|
86
|
+
*/
|
|
87
|
+
async persistEvents(
|
|
88
|
+
events: Array<{
|
|
89
|
+
localId: string;
|
|
90
|
+
type: string;
|
|
91
|
+
payload: any;
|
|
92
|
+
createdAt: string;
|
|
93
|
+
}>,
|
|
94
|
+
clientId: string,
|
|
95
|
+
token: TokenPayload | null,
|
|
96
|
+
): Promise<SyncableEvent[]> {
|
|
97
|
+
const persistedEvents: SyncableEvent[] = [];
|
|
98
|
+
|
|
99
|
+
for (const event of events) {
|
|
100
|
+
// Get event definition
|
|
101
|
+
const eventDef = this.eventDefinitions.get(event.type);
|
|
102
|
+
if (!eventDef) {
|
|
103
|
+
console.warn(`Unknown event type: ${event.type}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if token can emit this event
|
|
108
|
+
if (!canTokenEmitEvent(token, eventDef, event.payload)) {
|
|
109
|
+
console.warn(`Token not authorized to emit event: ${event.type}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Generate host ID
|
|
114
|
+
const hostId = `host_${++this.hostEventIdCounter}_${Date.now()}`;
|
|
115
|
+
|
|
116
|
+
const syncableEvent: SyncableEvent = {
|
|
117
|
+
localId: event.localId,
|
|
118
|
+
hostId,
|
|
119
|
+
type: event.type,
|
|
120
|
+
payload: event.payload,
|
|
121
|
+
createdAt: event.createdAt,
|
|
122
|
+
clientId,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Persist to database via event publisher
|
|
126
|
+
await this.eventPublisher.publish({
|
|
127
|
+
id: hostId,
|
|
128
|
+
type: event.type,
|
|
129
|
+
payload: event.payload,
|
|
130
|
+
createdAt: new Date(event.createdAt),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
persistedEvents.push(syncableEvent);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return persistedEvents;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get events after a specific host ID, filtered by token
|
|
141
|
+
*/
|
|
142
|
+
async getEventsSince(
|
|
143
|
+
lastHostEventId: string | null,
|
|
144
|
+
token: TokenPayload | null,
|
|
145
|
+
): Promise<SyncableEvent[]> {
|
|
146
|
+
// Query events from database
|
|
147
|
+
const eventsStore = this.dataStorage.getStore<any>("events");
|
|
148
|
+
const allEvents: any[] = await eventsStore.find({});
|
|
149
|
+
|
|
150
|
+
let filteredEvents: any[];
|
|
151
|
+
if (lastHostEventId) {
|
|
152
|
+
// Extract timestamp from lastHostEventId (format: host_{counter}_{timestamp})
|
|
153
|
+
const parts = lastHostEventId.split("_");
|
|
154
|
+
const lastTimestamp = parts.length >= 3 ? parseInt(parts[2], 10) : 0;
|
|
155
|
+
|
|
156
|
+
// Filter events with timestamp > lastTimestamp
|
|
157
|
+
filteredEvents = allEvents.filter((e) => {
|
|
158
|
+
if (!e._id.startsWith("host_")) return false;
|
|
159
|
+
const eventParts = e._id.split("_");
|
|
160
|
+
const eventTimestamp =
|
|
161
|
+
eventParts.length >= 3 ? parseInt(eventParts[2], 10) : 0;
|
|
162
|
+
return eventTimestamp > lastTimestamp;
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
// Return all host events
|
|
166
|
+
filteredEvents = allEvents.filter((e) => e._id.startsWith("host_"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Sort by timestamp to ensure correct order
|
|
170
|
+
filteredEvents.sort((a, b) => {
|
|
171
|
+
const aTimestamp = parseInt(a._id.split("_")[2] || "0", 10);
|
|
172
|
+
const bTimestamp = parseInt(b._id.split("_")[2] || "0", 10);
|
|
173
|
+
return aTimestamp - bTimestamp;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Convert to SyncableEvent format
|
|
177
|
+
const syncableEvents: SyncableEvent[] = filteredEvents.map((e) => ({
|
|
178
|
+
localId: e._id,
|
|
179
|
+
hostId: e._id,
|
|
180
|
+
type: e.type,
|
|
181
|
+
payload:
|
|
182
|
+
typeof e.payload === "string" ? JSON.parse(e.payload) : e.payload,
|
|
183
|
+
createdAt: e.createdAt,
|
|
184
|
+
clientId: "host",
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
// Filter by token authorization
|
|
188
|
+
return filterEventsForToken(token, syncableEvents, this.eventDefinitions);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the model for advanced operations
|
|
193
|
+
*/
|
|
194
|
+
getModel(): Model<ArcContextAny> {
|
|
195
|
+
return this.model;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get data storage for queries
|
|
200
|
+
*/
|
|
201
|
+
getDataStorage(): MasterDataStorage {
|
|
202
|
+
return this.dataStorage;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get event definitions map
|
|
207
|
+
*/
|
|
208
|
+
getEventDefinitions(): Map<string, ArcEventAny> {
|
|
209
|
+
return this.eventDefinitions;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Set auth token on the adapter
|
|
214
|
+
* Used to apply view protection for queries
|
|
215
|
+
*/
|
|
216
|
+
setAuthToken(rawToken: string | null): void {
|
|
217
|
+
this.authAdapter.setToken(rawToken);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ArcEventAny } from "@arcote.tech/arc";
|
|
2
|
+
import type { SyncableEvent, TokenPayload } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a token can receive an event based on explicit protectBy rules
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - Events WITHOUT protections: public, anyone can receive
|
|
9
|
+
* - Events WITH protections: MUST have matching read rule for token
|
|
10
|
+
*/
|
|
11
|
+
export function canTokenReceiveEvent(
|
|
12
|
+
token: TokenPayload | null,
|
|
13
|
+
event: ArcEventAny,
|
|
14
|
+
eventInstance: SyncableEvent,
|
|
15
|
+
): boolean {
|
|
16
|
+
// If event has no protections, it's public - allow everyone
|
|
17
|
+
if (!event.hasProtections) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Event has protections - require valid token
|
|
22
|
+
if (!token) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check event protections for matching read rule
|
|
27
|
+
for (const protection of event.protections) {
|
|
28
|
+
// Check if this protection applies to the token type
|
|
29
|
+
if (protection.token.name !== token.tokenType) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get protection rules for this token's params
|
|
34
|
+
const rules = protection.protectionFn(token.params);
|
|
35
|
+
|
|
36
|
+
// If read is explicitly false, deny access
|
|
37
|
+
if (rules.read === false) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check read conditions against event payload
|
|
42
|
+
if (rules.read) {
|
|
43
|
+
const matches = checkConditionsMatch(rules.read, eventInstance.payload);
|
|
44
|
+
if (matches) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// No matching protection rule found - deny access
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a token can emit an event
|
|
56
|
+
*/
|
|
57
|
+
export function canTokenEmitEvent(
|
|
58
|
+
token: TokenPayload | null,
|
|
59
|
+
event: ArcEventAny,
|
|
60
|
+
payload: any,
|
|
61
|
+
): boolean {
|
|
62
|
+
// If no token, only allow unprotected events
|
|
63
|
+
if (!token) {
|
|
64
|
+
return !event.hasProtections;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If event has no protections, allow all authenticated users
|
|
68
|
+
if (!event.hasProtections) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check event protections
|
|
73
|
+
for (const protection of event.protections) {
|
|
74
|
+
// Check if this protection applies to the token type
|
|
75
|
+
if (protection.token.name !== token.tokenType) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get protection rules for this token's params
|
|
80
|
+
const rules = protection.protectionFn(token.params);
|
|
81
|
+
|
|
82
|
+
// Check write rules against payload
|
|
83
|
+
if (rules.write) {
|
|
84
|
+
const matches = checkConditionsMatch(rules.write, payload);
|
|
85
|
+
if (matches) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if conditions match against data
|
|
96
|
+
*/
|
|
97
|
+
function checkConditionsMatch(
|
|
98
|
+
conditions: Record<string, any>,
|
|
99
|
+
data: Record<string, any>,
|
|
100
|
+
): boolean {
|
|
101
|
+
for (const [key, expectedValue] of Object.entries(conditions)) {
|
|
102
|
+
const actualValue = data[key];
|
|
103
|
+
|
|
104
|
+
if (actualValue !== expectedValue) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Filter events that a token can receive
|
|
113
|
+
*/
|
|
114
|
+
export function filterEventsForToken(
|
|
115
|
+
token: TokenPayload | null,
|
|
116
|
+
events: SyncableEvent[],
|
|
117
|
+
eventDefinitions: Map<string, ArcEventAny>,
|
|
118
|
+
): SyncableEvent[] {
|
|
119
|
+
return events.filter((eventInstance) => {
|
|
120
|
+
const eventDef = eventDefinitions.get(eventInstance.type);
|
|
121
|
+
if (!eventDef) {
|
|
122
|
+
// Unknown event type, don't send
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return canTokenReceiveEvent(token, eventDef, eventInstance);
|
|
126
|
+
});
|
|
127
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { ArcHost } from "./arc-host";
|
|
3
|
+
export { ConnectionManager } from "./connection-manager";
|
|
4
|
+
export { ContextHandler } from "./context-handler";
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
export type {
|
|
8
|
+
ArcHostConfig,
|
|
9
|
+
ClientToHostMessage,
|
|
10
|
+
ConnectedClient,
|
|
11
|
+
HostToClientMessage,
|
|
12
|
+
SyncableEvent,
|
|
13
|
+
TokenPayload,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
// Auth utilities
|
|
17
|
+
export {
|
|
18
|
+
canTokenEmitEvent,
|
|
19
|
+
canTokenReceiveEvent,
|
|
20
|
+
filterEventsForToken,
|
|
21
|
+
} from "./event-auth";
|
|
22
|
+
|
|
23
|
+
// Re-export adapters
|
|
24
|
+
export { sqliteAdapterFactory } from "../sqliteAdapter";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ArcContextAny, DBAdapterFactory } from "@arcote.tech/arc";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host configuration options
|
|
5
|
+
*/
|
|
6
|
+
export interface ArcHostConfig {
|
|
7
|
+
/** Context to serve */
|
|
8
|
+
context: ArcContextAny;
|
|
9
|
+
/** Database adapter factory */
|
|
10
|
+
dbAdapterFactory: DBAdapterFactory;
|
|
11
|
+
/** Server port */
|
|
12
|
+
port?: number;
|
|
13
|
+
/** JWT secret for token verification */
|
|
14
|
+
jwtSecret?: string;
|
|
15
|
+
/** JWT expiration time */
|
|
16
|
+
jwtExpiresIn?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Token payload from JWT
|
|
21
|
+
*/
|
|
22
|
+
export interface TokenPayload {
|
|
23
|
+
/** Token type name (e.g., "user") */
|
|
24
|
+
tokenType: string;
|
|
25
|
+
/** Token params (e.g., { userId: "123" }) */
|
|
26
|
+
params: Record<string, any>;
|
|
27
|
+
/** Issued at timestamp */
|
|
28
|
+
iat?: number;
|
|
29
|
+
/** Expiration timestamp */
|
|
30
|
+
exp?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Connected client info
|
|
35
|
+
*/
|
|
36
|
+
export interface ConnectedClient {
|
|
37
|
+
/** Unique client ID */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Token payload (decoded) */
|
|
40
|
+
token: TokenPayload | null;
|
|
41
|
+
/** Raw JWT token string */
|
|
42
|
+
rawToken: string | null;
|
|
43
|
+
/** Last synced host event ID */
|
|
44
|
+
lastHostEventId: string | null;
|
|
45
|
+
/** WebSocket instance */
|
|
46
|
+
ws: any;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Event with sync IDs
|
|
51
|
+
*/
|
|
52
|
+
export interface SyncableEvent {
|
|
53
|
+
/** Local ID generated by client */
|
|
54
|
+
localId: string;
|
|
55
|
+
/** Host ID assigned when persisted */
|
|
56
|
+
hostId: string;
|
|
57
|
+
/** Event type */
|
|
58
|
+
type: string;
|
|
59
|
+
/** Event payload */
|
|
60
|
+
payload: any;
|
|
61
|
+
/** Creation timestamp */
|
|
62
|
+
createdAt: string;
|
|
63
|
+
/** Client ID that created this event */
|
|
64
|
+
clientId: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Messages from client to host
|
|
69
|
+
*/
|
|
70
|
+
export type ClientToHostMessage =
|
|
71
|
+
| {
|
|
72
|
+
type: "sync-events";
|
|
73
|
+
events: Array<{
|
|
74
|
+
localId: string;
|
|
75
|
+
type: string;
|
|
76
|
+
payload: any;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
type: "request-sync";
|
|
82
|
+
lastHostEventId: string | null;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
type: "execute-command";
|
|
86
|
+
commandName: string;
|
|
87
|
+
params: any;
|
|
88
|
+
requestId: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Messages from host to client
|
|
93
|
+
*/
|
|
94
|
+
export type HostToClientMessage =
|
|
95
|
+
| {
|
|
96
|
+
type: "events";
|
|
97
|
+
events: SyncableEvent[];
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
type: "command-result";
|
|
101
|
+
requestId: string;
|
|
102
|
+
result?: any;
|
|
103
|
+
error?: string;
|
|
104
|
+
}
|
|
105
|
+
| {
|
|
106
|
+
type: "sync-complete";
|
|
107
|
+
lastHostEventId: string;
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
type: "error";
|
|
111
|
+
message: string;
|
|
112
|
+
};
|
package/dist/host.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { type ArcContextAny, type DatabaseAdapter, type DataStorageChanges, type RealTimeCommunicationAdapter } from "@arcote.tech/arc";
|
|
2
|
-
declare class RTCHost implements RealTimeCommunicationAdapter {
|
|
3
|
-
private context;
|
|
4
|
-
private server;
|
|
5
|
-
private dataStore;
|
|
6
|
-
private model;
|
|
7
|
-
constructor(context: ArcContextAny, dbAdapter: Promise<DatabaseAdapter>);
|
|
8
|
-
commitChanges(changes: DataStorageChanges[]): void;
|
|
9
|
-
sync(progressCallback: ({ store, size, }: {
|
|
10
|
-
store: string;
|
|
11
|
-
size: number;
|
|
12
|
-
}) => void): Promise<void>;
|
|
13
|
-
private verifyAuthToken;
|
|
14
|
-
/**
|
|
15
|
-
* Convert JWT payload to AuthContext
|
|
16
|
-
*/
|
|
17
|
-
private tokenToAuthContext;
|
|
18
|
-
/**
|
|
19
|
-
* Extract client IP address from request headers
|
|
20
|
-
*/
|
|
21
|
-
private getClientIpAddress;
|
|
22
|
-
/**
|
|
23
|
-
* Get default auth context for anonymous users
|
|
24
|
-
*/
|
|
25
|
-
private getDefaultAuthContext;
|
|
26
|
-
/**
|
|
27
|
-
* Parse FormData back to an object structure
|
|
28
|
-
*/
|
|
29
|
-
private parseFormDataToObject;
|
|
30
|
-
/**
|
|
31
|
-
* Helper method to set nested properties from FormData keys like "user[profile][avatar]"
|
|
32
|
-
*/
|
|
33
|
-
private setNestedProperty;
|
|
34
|
-
private handleCommand;
|
|
35
|
-
private handleQuery;
|
|
36
|
-
private handleRoute;
|
|
37
|
-
private setupServer;
|
|
38
|
-
private isPublicEndpoint;
|
|
39
|
-
private handleSync;
|
|
40
|
-
private onMessage;
|
|
41
|
-
private publishMessage;
|
|
42
|
-
}
|
|
43
|
-
export declare const rtcHostFactory: (context: ArcContextAny, dbAdapter: Promise<DatabaseAdapter>) => () => RTCHost;
|
|
44
|
-
export {};
|
|
45
|
-
//# sourceMappingURL=host.d.ts.map
|
package/dist/host.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../host.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAGvB,KAAK,4BAA4B,EAClC,MAAM,kBAAkB,CAAC;AAI1B,cAAM,OAAQ,YAAW,4BAA4B;IAMjD,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,KAAK,CAAuB;gBAG1B,OAAO,EAAE,aAAa,EAC9B,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC;IASrC,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAI5C,IAAI,CACR,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,IAAI,GACL,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,IAAI,GACT,OAAO,CAAC,IAAI,CAAC;YAIF,eAAe;IAS7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;OAEG;YACW,qBAAqB;IAWnC;;OAEG;IACH,OAAO,CAAC,iBAAiB;YAwCX,aAAa;YAoDb,WAAW;YAiEX,WAAW;IAgEzB,OAAO,CAAC,WAAW;IAoEnB,OAAO,CAAC,gBAAgB;YA4BV,UAAU;YA+CV,SAAS;IAevB,OAAO,CAAC,cAAc;CAGvB;AAED,eAAO,MAAM,cAAc,YACf,aAAa,aAAa,OAAO,CAAC,eAAe,CAAC,kBAE3D,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postgresAdapter.d.ts","sourceRoot":"","sources":["../postgresAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,kBAAkB,CAAC;AAqC1B,eAAO,MAAM,wBAAwB,qBACjB,MAAM,KACvB,gBAKF,CAAC"}
|