@elevasis/sdk 1.49.0 → 1.50.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/sdk",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "description": "SDK for building Elevasis organization resources",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "dist/test-utils/index.js",
33
33
  "dist/test-utils/index.d.ts",
34
34
  "dist/node/",
35
+ "dist/chunk-*.js",
35
36
  "dist/cli.cjs",
36
37
  "reference/"
37
38
  ],
@@ -41,8 +42,14 @@
41
42
  "remark-gfm": "^4.0.1"
42
43
  },
43
44
  "peerDependencies": {
45
+ "vitest": ">=3",
44
46
  "zod": "^4.1.0"
45
47
  },
48
+ "peerDependenciesMeta": {
49
+ "vitest": {
50
+ "optional": true
51
+ }
52
+ },
46
53
  "devDependencies": {
47
54
  "@types/node": "^22.0.0",
48
55
  "chalk": "^5.3.0",
@@ -54,8 +61,9 @@
54
61
  "rollup-plugin-dts": "^6.3.0",
55
62
  "tsup": "^8.0.0",
56
63
  "typescript": "5.9.2",
64
+ "vitest": "^3.2.4",
57
65
  "zod": "^4.1.0",
58
- "@repo/core": "0.64.0",
66
+ "@repo/core": "0.65.0",
59
67
  "@repo/eslint-config": "0.0.0",
60
68
  "@repo/typescript-config": "0.0.0"
61
69
  },
@@ -16,7 +16,10 @@ The helpers live here rather than in `@repo/ui` because they have three consumer
16
16
  | `ContentDistributionResponseSchema`, `CreateContentDistributionRequestSchema`, `UpdateContentDistributionRequestSchema` | `content_distributions` transport shape, including the manual-publish fields (`publishMethod`, `platformPostId`, `platformUrl`). |
17
17
  | `ContentItemSourceAssetResponseSchema`, `ContentItemSourceAssetInputSchema`, `UpdateContentItemSourceAssetRequestSchema`, `ReorderContentItemSourceAssetsRequestSchema`, `ContentAssetCropSchema` | `content_item_source_assets` transport shape — an item's ordered source assets, `position` 0 is the cover. `crop` is per-MEMBERSHIP normalized fractions, because the same photo crops differently in different items. The initial set rides `CreateContentItemRequestSchema.sourceAssets`; every later mutation is its own method, and `UpdateContentItemRequestSchema` deliberately never touches membership. `derivativePath` / `derivativeCrop` / `derivativeRenderedAt` carry the rendered crop: a stored JPEG in the `content-derivatives` bucket, so the cropped image exists as a file rather than only as CSS. Stale is derived — the two crops differ — never stored as a flag. |
18
18
  | `PlatformContent`, `YouTubeContent`, `LinkedInContent`, `InstagramContent`, `XContent`, `AnyPlatformContent`, `PlatformContentMap`, `Platform` | Typed interfaces for platform-specific content stored in `content_distributions.platform_content`. |
19
- | `isOpenContentReviewGate`, `getOpenContentReviewGates` | The one predicate deciding whether a `processing_state` entry is an open, unreviewed `queued` gate. Consumed by the `/queue` handler, `reviewItem`'s `stepKey` validation, and the review page. |
19
+ | `isOpenContentReviewGate`, `getOpenContentReviewGates`, `OpenContentReviewGate` | The one predicate deciding whether a `processing_state` entry is an open, unreviewed gate on a **queue-reviewed** step — `step.reviewVenue === 'queue'`, entry not `running`, and `executionId` still non-null. It answers "can the queue screen clear this", so a Workspace-reviewed step has no gate here. Consumed by the `/queue` handler, `reviewItem`'s `stepKey` validation, and the review page. |
20
+ | `resolveCurrentContentStep` | The step an item is sitting at: steps ascending by `order` (ties by `key`), first one not yet `'success'`, falling back to the last step once all have succeeded. Returns `undefined` for no declared steps and for an unplaced item whose `processingState` names no key the current catalog declares. Duplicates `deriveItemPlacement`'s ordering rule by hand — keep the two in sync. |
21
+ | `isAwaitingReview` | Cheap boolean for counting and filtering: is a person owed an action on this item. True when the step's `reviewMode` is not `'none'` and `item.reviewedAt` is still null. Takes whatever step the caller already resolved, typically via `resolveCurrentContentStep`. |
22
+ | `resolveReviewTarget`, `ContentReviewTarget` | The exhaustive answer surfaces render from — **where** the review happens, not just whether one is owed. An open queue gate wins first, then a current step with `reviewVenue === 'workspace'` that `isAwaitingReview` plus a resolved route, else `{ kind: 'none' }`. Deliberately separate from `isAwaitingReview` so callers stop recombining "is one owed" and "where" with `&&` at the point of use. |
20
23
  | `deriveContentBoard`, `ContentBoard`, `ContentBoardCard`, `ContentBoardColumn`, `ContentBoardGate` | Placement: `(items, pipeline, now)` to columns, gates, waiting counts, plus the `done` and `unplaced` buckets. Pure. Consumed by the Command Center board and `elevasis-sdk content:board`. |
