agent-canvas 0.2.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -17
- package/bin/agent-canvas.js +7 -7
- package/bin/agent-canvas.ts +48 -37
- package/bin/lib/commands/daemon.ts +46 -43
- package/bin/lib/commands/exec.ts +100 -100
- package/bin/lib/commands/fetch.ts +15 -15
- package/bin/lib/commands/install.ts +36 -36
- package/bin/lib/commands/instructions.ts +51 -51
- package/bin/lib/commands/push.ts +115 -97
- package/bin/lib/commands/review.ts +190 -0
- package/bin/lib/commands/watch.ts +24 -24
- package/bin/lib/config.ts +12 -12
- package/bin/lib/daemon-lifecycle.ts +47 -51
- package/bin/lib/helpers.ts +82 -76
- package/bin/lib/instructions/app-review.md +88 -0
- package/bin/lib/instructions/component-checklist.md +7 -5
- package/bin/lib/instructions/component-custom.md +20 -14
- package/bin/lib/instructions/component-diff.md +4 -3
- package/bin/lib/instructions/component-item.md +18 -8
- package/bin/lib/instructions/component-markdown.md +4 -2
- package/bin/lib/instructions/component-mermaid.md +12 -6
- package/bin/lib/instructions/component-multichoice.md +6 -2
- package/bin/lib/instructions/component-note.md +1 -1
- package/bin/lib/instructions/component-priority.md +1 -1
- package/bin/lib/instructions/component-rangeinput.md +9 -2
- package/bin/lib/instructions/component-secretinput.md +12 -11
- package/bin/lib/instructions/component-svg.md +62 -16
- package/bin/lib/instructions/component-table.md +6 -6
- package/bin/lib/instructions/component-usefeedback.md +8 -8
- package/bin/lib/instructions/component-userinput.md +6 -2
- package/bin/lib/instructions/flow-feature.md +22 -5
- package/bin/lib/instructions/flow-plan.md +5 -5
- package/bin/lib/instructions/flow-runbook.md +28 -25
- package/bin/lib/instructions/overview.md +27 -18
- package/bin/lib/instructions/theming.md +5 -0
- package/daemon/build.ts +171 -152
- package/daemon/client/App.tsx +1205 -1077
- package/daemon/client/CompareView.tsx +478 -458
- package/daemon/client/FileBrowser.tsx +241 -203
- package/daemon/client/FileViewer.tsx +220 -211
- package/daemon/client/OverviewView.tsx +283 -267
- package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
- package/daemon/client/RevisionSelect.tsx +111 -95
- package/daemon/client/SessionSwitcher.tsx +81 -81
- package/daemon/client/ShareDialog.tsx +211 -212
- package/daemon/client/apiSchemas.ts +78 -0
- package/daemon/client/canvas-runtime.ts +1 -1
- package/daemon/client/clientApi.ts +194 -206
- package/daemon/client/components/SecretInput.tsx +138 -138
- package/daemon/client/components/index.ts +2 -2
- package/daemon/client/domDiff.ts +341 -333
- package/daemon/client/errorMessage.ts +3 -0
- package/daemon/client/exportMarkdown.ts +222 -201
- package/daemon/client/lcs.ts +19 -14
- package/daemon/client/shareCrypto.ts +57 -57
- package/daemon/client/unifiedDiff.ts +660 -649
- package/daemon/dist/annotations-runtime.js +1 -1
- package/daemon/dist/client.css +2 -2
- package/daemon/dist/client.js +77 -80
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/review.js +361 -0
- package/daemon/dist/runtime.js +1 -1
- package/daemon/langMap.ts +24 -6
- package/daemon/review-widget/anchor.ts +187 -0
- package/daemon/review-widget/client.ts +95 -0
- package/daemon/review-widget/index.tsx +36 -0
- package/daemon/review-widget/markup.ts +71 -0
- package/daemon/review-widget/screenshot.ts +91 -0
- package/daemon/review-widget/source.ts +103 -0
- package/daemon/review-widget/styles.css +359 -0
- package/daemon/review-widget/text-modules.d.ts +4 -0
- package/daemon/review-widget/tsconfig.json +16 -0
- package/daemon/review-widget/widget.tsx +436 -0
- package/daemon/src/components.ts +3 -3
- package/daemon/src/handlers/api.ts +330 -328
- package/daemon/src/handlers/files.ts +112 -106
- package/daemon/src/handlers/static.ts +47 -46
- package/daemon/src/handlers/uploads.ts +61 -56
- package/daemon/src/paths.ts +13 -10
- package/daemon/src/remote-feedback.ts +192 -192
- package/daemon/src/review/markdown.ts +52 -0
- package/daemon/src/review/model.ts +132 -0
- package/daemon/src/review/proxy.ts +177 -0
- package/daemon/src/review/server.ts +199 -0
- package/daemon/src/review/store.ts +151 -0
- package/daemon/src/server.ts +127 -86
- package/daemon/src/share.ts +163 -169
- package/daemon/src/stubFetch.ts +7 -0
- package/daemon/tsconfig.json +19 -19
- package/package.json +47 -47
- package/skills/canvas/SKILL.md +5 -1
- package/bin/lib/commands/exec.test.ts +0 -27
- package/bin/lib/helpers.test.ts +0 -79
- package/daemon/src/files-api.test.ts +0 -78
- package/daemon/src/remote-feedback.test.ts +0 -165
- package/daemon/src/secrets-api.test.ts +0 -123
- package/daemon/src/share-e2e.test.ts +0 -277
- package/daemon/src/share.test.ts +0 -289
|
@@ -21,182 +21,170 @@
|
|
|
21
21
|
* work in cleartext.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
decryptString,
|
|
28
|
-
ENCRYPTION_META,
|
|
29
|
-
type EncryptionMeta,
|
|
30
|
-
} from "./shareCrypto";
|
|
31
|
-
import type { CanvasHost } from "@fabrika/canvas-kernel/client";
|
|
24
|
+
import type { CanvasHost } from '@fabrika/canvas-kernel/client'
|
|
25
|
+
import { type SessionMeta, sessionMetaSchema } from './apiSchemas'
|
|
26
|
+
import { decryptString, ENCRYPTION_META, type EncryptionMeta, encryptString, importShareKey } from './shareCrypto'
|
|
32
27
|
|
|
33
28
|
declare global {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
interface Window {
|
|
30
|
+
__CANVAS_SHARE__?: {
|
|
31
|
+
shareId: string
|
|
32
|
+
mode: 'shared'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
40
35
|
}
|
|
41
36
|
|
|
42
37
|
export interface SharedModeInfo {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
isShared: true
|
|
39
|
+
shareId: string
|
|
40
|
+
/** Lazy-imported AES-GCM key parsed from the URL fragment, if any. */
|
|
41
|
+
encryptionKey: Promise<CryptoKey> | null
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
export interface LocalModeInfo {
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
isShared: false
|
|
46
|
+
sessionId: string
|
|
52
47
|
}
|
|
53
48
|
|
|
54
|
-
export type ModeInfo = SharedModeInfo | LocalModeInfo
|
|
49
|
+
export type ModeInfo = SharedModeInfo | LocalModeInfo
|
|
55
50
|
|
|
56
51
|
/** Strip the `#k=...` fragment and parse out the key (base64url). */
|
|
57
52
|
function readFragmentKey(): string | null {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
if (typeof window === 'undefined') return null
|
|
54
|
+
const hash = window.location.hash.replace(/^#/, '')
|
|
55
|
+
if (!hash) return null
|
|
56
|
+
for (const part of hash.split('&')) {
|
|
57
|
+
const [k, v] = part.split('=')
|
|
58
|
+
if (k === 'k' && v) return decodeURIComponent(v)
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
/** One-time detection. In shared mode the daemon-provided sessionId from
|
|
69
64
|
* the URL path is ignored in favor of the worker-injected shareId. */
|
|
70
65
|
export function detectMode(): ModeInfo {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
if (typeof window !== 'undefined' && window.__CANVAS_SHARE__?.shareId) {
|
|
67
|
+
const encoded = readFragmentKey()
|
|
68
|
+
return {
|
|
69
|
+
isShared: true,
|
|
70
|
+
shareId: window.__CANVAS_SHARE__.shareId,
|
|
71
|
+
encryptionKey: encoded ? importShareKey(encoded) : null,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Local: session id is the path suffix /s/:sessionId
|
|
75
|
+
const sessionId = typeof window !== 'undefined'
|
|
76
|
+
? window.location.pathname.replace('/s/', '')
|
|
77
|
+
: ''
|
|
78
|
+
return { isShared: false, sessionId }
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
export const MODE: ModeInfo = detectMode()
|
|
81
|
+
export const MODE: ModeInfo = detectMode()
|
|
87
82
|
|
|
88
83
|
/** True iff we're in a shared canvas AND have a fragment key. */
|
|
89
84
|
export function isEncryptedShare(): boolean {
|
|
90
|
-
|
|
85
|
+
return MODE.isShared && MODE.encryptionKey !== null
|
|
91
86
|
}
|
|
92
87
|
|
|
93
88
|
/** Build a canonical id-ish string used as React keys / identifiers. */
|
|
94
89
|
export function getIdentifier(): string {
|
|
95
|
-
|
|
90
|
+
return MODE.isShared ? MODE.shareId : MODE.sessionId
|
|
96
91
|
}
|
|
97
92
|
|
|
98
93
|
// --- URL builders -----------------------------------------------------------
|
|
99
94
|
|
|
100
95
|
/** Metadata endpoint (revisions, canvas files, project root). */
|
|
101
96
|
export function metaUrl(): string {
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
if (MODE.isShared) return `/s/${MODE.shareId}/meta`
|
|
98
|
+
return `/api/session/${MODE.sessionId}/meta`
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
/** Compiled canvas JS module for a given filename (+ optional revision). */
|
|
107
102
|
export function canvasJsUrl(filename: string, revision?: number): string {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
const jsName = filename.replace(/\.jsx$/, '.js')
|
|
104
|
+
if (MODE.isShared) {
|
|
105
|
+
return `/s/${MODE.shareId}/canvas/${encodeURIComponent(jsName)}`
|
|
106
|
+
}
|
|
107
|
+
const rev = revision ? `?rev=${revision}&t=${Date.now()}` : `?t=${Date.now()}`
|
|
108
|
+
return `/api/session/${MODE.sessionId}/canvas/${encodeURIComponent(jsName)}${rev}`
|
|
114
109
|
}
|
|
115
110
|
|
|
116
111
|
/** Upload endpoint for annotation images. */
|
|
117
112
|
export function uploadUrl(): string {
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
if (MODE.isShared) return `/s/${MODE.shareId}/upload`
|
|
114
|
+
return `/api/session/${MODE.sessionId}/upload`
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
function localSecretUrl(fieldId: string, suffix:
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
function localSecretUrl(fieldId: string, suffix: 'status' | 'value'): string {
|
|
118
|
+
if (MODE.isShared) throw new Error('Secret inputs are available only on the local canvas daemon')
|
|
119
|
+
return `/api/session/${encodeURIComponent(MODE.sessionId)}/secrets/${encodeURIComponent(fieldId)}/${suffix}`
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
async function secretApiError(response: Response): Promise<Error> {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
const raw: unknown = await response.json().catch(() => null)
|
|
124
|
+
if (typeof raw === 'object' && raw !== null && 'error' in raw && typeof raw.error === 'string') {
|
|
125
|
+
return new Error(raw.error)
|
|
126
|
+
}
|
|
127
|
+
return new Error(`Secret API request failed with status ${response.status}`)
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
/** Return only readiness metadata. The secret value is never sent back to the browser. */
|
|
136
131
|
export async function getLocalSecretStatus(fieldId: string): Promise<boolean> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
const response = await fetch(localSecretUrl(fieldId, 'status'), { cache: 'no-store' })
|
|
133
|
+
if (!response.ok) throw await secretApiError(response)
|
|
134
|
+
const raw: unknown = await response.json()
|
|
135
|
+
return typeof raw === 'object' && raw !== null && 'ready' in raw && raw.ready === true
|
|
141
136
|
}
|
|
142
137
|
|
|
143
138
|
/** Deposit a secret into the daemon's in-memory vault. */
|
|
144
139
|
export async function storeLocalSecret(fieldId: string, value: string): Promise<void> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
const response = await fetch(localSecretUrl(fieldId, 'value'), {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
cache: 'no-store',
|
|
144
|
+
body: JSON.stringify({ value }),
|
|
145
|
+
})
|
|
146
|
+
if (!response.ok) throw await secretApiError(response)
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
/** Remove a deposited secret without retrieving its value. */
|
|
155
150
|
export async function clearLocalSecret(fieldId: string): Promise<void> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
const response = await fetch(localSecretUrl(fieldId, 'value'), {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
cache: 'no-store',
|
|
154
|
+
})
|
|
155
|
+
if (!response.ok) throw await secretApiError(response)
|
|
161
156
|
}
|
|
162
157
|
|
|
163
158
|
// --- Meta fetch (with optional decryption) ---------------------------------
|
|
164
159
|
|
|
165
|
-
interface RawMetaResponse {
|
|
166
|
-
encryption?: EncryptionMeta;
|
|
167
|
-
origin?: { sessionId?: string; label?: string; revision?: number; createdAt?: string };
|
|
168
|
-
[key: string]: unknown;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
160
|
/** Fetch + parse meta, decrypting origin.sessionId/label for encrypted
|
|
172
161
|
* shares. The shape returned is the same as the worker/daemon emits, so
|
|
173
162
|
* callers don't have to branch. */
|
|
174
|
-
export async function fetchMeta(): Promise<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return data;
|
|
163
|
+
export async function fetchMeta(): Promise<SessionMeta> {
|
|
164
|
+
const res = await fetch(metaUrl())
|
|
165
|
+
if (!res.ok) throw new Error(`meta ${res.status}`)
|
|
166
|
+
const data = sessionMetaSchema.parse(await res.json())
|
|
167
|
+
|
|
168
|
+
if (data.encryption && MODE.isShared && MODE.encryptionKey) {
|
|
169
|
+
const key = await MODE.encryptionKey
|
|
170
|
+
if (data.origin) {
|
|
171
|
+
const o = { ...data.origin }
|
|
172
|
+
if (o.sessionId) o.sessionId = await decryptString(key, o.sessionId)
|
|
173
|
+
if (o.label) o.label = await decryptString(key, o.label)
|
|
174
|
+
data.origin = o
|
|
175
|
+
}
|
|
176
|
+
// The worker synthesizes a one-element revisions array from the same
|
|
177
|
+
// ShareRecord.origin, so its `label` is also ciphertext — decrypt it
|
|
178
|
+
// in lockstep with origin.label above.
|
|
179
|
+
for (const r of data.revisions ?? []) {
|
|
180
|
+
if (r.label) {
|
|
181
|
+
try {
|
|
182
|
+
r.label = await decryptString(key, r.label)
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return data
|
|
200
188
|
}
|
|
201
189
|
|
|
202
190
|
// --- Canvas module loading -------------------------------------------------
|
|
@@ -207,24 +195,24 @@ export async function fetchMeta(): Promise<any> {
|
|
|
207
195
|
* Import maps still apply to Blob-URL module imports, so the resulting
|
|
208
196
|
* module's `#canvas/runtime` / `#canvas/components` imports resolve
|
|
209
197
|
* exactly as they would for an in-place script. */
|
|
210
|
-
export
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
198
|
+
export const loadCanvasModule: CanvasHost['loadCanvasModule'] = async (filename, revision) => {
|
|
199
|
+
const url = canvasJsUrl(filename, revision)
|
|
200
|
+
if (!isEncryptedShare()) {
|
|
201
|
+
return import(/* @vite-ignore */ url)
|
|
202
|
+
}
|
|
203
|
+
const res = await fetch(url)
|
|
204
|
+
if (!res.ok) throw new Error(`canvas ${res.status}`)
|
|
205
|
+
const ciphertext = await res.text()
|
|
206
|
+
const key = await (MODE as SharedModeInfo).encryptionKey!
|
|
207
|
+
const js = await decryptString(key, ciphertext)
|
|
208
|
+
const blob = new Blob([js], { type: 'application/javascript' })
|
|
209
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
210
|
+
try {
|
|
211
|
+
return await import(/* @vite-ignore */ blobUrl)
|
|
212
|
+
} finally {
|
|
213
|
+
// Revoke after a tick so the import has fully resolved its source.
|
|
214
|
+
setTimeout(() => URL.revokeObjectURL(blobUrl), 0)
|
|
215
|
+
}
|
|
228
216
|
}
|
|
229
217
|
|
|
230
218
|
// --- Feedback submission ----------------------------------------------------
|
|
@@ -233,27 +221,27 @@ export async function loadCanvasModule(filename: string, revision?: number): Pro
|
|
|
233
221
|
* Local-mode author info used when submitting feedback in shared mode.
|
|
234
222
|
* Prompted on first submit and cached in localStorage.
|
|
235
223
|
*/
|
|
236
|
-
const REVIEWER_KEY =
|
|
224
|
+
const REVIEWER_KEY = 'canvas-reviewer-identity'
|
|
237
225
|
|
|
238
226
|
export function getReviewerIdentity(): { id: string; name: string } | null {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
227
|
+
try {
|
|
228
|
+
const raw = localStorage.getItem(REVIEWER_KEY)
|
|
229
|
+
if (!raw) return null
|
|
230
|
+
return JSON.parse(raw)
|
|
231
|
+
} catch {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
246
234
|
}
|
|
247
235
|
|
|
248
236
|
export function setReviewerIdentity(name: string): { id: string; name: string } {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
237
|
+
let existing = getReviewerIdentity()
|
|
238
|
+
if (!existing) {
|
|
239
|
+
existing = { id: crypto.randomUUID?.() || String(Date.now()), name }
|
|
240
|
+
} else {
|
|
241
|
+
existing.name = name
|
|
242
|
+
}
|
|
243
|
+
localStorage.setItem(REVIEWER_KEY, JSON.stringify(existing))
|
|
244
|
+
return existing
|
|
257
245
|
}
|
|
258
246
|
|
|
259
247
|
/**
|
|
@@ -261,47 +249,47 @@ export function setReviewerIdentity(name: string): { id: string; name: string }
|
|
|
261
249
|
* feedback. Matches the worker's `FeedbackPostBody`.
|
|
262
250
|
*/
|
|
263
251
|
export interface SharedFeedbackPayload {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
252
|
+
author: { id: string; name: string }
|
|
253
|
+
revision: number
|
|
254
|
+
annotations: Array<Record<string, unknown>>
|
|
255
|
+
generalNote?: string
|
|
268
256
|
}
|
|
269
257
|
|
|
270
258
|
/** Encrypt the secret fields of a feedback payload in-place semantics. */
|
|
271
259
|
async function encryptFeedback(
|
|
272
|
-
|
|
273
|
-
|
|
260
|
+
payload: SharedFeedbackPayload,
|
|
261
|
+
key: CryptoKey,
|
|
274
262
|
): Promise<SharedFeedbackPayload & { encryption: EncryptionMeta }> {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
263
|
+
const annotations = await Promise.all(
|
|
264
|
+
payload.annotations.map(async (a) => {
|
|
265
|
+
const out: Record<string, unknown> = {
|
|
266
|
+
id: a.id,
|
|
267
|
+
createdAt: a.createdAt,
|
|
268
|
+
snippet: await encryptString(key, String(a.snippet ?? '')),
|
|
269
|
+
note: await encryptString(key, String(a.note ?? '')),
|
|
270
|
+
}
|
|
271
|
+
if (a.filePath) out.filePath = await encryptString(key, String(a.filePath))
|
|
272
|
+
if (a.canvasFile) out.canvasFile = await encryptString(key, String(a.canvasFile))
|
|
273
|
+
if (a.context !== undefined && a.context !== null) {
|
|
274
|
+
out.context = await encryptString(key, JSON.stringify(a.context))
|
|
275
|
+
}
|
|
276
|
+
// Image attachments are content-addressed worker URLs — already public
|
|
277
|
+
// by virtue of being served by the worker, so leaving them plaintext
|
|
278
|
+
// is fine and avoids breaking client-side rendering of the URL.
|
|
279
|
+
if (a.attachments) out.attachments = a.attachments
|
|
280
|
+
return out
|
|
281
|
+
}),
|
|
282
|
+
)
|
|
283
|
+
return {
|
|
284
|
+
author: {
|
|
285
|
+
id: payload.author.id,
|
|
286
|
+
name: await encryptString(key, payload.author.name),
|
|
287
|
+
},
|
|
288
|
+
revision: payload.revision,
|
|
289
|
+
annotations,
|
|
290
|
+
...(payload.generalNote ? { generalNote: await encryptString(key, payload.generalNote) } : {}),
|
|
291
|
+
encryption: ENCRYPTION_META,
|
|
292
|
+
}
|
|
305
293
|
}
|
|
306
294
|
|
|
307
295
|
/**
|
|
@@ -311,30 +299,30 @@ async function encryptFeedback(
|
|
|
311
299
|
* fragment key.
|
|
312
300
|
*/
|
|
313
301
|
export async function submitSharedFeedback(payload: SharedFeedbackPayload): Promise<void> {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
302
|
+
if (!MODE.isShared) throw new Error('submitSharedFeedback called outside shared mode')
|
|
303
|
+
let body: unknown = payload
|
|
304
|
+
if (MODE.encryptionKey) {
|
|
305
|
+
const key = await MODE.encryptionKey
|
|
306
|
+
body = await encryptFeedback(payload, key)
|
|
307
|
+
}
|
|
308
|
+
const res = await fetch(`/s/${MODE.shareId}/feedback`, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
311
|
+
body: JSON.stringify(body),
|
|
312
|
+
})
|
|
313
|
+
if (!res.ok) {
|
|
314
|
+
const text = await res.text().catch(() => '')
|
|
315
|
+
throw new Error(`Feedback submission failed: ${res.status} ${text}`)
|
|
316
|
+
}
|
|
329
317
|
}
|
|
330
318
|
|
|
331
319
|
// --- Feature flags ----------------------------------------------------------
|
|
332
320
|
|
|
333
321
|
/** Are daemon-only filesystem endpoints (file tree, file viewer) available? */
|
|
334
|
-
export const FS_AVAILABLE: boolean = !MODE.isShared
|
|
322
|
+
export const FS_AVAILABLE: boolean = !MODE.isShared
|
|
335
323
|
|
|
336
324
|
/** Is live WebSocket session updates available? */
|
|
337
|
-
export const WS_AVAILABLE: boolean = !MODE.isShared
|
|
325
|
+
export const WS_AVAILABLE: boolean = !MODE.isShared
|
|
338
326
|
|
|
339
327
|
// --- Kernel host binding ----------------------------------------------------
|
|
340
328
|
|
|
@@ -344,11 +332,11 @@ export const WS_AVAILABLE: boolean = !MODE.isShared;
|
|
|
344
332
|
* defaults: shared canvases come from the worker (and may be encrypted).
|
|
345
333
|
*/
|
|
346
334
|
export const canvasHost: CanvasHost = {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
335
|
+
get sessionId() {
|
|
336
|
+
return getIdentifier()
|
|
337
|
+
},
|
|
338
|
+
isShared: MODE.isShared,
|
|
339
|
+
fsAvailable: FS_AVAILABLE,
|
|
340
|
+
uploadUrl,
|
|
341
|
+
loadCanvasModule,
|
|
342
|
+
}
|