@lepsto/sdk-app 84.0.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 (183) hide show
  1. package/README.md +233 -0
  2. package/dist/_types/gen/analytics/index.d.ts +2 -0
  3. package/dist/_types/gen/analytics/queryOptions.gen.d.ts +74 -0
  4. package/dist/_types/gen/bindings.gen.d.ts +3 -0
  5. package/dist/_types/gen/brain/index.d.ts +2 -0
  6. package/dist/_types/gen/brain/queryOptions.gen.d.ts +166 -0
  7. package/dist/_types/gen/client.gen.d.ts +1087 -0
  8. package/dist/_types/gen/consent/index.d.ts +2 -0
  9. package/dist/_types/gen/consent/queryOptions.gen.d.ts +62 -0
  10. package/dist/_types/gen/content/index.d.ts +2 -0
  11. package/dist/_types/gen/content/queryOptions.gen.d.ts +126 -0
  12. package/dist/_types/gen/deployment/index.d.ts +2 -0
  13. package/dist/_types/gen/deployment/queryOptions.gen.d.ts +438 -0
  14. package/dist/_types/gen/mail/index.d.ts +2 -0
  15. package/dist/_types/gen/mail/queryOptions.gen.d.ts +206 -0
  16. package/dist/_types/gen/manifest.gen.d.ts +4 -0
  17. package/dist/_types/gen/observe/index.d.ts +2 -0
  18. package/dist/_types/gen/observe/queryOptions.gen.d.ts +142 -0
  19. package/dist/_types/gen/organization/index.d.ts +2 -0
  20. package/dist/_types/gen/organization/queryOptions.gen.d.ts +362 -0
  21. package/dist/_types/gen/realtime/index.d.ts +2 -0
  22. package/dist/_types/gen/realtime/queryOptions.gen.d.ts +114 -0
  23. package/dist/_types/gen/support/index.d.ts +2 -0
  24. package/dist/_types/gen/support/queryOptions.gen.d.ts +206 -0
  25. package/dist/_types/gen/tracking/index.d.ts +2 -0
  26. package/dist/_types/gen/tracking/queryOptions.gen.d.ts +150 -0
  27. package/dist/_types/gen/types.gen.d.ts +126011 -0
  28. package/dist/_types/gen/users/index.d.ts +2 -0
  29. package/dist/_types/gen/users/queryOptions.gen.d.ts +182 -0
  30. package/dist/_types/gen/waitlist/index.d.ts +2 -0
  31. package/dist/_types/gen/waitlist/queryOptions.gen.d.ts +30 -0
  32. package/dist/_types/gen/workflow/index.d.ts +2 -0
  33. package/dist/_types/gen/workflow/queryOptions.gen.d.ts +166 -0
  34. package/dist/_types/index.d.ts +11 -0
  35. package/dist/_types/package.json +1 -0
  36. package/dist/_types/react/index.d.ts +33 -0
  37. package/dist/_types/runtime/access-reason.d.ts +27 -0
  38. package/dist/_types/runtime/access.d.ts +40 -0
  39. package/dist/_types/runtime/compositions.d.ts +16 -0
  40. package/dist/_types/runtime/connectStream.d.ts +15 -0
  41. package/dist/_types/runtime/createLesslyApp.d.ts +10 -0
  42. package/dist/_types/runtime/csrf.d.ts +1 -0
  43. package/dist/_types/runtime/errors.d.ts +18 -0
  44. package/dist/_types/runtime/name.d.ts +26 -0
  45. package/dist/_types/runtime/operations.d.ts +13 -0
  46. package/dist/_types/runtime/permission-match.d.ts +62 -0
  47. package/dist/_types/runtime/request.d.ts +9 -0
  48. package/dist/_types/runtime/types.d.ts +129 -0
  49. package/dist/analytics/index.cjs +96 -0
  50. package/dist/analytics/index.cjs.map +1 -0
  51. package/dist/analytics/index.d.cts +1 -0
  52. package/dist/analytics/index.d.ts +1 -0
  53. package/dist/analytics/index.js +77 -0
  54. package/dist/analytics/index.js.map +1 -0
  55. package/dist/brain/index.cjs +211 -0
  56. package/dist/brain/index.cjs.map +1 -0
  57. package/dist/brain/index.d.cts +1 -0
  58. package/dist/brain/index.d.ts +1 -0
  59. package/dist/brain/index.js +169 -0
  60. package/dist/brain/index.js.map +1 -0
  61. package/dist/consent/index.cjs +81 -0
  62. package/dist/consent/index.cjs.map +1 -0
  63. package/dist/consent/index.d.cts +1 -0
  64. package/dist/consent/index.d.ts +1 -0
  65. package/dist/consent/index.js +65 -0
  66. package/dist/consent/index.js.map +1 -0
  67. package/dist/content/index.cjs +161 -0
  68. package/dist/content/index.cjs.map +1 -0
  69. package/dist/content/index.d.cts +1 -0
  70. package/dist/content/index.d.ts +1 -0
  71. package/dist/content/index.js +129 -0
  72. package/dist/content/index.js.map +1 -0
  73. package/dist/deployment/index.cjs +551 -0
  74. package/dist/deployment/index.cjs.map +1 -0
  75. package/dist/deployment/index.d.cts +1 -0
  76. package/dist/deployment/index.d.ts +1 -0
  77. package/dist/deployment/index.js +441 -0
  78. package/dist/deployment/index.js.map +1 -0
  79. package/dist/index.cjs +12565 -0
  80. package/dist/index.cjs.map +1 -0
  81. package/dist/index.d.cts +1 -0
  82. package/dist/index.d.ts +1 -0
  83. package/dist/index.js +12557 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/mail/index.cjs +261 -0
  86. package/dist/mail/index.cjs.map +1 -0
  87. package/dist/mail/index.d.cts +1 -0
  88. package/dist/mail/index.d.ts +1 -0
  89. package/dist/mail/index.js +209 -0
  90. package/dist/mail/index.js.map +1 -0
  91. package/dist/observe/index.cjs +181 -0
  92. package/dist/observe/index.cjs.map +1 -0
  93. package/dist/observe/index.d.cts +1 -0
  94. package/dist/observe/index.d.ts +1 -0
  95. package/dist/observe/index.js +145 -0
  96. package/dist/observe/index.js.map +1 -0
  97. package/dist/organization/index.cjs +456 -0
  98. package/dist/organization/index.cjs.map +1 -0
  99. package/dist/organization/index.d.cts +1 -0
  100. package/dist/organization/index.d.ts +1 -0
  101. package/dist/organization/index.js +365 -0
  102. package/dist/organization/index.js.map +1 -0
  103. package/dist/react/index.cjs +29 -0
  104. package/dist/react/index.cjs.map +1 -0
  105. package/dist/react/index.d.cts +1 -0
  106. package/dist/react/index.d.ts +1 -0
  107. package/dist/react/index.js +26 -0
  108. package/dist/react/index.js.map +1 -0
  109. package/dist/realtime/index.cjs +146 -0
  110. package/dist/realtime/index.cjs.map +1 -0
  111. package/dist/realtime/index.d.cts +1 -0
  112. package/dist/realtime/index.d.ts +1 -0
  113. package/dist/realtime/index.js +117 -0
  114. package/dist/realtime/index.js.map +1 -0
  115. package/dist/support/index.cjs +261 -0
  116. package/dist/support/index.cjs.map +1 -0
  117. package/dist/support/index.d.cts +1 -0
  118. package/dist/support/index.d.ts +1 -0
  119. package/dist/support/index.js +209 -0
  120. package/dist/support/index.js.map +1 -0
  121. package/dist/tracking/index.cjs +191 -0
  122. package/dist/tracking/index.cjs.map +1 -0
  123. package/dist/tracking/index.d.cts +1 -0
  124. package/dist/tracking/index.d.ts +1 -0
  125. package/dist/tracking/index.js +153 -0
  126. package/dist/tracking/index.js.map +1 -0
  127. package/dist/users/index.cjs +231 -0
  128. package/dist/users/index.cjs.map +1 -0
  129. package/dist/users/index.d.cts +1 -0
  130. package/dist/users/index.d.ts +1 -0
  131. package/dist/users/index.js +185 -0
  132. package/dist/users/index.js.map +1 -0
  133. package/dist/waitlist/index.cjs +41 -0
  134. package/dist/waitlist/index.cjs.map +1 -0
  135. package/dist/waitlist/index.d.cts +1 -0
  136. package/dist/waitlist/index.d.ts +1 -0
  137. package/dist/waitlist/index.js +33 -0
  138. package/dist/waitlist/index.js.map +1 -0
  139. package/dist/workflow/index.cjs +211 -0
  140. package/dist/workflow/index.cjs.map +1 -0
  141. package/dist/workflow/index.d.cts +1 -0
  142. package/dist/workflow/index.d.ts +1 -0
  143. package/dist/workflow/index.js +169 -0
  144. package/dist/workflow/index.js.map +1 -0
  145. package/docs/README.md +89 -0
  146. package/docs/recipes/access.md +781 -0
  147. package/docs/recipes/federation.md +285 -0
  148. package/docs/recipes/local-dev.md +111 -0
  149. package/docs/recipes/sdk-usage.md +194 -0
  150. package/docs/rules.md +522 -0
  151. package/package.json +212 -0
  152. package/src/gen/analytics/index.ts +3 -0
  153. package/src/gen/analytics/queryOptions.gen.ts +124 -0
  154. package/src/gen/bindings.gen.ts +11330 -0
  155. package/src/gen/brain/index.ts +3 -0
  156. package/src/gen/brain/queryOptions.gen.ts +278 -0
  157. package/src/gen/client.gen.ts +2288 -0
  158. package/src/gen/consent/index.ts +3 -0
  159. package/src/gen/consent/queryOptions.gen.ts +102 -0
  160. package/src/gen/content/index.ts +3 -0
  161. package/src/gen/content/queryOptions.gen.ts +202 -0
  162. package/src/gen/deployment/index.ts +3 -0
  163. package/src/gen/deployment/queryOptions.gen.ts +707 -0
  164. package/src/gen/mail/index.ts +3 -0
  165. package/src/gen/mail/queryOptions.gen.ts +331 -0
  166. package/src/gen/manifest.gen.ts +780 -0
  167. package/src/gen/observe/index.ts +3 -0
  168. package/src/gen/observe/queryOptions.gen.ts +235 -0
  169. package/src/gen/organization/index.ts +3 -0
  170. package/src/gen/organization/queryOptions.gen.ts +578 -0
  171. package/src/gen/realtime/index.ts +3 -0
  172. package/src/gen/realtime/queryOptions.gen.ts +185 -0
  173. package/src/gen/support/index.ts +3 -0
  174. package/src/gen/support/queryOptions.gen.ts +331 -0
  175. package/src/gen/tracking/index.ts +3 -0
  176. package/src/gen/tracking/queryOptions.gen.ts +249 -0
  177. package/src/gen/types.gen.ts +119506 -0
  178. package/src/gen/users/index.ts +3 -0
  179. package/src/gen/users/queryOptions.gen.ts +295 -0
  180. package/src/gen/waitlist/index.ts +3 -0
  181. package/src/gen/waitlist/queryOptions.gen.ts +50 -0
  182. package/src/gen/workflow/index.ts +3 -0
  183. package/src/gen/workflow/queryOptions.gen.ts +274 -0