21
24
  | `getContentItemIdentity` | Names an item for display when `title` cannot — first non-blank line of `body`, then the first string payload field, then `title`, then `'Untitled'`. |
22
25
 
@@ -109,7 +109,7 @@ Then inspect:
109
109
 
110
110
  - `core/config/organization-model.ts` -- Organization Model overrides plus Systems and Resources descriptors. Start here for feature labels, surface mapping, and resource identity/governance changes.
111
111
  - `ui/src/routes/__root.tsx` and the relevant nav config files
112
- - Access-gating call sites: `useAccess(accessKey)` from `@elevasis/ui/hooks` and `<AccessGuard accessKey={...}>` from `@elevasis/ui/auth`. The template ships no local `useFeatureAccess.ts` and there is no hook factory -- these two published entry points are the whole gating surface.
112
+ - Access-gating call sites: `useAccess(accessKey)` from `@elevasis/ui/hooks` and `<AccessGuard accessKey={...}>` from `@elevasis/ui/auth`. The template ships no local `useFeatureAccess.ts` and there is no hook factory -- these two published entry points are the whole gating surface. A System's top-level route file wraps that pairing in `<ProtectedSystemRoute accessKey={...}>` from `@elevasis/ui/features/auth`, which is what every template System route ships; see `ui.md` "System route guarding".
113
113
 
114
114
  Verify with: published docs for `@elevasis/core/organization-model`, plus current scaffold routes and manifests.
115
115
 
@@ -14,7 +14,9 @@ paths:
14
14
  - `ElevasisUIProvider` in `ui/src/main.tsx` auto-composes shared UI, auth, and API surface -- route files do not wire providers manually
15
15
  - `useApiClient()` from `@elevasis/ui/api` for authenticated API calls, passing `API_URL` from `@/lib/constants/api` -- never raw `fetch` with auth headers. There is no `ui/src/lib/hooks/` directory; see `ui.md` "API and Streaming"
16
16
  - `routeTree.gen.ts` is auto-generated on `pnpm dev` -- never edit manually
17
- - Auth protection: wrap page content with `ProtectedRoute` from `@elevasis/ui/auth`. Gate further with `AccessGuard accessKey={...}` nested inside `ProtectedRoute` -- a System path for system gating, or an `AccessKeys` constant (e.g. `AccessKeys.platformAdmin`) for permission-backed gating. `FeatureGuard`, `SystemGuard`, `SurfaceGuard`, and `AdminGuard` are retired -- do not reintroduce them
17
+ - Auth protection: wrap page content with `ProtectedRoute` from `@elevasis/ui/features/auth`. Gate further with `AccessGuard accessKey={...}` nested inside `ProtectedRoute` -- a System path for system gating, or an `AccessKeys` constant (e.g. `AccessKeys.platformAdmin`) for permission-backed gating. `FeatureGuard`, `SystemGuard`, `SurfaceGuard`, and `AdminGuard` are retired -- do not reintroduce them
18
+ - **For a System's top-level route, use `ProtectedSystemRoute accessKey={...}` from `@elevasis/ui/features/auth` instead of writing that pairing by hand.** It expands to exactly the `ProtectedRoute` + `AccessGuard` nesting above and adds `SystemUnavailableState` as the guard's `fallback`, so a denied user sees the reason rather than `AccessGuard`'s blank `fallback = null` default. Every System route in the template ships this shape; see `ui.md` "System route guarding". Keep `createFileRoute` in the route file and keep bare `AccessGuard` for gating below the route level
19
+ - **Two `ProtectedRoute` components exist and the subpath you import from decides what the user sees while the app initializes.** `@elevasis/ui/features/auth` is the one route files want: it wraps the base guard with an animated full-screen loader and a default `AppShellError` error fallback. `@elevasis/ui/auth` exports the headless base guard, whose `fallback` defaults to `null` -- importing it without passing a `fallback` renders a blank screen for the whole initialization window and swallows initialization errors. Reach for the headless one only when the surface renders its own loading state. `AccessKeys` is a plain constant table and stays on `@elevasis/ui/auth` (or `@elevasis/ui/hooks`) either way
18
20
  - Never fork `@elevasis/ui` components -- if a published component needs a tweak, that missing capability is a bug in `@elevasis/ui`
