@plures/praxis 0.2.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/FRAMEWORK.md +420 -0
- package/LICENSE +21 -0
- package/README.md +1310 -0
- package/dist/adapters/cli.d.ts +43 -0
- package/dist/adapters/cli.d.ts.map +1 -0
- package/dist/adapters/cli.js +126 -0
- package/dist/adapters/cli.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +26 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +233 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/cloud.d.ts +27 -0
- package/dist/cli/commands/cloud.d.ts.map +1 -0
- package/dist/cli/commands/cloud.js +232 -0
- package/dist/cli/commands/cloud.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +25 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +168 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +179 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cloud/auth.d.ts +51 -0
- package/dist/cloud/auth.d.ts.map +1 -0
- package/dist/cloud/auth.js +194 -0
- package/dist/cloud/auth.js.map +1 -0
- package/dist/cloud/billing.d.ts +184 -0
- package/dist/cloud/billing.d.ts.map +1 -0
- package/dist/cloud/billing.js +179 -0
- package/dist/cloud/billing.js.map +1 -0
- package/dist/cloud/client.d.ts +39 -0
- package/dist/cloud/client.d.ts.map +1 -0
- package/dist/cloud/client.js +176 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/cloud/index.d.ts +44 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +44 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/marketplace.d.ts +166 -0
- package/dist/cloud/marketplace.d.ts.map +1 -0
- package/dist/cloud/marketplace.js +159 -0
- package/dist/cloud/marketplace.js.map +1 -0
- package/dist/cloud/provisioning.d.ts +110 -0
- package/dist/cloud/provisioning.d.ts.map +1 -0
- package/dist/cloud/provisioning.js +148 -0
- package/dist/cloud/provisioning.js.map +1 -0
- package/dist/cloud/relay/endpoints.d.ts +62 -0
- package/dist/cloud/relay/endpoints.d.ts.map +1 -0
- package/dist/cloud/relay/endpoints.js +217 -0
- package/dist/cloud/relay/endpoints.js.map +1 -0
- package/dist/cloud/relay/health/index.d.ts +5 -0
- package/dist/cloud/relay/health/index.d.ts.map +1 -0
- package/dist/cloud/relay/health/index.js +9 -0
- package/dist/cloud/relay/health/index.js.map +1 -0
- package/dist/cloud/relay/stats/index.d.ts +5 -0
- package/dist/cloud/relay/stats/index.d.ts.map +1 -0
- package/dist/cloud/relay/stats/index.js +9 -0
- package/dist/cloud/relay/stats/index.js.map +1 -0
- package/dist/cloud/relay/sync/index.d.ts +5 -0
- package/dist/cloud/relay/sync/index.d.ts.map +1 -0
- package/dist/cloud/relay/sync/index.js +9 -0
- package/dist/cloud/relay/sync/index.js.map +1 -0
- package/dist/cloud/relay/usage/index.d.ts +5 -0
- package/dist/cloud/relay/usage/index.d.ts.map +1 -0
- package/dist/cloud/relay/usage/index.js +9 -0
- package/dist/cloud/relay/usage/index.js.map +1 -0
- package/dist/cloud/sponsors.d.ts +81 -0
- package/dist/cloud/sponsors.d.ts.map +1 -0
- package/dist/cloud/sponsors.js +130 -0
- package/dist/cloud/sponsors.js.map +1 -0
- package/dist/cloud/types.d.ts +169 -0
- package/dist/cloud/types.d.ts.map +1 -0
- package/dist/cloud/types.js +7 -0
- package/dist/cloud/types.js.map +1 -0
- package/dist/components/index.d.ts +43 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +17 -0
- package/dist/components/index.js.map +1 -0
- package/dist/core/actors.d.ts +95 -0
- package/dist/core/actors.d.ts.map +1 -0
- package/dist/core/actors.js +158 -0
- package/dist/core/actors.js.map +1 -0
- package/dist/core/component/generator.d.ts +122 -0
- package/dist/core/component/generator.d.ts.map +1 -0
- package/dist/core/component/generator.js +307 -0
- package/dist/core/component/generator.js.map +1 -0
- package/dist/core/engine.d.ts +92 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +199 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/introspection.d.ts +141 -0
- package/dist/core/introspection.d.ts.map +1 -0
- package/dist/core/introspection.js +208 -0
- package/dist/core/introspection.js.map +1 -0
- package/dist/core/logic/generator.d.ts +76 -0
- package/dist/core/logic/generator.d.ts.map +1 -0
- package/dist/core/logic/generator.js +339 -0
- package/dist/core/logic/generator.js.map +1 -0
- package/dist/core/pluresdb/generator.d.ts +58 -0
- package/dist/core/pluresdb/generator.d.ts.map +1 -0
- package/dist/core/pluresdb/generator.js +162 -0
- package/dist/core/pluresdb/generator.js.map +1 -0
- package/dist/core/protocol.d.ts +121 -0
- package/dist/core/protocol.d.ts.map +1 -0
- package/dist/core/protocol.js +46 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/rules.d.ts +120 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +81 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/schema/loader.d.ts +47 -0
- package/dist/core/schema/loader.d.ts.map +1 -0
- package/dist/core/schema/loader.js +189 -0
- package/dist/core/schema/loader.js.map +1 -0
- package/dist/core/schema/normalize.d.ts +72 -0
- package/dist/core/schema/normalize.d.ts.map +1 -0
- package/dist/core/schema/normalize.js +190 -0
- package/dist/core/schema/normalize.js.map +1 -0
- package/dist/core/schema/types.d.ts +370 -0
- package/dist/core/schema/types.d.ts.map +1 -0
- package/dist/core/schema/types.js +161 -0
- package/dist/core/schema/types.js.map +1 -0
- package/dist/dsl/index.d.ts +152 -0
- package/dist/dsl/index.d.ts.map +1 -0
- package/dist/dsl/index.js +132 -0
- package/dist/dsl/index.js.map +1 -0
- package/dist/dsl.d.ts +124 -0
- package/dist/dsl.d.ts.map +1 -0
- package/dist/dsl.js +130 -0
- package/dist/dsl.js.map +1 -0
- package/dist/examples/advanced-todo/index.d.ts +55 -0
- package/dist/examples/advanced-todo/index.d.ts.map +1 -0
- package/dist/examples/advanced-todo/index.js +222 -0
- package/dist/examples/advanced-todo/index.js.map +1 -0
- package/dist/examples/auth-basic/index.d.ts +17 -0
- package/dist/examples/auth-basic/index.d.ts.map +1 -0
- package/dist/examples/auth-basic/index.js +122 -0
- package/dist/examples/auth-basic/index.js.map +1 -0
- package/dist/examples/cart/index.d.ts +19 -0
- package/dist/examples/cart/index.d.ts.map +1 -0
- package/dist/examples/cart/index.js +202 -0
- package/dist/examples/cart/index.js.map +1 -0
- package/dist/examples/hero-ecommerce/index.d.ts +39 -0
- package/dist/examples/hero-ecommerce/index.d.ts.map +1 -0
- package/dist/examples/hero-ecommerce/index.js +506 -0
- package/dist/examples/hero-ecommerce/index.js.map +1 -0
- package/dist/examples/svelte-counter/index.d.ts +31 -0
- package/dist/examples/svelte-counter/index.d.ts.map +1 -0
- package/dist/examples/svelte-counter/index.js +123 -0
- package/dist/examples/svelte-counter/index.js.map +1 -0
- package/dist/flows.d.ts +125 -0
- package/dist/flows.d.ts.map +1 -0
- package/dist/flows.js +160 -0
- package/dist/flows.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/pluresdb.d.ts +56 -0
- package/dist/integrations/pluresdb.d.ts.map +1 -0
- package/dist/integrations/pluresdb.js +46 -0
- package/dist/integrations/pluresdb.js.map +1 -0
- package/dist/integrations/svelte.d.ts +306 -0
- package/dist/integrations/svelte.d.ts.map +1 -0
- package/dist/integrations/svelte.js +447 -0
- package/dist/integrations/svelte.js.map +1 -0
- package/dist/registry.d.ts +94 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +181 -0
- package/dist/registry.js.map +1 -0
- package/dist/runtime/terminal-adapter.d.ts +105 -0
- package/dist/runtime/terminal-adapter.d.ts.map +1 -0
- package/dist/runtime/terminal-adapter.js +113 -0
- package/dist/runtime/terminal-adapter.js.map +1 -0
- package/dist/step.d.ts +34 -0
- package/dist/step.d.ts.map +1 -0
- package/dist/step.js +111 -0
- package/dist/step.js.map +1 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/MONETIZATION.md +394 -0
- package/docs/TERMINAL_NODE.md +588 -0
- package/docs/guides/canvas.md +389 -0
- package/docs/guides/getting-started.md +347 -0
- package/docs/guides/history-state-pattern.md +618 -0
- package/docs/guides/orchestration.md +617 -0
- package/docs/guides/parallel-state-pattern.md +767 -0
- package/docs/guides/svelte-integration.md +691 -0
- package/package.json +96 -0
- package/src/__tests__/actors.test.ts +270 -0
- package/src/__tests__/billing.test.ts +175 -0
- package/src/__tests__/cloud.test.ts +247 -0
- package/src/__tests__/dsl.test.ts +154 -0
- package/src/__tests__/edge-cases.test.ts +475 -0
- package/src/__tests__/engine.test.ts +137 -0
- package/src/__tests__/generators.test.ts +270 -0
- package/src/__tests__/introspection.test.ts +321 -0
- package/src/__tests__/protocol.test.ts +40 -0
- package/src/__tests__/provisioning.test.ts +162 -0
- package/src/__tests__/schema.test.ts +241 -0
- package/src/__tests__/svelte-integration.test.ts +431 -0
- package/src/__tests__/terminal-node.test.ts +352 -0
- package/src/adapters/cli.ts +175 -0
- package/src/cli/commands/auth.ts +271 -0
- package/src/cli/commands/cloud.ts +281 -0
- package/src/cli/commands/generate.ts +225 -0
- package/src/cli/index.ts +190 -0
- package/src/cloud/README.md +383 -0
- package/src/cloud/auth.ts +245 -0
- package/src/cloud/billing.ts +336 -0
- package/src/cloud/client.ts +221 -0
- package/src/cloud/index.ts +121 -0
- package/src/cloud/marketplace.ts +303 -0
- package/src/cloud/provisioning.ts +254 -0
- package/src/cloud/relay/endpoints.ts +307 -0
- package/src/cloud/relay/health/function.json +17 -0
- package/src/cloud/relay/health/index.ts +10 -0
- package/src/cloud/relay/host.json +15 -0
- package/src/cloud/relay/local.settings.json +8 -0
- package/src/cloud/relay/stats/function.json +17 -0
- package/src/cloud/relay/stats/index.ts +10 -0
- package/src/cloud/relay/sync/function.json +17 -0
- package/src/cloud/relay/sync/index.ts +10 -0
- package/src/cloud/relay/usage/function.json +17 -0
- package/src/cloud/relay/usage/index.ts +10 -0
- package/src/cloud/sponsors.ts +213 -0
- package/src/cloud/types.ts +198 -0
- package/src/components/README.md +125 -0
- package/src/components/TerminalNode.svelte +457 -0
- package/src/components/index.ts +46 -0
- package/src/core/actors.ts +205 -0
- package/src/core/component/generator.ts +432 -0
- package/src/core/engine.ts +243 -0
- package/src/core/introspection.ts +329 -0
- package/src/core/logic/generator.ts +420 -0
- package/src/core/pluresdb/generator.ts +229 -0
- package/src/core/protocol.ts +132 -0
- package/src/core/rules.ts +167 -0
- package/src/core/schema/loader.ts +247 -0
- package/src/core/schema/normalize.ts +322 -0
- package/src/core/schema/types.ts +557 -0
- package/src/dsl/index.ts +218 -0
- package/src/dsl.ts +214 -0
- package/src/examples/advanced-todo/App.svelte +506 -0
- package/src/examples/advanced-todo/README.md +371 -0
- package/src/examples/advanced-todo/index.ts +309 -0
- package/src/examples/auth-basic/index.ts +163 -0
- package/src/examples/cart/index.ts +259 -0
- package/src/examples/hero-ecommerce/index.ts +657 -0
- package/src/examples/svelte-counter/index.ts +168 -0
- package/src/flows.ts +268 -0
- package/src/index.ts +154 -0
- package/src/integrations/pluresdb.ts +93 -0
- package/src/integrations/svelte.ts +617 -0
- package/src/registry.ts +223 -0
- package/src/runtime/terminal-adapter.ts +175 -0
- package/src/step.ts +151 -0
- package/src/types.ts +70 -0
- package/templates/basic-app/README.md +147 -0
- package/templates/fullstack-app/README.md +279 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Praxis Cloud Relay
|
|
2
|
+
|
|
3
|
+
Azure-hosted relay service for Praxis Cloud monetization (Tier 1).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **PluresDB CRDT Sync** - Synchronize facts and events across clients
|
|
8
|
+
- **Event Forwarding** - Publish events to Azure Event Grid / Service Bus
|
|
9
|
+
- **Schema Registry** - Store and retrieve application schemas
|
|
10
|
+
- **Encrypted Blob Storage** - Secure storage for generated docs and components
|
|
11
|
+
- **GitHub OAuth** - Identity provider integration
|
|
12
|
+
- **Usage Metering** - Track usage for billing purposes
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Setup Cloud Connection
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install Praxis
|
|
20
|
+
npm install @plures/praxis
|
|
21
|
+
|
|
22
|
+
# Initialize cloud connection
|
|
23
|
+
npx praxis cloud init
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The wizard will:
|
|
27
|
+
1. Authenticate with GitHub
|
|
28
|
+
2. Configure the Azure endpoint
|
|
29
|
+
3. Test the connection
|
|
30
|
+
4. Save configuration to `.praxis-cloud.json`
|
|
31
|
+
|
|
32
|
+
### 2. Use in Your Application
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { connectRelay } from "@plures/praxis/cloud";
|
|
36
|
+
|
|
37
|
+
// Connect to cloud relay
|
|
38
|
+
const relay = await connectRelay("https://praxis-relay.azurewebsites.net", {
|
|
39
|
+
appId: "my-app",
|
|
40
|
+
authToken: "your-github-token",
|
|
41
|
+
autoSync: true,
|
|
42
|
+
syncInterval: 5000
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Sync data
|
|
46
|
+
await relay.sync({
|
|
47
|
+
type: "delta",
|
|
48
|
+
appId: "my-app",
|
|
49
|
+
clock: {},
|
|
50
|
+
facts: [{ tag: "TaskCreated", payload: { id: "1", title: "Buy milk" } }],
|
|
51
|
+
events: [],
|
|
52
|
+
timestamp: Date.now()
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Get usage metrics
|
|
56
|
+
const usage = await relay.getUsage();
|
|
57
|
+
console.log(`Syncs: ${usage.syncCount}, Events: ${usage.eventCount}`);
|
|
58
|
+
|
|
59
|
+
// Check health
|
|
60
|
+
const health = await relay.getHealth();
|
|
61
|
+
console.log(`Status: ${health.status}`);
|
|
62
|
+
|
|
63
|
+
// Disconnect
|
|
64
|
+
await relay.disconnect();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## CLI Commands
|
|
68
|
+
|
|
69
|
+
### `praxis cloud init`
|
|
70
|
+
|
|
71
|
+
Setup wizard to connect your app to Praxis Cloud.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx praxis cloud init --endpoint https://praxis-relay.azurewebsites.net --app-id my-app
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
- `-e, --endpoint <url>` - Azure Function App endpoint URL
|
|
79
|
+
- `-a, --app-id <id>` - Application identifier
|
|
80
|
+
- `--auto-sync` - Enable automatic synchronization
|
|
81
|
+
- `--interval <ms>` - Sync interval in milliseconds
|
|
82
|
+
|
|
83
|
+
### `praxis cloud status`
|
|
84
|
+
|
|
85
|
+
Check connection status and service health.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx praxis cloud status
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `praxis cloud sync`
|
|
92
|
+
|
|
93
|
+
Manually trigger synchronization.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx praxis cloud sync
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `praxis cloud usage`
|
|
100
|
+
|
|
101
|
+
View usage metrics for billing.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx praxis cloud usage
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## API Reference
|
|
108
|
+
|
|
109
|
+
### `connectRelay(endpoint, options)`
|
|
110
|
+
|
|
111
|
+
Connect to Praxis Cloud Relay.
|
|
112
|
+
|
|
113
|
+
**Parameters:**
|
|
114
|
+
- `endpoint: string` - Azure Function App endpoint URL
|
|
115
|
+
- `options: CloudRelayConfig` - Configuration options
|
|
116
|
+
- `appId: string` - Application identifier (required)
|
|
117
|
+
- `authToken?: string` - GitHub OAuth token
|
|
118
|
+
- `autoSync?: boolean` - Enable automatic sync
|
|
119
|
+
- `syncInterval?: number` - Sync interval in ms (default: 5000)
|
|
120
|
+
- `encryption?: boolean` - Enable encryption for blob storage
|
|
121
|
+
|
|
122
|
+
**Returns:** `Promise<CloudRelayClient>`
|
|
123
|
+
|
|
124
|
+
### `CloudRelayClient`
|
|
125
|
+
|
|
126
|
+
#### `connect(): Promise<void>`
|
|
127
|
+
|
|
128
|
+
Connect to the relay service.
|
|
129
|
+
|
|
130
|
+
#### `disconnect(): Promise<void>`
|
|
131
|
+
|
|
132
|
+
Disconnect from the relay service.
|
|
133
|
+
|
|
134
|
+
#### `sync(message: CRDTSyncMessage): Promise<void>`
|
|
135
|
+
|
|
136
|
+
Sync facts and events using CRDT protocol.
|
|
137
|
+
|
|
138
|
+
**Parameters:**
|
|
139
|
+
- `message.type` - "sync" | "delta" | "snapshot"
|
|
140
|
+
- `message.appId` - Application identifier
|
|
141
|
+
- `message.clock` - Vector clock for causality
|
|
142
|
+
- `message.facts?` - Facts to sync
|
|
143
|
+
- `message.events?` - Events to forward
|
|
144
|
+
- `message.timestamp` - Timestamp
|
|
145
|
+
|
|
146
|
+
#### `getUsage(): Promise<UsageMetrics>`
|
|
147
|
+
|
|
148
|
+
Get usage metrics for the current billing period.
|
|
149
|
+
|
|
150
|
+
**Returns:**
|
|
151
|
+
- `syncCount` - Number of sync operations
|
|
152
|
+
- `eventCount` - Number of events forwarded
|
|
153
|
+
- `factCount` - Number of facts synced
|
|
154
|
+
- `storageBytes` - Storage used in bytes
|
|
155
|
+
- `periodStart` - Period start timestamp
|
|
156
|
+
- `periodEnd` - Period end timestamp
|
|
157
|
+
|
|
158
|
+
#### `getHealth(): Promise<HealthCheckResponse>`
|
|
159
|
+
|
|
160
|
+
Get service health status.
|
|
161
|
+
|
|
162
|
+
**Returns:**
|
|
163
|
+
- `status` - "healthy" | "degraded" | "unhealthy"
|
|
164
|
+
- `services` - Status of individual services
|
|
165
|
+
- `relay` - Relay service status
|
|
166
|
+
- `eventGrid` - Event Grid status
|
|
167
|
+
- `storage` - Blob storage status
|
|
168
|
+
- `auth` - Authentication status
|
|
169
|
+
|
|
170
|
+
#### `getStatus(): RelayStatus`
|
|
171
|
+
|
|
172
|
+
Get current connection status (synchronous).
|
|
173
|
+
|
|
174
|
+
**Returns:**
|
|
175
|
+
- `connected` - Connection state
|
|
176
|
+
- `lastSync` - Last sync timestamp
|
|
177
|
+
- `endpoint` - Endpoint URL
|
|
178
|
+
- `appId` - Application identifier
|
|
179
|
+
|
|
180
|
+
## Azure Deployment
|
|
181
|
+
|
|
182
|
+
### Prerequisites
|
|
183
|
+
|
|
184
|
+
- Azure account
|
|
185
|
+
- Azure CLI installed
|
|
186
|
+
- Azure Functions Core Tools
|
|
187
|
+
|
|
188
|
+
### Deploy to Azure
|
|
189
|
+
|
|
190
|
+
1. **Create Azure Function App:**
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
az functionapp create \
|
|
194
|
+
--name praxis-cloud-relay \
|
|
195
|
+
--resource-group praxis-rg \
|
|
196
|
+
--consumption-plan-location eastus \
|
|
197
|
+
--runtime node \
|
|
198
|
+
--runtime-version 18 \
|
|
199
|
+
--storage-account praxisstorage
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
2. **Deploy Functions:**
|
|
203
|
+
|
|
204
|
+
The GitHub Actions workflow (`.github/workflows/azure-functions.yml`) automatically deploys on push to `main`.
|
|
205
|
+
|
|
206
|
+
Or deploy manually:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cd src/cloud/relay
|
|
210
|
+
func azure functionapp publish praxis-cloud-relay
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Environment Variables
|
|
214
|
+
|
|
215
|
+
Configure in Azure Portal or via CLI:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
az functionapp config appsettings set \
|
|
219
|
+
--name praxis-cloud-relay \
|
|
220
|
+
--resource-group praxis-rg \
|
|
221
|
+
--settings \
|
|
222
|
+
"GITHUB_CLIENT_ID=your-client-id" \
|
|
223
|
+
"GITHUB_CLIENT_SECRET=your-client-secret" \
|
|
224
|
+
"AZURE_STORAGE_CONNECTION_STRING=your-connection-string"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Endpoints
|
|
228
|
+
|
|
229
|
+
The relay exposes the following HTTP endpoints:
|
|
230
|
+
|
|
231
|
+
### `GET /health`
|
|
232
|
+
|
|
233
|
+
Health check endpoint.
|
|
234
|
+
|
|
235
|
+
**Response:**
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"status": "healthy",
|
|
239
|
+
"timestamp": 1234567890,
|
|
240
|
+
"version": "0.1.0",
|
|
241
|
+
"services": {
|
|
242
|
+
"relay": true,
|
|
243
|
+
"eventGrid": true,
|
|
244
|
+
"storage": true,
|
|
245
|
+
"auth": true
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### `POST /sync`
|
|
251
|
+
|
|
252
|
+
CRDT synchronization endpoint.
|
|
253
|
+
|
|
254
|
+
**Request:**
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"type": "delta",
|
|
258
|
+
"appId": "my-app",
|
|
259
|
+
"clock": { "my-app": 5 },
|
|
260
|
+
"facts": [{ "tag": "TaskCreated", "payload": { "id": "1" } }],
|
|
261
|
+
"events": [],
|
|
262
|
+
"timestamp": 1234567890
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Response:**
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"success": true,
|
|
270
|
+
"clock": { "my-app": 5 },
|
|
271
|
+
"timestamp": 1234567890
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### `GET /usage?appId=<appId>`
|
|
276
|
+
|
|
277
|
+
Usage metrics endpoint.
|
|
278
|
+
|
|
279
|
+
**Response:**
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"appId": "my-app",
|
|
283
|
+
"syncCount": 100,
|
|
284
|
+
"eventCount": 500,
|
|
285
|
+
"factCount": 1000,
|
|
286
|
+
"storageBytes": 102400,
|
|
287
|
+
"periodStart": 1234567890,
|
|
288
|
+
"periodEnd": 1234567890
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `GET /stats?appId=<appId>`
|
|
293
|
+
|
|
294
|
+
Aggregated statistics endpoint.
|
|
295
|
+
|
|
296
|
+
**Response:**
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"appId": "my-app",
|
|
300
|
+
"totalSyncs": 100,
|
|
301
|
+
"usage": { ... },
|
|
302
|
+
"lastSync": 1234567890
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Authentication
|
|
307
|
+
|
|
308
|
+
Praxis Cloud uses GitHub OAuth for authentication.
|
|
309
|
+
|
|
310
|
+
### Device Flow (CLI)
|
|
311
|
+
|
|
312
|
+
The CLI uses GitHub's device flow for authentication:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { authenticateWithDeviceFlow } from "@plures/praxis/cloud";
|
|
316
|
+
|
|
317
|
+
const result = await authenticateWithDeviceFlow("your-client-id");
|
|
318
|
+
|
|
319
|
+
if (result.success) {
|
|
320
|
+
console.log(`Authenticated as ${result.user.login}`);
|
|
321
|
+
console.log(`Token: ${result.token}`);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Web Flow
|
|
326
|
+
|
|
327
|
+
For web applications, use the standard OAuth flow:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { createGitHubOAuth } from "@plures/praxis/cloud";
|
|
331
|
+
|
|
332
|
+
const oauth = createGitHubOAuth({
|
|
333
|
+
clientId: "your-client-id",
|
|
334
|
+
clientSecret: "your-client-secret",
|
|
335
|
+
redirectUri: "http://localhost:3000/callback"
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Redirect user to GitHub
|
|
339
|
+
window.location.href = oauth.getAuthorizationUrl();
|
|
340
|
+
|
|
341
|
+
// In callback route:
|
|
342
|
+
const result = await oauth.exchangeCode(code);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Configuration File
|
|
346
|
+
|
|
347
|
+
The `.praxis-cloud.json` file stores your cloud configuration:
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"endpoint": "https://praxis-relay.azurewebsites.net",
|
|
352
|
+
"appId": "my-app",
|
|
353
|
+
"authToken": "gho_xxxxxxxxxxxx",
|
|
354
|
+
"autoSync": true,
|
|
355
|
+
"syncInterval": 5000
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Security Note:** Add `.praxis-cloud.json` to your `.gitignore` to avoid committing credentials.
|
|
360
|
+
|
|
361
|
+
## Pricing (Tier 1)
|
|
362
|
+
|
|
363
|
+
Praxis Cloud Base Tier includes:
|
|
364
|
+
|
|
365
|
+
- **PluresDB CRDT Sync** - Up to 10,000 syncs/month
|
|
366
|
+
- **Event Forwarding** - Up to 50,000 events/month
|
|
367
|
+
- **Blob Storage** - 1 GB encrypted storage
|
|
368
|
+
- **GitHub OAuth** - Unlimited authentications
|
|
369
|
+
|
|
370
|
+
Additional usage is billed per:
|
|
371
|
+
- Sync operations: $0.001 per sync
|
|
372
|
+
- Events: $0.0001 per event
|
|
373
|
+
- Storage: $0.10 per GB/month
|
|
374
|
+
|
|
375
|
+
## Support
|
|
376
|
+
|
|
377
|
+
- [Documentation](https://github.com/plures/praxis/tree/main/docs)
|
|
378
|
+
- [GitHub Issues](https://github.com/plures/praxis/issues)
|
|
379
|
+
- [Discussions](https://github.com/plures/praxis/discussions)
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Authentication
|
|
3
|
+
*
|
|
4
|
+
* GitHub OAuth integration for Praxis Cloud Relay identity.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AuthResult, GitHubUser } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GitHub OAuth configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface GitHubOAuthConfig {
|
|
13
|
+
clientId: string;
|
|
14
|
+
clientSecret?: string;
|
|
15
|
+
redirectUri?: string;
|
|
16
|
+
scope?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GitHub OAuth client
|
|
21
|
+
*/
|
|
22
|
+
export class GitHubOAuth {
|
|
23
|
+
private config: GitHubOAuthConfig;
|
|
24
|
+
|
|
25
|
+
constructor(config: GitHubOAuthConfig) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the OAuth authorization URL
|
|
31
|
+
*/
|
|
32
|
+
getAuthorizationUrl(state?: string): string {
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
client_id: this.config.clientId,
|
|
35
|
+
scope: this.config.scope || "read:user user:email",
|
|
36
|
+
state: state || this.generateState(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (this.config.redirectUri) {
|
|
40
|
+
params.set("redirect_uri", this.config.redirectUri);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exchange authorization code for access token
|
|
48
|
+
*/
|
|
49
|
+
async exchangeCode(code: string): Promise<AuthResult> {
|
|
50
|
+
if (!this.config.clientSecret) {
|
|
51
|
+
throw new Error("Client secret is required for code exchange");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(
|
|
56
|
+
"https://github.com/login/oauth/access_token",
|
|
57
|
+
{
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Accept: "application/json",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
client_id: this.config.clientId,
|
|
65
|
+
client_secret: this.config.clientSecret,
|
|
66
|
+
code,
|
|
67
|
+
}),
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`Token exchange failed: ${response.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const data = await response.json() as any;
|
|
76
|
+
|
|
77
|
+
if (data.error) {
|
|
78
|
+
throw new Error(`GitHub OAuth error: ${data.error_description || data.error}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get user info
|
|
82
|
+
const user = await this.getUserInfo(data.access_token);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
token: data.access_token,
|
|
87
|
+
user,
|
|
88
|
+
expiresAt: data.expires_in
|
|
89
|
+
? Date.now() + data.expires_in * 1000
|
|
90
|
+
: undefined,
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get user information from GitHub
|
|
101
|
+
*/
|
|
102
|
+
async getUserInfo(token: string): Promise<GitHubUser> {
|
|
103
|
+
const response = await fetch("https://api.github.com/user", {
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${token}`,
|
|
106
|
+
Accept: "application/vnd.github.v3+json",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`Failed to get user info: ${response.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await response.json() as any;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: data.id,
|
|
118
|
+
login: data.login,
|
|
119
|
+
email: data.email,
|
|
120
|
+
name: data.name,
|
|
121
|
+
avatarUrl: data.avatar_url,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Verify a token is valid
|
|
127
|
+
*/
|
|
128
|
+
async verifyToken(token: string): Promise<boolean> {
|
|
129
|
+
try {
|
|
130
|
+
await this.getUserInfo(token);
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate a random state parameter for CSRF protection
|
|
139
|
+
*/
|
|
140
|
+
private generateState(): string {
|
|
141
|
+
const array = new Uint8Array(16);
|
|
142
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
143
|
+
crypto.getRandomValues(array);
|
|
144
|
+
} else {
|
|
145
|
+
// Fallback for Node.js
|
|
146
|
+
for (let i = 0; i < array.length; i++) {
|
|
147
|
+
array[i] = Math.floor(Math.random() * 256);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
151
|
+
""
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a GitHub OAuth client
|
|
158
|
+
*/
|
|
159
|
+
export function createGitHubOAuth(config: GitHubOAuthConfig): GitHubOAuth {
|
|
160
|
+
return new GitHubOAuth(config);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Authenticate with GitHub OAuth device flow (for CLI)
|
|
165
|
+
*/
|
|
166
|
+
export async function authenticateWithDeviceFlow(
|
|
167
|
+
clientId: string
|
|
168
|
+
): Promise<AuthResult> {
|
|
169
|
+
try {
|
|
170
|
+
// Request device code
|
|
171
|
+
const deviceResponse = await fetch(
|
|
172
|
+
"https://github.com/login/device/code",
|
|
173
|
+
{
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
Accept: "application/json",
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
client_id: clientId,
|
|
181
|
+
scope: "read:user user:email",
|
|
182
|
+
}),
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (!deviceResponse.ok) {
|
|
187
|
+
throw new Error(`Device flow initiation failed: ${deviceResponse.statusText}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const deviceData = await deviceResponse.json() as any;
|
|
191
|
+
|
|
192
|
+
console.log("\nTo authenticate with GitHub:");
|
|
193
|
+
console.log(`1. Visit: ${deviceData.verification_uri}`);
|
|
194
|
+
console.log(`2. Enter code: ${deviceData.user_code}`);
|
|
195
|
+
console.log("\nWaiting for authentication...\n");
|
|
196
|
+
|
|
197
|
+
// Poll for access token
|
|
198
|
+
const interval = deviceData.interval * 1000 || 5000;
|
|
199
|
+
const expiresAt = Date.now() + deviceData.expires_in * 1000;
|
|
200
|
+
|
|
201
|
+
while (Date.now() < expiresAt) {
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
203
|
+
|
|
204
|
+
const tokenResponse = await fetch(
|
|
205
|
+
"https://github.com/login/oauth/access_token",
|
|
206
|
+
{
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
Accept: "application/json",
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
client_id: clientId,
|
|
214
|
+
device_code: deviceData.device_code,
|
|
215
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
216
|
+
}),
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const tokenData = await tokenResponse.json() as any;
|
|
221
|
+
|
|
222
|
+
if (tokenData.access_token) {
|
|
223
|
+
// Get user info
|
|
224
|
+
const oauth = new GitHubOAuth({ clientId });
|
|
225
|
+
const user = await oauth.getUserInfo(tokenData.access_token);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
token: tokenData.access_token,
|
|
230
|
+
user,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (tokenData.error && tokenData.error !== "authorization_pending") {
|
|
235
|
+
throw new Error(`Authentication failed: ${tokenData.error_description || tokenData.error}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw new Error("Authentication timeout");
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|