@lessly/sdk-app 0.1.11

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.
@@ -0,0 +1,99 @@
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 { domains } = await sdk.tracking.domains.list({ page: 1 });
22
+ await sdk.tracking.domains.create({ host: 'links.example.com' });
23
+ ```
24
+
25
+ `productId` is the active product — read it from your `./App` props or the
26
+ `X-Product-Id` header (see APP-005), never decoded from a token.
27
+
28
+ ## Handle errors with `LesslyApiError`
29
+
30
+ Every failed call throws a typed `LesslyApiError` with `status`, `code`, and
31
+ `body`. Branch on `code` (stable) rather than `status` alone when you need to
32
+ distinguish platform-defined failure reasons:
33
+
34
+ ```ts
35
+ import { LesslyApiError } from '@lessly/sdk-app';
36
+
37
+ try {
38
+ await sdk.tracking.domains.create({ host: 'links.example.com' });
39
+ } catch (err) {
40
+ if (err instanceof LesslyApiError) {
41
+ if (err.status === 409) {
42
+ // err.code / err.body carry the platform's structured reason
43
+ }
44
+ }
45
+ throw err;
46
+ }
47
+ ```
48
+
49
+ ## Prefer the generated query/mutation option factories
50
+
51
+ Each namespace subpath (`@lessly/sdk-app/<namespace>`) ships framework-agnostic
52
+ query/mutation **option factories** — plain objects, not hooks — usable with
53
+ any TanStack Query adapter (React, Solid, Vue, Svelte). You pass the `sdk`
54
+ instance explicitly:
55
+
56
+ ```ts
57
+ // React example — works the same with any @tanstack/*-query adapter.
58
+ import { useQuery, useMutation } from '@tanstack/react-query';
59
+ import {
60
+ trackingDomainsListQueryOptions,
61
+ trackingDomainsCreateMutationOptions,
62
+ } from '@lessly/sdk-app/tracking';
63
+
64
+ function Domains() {
65
+ const { data } = useQuery(trackingDomainsListQueryOptions(sdk, { page: 1 }));
66
+ const create = useMutation(trackingDomainsCreateMutationOptions(sdk));
67
+ // create.mutate({ host: 'links.example.com' })
68
+ }
69
+ ```
70
+
71
+ The factories return `{ queryKey, queryFn }` (and `{ mutationKey, mutationFn }`),
72
+ so they also work directly with `queryClient.ensureQueryData(...)` and friends
73
+ — useful for prefetching outside a component, e.g. in a route loader:
74
+
75
+ ```ts
76
+ await queryClient.ensureQueryData(trackingDomainsListQueryOptions(sdk, { page: 1 }));
77
+ ```
78
+
79
+ Prefer these factories over hand-rolling calls against `sdk.<namespace>...`
80
+ directly (APP-006): they keep query keys, request shapes, and cache
81
+ invalidation consistent with the published API surface, and stay in sync with
82
+ it automatically when you run `npm i @lessly/sdk-app@latest`.
83
+
84
+ ## Namespaces and subpath imports
85
+
86
+ The SDK is organized by namespace (`tracking`, `mail`, `playground`, ...). Call
87
+ operations off `sdk.<namespace>.<resource>.<action>(...)`, and import that
88
+ namespace's option factories from the matching subpath,
89
+ `@lessly/sdk-app/<namespace>` — not the package root. This keeps each App's
90
+ bundle limited to the namespaces it actually uses.
91
+
92
+ ## What not to do
93
+
94
+ - Don't `fetch` a platform host directly, and don't use any other HTTP client
95
+ for platform data (APP-001) — always go through `sdk` or the generated
96
+ factories.
97
+ - Don't hand-roll request paths against the client when a
98
+ `*QueryOptions`/`*MutationOptions` factory already exists for that operation
99
+ (APP-006).
package/docs/rules.md ADDED
@@ -0,0 +1,89 @@
1
+ # App Rules
2
+
3
+ Normative rules for building a Lessly App on `@lessly/sdk-app`. Read this
4
+ before writing code; see `README.md` for the map of the rest of the guide.
5
+
6
+ Severity (RFC 2119): **MUST** blocks merge, **SHOULD** is a strong
7
+ recommendation, **MAY** is an allowed option.
8
+
9
+ Rule IDs (`APP-NNN`) are immutable once merged: a rule's ID and meaning never
10
+ change, even if its wording is later clarified. Some of these rules cover
11
+ ground shared with extensions and the platform's Module Federation host; where
12
+ that's true, this file references the existing `FED-*` rule ID from the
13
+ extensions-guide instead of restating it — go there for the full rule text,
14
+ rationale, and examples.
15
+
16
+ ---
17
+
18
+ ### APP-001 (MUST) — All platform data access goes through `@lessly/sdk-app`
19
+
20
+ Every read or write to a Lessly platform API MUST go through a
21
+ `@lessly/sdk-app` client created with `createLesslyApp`, or through the
22
+ generated `*QueryOptions`/`*MutationOptions` factories built on top of it. An
23
+ App MUST NOT `fetch` a platform host directly, and MUST NOT use any other HTTP
24
+ client to reach platform data.
25
+
26
+ ### APP-002 (MUST) — Frontend-only
27
+
28
+ An App MUST NOT ship a backend service, a database, or an MCP endpoint. All
29
+ of its logic runs in the browser, built on `@lessly/sdk-app` and the Module
30
+ Federation contract below. If a feature needs server-side state or a
31
+ service-to-service call, it belongs in an extension, not an App.
32
+
33
+ ### APP-003 (MUST) — Manifest is `lessly.app.yaml`, schema v1
34
+
35
+ The manifest file is `lessly.app.yaml`, and its top-level surface is exactly:
36
+ `app_schema_version: 1`, `id`, `title`, `version`, `nav` (with `nav.icon`). No
37
+ other top-level field is valid — in particular, no extension/backend manifest
38
+ field (`s2s`, `webhooks`, `billing`, `analytics`, `routing`, `public`, ...)
39
+ belongs in an App manifest.
40
+
41
+ ### APP-004 (MUST) — Module Federation contract
42
+
43
+ An App exposes `./App` as its Module Federation remote entry, implementing
44
+ the shell's remote contract (FED-001). The Module Federation `name` MUST equal
45
+ the manifest `id` (FED-004). React, React DOM, React Router, and `@lessly/ui`
46
+ MUST be declared as shared singletons, never bundled (FED-005), using
47
+ `@module-federation/vite` pinned at exactly `1.16.12` (FED-006). The asset
48
+ base in federation mode is the absolute path `/~/<id>/`. The build MUST
49
+ produce one merged dual-mode `dist/` covering both standalone and federation
50
+ artifacts (FED-007). An App MAY additionally expose a data-only `./navigation`
51
+ module (FED-011). This rule states the contract; it does not restate FED-001,
52
+ FED-004, FED-005, FED-006, FED-007, or FED-011 — see those rules for exact
53
+ wording, rationale, and examples.
54
+
55
+ ### APP-005 (MUST) — Auth is ambient; an App does not own it
56
+
57
+ Authentication is ambient: the hosting shell owns cookies in production and a
58
+ bearer proxy in local dev. An App MUST NOT implement login, token handling,
59
+ refresh, or product switching — those belong to the shell. An App MUST read
60
+ the active product from its props or the `X-Product-Id` header, never by
61
+ decoding a token.
62
+
63
+ An App MUST NOT rely on the `user` prop for anything beyond display. In
64
+ dev-standalone mode, `user` is a stub (`{ id }` sourced from the local dev
65
+ JWT, `displayName: 'Dev User'`) — it is not a real session. The real,
66
+ shell-authenticated user only arrives once the app is composed via
67
+ federation, through the `./App` props.
68
+
69
+ ### APP-006 (SHOULD) — Use the generated query/mutation option factories
70
+
71
+ Prefer the generated `*QueryOptions`/`*MutationOptions` factories
72
+ (`@lessly/sdk-app/<namespace>`) over hand-rolling request paths against the
73
+ SDK client directly. They keep query keys, request shapes, and cache
74
+ invalidation consistent with the published API surface, and stay in sync with
75
+ it automatically on upgrade.
76
+
77
+ ### APP-007 (MUST) — Versioning hygiene
78
+
79
+ Bump the manifest `version` on every release. Keep the SDK and this guide
80
+ current with:
81
+
82
+ ```sh
83
+ npm i @lessly/sdk-app@latest
84
+ ```
85
+
86
+ Use this exact `@latest` form — not `npm update`. `@lessly/sdk-app` is
87
+ pre-1.0 and pinned with a caret range, so `npm update` never crosses a minor
88
+ version; since this SDK also bumps its minor version whenever the platform
89
+ ships a new tool, `npm update` would never actually deliver those updates.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@lessly/sdk-app",
3
+ "version": "0.1.11",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "docs/*.md",
12
+ "docs/recipes",
13
+ "src/gen"
14
+ ],
15
+ "publishConfig": {
16
+ "registry": "https://registry.npmjs.org/",
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "generate": "tsx scripts/generate.ts fixtures/catalog.sample.json",
21
+ "ci": "tsx scripts/ci-run.ts",
22
+ "sync-exports": "tsx scripts/sync-exports.ts",
23
+ "build": "npm run sync-exports && tsup",
24
+ "prepublishOnly": "npm run build",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "json-schema-to-typescript": "^15.0.3",
31
+ "tsup": "^8.3.0",
32
+ "tsx": "^4.19.0",
33
+ "typescript": "^5.6.0",
34
+ "vitest": "^2.1.0"
35
+ },
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "require": "./dist/index.cjs"
41
+ },
42
+ "./organization": {
43
+ "types": "./dist/organization/index.d.ts",
44
+ "import": "./dist/organization/index.js",
45
+ "require": "./dist/organization/index.cjs"
46
+ },
47
+ "./playground": {
48
+ "types": "./dist/playground/index.d.ts",
49
+ "import": "./dist/playground/index.js",
50
+ "require": "./dist/playground/index.cjs"
51
+ }
52
+ },
53
+ "sdkContentHash": "sha256:c6888491b9e25a037a38993fa524e1a5b646053465b66a88c5f7dfe3ba69ce70"
54
+ }