@lincyaw/kernel 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +32 -2
- package/package.json +41 -3
- package/src/access.ts +18 -0
- package/src/anchors.ts +58 -0
- package/src/authoring.ts +56 -0
- package/src/blocks.ts +19 -0
- package/src/checks.ts +35 -0
- package/src/component-code.ts +6 -0
- package/src/contracts/agent.ts +76 -0
- package/src/contracts/anchors.ts +46 -0
- package/src/contracts/authoring-actions.ts +60 -0
- package/src/contracts/authoring-clients.ts +213 -0
- package/src/contracts/authoring.ts +180 -0
- package/src/contracts/checks.ts +29 -0
- package/src/contracts/classroom.ts +49 -0
- package/src/contracts/diagnostics.ts +37 -0
- package/src/contracts/document-audio.ts +150 -0
- package/src/contracts/document-comments.ts +31 -0
- package/src/contracts/document-history.ts +63 -0
- package/src/contracts/document-image.ts +32 -0
- package/src/contracts/document-reads.ts +57 -0
- package/src/contracts/document-voice.ts +44 -0
- package/src/contracts/document.ts +240 -0
- package/src/contracts/execution.ts +118 -0
- package/src/contracts/locale.ts +3 -0
- package/src/contracts/offline.ts +2 -0
- package/src/contracts/operations.ts +8 -0
- package/src/contracts/plugin.ts +183 -0
- package/src/contracts/presenter-panels.ts +17 -0
- package/src/contracts/preview.ts +386 -0
- package/src/contracts/production-assets.ts +61 -0
- package/src/contracts/production.ts +109 -0
- package/src/contracts/projects.ts +59 -0
- package/src/contracts/server.ts +108 -0
- package/src/contracts/service.ts +8 -0
- package/src/contracts/speech-settings.ts +16 -0
- package/src/contracts/speech.ts +149 -0
- package/src/contracts/steps.ts +15 -0
- package/src/contracts/transcription-adoption.ts +79 -0
- package/src/contracts/transcription.ts +112 -0
- package/src/contracts/voice.ts +97 -0
- package/src/contracts/voiceover-execution.ts +68 -0
- package/src/contracts/workspace.ts +458 -0
- package/src/contracts.ts +35 -0
- package/src/diagnostics.ts +7 -0
- package/src/document-authoring-actions.ts +47 -0
- package/src/document-caption-timings.ts +64 -0
- package/src/document-colors.ts +25 -0
- package/src/document-comments.ts +118 -0
- package/src/document-components.ts +251 -0
- package/src/document-operations.ts +132 -0
- package/src/document-pages.ts +215 -0
- package/src/document-reads.ts +81 -0
- package/src/document-schema.ts +85 -0
- package/src/document-voices.ts +153 -0
- package/src/document.ts +252 -0
- package/src/durations.ts +7 -0
- package/src/errors.ts +15 -0
- package/src/events.ts +32 -0
- package/src/execution.ts +25 -0
- package/src/fingerprint.ts +31 -0
- package/src/follow.ts +33 -0
- package/src/git.ts +12 -0
- package/src/html.ts +41 -0
- package/src/index.ts +82 -0
- package/src/ir.ts +367 -0
- package/src/jupyter-kernel.ts +9 -0
- package/src/kernel.ts +190 -0
- package/src/keyboard.ts +43 -0
- package/src/locale.ts +13 -0
- package/src/lock.ts +81 -0
- package/src/logging.ts +11 -0
- package/src/lower-myst.ts +312 -0
- package/src/node/assets.ts +23 -0
- package/src/node/authoring-actions.ts +50 -0
- package/src/node/authoring-clients.ts +14 -0
- package/src/node/document-audio.ts +22 -0
- package/src/node/document-history.ts +11 -0
- package/src/node/document-image.ts +8 -0
- package/src/node/document-reads.ts +10 -0
- package/src/node/document-voice.ts +7 -0
- package/src/node/ffmpeg.ts +29 -0
- package/src/node/file-version.ts +6 -0
- package/src/node/fonts.ts +22 -0
- package/src/node/index.ts +29 -0
- package/src/node/installed-packages.ts +11 -0
- package/src/node/lines.ts +24 -0
- package/src/node/locale.ts +7 -0
- package/src/node/network.ts +11 -0
- package/src/node/operator-speech.ts +91 -0
- package/src/node/packages.ts +74 -0
- package/src/node/paths.ts +7 -0
- package/src/node/projects.ts +7 -0
- package/src/node/resources.ts +86 -0
- package/src/node/revision.ts +6 -0
- package/src/node/run-directory.ts +70 -0
- package/src/node/strip-types.mjs +15 -0
- package/src/node/transcription-adoption.ts +9 -0
- package/src/node/transcription.ts +10 -0
- package/src/node/voiceover-routes.ts +73 -0
- package/src/node/voiceover.ts +199 -0
- package/src/page-source.ts +359 -0
- package/src/parameters.ts +81 -0
- package/src/preview-edit.ts +18 -0
- package/src/preview-events.ts +57 -0
- package/src/preview-frame.ts +1220 -0
- package/src/props.ts +17 -0
- package/src/published.ts +11 -0
- package/src/push-stream.ts +67 -0
- package/src/reading.ts +96 -0
- package/src/recording.ts +38 -0
- package/src/renderer-components.ts +30 -0
- package/src/requests.ts +28 -0
- package/src/responses.ts +49 -0
- package/src/run-outputs.ts +15 -0
- package/src/runner.py +454 -0
- package/src/services.ts +146 -0
- package/src/slides.ts +36 -0
- package/src/speech-words.ts +76 -0
- package/src/speech.ts +13 -0
- package/src/step-keys.ts +38 -0
- package/src/steps.ts +7 -0
- package/src/transcription-adoption.ts +105 -0
- package/src/uri-decoding.ts +8 -0
- package/src/urls.ts +13 -0
- package/src/views.ts +47 -0
- package/src/voice.ts +86 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { copyPublished, productionAssetFile } from './assets.ts'
|
|
2
|
+
export { fileVersion } from './file-version.ts'
|
|
3
|
+
export { DOCUMENT_CHANGE_JSON_SCHEMA, validateDocumentChange, validateDocumentPageChange, validateDocumentPageChangeApply, validateDocumentPageDuplication, validateDocumentPageDuplicationApply } from '../authoring.ts'
|
|
4
|
+
export { validateDocumentOutlineQuery, validateDocumentPartQuery } from './document-reads.ts'
|
|
5
|
+
export { DOCUMENT_AUTHORING_ACTION_SCHEMA, DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA, DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA, validateDocumentAuthoringAction, validateDocumentAuthoringActionReference, validateDocumentAuthoringActionImpact } from './authoring-actions.ts'
|
|
6
|
+
export { validateDocumentTranscriptionRequest } from './transcription.ts'
|
|
7
|
+
export { validateDocumentRecordingAdoption, validateDocumentAudioInspect, validateDocumentWorkspaceAudioImport, validateDocumentWorkspaceAudioInspect, validateDocumentMediaClipValidation, validateDocumentMediaClipEdit, validateDocumentProductionPlanQuery } from './document-audio.ts'
|
|
8
|
+
export { validateDocumentVoiceCatalogQuery } from './document-voice.ts'
|
|
9
|
+
export { createVoiceoverOwner, VoiceoverOperationError, validateVoiceoverStart, validateVoiceoverStatus, validateVoiceoverJobQuery, validateVoicePreviewCheck, validateVoicePreviewStart } from './voiceover.ts'
|
|
10
|
+
export { createVoiceoverRoutes } from './voiceover-routes.ts'
|
|
11
|
+
export { validateTranscriptionAdoptionDraft, validateTranscriptionAdoptionApply } from './transcription-adoption.ts'
|
|
12
|
+
export { FONT_LICENSE, fontLicense } from './fonts.ts'
|
|
13
|
+
export { findPackageBin, importSpecifier, literalGlob, packageOf, playwrightChromium, TYPESCRIPT_IMPORT } from './packages.ts'
|
|
14
|
+
export { ffmpegInstallCommand, findFfmpeg } from './ffmpeg.ts'
|
|
15
|
+
export type { Ffmpeg } from './ffmpeg.ts'
|
|
16
|
+
export { bracketIPv6, lanAddresses } from './network.ts'
|
|
17
|
+
export { forEachLine } from './lines.ts'
|
|
18
|
+
export { isInside } from './paths.ts'
|
|
19
|
+
export { backgroundPriority, machineState, NODE_RESOURCE_ARGS, nodeResourceArgs, resourceEnvironment, spawnBackgroundNode, spawnInteractiveNode, threadBudget } from './resources.ts'
|
|
20
|
+
export type { MachineState, ResourceClass } from './resources.ts'
|
|
21
|
+
export { irRevision } from './revision.ts'
|
|
22
|
+
export { findInstalledPackage } from './installed-packages.ts'
|
|
23
|
+
export { directSandbox, minimalRunEnv, RUN_REQUEST_FILE, RUN_RESULT_PREFIX, runResultFile } from './run-directory.ts'
|
|
24
|
+
export { validateDocumentHistoryQuery, validateDocumentHistoryRead, validateDocumentHistoryChange } from './document-history.ts'
|
|
25
|
+
export { validateAuthoringClientRequest, validateAuthoringClientQuery, validateAuthoringClientRequestQuery, validateAuthoringClientOpen, validateAuthoringClientReport, validateAuthoringClientAcknowledgement, validateAuthoringClientLease } from './authoring-clients.ts'
|
|
26
|
+
export { validateProjectPlan, validateProjectApply } from './projects.ts'
|
|
27
|
+
export { validateDocumentWorkspaceImageInspect, validateDocumentWorkspaceImageImport } from './document-image.ts'
|
|
28
|
+
export { systemLocale } from './locale.ts'
|
|
29
|
+
export { OperatorSpeechSettings, operatorSpeechSecrets, SpeechSettingsError } from './operator-speech.ts'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** The real directory of the package `name` installed in a `node_modules` at or above `from`, as Node resolves it. */
|
|
5
|
+
export function findInstalledPackage(name: string, from: string): string | undefined {
|
|
6
|
+
for (let dir = from; ; dir = dirname(dir)) {
|
|
7
|
+
const candidate = join(dir, 'node_modules', name)
|
|
8
|
+
if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate)
|
|
9
|
+
if (dirname(dir) === dir) return undefined
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream'
|
|
2
|
+
|
|
3
|
+
// Terminal colours and cursor moves, which a log shown on a page does not draw.
|
|
4
|
+
const ESCAPES = /\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*\x07/g
|
|
5
|
+
|
|
6
|
+
/** Calls `each` with every line `stream` writes (carriage returns end a line too), without terminal escapes. */
|
|
7
|
+
export function forEachLine(stream: Readable | null | undefined, each: (line: string) => void): void {
|
|
8
|
+
if (!stream) return
|
|
9
|
+
let rest = ''
|
|
10
|
+
stream.setEncoding('utf8')
|
|
11
|
+
stream.on('data', (chunk: string) => {
|
|
12
|
+
const parts = (rest + chunk).split(/\r\n|\r|\n/)
|
|
13
|
+
rest = parts.pop() ?? ''
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const line = part.replace(ESCAPES, '')
|
|
16
|
+
if (line.trim()) each(line)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
stream.on('end', () => {
|
|
20
|
+
const line = rest.replace(ESCAPES, '')
|
|
21
|
+
if (line.trim()) each(line)
|
|
22
|
+
rest = ''
|
|
23
|
+
})
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Locale } from '../contracts/locale.ts'
|
|
2
|
+
|
|
3
|
+
/** The language of command-line output: the system's (`LC_ALL`, `LC_MESSAGES`, `LANG`, else `Intl`'s); `zh*` is zh-CN. */
|
|
4
|
+
export function systemLocale(env: NodeJS.ProcessEnv = process.env): Locale {
|
|
5
|
+
const tag = env.LC_ALL || env.LC_MESSAGES || env.LANG || Intl.DateTimeFormat().resolvedOptions().locale
|
|
6
|
+
return /^zh/i.test(tag) ? 'zh-CN' : 'en'
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os'
|
|
2
|
+
|
|
3
|
+
/** This machine's IPv4 addresses on its local networks, for printing the addresses other devices can use. */
|
|
4
|
+
export function lanAddresses(): string[] {
|
|
5
|
+
return Object.values(networkInterfaces()).flatMap(entries => entries ?? []).filter(entry => entry.family === 'IPv4' && !entry.internal).map(entry => entry.address)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The host as a URL writes it: an IPv6 address in brackets. */
|
|
9
|
+
export function bracketIPv6(host: string): string {
|
|
10
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host
|
|
11
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import type { SpeechSettingsAnswer, SpeechSettingsChange } from '../contracts/speech-settings.ts'
|
|
6
|
+
|
|
7
|
+
const KEY = 'PRESMIUM_SPEECH_VOLCENGINE_KEY', APPID = 'PRESMIUM_SPEECH_VOLCENGINE_APPID'
|
|
8
|
+
const MAX_BYTES = 16384
|
|
9
|
+
const lockfile = createRequire(import.meta.url)('proper-lockfile') as { lock(file: string, options: { realpath: boolean, retries: number }): Promise<() => Promise<void>> }
|
|
10
|
+
interface Stored { version: 1, revision: string, key?: string, appId?: string }
|
|
11
|
+
const empty = (): Stored => ({ version: 1, revision: 'none' })
|
|
12
|
+
|
|
13
|
+
export class SpeechSettingsError extends Error {
|
|
14
|
+
readonly code: 'speech-settings-conflict' | 'speech-settings-invalid' | 'speech-settings-unavailable' | 'speech-settings-busy'
|
|
15
|
+
constructor(code: SpeechSettingsError['code']) { super(code); this.code = code }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One operator-owned file; no values leave this module except through the Node secret resolver. */
|
|
19
|
+
export class OperatorSpeechSettings {
|
|
20
|
+
readonly #file: string | undefined
|
|
21
|
+
readonly #fallback: (name: string) => string | undefined
|
|
22
|
+
constructor(directory: string | undefined, fallback: (name: string) => string | undefined) {
|
|
23
|
+
this.#file = directory ? join(directory, 'speech', 'credentials.json') : undefined
|
|
24
|
+
this.#fallback = fallback
|
|
25
|
+
}
|
|
26
|
+
#read(): Stored {
|
|
27
|
+
if (!this.#file) return empty()
|
|
28
|
+
let fd: number | undefined
|
|
29
|
+
try {
|
|
30
|
+
fd = openSync(this.#file, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
31
|
+
const stat = fstatSync(fd)
|
|
32
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) throw new SpeechSettingsError('speech-settings-unavailable')
|
|
33
|
+
const value = JSON.parse(readFileSync(fd, 'utf8')) as Stored
|
|
34
|
+
if (value?.version !== 1 || typeof value.revision !== 'string' || value.key !== undefined && typeof value.key !== 'string' || value.appId !== undefined && (typeof value.appId !== 'string' || !/^\d{1,32}$/.test(value.appId))) throw new SpeechSettingsError('speech-settings-unavailable')
|
|
35
|
+
return value
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return empty()
|
|
38
|
+
throw new SpeechSettingsError('speech-settings-unavailable')
|
|
39
|
+
} finally { if (fd !== undefined) closeSync(fd) }
|
|
40
|
+
}
|
|
41
|
+
secret = (name: string): string | undefined => {
|
|
42
|
+
if (name !== KEY && name !== APPID) return this.#fallback(name)
|
|
43
|
+
const saved = this.#read()
|
|
44
|
+
// API-key authentication must not inherit a different environment credential's App ID.
|
|
45
|
+
if (name === APPID && saved.key) return saved.appId
|
|
46
|
+
return (name === KEY ? saved.key : undefined) ?? this.#fallback(name)
|
|
47
|
+
}
|
|
48
|
+
read(): SpeechSettingsAnswer {
|
|
49
|
+
const value = this.#read(), key = value.key ?? this.#fallback(KEY)?.trim()
|
|
50
|
+
return { provider: 'volcengine', model: 'seed-tts-2.0', revision: value.revision, configured: Boolean(key), appIdConfigured: Boolean(value.key ? value.appId : this.#fallback(APPID)), source: value.key ? 'saved' : key ? 'environment' : 'none', stored: Boolean(value.key), writable: Boolean(this.#file), verification: 'configuration-only' }
|
|
51
|
+
}
|
|
52
|
+
async change(change: SpeechSettingsChange): Promise<SpeechSettingsAnswer> {
|
|
53
|
+
if (!this.#file) throw new SpeechSettingsError('speech-settings-unavailable')
|
|
54
|
+
const directory = dirname(this.#file)
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
57
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error('unsafe-directory')
|
|
58
|
+
} catch { throw new SpeechSettingsError('speech-settings-unavailable') }
|
|
59
|
+
let release: () => Promise<void>
|
|
60
|
+
try { release = await lockfile.lock(this.#file, { realpath: false, retries: 0 }) }
|
|
61
|
+
catch { throw new SpeechSettingsError('speech-settings-busy') }
|
|
62
|
+
let temporary: string | undefined
|
|
63
|
+
try {
|
|
64
|
+
const previous = this.#read()
|
|
65
|
+
if (change.revision !== previous.revision) throw new SpeechSettingsError('speech-settings-conflict')
|
|
66
|
+
let next: Stored = { version: 1, revision: randomUUID() }
|
|
67
|
+
if (change.action === 'save') {
|
|
68
|
+
const key = change.apiKey?.trim() || previous.key || this.#fallback(KEY)?.trim()
|
|
69
|
+
if (!key || !/^[\x21-\x7e]{8,8192}$/.test(key)) throw new SpeechSettingsError('speech-settings-invalid')
|
|
70
|
+
const appId = change.appId?.trim() ?? (change.apiKey?.trim() ? undefined : previous.key ? previous.appId : this.#fallback(APPID)?.trim())
|
|
71
|
+
if (appId && !/^\d{1,32}$/.test(appId)) throw new SpeechSettingsError('speech-settings-invalid')
|
|
72
|
+
next = { ...next, key, ...appId ? { appId } : {} }
|
|
73
|
+
}
|
|
74
|
+
temporary = `${this.#file}.${randomUUID()}.tmp`
|
|
75
|
+
writeFileSync(temporary, `${JSON.stringify(next)}\n`, { mode: 0o600, flag: 'wx' })
|
|
76
|
+
renameSync(temporary, this.#file)
|
|
77
|
+
temporary = undefined
|
|
78
|
+
return this.read()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof SpeechSettingsError) throw error
|
|
81
|
+
throw new SpeechSettingsError('speech-settings-unavailable')
|
|
82
|
+
} finally {
|
|
83
|
+
if (temporary) rmSync(temporary, { force: true })
|
|
84
|
+
await release()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function operatorSpeechSecrets(directory: string | undefined, fallback: (name: string) => string | undefined): (name: string) => string | undefined {
|
|
90
|
+
return new OperatorSpeechSettings(directory, fallback).secret
|
|
91
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
3
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
export const TYPESCRIPT_IMPORT: readonly string[] = ['--import', new URL('./strip-types.mjs', import.meta.url).href]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The import specifier of `file` in a module generated in `dir`: relative where it can be (Vite's dependency scan does
|
|
10
|
+
* not follow an import written as an absolute path), else absolute (another drive on Windows).
|
|
11
|
+
*/
|
|
12
|
+
export function importSpecifier(dir: string, file: string): string {
|
|
13
|
+
const path = relative(dir, file)
|
|
14
|
+
if (isAbsolute(path)) return file
|
|
15
|
+
const posix = path.split(sep).join('/')
|
|
16
|
+
return posix.startsWith('.') ? posix : `./${posix}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `file` as a glob that matches only it: Vite's `optimizeDeps.entries` are globs. */
|
|
20
|
+
export function literalGlob(file: string): string {
|
|
21
|
+
return file.split(sep).join('/').replace(/[\\*?[\]{}()!@+|]/g, '\\$&')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function packageOf(file: string): string | undefined {
|
|
25
|
+
const manifest = findPackageJSON(pathToFileURL(file))
|
|
26
|
+
return manifest && dirname(manifest)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function manifestOf(name: string, from: string): string | undefined {
|
|
30
|
+
const require = createRequire(from)
|
|
31
|
+
try {
|
|
32
|
+
return require.resolve(`${name}/package.json`)
|
|
33
|
+
} catch {}
|
|
34
|
+
let dir: string
|
|
35
|
+
try {
|
|
36
|
+
dir = dirname(require.resolve(name))
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
for (; dirname(dir) !== dir; dir = dirname(dir)) {
|
|
41
|
+
const file = join(dir, 'package.json')
|
|
42
|
+
if (existsSync(file) && (JSON.parse(readFileSync(file, 'utf8')) as { name?: string }).name === name) return file
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The file of the package's command `command` (its `bin`), the package as Node resolves it from `from`; undefined when either is missing. */
|
|
48
|
+
export function findPackageBin(name: string, command: string, from: string): string | undefined {
|
|
49
|
+
const manifest = manifestOf(name, from)
|
|
50
|
+
if (!manifest) return undefined
|
|
51
|
+
const { bin } = JSON.parse(readFileSync(manifest, 'utf8')) as { bin?: string | Record<string, string> }
|
|
52
|
+
const file = typeof bin === 'string' ? bin : bin?.[command]
|
|
53
|
+
return file === undefined ? undefined : join(dirname(manifest), file)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Playwright's Chromium (`playwright-chromium`), as the deck installs it, else as `fallbackFrom` (a module's URL or a
|
|
58
|
+
* `package.json`) finds it.
|
|
59
|
+
*/
|
|
60
|
+
export async function playwrightChromium<T = unknown>(deckDir: string, fallbackFrom?: string): Promise<T> {
|
|
61
|
+
for (const from of [join(deckDir, 'package.json'), ...fallbackFrom ? [fallbackFrom] : []]) {
|
|
62
|
+
let entry: string
|
|
63
|
+
try {
|
|
64
|
+
entry = createRequire(from).resolve('playwright-chromium')
|
|
65
|
+
} catch {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
const loaded = await import(pathToFileURL(entry).href) as { chromium?: T, default?: { chromium?: T } }
|
|
69
|
+
const chromium = loaded.chromium ?? loaded.default?.chromium
|
|
70
|
+
if (chromium) return chromium
|
|
71
|
+
throw new Error('playwright-chromium does not export its Chromium browser')
|
|
72
|
+
}
|
|
73
|
+
throw new Error('this needs playwright-chromium: install it in the deck (and its browser with pnpm exec playwright install chromium)')
|
|
74
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/** Whether path is root itself or lies under it; symbolic links are not resolved. */
|
|
4
|
+
export function isInside(path: string, root: string): boolean {
|
|
5
|
+
const rel = relative(root, path)
|
|
6
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
2
|
+
import { Validator } from '@cfworker/json-schema'
|
|
3
|
+
import { PROJECT_APPLY_SCHEMA, PROJECT_PLAN_SCHEMA } from '../contracts/projects.ts'
|
|
4
|
+
import type { ProjectApply, ProjectPlanRequest } from '../contracts/projects.ts'
|
|
5
|
+
const plan = new Validator(structuredClone(PROJECT_PLAN_SCHEMA) as unknown as Schema, '2020-12', false), apply = new Validator(structuredClone(PROJECT_APPLY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
6
|
+
export const validateProjectPlan = (value: unknown): value is ProjectPlanRequest => plan.validate(value).valid
|
|
7
|
+
export const validateProjectApply = (value: unknown): value is ProjectApply => apply.validate(value).valid
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import type { ChildProcess, SpawnOptions } from 'node:child_process'
|
|
3
|
+
import { availableParallelism, constants, freemem, loadavg, setPriority } from 'node:os'
|
|
4
|
+
import { TYPESCRIPT_IMPORT } from './packages.ts'
|
|
5
|
+
|
|
6
|
+
const THREAD_VARIABLES = ['GOMAXPROCS', 'RAYON_NUM_THREADS', 'OMP_NUM_THREADS', 'OPENBLAS_NUM_THREADS', 'MKL_NUM_THREADS', 'BLIS_NUM_THREADS', 'VECLIB_MAXIMUM_THREADS', 'NUMEXPR_MAX_THREADS', 'NUMEXPR_NUM_THREADS', 'POLARS_MAX_THREADS']
|
|
7
|
+
export const NODE_RESOURCE_ARGS: readonly string[] = ['--v8-pool-size=1']
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `interactive`: what the author waits for, the workspace service and the previews' dev servers; `background`: what
|
|
11
|
+
* runs while the author does something else, builds, exports and code runs.
|
|
12
|
+
*/
|
|
13
|
+
export type ResourceClass = 'interactive' | 'background'
|
|
14
|
+
|
|
15
|
+
/** What the machine has now: its processors, how many of them are busy (the one-minute load), and its free memory. */
|
|
16
|
+
export interface MachineState {
|
|
17
|
+
cpus: number
|
|
18
|
+
load: number
|
|
19
|
+
free: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function machineState(): MachineState {
|
|
23
|
+
return { cpus: availableParallelism(), load: loadavg()[0] ?? 0, free: freemem() }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Bundlers gain little from more threads than this, and each thread holds memory.
|
|
27
|
+
const INTERACTIVE_MAX = 8
|
|
28
|
+
// Below this much free memory an interactive child takes two threads at most.
|
|
29
|
+
const LOW_MEMORY = 2 * 1024 ** 3
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The native threads a child may use. A background child keeps to the operator's `PRESMIUM_MAX_THREADS`, one by default
|
|
33
|
+
* and at most half the processors. An interactive child takes three quarters of the processors that are idle now (all
|
|
34
|
+
* of them where the load is not known, as on Windows), keeping one for the rest of the machine, at most eight, and two
|
|
35
|
+
* when memory is short; `PRESMIUM_MAX_THREADS` caps it.
|
|
36
|
+
*/
|
|
37
|
+
export function threadBudget(kind: ResourceClass, source: NodeJS.ProcessEnv = process.env, machine: MachineState = machineState()): number {
|
|
38
|
+
const requested = Number(source.PRESMIUM_MAX_THREADS)
|
|
39
|
+
const operator = Number.isInteger(requested) && requested > 0 ? requested : undefined
|
|
40
|
+
if (kind === 'background') return Math.min(operator ?? 1, Math.max(1, Math.floor(machine.cpus / 2)))
|
|
41
|
+
const idle = Math.max(1, machine.cpus - Math.min(machine.load, machine.cpus))
|
|
42
|
+
let threads = Math.max(1, Math.min(Math.floor(idle * 0.75), machine.cpus - 1, INTERACTIVE_MAX))
|
|
43
|
+
if (machine.free < LOW_MEMORY) threads = Math.min(threads, 2)
|
|
44
|
+
return operator === undefined ? threads : Math.min(threads, operator)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Bound native thread pools as well as JS jobs; leave CPU capacity for other apps. */
|
|
48
|
+
export function resourceEnvironment(source: NodeJS.ProcessEnv = process.env, kind: ResourceClass = 'background', machine?: MachineState): Record<string, string> {
|
|
49
|
+
const env = Object.fromEntries(Object.entries(source).filter((entry): entry is [string, string] => entry[1] !== undefined))
|
|
50
|
+
const threads = threadBudget(kind, source, machine)
|
|
51
|
+
if (source.PRESMIUM_MAX_THREADS !== undefined && kind === 'background') env.PRESMIUM_MAX_THREADS = String(threads)
|
|
52
|
+
for (const name of THREAD_VARIABLES) {
|
|
53
|
+
const current = Number(source[name])
|
|
54
|
+
// An interactive child takes its own budget: a smaller value it inherited came from a background parent's limit.
|
|
55
|
+
env[name] = String(kind === 'background' && Number.isInteger(current) && current > 0 ? Math.min(current, threads) : threads)
|
|
56
|
+
}
|
|
57
|
+
const ioThreads = Number(source.UV_THREADPOOL_SIZE)
|
|
58
|
+
env.UV_THREADPOOL_SIZE = kind === 'interactive'
|
|
59
|
+
? String(Math.max(4, threads))
|
|
60
|
+
: String(Number.isInteger(ioThreads) && ioThreads > 0 ? Math.min(ioThreads, 2) : 2)
|
|
61
|
+
return env
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Node's own arguments for a child: V8's background pool as large as its thread budget. */
|
|
65
|
+
export function nodeResourceArgs(kind: ResourceClass, source: NodeJS.ProcessEnv = process.env, machine?: MachineState): string[] {
|
|
66
|
+
return kind === 'background' ? [...NODE_RESOURCE_ARGS] : [`--v8-pool-size=${threadBudget(kind, source, machine)}`]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Child priorities are best effort on platforms that restrict changing niceness. */
|
|
70
|
+
export function backgroundPriority(pid: number | undefined): void {
|
|
71
|
+
if (pid === undefined) return
|
|
72
|
+
try { setPriority(pid, constants.priority.PRIORITY_BELOW_NORMAL) } catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Starts Node on `script` as a background child: one V8 worker, bounded thread pools, TypeScript imports and a lower priority. */
|
|
76
|
+
export function spawnBackgroundNode(script: string, args: readonly string[], options: SpawnOptions = {}): ChildProcess {
|
|
77
|
+
const child = spawn(process.execPath, [...NODE_RESOURCE_ARGS, ...TYPESCRIPT_IMPORT, script, ...args], { env: resourceEnvironment(), ...options })
|
|
78
|
+
backgroundPriority(child.pid)
|
|
79
|
+
return child
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Starts Node on `script` as a child the author waits for: the interactive thread budget at normal priority, with TypeScript imports. */
|
|
83
|
+
export function spawnInteractiveNode(script: string, args: readonly string[], options: SpawnOptions = {}): ChildProcess {
|
|
84
|
+
const machine = machineState()
|
|
85
|
+
return spawn(process.execPath, [...nodeResourceArgs('interactive', process.env, machine), ...TYPESCRIPT_IMPORT, script, ...args], { env: resourceEnvironment(process.env, 'interactive', machine), ...options })
|
|
86
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import type { ProcessSandbox, SandboxResult, SandboxRun } from '../contracts/execution.ts'
|
|
6
|
+
import { backgroundPriority, resourceEnvironment } from './resources.ts'
|
|
7
|
+
|
|
8
|
+
/** The runner reads its requests from this file of the run directory (`runner.py`, the script and WASI runners). */
|
|
9
|
+
export const RUN_REQUEST_FILE = '.presmium/request.json'
|
|
10
|
+
|
|
11
|
+
export const RUN_RESULT_PREFIX = '.presmium/result-'
|
|
12
|
+
|
|
13
|
+
/** The runner writes the result of the request at this index here, once it is complete. */
|
|
14
|
+
export function runResultFile(index: number): string {
|
|
15
|
+
return `${RUN_RESULT_PREFIX}${index}.json`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The environment of a run that is given none: `PATH`, `LANG` and a temporary `HOME`, no credentials. */
|
|
19
|
+
export function minimalRunEnv(): Record<string, string> {
|
|
20
|
+
const home = join(tmpdir(), 'presmium-home')
|
|
21
|
+
mkdirSync(home, { recursive: true })
|
|
22
|
+
return { PATH: process.env.PATH ?? '', LANG: process.env.LANG ?? 'C.UTF-8', HOME: home }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The process sandbox when none is configured: runs the command directly, in a new temporary run directory. */
|
|
26
|
+
export const directSandbox: ProcessSandbox = {
|
|
27
|
+
kind: 'none',
|
|
28
|
+
run(request: SandboxRun): Promise<SandboxResult> {
|
|
29
|
+
const dir = mkdtempSync(join(tmpdir(), 'presmium-run-'))
|
|
30
|
+
for (const [path, content] of Object.entries(request.files)) {
|
|
31
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true })
|
|
32
|
+
writeFileSync(join(dir, path), content)
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const [command, ...args] = request.argv
|
|
36
|
+
const child = spawn(command!, args, { cwd: dir, env: resourceEnvironment(request.env), stdio: ['ignore', 'pipe', 'pipe'] })
|
|
37
|
+
backgroundPriority(child.pid)
|
|
38
|
+
const out: Buffer[] = []
|
|
39
|
+
const err: Buffer[] = []
|
|
40
|
+
let stopped = false
|
|
41
|
+
const stop = () => {
|
|
42
|
+
stopped = true
|
|
43
|
+
child.kill('SIGKILL')
|
|
44
|
+
}
|
|
45
|
+
const timer = setTimeout(stop, request.timeoutMs)
|
|
46
|
+
if (request.signal?.aborted) stop()
|
|
47
|
+
request.signal?.addEventListener('abort', stop, { once: true })
|
|
48
|
+
child.stdout.on('data', (chunk: Buffer) => out.push(chunk))
|
|
49
|
+
child.stderr.on('data', (chunk: Buffer) => err.push(chunk))
|
|
50
|
+
let settled = false
|
|
51
|
+
const finish = (exitCode: number | null, error?: Error) => {
|
|
52
|
+
if (settled) return
|
|
53
|
+
settled = true
|
|
54
|
+
clearTimeout(timer)
|
|
55
|
+
request.signal?.removeEventListener('abort', stop)
|
|
56
|
+
const files: Record<string, Uint8Array> = {}
|
|
57
|
+
for (const path of request.collect) {
|
|
58
|
+
try {
|
|
59
|
+
files[path] = readFileSync(join(dir, path))
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
rmSync(dir, { recursive: true, force: true })
|
|
63
|
+
const stderr = Buffer.concat(err).toString('utf8').replaceAll(dir, '.')
|
|
64
|
+
resolve({ exitCode: stopped ? null : exitCode, stdout: Buffer.concat(out).toString('utf8'), stderr: error ? `${stderr}${error.message}` : stderr, files })
|
|
65
|
+
}
|
|
66
|
+
child.on('error', error => finish(null, error))
|
|
67
|
+
child.on('close', code => finish(code))
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { registerHooks, stripTypeScriptTypes } from 'node:module'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const emitWarning = process.emitWarning
|
|
6
|
+
process.emitWarning = (warning, ...args) => {
|
|
7
|
+
if (!String(warning).includes('stripTypeScriptTypes')) emitWarning.call(process, warning, ...args)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
registerHooks({
|
|
11
|
+
load(url, context, nextLoad) {
|
|
12
|
+
if (!url.startsWith('file:') || !/\.m?ts$/.test(url) || !url.includes('/node_modules/')) return nextLoad(url, context)
|
|
13
|
+
return { format: 'module', source: stripTypeScriptTypes(readFileSync(fileURLToPath(url), 'utf8')), shortCircuit: true }
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { TRANSCRIPTION_ADOPTION_APPLY_SCHEMA, TRANSCRIPTION_ADOPTION_DRAFT_SCHEMA } from '../contracts/transcription-adoption.ts'
|
|
4
|
+
import type { TranscriptionAdoptionApply, TranscriptionAdoptionDraft } from '../contracts/transcription-adoption.ts'
|
|
5
|
+
|
|
6
|
+
const draft = new Validator(structuredClone(TRANSCRIPTION_ADOPTION_DRAFT_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const apply = new Validator(structuredClone(TRANSCRIPTION_ADOPTION_APPLY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
export function validateTranscriptionAdoptionDraft(value: unknown): value is TranscriptionAdoptionDraft { return draft.validate(value).valid }
|
|
9
|
+
export function validateTranscriptionAdoptionApply(value: unknown): value is TranscriptionAdoptionApply { return apply.validate(value).valid }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_TRANSCRIPTION_REQUEST_SCHEMA } from '../contracts/transcription.ts'
|
|
4
|
+
import type { DocumentTranscriptionRequest } from '../contracts/transcription.ts'
|
|
5
|
+
|
|
6
|
+
const requestValidator = new Validator(structuredClone(DOCUMENT_TRANSCRIPTION_REQUEST_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
|
|
8
|
+
export function validateDocumentTranscriptionRequest(value: unknown): value is DocumentTranscriptionRequest {
|
|
9
|
+
return requestValidator.validate(value).valid
|
|
10
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { errorMessage, hasRole, isRecord, jsonResponse } from '../index.ts'
|
|
2
|
+
import type { DocumentVoiceTasks, VoicePreviewCheck, VoicePreviewStart, VoiceoverStart } from '../contracts/voiceover-execution.ts'
|
|
3
|
+
import type { Route } from '../contracts/server.ts'
|
|
4
|
+
import { validateVoiceoverJobQuery, validateVoiceoverStart, validateVoicePreviewCheck, validateVoicePreviewStart, VoiceoverOperationError } from './voiceover.ts'
|
|
5
|
+
|
|
6
|
+
/** The legacy Web wire shapes adapt to the same author service used by MCP. */
|
|
7
|
+
export function createVoiceoverRoutes(service: DocumentVoiceTasks): Record<string, Route> {
|
|
8
|
+
const route = (handle: Route['handle']): Route => ({ role: 'editor', maxBody: 64 << 10, async handle(request, ctx) {
|
|
9
|
+
try {
|
|
10
|
+
if (!hasRole(ctx.member, 'editor')) throw new VoiceoverOperationError(403, 'voice-authoring-permission-denied')
|
|
11
|
+
return await handle(request, ctx)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const status = isRecord(error) && typeof error.status === 'number' ? error.status : 502
|
|
14
|
+
return jsonResponse({ error: errorMessage(error) }, status)
|
|
15
|
+
}
|
|
16
|
+
} })
|
|
17
|
+
const body = async (request: Request) => await request.json().catch(() => undefined) as unknown
|
|
18
|
+
const confirmed = (value: unknown) => {
|
|
19
|
+
if (!isRecord(value)) throw new VoiceoverOperationError(400, 'voice-generation-invalid')
|
|
20
|
+
if (value.confirmed !== true) throw new VoiceoverOperationError(409, 'voice-confirmation-required')
|
|
21
|
+
const { confirmed: _confirmed, ...request } = value
|
|
22
|
+
return request
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
voiceover: route(async (request, ctx) => {
|
|
26
|
+
if (request.method === 'GET') return jsonResponse(await service.snapshot(ctx.document, {}, ctx.member, request.signal))
|
|
27
|
+
if (request.method !== 'POST') throw new VoiceoverOperationError(405, 'GET or POST')
|
|
28
|
+
const value = confirmed(await body(request))
|
|
29
|
+
if (!validateVoiceoverStart(value)) throw new VoiceoverOperationError(400, 'voice-generation-invalid')
|
|
30
|
+
return jsonResponse(await service.start(ctx.document, value as VoiceoverStart, ctx.member, request.signal), 202)
|
|
31
|
+
}),
|
|
32
|
+
'voiceover/jobs': route(async (request, ctx) => {
|
|
33
|
+
if (request.method !== 'GET') throw new VoiceoverOperationError(405, 'GET')
|
|
34
|
+
return jsonResponse(await service.listJobs(ctx.document, ctx.member))
|
|
35
|
+
}),
|
|
36
|
+
'voiceover/job': route(async (request, ctx) => {
|
|
37
|
+
if (request.method !== 'GET') throw new VoiceoverOperationError(405, 'GET')
|
|
38
|
+
const query = Object.fromEntries([...new URL(request.url).searchParams].filter(([key]) => key !== 'document'))
|
|
39
|
+
if (!validateVoiceoverJobQuery(query)) throw new VoiceoverOperationError(400, 'voice-job-query-invalid')
|
|
40
|
+
return jsonResponse(await service.getJob(ctx.document, query, ctx.member))
|
|
41
|
+
}),
|
|
42
|
+
'voiceover/cancel': route(async (request, ctx) => {
|
|
43
|
+
if (request.method !== 'POST') throw new VoiceoverOperationError(405, 'POST')
|
|
44
|
+
const value = await body(request)
|
|
45
|
+
if (!validateVoiceoverJobQuery(value)) throw new VoiceoverOperationError(400, 'voice-cancel-invalid')
|
|
46
|
+
const answer = await service.cancel(ctx.document, value, ctx.member)
|
|
47
|
+
return jsonResponse('id' in value ? answer.job : answer)
|
|
48
|
+
}),
|
|
49
|
+
'voice-preview': route(async (request, ctx) => {
|
|
50
|
+
if (request.method === 'GET') {
|
|
51
|
+
const params = new URL(request.url).searchParams
|
|
52
|
+
let voice: unknown, target: unknown, source: unknown
|
|
53
|
+
try { voice = JSON.parse(params.get('voice') ?? 'null'); target = JSON.parse(params.get('target') ?? 'null'); source = JSON.parse(params.get('source') ?? 'null') }
|
|
54
|
+
catch { throw new VoiceoverOperationError(400, 'voice-preview-invalid') }
|
|
55
|
+
const query = { revision: params.get('revision'), voice, mode: params.get('mode'), ...target !== null ? { target } : {}, ...source !== null ? { source } : {} }
|
|
56
|
+
if (!validateVoicePreviewCheck(query)) throw new VoiceoverOperationError(400, 'voice-preview-invalid')
|
|
57
|
+
return jsonResponse(await service.quotePreview(ctx.document, query as VoicePreviewCheck, ctx.member, request.signal))
|
|
58
|
+
}
|
|
59
|
+
if (request.method !== 'POST') throw new VoiceoverOperationError(405, 'GET or POST')
|
|
60
|
+
const value = confirmed(await body(request))
|
|
61
|
+
if (!validateVoicePreviewStart(value)) throw new VoiceoverOperationError(400, 'voice-preview-invalid')
|
|
62
|
+
const result = await service.preview(ctx.document, value as VoicePreviewStart, ctx.member, request.signal)
|
|
63
|
+
const { resource, bytes } = await service.readPreview(ctx.document, result.id, ctx.member)
|
|
64
|
+
const { id: _id, byteLength: _length, sha256: _sha, mediaType, ...answer } = resource
|
|
65
|
+
return jsonResponse({ ...answer, audio: `data:${mediaType};base64,${Buffer.from(bytes).toString('base64')}` })
|
|
66
|
+
}),
|
|
67
|
+
'voice-preview/media': route(async (request, ctx) => {
|
|
68
|
+
if (request.method !== 'GET') throw new VoiceoverOperationError(405, 'GET')
|
|
69
|
+
const id = new URL(request.url).searchParams.get('id') ?? '', result = await service.readPreview(ctx.document, id, ctx.member)
|
|
70
|
+
return new Response(new Uint8Array(result.bytes), { headers: { 'Content-Type': result.resource.mediaType, 'Content-Length': String(result.bytes.byteLength), 'Cache-Control': 'no-store' } })
|
|
71
|
+
}),
|
|
72
|
+
}
|
|
73
|
+
}
|