@stripchatdev/create-stripchat-extension 0.1.7

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 (91) hide show
  1. package/README.md +35 -0
  2. package/dist/bin/cli.d.ts +2 -0
  3. package/dist/bin/cli.js +176 -0
  4. package/dist/package.json +28 -0
  5. package/dist/src/constants.d.ts +19 -0
  6. package/dist/src/constants.js +48 -0
  7. package/dist/src/executor.d.ts +2 -0
  8. package/dist/src/executor.js +22 -0
  9. package/dist/src/executor.test.d.ts +1 -0
  10. package/dist/src/executor.test.js +77 -0
  11. package/dist/src/helpers/detectPackageManager.d.ts +2 -0
  12. package/dist/src/helpers/detectPackageManager.js +12 -0
  13. package/dist/src/helpers/detectPackageManager.test.d.ts +1 -0
  14. package/dist/src/helpers/detectPackageManager.test.js +47 -0
  15. package/dist/src/helpers/isDirectory.d.ts +1 -0
  16. package/dist/src/helpers/isDirectory.js +9 -0
  17. package/dist/src/helpers/isDirectory.test.d.ts +1 -0
  18. package/dist/src/helpers/isDirectory.test.js +22 -0
  19. package/dist/src/helpers/isTypescriptOnlyFile.d.ts +1 -0
  20. package/dist/src/helpers/isTypescriptOnlyFile.js +5 -0
  21. package/dist/src/helpers/isTypescriptOnlyFile.test.d.ts +1 -0
  22. package/dist/src/helpers/isTypescriptOnlyFile.test.js +22 -0
  23. package/dist/src/helpers/normalizeRelPath.d.ts +1 -0
  24. package/dist/src/helpers/normalizeRelPath.js +1 -0
  25. package/dist/src/helpers/normalizeRelPath.test.d.ts +1 -0
  26. package/dist/src/helpers/normalizeRelPath.test.js +19 -0
  27. package/dist/src/helpers/resolveTemplateExtension.d.ts +1 -0
  28. package/dist/src/helpers/resolveTemplateExtension.js +17 -0
  29. package/dist/src/helpers/resolveTemplateExtension.test.d.ts +1 -0
  30. package/dist/src/helpers/resolveTemplateExtension.test.js +50 -0
  31. package/dist/src/helpers/resolveTemplatesDir.d.ts +1 -0
  32. package/dist/src/helpers/resolveTemplatesDir.js +11 -0
  33. package/dist/src/helpers/resolveTemplatesDir.test.d.ts +1 -0
  34. package/dist/src/helpers/resolveTemplatesDir.test.js +21 -0
  35. package/dist/src/mappers/mapProjectConfigToTemplateModel.d.ts +2 -0
  36. package/dist/src/mappers/mapProjectConfigToTemplateModel.js +26 -0
  37. package/dist/src/mappers/mapProjectConfigToTemplateModel.test.d.ts +1 -0
  38. package/dist/src/mappers/mapProjectConfigToTemplateModel.test.js +89 -0
  39. package/dist/src/planner.d.ts +2 -0
  40. package/dist/src/planner.js +126 -0
  41. package/dist/src/planner.test.d.ts +1 -0
  42. package/dist/src/planner.test.js +46 -0
  43. package/dist/src/types.d.ts +50 -0
  44. package/dist/src/types.js +1 -0
  45. package/dist/src/validators/index.d.ts +6 -0
  46. package/dist/src/validators/index.js +6 -0
  47. package/dist/src/validators/isValidCategory.d.ts +2 -0
  48. package/dist/src/validators/isValidCategory.js +2 -0
  49. package/dist/src/validators/isValidCategory.test.d.ts +1 -0
  50. package/dist/src/validators/isValidCategory.test.js +10 -0
  51. package/dist/src/validators/isValidFramework.d.ts +2 -0
  52. package/dist/src/validators/isValidFramework.js +2 -0
  53. package/dist/src/validators/isValidFramework.test.d.ts +1 -0
  54. package/dist/src/validators/isValidFramework.test.js +10 -0
  55. package/dist/src/validators/isValidLanguage.d.ts +2 -0
  56. package/dist/src/validators/isValidLanguage.js +2 -0
  57. package/dist/src/validators/isValidLanguage.test.d.ts +1 -0
  58. package/dist/src/validators/isValidLanguage.test.js +10 -0
  59. package/dist/src/validators/validateName.d.ts +1 -0
  60. package/dist/src/validators/validateName.js +10 -0
  61. package/dist/src/validators/validateName.test.d.ts +1 -0
  62. package/dist/src/validators/validateName.test.js +33 -0
  63. package/dist/src/validators/validateProjectDir.d.ts +1 -0
  64. package/dist/src/validators/validateProjectDir.js +6 -0
  65. package/dist/src/validators/validateProjectDir.test.d.ts +1 -0
  66. package/dist/src/validators/validateProjectDir.test.js +19 -0
  67. package/dist/src/validators/validateSlots.d.ts +5 -0
  68. package/dist/src/validators/validateSlots.js +15 -0
  69. package/dist/src/validators/validateSlots.test.d.ts +1 -0
  70. package/dist/src/validators/validateSlots.test.js +34 -0
  71. package/dist/tsconfig.tsbuildinfo +1 -0
  72. package/package.json +28 -0
  73. package/templates/.gitignore.ejs +33 -0
  74. package/templates/AGENTS.md.ejs +503 -0
  75. package/templates/CLAUDE.md +1 -0
  76. package/templates/README.md.ejs +38 -0
  77. package/templates/package.json.ejs +19 -0
  78. package/templates/public/icon.svg +4 -0
  79. package/templates/public/manifest.json.ejs +16 -0
  80. package/templates/public/resolveSlotPage.js.ejs +10 -0
  81. package/templates/src/slots/background/app.ts.ejs +37 -0
  82. package/templates/src/slots/background/background.html.ejs +16 -0
  83. package/templates/src/slots/background/main.ts.ejs +3 -0
  84. package/templates/src/slots/slot/app.ts.ejs +22 -0
  85. package/templates/src/slots/slot/app.tsx.ejs +22 -0
  86. package/templates/src/slots/slot/main.css.ejs +30 -0
  87. package/templates/src/slots/slot/main.ts.ejs +3 -0
  88. package/templates/src/slots/slot/main.tsx.ejs +9 -0
  89. package/templates/src/slots/slot/slot.html.ejs +21 -0
  90. package/templates/tsconfig.json.ejs +26 -0
  91. package/templates/vite.config.ts.ejs +23 -0
