@feltdb/core 0.7.2 → 0.7.4

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.
Files changed (96) hide show
  1. package/dist/application-development.d.ts +76 -0
  2. package/dist/application-development.d.ts.map +1 -0
  3. package/dist/application-development.js +137 -0
  4. package/dist/application-manifest.d.ts +5 -1
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +10 -3
  7. package/dist/canonical-application.d.ts +26 -0
  8. package/dist/canonical-application.d.ts.map +1 -0
  9. package/dist/canonical-application.js +45 -0
  10. package/dist/cli/ai-model-config.js +97 -0
  11. package/dist/cli/commands.js +485 -30
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/proposal-client.js +54 -0
  14. package/dist/cli/publish-engine.js +162 -0
  15. package/dist/cli/source-sync.js +139 -0
  16. package/dist/create/cli.js +1 -1
  17. package/dist/create/create.js +30 -6
  18. package/dist/create/managed-account.js +14 -7
  19. package/dist/create/package-versions.js +1 -1
  20. package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
  21. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +847 -0
  22. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +338 -29
  23. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  24. package/dist/create/server-source/crates/feltdb/src/lib.rs +1 -0
  25. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
  26. package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
  27. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
  28. package/dist/create/template/default-project/README.md +57 -0
  29. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  30. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  31. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  32. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  33. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  34. package/dist/create/template/default-project/feltdb.flow +169 -0
  35. package/dist/create/template/default-project/src/App.tsx +20 -0
  36. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  37. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  38. package/dist/create/template/default-project/src/index.tsx +14 -0
  39. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  40. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  41. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  42. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  43. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  44. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  45. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  46. package/dist/create/template/default-project/src/styles-application.css +21 -0
  47. package/dist/create/template/default-project/src/styles.css +1 -0
  48. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  49. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  50. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  51. package/dist/db.d.ts +49 -1
  52. package/dist/db.d.ts.map +1 -1
  53. package/dist/db.js +77 -0
  54. package/dist/error-codes.d.ts +20 -1
  55. package/dist/error-codes.d.ts.map +1 -1
  56. package/dist/error-codes.js +25 -0
  57. package/dist/flowspec.d.ts.map +1 -1
  58. package/dist/flowspec.js +27 -6
  59. package/dist/http-db.d.ts +15 -2
  60. package/dist/http-db.d.ts.map +1 -1
  61. package/dist/http-db.js +113 -5
  62. package/dist/index-core.d.ts +3 -0
  63. package/dist/index-core.d.ts.map +1 -1
  64. package/dist/index-core.js +3 -0
  65. package/dist/proposal.d.ts +85 -0
  66. package/dist/proposal.d.ts.map +1 -0
  67. package/dist/proposal.js +105 -0
  68. package/dist/studio/app.d.ts +5 -1
  69. package/dist/studio/app.d.ts.map +1 -1
  70. package/dist/studio/components/ApplicationDesigner.d.ts +10 -1
  71. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  72. package/dist/studio/components/AskFeltDB.d.ts +20 -0
  73. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  74. package/dist/studio/components/ProposalBrowser.d.ts +3 -0
  75. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  76. package/dist/studio/components/index.d.ts +2 -0
  77. package/dist/studio/components/index.d.ts.map +1 -1
  78. package/dist/studio/components/index.js +4 -4
  79. package/dist/studio/{components-C5p2TfIU.js → components-Bm4sjhcX.js} +531 -192
  80. package/dist/studio/index.js +184 -172
  81. package/dist/studio/studio.css +1 -1
  82. package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
  83. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
  84. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  85. package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
  86. package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
  87. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  88. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  89. package/dist/studio-app/index.html +2 -2
  90. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  91. package/dist/workspace/local-development-authority.d.ts +3 -0
  92. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  93. package/dist/workspace/local-development-authority.js +62 -1
  94. package/package.json +1 -1
  95. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  96. package/dist/studio-app/assets/index-C71X92EK.js +0 -28
