@lotics/app-sdk 0.125.9 → 0.126.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.
@@ -1,7 +1,7 @@
1
1
  # Members & select options
2
2
 
3
- How an app renders and picks **people** and **select-field options**, the host context they are
4
- read from, and **record comments**. Query mechanics live in [queries](./queries.md); the authority
3
+ How an app renders and picks **people** and **select-field options**, and the host context they are
4
+ read from. Query mechanics live in [queries](./queries.md); the authority
5
5
  model in [security](./security.md).
6
6
 
7
7
  ## Cells vs. catalogs — the model
@@ -261,11 +261,6 @@ host did not say (a visitor from outside the organization, an older host). Both
261
261
  context, read once as the app opens. Display only: the workflow body re-checks who may act
262
262
  (`current_member_stands_in_for(...)`).
263
263
 
264
- `openableApps` lists the workspace's apps the viewer can open, each by its `id` and its `name`, and `table_id`, the table its register lists, where the host says — what a
265
- way through to a sibling app (`openApp`, `peekApp`) is offered by. `null` where the host did not say (standalone, a
266
- render, an older host, or a page that had not listed its apps when the app opened). Display only: the
267
- app opened checks the viewer itself.
268
-
269
264
  `hostNamesApp` is `true` where the host's own chrome already names the app on screen (the workspace's
270
265
  app page), `false` everywhere else and from an older host. Where it is `true`, a screen that titles
271
266
  itself with the app's name draws no visible title, keeping its `<h1>` for assistive technology alone
@@ -290,94 +285,6 @@ until it resolves, where it failed, and from a host that predates them.
290
285
  where the field states no code of its own. Pass it as the money's `currency`: a money format
291
286
  given no code prints a default market, which says nothing about this workspace.
292
287
 
