@collabhut/plugin-sdk 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1046 -0
  3. package/dist/helpers/note-utils.d.ts +128 -0
  4. package/dist/helpers/note-utils.d.ts.map +1 -0
  5. package/dist/helpers/note-utils.js +155 -0
  6. package/dist/index.d.ts +13 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/types/audio-effect.d.ts +70 -0
  10. package/dist/types/audio-effect.d.ts.map +1 -0
  11. package/dist/types/audio-effect.js +1 -0
  12. package/dist/types/context.d.ts +39 -0
  13. package/dist/types/context.d.ts.map +1 -0
  14. package/dist/types/context.js +1 -0
  15. package/dist/types/events.d.ts +119 -0
  16. package/dist/types/events.d.ts.map +1 -0
  17. package/dist/types/events.js +19 -0
  18. package/dist/types/instrument.d.ts +83 -0
  19. package/dist/types/instrument.d.ts.map +1 -0
  20. package/dist/types/instrument.js +1 -0
  21. package/dist/types/licensing.d.ts +118 -0
  22. package/dist/types/licensing.d.ts.map +1 -0
  23. package/dist/types/licensing.js +27 -0
  24. package/dist/types/manifest.d.ts +90 -0
  25. package/dist/types/manifest.d.ts.map +1 -0
  26. package/dist/types/manifest.js +1 -0
  27. package/dist/types/midi-effect.d.ts +101 -0
  28. package/dist/types/midi-effect.d.ts.map +1 -0
  29. package/dist/types/midi-effect.js +1 -0
  30. package/dist/types/parameters.d.ts +76 -0
  31. package/dist/types/parameters.d.ts.map +1 -0
  32. package/dist/types/parameters.js +1 -0
  33. package/dist/types/shader.d.ts +110 -0
  34. package/dist/types/shader.d.ts.map +1 -0
  35. package/dist/types/shader.js +1 -0
  36. package/dist/types/vocal-preset.d.ts +149 -0
  37. package/dist/types/vocal-preset.d.ts.map +1 -0
  38. package/dist/types/vocal-preset.js +54 -0
  39. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,1046 @@