@@ -0,0 +1,16 @@
1
+ import type { Capability } from '@feltdb/core';
2
+ import { projects, type Project } from '../src/feltdb';
3
+
4
+ export interface ProjectSearchResult { id: string; name: string; description: string }
5
+
6
+ export const projectSearch: Capability = {
7
+ id: 'project-search',
8
+ name: 'Project search',
9
+ async execute(query: string): Promise<ProjectSearchResult[]> {
10
+ // The collection read crosses FeltDB's normal authorization boundary. The
11
+ // capability narrows results; it never grants access to additional rows.
12
+ const words = query.trim().toLowerCase();
13
+ const authorized = await projects.all() as Project[];
14
+ return authorized.filter(project => `${project.name} ${project.description}`.toLowerCase().includes(words)).map(({ id, name, description }) => ({ id, name, description }));
15
+ },
16
+ };
@@ -0,0 +1,15 @@
1
+ import type { Capability } from '@feltdb/core';
2
+ import { auditEvents, projects, type AuditEvent, type Project } from '../src/feltdb';
3
+
4
+ export interface ProjectSummary { title: string; summary: string; activityCount: number }
5
+
6
+ export const projectSummary: Capability = {
7
+ id: 'project-summary',
8
+ name: 'Project summary',
9
+ async execute(projectId: string): Promise<ProjectSummary> {
10
+ const project = await projects.get(projectId) as Project | null;
11
+ if (!project) throw new Error('Project is unavailable in the current authorization context.');
12
+ const activity = (await auditEvents.all() as AuditEvent[]).filter(event => event.resource_id === project.id);
13
+ return { title: project.name, summary: project.description || `${project.name} is active and ready for its next state transition.`, activityCount: activity.length };
14
+ },
15
+ };
@@ -0,0 +1,169 @@
1
+ flow_version 1
2
+
3
+ app FeltDBStarter {
4
+ collection User {
5
+ email: text
6
+ name: text
7
+ created_at: datetime
8
+ index email using hash(email)
9
+ }
10
+ collection Organization {
11
+ name: text
12
+ created_at: datetime
13
+ }
14
+ collection Membership {
15
+ user: ref User
16
+ organization: ref Organization
17
+ role: text
18
+ created_at: datetime
19
+ index user using hash(user)
20
+ index organization using hash(organization)
21
+ }
22
+ collection Session {
23
+ user: ref User
24
+ expires_at: datetime
25
+ created_at: datetime
26
+ index user using hash(user)
27
+ }
28
+ collection Project {
29
+ organization: ref Organization
30
+ name: text
31
+ description: text
32
+ owner_id: text
33
+ created_at: datetime
34
+ updated_at: datetime
35
+ index organization using hash(organization)
36
+ }
37
+ collection Invitation {
38
+ organization: ref Organization
39
+ user: ref User
40
+ email: text
41
+ role: text
42
+ status: text
43
+ created_at: datetime
44
+ updated_at: datetime
45
+ index organization using hash(organization)
46
+ }
47
+ collection AuditEvent {
48
+ organization: ref Organization
49
+ user: ref User
50
+ action: text
51
+ resource_type: text
52
+ resource_id: text
53
+ details: text
54
+ created_at: datetime
55
+ index organization using hash(organization)
56
+ }
57
+ collection Team {
58
+ organization: ref Organization
59
+ name: text
60
+ description: text
61
+ owner: ref User
62
+ created_at: datetime
63
+ updated_at: datetime
64
+ index organization using hash(organization)
65
+ }
66
+ collection TeamMembership {
67
+ organization: ref Organization
68
+ team: ref Team
69
+ user: ref User
70
+ role: text
71
+ created_at: datetime
72
+ updated_at: datetime
73
+ index organization using hash(organization)
74
+ }
75
+
76
+ capability ProjectSearch {
77
+ visibility public
78
+ read Project
79
+ }
80
+ capability ProjectSummary {
81
+ visibility agent
82
+ read Project
83
+ read AuditEvent
84
+ }
85
+ capability ActivitySummary {
86
+ visibility agent
87
+ read AuditEvent
88
+ }
89
+ capability RecordAgentActivity {
90
+ visibility internal
91
+ write AuditEvent
92
+ }
93
+
94
+ workflow ProjectCreated {
95
+ step persist_project {
96
+ write Project
97
+ }
98
+ step record_activity {
99
+ write AuditEvent
100
+ }
101
+ }
102
+ workflow InvitationLifecycle {
103
+ step create_invitation {
104
+ write Invitation
105
+ }
106
+ step accept_invitation {
107
+ write Membership
108
+ }
109
+ step record_activity {
110
+ write AuditEvent
111
+ }
112
+ }
113
+ workflow AgentAssistedSummary {
114
+ step read_authorized_state {
115
+ capability ProjectSummary
116
+ }
117
+ step record_activity {
118
+ capability RecordAgentActivity
119
+ }
120
+ }
121
+
122
+ agent ProjectAssistant {
123
+ capability ProjectSearch
124
+ capability ProjectSummary
125
+ capability RecordAgentActivity
126
+ workflow AgentAssistedSummary
127
+ }
128
+ agent ActivityAssistant {
129
+ capability ActivitySummary
130
+ capability RecordAgentActivity
131
+ workflow AgentAssistedSummary
132
+ }
133
+ policy User {
134
+ read: authenticated
135
+ write: owner
136
+ }
137
+ policy Organization {
138
+ read: member
139
+ write: owner
140
+ }
141
+ policy Membership {
142
+ read: member
143
+ write: member
144
+ }
145
+ policy Session {
146
+ read: self(user)
147
+ write: self(user)
148
+ }
149
+ policy Project {
150
+ read: member
151
+ write: member
152
+ }
153
+ policy Invitation {
154
+ read: self(user) | member
155
+ write: member
156
+ }
157
+ policy AuditEvent {
158
+ read: member
159
+ write: authenticated
160
+ }
161
+ policy Team {
162
+ read: member
163
+ write: member
164
+ }
165
+ policy TeamMembership {
166
+ read: member
167
+ write: member
168
+ }
169
+ }
@@ -0,0 +1,20 @@
1
+ import { AuthProvider, useAuth } from './context/AuthContext';
2
+ import { Dashboard } from './pages/Dashboard';
3
+ import { SignIn } from './pages/SignIn';
4
+ import { SignUp } from './pages/SignUp';
5
+ import './styles.css';
6
+ import './styles-application.css';
7
+
8
+ function Application() {
9
+ const { authenticated, loading } = useAuth();
10
+ if (loading) return <main className="center"><div className="loader" /><p>Resolving your FeltDB session…</p></main>;
11
+ if (!authenticated) return <AuthScreen />;
12
+ return <Dashboard />;
13
+ }
14
+
15
+ function AuthScreen() {
16
+ const signIn = new URLSearchParams(window.location.search).get('view') === 'signin';
17
+ return <main className="auth-layout"><section className="intro"><a className="brand" href="https://feltdb.com">FeltDB</a><div><span className="eyebrow">State first</span><h1>A real application, from the first command.</h1><p>Sign up to create a durable user, organization, membership, and session—then inspect all of it in Studio.</p></div><div className="flow"><span>Identity</span><i>→</i><span>Tenant</span><i>→</i><span>State</span></div></section><section className="auth-card">{signIn ? <SignIn /> : <SignUp />}</section></main>;
18
+ }
19
+
20
+ export function App() { return <AuthProvider><Application /></AuthProvider>; }
@@ -0,0 +1,27 @@
1
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { createAccount, createSession, db, memberships, organizations, sessions, users, type Membership, type Organization, type Session, type User } from '../feltdb';
4
+
5
+ const SESSION_KEY = 'feltdb.example.session';
6
+ interface AuthValue { loading: boolean; authenticated: boolean; user?: User; session?: Session; organization?: Organization; membership?: Membership; signUp(input: { name: string; email: string; password: string; organization: string }): Promise<void>; signIn(email: string, password: string): Promise<void>; signOut(): Promise<void> }
7
+ const AuthContext = createContext<AuthValue | undefined>(undefined);
8
+
9
+ export function AuthProvider({ children }: { children: ReactNode }) {
10
+ const sessionRecords = useCollection<Session>(sessions); const userRecords = useCollection<User>(users); const membershipRecords = useCollection<Membership>(memberships); const organizationRecords = useCollection<Organization>(organizations);
11
+ const [sessionId, setSessionId] = useState<string | null>(null);
12
+ useEffect(() => setSessionId(sessionStorage.getItem(SESSION_KEY)), []);
13
+ const session = sessionRecords.data.find(item => item.id === sessionId && Date.parse(item.expires_at) > Date.now());
14
+ const user = userRecords.data.find(item => item.id === session?.user);
15
+ const membership = membershipRecords.data.find(item => item.user === user?.id);
16
+ const organization = organizationRecords.data.find(item => item.id === membership?.organization);
17
+ const loading = sessionRecords.loading || userRecords.loading || membershipRecords.loading || organizationRecords.loading;
18
+ const establish = (next: Session) => { sessionStorage.setItem(SESSION_KEY, next.id); setSessionId(next.id); };
19
+ const value = useMemo<AuthValue>(() => ({ loading, authenticated: Boolean(session && user && membership && organization), session, user, membership, organization,
20
+ signUp: async input => { await db.auth.signUp({ email: input.email, password: input.password, display_name: input.name }); establish(await createAccount(input)); },
21
+ signIn: async (email, password) => { await db.auth.signIn({ email, password }); establish(await createSession(email)); },
22
+ signOut: async () => { if (session) await sessions.delete(session.id); await db.auth.signOut(); sessionStorage.removeItem(SESSION_KEY); setSessionId(null); },
23
+ }), [loading, session, user, membership, organization]);
24
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
25
+ }
26
+
27
+ export function useAuth() { const value = useContext(AuthContext); if (!value) throw new Error('useAuth must be used inside AuthProvider'); return value; }
@@ -0,0 +1,87 @@
1
+ import { configureDevelopmentRuntimeBridge, createFeltDB } from '@feltdb/core';
2
+
3
+ configureDevelopmentRuntimeBridge({
4
+ sessionId: import.meta.env.VITE_FELTDB_DEV_SESSION_ID,
5
+ workspaceId: import.meta.env.VITE_FELTDB_WORKSPACE_ID,
6
+ namespace: import.meta.env.VITE_FELTDB_NAMESPACE,
7
+ runtime: import.meta.env.VITE_FELTDB_RUNTIME,
8
+ authorityUrl: import.meta.env.VITE_FELTDB_AUTHORITY_URL,
9
+ bridgeUrl: import.meta.env.VITE_FELTDB_DEV_BRIDGE_URL,
10
+ applicationUrl: import.meta.env.VITE_FELTDB_APPLICATION_URL,
11
+ });
12
+
13
+ export const managedRuntime = Boolean(import.meta.env.VITE_FELTDB_MANAGED_URL);
14
+ export const db = createFeltDB(managedRuntime ? {
15
+ namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || 'feltdb-starter',
16
+ server: {
17
+ url: import.meta.env.VITE_FELTDB_MANAGED_URL,
18
+ applicationId: import.meta.env.VITE_FELTDB_MANAGED_APPLICATION_ID,
19
+ environment: import.meta.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
20
+ },
21
+ } : { namespace: import.meta.env.VITE_FELTDB_NAMESPACE || 'feltdb-starter', browser: true });
22
+
23
+ export interface User { id: string; email: string; name: string; created_at: string }
24
+ export interface Organization { id: string; name: string; created_at: string }
25
+ export interface Membership { id: string; user: string; organization: string; role: 'owner' | 'member' | 'viewer'; created_at: string }
26
+ export interface Session { id: string; user: string; expires_at: string; created_at: string }
27
+ export interface Project { id: string; organization: string; name: string; description: string; owner_id: string; created_at: string; updated_at: string }
28
+ export interface Invitation { id: string; organization: string; user: string; email: string; role: string; status: string; created_at: string; updated_at: string }
29
+ export interface AuditEvent { id: string; organization: string; user: string; action: string; resource_type: string; resource_id: string; details: string; created_at: string }
30
+ export interface Team { id: string; organization: string; name: string; description: string; owner: string; created_at: string; updated_at: string }
31
+ export interface TeamMembership { id: string; organization: string; team: string; user: string; role: string; created_at: string; updated_at: string }
32
+
33
+ export const users = db.collection<User>('User');
34
+ export const organizations = db.collection<Organization>('Organization');
35
+ export const memberships = db.collection<Membership>('Membership');
36
+ export const sessions = db.collection<Session>('Session');
37
+ export const projects = db.collection<Project>('Project');
38
+ export const invitations = db.collection<Invitation>('Invitation');
39
+ export const auditEvents = db.collection<AuditEvent>('AuditEvent');
40
+ export const teams = db.collection<Team>('Team');
41
+ export const teamMemberships = db.collection<TeamMembership>('TeamMembership');
42
+
43
+ const makeId = (prefix: string) => `${prefix}_${crypto.randomUUID()}`;
44
+
45
+ export async function createAccount(input: { name: string; email: string; organization: string }) {
46
+ const email = input.email.trim().toLowerCase();
47
+ if ((await users.find({ email })).length) throw new Error('An account already exists for that email.');
48
+ const now = new Date().toISOString();
49
+ const user: User = { id: makeId('user'), email, name: input.name.trim(), created_at: now };
50
+ const organization: Organization = { id: makeId('org'), name: input.organization.trim(), created_at: now };
51
+ const membership: Membership = { id: makeId('membership'), user: user.id, organization: organization.id, role: 'owner', created_at: now };
52
+ const session: Session = { id: makeId('session'), user: user.id, created_at: now, expires_at: new Date(Date.now() + 86_400_000).toISOString() };
53
+ const event: AuditEvent = { id: makeId('event'), organization: organization.id, user: user.id, action: 'account.created', resource_type: 'User', resource_id: user.id, details: 'Initial organization and owner membership created', created_at: now };
54
+ await db.transaction(async transaction => {
55
+ transaction.collection<User>('User').insert(user, user.id, { requireAbsent: true });
56
+ transaction.collection<Organization>('Organization').insert(organization, organization.id, { requireAbsent: true });
57
+ transaction.collection<Membership>('Membership').insert(membership, membership.id, { requireAbsent: true });
58
+ transaction.collection<Session>('Session').insert(session, session.id, { requireAbsent: true });
59
+ transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
60
+ }, { transactionId: makeId('signup') });
61
+ return session;
62
+ }
63
+
64
+ export async function createSession(emailInput: string) {
65
+ const user = (await users.find({ email: emailInput.trim().toLowerCase() }))[0];
66
+ if (!user) throw new Error('Account not found.');
67
+ const now = new Date().toISOString();
68
+ const session: Session = { id: makeId('session'), user: user.id, created_at: now, expires_at: new Date(Date.now() + 86_400_000).toISOString() };
69
+ await sessions.insert(session, session.id);
70
+ return session;
71
+ }
72
+
73
+ export async function createProject(input: { organization: string; owner: string; name: string }) {
74
+ const now = new Date().toISOString();
75
+ const project: Project = { id: makeId('project'), organization: input.organization, owner_id: input.owner, name: input.name.trim(), description: '', created_at: now, updated_at: now };
76
+ const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.owner, action: 'project.created', resource_type: 'Project', resource_id: project.id, details: project.name, created_at: now };
77
+ await db.transaction(async transaction => {
78
+ transaction.collection<Project>('Project').insert(project, project.id, { requireAbsent: true });
79
+ transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
80
+ });
81
+ }
82
+
83
+ export async function recordAgentActivity(input: { organization: string; user: string; assistant: string; subject: string }) {
84
+ const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.user, action: 'agent.summary', resource_type: 'AgentExecution', resource_id: input.subject, details: `${input.assistant} summarized authorized application state`, created_at: new Date().toISOString() };
85
+ await auditEvents.insert(event, event.id);
86
+ return event;
87
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Application entry point
3
+ */
4
+
5
+ import React from 'react';
6
+ import ReactDOM from 'react-dom/client';
7
+ import { App } from './App';
8
+
9
+ const root = ReactDOM.createRoot(document.getElementById('root')!);
10
+ root.render(
11
+ <React.StrictMode>
12
+ <App />
13
+ </React.StrictMode>
14
+ );
@@ -0,0 +1,7 @@
1
+ import { useCollection } from '@feltdb/core/react';
2
+ import { auditEvents } from '../feltdb';
3
+
4
+ export function Activity() {
5
+ const state = useCollection(auditEvents);
6
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Durable history</span><h1>Activity</h1><p>Workflow and agent actions land in the same application state.</p></div><span className="count">{state.data.length}</span></div><article className="panel records">{state.data.length ? state.data.slice().reverse().map(event => <div className="record" key={event.id}><span className="record-mark">↗</span><div><strong>{event.action}</strong><small>{event.details} · {new Date(event.created_at).toLocaleString()}</small></div></div>) : <div className="empty"><strong>No activity yet</strong><span>Create a project or run an assistant.</span></div>}</article></section>;
7
+ }
@@ -0,0 +1,15 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { Capability } from '@feltdb/core';
3
+ import { useCapability, useCollection } from '@feltdb/core/react';
4
+ import { projects } from '../feltdb';
5
+ import { useAuth } from '../context/AuthContext';
6
+ import { runProjectAssistant } from '../../agents/project-assistant';
7
+ import { runActivityAssistant } from '../../agents/activity-assistant';
8
+
9
+ export function Agents() {
10
+ const { user, organization } = useAuth(); const projectState = useCollection(projects); const [projectId, setProjectId] = useState('');
11
+ const projectCapability = useMemo<Capability>(() => ({ id: 'run-project-assistant', name: 'Run Project Assistant', execute: () => runProjectAssistant({ projectId, organization: organization!.id, user: user!.id }) }), [projectId, organization, user]);
12
+ const activityCapability = useMemo<Capability>(() => ({ id: 'run-activity-assistant', name: 'Run Activity Assistant', execute: () => runActivityAssistant({ organization: organization!.id, user: user!.id }) }), [organization, user]);
13
+ const projectAgent = useCapability<[], { title: string; summary: string; activityCount: number }>(projectCapability); const activityAgent = useCapability<[], { summary: string; total: number }>(activityCapability);
14
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Authorized agents</span><h1>Assistants</h1><p>Each assistant invokes bounded capabilities over the state you can already access.</p></div></div><div className="agent-grid"><article className="panel"><span className="agent-state">Project Assistant · project-summary</span><h2>Explain a project</h2><select value={projectId} onChange={event => setProjectId(event.target.value)}><option value="">Select a project</option>{projectState.data.map(project => <option key={project.id} value={project.id}>{project.name}</option>)}</select><button className="primary" disabled={!projectId || projectAgent.loading} onClick={() => void projectAgent.execute()}>{projectAgent.loading ? 'Running…' : 'Summarize project'}</button>{projectAgent.result && <div className="result"><strong>{projectAgent.result.title}</strong><p>{projectAgent.result.summary}</p><small>{projectAgent.result.activityCount} related activity events</small></div>}</article><article className="panel"><span className="agent-state">Activity Assistant · activity-summary</span><h2>Explain recent activity</h2><p className="muted">Reads authorized AuditEvent state and records its own execution.</p><button className="primary" disabled={activityAgent.loading} onClick={() => void activityAgent.execute()}>{activityAgent.loading ? 'Running…' : 'Summarize activity'}</button>{activityAgent.result && <div className="result"><p>{activityAgent.result.summary}</p></div>}</article></div></section>;
15
+ }
@@ -0,0 +1,17 @@
1
+ import { useState } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { auditEvents, invitations, projects, teams } from '../feltdb';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { Projects } from './Projects';
6
+ import { Invitations } from './Invitations';
7
+ import { Activity } from './Activity';
8
+ import { Agents } from './Agents';
9
+
10
+ type View = 'overview' | 'projects' | 'invitations' | 'activity' | 'agents';
11
+
12
+ export function Dashboard() {
13
+ const { user, organization, signOut } = useAuth(); const [view, setView] = useState<View>('overview');
14
+ const projectState = useCollection(projects); const teamState = useCollection(teams); const invitationState = useCollection(invitations); const activityState = useCollection(auditEvents);
15
+ const stats = [{ label: 'Projects', value: projectState.data.length }, { label: 'Teams', value: teamState.data.length }, { label: 'Invitations', value: invitationState.data.filter(item => item.status === 'pending').length }, { label: 'Activity', value: activityState.data.length }];
16
+ return <main className="dashboard"><header><a className="brand" href="https://feltdb.com">FeltDB</a><nav>{(['overview', 'projects', 'invitations', 'activity', 'agents'] as View[]).map(item => <button className={view === item ? 'nav-active' : 'quiet'} onClick={() => setView(item)} key={item}>{item}</button>)}</nav><div><span>{user?.name}</span><button className="quiet" onClick={() => void signOut()}>Sign out</button></div></header>{view === 'overview' && <><section className="hero"><div><span className="eyebrow">{organization?.name}</span><h1>Welcome, {user?.name.split(' ')[0]}.</h1><p>State, workflows, and agents share one authorization boundary.</p></div><code>member · {organization?.id.slice(0, 12)}…</code></section><section className="stats">{stats.map(stat => <article key={stat.label}><span>{stat.label}</span><strong>{stat.value}</strong></article>)}</section><section className="grid"><article className="panel"><span className="eyebrow">Start here</span><h2>One application, visible in Studio</h2><div className="journey"><button onClick={() => setView('projects')}>1 <span>Create a project<small>Atomic Project + AuditEvent workflow</small></span></button><button onClick={() => setView('invitations')}>2 <span>Invite a member<small>Identity and relational authorization</small></span></button><button onClick={() => setView('agents')}>3 <span>Run an assistant<small>Bounded capabilities over authorized state</small></span></button></div></article><aside className="panel"><span className="eyebrow">Runtime</span><h2>One state boundary</h2><div className="stack"><div><i className="online" /><span>Application connected</span></div><div><i /><span>Authorization in FeltDB</span></div><div><i /><span>Workflows and agents persisted</span></div></div><p className="hint">Run <code>npm run feltdb:studio</code> to inspect the same records.</p></aside></section></>}{view === 'projects' && <Projects />}{view === 'invitations' && <Invitations />}{view === 'activity' && <Activity />}{view === 'agents' && <Agents />}</main>;
17
+ }
@@ -0,0 +1,11 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { invitations } from '../feltdb';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { runAcceptInvitationWorkflow, runInviteMemberWorkflow } from '../../workflows/invitation';
6
+
7
+ export function Invitations() {
8
+ const { user, organization } = useAuth(); const state = useCollection(invitations); const [message, setMessage] = useState('');
9
+ async function invite(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (!user || !organization) return; const form = event.currentTarget; setMessage(''); try { await runInviteMemberWorkflow({ organization: organization.id, actor: user.id, email: String(new FormData(form).get('email')), role: 'member' }); form.reset(); } catch (error) { setMessage(error instanceof Error ? error.message : 'Invitation failed.'); } }
10
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Invitation workflow</span><h1>Invitations</h1><p>Invitation visibility combines <code>self(user) | member</code>.</p></div><span className="count">{state.data.filter(item => item.status === 'pending').length}</span></div><article className="panel"><form className="project-form" onSubmit={invite}><input name="email" type="email" required placeholder="Existing user email" aria-label="Invite email" /><button>Invite member</button></form>{message && <p className="error">{message}</p>}<div className="records">{state.data.length ? state.data.slice().reverse().map(invitation => <div className="record spread" key={invitation.id}><div><strong>{invitation.email}</strong><small>{invitation.role} · {invitation.status}</small></div>{invitation.user === user?.id && invitation.status === 'pending' && <button onClick={() => void runAcceptInvitationWorkflow(invitation, user.id)}>Accept</button>}</div>) : <div className="empty"><strong>No invitations yet</strong><span>For this starter, invite another user who has already signed up.</span></div>}</div></article></section>;
11
+ }
@@ -0,0 +1,11 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { projects } from '../feltdb';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { runProjectCreatedWorkflow } from '../../workflows/project-created';
6
+
7
+ export function Projects() {
8
+ const { user, organization } = useAuth(); const state = useCollection(projects); const [busy, setBusy] = useState(false);
9
+ async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (!user || !organization) return; const form = event.currentTarget; setBusy(true); await runProjectCreatedWorkflow({ organization: organization.id, owner: user.id, name: String(new FormData(form).get('name')) }); form.reset(); setBusy(false); }
10
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Project workflow</span><h1>Projects</h1><p>Creating a project atomically persists both Project and AuditEvent state.</p></div><span className="count">{state.data.length}</span></div><article className="panel"><form className="project-form" onSubmit={submit}><input name="name" required placeholder="A new project" aria-label="Project name" /><button disabled={busy}>{busy ? 'Running workflow…' : 'Create project'}</button></form><div className="records">{state.data.length ? state.data.slice().reverse().map(project => <div className="record" key={project.id}><span className="record-mark">{project.name.charAt(0).toUpperCase()}</span><div><strong>{project.name}</strong><small>ProjectCreated workflow · {new Date(project.created_at).toLocaleDateString()}</small></div></div>) : <div className="empty"><strong>No projects yet</strong><span>Create one to produce your first workflow state.</span></div>}</div></article></section>;
11
+ }
@@ -0,0 +1,8 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ export function SignIn() {
5
+ const { signIn } = useAuth(); const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
6
+ async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); setError(''); const form = new FormData(event.currentTarget); try { await signIn(String(form.get('email')), String(form.get('password'))); } catch { setError('Those credentials could not be verified.'); setBusy(false); } }
7
+ return <><span className="eyebrow">Welcome back</span><h2>Sign in to FeltDB.</h2><p className="muted">Managed apps exchange credentials for a short-lived, application-scoped actor session.</p><form onSubmit={submit}><label>Email<input name="email" type="email" autoComplete="email" required placeholder="ada@example.com" /></label><label>Password<input name="password" type="password" autoComplete="current-password" required /></label>{error && <p className="error">{error}</p>}<button disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button></form><p className="switch">New to this app? <a href="?view=signup">Create an account</a></p></>;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ export function SignUp() {
5
+ const { signUp } = useAuth(); const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
6
+ async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); setError(''); const form = new FormData(event.currentTarget); try { await signUp({ name: String(form.get('name')), email: String(form.get('email')), password: String(form.get('password')), organization: String(form.get('organization')) }); } catch { setError('We could not create your account. Check the details and try again.'); setBusy(false); } }
7
+ return <><span className="eyebrow">Create your workspace</span><h2>Start with durable state.</h2><p className="muted">Your actor, account, and organization are established at the FeltDB boundary.</p><form onSubmit={submit}><label>Name<input name="name" autoComplete="name" required placeholder="Ada Lovelace" /></label><label>Work email<input name="email" type="email" autoComplete="email" required placeholder="ada@example.com" /></label><label>Password<input name="password" type="password" autoComplete="new-password" minLength={8} required /></label><label>Organization<input name="organization" required placeholder="Analytical Engines" /></label>{error && <p className="error">{error}</p>}<button disabled={busy}>{busy ? 'Creating…' : 'Create account'}</button></form><p className="switch">Already have an account? <a href="?view=signin">Sign in</a></p></>;
8
+ }
@@ -0,0 +1,21 @@
1
+ .dashboard header nav { display: flex; gap: .15rem; }
2
+ .dashboard header nav button { text-transform: capitalize; }
3
+ .nav-active { background: #dff0e5; color: #17633b; border-radius: 6px; padding: .5rem .7rem; }
4
+ .page { padding: clamp(3rem, 7vw, 6rem) 0; }
5
+ .page-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 2rem; }
6
+ .page-heading h1 { font: 500 clamp(3rem, 6vw, 5rem)/1 Georgia, serif; letter-spacing: -.055em; margin: .6rem 0; }
7
+ .page-heading p { color: #68746d; }
8
+ .count { font: 500 3rem Georgia, serif; color: #268453; }
9
+ .journey { display: grid; margin-top: 1.5rem; }
10
+ .journey button { display: flex; gap: 1rem; text-align: left; background: transparent; border-top: 1px solid #dedfd9; padding: 1.1rem .2rem; color: #268453; font-weight: 800; }
11
+ .journey button span { display: grid; gap: .25rem; color: #102019; }
12
+ .journey small { font-weight: 400; }
13
+ .spread { justify-content: space-between; }
14
+ .spread button { background: #dff0e5; color: #17633b; border-radius: 6px; padding: .55rem .8rem; }
15
+ .agent-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
16
+ .agent-grid select { width: 100%; padding: .85rem; border: 1px solid #ccd2cd; border-radius: 8px; background: #fff; margin: 1rem 0; }
17
+ .primary { display: block; width: 100%; padding: .85rem; border-radius: 8px; background: #102019; color: #fff; font-weight: 700; }
18
+ .agent-state { font: 700 .68rem ui-monospace, monospace; color: #268453; text-transform: uppercase; }
19
+ .result { margin-top: 1rem; padding: 1rem; border-radius: 8px; background: #eef3ee; line-height: 1.5; }
20
+ @media (max-width: 900px) { .dashboard header nav { display: none; } }
21
+ @media (max-width: 800px) { .agent-grid { grid-template-columns: 1fr; } }
@@ -0,0 +1 @@
1
+ :root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#102019;background:#f4f3ed;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit}button,input{font:inherit}button{border:0;cursor:pointer}.brand{font-weight:800;font-size:1.2rem;text-decoration:none;letter-spacing:-.04em}.auth-layout{min-height:100vh;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(380px,.75fr)}.intro{padding:clamp(2rem,6vw,6rem);background:#102019;color:#f8f7f1;display:flex;flex-direction:column;justify-content:space-between}.intro h1{max-width:730px;font:500 clamp(3rem,6vw,6.5rem)/.98 Georgia,serif;letter-spacing:-.055em;margin:.8rem 0 1.5rem}.intro p{max-width:590px;color:#bdc9c1;font-size:1.08rem;line-height:1.65}.flow{display:flex;gap:1rem;color:#9eb0a5;font:600 .73rem ui-monospace,monospace;text-transform:uppercase;letter-spacing:.12em}.flow i{color:#49c17d}.auth-card{background:#fbfaf5;padding:clamp(2rem,6vw,6rem);display:flex;flex-direction:column;justify-content:center}h2{font:500 2rem/1.1 Georgia,serif;letter-spacing:-.035em;margin:.7rem 0}.muted,.switch{color:#68746d;line-height:1.55}form{display:grid;gap:1rem;margin:2rem 0 1.5rem}label{display:grid;gap:.45rem;font-weight:650;font-size:.8rem}input{width:100%;border:1px solid #ccd2cd;border-radius:8px;padding:.85rem 1rem;background:#fff;color:#102019}input:focus{outline:2px solid #49c17d;outline-offset:1px}form button,.project-form button{border-radius:8px;padding:.9rem 1.1rem;background:#102019;color:#fff;font-weight:700}button:disabled{opacity:.55}.eyebrow{color:#268453;font:700 .7rem ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase}.intro .eyebrow{color:#64dd98}.error{color:#a42c2c;font-size:.85rem}.switch{font-size:.85rem}.center{min-height:100vh;display:grid;place-content:center;text-align:center;color:#68746d}.loader{width:24px;height:24px;margin:auto;border:2px solid #cbd2cc;border-top-color:#268453;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.dashboard{min-height:100vh;padding:0 clamp(1.25rem,5vw,5rem) 5rem}.dashboard header{height:80px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d5d7d1}.dashboard header>div{display:flex;gap:1.2rem;align-items:center;font-size:.85rem}.quiet{background:transparent;color:#68746d;padding:.5rem}.hero{padding:clamp(3rem,7vw,6.5rem) 0 3rem;display:flex;justify-content:space-between;align-items:end;gap:2rem}.hero h1{font:500 clamp(3rem,7vw,6rem)/1 Georgia,serif;letter-spacing:-.055em;margin:.7rem 0 1rem}.hero p{color:#68746d}code{font:.72rem ui-monospace,monospace;color:#26784e}.stats{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid #d5d7d1;border-radius:12px;overflow:hidden;background:#faf9f4}.stats article{padding:1.3rem;border-right:1px solid #d5d7d1;display:grid;gap:.45rem}.stats article:last-child{border:0}.stats span{color:#68746d;font-size:.75rem}.stats strong{font:500 2rem Georgia,serif}.grid{display:grid;grid-template-columns:1.6fr .8fr;gap:1rem;margin-top:1rem}.panel{background:#faf9f4;border:1px solid #d5d7d1;border-radius:12px;padding:1.5rem}.panel-title{display:flex;align-items:start;justify-content:space-between}.panel-title>span{color:#68746d;font-size:.75rem}.project-form{grid-template-columns:1fr auto;margin:1.4rem 0}.records{display:grid;gap:.6rem}.record{padding:.8rem;border-top:1px solid #e1e1db;display:flex;align-items:center;gap:.8rem}.record-mark{width:36px;height:36px;border-radius:8px;display:grid;place-items:center;background:#dff0e5;color:#26784e;font-weight:800}.record div{display:grid;gap:.2rem}small{color:#7b857f}.empty{min-height:110px;display:grid;place-content:center;text-align:center;gap:.35rem;color:#68746d}.empty strong{color:#102019}.stack{display:grid;gap:1rem;margin:2rem 0}.stack div{display:flex;gap:.7rem;font-size:.85rem}.stack i{width:8px;height:8px;margin-top:.3rem;border-radius:50%;background:#9aa49d}.stack .online{background:#39ad6b;box-shadow:0 0 0 4px #dff0e5}.hint{color:#68746d;font-size:.85rem;line-height:1.6}@media(max-width:800px){.auth-layout,.grid{grid-template-columns:1fr}.intro{min-height:52vh;gap:3rem}.stats{grid-template-columns:repeat(2,1fr)}.stats article:nth-child(2){border-right:0}.hero{align-items:start;flex-direction:column}}
@@ -0,0 +1 @@
1
+ export { runProjectAssistant as runAgentAssistedSummaryWorkflow } from '../agents/project-assistant';
@@ -0,0 +1,30 @@
1
+ import { auditEvents, db, invitations, memberships, users, type AuditEvent, type Invitation, type Membership } from '../src/feltdb';
2
+
3
+ const makeId = (prefix: string) => `${prefix}_${crypto.randomUUID()}`;
4
+
5
+ export async function runInviteMemberWorkflow(input: { organization: string; actor: string; email: string; role: string }) {
6
+ const invited = (await users.find({ email: input.email.trim().toLowerCase() }))[0];
7
+ if (!invited) throw new Error('Invite an existing application user in this starter.');
8
+ const now = new Date().toISOString();
9
+ const invitation: Invitation = { id: makeId('invitation'), organization: input.organization, user: invited.id, email: invited.email, role: input.role, status: 'pending', created_at: now, updated_at: now };
10
+ const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.actor, action: 'invitation.created', resource_type: 'Invitation', resource_id: invitation.id, details: invited.email, created_at: now };
11
+ await db.transaction(async transaction => {
12
+ transaction.collection<Invitation>('Invitation').insert(invitation, invitation.id, { requireAbsent: true });
13
+ transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
14
+ });
15
+ return invitation;
16
+ }
17
+
18
+ export async function runAcceptInvitationWorkflow(invitation: Invitation, userId: string) {
19
+ if (invitation.user !== userId || invitation.status !== 'pending') throw new Error('This invitation cannot be accepted by the current identity.');
20
+ const now = new Date().toISOString();
21
+ const membership: Membership = { id: makeId('membership'), user: userId, organization: invitation.organization, role: invitation.role as Membership['role'], created_at: now };
22
+ const accepted = { ...invitation, status: 'accepted', updated_at: now };
23
+ const event: AuditEvent = { id: makeId('event'), organization: invitation.organization, user: userId, action: 'invitation.accepted', resource_type: 'Invitation', resource_id: invitation.id, details: membership.role, created_at: now };
24
+ await db.transaction(async transaction => {
25
+ transaction.collection<Invitation>('Invitation').insert(accepted, invitation.id);
26
+ transaction.collection<Membership>('Membership').insert(membership, membership.id, { requireAbsent: true });
27
+ transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
28
+ });
29
+ return membership;
30
+ }
@@ -0,0 +1,2 @@
1
+ // Project and AuditEvent are committed atomically by the canonical operation.
2
+ export { createProject as runProjectCreatedWorkflow } from '../src/feltdb';
package/dist/db.d.ts CHANGED
@@ -29,7 +29,9 @@ interface FeltDBBaseOptions {
29
29
  export type FeltDBOptions = FeltDBBaseOptions & ({
30
30
  server: {
31
31
  url: string;
32
- token: string;
32
+ token?: string;
33
+ applicationId?: string;
34
+ environment?: string;
33
35
  };
34
36
  memory?: never;
35
37
  path?: never;
@@ -250,6 +252,7 @@ export declare class StateFirstDB {
250
252
  private capabilityWorkers;
251
253
  private observation?;
252
254
  private closeDevelopmentBridge?;
255
+ private localActorSession?;
253
256
  readonly workloads: WorkloadClient;
254
257
  readonly artifacts: ArtifactClient;
255
258
  readonly bundle: BundleClient;
@@ -259,7 +262,52 @@ export declare class StateFirstDB {
259
262
  readonly workers: WorkerClient;
260
263
  readonly workerPools: WorkerPoolClient;
261
264
  readonly mesh: MeshClient;
265
+ readonly workflows: {
266
+ run: (name: string, input?: unknown, options?: {
267
+ idempotencyKey?: string;
268
+ }) => Promise<unknown>;
269
+ };
270
+ readonly agents: {
271
+ run: (name: string, input?: unknown, options?: {
272
+ idempotencyKey?: string;
273
+ goal?: string;
274
+ }) => Promise<unknown>;
275
+ };
276
+ readonly capabilities: {
277
+ run: <T = unknown>(name: string, input?: unknown, options?: {
278
+ idempotencyKey?: string;
279
+ }) => Promise<T>;
280
+ };
281
+ readonly application: {
282
+ get: () => Promise<Record<string, unknown> | {
283
+ application_id: any;
284
+ environment: string;
285
+ version: any;
286
+ contract_hash: string;
287
+ dsl_version: number;
288
+ snapshot: import("./application-development.js").FeltDBContractSnapshot;
289
+ }>;
290
+ };
291
+ readonly schema: {
292
+ get: () => Promise<Record<string, unknown>>;
293
+ };
294
+ readonly auth: {
295
+ signUp: (input: {
296
+ email: string;
297
+ password: string;
298
+ display_name?: string;
299
+ }) => Promise<any>;
300
+ signIn: (input: {
301
+ email: string;
302
+ password: string;
303
+ }) => Promise<any>;
304
+ signOut: () => Promise<any>;
305
+ session: () => Promise<any>;
306
+ };
262
307
  constructor(jsDb: JsDb, observation?: DevelopmentObservationSink, closeDevelopmentBridge?: () => Promise<void>);
308
+ private applicationRequest;
309
+ private authRequest;
310
+ private localAuthRequest;
263
311
  /**
264
312
  * Detect runtime environment and storage characteristics
265
313
  */