293
- ## Comments: `useComments` / `useCommentCounts`
294
-
295
- Record comments — member-to-member discussion attached to any record the app reaches
296
- (`dist/comments.d.ts`).
297
-
298
- ### Enabling comments
299
-
300
- Opt in on the app — a JSON app states it in its model; a custom-code app sets it live, as a new
301
- version:
302
-
303
- ```jsonc
304
- // set_app
305
- { "app_id": "app_…", "capabilities": { "comments": true } }
306
- ```
307
-
308
- Both hooks expose **`available`** — `true` only when a member is signed in **and** the app declared
309
- the capability. Gate the composer on it. When `false` (any standalone/public app view, or an app
310
- that didn't opt in) lists stay empty, `loading` settles to `false`, and every mutation rejects —
311
- client-side up front, and re-checked server-side.
312
-
313
- ### Authority & attribution
314
-
315
- - **Access runs under the app, not the member's table IAM.** A member who can use the app (and the
316
- app declares `comments`) may read/write comments on any record the app's workspace holds,
317
- regardless of their own table access. The one floor: the record must live in the app's own
318
- workspace — a crafted foreign `record_id` errors.
319
- - **The author is always the real signed-in member** — correct attribution, enforced server-side.
320
- **Warning:** under "View as", comments still author as the real member (the admin), not the
321
- view-as target — unlike `useViewer` and `is_current_member` scoping, which follow the target.
322
- - **Edit and delete are author-only**, checked server-side against the viewer. `Timeline`'s
323
- `currentMemberId` prop drives the matching affordance client-side.
324
-
325
- ### Reading & writing
326
-
327
- ```tsx
328
- // In a record screen, where an id definitionally exists:
329
- const { comments, available, createComment } = useComments({ record_id: recordId });
330
-
331
- // From a row, gate the whole panel rather than passing a placeholder:
332
- {row.__source_record_id && <CommentsPanel recordId={row.__source_record_id} />}
333
- ```
334
-
335
- `record_id` must be a **real** id — the fetch keys on `available` alone, so an empty string is not
336
- skipped, it fetches comments for nothing. When the id comes from a row's `__source_record_id`
337
- addressing column (see [queries](./queries.md)) narrow it first: a grouped query answers its addressing
338
- columns as `null`, and a hook cannot be called conditionally, so the guard belongs at the component that
339
- renders the panel. State: `{ comments, loading, error, available, createComment,
340
- updateComment, deleteComment, refetch }`.
341
-
342
- - `comments` — newest first on the wire (server order); a thread usually reads oldest-first, so
343
- reverse it for display. Each `AppComment`: `{ id, record_id,
344
- table_id, member_id, author, content, files, workspace_id, created_at, updated_at }`. Attachments
345
- (`AppCommentFile`) carry `id` / `filename` / `mime_type` and a presigned `url` (plus
346
- `thumbnail_url` for an image), valid 24 hours and refreshed by every read. A file's identity is
347
- its `id`: the server reads every attachment from the file by that id, so nothing else you hold
348
- about a file can affect what is stored.
349
- - `createComment({ content, file_ids? })` — posts as the viewing member. `file_ids` come from
350
- `useFileUpload` / `useAttachments` (see [files](./files.md)). A comment must have content or at
351
- least one file (empty input is a client no-op; the server enforces the same rule). A file id
352
- that names no live file in this workspace refuses the comment. Content max 10,000 characters.
353
- - `updateComment(id, { content, files? })` — omit `files` to keep the current attachment set (a
354
- text-only edit never drops attachments); pass `files` to replace the whole set. An id the comment does not already hold must name a live
355
- file in this workspace; one it holds stays even once its file is archived.
356
- - All three mutations apply **optimistically** with rollback on error, then repopulate from the
357
- server. **Limitation:** an optimistic create renders a placeholder row (temporary id, empty
358
- `table_id`/`workspace_id`, `files: null`) until the refetch lands — don't persist anything keyed
359
- on it.
360
- - **The author names itself.** `author` carries `{ name, image? }`, resolved server-side on every
361
- comment the server returns — list, create and update alike — including one written from another
362
- app by someone the record references nowhere, so a thread needs no declared member access.
363
- `name` is `null` when the id no longer resolves in the org (a removed member) — render a
364
- fallback (`Timeline` has an `unknownMember` label for exactly this). The one comment with no
365
- `author` is the optimistic row `createComment` renders locally: the SDK knows the viewer's id and
366
- not their name, and it will not invent one. The server's row replaces it when the refetch lands.
367
- - **Freshness:** cached like a query — re-read on arrival and on focus/reconnect, so another
368
- viewer's comment appears on the next arrival, focus or explicit `refetch()` — not on its own: the realtime channel carries **record**
369
- changes, and a comment emits no record event. Your queries DO get pushed
370
- ([data_fetching.md](./data_fetching.md)).
371
-
372
- ### Counts
373
-
374
- `useCommentCounts({ table_id })` → `{ counts, loading, error, available, refetch }` where `counts`
375
- is `{ record_id: count }` for the whole table — row badges without shipping any comment content
376
- (one server-side aggregate). Same gate and authority as `useComments`. `table_id` is the source
377
- table's id: read it off a row of the query that renders the badges (`rows[0]?.__source_table_id`)
378
- rather than pasting a `tbl_` id, which a copy of the app cannot carry. Pass `undefined` while no
379
- row is on screen — no request is sent and `counts` is `{}`.
380
-
381
288
  ## Outside links: `useOutsideLinks`
382
289
 
383
290
  `useOutsideLinks(recordId)` (`dist/outside_link.d.ts`) → `{ links, loading, error, available, create, revoke }` — a
@@ -400,4 +307,3 @@ to the link made, or `null` when the member backed out; `revoke` to whether it d
400
307
  | `useMembers` | ✓ (same-org + declaration gates) | ✗ errors |
401
308
  | `useViewer` | member id (view-as target) | `null` |