1
+ # @collabhut/plugin-sdk
2
+
3
+ > Official TypeScript SDK for building CollabDAW plugins.
4
+
5
+ The `@collabhut/plugin-sdk` package gives you everything you need to create,
6
+ type-check, and publish plugins for [CollabDAW](https://collabhut.com) — the
7
+ collaborative music production environment.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ 1. [Overview](#overview)
14
+ 2. [Plugin types](#plugin-types)
15
+ 3. [Installation](#installation)
16
+ 4. [Project setup](#project-setup)
17
+ 5. [Getting started — Hello World audio effect](#getting-started--hello-world-audio-effect)
18
+ 6. [Audio effect plugins](#audio-effect-plugins)
19
+ 7. [MIDI effect plugins](#midi-effect-plugins)
20
+ 8. [Instrument plugins](#instrument-plugins)
21
+ 9. [Vocal preset plugins](#vocal-preset-plugins)
22
+ 10. [Shader plugins & ShaderToy compatibility](#shader-plugins--shadertoy-compatibility)
23
+ 11. [Parameter system](#parameter-system)
24
+ 12. [Note utilities](#note-utilities)
25
+ 13. [Security & sandbox model](#security--sandbox-model)
26
+ 14. [Licensing & verification](#licensing--verification)
27
+ 15. [Collaboration access rules](#collaboration-access-rules)
28
+ 16. [Publishing to the CollabHut Marketplace](#publishing-to-the-collabhut-marketplace)
29
+ 17. [Plugin IDE & .dawplugin files](#plugin-ide--dawplugin-files)
30
+ 18. [API reference](#api-reference)
31
+ 19. [Frequently asked questions](#frequently-asked-questions)
32
+
33
+ ---
34
+
35
+ ## Overview
36
+
37
+ Plugins for CollabDAW are **TypeScript ES modules** that export a `manifest`
38
+ object and a factory function (or, for vocal presets and shaders, a data
39
+ object). They are compiled to a signed bundle by the CollabHut build service
40
+ and hosted on the CollabHut CDN.
41
+
42
+ ```
43
+ your-plugin/
44
+ src/
45
+ index.ts ← exports manifest + factory (or preset / shader)
46
+ package.json
47
+ tsconfig.json
48
+ ```
49
+
50
+ Key design decisions:
51
+
52
+ | Property | Detail |
53
+ |---|---|
54
+ | **Language** | TypeScript 5+ (strict mode required) |
55
+ | **Runtime** | Sandboxed Web Worker inside CollabDAW |
56
+ | **Audio API** | Web Audio API (`AudioContext`) — injected via `PluginContext` |
57
+ | **Network** | Only `cdn.collabhut.com` — via `context.fetchAsset()` |
58
+ | **No DOM access** | `document`, `window`, `localStorage` are unmounted |
59
+ | **Licensing** | Server-side only — no user key-entry friction |
60
+
61
+ ---
62
+
63
+ ## Plugin types
64
+
65
+ | Type | `manifest.type` | What it does |
66
+ |---|---|---|
67
+ | Audio Effect | `"audio-effect"` | Processes a live audio stream (EQ, reverb, compressor…) |
68
+ | MIDI Effect | `"midi-effect"` | Transforms or generates MIDI events (arpeggiator, chord generator…) |
69
+ | Instrument | `"instrument"` | Synthesises audio from MIDI notes (synth, sampler…) |
70
+ | Vocal Preset | `"vocal-preset"` | Configures CollabDAW's built-in vocal chain (static data, no DSP code) |
71
+ | Shader | `"shader"` | GLSL fragment shader displayed in the ShaderToy panel |
72
+
73
+ ---
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ npm install --save-dev @collabhut/plugin-sdk
79
+ # or
80
+ pnpm add -D @collabhut/plugin-sdk
81
+ ```
82
+
83
+ The package is **type-only at runtime** — all exports are TypeScript
84
+ interfaces. Helper functions (`noteToHz`, `gainToDb`, etc.) are the only
85
+ compiled JavaScript.
86
+
87
+ ---
88
+
89
+ ## Project setup
90
+
91
+ ### `tsconfig.json`
92
+
93
+ ```json
94
+ {
95
+ "compilerOptions": {
96
+ "target": "ESNext",
97
+ "module": "ESNext",
98
+ "moduleResolution": "bundler",
99
+ "strict": true,
100
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
101
+ "outDir": "dist",
102
+ "rootDir": "src",
103
+ "declaration": true,
104
+ "declarationDir": "dist",
105
+ "sourceMap": true
106
+ },
107
+ "include": ["src"]
108
+ }
109
+ ```
110
+
111
+ > `"DOM"` is required so TypeScript knows about `AudioContext`, `AudioNode`,
112
+ > `Worker`, and `SharedArrayBuffer`.
113
+
114
+ ### `package.json`
115
+
116
+ ```json
117
+ {
118
+ "name": "@your-org/my-plugin",
119
+ "version": "1.0.0",
120
+ "type": "module",
121
+ "main": "./dist/index.js",
122
+ "types": "./dist/index.d.ts",
123
+ "exports": {
124
+ ".": {
125
+ "import": "./dist/index.js",
126
+ "types": "./dist/index.d.ts"
127
+ }
128
+ },
129
+ "scripts": {
130
+ "build": "tsc"
131
+ },
132
+ "devDependencies": {
133
+ "@collabhut/plugin-sdk": "^0.1.0",
134
+ "typescript": "^5.0.0"
135
+ }
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Getting started — Hello World audio effect
142
+
143
+ This is the minimal complete plugin. It creates a `GainNode` that lets the
144
+ user control the volume from within CollabDAW.
145
+
146
+ ```ts
147
+ // src/index.ts
148
+ import type {
149
+ AudioEffectModule,
150
+ AudioEffectFactory,
151
+ PluginManifest,
152
+ } from "@collabhut/plugin-sdk"
153
+
154
+ export const manifest = {
155
+ id: "com.myorg.hello-gain",
156
+ name: "Hello Gain",
157
+ version: "1.0.0",
158
+ type: "audio-effect",
159
+ description: "A simple gain control — the Hello World of CollabDAW plugins.",
160
+ author: {
161
+ name: "My Org",
162
+ url: "https://myorg.com",
163
+ },
164
+ params: [
165
+ {
166
+ type: "range",
167
+ id: "gain",
168
+ label: "Gain",
169
+ min: 0,
170
+ max: 2,
171
+ default: 1,
172
+ decimals: 2,
173
+ },
174
+ ],
175
+ pricing: "free",
176
+ minApiVersion: "0.1.0",
177
+ } satisfies PluginManifest
178
+
179
+ const factory: AudioEffectFactory<typeof manifest.params> = (context, params) => {
180
+ const gain = context.audioContext.createGain()
181
+ gain.gain.value = params.gain as number
182
+ return {
183
+ input: gain,
184
+ output: gain,
185
+ automationTargets: { gain: gain.gain },
186
+ }
187
+ }
188
+
189
+ const mod: AudioEffectModule = { manifest, factory }
190
+ export default mod
191
+ ```
192
+
193
+ Build it:
194
+
195
+ ```bash
196
+ pnpm build
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Audio effect plugins
202
+
203
+ An audio effect plugin creates a **Web Audio subgraph** and exposes input/output
204
+ nodes. The DAW connects your plugin into the track's processing chain.
205
+
206
+ ```
207
+ Track source → [your input node] → [your DSP graph] → [your output node] → track bus
208
+ ```
209
+
210
+ ### Stereo compressor example
211
+
212
+ ```ts
213
+ // src/index.ts
214
+ import type { AudioEffectModule, AudioEffectFactory, PluginManifest } from "@collabhut/plugin-sdk"
215
+
216
+ export const manifest = {
217
+ id: "com.myorg.stereo-compressor",
218
+ name: "Stereo Compressor",
219
+ version: "1.0.0",
220
+ type: "audio-effect",
221
+ description: "Classic stereo dynamics compressor with optional side-chain.",
222
+ author: { name: "My Org" },
223
+ params: [
224
+ { type: "range", id: "threshold", label: "Threshold", min: -60, max: 0, default: -18, unit: "dB" },
225
+ { type: "range", id: "ratio", label: "Ratio", min: 1, max: 20, default: 4, decimals: 1 },
226
+ { type: "range", id: "attack", label: "Attack", min: 0, max: 1, default: 0.003, unit: "s", decimals: 3, curve: "log" },
227
+ { type: "range", id: "release", label: "Release", min: 0, max: 2, default: 0.15, unit: "s", decimals: 2, curve: "log" },
228
+ { type: "range", id: "makeUpGain",label: "Make-up", min: 0, max: 24, default: 6, unit: "dB" },
229
+ { type: "bool", id: "softClip", label: "Soft Clip", default: false },
230
+ ],
231
+ pricing: "paid",
232
+ minApiVersion: "0.1.0",
233
+ } satisfies PluginManifest
234
+
235
+ const factory: AudioEffectFactory<typeof manifest.params> = (context, params) => {
236
+ const comp = context.audioContext.createDynamicsCompressor()
237
+ comp.threshold.value = params.threshold as number
238
+ comp.ratio.value = params.ratio as number
239
+ comp.attack.value = params.attack as number
240
+ comp.release.value = params.release as number
241
+
242
+ const makeUp = context.audioContext.createGain()
243
+ makeUp.gain.value = 10 ** ((params.makeUpGain as number) / 20)
244
+
245
+ comp.connect(makeUp)
246
+
247
+ return {
248
+ input: comp,
249
+ output: makeUp,
250
+ automationTargets: {
251
+ threshold: comp.threshold,
252
+ ratio: comp.ratio,
253
+ attack: comp.attack,
254
+ release: comp.release,
255
+ },
256
+ }
257
+ }
258
+
259
+ const mod: AudioEffectModule = { manifest, factory }
260
+ export default mod
261
+ ```
262
+
263
+ ### Fetching an impulse response for a convolution reverb
264
+
265
+ ```ts
266
+ const factory: AudioEffectFactory = async (context, _params) => {
267
+ // Only cdn.collabhut.com URLs are permitted
268
+ const irBuffer = await context.fetchAsset(
269
+ "https://cdn.collabhut.com/ir/hall-large.wav"
270
+ )
271
+ const decoded = await context.audioContext.decodeAudioData(irBuffer)
272
+
273
+ const convolver = context.audioContext.createConvolver()
274
+ convolver.buffer = decoded
275
+
276
+ return { input: convolver, output: convolver }
277
+ }
278
+ ```
279
+
280
+ > `context.fetchAsset()` throws `TypeError` for any URL not on
281
+ > `cdn.collabhut.com`. The restriction is enforced in the sandbox.
282
+
283
+ ---
284
+
285
+ ## MIDI effect plugins
286
+
287
+ MIDI effects transform or generate MIDI events on a per-block basis.
288
+ Your plugin receives the incoming events and returns the processed list.
289
+
290
+ ### Arpeggiator example
291
+
292
+ ```ts
293
+ // src/index.ts
294
+ import type {
295
+ MidiEffectModule,
296
+ MidiEffectFactory,
297
+ MidiEvent,
298
+ MidiNoteOnEvent,
299
+ PluginManifest,
300
+ } from "@collabhut/plugin-sdk"
301
+ import { transpose } from "@collabhut/plugin-sdk"
302
+
303
+ export const manifest = {
304
+ id: "com.myorg.simple-arp",
305
+ name: "Simple Arpeggiator",
306
+ version: "1.0.0",
307
+ type: "midi-effect",
308
+ description: "Fans held notes into an up-down arpeggio pattern.",
309
+ author: { name: "My Org" },
310
+ params: [
311
+ {
312
+ type: "choice",
313
+ id: "pattern",
314
+ label: "Pattern",
315
+ choices: ["up", "down", "up-down"],
316
+ default: "up",
317
+ },
318
+ ],
319
+ pricing: "free",
320
+ minApiVersion: "0.1.0",
321
+ } satisfies PluginManifest
322
+
323
+ const factory: MidiEffectFactory = (_context, params) => {
324
+ const heldNotes: MidiNoteOnEvent[] = []
325
+
326
+ return {
327
+ process(events) {
328
+ const out: MidiEvent[] = []
329
+
330
+ for (const ev of events) {
331
+ if (ev.type === "note-on") {
332
+ heldNotes.push(ev)
333
+ } else if (ev.type === "note-off") {
334
+ const idx = heldNotes.findIndex(n => n.note === ev.note)
335
+ if (idx !== -1) heldNotes.splice(idx, 1)
336
+ }
337
+ // Emit original event
338
+ out.push(ev)
339
+ }
340
+
341
+ // Emit transposed copies for each held note based on pattern
342
+ const pattern = params.pattern as string
343
+ for (const held of heldNotes) {
344
+ if (pattern === "up" || pattern === "up-down") {
345
+ out.push({ ...held, note: transpose(held.note, 12) })
346
+ }
347
+ if (pattern === "up-down") {
348
+ out.push({ ...held, note: transpose(held.note, -12) })
349
+ }
350
+ }
351
+
352
+ return out
353
+ },
354
+ dispose() {
355
+ heldNotes.length = 0
356
+ },
357
+ }
358
+ }
359
+
360
+ const mod: MidiEffectModule = { manifest, factory }
361
+ export default mod
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Instrument plugins
367
+
368
+ Instrument plugins are **polyphonic synthesisers**. The DAW calls `noteOn()`
369
+ for each MIDI note and stores the returned voice. When the note ends it calls
370
+ `voice.stop()`.
371
+
372
+ ### Simple sine synth
373
+
374
+ ```ts
375
+ // src/index.ts
376
+ import type { InstrumentModule, InstrumentFactory, InstrumentVoice, PluginManifest } from "@collabhut/plugin-sdk"
377
+ import { noteToHz, dbToGain } from "@collabhut/plugin-sdk"
378
+
379
+ export const manifest = {
380
+ id: "com.myorg.sine-synth",
381
+ name: "Sine Synth",
382
+ version: "1.0.0",
383
+ type: "instrument",
384
+ description: "A pure sine-wave synthesiser with ADSR envelope.",
385
+ author: { name: "My Org" },
386
+ params: [
387
+ { type: "range", id: "attack", label: "Attack", min: 0.001, max: 2, default: 0.01, unit: "s", curve: "log", decimals: 3 },
388
+ { type: "range", id: "decay", label: "Decay", min: 0.001, max: 2, default: 0.1, unit: "s", curve: "log", decimals: 3 },
389
+ { type: "range", id: "sustain", label: "Sustain", min: 0, max: 1, default: 0.7, decimals: 2 },
390
+ { type: "range", id: "release", label: "Release", min: 0.001, max: 4, default: 0.3, unit: "s", curve: "log", decimals: 3 },
391
+ { type: "range", id: "volume", label: "Volume", min: -60, max: 0, default: -12, unit: "dB" },
392
+ ],
393
+ pricing: "free",
394
+ minApiVersion: "0.1.0",
395
+ } satisfies PluginManifest
396
+
397
+ const factory: InstrumentFactory = (context, params) => ({
398
+ noteOn(note, velocity, time): InstrumentVoice {
399
+ const { audioContext } = context
400
+
401
+ const osc = audioContext.createOscillator()
402
+ const gain = audioContext.createGain()
403
+
404
+ osc.type = "sine"
405
+ osc.frequency.value = noteToHz(note)
406
+ gain.gain.value = 0
407
+
408
+ osc.connect(gain)
409
+ osc.start(time)
410
+
411
+ const velGain = velocity / 127
412
+ const maxGain = dbToGain(params.volume as number) * velGain
413
+ const attack = params.attack as number
414
+ const decay = params.decay as number
415
+ const sustain = params.sustain as number
416
+ const release = params.release as number
417
+
418
+ // ADSR
419
+ gain.gain.linearRampToValueAtTime(maxGain, time + attack)
420
+ gain.gain.linearRampToValueAtTime(maxGain * sustain, time + attack + decay)
421
+
422
+ return {
423
+ output: gain,
424
+ stop(_vel, releaseTime) {
425
+ gain.gain.cancelScheduledValues(releaseTime)
426
+ gain.gain.setValueAtTime(gain.gain.value, releaseTime)
427
+ gain.gain.linearRampToValueAtTime(0, releaseTime + release)
428
+ osc.stop(releaseTime + release + 0.01)
429
+ },
430
+ kill() {
431
+ osc.stop()
432
+ gain.disconnect()
433
+ },
434
+ }
435
+ },
436
+ onMidi(_ev) {},
437
+ dispose() {},
438
+ })
439
+
440
+ const mod: InstrumentModule = { manifest, factory }
441
+ export default mod
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Vocal preset plugins
447
+
448
+ A vocal preset is **only data** — no JavaScript runs at playback time.
449
+ CollabDAW reads the `VocalPreset` object and applies it to the built-in vocal
450
+ engine. This means vocal presets have:
451
+
452
+ - **Zero sandbox risk** (no code execution)
453
+ - **No latency penalty**
454
+ - **Instant parameter recall**
455
+
456
+ ```ts
457
+ // src/index.ts
458
+ import type { VocalPresetModule } from "@collabhut/plugin-sdk"
459
+
460
+ const mod: VocalPresetModule = {
461
+ manifest: {
462
+ id: "com.myorg.vintage-tape-vocals",
463
+ name: "Vintage Tape Vocals",
464
+ version: "1.0.0",
465
+ type: "vocal-preset",
466
+ description: "Warm, slightly compressed vocals with subtle tape saturation.",
467
+ author: { name: "My Org" },
468
+ pricing: "paid",
469
+ minApiVersion: "0.1.0",
470
+ },
471
+ preset: {
472
+ inputGain: 0,
473
+ outputGain: -2,
474
+ eq: {
475
+ enabled: true,
476
+ bands: [
477
+ { frequency: 100, gain: -4, q: 0.7, type: "highpass" },
478
+ { frequency: 250, gain: -2, q: 1.2, type: "peaking" },
479
+ { frequency: 1500, gain: 2, q: 1.5, type: "peaking" },
480
+ { frequency: 6000, gain: 3, q: 0.9, type: "peaking" },
481
+ { frequency: 12000, gain: 1.5, q: 0.7, type: "highshelf"},
482
+ ],
483
+ },
484
+ compressor: {
485
+ enabled: true,
486
+ threshold: -20,
487
+ knee: 8,
488
+ ratio: 4,
489
+ attack: 0.005,
490
+ release: 0.12,
491
+ makeUpGain: 5,
492
+ },
493
+ deEsser: { enabled: true, frequency: 7200, threshold: -28, range: 10 },
494
+ saturation: { enabled: true, drive: 0.15, mix: 0.35 },
495
+ pitchCorrection: { enabled: false, scale: "chromatic", strength: 0, speed: 0.1 },
496
+ chorus: { enabled: false, rate: 0.5, depth: 0.3, delay: 0.02, mix: 0.3 },
497
+ reverb: { enabled: true, roomSize: 0.25, damping: 0.75, preDelay: 0.015, mix: 0.18 },
498
+ delay: { enabled: false, time: 0.25, feedback: 0.3, filter: 4000, mix: 0.2 },
499
+ },
500
+ }
501
+
502
+ export default mod
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Shader plugins & ShaderToy compatibility
508
+
509
+ Shader plugins render a GLSL fragment shader in the **ShaderToy panel** of
510
+ CollabDAW. They can be static visuals, or they can react to audio in real time.
511
+
512
+ ### Audio-reactive circle shader
513
+
514
+ ```ts
515
+ // src/index.ts
516
+ import type { ShaderModule } from "@collabhut/plugin-sdk"
517
+
518
+ const mod: ShaderModule = {
519
+ manifest: {
520
+ id: "com.myorg.audio-circle",
521
+ name: "Audio Circle",
522
+ version: "1.0.0",
523
+ type: "shader",
524
+ description: "A circle that pulses with the music.",
525
+ author: { name: "My Org" },
526
+ pricing: "free",
527
+ minApiVersion: "0.1.0",
528
+ shadertoyCompatible: false,
529
+ },
530
+ shader: {
531
+ glsl: `
532
+ precision mediump float;
533
+
534
+ uniform vec2 uResolution;
535
+ uniform float uRms;
536
+ uniform float uBeat;
537
+
538
+ void main() {
539
+ vec2 uv = (gl_FragCoord.xy / uResolution) * 2.0 - 1.0;
540
+ uv.x *= uResolution.x / uResolution.y;
541
+
542
+ float radius = 0.3 + uRms * 0.4;
543
+ float ring = smoothstep(radius + 0.01, radius, length(uv));
544
+ float pulse = 0.5 + 0.5 * sin(uBeat * 6.2831);
545
+
546
+ vec3 col = mix(vec3(0.0, 0.4, 0.8), vec3(0.8, 0.2, 0.5), pulse);
547
+ gl_FragColor = vec4(col * ring, 1.0);
548
+ }
549
+ `,
550
+ uniforms: [
551
+ { name: "uResolution", type: "vec2", source: "resolution" },
552
+ { name: "uRms", type: "float", source: "audio-rms" },
553
+ { name: "uBeat", type: "float", source: "beat" },
554
+ ],
555
+ },
556
+ }
557
+
558
+ export default mod
559
+ ```
560
+
561
+ ### ShaderToy port
562
+
563
+ When `shadertoyCompatible: true`, CollabDAW pre-injects the standard ShaderToy
564
+ uniforms so you can paste ShaderToy code almost as-is.
565
+
566
+ Pre-injected uniforms (do **not** declare these in `uniforms`):
567
+
568
+ | ShaderToy name | Type | Source |
569
+ |---|---|---|
570
+ | `iTime` | float | Playback time in seconds |
571
+ | `iResolution` | vec3 | Canvas size (z = pixel ratio) |
572
+ | `iMouse` | vec4 | Mouse position |
573
+ | `iChannel0` | sampler2D | Audio spectrum texture |
574
+
575
+ ```ts
576
+ import type { ShaderModule } from "@collabhut/plugin-sdk"
577
+
578
+ const mod: ShaderModule = {
579
+ manifest: {
580
+ id: "com.myorg.shadertoy-port",
581
+ name: "Classic Plasma",
582
+ version: "1.0.0",
583
+ type: "shader",
584
+ shadertoyCompatible: true, // ← enables automatic uniform injection
585
+ description: "ShaderToy plasma effect ported to CollabDAW.",
586
+ author: { name: "My Org" },
587
+ pricing: "free",
588
+ minApiVersion: "0.1.0",
589
+ },
590
+ shader: {
591
+ // iTime, iResolution, iMouse are injected automatically
592
+ glsl: `
593
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
594
+ vec2 uv = fragCoord / iResolution.xy;
595
+ float t = iTime;
596
+ vec3 col = 0.5 + 0.5 * cos(t + uv.xyx + vec3(0, 2, 4));
597
+ fragColor = vec4(col, 1.0);
598
+ }
599
+ `,
600
+ },
601
+ }
602
+
603
+ export default mod
604
+ ```
605
+
606
+ ---
607
+
608
+ ## Parameter system
609
+
610
+ Parameters appear as knobs, toggles, and dropdowns in the CollabDAW plugin GUI.
611
+
612
+ ### Range parameter (knob / slider)
613
+
614
+ ```ts
615
+ {
616
+ type: "range",
617
+ id: "cutoff", // unique within the plugin, used as key in params object
618
+ label: "Cutoff",
619
+ min: 20,
620
+ max: 20000,
621
+ default: 1000,
622
+ unit: "Hz", // displayed in the tooltip
623
+ curve: "log", // "linear" | "log" | "exp"
624
+ decimals: 0,
625
+ group: "Filter", // optional — groups params in the UI
626
+ }
627
+ ```
628
+
629
+ ### Bool parameter (toggle)
630
+
631
+ ```ts
632
+ {
633
+ type: "bool",
634
+ id: "bypass",
635
+ label: "Bypass",
636
+ default: false,
637
+ }
638
+ ```
639
+
640
+ ### Choice parameter (dropdown)
641
+
642
+ ```ts
643
+ {
644
+ type: "choice",
645
+ id: "filterType",
646
+ label: "Filter",
647
+ choices: ["lowpass", "highpass", "bandpass", "notch"],
648
+ default: "lowpass",
649
+ }
650
+ ```
651
+
652
+ ### Type-safe param access
653
+
654
+ The `params` argument in your factory is typed as
655
+ `Readonly<Record<string, number | boolean | string>>`. Cast to the concrete
656
+ type within your factory:
657
+
658
+ ```ts
659
+ const cutoff = params.cutoff as number // from a "range" param
660
+ const bypass = params.bypass as boolean // from a "bool" param
661
+ const filter = params.filterType as string // from a "choice" param
662
+ ```
663
+
664
+ ---
665
+
666
+ ## Note utilities
667
+
668
+ All helpers are pure functions, zero-dependency, and safe to use in Workers.
669
+
670
+ ```ts
671
+ import {
672
+ noteToHz, hzToNote, hzToNoteFractional,
673
+ midiToName, nameToMidi,
674
+ semitonesBetween, transpose,
675
+ beatsToSeconds, secondsToBeats,
676
+ gainToDb, dbToGain,
677
+ clamp, lerp,
678
+ } from "@collabhut/plugin-sdk"
679
+
680
+ noteToHz(69) // 440 (A4)
681
+ noteToHz(60) // 261.63 (C4, middle C)
682
+ hzToNote(440) // 69
683
+ hzToNoteFractional(450) // 69.39 (fractional for pitch detection)
684
+ midiToName(60) // "C4"
685
+ midiToName(57) // "A3"
686
+ nameToMidi("A", 4) // 69
687
+ transpose(60, 7) // 67 (C4 → G4, perfect fifth)
688
+ transpose(60, -12) // 48 (C4 → C3, one octave down)
689
+ semitonesBetween(60, 67)// 7
690
+
691
+ beatsToSeconds(1, 120) // 0.5
692
+ secondsToBeats(0.5, 120)// 1
693
+
694
+ gainToDb(1) // 0
695
+ gainToDb(0.5) // -6.02
696
+ dbToGain(0) // 1
697
+ dbToGain(-6) // 0.501
698
+
699
+ clamp(1.5, 0, 1) // 1
700
+ lerp(0, 100, 0.5) // 50
701
+ ```
702
+
703
+ ---
704
+
705
+ ## Security & sandbox model
706
+
707
+ ### What plugins can do
708
+
709
+ - Use the Web Audio API through `context.audioContext`
710
+ - Fetch binary assets from `cdn.collabhut.com` via `context.fetchAsset()`
711
+ - Use `SharedArrayBuffer` for lock-free transport reads (provided by the host)
712
+ - Use standard Web APIs available in Workers: `crypto`, `TextEncoder`,
713
+ `URL`, `Math`, `JSON`, `console`
714
+
715
+ ### What plugins cannot do
716
+
717
+ | Blocked | Reason |
718
+ |---|---|
719
+ | `fetch()` to arbitrary URLs | Prevented at the sandbox level; only `context.fetchAsset()` is available and it checks `cdn.collabhut.com` |
720
+ | `XMLHttpRequest` | Not available in Workers by default |
721
+ | DOM access (`document`, `window`) | Workers have no DOM |
722
+ | `localStorage`, `sessionStorage`, `IndexedDB` | Storage isolation |
723
+ | `WebSocket`, `WebRTC` | Network isolation |
724
+ | `navigator.mediaDevices` | Mic/camera are host-only |
725
+ | `eval()`, `new Function()` | Dynamic code execution is prohibited in the build validator |
726
+
727
+ ### How the sandbox is enforced
728
+
729
+ 1. **Build-time**: The CollabHut build service analyses your compiled bundle
730
+ with static analysis. Prohibited APIs (`eval`, `Function`, `XMLHttpRequest`,
731
+ direct `fetch`) cause the build to be rejected.
732
+ 2. **Runtime**: The plugin Worker is created with a strict
733
+ `Content-Security-Policy` that blocks all network access except
734
+ `cdn.collabhut.com`.
735
+ 3. **CDN fetch proxy**: All asset fetches go through `context.fetchAsset()`
736
+ which validates the URL before making the request on the main thread.
737
+
738
+ ### Malicious code detection
739
+
740
+ The build service scans for:
741
+
742
+ - Obfuscated strings that expand to prohibited API names
743
+ - Attempts to access `globalThis` to bypass restrictions
744
+ - Prototype pollution patterns
745
+ - Infinite loops without exit conditions
746
+
747
+ Submissions that fail these checks are rejected and the developer account is
748
+ flagged for manual review.
749
+
750
+ ---
751
+
752
+ ## Licensing & verification
753
+
754
+ ### How it works (no user friction)
755
+
756
+ CollabDAW verifies plugin licenses **server-side** when a project containing a
757
+ plugin is opened. The flow is:
758
+
759
+ ```
760
+ DAW opens project
761
+ → DAW sends session token + plugin manifest IDs to CollabHut API
762
+ → API returns signed LicenseToken per plugin (or denial)
763
+ → DAW validates token signature against embedded public key
764
+ → Plugin is loaded (granted) or an in-app notice is shown (denied)
765
+ ```
766
+
767
+ There is **no key-entry dialog** — users never type a license key.
768
+
769
+ ### Token lifetime
770
+
771
+ Tokens are short-lived (typically 1 hour) so revocations take effect quickly.
772
+ The DAW re-verifies automatically in the background.
773
+
774
+ ### Denial reasons
775
+
776
+ | Code | Shown when |
777
+ |---|---|
778
+ | `"not-purchased"` | User has not bought the plugin |
779
+ | `"revoked"` | License was revoked (charge-back, TOS violation) |
780
+ | `"collab-completed"` | The collaboration granting access is finished |
781
+ | `"collab-not-member"` | User was removed from the collaboration |
782
+ | `"expired"` | Token is past expiry — re-verification needed |
783
+ | `"plugin-delisted"` | Plugin was removed from the marketplace |
784
+
785
+ ---
786
+
787
+ ## Collaboration access rules
788
+
789
+ > **This section describes mandatory policy — deviation is not permitted.**
790
+
791
+ A user who does **not** directly own a paid plugin may still use it **if all of
792
+ the following conditions are met simultaneously**:
793
+
794
+ 1. They are a participant in an **active** (non-completed) collaboration on
795
+ CollabHut.
796
+ 2. At least one **other** participant in that same collaboration owns a valid
797
+ license for the plugin.
798
+ 3. The plugin's `manifest.pricing` is `"paid"` (free plugins require no
799
+ license at all).
800
+
801
+ ### What "active" means
802
+
803
+ A collaboration is active while its `status` field in the database is
804
+ `"active"` or `"pending"`. Once it is marked `"completed"`, collab-access
805
+ tokens are revoked at the next re-verification cycle.
806
+
807
+ ### What non-owners can and cannot do
808
+
809
+ | Action | Non-owner with collab access | Without collab access |
810
+ |---|---|---|
811
+ | Use plugin during project session | ✅ | ❌ |
812
+ | Export / bounce with plugin active | ✅ (while collab active) | ❌ |
813
+ | Export after collab completes | ❌ | ❌ |
814
+ | Upload plugin to marketplace | ❌ (only the license owner can) | ❌ |
815
+
816
+ ### Developer responsibility
817
+
818
+ You **do not** need to implement these rules in your plugin code — they are
819
+ enforced by CollabDAW and the CollabHut API. The `LicenseCheckResult` your
820
+ plugin receives is already resolved.
821
+
822
+ This policy is also documented in:
823
+ - CollabDAW's in-app scripting manual (Docs panel, `/docs`)
824
+ - The CollabHut Marketplace terms of service
825
+ - The developer agreement you accept when publishing
826
+
827
+ ---
828
+
829
+ ## Publishing to the CollabHut Marketplace
830
+
831
+ ### Build your plugin
832
+
833
+ ```bash
834
+ pnpm build
835
+ # or
836
+ npx tsc
837
+ ```
838
+
839
+ Verify the output:
840
+
841
+ ```bash
842
+ ls dist/
843
+ # index.js index.d.ts index.js.map
844
+ ```
845
+
846
+ ### Validate locally
847
+
848
+ ```bash
849
+ npx collabhut-plugin-validate dist/index.js
850
+ ```
851
+
852
+ This runs the same static analysis as the build service.
853
+
854
+ ### Create a Marketplace listing
855
+
856
+ 1. Go to [collabhut.com/marketplace/developer](https://collabhut.com/marketplace/developer)
857
+ 2. Click **New Plugin Listing**
858
+ 3. Fill in:
859
+ - Plugin name and description (pulled from `manifest` on upload)
860
+ - Pricing: free or paid (must match `manifest.pricing`)
861
+ - Category, tags, cover image
862
+ 4. Upload `dist/index.js` — the build service signs the bundle
863
+ 5. After approval (automated for free, manual review for paid), the plugin
864
+ goes live
865
+
866
+ ### Bundle upload rules
867
+
868
+ | Rule | Detail |
869
+ |---|---|
870
+ | Single file only | `index.js` — no external dependencies at runtime |
871
+ | Max bundle size | 2 MB (request an increase for sample-based instruments) |
872
+ | No `.dawplugin` uploads | Plugin IDE working files are not marketplace bundles |
873
+ | No source maps required | Strip them for smaller uploads |
874
+ | No TypeScript source | Compiled JS only |
875
+
876
+ ### Versioning
877
+
878
+ Follow semantic versioning. To publish an update:
879
+ 1. Increment `version` in your `manifest` object
880
+ 2. Rebuild and upload the new bundle
881
+ 3. Old installs auto-update when users next open a project containing the plugin
882
+
883
+ ### Pricing
884
+
885
+ - **Free plugins**: Unlimited downloads, no review delay
886
+ - **Paid plugins**: Set a price in USD (minimum $0.99). 80% revenue share.
887
+ Manual review takes 1–3 business days.
888
+
889
+ ---
890
+
891
+ ## Plugin IDE & .dawplugin files
892
+
893
+ CollabDAW includes a built-in **Plugin IDE** accessible from the sidebar
894
+ (Plugins screen). It lets you:
895
+
896
+ - Write and edit plugin TypeScript in a code editor
897
+ - Preview shader output in real time
898
+ - Test audio/MIDI effect output on a test signal
899
+ - Save your work as a **`.dawplugin` file** — a local project format
900
+
901
+ ### .dawplugin format
902
+
903
+ A `.dawplugin` file is a ZIP archive containing:
904
+
905
+ ```
906
+ my-plugin.dawplugin/
907
+ plugin.json ← manifest + IDE metadata
908
+ src/
909
+ index.ts ← your TypeScript source
910
+ assets/ ← local test assets (not included in marketplace bundle)
911
+ ```
912
+
913
+ **`.dawplugin` files are for development only.** They cannot be uploaded to
914
+ the marketplace directly — you must build the compiled bundle first.
915
+
916
+ ### Workflow
917
+
918
+ ```
919
+ IDE → .dawplugin → tsc → dist/index.js → collabhut-plugin-validate → marketplace upload
920
+ ```
921
+
922
+ ---
923
+
924
+ ## API reference
925
+
926
+ ### `PluginManifest`
927
+
928
+ | Field | Type | Required | Description |
929
+ |---|---|---|---|
930
+ | `id` | `string` | ✅ | Globally unique reverse-domain ID, e.g. `"com.myorg.plugin-name"` |
931
+ | `name` | `string` | ✅ | Display name |
932
+ | `version` | `SemVer` | ✅ | Semantic version string, e.g. `"1.2.3"` |
933
+ | `type` | `PluginType` | ✅ | One of the five plugin types |
934
+ | `description` | `string` | ✅ | Short description (max 200 chars) |
935
+ | `author` | `PluginAuthor` | ✅ | Author name, optional URL and email |
936
+ | `homepage` | `string` | — | Plugin or developer homepage URL |
937
+ | `tags` | `string[]` | — | Discoverability tags |
938
+ | `params` | `PluginParam[]` | — | Parameter definitions |
939
+ | `pricing` | `"free" \| "paid"` | ✅ | Must match marketplace listing |
940
+ | `minApiVersion` | `SemVer` | ✅ | Minimum CollabDAW API version required |
941
+ | `shadertoyCompatible` | `boolean` | — | Only for `type: "shader"` |
942
+
943
+ ### `PluginContext`
944
+
945
+ Injected into every factory function.
946
+
947
+ | Property | Type | Description |
948
+ |---|---|---|
949
+ | `audioContext` | `AudioContext` | The track's Web Audio context |
950
+ | `sampleRate` | `number` | Current sample rate in Hz |
951
+ | `bpm` | `number` | Current session BPM |
952
+ | `positionBeats` | `number` | Current playback position in beats |
953
+ | `isPlaying` | `boolean` | Whether the transport is playing |
954
+ | `fetchAsset(url)` | `Promise<ArrayBuffer>` | Fetch a CDN asset (CDN-only) |
955
+
956
+ `InstrumentContext` extends `PluginContext` with:
957
+
958
+ | Property | Type | Description |
959
+ |---|---|---|
960
+ | `maxPolyphony` | `number` | Maximum simultaneous voices the host will trigger |
961
+
962
+ ### `AudioEffectIO`
963
+
964
+ Returned by an audio-effect factory.
965
+
966
+ | Property | Type | Required | Description |
967
+ |---|---|---|---|
968
+ | `input` | `AudioNode` | ✅ | Host connects track signal here |
969
+ | `output` | `AudioNode` | ✅ | Plugin outputs processed audio here |
970
+ | `sidechain` | `AudioNode` | — | Optional side-chain input |
971
+ | `automationTargets` | `Record<string, AudioParam>` | — | Automation-capable params |
972
+
973
+ ### `MidiEffectPlugin`
974
+
975
+ | Method | Signature | Description |
976
+ |---|---|---|
977
+ | `process` | `(events: ReadonlyArray<MidiEvent>) => MidiEvent[]` | Process one audio block |
978
+ | `dispose` | `() => void` | Clean up resources |
979
+
980
+ ### `InstrumentVoice`
981
+
982
+ | Member | Description |
983
+ |---|---|
984
+ | `output: AudioNode` | Audio output for this voice |
985
+ | `stop(velocity, time)` | Begin release phase |
986
+ | `kill()` | Immediate hard stop (no release) |
987
+
988
+ ### `InstrumentPlugin`
989
+
990
+ | Method | Description |
991
+ |---|---|
992
+ | `noteOn(note, velocity, time)` | Start a new voice |
993
+ | `onMidi(event)` | Handle non-note MIDI events |
994
+ | `dispose()` | Clean up resources |
995
+
996
+ ---
997
+
998
+ ## Frequently asked questions
999
+
1000
+ **Can I use npm packages in my plugin?**
1001
+
1002
+ Only packages that compile to pure ES2022+ JavaScript with no DOM or Node.js
1003
+ dependencies. All dependencies must be bundled into your single `index.js`.
1004
+ Use a bundler like `esbuild` or `rollup` before upload.
1005
+
1006
+ **Can I use WebAssembly?**
1007
+
1008
+ Yes — load your `.wasm` module via `context.fetchAsset()` and instantiate with
1009
+ `WebAssembly.instantiate()`. The `.wasm` file must be hosted on
1010
+ `cdn.collabhut.com`; contact support to have assets uploaded there.
1011
+
1012
+ **Can I access the microphone?**
1013
+
1014
+ No — audio input is managed exclusively by the host. For instrument plugins
1015
+ the host provides a MIDI stream; for audio effects the host provides the
1016
+ rendered audio block.
1017
+
1018
+ **How do I test my plugin locally before publishing?**
1019
+
1020
+ Use the Plugin IDE built into CollabDAW (sidebar → Plugins). It gives you a
1021
+ live preview environment with a test signal.
1022
+
1023
+ **My plugin uses external SDKs (Tone.js, etc.) — is that allowed?**
1024
+
1025
+ You can bundle them, but they must not attempt network access or DOM manipulation.
1026
+ Tone.js is partially safe; avoid its transport and timeline features
1027
+ (which inject into `window`) and use only its DSP utilities.
1028
+
1029
+ **Can I use `async` / `await` in my factory functions?**
1030
+
1031
+ Yes — the DAW awaits the factory if it returns a `Promise`. Use it freely
1032
+ for one-shot setup like loading impulse responses or WASM modules. The plugin
1033
+ will not be connected to the audio graph until the promise resolves, so there
1034
+ is no risk of glitches.
1035
+
1036
+ **What happens to collab-access plugins when a project is opened outside of a collab?**
1037
+
1038
+ The plugin will show a "License not available outside collaboration" notice.
1039
+ The track will be muted until either the user purchases the plugin or they open
1040
+ the project from within an active collaboration where a participant owns a
1041
+ license.
1042
+
1043
+ ---
1044
+
1045
+ *For support and community discussion, visit the
1046
+ [CollabHut Developer Discord](https://discord.gg/collabhut).*