@feltdb/core 0.5.7 → 0.6.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/cli/commands.js +110 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +127 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +21 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +10 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
- package/dist/create/template/dot-feltdb-README.md +58 -0
- package/dist/create/workspace-initialization.js +77 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/development-node.d.ts +42 -0
- package/dist/workspace/development-node.d.ts.map +1 -0
- package/dist/workspace/development-node.js +208 -0
- package/dist/workspace/index.d.ts +20 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +16 -0
- package/dist/workspace/workspace-connection.d.ts +174 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -0
- package/dist/workspace/workspace-connection.js +290 -0
- package/dist/workspace/workspace-identity.d.ts +13 -0
- package/dist/workspace/workspace-identity.d.ts.map +1 -0
- package/dist/workspace/workspace-identity.js +70 -0
- package/dist/workspace/workspace-types.d.ts +82 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -0
- package/dist/workspace/workspace-types.js +7 -0
- package/package.json +5 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
package/dist/cli/commands.js
CHANGED
|
@@ -8,6 +8,7 @@ import net from 'net';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import { spawn, spawnSync } from 'child_process';
|
|
10
10
|
import { createFeltDB, diffFlowSpec, formatFlowSpec, parseFlowSpec, planFlowSpecMigration, validateFlowSpec } from '@feltdb/core';
|
|
11
|
+
import { discoverWorkspace, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace } from './workspace-integration.js';
|
|
11
12
|
function loadProjectEnvironment(file = path.resolve('.env.local')) {
|
|
12
13
|
if (!fs.existsSync(file))
|
|
13
14
|
return;
|
|
@@ -49,6 +50,8 @@ export async function handleCommand(command, args) {
|
|
|
49
50
|
return handleConnect(args);
|
|
50
51
|
case 'status':
|
|
51
52
|
return handleStatus();
|
|
53
|
+
case 'workspace':
|
|
54
|
+
return handleWorkspace(args);
|
|
52
55
|
case 'dev':
|
|
53
56
|
return handleDev(args);
|
|
54
57
|
case 'build':
|
|
@@ -365,6 +368,51 @@ async function handleStatus() {
|
|
|
365
368
|
console.log('Workflows: 5');
|
|
366
369
|
console.log('Health: ✓\n');
|
|
367
370
|
}
|
|
371
|
+
async function handleWorkspace(args) {
|
|
372
|
+
const subcommand = args[0] || 'status';
|
|
373
|
+
switch (subcommand) {
|
|
374
|
+
case 'status':
|
|
375
|
+
return handleWorkspaceStatus();
|
|
376
|
+
case 'info':
|
|
377
|
+
return handleWorkspaceInfo();
|
|
378
|
+
case 'connect':
|
|
379
|
+
return handleWorkspaceConnect(args.slice(1));
|
|
380
|
+
default:
|
|
381
|
+
console.error(`Unknown workspace subcommand: ${subcommand}`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function handleWorkspaceStatus() {
|
|
386
|
+
displayWorkspaceStatus(process.cwd());
|
|
387
|
+
}
|
|
388
|
+
async function handleWorkspaceInfo() {
|
|
389
|
+
const workspace = discoverWorkspace(process.cwd());
|
|
390
|
+
if (!workspace) {
|
|
391
|
+
console.log('❌ No Development Workspace found');
|
|
392
|
+
console.log(' Run "feltdb dev" to initialize the workspace');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log('\n📋 Workspace Information\n');
|
|
396
|
+
console.log(` Workspace ID: ${workspace.workspaceId}`);
|
|
397
|
+
console.log(` Project: ${workspace.projectId}`);
|
|
398
|
+
console.log(` Version: ${workspace.version}`);
|
|
399
|
+
console.log(` Location: .feltdb/workspace.json\n`);
|
|
400
|
+
console.log(' This workspace enables:');
|
|
401
|
+
console.log(' • Browser extensions to discover and connect');
|
|
402
|
+
console.log(' • IDE integrations to share state');
|
|
403
|
+
console.log(' • Agents to coordinate through the same interface');
|
|
404
|
+
console.log(' • CLI to manage workspace discovery\n');
|
|
405
|
+
}
|
|
406
|
+
async function handleWorkspaceConnect(args) {
|
|
407
|
+
const workspaceId = args[0];
|
|
408
|
+
if (!workspaceId) {
|
|
409
|
+
console.error('Usage: feltdb workspace connect <workspace-id>');
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
console.log(`🔗 Connecting to workspace: ${workspaceId}`);
|
|
413
|
+
console.log(' This feature will be implemented in the next phase');
|
|
414
|
+
console.log(' (when @feltdb/client workspace APIs are available)\n');
|
|
415
|
+
}
|
|
368
416
|
function runLocalVite(args, waitForExit) {
|
|
369
417
|
const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
370
418
|
const child = spawn(executable, ['exec', '--', 'vite', ...args], {
|
|
@@ -383,11 +431,50 @@ async function handleDev(args) {
|
|
|
383
431
|
var _a, _b, _c;
|
|
384
432
|
loadProjectEnvironment();
|
|
385
433
|
console.log('🚀 Starting FeltDB development server...\n');
|
|
386
|
-
|
|
387
|
-
const configPath = path.join(
|
|
434
|
+
const projectDir = process.cwd();
|
|
435
|
+
const configPath = path.join(projectDir, 'feltdb.config.json');
|
|
436
|
+
// Initialize workspace if needed
|
|
437
|
+
let workspace = discoverWorkspace(projectDir);
|
|
438
|
+
if (!workspace) {
|
|
439
|
+
// Get project name from package.json
|
|
440
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
441
|
+
let projectId = path.basename(projectDir);
|
|
442
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
443
|
+
try {
|
|
444
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
445
|
+
projectId = packageJson.name || projectId;
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// Use default projectId if package.json parsing fails
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
workspace = initializeWorkspace(projectDir, projectId);
|
|
452
|
+
console.log(`✨ Initialized Development Workspace`);
|
|
453
|
+
console.log(` Workspace ID: ${workspace.workspaceId}\n`);
|
|
454
|
+
}
|
|
455
|
+
// Create feltdb.config.json if it doesn't exist
|
|
388
456
|
if (!fs.existsSync(configPath)) {
|
|
389
|
-
|
|
390
|
-
|
|
457
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
458
|
+
let namespace = path.basename(projectDir);
|
|
459
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
460
|
+
try {
|
|
461
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
462
|
+
namespace = packageJson.name || namespace;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Use default namespace if package.json parsing fails
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const defaultConfig = {
|
|
469
|
+
namespace,
|
|
470
|
+
runtime: 'browser',
|
|
471
|
+
storage: 'indexeddb',
|
|
472
|
+
distributed: true,
|
|
473
|
+
agents: { enabled: false },
|
|
474
|
+
capabilities: { search: true },
|
|
475
|
+
};
|
|
476
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
477
|
+
console.log(`✨ Created feltdb.config.json with default configuration\n`);
|
|
391
478
|
}
|
|
392
479
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
393
480
|
if (config.runtime === 'managed') {
|
|
@@ -432,11 +519,29 @@ async function handleDev(args) {
|
|
|
432
519
|
selfHostedStarted = true;
|
|
433
520
|
(_c = process.env).VITE_FELTDB_URL || (_c.VITE_FELTDB_URL = 'http://127.0.0.1:7700');
|
|
434
521
|
}
|
|
522
|
+
// Generate pairing token for the workspace
|
|
523
|
+
// This enables browsers and IDEs to discover and connect to the workspace
|
|
524
|
+
let pairingToken = null;
|
|
525
|
+
if (workspace) {
|
|
526
|
+
const token = generatePairingToken();
|
|
527
|
+
token.workspaceId = workspace.workspaceId;
|
|
528
|
+
persistPairingToken(process.cwd(), token);
|
|
529
|
+
pairingToken = token.token;
|
|
530
|
+
}
|
|
435
531
|
console.log('FeltDB Dev Server');
|
|
436
532
|
console.log(` Namespace: ${config.namespace}`);
|
|
437
533
|
console.log(` Runtime: ${config.runtime}`);
|
|
438
534
|
console.log(` Storage: ${config.storage}`);
|
|
439
|
-
console.log(` Distributed: ${config.distributed}
|
|
535
|
+
console.log(` Distributed: ${config.distributed}`);
|
|
536
|
+
if (workspace) {
|
|
537
|
+
console.log(` Workspace: ${workspace.workspaceId}\n`);
|
|
538
|
+
console.log(`🔗 Development Workspace`);
|
|
539
|
+
if (pairingToken) {
|
|
540
|
+
console.log(` Pairing Code: ${pairingToken}`);
|
|
541
|
+
console.log(` Clients can use this code to auto-discover the workspace`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
console.log();
|
|
440
545
|
const open = !args.includes('--no-open');
|
|
441
546
|
console.log(`Application: http://127.0.0.1:${appPort}`);
|
|
442
547
|
console.log(`Studio: http://127.0.0.1:${studioPort}\n`);
|
package/dist/cli/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as path from 'path';
|
|
|
23
23
|
import * as readline from 'readline';
|
|
24
24
|
import { getClient } from './api-client.js';
|
|
25
25
|
import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
|
|
26
|
-
const VERSION = '0.
|
|
26
|
+
const VERSION = '0.6.1';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB Development Workspace Integration
|
|
3
|
+
*
|
|
4
|
+
* Integrates Development Workspace support into the CLI:
|
|
5
|
+
* - Discovers workspace identity from .feltdb/workspace.json
|
|
6
|
+
* - Launches Development Workspace authority via getDevelopmentNode()
|
|
7
|
+
* - Manages pairing tokens for browser/IDE discovery
|
|
8
|
+
* - Provides workspace status and connection information
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { randomBytes } from 'crypto';
|
|
13
|
+
/**
|
|
14
|
+
* Discover workspace identity from .feltdb/workspace.json
|
|
15
|
+
* Returns null if workspace is not initialized
|
|
16
|
+
*/
|
|
17
|
+
export function discoverWorkspace(projectDir) {
|
|
18
|
+
const workspacePath = path.join(projectDir, '.feltdb', 'workspace.json');
|
|
19
|
+
if (!fs.existsSync(workspacePath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(workspacePath, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error('Failed to read workspace.json:', error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate a short-lived pairing token for browser/IDE discovery
|
|
33
|
+
* Format: FELT-XXXX (6 random alphanumeric characters)
|
|
34
|
+
* Expires in 15 minutes
|
|
35
|
+
*/
|
|
36
|
+
export function generatePairingToken() {
|
|
37
|
+
const randomPart = randomBytes(4).toString('hex').toUpperCase().slice(0, 6);
|
|
38
|
+
const token = `FELT-${randomPart}`;
|
|
39
|
+
const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
|
|
40
|
+
return { token, workspaceId: '', expiresAt };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Persist pairing token to .feltdb/pairing.json
|
|
44
|
+
* This file should be gitignored but is readable by clients
|
|
45
|
+
*/
|
|
46
|
+
export function persistPairingToken(projectDir, token) {
|
|
47
|
+
const feltdbDir = path.join(projectDir, '.feltdb');
|
|
48
|
+
if (!fs.existsSync(feltdbDir)) {
|
|
49
|
+
fs.mkdirSync(feltdbDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const pairingPath = path.join(feltdbDir, 'pairing.json');
|
|
52
|
+
fs.writeFileSync(pairingPath, JSON.stringify(token, null, 2) + '\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read pairing token from .feltdb/pairing.json
|
|
56
|
+
* Returns null if token doesn't exist or has expired
|
|
57
|
+
*/
|
|
58
|
+
export function readPairingToken(projectDir) {
|
|
59
|
+
const pairingPath = path.join(projectDir, '.feltdb', 'pairing.json');
|
|
60
|
+
if (!fs.existsSync(pairingPath)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(pairingPath, 'utf-8');
|
|
65
|
+
const token = JSON.parse(content);
|
|
66
|
+
// Check if token has expired
|
|
67
|
+
if (token.expiresAt && Date.now() > token.expiresAt) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return token;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate a unique workspace ID
|
|
78
|
+
* Format: ws_<projectId>_<timestamp>_<random>
|
|
79
|
+
*/
|
|
80
|
+
export function generateWorkspaceId(projectId) {
|
|
81
|
+
const timestamp = Date.now();
|
|
82
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
83
|
+
return `ws_${projectId}_${timestamp}_${random}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Initialize development workspace for a project
|
|
87
|
+
* Creates .feltdb/workspace.json with workspace discovery information.
|
|
88
|
+
*/
|
|
89
|
+
export function initializeWorkspace(projectDir, projectId) {
|
|
90
|
+
const feltdbDir = path.join(projectDir, '.feltdb');
|
|
91
|
+
// Ensure .feltdb directory exists
|
|
92
|
+
if (!fs.existsSync(feltdbDir)) {
|
|
93
|
+
fs.mkdirSync(feltdbDir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
// Create workspace discovery
|
|
96
|
+
const workspaceId = generateWorkspaceId(projectId);
|
|
97
|
+
const discovery = {
|
|
98
|
+
workspaceId,
|
|
99
|
+
projectId,
|
|
100
|
+
version: 1,
|
|
101
|
+
};
|
|
102
|
+
// Write workspace.json
|
|
103
|
+
const workspacePath = path.join(feltdbDir, 'workspace.json');
|
|
104
|
+
fs.writeFileSync(workspacePath, JSON.stringify(discovery, null, 2) + '\n');
|
|
105
|
+
return discovery;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Display workspace status information
|
|
109
|
+
*/
|
|
110
|
+
export function displayWorkspaceStatus(projectDir) {
|
|
111
|
+
const workspace = discoverWorkspace(projectDir);
|
|
112
|
+
if (!workspace) {
|
|
113
|
+
console.log('❌ No Development Workspace found');
|
|
114
|
+
console.log(' Run "feltdb dev" to initialize the workspace');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const pairingToken = readPairingToken(projectDir);
|
|
118
|
+
console.log('\n🔗 FeltDB Development Workspace\n');
|
|
119
|
+
console.log(` Project: ${workspace.projectId}`);
|
|
120
|
+
console.log(` Workspace: ${workspace.workspaceId}`);
|
|
121
|
+
if (pairingToken) {
|
|
122
|
+
console.log(` Pairing: ${pairingToken.token}`);
|
|
123
|
+
const minutesRemaining = Math.ceil((pairingToken.expiresAt - Date.now()) / 60000);
|
|
124
|
+
console.log(` Expires in: ${minutesRemaining} minutes`);
|
|
125
|
+
}
|
|
126
|
+
console.log('\n Location: .feltdb/workspace.json\n');
|
|
127
|
+
}
|
package/dist/create/cli.js
CHANGED
|
@@ -307,7 +307,7 @@ Learn more: https://github.com/rkendel1/feltdb`);
|
|
|
307
307
|
await createProject({
|
|
308
308
|
projectName,
|
|
309
309
|
autoYes: shouldAutoYes,
|
|
310
|
-
templatesDir: path.join(__dirname, '../
|
|
310
|
+
templatesDir: path.join(__dirname, '../template'),
|
|
311
311
|
runtime: options.runtime,
|
|
312
312
|
framework: options.framework,
|
|
313
313
|
distributed: options.distributed,
|
package/dist/create/create.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import { randomBytes } from 'crypto';
|
|
7
7
|
import { feltdbPackageRange } from './package-versions.js';
|
|
8
8
|
import { generateDockerCompose, generateDockerfile, generateDockerIgnore, generateDotEnvLocal, generateFeltDBDockerfile, generateStudioDockerfile, } from './docker-compose-generator.js';
|
|
9
|
+
import { initializeWorkspace, appendWorkspaceGitignore, } from './workspace-initialization.js';
|
|
9
10
|
export async function createProject(options) {
|
|
10
11
|
const { projectName, templatesDir } = options;
|
|
11
12
|
const projectDir = path.resolve(process.cwd(), projectName);
|
|
@@ -1759,4 +1760,24 @@ If port 5173 is in use:
|
|
|
1759
1760
|
MIT
|
|
1760
1761
|
`;
|
|
1761
1762
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1763
|
+
// Initialize Development Workspace
|
|
1764
|
+
// This creates .feltdb/workspace.json which enables all FeltDB-aware tools
|
|
1765
|
+
// (CLI, IDE, agents, browser extensions) to discover and connect to the
|
|
1766
|
+
// same workspace without manual configuration.
|
|
1767
|
+
const workspaceDiscovery = initializeWorkspace(projectDir, applicationName);
|
|
1768
|
+
appendWorkspaceGitignore(projectDir);
|
|
1769
|
+
// Copy .feltdb README from template
|
|
1770
|
+
const templateReadmePath = path.join(templatesDir, 'dot-feltdb-README.md');
|
|
1771
|
+
const targetReadmePath = path.join(projectDir, '.feltdb', 'README.md');
|
|
1772
|
+
if (fs.existsSync(templateReadmePath)) {
|
|
1773
|
+
fs.copyFileSync(templateReadmePath, targetReadmePath);
|
|
1774
|
+
}
|
|
1775
|
+
console.log(`\n✓ Development Workspace initialized`);
|
|
1776
|
+
console.log(` Workspace ID: ${workspaceDiscovery.workspaceId}`);
|
|
1777
|
+
console.log(` Location: ${path.join(projectName, '.feltdb/workspace.json')}`);
|
|
1778
|
+
console.log(`\nNext steps:`);
|
|
1779
|
+
console.log(` cd ${projectName}`);
|
|
1780
|
+
console.log(` npm install`);
|
|
1781
|
+
console.log(` npm run dev`);
|
|
1782
|
+
console.log(`\nYour Development Workspace is ready for use!`);
|
|
1762
1783
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.6.1';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -513,6 +513,7 @@ dependencies = [
|
|
|
513
513
|
"base64",
|
|
514
514
|
"feltdb",
|
|
515
515
|
"futures-core",
|
|
516
|
+
"hmac",
|
|
516
517
|
"password-hash",
|
|
517
518
|
"rand 0.8.7",
|
|
518
519
|
"reqwest",
|
|
@@ -668,6 +669,15 @@ version = "0.5.2"
|
|
|
668
669
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
669
670
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
|
670
671
|
|
|
672
|
+
[[package]]
|
|
673
|
+
name = "hmac"
|
|
674
|
+
version = "0.12.1"
|
|
675
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
676
|
+
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
|
677
|
+
dependencies = [
|
|
678
|
+
"digest",
|
|
679
|
+
]
|
|
680
|
+
|
|
671
681
|
[[package]]
|
|
672
682
|
name = "http"
|
|
673
683
|
version = "1.5.0"
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
use std::sync::{atomic::AtomicU64, Arc};
|
|
2
1
|
use std::path::PathBuf;
|
|
2
|
+
use std::{
|
|
3
|
+
sync::{atomic::AtomicU64, Arc},
|
|
4
|
+
time::Instant,
|
|
5
|
+
};
|
|
3
6
|
|
|
4
7
|
use feltdb::{
|
|
5
8
|
FeltDb,
|
|
@@ -33,6 +36,7 @@ use crate::{
|
|
|
33
36
|
|
|
34
37
|
#[derive(Clone)]
|
|
35
38
|
pub struct AppState {
|
|
39
|
+
pub started_at: Instant,
|
|
36
40
|
pub db: FeltDb,
|
|
37
41
|
pub namespace: Arc<str>,
|
|
38
42
|
pub ids: Arc<AtomicU64>,
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
3
|
+
|
|
4
|
+
/// Authenticated principal derived exclusively from verified credentials.
|
|
5
|
+
/// Never derived from request body or untrusted sources.
|
|
6
|
+
/// Available to authorization evaluation and included in request context.
|
|
7
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
8
|
+
pub struct AuthenticatedPrincipal {
|
|
9
|
+
/// ID of the service key used for authentication.
|
|
10
|
+
/// Preserved separately from actorId for auditing.
|
|
11
|
+
pub service_key_id: String,
|
|
12
|
+
|
|
13
|
+
/// ID of the actor (user, service, workload, etc.) performing the operation.
|
|
14
|
+
pub actor_id: String,
|
|
15
|
+
|
|
16
|
+
/// Tenant ID from authenticated credentials.
|
|
17
|
+
pub tenant_id: String,
|
|
18
|
+
|
|
19
|
+
/// Roles granted to this principal.
|
|
20
|
+
pub roles: Vec<String>,
|
|
21
|
+
|
|
22
|
+
/// Authentication method used.
|
|
23
|
+
pub auth_method: AuthMethod,
|
|
24
|
+
|
|
25
|
+
/// When this principal was authenticated.
|
|
26
|
+
pub authenticated_at: u64,
|
|
27
|
+
|
|
28
|
+
/// Provenance tracking for audit.
|
|
29
|
+
pub provenance: AuthenticationProvenance,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
33
|
+
#[serde(rename_all = "lowercase")]
|
|
34
|
+
pub enum AuthMethod {
|
|
35
|
+
ApiKey,
|
|
36
|
+
Session,
|
|
37
|
+
Delegation,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
41
|
+
pub struct AuthenticationProvenance {
|
|
42
|
+
/// Source of the credential (e.g., "bearer_token", "session_cookie", "delegation_token").
|
|
43
|
+
pub credential_source: String,
|
|
44
|
+
|
|
45
|
+
/// Hash of the credential for audit purposes (never the credential itself).
|
|
46
|
+
pub credential_hash: Option<String>,
|
|
47
|
+
|
|
48
|
+
/// Additional context about the authentication.
|
|
49
|
+
pub context: String,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl AuthenticatedPrincipal {
|
|
53
|
+
/// Creates a new AuthenticatedPrincipal from API key authentication.
|
|
54
|
+
pub fn from_api_key(
|
|
55
|
+
service_key_id: String,
|
|
56
|
+
actor_id: String,
|
|
57
|
+
tenant_id: String,
|
|
58
|
+
roles: Vec<String>,
|
|
59
|
+
) -> Self {
|
|
60
|
+
Self {
|
|
61
|
+
service_key_id,
|
|
62
|
+
actor_id,
|
|
63
|
+
tenant_id,
|
|
64
|
+
roles,
|
|
65
|
+
auth_method: AuthMethod::ApiKey,
|
|
66
|
+
authenticated_at: now(),
|
|
67
|
+
provenance: AuthenticationProvenance {
|
|
68
|
+
credential_source: "bearer_token".into(),
|
|
69
|
+
credential_hash: None,
|
|
70
|
+
context: "API key authentication".into(),
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Creates a new AuthenticatedPrincipal from session authentication.
|
|
76
|
+
pub fn from_session(
|
|
77
|
+
service_key_id: String,
|
|
78
|
+
actor_id: String,
|
|
79
|
+
tenant_id: String,
|
|
80
|
+
roles: Vec<String>,
|
|
81
|
+
) -> Self {
|
|
82
|
+
Self {
|
|
83
|
+
service_key_id,
|
|
84
|
+
actor_id,
|
|
85
|
+
tenant_id,
|
|
86
|
+
roles,
|
|
87
|
+
auth_method: AuthMethod::Session,
|
|
88
|
+
authenticated_at: now(),
|
|
89
|
+
provenance: AuthenticationProvenance {
|
|
90
|
+
credential_source: "session_cookie".into(),
|
|
91
|
+
credential_hash: None,
|
|
92
|
+
context: "Session authentication".into(),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Creates a new AuthenticatedPrincipal from delegation token.
|
|
98
|
+
pub fn from_delegation(
|
|
99
|
+
service_key_id: String,
|
|
100
|
+
actor_id: String,
|
|
101
|
+
tenant_id: String,
|
|
102
|
+
roles: Vec<String>,
|
|
103
|
+
delegation_context: String,
|
|
104
|
+
) -> Self {
|
|
105
|
+
Self {
|
|
106
|
+
service_key_id,
|
|
107
|
+
actor_id,
|
|
108
|
+
tenant_id,
|
|
109
|
+
roles,
|
|
110
|
+
auth_method: AuthMethod::Delegation,
|
|
111
|
+
authenticated_at: now(),
|
|
112
|
+
provenance: AuthenticationProvenance {
|
|
113
|
+
credential_source: "delegation_token".into(),
|
|
114
|
+
credential_hash: None,
|
|
115
|
+
context: delegation_context,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Checks if this principal has a specific role.
|
|
121
|
+
pub fn has_role(&self, role: &str) -> bool {
|
|
122
|
+
self.roles.iter().any(|r| r == role || r == "*")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Checks if actor_id matches expected value, rejecting mismatches.
|
|
126
|
+
pub fn verify_actor_id(&self, expected: &str) -> Result<(), String> {
|
|
127
|
+
if self.actor_id != expected {
|
|
128
|
+
Err(format!(
|
|
129
|
+
"actor_id mismatch: authenticated as {} but operation requested {}",
|
|
130
|
+
self.actor_id, expected
|
|
131
|
+
))
|
|
132
|
+
} else {
|
|
133
|
+
Ok(())
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Checks if tenant_id matches expected value, rejecting mismatches.
|
|
138
|
+
pub fn verify_tenant_id(&self, expected: &str) -> Result<(), String> {
|
|
139
|
+
if self.tenant_id != expected {
|
|
140
|
+
Err(format!(
|
|
141
|
+
"tenant_id mismatch: authenticated for {} but operation requested {}",
|
|
142
|
+
self.tenant_id, expected
|
|
143
|
+
))
|
|
144
|
+
} else {
|
|
145
|
+
Ok(())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn now() -> u64 {
|
|
151
|
+
SystemTime::now()
|
|
152
|
+
.duration_since(UNIX_EPOCH)
|
|
153
|
+
.unwrap_or_default()
|
|
154
|
+
.as_secs()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
#[cfg(test)]
|
|
159
|
+
mod tests {
|
|
160
|
+
use super::*;
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn authenticated_principal_preserves_separation() {
|
|
164
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
165
|
+
"key_abc123".into(),
|
|
166
|
+
"user_xyz".into(),
|
|
167
|
+
"tenant_prod".into(),
|
|
168
|
+
vec!["read".into(), "write".into()],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
assert_eq!(principal.service_key_id, "key_abc123");
|
|
172
|
+
assert_eq!(principal.actor_id, "user_xyz");
|
|
173
|
+
assert_eq!(principal.tenant_id, "tenant_prod");
|
|
174
|
+
assert_eq!(principal.auth_method, AuthMethod::ApiKey);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn verify_actor_id_rejects_mismatches() {
|
|
179
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
180
|
+
"key_abc123".into(),
|
|
181
|
+
"user_xyz".into(),
|
|
182
|
+
"tenant_prod".into(),
|
|
183
|
+
vec![],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
assert!(principal.verify_actor_id("user_xyz").is_ok());
|
|
187
|
+
assert!(principal.verify_actor_id("user_other").is_err());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn verify_tenant_id_rejects_mismatches() {
|
|
192
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
193
|
+
"key_abc123".into(),
|
|
194
|
+
"user_xyz".into(),
|
|
195
|
+
"tenant_prod".into(),
|
|
196
|
+
vec![],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
assert!(principal.verify_tenant_id("tenant_prod").is_ok());
|
|
200
|
+
assert!(principal.verify_tenant_id("tenant_other").is_err());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn has_role_works_correctly() {
|
|
205
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
206
|
+
"key_abc123".into(),
|
|
207
|
+
"user_xyz".into(),
|
|
208
|
+
"tenant_prod".into(),
|
|
209
|
+
vec!["admin".into(), "viewer".into()],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
assert!(principal.has_role("admin"));
|
|
213
|
+
assert!(principal.has_role("viewer"));
|
|
214
|
+
assert!(!principal.has_role("editor"));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn wildcard_role_matches_all() {
|
|
219
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
220
|
+
"key_abc123".into(),
|
|
221
|
+
"user_xyz".into(),
|
|
222
|
+
"tenant_prod".into(),
|
|
223
|
+
vec!["*".into()],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
assert!(principal.has_role("any_role"));
|
|
227
|
+
assert!(principal.has_role("admin"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn from_session_creates_correct_principal() {
|
|
232
|
+
let principal = AuthenticatedPrincipal::from_session(
|
|
233
|
+
"key_session".into(),
|
|
234
|
+
"user_abc".into(),
|
|
235
|
+
"tenant_staging".into(),
|
|
236
|
+
vec!["viewer".into()],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
assert_eq!(principal.auth_method, AuthMethod::Session);
|
|
240
|
+
assert_eq!(principal.provenance.credential_source, "session_cookie");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn from_delegation_creates_correct_principal() {
|
|
245
|
+
let principal = AuthenticatedPrincipal::from_delegation(
|
|
246
|
+
"key_delegated".into(),
|
|
247
|
+
"service_xyz".into(),
|
|
248
|
+
"tenant_prod".into(),
|
|
249
|
+
vec!["execute".into()],
|
|
250
|
+
"delegated from sherpa".into(),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
assert_eq!(principal.auth_method, AuthMethod::Delegation);
|
|
254
|
+
assert_eq!(principal.provenance.credential_source, "delegation_token");
|
|
255
|
+
assert_eq!(principal.provenance.context, "delegated from sherpa");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn multiple_calls_to_verify_preserve_state() {
|
|
260
|
+
let principal = AuthenticatedPrincipal::from_api_key(
|
|
261
|
+
"key_abc123".into(),
|
|
262
|
+
"user_xyz".into(),
|
|
263
|
+
"tenant_prod".into(),
|
|
264
|
+
vec![],
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Verify multiple times to ensure no state mutation
|
|
268
|
+
assert!(principal.verify_actor_id("user_xyz").is_ok());
|
|
269
|
+
assert!(principal.verify_actor_id("user_xyz").is_ok());
|
|
270
|
+
assert!(principal.verify_tenant_id("tenant_prod").is_ok());
|
|
271
|
+
assert!(principal.verify_tenant_id("tenant_prod").is_ok());
|
|
272
|
+
}
|
|
273
|
+
}
|