19
21
 
20
22
  ## Silent-Break Gotchas
@@ -11,11 +11,13 @@ External projects consume the **published `@elevasis/*` surface only**. The Elev
11
11
 
12
12
  ## What You Can Import
13
13
 
14
- | Package | Subpaths | Where it lives in `node_modules` |
15
- | ---------------- | -------------------------------------------- | -------------------------------- |
16
- | `@elevasis/sdk` | default, `/worker`, `/node`, `/test-utils` | `node_modules/@elevasis/sdk/` |
17
- | `@elevasis/ui` | default, `/auth`, `/initialization`, etc. | `node_modules/@elevasis/ui/` |
18
- | `@elevasis/core` | `/organization-model`, `/entities`, `/utils` | `node_modules/@elevasis/core/` |
14
+ | Package | Subpaths | Where it lives in `node_modules` |
15
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
16
+ | `@elevasis/sdk` | default, `/worker`, `/node`, `/test-utils` | `node_modules/@elevasis/sdk/` |
17
+ | `@elevasis/ui` | default, `/auth`, `/initialization`, etc. | `node_modules/@elevasis/ui/` |
18
+ | `@elevasis/core` | default, `/auth`, `/test-utils`, `/organization-model`, `/organization-model/readiness`, `/content`, `/entities`, `/knowledge` | `node_modules/@elevasis/core/` |
19
+
20
+ `@elevasis/core`'s eight subpaths are its `publishConfig.exports`, and that is the whole published surface -- there is no `/utils`. `/content` is where `deriveContentBoard`, `resolveReviewTarget`, `isAwaitingReview`, and the content item and source-asset schemas live, so an import from it is legitimate.
19
21
 
20
22
  ## What You Will See in Monorepo Docs (and should NOT import)
21
23
 
@@ -60,10 +60,12 @@ The app uses WorkOS AuthKit through `ElevasisUIProvider`. Authentication is enfo
60
60
 
61
61
  **Route protection:**
62
62
 
63
- Wrap protected route components with `ProtectedRoute` from `@elevasis/ui/auth`. For access-gated feature routes, nest it with `AccessGuard` (see `ui/src/routes/lead-gen.tsx` and `crm.tsx` for the canonical pairing):
63
+ Wrap protected route components with `ProtectedRoute` from `@elevasis/ui/features/auth`. For a **System's top-level route**, use `ProtectedSystemRoute` instead -- it is the canonical shape and is what `ui/src/routes/crm.tsx`, `lead-gen.tsx`, `monitoring.tsx`, `operations.tsx`, `projects.tsx`, `content.tsx`, and `knowledge.tsx` all ship as. See "System route guarding" below. `ProtectedRoute` on its own is for a protected route that gates on authentication alone:
64
+
65
+ `@elevasis/ui` publishes two components under this name and they behave differently. The one route files want is `@elevasis/ui/features/auth`: it wraps the base guard with an animated full-screen loader that fades out when initialization completes, and supplies a default `AppShellError` fallback so an initialization failure renders a retry surface instead of nothing. `@elevasis/ui/auth` exports the headless base guard, whose `fallback` prop defaults to `null` -- a route that imports it and passes no `fallback` shows a blank screen for the entire initialization window and silently swallows blocking errors. Import the headless one only when the surface deliberately renders its own loading state.
64
66
 
65
67
  ```tsx
66
- import { ProtectedRoute } from '@elevasis/ui/auth'
68
+ import { ProtectedRoute } from '@elevasis/ui/features/auth'
67
69
 
68
70
  function HomePageGuarded() {
69
71
  return (
@@ -74,14 +76,38 @@ function HomePageGuarded() {
74
76
  }
75
77
  ```
76
78
 
