deepspace 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- package/scripts/add-feature.cjs +654 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks/Challenges Feature - Schema
|
|
3
|
+
*
|
|
4
|
+
* A claimable task system demonstrating:
|
|
5
|
+
* - 'unclaimed-or-own' permission for claiming unclaimed items
|
|
6
|
+
* - writableFields for restricting which fields users can update
|
|
7
|
+
* - timestampTrigger for automatic timestamp fields
|
|
8
|
+
* - Admin-only grading workflow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
12
|
+
|
|
13
|
+
export const challengesSchema: CollectionSchema = {
|
|
14
|
+
name: 'challenges',
|
|
15
|
+
columns: [
|
|
16
|
+
{ name: 'title', storage: 'text', interpretation: 'plain', required: true },
|
|
17
|
+
{ name: 'description', storage: 'text', interpretation: 'plain', required: true },
|
|
18
|
+
{ name: 'difficulty', storage: 'text', interpretation: 'plain', default: 'medium' },
|
|
19
|
+
{ name: 'points', storage: 'number', interpretation: 'plain', default: 10 },
|
|
20
|
+
// Claim fields
|
|
21
|
+
{ name: 'claimedById', storage: 'text', interpretation: 'plain' },
|
|
22
|
+
{ name: 'claimedAt', storage: 'text', interpretation: 'plain', timestampTrigger: { field: 'claimedById' } },
|
|
23
|
+
// Submission fields
|
|
24
|
+
{ name: 'submitted', storage: 'number', interpretation: { kind: 'boolean' }, default: false },
|
|
25
|
+
{ name: 'submissionUrl', storage: 'text', interpretation: 'plain' },
|
|
26
|
+
{ name: 'submissionNotes', storage: 'text', interpretation: 'plain' },
|
|
27
|
+
{ name: 'submittedAt', storage: 'text', interpretation: 'plain', timestampTrigger: { field: 'submitted', value: true } },
|
|
28
|
+
// Grading fields (admin only)
|
|
29
|
+
{ name: 'grade', storage: 'text', interpretation: 'plain' },
|
|
30
|
+
{ name: 'feedback', storage: 'text', interpretation: 'plain' },
|
|
31
|
+
{ name: 'gradedById', storage: 'text', interpretation: 'plain' },
|
|
32
|
+
{ name: 'gradedAt', storage: 'text', interpretation: 'plain', timestampTrigger: { field: 'grade' } },
|
|
33
|
+
// Creator
|
|
34
|
+
{ name: 'createdById', storage: 'text', interpretation: 'plain', required: true, userBound: true, immutable: true },
|
|
35
|
+
],
|
|
36
|
+
ownerField: 'claimedById',
|
|
37
|
+
permissions: {
|
|
38
|
+
viewer: {
|
|
39
|
+
read: true,
|
|
40
|
+
create: false,
|
|
41
|
+
update: false,
|
|
42
|
+
delete: false,
|
|
43
|
+
},
|
|
44
|
+
member: {
|
|
45
|
+
read: true,
|
|
46
|
+
create: true,
|
|
47
|
+
update: 'unclaimed-or-own',
|
|
48
|
+
delete: 'own',
|
|
49
|
+
writableFields: [
|
|
50
|
+
'claimedById',
|
|
51
|
+
'submitted', 'submissionUrl', 'submissionNotes',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
55
|
+
},
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "testing",
|
|
3
|
+
"name": "Testing",
|
|
4
|
+
"category": "data",
|
|
5
|
+
"description": "E2E test page exercising RBAC and CRUD against RecordRoom",
|
|
6
|
+
"details": "Test infrastructure page used by Playwright e2e tests. Displays current role, creates/reads/updates/deletes test items, and verifies RBAC permission enforcement. Also installs cpu-burn and subrequest-burn server actions that intentionally exceed the dispatch worker's per-invocation limits — used by the e2e dispatch-limits spec to assert denial-of-wallet protection fires.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"src": "src/TestPage.tsx",
|
|
10
|
+
"dest": "src/pages/test.tsx"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"src": "src/test-schema.ts",
|
|
14
|
+
"dest": "src/schemas/test-schema.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"src": "src/cpu-burn-action.ts",
|
|
18
|
+
"dest": "src/actions/cpu-burn.ts"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "src/subrequest-burn-action.ts",
|
|
22
|
+
"dest": "src/actions/subrequest-burn.ts"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"schema": {
|
|
26
|
+
"exportName": "testItemsSchema",
|
|
27
|
+
"importPath": "./schemas/test-schema",
|
|
28
|
+
"spreadOperator": false
|
|
29
|
+
},
|
|
30
|
+
"actions": [
|
|
31
|
+
{
|
|
32
|
+
"exportName": "cpuBurnAction",
|
|
33
|
+
"registerAs": "cpu-burn",
|
|
34
|
+
"importPath": "./cpu-burn"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"exportName": "subrequestBurnAction",
|
|
38
|
+
"registerAs": "subrequest-burn",
|
|
39
|
+
"importPath": "./subrequest-burn"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"route": {
|
|
43
|
+
"path": "/test",
|
|
44
|
+
"protected": false
|
|
45
|
+
},
|
|
46
|
+
"requires": [],
|
|
47
|
+
"instructions": [],
|
|
48
|
+
"patterns": []
|
|
49
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestPage — exercises RBAC and CRUD against the RecordRoom.
|
|
3
|
+
*
|
|
4
|
+
* Used by Playwright e2e tests to verify real data flow.
|
|
5
|
+
* Displays current role, creates/reads/updates/deletes items.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import { useQuery } from 'deepspace'
|
|
10
|
+
import { useMutations } from 'deepspace'
|
|
11
|
+
import { useUser } from 'deepspace'
|
|
12
|
+
import { useRecordContext } from 'deepspace'
|
|
13
|
+
import { useAuth } from 'deepspace'
|
|
14
|
+
|
|
15
|
+
interface ItemData {
|
|
16
|
+
title: string
|
|
17
|
+
description: string
|
|
18
|
+
status: string
|
|
19
|
+
createdBy: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function TestPage() {
|
|
23
|
+
const { isSignedIn } = useAuth()
|
|
24
|
+
const { user } = useUser()
|
|
25
|
+
const { roomRole } = useRecordContext()
|
|
26
|
+
const { records: items, status } = useQuery<ItemData>('test-items')
|
|
27
|
+
const isLoading = status === 'loading'
|
|
28
|
+
const { createConfirmed, putConfirmed, removeConfirmed } = useMutations<ItemData>('test-items')
|
|
29
|
+
|
|
30
|
+
const [lastResult, setLastResult] = useState<string>('')
|
|
31
|
+
const [lastError, setLastError] = useState<string>('')
|
|
32
|
+
|
|
33
|
+
const clearStatus = () => { setLastResult(''); setLastError('') }
|
|
34
|
+
|
|
35
|
+
const tryCreate = async () => {
|
|
36
|
+
clearStatus()
|
|
37
|
+
try {
|
|
38
|
+
const id = await createConfirmed({ title: 'Test Item', description: 'Created by test', status: 'draft', createdBy: user?.id ?? 'anonymous' })
|
|
39
|
+
setLastResult(`created:${id}`)
|
|
40
|
+
} catch (e: any) {
|
|
41
|
+
setLastError(e.message ?? String(e))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tryCreatePublished = async () => {
|
|
46
|
+
clearStatus()
|
|
47
|
+
try {
|
|
48
|
+
const id = await createConfirmed({ title: 'Public Item', description: 'Visible to anonymous', status: 'published', createdBy: user?.id ?? 'anonymous' })
|
|
49
|
+
setLastResult(`created:${id}`)
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
setLastError(e.message ?? String(e))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tryUpdate = async (recordId: string) => {
|
|
56
|
+
clearStatus()
|
|
57
|
+
try {
|
|
58
|
+
await putConfirmed(recordId, { title: 'Updated Title', description: 'Updated by test', status: 'draft', createdBy: user?.id ?? 'anonymous' })
|
|
59
|
+
setLastResult(`updated:${recordId}`)
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
setLastError(e.message ?? String(e))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tryDelete = async (recordId: string) => {
|
|
66
|
+
clearStatus()
|
|
67
|
+
try {
|
|
68
|
+
await removeConfirmed(recordId)
|
|
69
|
+
setLastResult(`deleted:${recordId}`)
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
setLastError(e.message ?? String(e))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="mx-auto max-w-3xl p-8" data-testid="test-page">
|
|
77
|
+
<div className="mb-8 rounded-lg border border-border bg-card p-4">
|
|
78
|
+
<h2 className="text-lg font-semibold text-foreground mb-2">Connection Status</h2>
|
|
79
|
+
<div className="space-y-1 text-sm">
|
|
80
|
+
<div>Signed in: <span data-testid="test-signed-in">{String(isSignedIn)}</span></div>
|
|
81
|
+
<div>User ID: <span data-testid="test-user-id">{user?.id ?? 'none'}</span></div>
|
|
82
|
+
<div>Role: <span data-testid="test-user-role">{roomRole ?? 'connecting'}</span></div>
|
|
83
|
+
<div>User Name: <span data-testid="test-user-name">{user?.name ?? 'Anonymous'}</span></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="mb-8 rounded-lg border border-border bg-card p-4">
|
|
88
|
+
<h2 className="text-lg font-semibold text-foreground mb-2">Actions</h2>
|
|
89
|
+
<div className="flex flex-wrap gap-2">
|
|
90
|
+
<button data-testid="test-create-item" onClick={tryCreate}
|
|
91
|
+
className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90">
|
|
92
|
+
Create Draft Item
|
|
93
|
+
</button>
|
|
94
|
+
<button data-testid="test-create-published" onClick={tryCreatePublished}
|
|
95
|
+
className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90">
|
|
96
|
+
Create Published Item
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="mt-3 min-h-[2.25rem]">
|
|
100
|
+
{lastResult && (
|
|
101
|
+
<div data-testid="test-last-result" className="rounded bg-success/20 px-3 py-2 text-sm text-success">
|
|
102
|
+
{lastResult}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
{lastError && (
|
|
106
|
+
<div data-testid="test-last-error" className="rounded bg-destructive/20 px-3 py-2 text-sm text-destructive">
|
|
107
|
+
{lastError}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
114
|
+
<h2 className="text-lg font-semibold text-foreground mb-2">
|
|
115
|
+
Items {isLoading ? '(loading...)' : `(${items?.length ?? 0})`}
|
|
116
|
+
</h2>
|
|
117
|
+
<div data-testid="test-items-list" className="space-y-2">
|
|
118
|
+
{items?.map((item) => (
|
|
119
|
+
<div key={item.recordId} data-testid={`test-item-${item.recordId}`}
|
|
120
|
+
className="flex items-center justify-between rounded border border-border px-3 py-2">
|
|
121
|
+
<div className="text-sm">
|
|
122
|
+
<span className="font-medium text-foreground">{item.data.title}</span>
|
|
123
|
+
<span className="ml-2 text-muted-foreground">({item.data.status})</span>
|
|
124
|
+
<span className="ml-2 text-muted-foreground text-xs">by {item.data.createdBy}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="flex gap-1">
|
|
127
|
+
<button data-testid={`test-update-${item.recordId}`} onClick={() => tryUpdate(item.recordId)}
|
|
128
|
+
className="rounded bg-secondary px-2 py-1 text-xs text-secondary-foreground hover:opacity-90">
|
|
129
|
+
Update
|
|
130
|
+
</button>
|
|
131
|
+
<button data-testid={`test-delete-${item.recordId}`} onClick={() => tryDelete(item.recordId)}
|
|
132
|
+
className="rounded bg-destructive/20 px-2 py-1 text-xs text-destructive hover:opacity-90">
|
|
133
|
+
Delete
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
{!isLoading && (!items || items.length === 0) && (
|
|
139
|
+
<div data-testid="test-items-empty" className="text-sm text-muted-foreground">No items</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpu-burn — server action that deliberately exceeds the dispatcher's
|
|
3
|
+
* CPU limit so the e2e `testing.spec.ts` can assert that
|
|
4
|
+
* `DEFAULT_LIMITS.cpuMs` on the dispatch worker actually fires.
|
|
5
|
+
*
|
|
6
|
+
* Runs a tight loop of hash computations until the runtime kills it. In
|
|
7
|
+
* production this would be a denial-of-wallet event; in the testing
|
|
8
|
+
* feature it's the whole point — we want the kill.
|
|
9
|
+
*
|
|
10
|
+
* The action returns only if CF *didn't* enforce the limit, which is
|
|
11
|
+
* itself a test failure signal (hence the misleading `success: true`
|
|
12
|
+
* — the spec asserts on the response status / outcome, not the body).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ActionHandler } from 'deepspace/worker'
|
|
16
|
+
|
|
17
|
+
interface OwnerEnv {
|
|
18
|
+
OWNER_USER_ID?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const cpuBurnAction: ActionHandler<OwnerEnv> = async (ctx) => {
|
|
22
|
+
// Owner-only: the whole point of this action is to deliberately blow
|
|
23
|
+
// the dispatcher's CPU cap, which burns real CF compute. Gate to the
|
|
24
|
+
// app owner so a random authenticated user can't weaponize every
|
|
25
|
+
// deployed app that installed the testing feature.
|
|
26
|
+
const ownerId = ctx.env.OWNER_USER_ID
|
|
27
|
+
if (ownerId && ctx.userId !== ownerId) {
|
|
28
|
+
return { success: false, error: 'Forbidden: owner only' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Keep the loop tight and work-generating so v8 can't eliminate it as
|
|
32
|
+
// dead code. Each iteration hashes a different value so the work
|
|
33
|
+
// isn't constant-foldable.
|
|
34
|
+
const encoder = new TextEncoder()
|
|
35
|
+
let counter = 0n
|
|
36
|
+
const deadline = Date.now() + 15_000 // safety: stop after 15s if CF didn't
|
|
37
|
+
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
const bytes = encoder.encode(`cpu-burn-${counter}`)
|
|
40
|
+
await crypto.subtle.digest('SHA-256', bytes)
|
|
41
|
+
counter += 1n
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If we got here, the dispatcher's limit did NOT fire — which is the
|
|
45
|
+
// thing the spec is checking against. Return a marker so the failure
|
|
46
|
+
// is diagnosable if the response ever makes it back.
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
data: { iterations: counter.toString(), note: 'limit-did-not-fire' },
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subrequest-burn — server action that fires more outbound fetches than
|
|
3
|
+
* the dispatcher's `DEFAULT_LIMITS.subRequests` allows, so the e2e
|
|
4
|
+
* `testing.spec.ts` can assert the subRequests cap actually enforces.
|
|
5
|
+
*
|
|
6
|
+
* Hits the platform-worker health endpoint 60 times (cap is 50). The
|
|
7
|
+
* 51st subrequest should fail inside the user worker.
|
|
8
|
+
*
|
|
9
|
+
* Platform-worker is an internal service binding — no public network
|
|
10
|
+
* egress, no third-party rate-limit headaches.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ActionHandler } from 'deepspace/worker'
|
|
14
|
+
|
|
15
|
+
interface OwnerEnv {
|
|
16
|
+
OWNER_USER_ID?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const subrequestBurnAction: ActionHandler<OwnerEnv> = async (ctx) => {
|
|
20
|
+
// Owner-only — same reasoning as cpu-burn. Without this guard any
|
|
21
|
+
// authenticated user could force our dispatcher to emit 50 outbound
|
|
22
|
+
// fetches on command.
|
|
23
|
+
const ownerId = ctx.env.OWNER_USER_ID
|
|
24
|
+
if (ownerId && ctx.userId !== ownerId) {
|
|
25
|
+
return { success: false, error: 'Forbidden: owner only' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Target is an arbitrary URL — the test passes `https://example.com`
|
|
29
|
+
// but callers could point at an internal endpoint too. Fixed fallback
|
|
30
|
+
// so the spec works even when params aren't supplied.
|
|
31
|
+
const target = (ctx.params.target as string | undefined) ?? 'https://example.com'
|
|
32
|
+
const count = Math.max(1, Math.min(60, (ctx.params.count as number | undefined) ?? 60))
|
|
33
|
+
|
|
34
|
+
let completed = 0
|
|
35
|
+
let lastError: string | null = null
|
|
36
|
+
try {
|
|
37
|
+
for (let i = 0; i < count; i++) {
|
|
38
|
+
const res = await fetch(`${target}?i=${i}`, { method: 'GET' })
|
|
39
|
+
// Drain the body so the subrequest actually completes.
|
|
40
|
+
await res.arrayBuffer()
|
|
41
|
+
completed++
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
lastError = err instanceof Error ? err.message : String(err)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If we completed all N subrequests, the cap didn't fire. Surface
|
|
48
|
+
// that as data so the spec can diagnose.
|
|
49
|
+
return {
|
|
50
|
+
success: completed === count,
|
|
51
|
+
data: { attempted: count, completed, lastError },
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Items Schema — exercises RBAC and CRUD for e2e testing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CollectionSchema } from 'deepspace/worker'
|
|
6
|
+
import { ROLE_ANONYMOUS } from 'deepspace/worker'
|
|
7
|
+
|
|
8
|
+
export const testItemsSchema: CollectionSchema = {
|
|
9
|
+
name: 'test-items',
|
|
10
|
+
columns: [
|
|
11
|
+
{ name: 'title', storage: 'text', interpretation: 'plain' },
|
|
12
|
+
{ name: 'description', storage: 'text', interpretation: 'plain' },
|
|
13
|
+
{ name: 'status', storage: 'text', interpretation: { kind: 'select', options: ['draft', 'published', 'archived'] } },
|
|
14
|
+
{ name: 'createdBy', storage: 'text', interpretation: 'plain' },
|
|
15
|
+
],
|
|
16
|
+
ownerField: 'createdBy',
|
|
17
|
+
permissions: {
|
|
18
|
+
[ROLE_ANONYMOUS]: { read: 'published', create: false, update: false, delete: false },
|
|
19
|
+
member: { read: true, create: true, update: 'own', delete: 'own' },
|
|
20
|
+
admin: { read: true, create: true, update: true, delete: true },
|
|
21
|
+
},
|
|
22
|
+
visibilityField: { field: 'status', value: 'published' },
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "topbar",
|
|
3
|
+
"name": "Topbar",
|
|
4
|
+
"category": "nav",
|
|
5
|
+
"description": "Horizontal top navigation bar with search, user menu, and notifications",
|
|
6
|
+
"details": "Configurable top navigation bar component with logo/brand area, horizontal nav links with active state, optional search input, user avatar dropdown menu, and notification bell with badge count.",
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"src": "src/Topbar.tsx",
|
|
10
|
+
"dest": "src/components/topbar/Topbar.tsx"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"requires": [],
|
|
14
|
+
"instructions": [
|
|
15
|
+
"Import Topbar into your layout and configure navItems, logo, and user props"
|
|
16
|
+
],
|
|
17
|
+
"patterns": []
|
|
18
|
+
}
|