@feltdb/core 0.7.3 → 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.
- package/dist/application-development.d.ts +76 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +137 -0
- package/dist/application-manifest.d.ts +5 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +26 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +45 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/commands.js +466 -105
- package/dist/cli/index.js +1 -1
- package/dist/cli/proposal-client.js +54 -0
- package/dist/cli/publish-engine.js +162 -0
- package/dist/cli/source-sync.js +139 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +30 -6
- package/dist/create/managed-account.js +14 -7
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
- package/dist/create/template/default-project/README.md +57 -0
- package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
- package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
- package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
- package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
- package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
- package/dist/create/template/default-project/feltdb.flow +169 -0
- package/dist/create/template/default-project/src/App.tsx +20 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
- package/dist/create/template/default-project/src/feltdb.ts +87 -0
- package/dist/create/template/default-project/src/index.tsx +14 -0
- package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
- package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
- package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
- package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
- package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
- package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
- package/dist/create/template/default-project/src/styles-application.css +21 -0
- package/dist/create/template/default-project/src/styles.css +1 -0
- package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
- package/dist/create/template/default-project/workflows/invitation.ts +30 -0
- package/dist/create/template/default-project/workflows/project-created.ts +2 -0
- package/dist/db.d.ts +49 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +77 -0
- package/dist/error-codes.d.ts +20 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +25 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +27 -6
- package/dist/http-db.d.ts +15 -2
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +113 -5
- package/dist/index-core.d.ts +3 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +3 -0
- package/dist/proposal.d.ts +85 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +105 -0
- package/dist/studio/app.d.ts +3 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +20 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +3 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/index.d.ts +2 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/{components-Dxuhrv8_.js → components-Bm4sjhcX.js} +335 -5
- package/dist/studio/index.js +168 -157
- package/dist/studio/studio.css +1 -1
- package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
- package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
- package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
- package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +3 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +62 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-B5tnmvSD.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
|
+
}
|
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
|
|
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
|
*/
|