79
+ **System route guarding:**
80
+
81
+ `ProtectedSystemRoute` from `@elevasis/ui/features/auth` is the shape a System's top-level route file uses. It composes `ProtectedRoute` + `AccessGuard` and supplies `SystemUnavailableState` as the guard's `fallback`, so a denial renders the specific reason `checkAccess` already computed instead of `AccessGuard`'s own blank `fallback = null` default:
82
+
83
+ ```tsx
84
+ import { ProtectedSystemRoute } from '@elevasis/ui/features/auth'
85
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
86
+
87
+ export const Route = createFileRoute('/crm')({
88
+ component: CrmLayout
89
+ })
90
+
91
+ function CrmLayout() {
92
+ return (
93
+ <ProtectedSystemRoute accessKey="sales.crm">
94
+ <Outlet />
95
+ </ProtectedSystemRoute>
96
+ )
97
+ }
98
+ ```
99
+
100
+ Three things this does not change. `createFileRoute` stays in the route file -- TanStack's file-based routing requires it there, and `ProtectedSystemRoute` only replaces the route's layout body. `AccessGuard` is unchanged and is still what you reach for below the route level, or for a permission-backed `AccessKeys` gate that is not a System path. And a leaf page under an already-guarded layout route needs no guard of its own -- the layout's guard covers it.
101
+
77
102
  **Access gating:**
78
103
 
79
104
  Access is resolved by the unified Access Model -- System lifecycle from the organization model, role permissions, diagnostic allowlists, membership scope, and platform-admin bypass -- through two published symbols. There is no `createFeatureAccessHook` and no `SystemGuard`; both are retired and must not be reintroduced.
80
105
 
81
- `AccessGuard` from `@elevasis/ui/auth` wraps a subtree:
106
+ `AccessGuard` from `@elevasis/ui/auth` wraps a subtree. Prefer `ProtectedSystemRoute` above when the subtree is a whole System's route; the hand-written pairing below is what it expands to, and is still correct for non-System gating:
82
107
 
83
108
  ```tsx
84
- import { AccessGuard, AccessKeys, ProtectedRoute } from '@elevasis/ui/auth'
109
+ import { AccessKeys } from '@elevasis/ui/auth'
110
+ import { AccessGuard, ProtectedRoute } from '@elevasis/ui/features/auth'
85
111
 
86
112
  <ProtectedRoute>
87
113
  <AccessGuard accessKey="sales.crm">
@@ -52,23 +52,25 @@ Dotted IDs such as `analytics.reports` inherit lifecycle and shell placement fro
52
52
 
53
53
  ## Route-level system gate
54
54
 
55
+ `ProtectedSystemRoute` is the shape a System's top-level route uses. It composes `ProtectedRoute` + `AccessGuard` and supplies `SystemUnavailableState` as the fallback, so a denial renders the reason `checkAccess` computed instead of a blank screen:
56
+
55
57
  ```tsx
56
- import { AccessGuard, ProtectedRoute } from '@elevasis/ui/auth'
58
+ import { ProtectedSystemRoute } from '@elevasis/ui/features/auth'
57
59
  import { createFileRoute, Outlet } from '@tanstack/react-router'
58
60
 
59
61
  export const Route = createFileRoute('/analytics')({ component: AnalyticsLayout })
60
62
 
61
63
  function AnalyticsLayout() {
62
64
  return (
63
- <ProtectedRoute>
64
- <AccessGuard accessKey="analytics">
65
- <Outlet />
66
- </AccessGuard>
67
- </ProtectedRoute>
65
+ <ProtectedSystemRoute accessKey="analytics">
66
+ <Outlet />
67
+ </ProtectedSystemRoute>
68
68
  )
69
69
  }
70
70
  ```
71
71
 
72
+ Writing the pairing by hand is still correct and is exactly what the above expands to. Reach for it when the gate is not a whole System's route -- see the admin-only recipe below.
73
+
72
74
  The sidebar is derived from `OrganizationModel.systems`; hiding a node there is display behavior only. Keep route guards in place for direct URL access.
73
75
 
74
76
  ## Admin-only route
@@ -18,7 +18,17 @@ The shell derives visible sidebar entries from `shellModel.topLevel()` and `shel
18
18
 
19
19
  ## Route Guards
20
20
 
21
- Navigation visibility is cosmetic. Always guard routes directly:
21
+ Navigation visibility is cosmetic. Always guard routes directly. For a System's top-level route use `ProtectedSystemRoute`, which composes `ProtectedRoute` + `AccessGuard` and renders `SystemUnavailableState` on denial instead of a blank fallback:
22
+
23
+ ```tsx
24
+ import { ProtectedSystemRoute } from '@elevasis/ui/features/auth'
25
+
26
+ <ProtectedSystemRoute accessKey="sales.crm">
27
+ <Outlet />
28
+ </ProtectedSystemRoute>
29
+ ```
30
+
31
+ That expands to the pairing below, which stays correct for gating that is not a whole System's route:
22
32
 
23
33
  ```tsx
24
34
  import { AccessGuard } from '@elevasis/ui/auth'