@lotics/app-sdk 0.90.1 → 0.90.3
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/AGENTS.md +5 -1
- package/dist/src/hooks.d.ts +1 -1
- package/dist/src/hooks.js +1 -1
- package/dist/src/router.js +1 -1
- package/dist/src/rpc.d.ts +7 -6
- package/dist/src/rpc.js +33 -9
- package/docs/ai.md +1 -1
- package/docs/data_fetching.md +1 -1
- package/docs/mutations.md +1 -1
- package/docs/navigation_and_state.md +2 -2
- package/docs/queries.md +2 -2
- package/docs/runtime.md +5 -4
- package/docs/security.md +1 -1
- package/docs/workflows.md +4 -5
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -24,7 +24,7 @@ signature; open the file.**
|
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
25
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics; a chat mutation refetches your queries through the realtime channel, not a separate poke). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
27
|
-
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`openApp`/`downloadFile`, geofencing, and the publish chain for SDK contributors. |
|
|
27
|
+
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, **the API address a standalone bundle reads out of its own page** (`<meta name="lotics-api-base">`, declared by whatever serves the app — no address is compiled into the SDK, so one bundle runs on any instance and a page without it refuses rather than guessing), `rpc()`, the design-time mock harness (`fixture` + `?__mock=1` — queries AND workflows, so an AI screen's in-flight/done/error states are reviewable without running or paying for anything), `openExternal`/`openApp`/`downloadFile`, geofencing, and the publish chain for SDK contributors. |
|
|
28
28
|
|
|
29
29
|
## Non-negotiables (each detailed in its doc)
|
|
30
30
|
|
|
@@ -47,6 +47,10 @@ signature; open the file.**
|
|
|
47
47
|
- **Narrow `__source_record_id` / `__source_table_id` before use** — a grouped query emits neither,
|
|
48
48
|
so an unchecked read hands a workflow (or an AI record ref) `undefined`.
|
|
49
49
|
→ [data_fetching](./docs/data_fetching.md)
|
|
50
|
+
- **A standalone app takes its API address from the page it was served in**, never from a
|
|
51
|
+
constant — the serving host declares `<meta name="lotics-api-base">`, and a bundle that finds
|
|
52
|
+
none refuses instead of addressing someone else's instance. Nothing for an app to configure;
|
|
53
|
+
build against 0.90.2 or later. → [runtime](./docs/runtime.md)
|
|
50
54
|
- **Errors fail loud** — no swallowed catches, no silent fallbacks.
|
|
51
55
|
|
|
52
56
|
## Keeping this reference current
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -586,7 +586,7 @@ export declare function useAttachments(): AttachmentsState;
|
|
|
586
586
|
* });
|
|
587
587
|
* ```
|
|
588
588
|
*
|
|
589
|
-
* No-ops with no embedding host (standalone
|
|
589
|
+
* No-ops with no embedding host (standalone on the app's own origin — there is no
|
|
590
590
|
* chat surface to inform) and in mock mode. Since 0.52.
|
|
591
591
|
*/
|
|
592
592
|
export declare function useAiContext(slot: string, context: AiContextValue | null): void;
|
package/dist/src/hooks.js
CHANGED
|
@@ -498,7 +498,7 @@ function serializeAiContext(context) {
|
|
|
498
498
|
* });
|
|
499
499
|
* ```
|
|
500
500
|
*
|
|
501
|
-
* No-ops with no embedding host (standalone
|
|
501
|
+
* No-ops with no embedding host (standalone on the app's own origin — there is no
|
|
502
502
|
* chat surface to inform) and in mock mode. Since 0.52.
|
|
503
503
|
*/
|
|
504
504
|
export function useAiContext(slot, context) {
|
package/dist/src/router.js
CHANGED
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* addressable URLs. The app owns its OWN url (a plain browser history) in both
|
|
6
6
|
* modes; the host url only ever *mirrors* the screen, it never drives the router:
|
|
7
7
|
*
|
|
8
|
-
|
|
8
|
+
* - **Standalone** (the app's own origin): the page's own browser history — real
|
|
9
9
|
* path URLs, native browser back/forward, deep-link/refresh via the app host's
|
|
10
10
|
* SPA fallback.
|
|
11
11
|
* - **Embedded** (inside the Lotics host): the app drives the IFRAME's own url
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -11,10 +11,11 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
11
11
|
* ops over postMessage and the host makes the API call. Used by internal
|
|
12
12
|
* apps — the member's credentials must never reach the app.
|
|
13
13
|
*
|
|
14
|
-
* - **Standalone** — the app is served on its own
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - **Standalone** — the app is served on its own origin, with no host. It
|
|
15
|
+
* calls the public `/v1/apps/{id}/*` endpoints directly, at the API address
|
|
16
|
+
* the serving host injected into the page (`API_BASE_META`); those are
|
|
17
|
+
* anonymous-accessible for a publicly-shared app. The app holds no
|
|
18
|
+
* credentials, so there is nothing to protect.
|
|
18
19
|
*
|
|
19
20
|
* Wire protocol (bridged — must match `app_iframe_host.tsx`):
|
|
20
21
|
* app → host: { id, op, payload }
|
|
@@ -84,7 +85,7 @@ export interface AppContext {
|
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
86
87
|
* Whether the app is running embedded in a Lotics host (vs. standalone at its
|
|
87
|
-
* own
|
|
88
|
+
* own origin). `rpc()`, `useUrlState`, and `AppRouter` use this to
|
|
88
89
|
* pick the transport / behaviour; an app rarely needs it directly.
|
|
89
90
|
*/
|
|
90
91
|
export declare function isEmbedded(): boolean;
|
|
@@ -106,7 +107,7 @@ export declare function subscribeUrlParams(cb: (params: UrlParams) => void): ()
|
|
|
106
107
|
/**
|
|
107
108
|
* Push a fire-and-forget notification to the embedding host — no id, no reply.
|
|
108
109
|
* Only the embedded host can receive it (it owns the chat surface), so this
|
|
109
|
-
* no-ops standalone (
|
|
110
|
+
* no-ops standalone (the app's own origin has no host to inform) and never
|
|
110
111
|
* throws. Distinct from `rpc()`: this is one-way, app → host.
|
|
111
112
|
*/
|
|
112
113
|
export declare function postHostNotification(message: HostNotification): void;
|
package/dist/src/rpc.js
CHANGED
|
@@ -19,7 +19,7 @@ function getHostOrigin() {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Whether the app is running embedded in a Lotics host (vs. standalone at its
|
|
22
|
-
* own
|
|
22
|
+
* own origin). `rpc()`, `useUrlState`, and `AppRouter` use this to
|
|
23
23
|
* pick the transport / behaviour; an app rarely needs it directly.
|
|
24
24
|
*/
|
|
25
25
|
export function isEmbedded() {
|
|
@@ -81,7 +81,7 @@ export function subscribeUrlParams(cb) {
|
|
|
81
81
|
/**
|
|
82
82
|
* Push a fire-and-forget notification to the embedding host — no id, no reply.
|
|
83
83
|
* Only the embedded host can receive it (it owns the chat surface), so this
|
|
84
|
-
* no-ops standalone (
|
|
84
|
+
* no-ops standalone (the app's own origin has no host to inform) and never
|
|
85
85
|
* throws. Distinct from `rpc()`: this is one-way, app → host.
|
|
86
86
|
*/
|
|
87
87
|
export function postHostNotification(message) {
|
|
@@ -307,7 +307,7 @@ export function rpcAgentRunContinue(payload, onText) {
|
|
|
307
307
|
const continueToken = runTokens.get(payload.run_id);
|
|
308
308
|
if (continueToken)
|
|
309
309
|
headers[APP_AGENT_RUN_TOKEN_HEADER] = continueToken;
|
|
310
|
-
const res = await fetch(`${
|
|
310
|
+
const res = await fetch(`${apiBase()}/v1/apps/${app_id}/agent-runs/${encodeURIComponent(payload.run_id)}/continue`, {
|
|
311
311
|
method: "POST",
|
|
312
312
|
headers,
|
|
313
313
|
body: JSON.stringify({ tool_call_id: payload.tool_call_id, output: payload.output }),
|
|
@@ -344,7 +344,7 @@ function agentRunStandalone(payload, onText, onRunId) {
|
|
|
344
344
|
const headers = { "content-type": "application/json" };
|
|
345
345
|
if (sessionToken)
|
|
346
346
|
headers[APP_PUBLIC_SESSION_HEADER] = sessionToken;
|
|
347
|
-
const res = await fetch(`${
|
|
347
|
+
const res = await fetch(`${apiBase()}/v1/apps/${app_id}/agents/${encodeURIComponent(payload.alias)}/runs`, {
|
|
348
348
|
method: "POST",
|
|
349
349
|
headers,
|
|
350
350
|
body: JSON.stringify({ session_id: payload.session_id, input: payload.input }),
|
|
@@ -382,9 +382,33 @@ function agentRunStandalone(payload, onText, onRunId) {
|
|
|
382
382
|
return { done, abort: () => controller.abort() };
|
|
383
383
|
}
|
|
384
384
|
// ── Standalone transport ────────────────────────────────────────────────────
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Meta the host serving a standalone bundle injects into `index.html`, naming
|
|
387
|
+
* the API the app calls.
|
|
388
|
+
*
|
|
389
|
+
* It is the serving host's to state, not the SDK's to assume: the same bundle
|
|
390
|
+
* is served by whoever runs the instance, and an app that carried a compiled-in
|
|
391
|
+
* API address would call somebody else's server. A bridged app never reads it —
|
|
392
|
+
* there the host's own origin arrives on `?lotics_host=`.
|
|
393
|
+
*/
|
|
394
|
+
const API_BASE_META = "lotics-api-base";
|
|
395
|
+
let apiBaseCache = null;
|
|
396
|
+
function apiBase() {
|
|
397
|
+
if (apiBaseCache !== null)
|
|
398
|
+
return apiBaseCache;
|
|
399
|
+
const content = document
|
|
400
|
+
.querySelector(`meta[name="${API_BASE_META}"]`)
|
|
401
|
+
?.getAttribute("content")
|
|
402
|
+
?.trim();
|
|
403
|
+
if (!content) {
|
|
404
|
+
// No default: guessing would send this app's data to whatever address the
|
|
405
|
+
// guess named. Say what is missing and who puts it there.
|
|
406
|
+
throw new Error(`This app has no API address. A standalone app reads it from <meta name="${API_BASE_META}"> ` +
|
|
407
|
+
"in the page the app host serves, and this page carries none.");
|
|
408
|
+
}
|
|
409
|
+
apiBaseCache = content.replace(/\/+$/, "");
|
|
410
|
+
return apiBaseCache;
|
|
411
|
+
}
|
|
388
412
|
const PASSWORD_REQUIRED_CODE = "PASSWORD_REQUIRED";
|
|
389
413
|
/**
|
|
390
414
|
* Boot-time resolution result. Promise is shared so concurrent first calls
|
|
@@ -592,7 +616,7 @@ async function apiCall(method, path, body, opts) {
|
|
|
592
616
|
}, API_TIMEOUT_MS);
|
|
593
617
|
let res;
|
|
594
618
|
try {
|
|
595
|
-
res = await fetch(`${
|
|
619
|
+
res = await fetch(`${apiBase()}${path}`, {
|
|
596
620
|
method,
|
|
597
621
|
headers,
|
|
598
622
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -708,7 +732,7 @@ async function standaloneMembers(p) {
|
|
|
708
732
|
}
|
|
709
733
|
/**
|
|
710
734
|
* Open an external URL in a new tab, scheme-validated. In standalone mode the
|
|
711
|
-
* app is a normal top-level page
|
|
735
|
+
* app is a normal top-level page on its own origin, so `window.open` is not
|
|
712
736
|
* sandbox-blocked — open directly. (Bridged apps route this op to the host,
|
|
713
737
|
* which opens it in the un-sandboxed parent frame; see `app_iframe_host`.)
|
|
714
738
|
* The scheme is re-validated wherever the open actually happens — never trust a
|
package/docs/ai.md
CHANGED
|
@@ -450,7 +450,7 @@ caps above).
|
|
|
450
450
|
|
|
451
451
|
When the ambient chat agent's turn ends and it mutated records, the host pushes every mounted query hook to re-read, so the screen the member is looking at reflects the agent's change without a manual refresh. That companion behavior is automatic — you write no code for it — and is documented with the query caching contract in [data_fetching](./data_fetching.md#caching-loading-states-and-errors).
|
|
452
452
|
|
|
453
|
-
**No-ops** with no embedding host (standalone
|
|
453
|
+
**No-ops** with no embedding host (standalone on the app's own origin — there's no chat surface to inform) and in mock mode.
|
|
454
454
|
|
|
455
455
|
---
|
|
456
456
|
|
package/docs/data_fetching.md
CHANGED
|
@@ -349,7 +349,7 @@ you supply the total yourself.
|
|
|
349
349
|
|
|
350
350
|
## Standalone (public) transport
|
|
351
351
|
|
|
352
|
-
An app served standalone
|
|
352
|
+
An app served standalone on its own origin (a public share with no Lotics host — see
|
|
353
353
|
[runtime](./runtime.md)) reaches the query endpoint through a thinner transport that forwards only
|
|
354
354
|
`alias`, `params`, `limit`, and `offset`. It **silently drops** `sort`, `filter`, `count`, and the
|
|
355
355
|
keyset `cursor`. Design a public app around this:
|
package/docs/mutations.md
CHANGED
|
@@ -70,7 +70,7 @@ Every call resolves to a `WorkflowResult<TData>` (type exported from the package
|
|
|
70
70
|
### The failure model: check `status`, never just try/catch
|
|
71
71
|
|
|
72
72
|
**Every failure resolves — the promise (almost) never rejects.** All three transports
|
|
73
|
-
(embedded product host, standalone
|
|
73
|
+
(embedded product host, standalone on the app's own origin, and the `lotics app dev` harness)
|
|
74
74
|
convert every failure into a resolved `{ status: "error", message }`:
|
|
75
75
|
|
|
76
76
|
| Failure | What resolves |
|
|
@@ -26,7 +26,7 @@ into a query's *server* params (see [named-query params](./queries.md) and the
|
|
|
26
26
|
|
|
27
27
|
The embedded-vs-standalone distinction below is the runtime's: embedded means
|
|
28
28
|
the app runs in an iframe inside the Lotics host, standalone means it runs on
|
|
29
|
-
its own
|
|
29
|
+
its own app-host page (see [runtime](./runtime.md)). Both mechanisms
|
|
30
30
|
expose the same API in both modes with no per-mode code (the few behavioral
|
|
31
31
|
differences — embedded first-paint hydration, cross-screen persistence — are
|
|
32
32
|
flagged below); mode is detected automatically (from the `lotics_host` param
|
|
@@ -74,7 +74,7 @@ routes, and splats all work. Inside the tree, use react-router normally:
|
|
|
74
74
|
The app owns its **own** URL in both modes — the host URL only ever *mirrors*
|
|
75
75
|
the screen, it never drives the router:
|
|
76
76
|
|
|
77
|
-
- **Standalone** (
|
|
77
|
+
- **Standalone** (the app's own origin): a normal browser router. Screens are
|
|
78
78
|
real path URLs, browser Back/Forward are native, and deep-links/refresh work
|
|
79
79
|
because the app host serves the entry HTML for any path the build didn't
|
|
80
80
|
emit (SPA fallback).
|
package/docs/queries.md
CHANGED
|
@@ -464,7 +464,7 @@ derived surfaces share one implementation.
|
|
|
464
464
|
| number | `number` | `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, `is_not_empty` | same set | Full numeric aggregate set (§8). |
|
|
465
465
|
| date / datetime | `date` / `datetime` (datetime when the field's format includes time) | `on`, `before`, `after`, `on_or_before`, `on_or_after`, `between`, `time_of_day`, `is_empty`, `is_not_empty` — values are `DateTimePoint`s (below) | same set | Day-level filters on datetime values expand to the full-day window. Sortable; bucketable in `group.by` (§8); `earliest`/`latest`/`min`/`max`/`date_range` aggregate. |
|
|
466
466
|
| boolean | `boolean` | `equals` (value `true`/`false`; `false` matches NULL/missing) | same | `checked`/`unchecked`/`percent_*` aggregate (`unchecked` counts false **or** empty). |
|
|
467
|
-
| select (single & multi) | `select` | `has_any_of`, `has_none_of`, `has_all_of`, `is_empty`, `is_not_empty` — values are **option keys** (`opt_*`), never labels |
|
|
467
|
+
| select (single & multi) | `select` | `has_any_of`, `has_none_of`, `has_all_of`, `is_empty`, `is_not_empty` — values are **option keys** (`opt_*`), never labels | the **runtime** `filter` (§9) also takes option **names**, resolving each to its key — an unknown one is refused rather than matching nothing, and the value stays an array either way; a select column the query DERIVES instead of projecting from a field carries no option set, so addressing one there is refused | Cells are arrays even for single-selects. Sorting a select column in a query orders by raw JSON, **not** configured option order. `unnest` fans option keys. |
|
|
468
468
|
| select_member | `select_member` | select ops + `is_current_member`, `is_not_current_member` (field-scoped, no value) | same — `is_current_member` **works at the runtime layer** too | `unnest` fans member ids. Anonymous public request: current-member binds the app owner (§1). |
|
|
469
469
|
| select_record_link | `select_record_link` | membership by linked-record **id**: `has_any_of`, `has_none_of`, `has_all_of`; text over the cached **display**: `contains`, `not_contains`, `starts_with`, `ends_with`; `is_empty`, `is_not_empty` | same — id-membership works at the runtime layer (converged `[{id}]` containment) | Sorting orders by raw JSON, not display text — project the display (link extraction / `display_output`) and sort that. `unnest` fans link ids (+ display). |
|
|
470
470
|
| files | `files` | `has_filename`, `has_mime_type` (substring), `has_file_count` (exact count), `is_empty`, `is_not_empty` | same | Presign-enriched at delivery (§1); `unnest` fans file ids. Only presence-counting aggregates. |
|
|
@@ -836,7 +836,7 @@ Build `filter` from UI column-filters with `columnFilterToConditions` (`@lotics/
|
|
|
836
836
|
`useFieldOptions` for a select filter's option set.
|
|
837
837
|
|
|
838
838
|
**Warning (transport gaps):** the embedded product host and the `lotics app dev` forwarder pass
|
|
839
|
-
`sort`/`filter`/`count` through. The **standalone public transport** (
|
|
839
|
+
`sort`/`filter`/`count` through. The **standalone public transport** (the app's own origin)
|
|
840
840
|
forwards only `alias`/`params`/`limit`/`offset` — runtime `sort`/`filter` are silently ignored
|
|
841
841
|
there and a `count` never resolves ([data_fetching](./data_fetching.md) → Standalone transport).
|
|
842
842
|
A standalone app must bake ordering/scoping into the template (or params) rather than rely on
|
package/docs/runtime.md
CHANGED
|
@@ -108,9 +108,10 @@ helper works identically in both. `isEmbedded(): boolean` is exported
|
|
|
108
108
|
|
|
109
109
|
| | **Embedded (bridged)** | **Standalone (direct)** |
|
|
110
110
|
|---|---|---|
|
|
111
|
-
| Where | Iframe inside the Lotics product, or the `lotics app dev` wrapper page | The app's own top-level page
|
|
111
|
+
| Where | Iframe inside the Lotics product, or the `lotics app dev` wrapper page | The app's own top-level page, on its own origin |
|
|
112
112
|
| 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) |
|
|
113
|
-
| How ops travel | `postMessage` to the parent frame; the host makes the API call with its session | `fetch` to the public
|
|
113
|
+
| 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 |
|
|
114
|
+
| 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. Every serving host injects it (the app host and `lotics app dev`), so there is nothing for an app to set |
|
|
114
115
|
| Viewer identity | The signed-in member (`useViewer`, comments, agent runs available) | `member_id` is `null`; members-only surfaces reject |
|
|
115
116
|
|
|
116
117
|
### Embedded: the postMessage bridge
|
|
@@ -137,7 +138,7 @@ haven't verified exists in the host (see
|
|
|
137
138
|
ops to the API; the bare Vite origin has no host param, so the SDK falls into
|
|
138
139
|
standalone mode, tries to resolve an app from the hostname, and every data call
|
|
139
140
|
fails. Dev apps are always bridged; standalone mode exists only on the deployed
|
|
140
|
-
|
|
141
|
+
app host.
|
|
141
142
|
|
|
142
143
|
### Standalone: direct public API
|
|
143
144
|
|
|
@@ -145,7 +146,7 @@ On first data call the SDK resolves the app's identity from its own subdomain
|
|
|
145
146
|
(one shared `GET /v1/apps/by-subdomain/{slug}` fetch — concurrent first calls
|
|
146
147
|
coalesce; a transient failure isn't cached, the next call retries). If the app
|
|
147
148
|
is password-protected, the visitor never reaches the app at all until they
|
|
148
|
-
clear the gate:
|
|
149
|
+
clear the gate: the app's own origin is served by the app host, which
|
|
149
150
|
withholds every byte of the bundle and serves a password page instead. On
|
|
150
151
|
success it sets `lotics_app_session` on the app's origin; the SDK reads that
|
|
151
152
|
cookie and forwards it as `X-Lotics-App-Session` on data calls. The SDK owns no
|
package/docs/security.md
CHANGED
|
@@ -89,7 +89,7 @@ returns `true` when the **triggering member** belongs to any of the listed group
|
|
|
89
89
|
|
|
90
90
|
## Public apps: anonymous reach and its bounds
|
|
91
91
|
|
|
92
|
-
A publicly-shared app (
|
|
92
|
+
A publicly-shared app (its own origin, or its public link) is reachable by **anyone** — the public share grants `app:use` to anonymous visitors and to authenticated members of any other org alike. What anonymous visitors can and cannot do:
|
|
93
93
|
|
|
94
94
|
| Surface | Anonymous access |
|
|
95
95
|
|---|---|
|
package/docs/workflows.md
CHANGED
|
@@ -16,9 +16,8 @@ engine, no `eval`, no sandbox. Four consequences shape everything below:
|
|
|
16
16
|
|
|
17
17
|
- **Only the listed forms exist.** Anything outside them fails the save with a precise error
|
|
18
18
|
naming the rule and, where one exists, the substitute. A save never partially succeeds.
|
|
19
|
-
- **A successful save may still return `warnings[]`** — advisory lint that does not block
|
|
20
|
-
|
|
21
|
-
`validate` guard). Read them; they are the failures that only show up in production.
|
|
19
|
+
- **A successful save may still return `warnings[]`** — advisory lint that does not block.
|
|
20
|
+
Read them; they are the failures that only show up in production.
|
|
22
21
|
- **One canonical form per concept.** Several JS spellings are accepted and *lowered* to one
|
|
23
22
|
stored form. The stored tree renders back to source on `lotics app pull`, so a pulled body
|
|
24
23
|
shows the canonical spelling, not the sugar you typed (see *Round-tripping* below).
|
|
@@ -803,8 +802,8 @@ What only the **server** can decide, so `check` stays green and `set` may still
|
|
|
803
802
|
- **`switch` case validation** against a single-select's options, and the multi-select rejection;
|
|
804
803
|
- the **literal `formatDate` format probe**;
|
|
805
804
|
- **wait inside a loop**, and the `before_*` restrictions on waits and `agent` steps;
|
|
806
|
-
- the **lint** —
|
|
807
|
-
|
|
805
|
+
- the **lint** — deep link chains, an input the alias declares that this body never reads, and
|
|
806
|
+
an unguarded `set` in a body that generates a document (all warnings).
|
|
808
807
|
|
|
809
808
|
There is no separate verify endpoint: the loop is `set` → read the returned diagnostics → fix →
|
|
810
809
|
`set`. Diagnostics arrive **batched** — independent errors across the whole body come back in one
|