@@ -0,0 +1,503 @@
1
+ # <%= name %>
2
+ Use this file as the primary coding contract for this starter. Use the Stripchat docs to get more details.
3
+ - Category: <%= config.category %>
4
+ - Slots: <%= slots.join(', ') %>
5
+ - Framework: <%= framework %>
6
+ - Language: <%= language %>
7
+
8
+ ## Starter Map
9
+ - public/manifest.json declares slots, page keys, optional settings entry, and backend actions.
10
+ - public/resolveSlotPage.js is the runtime resolver. It returns a page key from views.pages or null.
11
+ - Root HTML files like <%= slots[0] %>.html and background.html are emitted into the final archive root.
12
+ - Visual slot entry modules live in src/slots/<slot>/main.<%= isJsxFramework ? jsxExt : ext %>.
13
+ - Visual slot page code lives in src/slots/<slot>/app.<%= isJsxFramework ? jsxExt : ext %>.
14
+ - Background entry module lives in src/slots/background/main.<%= ext %>.
15
+ - Background page code lives in src/slots/background/app.<%= ext %>.
16
+ - If you add settings.html, it is also a root HTML file and must have its own entry module.
17
+ - vite.config.<%= isTypescript ? 'ts' : 'js' %> must include every root HTML file in build.rollupOptions.input.
18
+
19
+ ## Minimal views shape:
20
+ ```json
21
+ {
22
+ "version": "v2.0",
23
+ "views": {
24
+ "slots": ["mainGameFun", "background"],
25
+ "pages": {
26
+ "mainGameFun": "mainGameFun.html",
27
+ "background": "background.html"
28
+ },
29
+ "resolveSlotPageScript": "resolveSlotPage.js"
30
+ }
31
+ }
32
+ ```
33
+ ## Hard Rules
34
+ - Call createExtHelper() exactly once in the first executed module of every page before any other SDK logic.
35
+ - In this starter, keep createExtHelper() in each page's app.* file.
36
+ - If you add settings.html, call createExtHelper() there too.
37
+ - Keep public/resolveSlotPage.js as plain synchronous JS. Do not bundle it, make it async, or import app code into it.
38
+ - public/resolveSlotPage.js must return only a key from public/manifest.json views.pages or null.
39
+ - settings.html does not go through public/resolveSlotPage.js.
40
+ - Every views.pages key must map to a real root HTML file.
41
+ - Every root HTML file must be included in vite.config.<%= isTypescript ? 'ts' : 'js' %> build.rollupOptions.input.
42
+ - Non-background slots can unmount at any time. Do not keep durable state, timers, polling, or coordination logic in them.
43
+ - Keep durable state, timers, polling, and cross-slot coordination in background.
44
+ - If you subscribe to SDK events, DOM events, or timers, clean them up on unmount or teardown.
45
+ - Use named handler functions when subscribing so you can unsubscribe the same reference.
46
+ - Slots can unmount, so you should always unsubscribe from sdk events and browser apis correctly.
47
+ - Use v1.ext.whisper.local for communication between this extension's slots in the current browser tab.
48
+ - Use v1.ext.whisper only when every loaded client in the room should receive the message.
49
+ - When a user sends v1.ext.whisper during a public show, paymentData is required. Obtain it from v1.payment.tokens.spend.succeeded. The model can whisper without paymentData.
50
+ - Both local and broadcast whispers arrive through the same event: v1.ext.whispered.
51
+ - Always include a stable type field in whisper payloads.
52
+ - Use v1.payment.tokens.spend.succeeded to confirm a spend in the paying client's iframe.
53
+ - Use v1.tokens.spent in the model/background iframe to react to the room-side tip.
54
+ - Do not add Save or Cancel buttons inside settings.html. The platform renders them outside the iframe.
55
+ - Do not use viewport @media queries. The iframe viewport is the slot container, not the browser window.
56
+ - Do not manage iframe scrolling with overflow: auto or overflow: scroll on html or body.
57
+ - Set background styles on <html>, not <body>.
58
+ - Use CSS variables with html[data-theme='dark'] and html[data-theme='light'] selectors for theming.
59
+ - The SDK sets html[data-theme] when createExtHelper() runs.
60
+ - Do not access the parent DOM.
61
+ - Do not rely on host cookies.
62
+ - Actions are strictly reviewed before publishing. Use it only when necessary (e.g. third-party API calls with secrets).
63
+ - If you use moveableOverlay, define moveableSlot.width and moveableSlot.height in the manifest. Max size is 450x740.
64
+ - Do not invent new slot names or slot type constants.
65
+ - Do not invent SDK request names, event names, method names, manifest field names, or payload keys. Use only documented names from the Stripchat docs and this starter.
66
+ - Prefer extending the generated wiring over replacing it.
67
+ - In TypeScript, treat settings and ad-hoc payloads as unknown until narrowed.
68
+ - Keep public/manifest.json valid JSON with no trailing commas.
69
+ - In TypeScript, import types from @stripchatdev/ext-helper and use them
70
+ - ALWAYS USE background slot to handle long-running orchestration logic — timers, polling, state machines, centralized state management, events, whispers
71
+ - The flow should be: visual slot -> whisper.local to background -> background handles logic. Background should subscribe to events and coordinate it
72
+ - Create background slot for each actor (model, viewer). Do not put model-only and viewer-only logic in the same background.
73
+
74
+ ## Slot Guardrails
75
+ - Generated slot sets for this starter:
76
+ - Games & Fun: mainGameFun, background, optional rightOverlay
77
+ - Sex Toys: mainSexToy, background, optional moveableOverlay
78
+ - Resolver slotType constants:
79
+ - mainGameFun -> EXTENSION_SLOT_MAIN_GAME_FUN
80
+ - mainSexToy -> EXTENSION_SLOT_MAIN_SEX_TOY
81
+ - rightOverlay -> EXTENSION_SLOT_RIGHT_OVERLAY
82
+ - moveableOverlay -> EXTENSION_SLOT_MOVEABLE_OVERLAY
83
+ - background -> EXTENSION_SLOT_BACKGROUND
84
+
85
+ ## Default Runtime Pattern
86
+ - background owns long-lived state and coordination.
87
+ - Visual slots render UI from current state and forward user actions.
88
+ - Load initial context once with v1.ext.context.get.
89
+ - Keep context fresh with v1.ext.context.updated. Payload is { context: TV1ExtContext }.
90
+ - Send same-tab state updates with v1.ext.whisper.local.
91
+ - If all loaded clients need the event, use v1.ext.whisper.
92
+ - Assume every visual slot can mount, unmount, and remount without notice.
93
+
94
+ ## Resolver
95
+ - Start from the generated resolver and extend it only when needed.
96
+ - Keep resolver logic cheap, synchronous, and defensive.
97
+ - context.user may be missing when the visitor is anonymous.
98
+ - extSettings may be missing or partial.
99
+ - Return null when a slot should not render.
100
+ - If you add model, viewer, or guest variants, add new page keys and branch here.
101
+ - Every returned string must be a key in manifest.json views.pages.
102
+
103
+ ### Generated resolver for this project:
104
+ ```
105
+ export default function resolveSlotPage(slotType) {
106
+ switch (slotType) {
107
+ <% slotResolverEntries.forEach((entry) => { -%>
108
+ case '<%= entry.slotType %>':
109
+ return '<%= entry.slot %>';
110
+ <% }); -%>
111
+ default:
112
+ return null;
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Example of context-aware branching:
118
+
119
+ ```
120
+ export default function resolveSlotPage(slotType, context, extSettings) {
121
+ const isCurrentUserModel = context.user?.id === context.model?.id || context.user?.isModel;
122
+ if (slotType === 'EXTENSION_SLOT_MAIN_GAME_FUN') {
123
+ if (isCurrentUserModel) return 'mainGameFunModel';
124
+ if (!context.user) return 'mainGameFunGuest';
125
+ return 'mainGameFunViewer';
126
+ }
127
+ if (slotType === 'EXTENSION_SLOT_BACKGROUND') {
128
+ return 'background';
129
+ }
130
+ return null;
131
+ }
132
+ ```
133
+
134
+
135
+ ## Runtime Context
136
+ - context.model is the broadcaster for the current room.
137
+ - context.user is the current user running the extension.
138
+ - If context.user is missing, the visitor is anonymous.
139
+ - If context.user?.id === context.model?.id or context.user?.isModel is true, the current user is the broadcaster.
140
+ - Common model statuses: public, private, exclusivePrivate, groupShow, ticketShow, idle, off.
141
+ - Common user statuses: public, private, exclusivePrivate, groupShow, ticketShow, spy, off.
142
+ - High-value branching fields:
143
+ - context.model?.id
144
+ - context.model?.status
145
+ - context.user?.id
146
+ - context.user?.isModel
147
+ - context.user?.status
148
+ - context.user?.hasTokens
149
+
150
+ ## Entity Shapes
151
+
152
+ ### TV1ExtContext
153
+ - model?: TV1ExtModel
154
+ - user?: TV1ExtUser
155
+
156
+ ### TV1ExtModel
157
+ - id: number
158
+ - username: string
159
+ - name: string
160
+ - avatarUrlThumb: string
161
+ - gender: 'male' | 'female' | 'group' | 'tranny'
162
+ - status: 'public' | 'private' | 'exclusivePrivate' | 'groupShow' | 'ticketShow' | 'idle' | 'off'
163
+ - isPrivateEnabled: boolean
164
+ - isSpyEnabled: boolean
165
+ - isExclusivePrivateEnabled: boolean
166
+ - isNonNude: boolean
167
+
168
+ ### TV1ExtUser
169
+ - id: number
170
+ - username: string
171
+ - status: 'public' | 'private' | 'exclusivePrivate' | 'groupShow' | 'ticketShow' | 'spy' | 'off'
172
+ - hasTokens: boolean
173
+ - hasPaidBefore: boolean
174
+ - hasUltimateSubscription: boolean
175
+ - isModel: boolean
176
+ - userRanking?: { league: string; level: number }
177
+
178
+ ### TV1PaymentData
179
+ - amount: string
180
+ - paymentToken: string
181
+ - transactionId: string
182
+ - userId: string
183
+
184
+ ### TV1TipData (payload of v1.tokens.spent)
185
+ - amount: number
186
+ - isAnonymous: boolean
187
+ - isFromTheCurrentUser: boolean
188
+ - isOriginalSource: boolean
189
+ - message: string | undefined
190
+ - source: 'console' | 'tipMenu' | 'goal' | 'epicGoal' | 'fullscreen' | 'interactiveToy' | 'publicChat' | 'privateChat' | 'extension' | 'sendTipButton' | 'feed'
191
+ - type: 'private' | 'public'
192
+ - user: TV1ExtUser | null | undefined
193
+
194
+
195
+ ## Common SDK Calls
196
+ - v1.ext.context.get -> load initial room context.
197
+ - v1.ext.context.updated -> react to context changes. Event payload is { context: TV1ExtContext }.
198
+ - v1.ext.whisper.local -> send same-tab messages between this extension's iframes.
199
+ - v1.ext.whisper -> broadcast to all loaded clients in the room. Users in public shows must include paymentData from v1.payment.tokens.spend.succeeded.
200
+ - v1.ext.whispered -> receive both local and broadcast whispers.
201
+ - v1.chat.message.send -> send a chat message on behalf of the extension.
202
+ - v1.ext.settings.get -> read saved settings in slot pages and background.
203
+ - v1.model.ext.settings.get -> load saved settings inside settings.html.
204
+ - v1.model.ext.settings.set.requested -> platform save event for settings.html.
205
+ - v1.model.ext.settings.set -> persist settings from settings.html.
206
+ - v1.ext.actions.call -> call backend actions declared in the manifest.
207
+ - v1.payment.tokens.spend -> request token spend from the current user. Result is null; confirmation arrives via event.
208
+ - v1.payment.tokens.spend.succeeded -> confirm successful token spend on the paying client. Payload includes paymentData (TV1PaymentData) needed for v1.ext.whisper in public shows.
209
+ - v1.tokens.spent -> receive room-side tip events. Should be used in background to react on tips (e.g. for toys)
210
+ - v1.private.request -> request a private show with the broadcaster.
211
+ - v1.ext.signup.open -> open sign-up for anonymous users.
212
+ - v1.ext.activity.request -> keep the extension active for a duration. Returns { isGranted: boolean }.
213
+ - v1.ext.movableOverlay.update -> update moveableOverlay status pill (status + message). Sex Toys only.
214
+ - v1.monitoring.report.error -> report meaningful failures.
215
+ - v1.monitoring.report.log -> report important lifecycle events or fallback decisions.
216
+
217
+ ## Canonical Examples and Practices
218
+
219
+ ### Settings save flow in settings.html
220
+ ```
221
+ import { createExtHelper } from '@stripchatdev/ext-helper';
222
+ const ext = createExtHelper();
223
+
224
+ const DEFAULT_SETTINGS = {
225
+ isEnabled: true,
226
+ viewerMessage: 'Welcome to the game',
227
+ };
228
+
229
+ const toSettings = (value) => {
230
+ if (!value || typeof value !== 'object') {
231
+ return DEFAULT_SETTINGS;
232
+ }
233
+ const input = /** @type {Record<string, unknown>} */ (value);
234
+ return {
235
+ isEnabled:
236
+ typeof input.isEnabled === 'boolean' ? input.isEnabled : DEFAULT_SETTINGS.isEnabled,
237
+ viewerMessage:
238
+ typeof input.viewerMessage === 'string'
239
+ ? input.viewerMessage
240
+ : DEFAULT_SETTINGS.viewerMessage,
241
+ };
242
+ };
243
+ const init = async () => {
244
+ const res = await ext.makeRequest('v1.model.ext.settings.get', null);
245
+ const savedSettings = toSettings(res.settings);
246
+ // populate form fields from savedSettings here
247
+ const onSettingsSaveRequested = async () => {
248
+ // read current values from your form fields here
249
+ const settings = {
250
+ isEnabled: true,
251
+ viewerMessage: 'Hello',
252
+ };
253
+ const isError =
254
+ typeof settings.viewerMessage !== 'string' ||
255
+ settings.viewerMessage.trim().length === 0;
256
+ await ext.makeRequest('v1.model.ext.settings.set', {
257
+ settings,
258
+ isError,
259
+ });
260
+ };
261
+ ext.subscribe('v1.model.ext.settings.set.requested', onSettingsSaveRequested);
262
+ const cleanup = () => {
263
+ ext.unsubscribe('v1.model.ext.settings.set.requested', onSettingsSaveRequested);
264
+ };
265
+ };
266
+ void init();
267
+ ```
268
+
269
+ ### Backend action manifest and call
270
+ Manifest snippet:
271
+ ```
272
+ {
273
+ "actions": [
274
+ {
275
+ "name": "createToken",
276
+ "type": "externalCall",
277
+ "config": {
278
+ "action": "https://example.com/api/token",
279
+ "method": "POST"
280
+ }
281
+ }
282
+ ]
283
+ }
284
+ ```
285
+
286
+ Call from extension code
287
+ ```
288
+ import { createExtHelper } from '@stripchatdev/ext-helper';
289
+ const ext = createExtHelper();
290
+ const init = async () => {
291
+ const result = await ext.makeRequest('v1.ext.actions.call', {
292
+ actionName: 'createToken',
293
+ params: {},
294
+ });
295
+ if (result.code !== 200) {
296
+ await ext.makeRequest('v1.monitoring.report.error', {
297
+ message: 'createToken action failed',
298
+ data: { code: result.code, body: result.body },
299
+ });
300
+ return;
301
+ }
302
+ if (!result.body || typeof result.body !== 'object') {
303
+ await ext.makeRequest('v1.monitoring.report.error', {
304
+ message: 'createToken returned invalid body',
305
+ data: { body: result.body },
306
+ });
307
+ return;
308
+ }
309
+ const body = /** @type {Record<string, unknown>} */ (result.body);
310
+ if (typeof body.token !== 'string') {
311
+ await ext.makeRequest('v1.monitoring.report.error', {
312
+ message: 'createToken returned invalid token',
313
+ data: { body: result.body },
314
+ });
315
+ return;
316
+ }
317
+ const token = body.token;
318
+ console.log('Received token:', token);
319
+ };
320
+ void init();
321
+ ```
322
+
323
+ ### Token spend in user iframe, model background reacts via v1.tokens.spent
324
+ User mainGameFun slot:
325
+ ```
326
+ import { createExtHelper } from '@stripchatdev/ext-helper';
327
+ const ext = createExtHelper();
328
+ const spendForRound = async () => {
329
+ await ext.makeRequest('v1.payment.tokens.spend', {
330
+ tokensAmount: 10,
331
+ tokensSpendData: {
332
+ type: 'ROUND_JOIN',
333
+ roundId: 'round-1',
334
+ },
335
+ });
336
+ };
337
+ const onTokensSpendSucceeded = (data) => {
338
+ console.log('Spend confirmed:', data.tokensAmount, data.tokensSpendData);
339
+ };
340
+ ext.subscribe('v1.payment.tokens.spend.succeeded', onTokensSpendSucceeded);
341
+ const cleanup = () => {
342
+ ext.unsubscribe('v1.payment.tokens.spend.succeeded', onTokensSpendSucceeded);
343
+ };
344
+
345
+ void spendForRound();
346
+ ```
347
+
348
+ Model background slot (reacts to tip with an API call):
349
+ ```
350
+ import { createExtHelper } from '@stripchatdev/ext-helper';
351
+ const ext = createExtHelper();
352
+ const onTokensSpent = async ({ tipData }) => {
353
+ if (!tipData.isOriginalSource) {
354
+ return;
355
+ }
356
+ const result = await ext.makeRequest('v1.ext.actions.call', {
357
+ actionName: 'recordPayment',
358
+ params: {
359
+ amount: tipData.amount,
360
+ userId: tipData.user?.id,
361
+ },
362
+ });
363
+ if (result.code !== 200) {
364
+ await ext.makeRequest('v1.monitoring.report.error', {
365
+ message: 'recordPayment action failed',
366
+ data: { code: result.code, body: result.body },
367
+ });
368
+ }
369
+ };
370
+ ext.subscribe('v1.tokens.spent', onTokensSpent);
371
+ const cleanup = () => {
372
+ ext.unsubscribe('v1.tokens.spent', onTokensSpent);
373
+ };
374
+ ```
375
+
376
+ ### User-initiated whisper with payment token (full flow)
377
+ When a user needs to broadcast a whisper during a public show, paymentData is required.
378
+ Flow: visual slot calls v1.payment.tokens.spend -> user background listens to v1.payment.tokens.spend.succeeded -> user background sends v1.ext.whisper with paymentData.
379
+
380
+ User visual slot (initiates spend on user action):
381
+ ```
382
+ import { createExtHelper } from '@stripchatdev/ext-helper';
383
+ const ext = createExtHelper();
384
+
385
+ const spinWheel = async () => {
386
+ await ext.makeRequest('v1.payment.tokens.spend', {
387
+ tokensAmount: 10,
388
+ tokensSpendData: { action: 'spin_wheel' },
389
+ });
390
+ };
391
+ ```
392
+
393
+ User background slot (listens for payment confirmation, whispers with paymentData):
394
+ ```
395
+ import { createExtHelper } from '@stripchatdev/ext-helper';
396
+ const ext = createExtHelper();
397
+
398
+ const onSpendSucceeded = async (data) => {
399
+ await ext.makeRequest('v1.ext.whisper', {
400
+ data: { type: 'SPIN_RESULT', value: 7 },
401
+ paymentData: data.paymentData,
402
+ });
403
+ };
404
+ ext.subscribe('v1.payment.tokens.spend.succeeded', onSpendSucceeded);
405
+ const cleanup = () => {
406
+ ext.unsubscribe('v1.payment.tokens.spend.succeeded', onSpendSucceeded);
407
+ };
408
+ ```
409
+
410
+ All clients receiving the broadcast:
411
+ ```
412
+ import { createExtHelper } from '@stripchatdev/ext-helper';
413
+ const ext = createExtHelper();
414
+ const onWhispered = (data) => {
415
+ if (data.type !== 'SPIN_RESULT') {
416
+ return;
417
+ }
418
+ showResult(data.value);
419
+ };
420
+ ext.subscribe('v1.ext.whispered', onWhispered);
421
+ const cleanup = () => {
422
+ ext.unsubscribe('v1.ext.whispered', onWhispered);
423
+ };
424
+ ```
425
+
426
+
427
+ ## Layout And Theme
428
+ - Width is controlled by the parent slot container.
429
+ - Build layouts with flexbox, grid, %, fr, min(), max(), and clamp().
430
+ - Set html { height: 100% }.
431
+ - Let body grow naturally with content.
432
+ - Put the page background on <html>.
433
+ - Do not rely on viewport-based responsive logic.
434
+ - Assume slot width can change at any time.
435
+ - Use html[data-theme='dark'] and html[data-theme='light'] for theme-specific tokens.
436
+
437
+ ## Monitoring
438
+ - Use monitoring for meaningful failures and important lifecycle events.
439
+ - Keep monitoring messages short and stable.
440
+ - Put variable details in data.
441
+ - Do not spam monitoring for routine events.
442
+
443
+ ## Common Changes
444
+
445
+ ### Add a page variant
446
+ 1. Create a new root HTML file at the project root.
447
+ 2. Add its page key to public/manifest.json views.pages.
448
+ 3. Update public/resolveSlotPage.js to return that page key or null.
449
+ 4. Add the new HTML file to vite.config.<%= isTypescript ? 'ts' : 'js' %> input.
450
+ 5. Create or reuse the page entry module and page code.
451
+ 6. Keep manifest, resolver, HTML, and Vite input in sync.
452
+ ### Add model, viewer, or guest variants
453
+ 1. Keep the slot name the same.
454
+ 2. Add separate page keys in views.pages.
455
+ 3. Branch in public/resolveSlotPage.js using context.user, context.model, and context.user?.isModel.
456
+ 4. Return null when that actor should not render the slot.
457
+ 5. Keep shared state in background.
458
+ ### Add a settings page
459
+ 1. Create settings.html as a root HTML file.
460
+ 2. Create a page entry module for it and call createExtHelper() there.
461
+ 3. Add views.settings to public/manifest.json.
462
+ 4. Add settings.html to vite.config.<%= isTypescript ? 'ts' : 'js' %> input.
463
+ 5. Load with v1.model.ext.settings.get.
464
+ 6. Save in response to v1.model.ext.settings.set.requested.
465
+ 7. Persist with v1.model.ext.settings.set.
466
+ ### Add a backend action
467
+ 1. Declare the action in public/manifest.json actions[].
468
+ 2. Use externalCall for HTTP requests. Use delayedEvent only for deferred follow-up work.
469
+ 3. Call the action with v1.ext.actions.call, passing all required params.
470
+ 4. Handle non-success result.code explicitly.
471
+ 5. Do not hardcode secrets in slot code.
472
+ ### Add moveableOverlay
473
+ 1. Add moveableOverlay to the slot configuration.
474
+ 2. Add a page key and root HTML file for it.
475
+ 3. Add moveableSlot to the manifest with width and height. Max size is 450x740.
476
+ 4. Keep public/icon.svg in the archive root.
477
+ 5. Add the HTML file to Vite input.
478
+ 6. Update the resolver to return the page key only when appropriate.
479
+ ### Final Check Before Finishing
480
+ - createExtHelper() is called once per page.
481
+ - public/resolveSlotPage.js is sync and returns only valid page keys or null.
482
+ - Every page key in views.pages points to a real root HTML file.
483
+ - Every root HTML file is included in Vite input.
484
+ - Subscriptions, timers, and DOM listeners are cleaned up.
485
+ - Durable logic lives in background.
486
+ - Theme styles are based on html[data-theme].
487
+ - Layout uses flexible CSS without viewport @media queries.
488
+ - manifest.json is valid JSON.
489
+
490
+ ### References
491
+ - Overview: https://extensions.stripchat.com/docs/overview/how-extensions-work
492
+ - Manifest: https://extensions.stripchat.com/docs/getting-started/manifest
493
+ - Resolve slot page: https://extensions.stripchat.com/docs/getting-started/resolve-slot-page
494
+ - Slots: https://extensions.stripchat.com/docs/getting-started/slots
495
+ - Slots communication: https://extensions.stripchat.com/docs/getting-started/slots-communication
496
+ - Slots layout: https://extensions.stripchat.com/docs/getting-started/slots-layout
497
+ - Theming: https://extensions.stripchat.com/docs/getting-started/theming
498
+ - Settings: https://extensions.stripchat.com/docs/getting-started/settings
499
+ - Backend actions: https://extensions.stripchat.com/docs/getting-started/backend-actions
500
+ - Monitoring: https://extensions.stripchat.com/docs/getting-started/monitoring
501
+ - Requests: https://extensions.stripchat.com/docs/api/requests
502
+ - Events: https://extensions.stripchat.com/docs/api/events
503
+ - Entities: https://extensions.stripchat.com/docs/api/entities
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,38 @@
1
+ # <%= name %>
2
+
3
+ Stripchat extension built with <%= framework === 'preact' ? 'Preact' : 'Vanilla' %> + <%= isTypescript ? 'TypeScript' : 'JavaScript' %>
4
+
5
+ ## Start Here
6
+
7
+ Read `AGENTS.md` first. It is the project contract for SDK usage, slot lifecycles, resolver rules, theming, and settings.
8
+
9
+ ## Generated Wiring
10
+
11
+ - `public/manifest.json` declares slots, page keys, optional settings entry, and backend actions.
12
+ - `public/resolveSlotPage.js` maps slot types to page keys from `views.pages`.
13
+ - Root HTML files like `<%= slots[0] %>.html` and `background.html` are emitted to the archive root.
14
+ - Visual slots should stay disposable; keep durable state and coordination in `background`.
15
+ - `vite.config.<%= isTypescript ? 'ts' : 'js' %>` must include every root HTML file in `build.rollupOptions.input`.
16
+
17
+ ## Documentation
18
+
19
+ Learn more about building Stripchat extensions in the [official documentation](https://extensions.stripchat.com/docs/).
20
+
21
+ ## Common Changes
22
+
23
+ - Add a page: create a root HTML file, add it to `public/manifest.json`, return its page key from `public/resolveSlotPage.js`, and include it in `vite.config.<%= isTypescript ? 'ts' : 'js' %>`.
24
+ - Add settings: create `settings.html`, add `views.settings`, add its Vite input, load with `v1.model.ext.settings.get`, and save on `v1.model.ext.settings.set.requested`.
25
+ - Add an action: declare it in `public/manifest.json` and call it with `v1.ext.actions.call`.
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ # Install dependencies
31
+ <%= packageManagerCommands.install %>
32
+
33
+ # Start development server
34
+ <%= packageManagerCommands.dev %>
35
+
36
+ # Build for production
37
+ <%= packageManagerCommands.build %>
38
+ ```
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "<%= name %>",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "<%- isTypescript ? 'tsc && vite build' : 'vite build' %>",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@stripchatdev/ext-helper": "<%= extHelperVersion %>"<% if (framework === 'preact') { %>,
12
+ "preact": "10.25.4"<% } %>
13
+ },
14
+ "devDependencies": {
15
+ "vite": "6.3.5"<% if (isTypescript) { %>,
16
+ "typescript": "5.8.2"<% } %><% if (framework === 'preact') { %>,
17
+ "@preact/preset-vite": "2.10.5"<% } %>
18
+ }
19
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <rect width="64" height="64" rx="12" fill="#333"/>
3
+ <path d="M20 44V20h8l8 12 8-12h8v24h-8V32l-8 12-8-12v12z" fill="#fff"/>
4
+ </svg>
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": "v2.0",
3
+ "views": {
4
+ "slots": <%- JSON.stringify(slots) %>,
5
+ "pages": {
6
+ <% slots.forEach((slot, i) => { -%>
7
+ "<%= slot %>": "<%= slot %>.html"<%= i < slots.length - 1 ? ',' : '' %>
8
+ <% }); -%>
9
+ },
10
+ "resolveSlotPageScript": "resolveSlotPage.js"
11
+ }<% if (slots.includes('moveableOverlay')) { %>,
12
+ "moveableSlot": {
13
+ "width": 400,
14
+ "height": 600
15
+ }<% } %>
16
+ }
@@ -0,0 +1,10 @@
1
+ export default function resolveSlotPage(slotType, context, extSettings) {
2
+ switch (slotType) {
3
+ <% slotResolverEntries.forEach((entry) => { -%>
4
+ case '<%= entry.slotType %>':
5
+ return '<%= entry.slot %>';
6
+ <% }); -%>
7
+ default:
8
+ return null;
9
+ }
10
+ }
@@ -0,0 +1,37 @@
1
+ <% if (isTypescript) { -%>
2
+ import type { TEvents } from '@stripchatdev/ext-helper';
3
+ <% } -%>
4
+ import { createExtHelper } from '@stripchatdev/ext-helper';
5
+
6
+ const extHelper = createExtHelper();
7
+
8
+ export const init = () => {
9
+ const handleWhispered = async (data<%= isTypescript ? ": TEvents['v1.ext.whispered']" : '' %>) => {
10
+ await extHelper.makeRequest('v1.monitoring.report.log', {
11
+ message: 'background whisper received',
12
+ data,
13
+ });
14
+ };
15
+
16
+ const handleContextUpdated = async ({ context }<%= isTypescript ? ": TEvents['v1.ext.context.updated']" : '' %>) => {
17
+ await extHelper.makeRequest('v1.monitoring.report.log', {
18
+ message: 'background context updated',
19
+ data: { context },
20
+ });
21
+ };
22
+
23
+ extHelper.subscribe('v1.ext.whispered', handleWhispered);
24
+ extHelper.subscribe('v1.ext.context.updated', handleContextUpdated);
25
+
26
+ extHelper.makeRequest('v1.ext.context.get', null).then((ctx) => {
27
+ void extHelper.makeRequest('v1.monitoring.report.log', {
28
+ message: 'background slot ready',
29
+ data: { context: ctx },
30
+ });
31
+ });
32
+
33
+ return () => {
34
+ extHelper.unsubscribe('v1.ext.whispered', handleWhispered);
35
+ extHelper.unsubscribe('v1.ext.context.updated', handleContextUpdated);
36
+ };
37
+ };
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>
8
+ <%= slot %>
9
+ </title>
10
+ </head>
11
+
12
+ <body>
13
+ <script type="module" src="<%= entryModulePath %>"></script>
14
+ </body>
15
+
16
+ </html>
@@ -0,0 +1,3 @@
1
+ import { init } from './app';
2
+
3
+ init();
@@ -0,0 +1,22 @@
1
+ import { createExtHelper } from '@stripchatdev/ext-helper';
2
+
3
+ const extHelper = createExtHelper();
4
+
5
+ export const init = () => {
6
+ const container = document.createElement('div');
7
+ container.style.padding = '16px';
8
+ document.body.appendChild(container);
9
+
10
+ const heading = document.createElement('h2');
11
+ heading.textContent = '<%= slot %>';
12
+ container.appendChild(heading);
13
+
14
+ const status = document.createElement('p');
15
+ status.textContent = 'Loading...';
16
+ container.appendChild(status);
17
+
18
+ extHelper.makeRequest('v1.ext.context.get', null).then((ctx) => {
19
+ const name = ctx.model?.username ?? 'unknown';
20
+ status.textContent = `Connected to ${name}'s room`;
21
+ });
22
+ };