@@ -0,0 +1,285 @@
1
+ # Recipe: Module Federation setup
2
+
3
+ How to wire the Module Federation contract an App must implement (APP-004),
4
+ and the dual-mode CSS graph that goes with it (APP-008, APP-009, APP-010).
5
+ This page shows the *how*; for exact wording, rationale, and examples of the
6
+ shared rules, see the referenced `FED-*` IDs in the toolkit-guide — this
7
+ recipe does not restate them.
8
+
9
+ ## Expose `./App` — and nothing else
10
+
11
+ Your app's federation config exposes exactly one module: a React component at
12
+ `./App` (FED-001).
13
+
14
+ ```ts
15
+ // vite.config.ts (federation mode only)
16
+ federation({
17
+ name: APP_NAME, // MUST equal the manifest `id` (FED-004)
18
+ filename: 'remoteEntry.js',
19
+ manifest: true,
20
+ exposes: {
21
+ './App': './src/federation/app-remote.tsx',
22
+ },
23
+ shared: SHARED,
24
+ });
25
+ ```
26
+
27
+ There is no `./navigation` module. FED-011 — the shell's extension-menu
28
+ contract — is **RETIRED (2026-08)**: the shell never registers, preloads or
29
+ calls it, there is no `subscribeNavItems` (v1 or v2) and no
30
+ `navContractVersion` negotiation, and the menu the shell renders while your app
31
+ is open comes from **your own** `lessly.app.yaml` — the `nav.sections` block
32
+ (APP-011) — not from your App code, and not from a toolkit manifest.
33
+
34
+ Still mapping the expose is inert, not an error — you just ship a chunk nobody
35
+ fetches. If your app has one, the cleanup is: delete
36
+ `src/federation/navigation.ts`, drop the `./navigation` entry from `exposes`,
37
+ and delete any `navContractVersion` / `notify()` plumbing `./App` called. What
38
+ the module used to feed the menu — environment lists, tool groups, live badges,
39
+ status dots, a settings gear — belongs **in your own pages**, rendered by
40
+ `./App` under its relative routes (FED-003). Deep links, refresh and the back
41
+ button are unaffected: the dynamic items only ever fed the menu, never the
42
+ router.
43
+
44
+ ## The menu moves to the manifest, not to a module
45
+
46
+ The rows the module used to publish are now data in your App's manifest:
47
+
48
+ ```yaml
49
+ # lessly.app.yaml
50
+ nav:
51
+ icon: flask-conical
52
+ sections:
53
+ - id: overview
54
+ items:
55
+ - label: Dashboard
56
+ path: '' # '' = your App's root
57
+ match: exact
58
+ icon: gauge
59
+ - label: Activity
60
+ path: activity
61
+ icon: activity
62
+ ```
63
+
64
+ `path` is relative to your App's base path (`/<productSlug>/~/<id>`), so these
65
+ are the same relative routes `./App` already renders under (FED-003). The full
66
+ field constraints, the worked example, what happens when you ship no block, and
67
+ how a change reaches production are in APP-011.
68
+
69
+ ## Shared singletons: `>=` floors, not carets, on 0.x packages
70
+
71
+ `react`, `react-dom`, `react-router`, and `@lessly/ui` MUST be declared as
72
+ shared singletons, never bundled (FED-005), pinned via `@module-federation/vite`
73
+ at exactly `1.16.12` (FED-006):
74
+
75
+ ```ts
76
+ const SHARED = {
77
+ react: { singleton: true, strictVersion: false, requiredVersion: '^19' },
78
+ 'react-dom': { singleton: true, strictVersion: false, requiredVersion: '^19' },
79
+ 'react-router': { singleton: true, strictVersion: false, requiredVersion: '^7.18' },
80
+ // 0.x singletons use a `>=` floor, NOT a caret: `^0.x` resolves to `<0.(x+1).0`,
81
+ // so a routine shell ui/icon bump would fail Module Federation version negotiation.
82
+ '@lessly/ui': { singleton: true, strictVersion: false, requiredVersion: '>=0.1.0' },
83
+ };
84
+ ```
85
+
86
+ `@lessly/ui` is pre-1.0. A caret range on a 0.x package (`^0.1.0`) resolves to
87
+ `<0.2.0`, so the next time the shell bumps `@lessly/ui`'s minor version, MF
88
+ version negotiation fails and your remote won't load under the shell. Use a
89
+ `>=` floor for every 0.x shared singleton instead — it only guarantees a
90
+ minimum, so it survives minor bumps upstream. Any other 0.x singleton you add
91
+ (e.g. an icon package) follows the same rule.
92
+
93
+ ## Dual build: standalone + federation, merged into one `dist/`
94
+
95
+ The build MUST produce one merged dual-mode `dist/` covering both standalone
96
+ and federation artifacts (FED-007): a standalone build (plain SPA, `base: '/'`)
97
+ and a federation build (`base: '/~/<id>/'`, emits `remoteEntry.js` +
98
+ `mf-manifest.json`), merged so both coexist:
99
+
100
+ ```jsonc
101
+ // package.json scripts
102
+ {
103
+ "build": "tsc -b && pnpm build:standalone && pnpm build:federation && pnpm build:merge",
104
+ "build:standalone": "vite build",
105
+ "build:federation": "vite build --mode federation",
106
+ "build:merge": "node scripts/merge-federation-dist.mjs"
107
+ }
108
+ ```
109
+
110
+ ```ts
111
+ // vite.config.ts
112
+ export default defineConfig(({ mode }) => {
113
+ const isFederation = mode === 'federation';
114
+ return {
115
+ base: isFederation ? `/~/${APP_NAME}/` : '/',
116
+ build: {
117
+ outDir: isFederation ? 'dist-federation' : 'dist',
118
+ emptyOutDir: true,
119
+ },
120
+ // ...
121
+ };
122
+ });
123
+ ```
124
+
125
+ The merge step copies `dist-federation/*` into `dist/`, keeping the
126
+ **standalone** `index.html` (not the federation one) and failing loudly on any
127
+ filename collision with differing content, then removes `dist-federation/`.
128
+ The result: one `dist/` that serves the standalone SPA at `/` and the
129
+ federation remote entry + assets at `/~/<id>/`.
130
+
131
+ ## Absolute asset base in federation mode
132
+
133
+ The asset base in federation mode is the absolute path `/~/<id>/` — not a
134
+ relative path, and `<id>` MUST match the manifest `id` and the MF `name`
135
+ (FED-004). Getting this wrong breaks asset resolution once the shell mounts
136
+ your remote at that path.
137
+
138
+ ## Styles: two entries, one vocabulary
139
+
140
+ Federation mode and standalone mode enter the CSS graph through **different**
141
+ stylesheets, and that split is the whole point. Standalone is your app's own
142
+ page, so it owns the base layer, the fonts and the full kit stylesheet.
143
+ Federation mounts your subtree inside the shell's already-painted page, so the
144
+ federation entry must add nothing global (APP-008), must take its scales from
145
+ the shell's preset (APP-009), and must park its utilities in
146
+ `layer(remote-utilities)` (APP-010).
147
+
148
+ Four files, ready to copy.
149
+
150
+ ```css
151
+ /* src/index.federation.css — the federation entry, imported by ./App.
152
+ Utilities only: no preflight, no fonts, no base layer, no @theme, and no
153
+ tailwindcss/theme.css at all. */
154
+
155
+ /* Layer order first, so positions are fixed before Tailwind emits its own.
156
+ Mirrors the shell's declaration in lessly-workspace:apps/web/src/index.css. */
157
+ @layer theme, base, components, remote-utilities, utilities;
158
+
159
+ @config '../tailwind.config.ts';
160
+ @import 'tailwindcss/utilities.css' layer(remote-utilities);
161
+ @import '@lessly/ui/styles-federated.css';
162
+
163
+ /* Tailwind must scan the kit's compiled output, or the classes @lessly/ui
164
+ components use are never generated. */
165
+ @source '../node_modules/@lessly/ui/dist/index.js';
166
+
167
+ /* ...and must NOT scan your tests: a utility class named as a test fixture is
168
+ just a string to Tailwind, and would be compiled into the shipped sheet. */
169
+ @source not '../src/**/*.test.ts';
170
+ @source not '../src/**/*.test.tsx';
171
+ ```
172
+
173
+ ```css
174
+ /* src/index.css — the standalone entry, imported by src/main.tsx. Your app
175
+ owns the page here: full tailwindcss (preflight included), the fonts, the
176
+ full kit stylesheet, and the base layer. Same @config as the federation
177
+ entry — one vocabulary on both (APP-009). */
178
+ @config '../tailwind.config.ts';
179
+ @import 'tailwindcss';
180
+ @import url('https://fonts.googleapis.com/css2?family=Inter');
181
+ @import '@lessly/ui/styles.css';
182
+ @import './base.css';
183
+
184
+ /* Same reason as in the federation entry: Tailwind generates utilities per
185
+ entry, and neither entry's scan reaches node_modules on its own. */
186
+ @source '../node_modules/@lessly/ui/dist/index.js';
187
+ ```
188
+
189
+ ```css
190
+ /* src/base.css — the global base layer. Imported by the STANDALONE entry
191
+ ONLY; it must never be reachable from src/index.federation.css. */
192
+ @layer base {
193
+ * { @apply border-border-subtle; }
194
+ body { @apply bg-bg-primary text-text-primary; font-family: var(--font-sans); }
195
+ }
196
+ ```
197
+
198
+ ```ts
199
+ // tailwind.config.ts — the single source of scale vocabulary, for both entries
200
+ import { lesslyPreset } from '@lessly/ui/tailwind-preset';
201
+
202
+ export default {
203
+ presets: [lesslyPreset],
204
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
205
+ };
206
+ ```
207
+
208
+ Wire them up:
209
+
210
+ ```tsx
211
+ // src/federation/app-remote.tsx
212
+ import '../index.federation.css';
213
+ ```
214
+
215
+ ```tsx
216
+ // src/main.tsx
217
+ import './index.css';
218
+ ```
219
+
220
+ ### Three things that are easy to get wrong
221
+
222
+ **`theme.css` is optional, and it is not a token bridge.** There is no
223
+ `src/theme.css` in the layout above, and an app with no tokens of its own does
224
+ not need one. The kit's tokens come from `@lessly/ui/styles-federated.css` in
225
+ federation and `@lessly/ui/styles.css` in standalone; re-declaring them locally
226
+ is a hand-copied set that drifts from the kit silently, and an `@theme` block
227
+ restating the kit's scales is exactly what APP-009 forbids.
228
+
229
+ Add `src/theme.css` only for tokens **your app owns and the kit does not** —
230
+ `--hover-bg` and the like — and then it holds **variable declarations only**.
231
+ Both entries import it, so a `@layer base { body { … } }` block in it is
232
+ reachable from the federation entry and will re-font the shell's chrome after
233
+ first paint. That exact block is what shipped the production font jump; it
234
+ belongs in `src/base.css`, standalone-only.
235
+
236
+ **The sanctioned federated export, not a hand-rolled filter.**
237
+ `@lessly/ui/styles-federated.css` is `styles.css` minus the `@font-face` blocks
238
+ and the `@layer base` — tokens, `--color-*` aliases, `.link`, the `.motion-*`
239
+ helpers and the compiled tw-animate-css layer are all still there. A prebuild
240
+ script that reads the kit's stylesheet out of `node_modules` and strips the
241
+ forbidden blocks itself breaks on any restructure of a file you don't own.
242
+ Depend on `@lessly/ui` at `^4.0.0`, the range the shell runs.
243
+
244
+ **`layer(remote-utilities)` only after the shell declares the order.** The
245
+ shell already ships `@layer theme, base, components, remote-utilities,
246
+ utilities;` (`lessly-workspace:apps/web/src/index.css`). Adopt the layer
247
+ against a shell that hasn't, and `remote-utilities` becomes the last-declared —
248
+ and therefore highest-priority — layer, so every one of your utilities beats
249
+ every shell utility. Verify the shell first; adopting early is a regression,
250
+ not a head start.
251
+
252
+ ### Guard it
253
+
254
+ A CSS regression here is invisible in a standalone build — it only shows once
255
+ your app is composed into a live shell. Keep a standing test over the **built**
256
+ federation CSS (`dist-federation/`), not over the entry's import list, since an
257
+ imported stylesheet can carry what the entry looks clean of. What it must
258
+ assert, and the two shapes it must not confuse:
259
+
260
+ ```bash
261
+ # No fonts, and no base-layer BLOCK. Note the `{`: `@layer base, components;`
262
+ # is a harmless order declaration and must not trip the guard.
263
+ grep -REn "@font-face|@layer[[:space:]]+base[[:space:]]*\{" dist-federation/
264
+
265
+ # A global rule is a violation only if it declares more than custom properties:
266
+ # styles-federated.css's `*, ::before, ::after { --tw-*: … }` is legal.
267
+ grep -REn "(^|[,}])[[:space:]]*(html|body|\*)[^{]*\{" dist-federation/
268
+ grep -R -- "-webkit-text-size-adjust" dist-federation/ && echo "preflight leaked"
269
+
270
+ # Utilities in the app's own layer, never the shell's.
271
+ grep -REn "@layer[[:space:]]+utilities[[:space:]]*\{" dist-federation/ \
272
+ && echo "APP-010 — utilities still in the shell's layer"
273
+ ```
274
+
275
+ ## Fast Refresh under a local shell
276
+
277
+ When developing your remote composed under a local shell (`vite --mode
278
+ federation`), point `reactRefreshHost` at the shell's dev origin so React Fast
279
+ Refresh works through the federation boundary:
280
+
281
+ ```ts
282
+ react(isFederation ? { reactRefreshHost: shellDevOrigin } : {})
283
+ ```
284
+
285
+ See `recipes/local-dev.md` for how to run this composed setup end to end.
@@ -0,0 +1,111 @@
1
+ # Recipe: running an App locally
2
+
3
+ How to run an App standalone and, when needed, composed under a local shell.
4
+ Auth stays ambient in both modes (APP-005) — you never implement login or
5
+ token handling yourself.
6
+
7
+ ## Standalone: `pnpm dev`
8
+
9
+ ```sh
10
+ pnpm dev
11
+ ```
12
+
13
+ This starts the app on its own, outside any shell, proxied through
14
+ `@lessly/app-dev`'s local dev server. Platform API calls made through your
15
+ `sdk` (created with `createLesslyApp`, see `recipes/sdk-usage.md`) are
16
+ forwarded through a bearer proxy at exactly this path:
17
+
18
+ ```
19
+ /lessly-api
20
+ ```
21
+
22
+ `@lessly/app-dev` attaches your locally stored bearer token to everything it
23
+ proxies through `/lessly-api`, so your SDK client just talks to that path in
24
+ dev — it never sees or handles the token itself.
25
+
26
+ So in dev you pass `baseUrl: '/lessly-api'` — a page-relative `baseUrl` is
27
+ supported and resolved against the current document (browser-only); see
28
+ `recipes/sdk-usage.md`.
29
+
30
+ ### Device-code login
31
+
32
+ The first time you run `pnpm dev` (or once your local session expires),
33
+ `@lessly/app-dev` walks you through a device-code login: it prints a
34
+ verification URL and a short code, opens your browser, and polls until you
35
+ approve. Once approved, it stores the resulting bearer token locally and the
36
+ `/lessly-api` proxy starts attaching it automatically. You don't write any of
37
+ this — it's ambient, per APP-005: an App MUST NOT implement its own login,
38
+ token handling, or refresh.
39
+
40
+ ### `.env.local`
41
+
42
+ Set which product your local session targets:
43
+
44
+ ```sh
45
+ # .env.local
46
+ VITE_PRODUCT_ID=prod_123
47
+ ```
48
+
49
+ Vite does not load `.env.example` — copy it to `.env.local` and edit the
50
+ value. `.env.local` is per-developer and untracked; don't commit it.
51
+
52
+ ### The stub `user` prop
53
+
54
+ In dev-standalone, the `user` prop your app receives is a **stub**:
55
+ `{ id: <from the local dev JWT>, displayName: 'Dev User' }`. It is not a real
56
+ session — it exists only so you have *something* to render while developing
57
+ outside a shell. Per APP-005, don't build any behavior on `user` beyond
58
+ display (a name badge, an avatar fallback). The real, shell-authenticated
59
+ user only arrives once your app is composed via federation, through the
60
+ `./App` props — code that branches on `user` for anything beyond display will
61
+ work in dev-standalone and misbehave once composed under a real shell.
62
+
63
+ ## Composed: running under a local shell
64
+
65
+ To see your app the way it will actually render — composed under the shell,
66
+ receiving real federation props — run:
67
+
68
+ ```sh
69
+ pnpm dev:shell
70
+ ```
71
+
72
+ This builds/serves your remote in federation mode (`vite --mode federation`,
73
+ see `recipes/federation.md`) and points it at a local or staging shell for
74
+ Fast Refresh (`reactRefreshHost`).
75
+
76
+ ### Pointing a staging shell at your local remote
77
+
78
+ To make a **staging** shell load your app from your local dev server instead
79
+ of its deployed remote, set this in the shell's browser `localStorage`
80
+ (**staging only** — this override does not exist in production):
81
+
82
+ ```
83
+ localStorage['lessly:dev-remote'] = "<slug>=<origin>"
84
+ ```
85
+
86
+ `<slug>` is your app's manifest `id`; `<origin>` is your local dev server's
87
+ origin (e.g. `http://localhost:5173`). For example:
88
+
89
+ ```js
90
+ localStorage['lessly:dev-remote'] = 'my-app=http://localhost:5173';
91
+ ```
92
+
93
+ Reload the staging shell after setting this — it reads the override at
94
+ startup and fetches your remote entry from `<origin>` instead of the deployed
95
+ one for the app matching `<slug>`. Remove the key (or clear `localStorage`) to
96
+ go back to the deployed remote.
97
+
98
+ ## Keeping your local setup current
99
+
100
+ Update the SDK and `@lessly/app-dev` with the `@latest` form, never `npm
101
+ update`:
102
+
103
+ ```sh
104
+ npm i @lessly/sdk-app@latest
105
+ npm i @lessly/app-dev@latest
106
+ ```
107
+
108
+ `@lessly/sdk-app` and `@lessly/app-dev` are pre-1.0 and pinned with caret
109
+ ranges, so `npm update` never crosses a minor version — and since the SDK
110
+ bumps its minor version whenever the platform ships a new tool, `npm update`
111
+ would never actually deliver those updates (see APP-007).
@@ -0,0 +1,194 @@
1
+ # Recipe: calling the platform API
2
+
3
+ How to reach Lessly platform data from an App. The normative rule is APP-001
4
+ (all platform data access goes through `@lessly/sdk-app`) and APP-006 (prefer
5
+ the generated option factories) — this page shows the *how*.
6
+
7
+ ## Create the client
8
+
9
+ Create one client with `createLesslyApp` and call operations through the
10
+ Proxy namespace tree:
11
+
12
+ ```ts
13
+ import { createLesslyApp } from '@lessly/sdk-app';
14
+
15
+ const sdk = createLesslyApp({
16
+ baseUrl: 'https://api.lessly.dev',
17
+ productId: 'prod_123',
18
+ // getCsrfToken defaults to reading the `lessly_csrf` cookie; override for SSR/tests.
19
+ });
20
+
21
+ const connectors = await sdk.organization.connectors.list({ productId: 'prod_123' });
22
+ await sdk.organization.product.create({ name: 'Acme' });
23
+ ```
24
+
25
+ > The exact namespaces you see depend on the live catalog — see
26
+ > [Namespaces and subpath imports](#namespaces-and-subpath-imports). The calls
27
+ > above use `organization`; substitute the namespace/resource you need.
28
+
29
+ ### `baseUrl`: absolute or page-relative
30
+
31
+ `baseUrl` may be either form:
32
+
33
+ - **Absolute** — `'https://api.lessly.dev'`. Works anywhere, including SSR and
34
+ tests.
35
+ - **Page-relative** — `'/lessly-api'`, the path `pnpm dev` proxies through
36
+ (see `recipes/local-dev.md`). It is resolved against the current document via
37
+ `globalThis.location`, so it is **browser-only**: outside a browser the SDK
38
+ throws a named error (`@lessly/sdk-app: relative baseUrl "/lessly-api" cannot
39
+ be resolved outside a browser`) instead of an opaque `Invalid URL`. Tests and
40
+ SSR must pass an absolute base.
41
+
42
+ ```ts
43
+ const sdk = createLesslyApp({
44
+ baseUrl: import.meta.env.DEV ? '/lessly-api' : 'https://api.lessly.dev',
45
+ productId: 'prod_123',
46
+ });
47
+ ```
48
+
49
+ `productId` is the active product — read it from your `./App` props or the
50
+ `X-Product-Id` header (see APP-005), never decoded from a token.
51
+
52
+ ## Handle errors with `LesslyApiError`
53
+
54
+ Every failed call throws a typed `LesslyApiError` with `status`, `code`, and
55
+ `body`. Branch on `code` (stable) rather than `status` alone when you need to
56
+ distinguish platform-defined failure reasons:
57
+
58
+ ```ts
59
+ import { LesslyApiError } from '@lessly/sdk-app';
60
+
61
+ try {
62
+ await sdk.organization.product.create({ name: 'Acme' });
63
+ } catch (err) {
64
+ if (err instanceof LesslyApiError) {
65
+ if (err.status === 409) {
66
+ // err.code / err.body carry the platform's structured reason
67
+ }
68
+ }
69
+ throw err;
70
+ }
71
+ ```
72
+
73
+ ### Missing CSRF cookie
74
+
75
+ Mutating calls (`POST`, `PUT`, `PATCH`, `DELETE`) require the `lessly_csrf`
76
+ cookie. If it is absent, the SDK throws before any network call:
77
+ `LesslyApiError` with `status: 0` and `code: 'csrf_cookie_missing'`. There is no
78
+ retry and no hidden token refresh — re-establish the session, or supply
79
+ `getCsrfToken` for SSR/tests. Read-only calls are unaffected.
80
+
81
+ ## Prefer the generated query/mutation option factories
82
+
83
+ Each namespace subpath (`@lessly/sdk-app/<namespace>`) ships framework-agnostic
84
+ query/mutation **option factories** — plain objects, not hooks — usable with
85
+ any TanStack Query adapter (React, Solid, Vue, Svelte). You pass the `sdk`
86
+ instance explicitly:
87
+
88
+ ```ts
89
+ // React example — works the same with any @tanstack/*-query adapter.
90
+ import { useQuery, useMutation } from '@tanstack/react-query';
91
+ import {
92
+ organizationConnectorsListQueryOptions,
93
+ organizationProductCreateMutationOptions,
94
+ } from '@lessly/sdk-app/organization';
95
+
96
+ function Connectors() {
97
+ const { data } = useQuery(organizationConnectorsListQueryOptions(sdk, { productId: 'prod_123' }));
98
+ const create = useMutation(organizationProductCreateMutationOptions(sdk));
99
+ // create.mutate({ name: 'Acme' })
100
+ }
101
+ ```
102
+
103
+ The factories return `{ queryKey, queryFn }` (and `{ mutationKey, mutationFn }`),
104
+ so they also work directly with `queryClient.ensureQueryData(...)` and friends
105
+ — useful for prefetching outside a component, e.g. in a route loader:
106
+
107
+ ```ts
108
+ await queryClient.ensureQueryData(organizationConnectorsListQueryOptions(sdk, { productId: 'prod_123' }));
109
+ ```
110
+
111
+ Prefer these factories over hand-rolling calls against `sdk.<namespace>...`
112
+ directly (APP-006): they keep query keys, request shapes, and cache
113
+ invalidation consistent with the published API surface, and stay in sync with
114
+ it automatically when you run `npm i @lessly/sdk-app@latest`.
115
+
116
+ ## Namespaces and subpath imports
117
+
118
+ The SDK is organized by namespace (currently `organization` and `playground`).
119
+ Call operations off `sdk.<namespace>.<resource>.<action>(...)`, and import that
120
+ namespace's option factories from the matching subpath,
121
+ `@lessly/sdk-app/<namespace>` — not the package root. This keeps each App's
122
+ bundle limited to the namespaces it actually uses.
123
+
124
+ Some operations belong to the namespace itself rather than to a resource under
125
+ it — listing the organizations you can see, for example, has no entity to hang
126
+ off. Those are called directly on the namespace, `sdk.<namespace>.<action>(...)`,
127
+ and their query keys drop the resource segment to match. Both forms are
128
+ generated from the live catalog; check `client.gen.ts` (or your editor's
129
+ autocomplete) for which shape a given operation takes.
130
+
131
+ The available namespaces track the live catalog and **grow as platform
132
+ extensions migrate to `@lessly-platform/tools`** — a namespace appears here only once its
133
+ tools carry a REST binding. Run `npm i @lessly/sdk-app@latest` to pick up newly
134
+ migrated namespaces. To see exactly what a given version exposes, check its
135
+ subpaths in `package.json` `exports`, or the `manifest.gen.ts` namespace list.
136
+
137
+ ## Streaming (`*Connect` factories)
138
+
139
+ Some catalog tools are ws-bound: instead of a request/response pair they carry
140
+ a `ws` binding, and the generator emits a `<toolId>Connect` factory for them
141
+ next to the option factories. That factory is the **only** sanctioned way to
142
+ stream from an App (APP-001) — it derives `ws(s)://` from your `baseUrl`, opens
143
+ the socket, and hands back a small handle:
144
+
145
+ ```ts
146
+ import { createLesslyApp } from '@lessly/sdk-app';
147
+ import { playgroundWsEchoConnect } from '@lessly/sdk-app/playground';
148
+
149
+ const sdk = createLesslyApp({ baseUrl: 'https://api.lessly.dev', productId: 'prod_123' });
150
+
151
+ const stream = playgroundWsEchoConnect(sdk, { room: 'lobby' });
152
+ const off = stream.onMessage((frame) => console.log(frame));
153
+ stream.onClose(({ code, reason, wasClean }) => console.log('closed', code, reason, wasClean));
154
+ stream.send('hello'); // queued until the socket is OPEN, then flushed in order
155
+ // later
156
+ off();
157
+ stream.close(1000, 'done');
158
+ ```
159
+
160
+ What the handle does and does not do:
161
+
162
+ - **Session cookies ride the upgrade.** No CSRF token and no product header are
163
+ sent — a browser `WebSocket` cannot set headers at all, and the gateway
164
+ authenticates the upgrade from the session.
165
+ - **Frames are raw.** `onMessage` delivers `MessageEvent.data` exactly as it
166
+ arrived (`string`, `ArrayBuffer` or `Blob`); the SDK never parses it. Framing
167
+ is protocol-specific and is documented by the tool's own description.
168
+ - **No reconnection.** A dropped connection surfaces once, through `onClose`
169
+ with the close code (`1006` for an abnormal drop). Whether and how to
170
+ reconnect is the App's decision — the SDK will not retry behind your back.
171
+ - **Params are placed like a GET.** `:token` segments are filled from the input,
172
+ everything else becomes a query parameter.
173
+
174
+ The `*Connect` factory is the App-facing surface, and the only one APP-001
175
+ sanctions. The lower-level pieces it is built on — `sdk.openStream(binding, …)`
176
+ and the `connectStream` runtime — are the SDK's own primitive: they are exported
177
+ so the generated code can use them and so tests can substitute a socket, not so
178
+ that App code can address a route by hand. Call the factory.
179
+
180
+ Streaming needs a browser: outside one (SSR, node tests) there is no
181
+ `WebSocket`, and the SDK says so by name rather than failing opaquely. Pass
182
+ `options.WebSocket` to supply an implementation in tests.
183
+
184
+ ## What not to do
185
+
186
+ - Don't open a `WebSocket` to a platform host, and don't hand-build a
187
+ `ws(s)://` URL from `baseUrl` (APP-001) — use the generated `*Connect`
188
+ factory. A tool without a `ws` binding has no sanctioned stream.
189
+ - Don't `fetch` a platform host directly, and don't use any other HTTP client
190
+ for platform data (APP-001) — always go through `sdk` or the generated
191
+ factories.
192
+ - Don't hand-roll request paths against the client when a
193
+ `*QueryOptions`/`*MutationOptions` factory already exists for that operation
194
+ (APP-006).