402
309
  | `useWorkspaceTimezone` / `useWorkspaceCurrency` | ✓ | ✓ |
403
- | `useComments` / `useCommentCounts` | ✓ when capability declared | ✗ `available: false` |
package/docs/mutations.md CHANGED
@@ -332,8 +332,7 @@ const onClose = async (recordId: string) => {
332
332
  Only on **success**: a refused write changed nothing. A body that names a table only at run time,
333
333
  a run whose writes could not be predicted, a run before the app's write model has loaded, a run
334
334
  by a visitor from outside your organization, and a `useAgentRun` leg that lands, re-read every
335
- mounted query. A comment written or deleted re-reads
336
- every `useCommentCounts`. Under a design-time
335
+ mounted query. Under a design-time
337
336
  fixture every mounted read re-reads, since a fixture has no body to read tables from. The host's
338
337
  realtime push is separate: it carries changes **other** people make, and is far too slow for your
339
338
  own.
@@ -54,6 +54,9 @@ routes, and splats all work. Inside the tree, use react-router normally:
54
54
  message naming the path and a link to the first route — instead of an empty
55
55
  page, and a layout route keeps its shell around that message. Declaring your
56
56
  own `{ path: "*" }` replaces it.
57
+ - **A route that decides its own addresses draws the same screen.** `NotFoundScreen`
58
+ (`{ home, words }`) is exported from `@lotics/app-sdk/router` for a splat route
59
+ that parses the path itself and finds none of its screens at it.
57
60
  - **The not-found screen speaks the app's language.** Its two strings default
58
61
  to English; an app whose reader reads another language passes them as
59
62
  `notFound`, next to `routes`:
@@ -130,6 +133,10 @@ never the query or hash. The host counts one view per change, so a
130
133
  static segment is reported as written, so never build a route's `path` from
131
134
  your data: a record's id or name goes in a `:param`.
132
135
 
136
+ A splat route (`path: "/*"`) stands for many screens, so it states each one's
137
+ shape itself, from the path alone and with no id or text in it:
138
+ `handle: { shape: (pathname) => "/:view/:row" }`.
139
+
133
140
  A drawer the app opens at a query key rather than a path is a screen too when
134
141
  `drawers` names its key, listed from the one drawn lowest:
135
142
 
package/docs/runtime.md CHANGED
@@ -3,7 +3,7 @@
3
3
  How an app boots and talks to the platform: **`mount()`** (with the design-time mock harness),
4
4
  the **two transports** the SDK switches between (app code never branches), the raw **`rpc()`**
5
5
  escape hatch, and the browser capabilities the sandbox would otherwise block — **`openExternal`**,
6
- **`openApp`** / **`peekApp`**, **`downloadFile`**, and **`requestGeofencedLocation`** / **`isWithinZone`**.
6
+ **`downloadFile`**, and **`requestGeofencedLocation`** / **`isWithinZone`**.
7
7
 
8
8
  ## `mount()` — the entry point
9
9
 
@@ -114,7 +114,7 @@ pending state; `fixture.exportApps[key]` answers `exportApp(key, narrowing)` the
114
114
  "live", elapsed_seconds: 754, inputs: { site: "rec_1" } } }` makes
115
115
  `useRecording("log_visit")` available with that `state`; `start` and `stop`
116
116
  resolve without recording anything or moving the state.
117
- - **Not mocked:** uploads, `useFieldOptions`, members, comments. In mock mode
117
+ - **Not mocked:** uploads, `useFieldOptions`, members. In mock mode
118
118
  those still hit the real transport.
119
119
  - **Analytics is disabled** whenever `?__mock=1` is present, fixture or not — a
120
120
  screenshot/design-time load emits no events.
@@ -142,7 +142,7 @@ helper works identically in both. `isEmbedded(): boolean` is exported
142
142
  | Who holds credentials | The **host** — the member's session never reaches the app | Nobody — the visitor is anonymous (an optional app password gates access, not identity) |
143
143
  | How ops travel | `postMessage` to the parent frame; the host makes the API call with its session | `fetch` to the public `/v1/apps/{id}/*` endpoints, at the API address the serving host declared in the page |
144
144
  | Which API | The host's, implicitly — it makes the call | `<meta name="lotics-api-base" content="…">` in the served document, read on the first call. **No address is compiled in**: one bundle runs on any Lotics, and a page that declares none makes the SDK refuse rather than address an instance nobody named. The app host injects it, so there is nothing for an app to set |
145
- | Viewer identity | The signed-in member (`useViewer`, comments, agent runs available) | `member_id` is `null`; members-only surfaces reject |
145
+ | Viewer identity | The signed-in member (`useViewer`, agent runs available) | `member_id` is `null`; members-only surfaces reject |
146
146
 
147
147
  ### Embedded: the postMessage bridge
148
148
 
@@ -218,13 +218,10 @@ surfaces:
218
218
  | Op | Embedded (product) | Standalone |
219
219
  |---|---|---|
220
220
  | `query`, `field_options`, `workflow`, `members`, `context`, `upload`, `urlState.get/set`, `openExternal` | yes | yes |
221
- | `comments.*` | yes | rejects — `"Comments are available only in embedded apps — a signed-in member is required."` |
222
221
  | `agentRun` (streaming, internal to `useAgentRun`) | yes | yes |
223
222
  | `agentRun.get`, `agentRun.cancel` | yes | yes |
224
223
  | `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | rejects — a standalone caller is anonymous, and a history is its member's (`"App agent runs require an authenticated member …"`) |
225
224
  | `askAi` | yes | rejects — `"askAi is only available when the app runs inside Lotics"` |
226
- | `openApp` | yes — routes to the sibling in the same tab | rejects — `"openApp needs the Lotics host …"` |
227
- | `peekApp` | yes, when the context states `peek_app_enabled` — opens the sibling in a sheet over this app | rejects — `"peekApp needs the Lotics host …"` |
228
225
  | `recording.start`, `recording.stop` | yes, when the context states `recording_enabled` | rejects — `"Recording needs the Lotics host and a signed-in member …"` |
229
226
 
230
227
  **Limitation:** Don't build a session-log UI on `useAgentRuns`; keep the log in app
@@ -259,21 +256,18 @@ semantics are documented:
259
256
  |---|---|---|---|
260
257
  | `query` | `{ alias, params?, limit?, offset?, … }` | `{ rows }` | [queries](./queries.md), [data fetching](./data_fetching.md) |
261
258
  | `export` | `{ alias, params?, filter?, sort?, report, file }` (`exportQuery`) or `{ export: { key, … } }` (`exportApp`) | `{ file }` | [data fetching](./data_fetching.md) |
262
- | `spec` | `{}` | `HostSpec`: the app's compiled `app.json` itself (format 30: `lists`, `pages` by entity, `entities`, flat `acts` and `checks`; a dashboard's readings), never the route's wrapper | this doc |
259
+ | `spec` | `{}` | `HostSpec`: the app's compiled `app.json` itself (format 31: `views`, the root Lists and Sheets in order; `pages` by entity, each with the Views its Lists draw; `entities`; flat `acts` and `checks`), never the route's wrapper | this doc |
263
260
  | `field_options` | `{ alias }` | `{ fields }` | [members & options](./members_and_options.md) |
264
261
  | `workflow` | `{ alias, inputs }` | `WorkflowResult` | [mutations](./mutations.md) |
265
262
  | `upload` | `{ file: File }` (the `File` crosses the bridge by structured clone) | uploaded-file object | [files](./files.md) |
266
263
  | `members` | `{ group? }` | `{ members }` | [members & options](./members_and_options.md) |
267
264
  | `context` | `{}` | app identity (below) | this doc |
268
265
  | `openExternal` | `{ url }` | `void` | this doc |
269
- | `openApp` | `{ app_id, route }` | `void` | this doc |
270
- | `peekApp` | `{ app_id, route }` | `void` | this doc |
271
266
  | `askAi` | prompt/files/records seed | `void` | [ai](./ai.md) |
272
267
  | `agentRuns` | `{ session_id, limit?, offset? }` | `{ runs }` | [ai](./ai.md) |
273
268
  | `agentRun.get` | `{ run_id }` | `{ run }` | [ai](./ai.md) |
274
269
  | `agentRun.cancel` | `{ run_id }` | `{ ok: true }` | [ai](./ai.md) |
275
270
  | `urlState.get` / `urlState.set` | — / `{ params }` | `UrlParams` / `void` | [navigation & state](./navigation_and_state.md) |
276
- | `comments.list/create/update/delete/counts` | comment ops | comment payloads | [members & options](./members_and_options.md) |
277
271
  | `recording.start` / `recording.stop` | `{ alias, inputs }` / `{ alias }` | `{ started }` / `void` | [mutations](./mutations.md#recording-into-a-workflow-userecording) |
278
272
  | `outsideLink.list/create/revoke` | `{ record_id }` / `{ record_id, outside }` / `{ record_id, link_id }` | links / link or `null` / `{ id }` or `null` | [members & options](./members_and_options.md#outside-links-useoutsidelinks) |
279
273
 
@@ -285,7 +279,7 @@ The **streaming** agent-run op is *not* reachable through `rpc()` — its
285
279
  response is a chunk stream, not a single value; it's internal to `useAgentRun`.
286
280
 
287
281
  `rpc("context", {})` resolves the viewer and the workspace:
288
- `{ member_id, comments_enabled, timezone, default_currency, recording_enabled?, peek_app_enabled?, recordings? }`. `member_id` is the
282
+ `{ member_id, timezone, default_currency, recording_enabled?, recordings? }`. `member_id` is the
289
283
  signed-in member when embedded, `null` standalone; `timezone` is the workspace's
290
284
  IANA zone and `default_currency` its ISO 4217 code, from the host when embedded
291
285
  and from `/v1/apps/by-subdomain/{subdomain}` standalone. Read them through
@@ -317,9 +311,9 @@ embedded app iframe is sandboxed **without `allow-popups`**, so a direct
317
311
  host frame (embedded) or the app's own top-level
318
312
  page (standalone). Opens in a new tab with `noopener,noreferrer`.
319
313
 
320
- - **Scheme validation at the point of open**: only `http:` and `https:` are
321
- allowed; any other scheme (`javascript:`, `data:`, …) rejects the promise with
322
- `openExternal: unsupported URL scheme "<protocol>"`, and a non-string `url`
314
+ - **Scheme validation at the point of open**: only `http:`, `https:`, `tel:`,
315
+ `mailto:` (the last two open in place); any other scheme (`javascript:`, `data:`)
316
+ rejects with `openExternal: unsupported URL scheme "<scheme>"`, a non-string `url`
323
317
  rejects with `openExternal requires a url string`. The opener re-validates on
324
318
  its own side — a URL handed across the bridge is never trusted.
325
319
  - Typical use: opening a workflow-generated file's `url` from
@@ -327,55 +321,6 @@ page (standalone). Opens in a new tab with `noopener,noreferrer`.
327
321
  - **Not a preview mechanism.** To *view* a file inline, render it in the app
328
322
  (see [files](./files.md)); `openExternal` is "leave the app".
329
323
 
330
- ## `openApp()` — the cross-app hop
331
-
332
- ```tsx
333
- import { openApp, isEmbedded } from "@lotics/app-sdk";
334
- await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
335
- ```
336
-
337
- `openApp(appId: string, route?: string): Promise<void>` (`dist/open_app.d.ts`).
338
- A workspace built as several apps around one spine shows another app's record
339
- read-only with a way THROUGH to the app that owns it; this is the way through.
340
- The host owns app routing, so it lands the viewer on `route` inside `appId` in
341
- the **same tab**, chrome kept — `_loc` carries the screen exactly as
342
- `AppRouter` mirrors it, so the target boots at that record rather than at its
343
- root. `route` is the target app's own in-app path, exactly as its router
344
- declares it (`/` for its register); it is the target's to define.
345
-
346
- - **Checked at the bridge, host-side**: the id by shape (`app_…`), the route as
347
- an in-app path — starts with `/`, never `//` or a scheme. An app never
348
- assembles the host's URL itself.
349
- - **By transport**: embedded routes in place; standalone rejects (`openApp needs the Lotics host …`) — gate the
350
- control on `isEmbedded()`.
351
- - A sibling's id is this workspace's: a copy of the suite has different ones,
352
- and the portability gate refuses a concrete `app_` in `src/` and in every
353
- `.md`. The id reaches the app as data — a field the model binds, a query's
354
- row — never as a literal.
355
- - Put it on a control the person presses. A hop on mount pushes the host's
356
- history without a gesture, and two apps that each do it leave Back nowhere
357
- to go. A hop to the app's own id is refused — navigate inside an app with
358
- its own router.
359
-
360
- ## `peekApp()` — a sibling's record, over this app
361
-
362
- ```tsx
363
- import { peekApp, useAppContext } from "@lotics/app-sdk";
364
- const { peekAppEnabled } = useAppContext();
365
- if (peekAppEnabled) await peekApp(PARTIES_APP_ID, `/${party.id}`);
366
- ```
367
-
368
- `peekApp(appId: string, route?: string): Promise<void>` (`dist/open_app.d.ts`). The host opens `route` of `appId` in
369
- a side sheet over this app — a panel from the right on a desk, a full-height sheet on a phone — and this app stays as
370
- it was beneath. The sibling draws its own record, in its own frame, under its own workflows and permissions; this app
371
- never draws a second copy of it. The sheet's head carries the sibling's name, "Open in <name>" (what `openApp` does,
372
- closing the sheet) and a close; Escape closes it too. One sheet at a time: a `peekApp` from inside the sheet replaces it.
373
-
374
- - **Checked at the bridge**, as `openApp`'s target: the id by shape, the route as an in-app path. A hop to the app's own
375
- id, and to an app the host's `openableApps` does not list, is refused.
376
- - **By transport**: embedded in the product host opens the sheet; standalone rejects (`peekApp needs the Lotics host …`).
377
- A host that does not serve the op states no `peekAppEnabled` in `context` — gate the control on it, else offer `openApp`.
378
-
379
324
  ## `downloadFile()` — save browser-built bytes
380
325
 
381
326
  ```tsx
@@ -465,7 +410,7 @@ response under a `Content-Security-Policy` whose `connect-src` reaches only the
465
410
  Lotics API, so a third-party collector's requests are blocked and the only
466
411
  visible result is console noise.
467
412
 
468
- Most of what an app does is already recorded platform-side — a comment, an
413
+ Most of what an app does is already recorded platform-side — an
469
414
  upload, a workflow or agent run each leave a row, and a run's outcome, including
470
415
  a user's Stop (`cancel_requested_at`), lands in `app_agent_runs`. App opens are
471
416
  counted at the serving edge. If a funnel needs something those cannot answer,
package/docs/security.md CHANGED
@@ -9,7 +9,6 @@ Every data operation an app performs — queries, workflows, agent runs — exec
9
9
  | Named queries (`useQuery`, the query RPC) | App owner | Yes — bound server-side into `is_current_member` / `current_member` filter predicates |
10
10
  | Workflows (`useWorkflow`) | App owner | Yes — `runtime.triggered_by_member_id` in the workflow body (`null` for anonymous) |
11
11
  | Agent runs (`useAgentRun`) | App owner | Yes — requires an authenticated member; runs are private to that member |
12
- | Comments (`useComments`) | App authority for **access**; the **author** is always the real member | Always — members-only, anonymous callers are rejected |
13
12
 
14
13
  Consequences of owner authority:
15
14
 
@@ -18,8 +17,6 @@ Consequences of owner authority:
18
17
  - **The viewer never widens or narrows a data op by existing.** A member with zero table access sees whatever the app's queries project, and a member with admin table access sees no more than that. `app:use` (or the public grant, below) is the only gate on invoking the app's declared surface.
19
18
  - **The boundary is what a CALLER can do**, not what the owner can: any caller, including an anonymous public visitor, can only invoke the declared aliases with typed, per-input-constrained values. A caller gaining data or capability the aliases don't declare is a vulnerability; the sections below prevent the ones the platform cannot detect for you.
20
19
 
21
- Comments are the one deliberate exception: access is app-authority (any member with `app:use` + the app's declared `comments` capability may read/post on any record in the app's workspace, regardless of their own table access — a tenant floor rejects record ids outside the app's workspace), but the **author is always the real authenticated member** — never the app owner, never a "View as" subject — and edit/delete are author-only. Anonymous visitors cannot comment: the comment endpoints require an authenticated member.
22
-
23
20
  ## The caller boundary
24
21
 
25
22
  Callers never submit query ASTs or workflow definitions — the server holds the canonical, bind-validated template and the caller supplies only an **alias plus typed values**. Three per-input constraints are enforced server-side on **workflow and agent-run inputs** at invocation time, so a hand-crafted request can't redirect a run executing under owner authority:
@@ -100,7 +97,6 @@ A publicly-shared app (its own origin, or its public link) is reachable by **any
100
97
  | File upload (workflow `file` inputs) | Yes — bounded to the app's workspace |
101
98
  | Query/workflow file outputs | Yes — file cells and workflow-produced files return direct presigned URLs (24-hour TTL) that anonymous viewers can fetch; see [files](./files.md) |
102
99
  | Agent runs (`useAgentRun`) | **No** — rejected: agent runs require an authenticated member, and each member's run history is private to them (a guessed session id cannot read another member's thread) |
103
- | Comments (`useComments`) | **No** — members-only |
104
100
  | Member roster (`useMembers`) | **No** — same-org members only (below) |
105
101
 
106
102
  **Warning — `is_current_member` on a public app resolves to the app owner.** An anonymous request has no viewer to bind, so both `is_current_member` and `current_member in_any_group` fall back to the owner's identity and the owner's groups. A "my items" query shows every anonymous visitor the *owner's* items, and a manager-only read template gated on a group the owner belongs to **passes for every anonymous visitor**. Never rely on viewer-bound predicates in a public app.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.125.9",
3
+ "version": "0.126.0",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through — typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,24 +0,0 @@
1
- /**
2
- * Land on `route` — the target app's own path — inside sibling app `appId`,
3
- * same tab.
4
- *
5
- * ```tsx
6
- * import { openApp } from "@lotics/app-sdk";
7
- * await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
8
- * ```
9
- *
10
- * Rejects standalone; gate on `isEmbedded()`.
11
- */
12
- export declare function openApp(appId: string, route?: string): Promise<void>;
13
- /**
14
- * Open sibling app `appId`'s own `route` in a sheet over this app, which stays as it was beneath. The sibling draws its
15
- * record itself, in its own frame, under its own workflows and permissions.
16
- *
17
- * ```tsx
18
- * import { peekApp } from "@lotics/app-sdk";
19
- * await peekApp(PARTIES_APP_ID, `/${party.id}`);
20
- * ```
21
- *
22
- * Rejects standalone, and on a host that does not state `useAppContext().peekAppEnabled`.
23
- */
24
- export declare function peekApp(appId: string, route?: string): Promise<void>;