@plitzi/sdk-elements 0.38.14 → 0.38.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # @plitzi/sdk-elements
2
2
 
3
+ ## 0.38.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 6cdd5cf: ## Security
8
+
9
+ - **A write another site caused is refused, cookie or none** (`@plitzi/sdk-server`, `@plitzi/sdk-shared`): the page
10
+ server guarded nothing it answers past `/auth` — a server action (`/_action`), a realtime publish, a space's `/fn/`
11
+ route, its runtime's endpoints, the deployment's own `data` stages, the adapter-only `loginPath`, `logoutPath` and
12
+ `exchangePath`. A plain `text/plain` POST from any page open in the same browser ran them, with the visitor's session
13
+ cookie when there was one (`SameSite=None` off this machine) and, in a project nobody signs in to, as whoever holds
14
+ the server's own credentials. Every unsafe request a browser says another site caused (`Sec-Fetch-Site: cross-site`,
15
+ or a foreign `Origin`) is now refused `403 reason: 'foreign'`, saying how to allow it: `crossSiteWrites.origins`
16
+ (with `auth`, `identity.platformOrigins` too) and `crossSiteWrites.paths` for a form other sites post to by design.
17
+ `Authorization`, and callers that are not browsers — webhooks, scripts — pass as before. One rule for this and the
18
+ sign-in check (`crossSiteRequest`, `csrf.ts`). A self-hosted runtime's endpoints now sit where the platform forwards a
19
+ space's — after the auth chain and this guard (`data`, was `preAuth`).
20
+
21
+ ## Testing
22
+ - **`page check` and `page shot` anywhere the project is served** (`@plitzi/cli`): `--url <origin>` for a second server
23
+ of it, staging or a deploy, instead of `npm start`'s port; `page check --space <module>` holds the pages to that space
24
+ instead of the project's — an agent's page — authored with `authorIsolated`.
25
+ - **A route with a query is checked as its page** (`@plitzi/cli`): `page check "/?v=2"` matched the query as part of
26
+ the path, found no page, and checked the space's "not found" page in its place.
27
+
28
+ - **What an element that chooses its children draws is checked** (`@plitzi/sdk-authoring`, `@plitzi/cli`): with
29
+ `choosesChildren`, the page checks owed none of its children — and checked none of them either: a dashboard drawing
30
+ three panels reported `checked: 1`. Each child it may draw, and what is inside one under no condition of its own,
31
+ now carries `shownWith` (the child it comes with) on its handle: once that child is on screen it and everything
32
+ named in it are owed like the rest of the page, and `checked` counts them. `onScreenIfChosen(handles, page)` lists
33
+ them.
34
+ - **A page that met a server error is looked at once more** (`@plitzi/cli`): `page check` and `verify` failed a page
35
+ for a `500` from somebody else's service having a bad second (`/fn/<space>/weather`), and passed on the next run with
36
+ nothing changed. A page whose requests got a `5xx` is checked again once; its report is the second look, and says
37
+ what failed the first time (`retriedAfter`). A `4xx` fails the same way twice, and is never retried.
38
+
39
+ ## Plugins
40
+ - **`useNotify`** (`@plitzi/plitzi-sdk`): a plugin shows the page's notifications — `notify('Deleted 79 recordings', {
41
+ appearance: 'danger' })` — the same toasts an `addNotification` step shows, in the space's theme and `notifications`
42
+ look, with no trigger, flow or `window` event of the space's to wire. The step's vocabulary (`NOTIFICATION_PLACEMENTS`,
43
+ `NOTIFICATION_APPEARANCES`, `NOTIFICATION_DEFAULTS`) is one list in `@plitzi/sdk-shared`, read by both.
44
+ - **`appearance: 'danger'` looks like one** (`@plitzi/plitzi-sdk`): it reached the toast library as a type it does not
45
+ know, so a danger notification showed neither the error icon nor the colour `notifications.danger` sets.
46
+ A value a notification does not take — from a flow, or a plugin in plain JavaScript — is set aside by name and said
47
+ in the console, rather than shown as the default in silence (`readNotificationOptions`, `@plitzi/sdk-shared`).
48
+ - **`useServerAction`** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-authoring`, `@plitzi/sdk-interactions`,
49
+ `@plitzi/sdk-server`): a plugin runs one of the space's server actions, awaited —
50
+ `useServerAction<typeof habitDone>('habits-done')({ id })` answers `{ ok, output }` or `{ ok: false, reason, error }`
51
+ — through the `runServerAction` step itself: the same access, refusals, session handling and dev-tools entry.
52
+ `defineAction` keeps the literal types of its `call` way in, so the input is typed by the fields it declares (the
53
+ required ones required); `taskAction` types both ends from the task's `run`, and `FunctionTask<T, R>` takes what
54
+ `run` answers. `CallInputOf` / `CallOutputOf` read them. One operation is one action, never a `case` in one route.
55
+ What a run answers is typed (`ServerActionStatus`): a status the server never uses reads as a run that did not
56
+ complete, never as one that did.
57
+ The key those types ride under (`CALL_TYPES`) is exported, by `@plitzi/sdk-shared` and `@plitzi/sdk-authoring` with
58
+ `TypedActionEntry`, `CallInputOf` and `CallOutputOf`: a package that emits declarations could not write the type of an
59
+ action it exports (`TS4023` on every `export const actions = [defineAction(…)]`).
60
+ - **A plugin is drawn on the server first** (docs): a preference per device belongs in kept state (`keepState`,
61
+ `paintedState`) as a bound prop, never read from `localStorage` while rendering — React discards the whole tree on the
62
+ mismatch, and only in the browser of whoever saved something. `@plitzi/plitzi-ui` is not the page's: a plugin
63
+ importing it bundles its own copy, with its own contexts and its own look. The authoring skill's `plugin-reach.md`
64
+ says what a plugin says and to whom: a notification, another element (an event and the space's flow), a server
65
+ action, shared state, other pages.
66
+
67
+ ## Realtime
68
+ - **A channel that retains** (`@plitzi/sdk-shared`, `@plitzi/sdk-server`, `@plitzi/sdk-elements`): declared
69
+ `retain: true` (with `publish: 'server'`, which authoring requires), the server keeps the latest message of each type
70
+ on a topic — said by `ctx.publish` or a flow's `realtime.publish` — and hands them to every page that joins, marked
71
+ `retained`. A page reads them as `<channel>.retained.<type>`, a plugin as `useChannel(topic).retained`, the newest of
72
+ a type holding whichever order they arrive in; `null` forgets a type. Kept in the store the actions' `kv` uses (beside
73
+ the grants, by compare-and-swap), so every replica hands out the same; a topic keeps at most 64 types and 512 KB, and
74
+ a publish past that is refused before anything is sent. State every screen shares is one publish and one binding —
75
+ no kv key, initial read or copy in `state` of the project's own. The fleet store `realtime.grants` is now
76
+ `realtime.kv`.
77
+ - **The MCP writes every field a channel takes** (`@plitzi/sdk-mcp`): `patchSettings`'s channel had no `grant` — which
78
+ the guide taught — nor `retain` or the limits, so an agent could not declare a private channel at all. All are there
79
+ now, and the guide says what `retain` is for and that `channel_<id>.retained` holds it.
80
+
81
+ ## Authoring
82
+ - **`plitzi explain` knows every `@plitzi` package installed** (`@plitzi/cli`): a name that was not
83
+ `@plitzi/sdk-authoring`'s (`createServer`, `createJsonAdapters`, `useChannel`) was answered as the project's own code,
84
+ to `grep` in `src/`. It is read from the published types of every one installed, through their re-exports and entry
85
+ points (`@plitzi/sdk-server/handlers`), and a name two packages export — `createServer` is sdk-server's and sdk-mcp's
86
+ — is answered with both.
87
+
88
+ - **A space module somebody else wrote, authored where it reaches nothing** (`@plitzi/sdk-authoring`):
89
+ `authorIsolated(file, { timeoutMs })` of `@plitzi/sdk-authoring/node` runs `authorSpace` on it in a process Node's
90
+ permission model holds to reading the module's folder and its packages — no file written, no process started, no
91
+ variable of the caller's environment — kills one that runs too long, and validates what it answers again, so a forged
92
+ answer gains nothing. A refusal comes back as `authorSpace` says it, with the problems as data.
93
+ - **Pages an agent writes, served** (`@plitzi/sdk-server`): `createAuthoringHost({ dir, frameAncestors })` serves each
94
+ space it is handed at an address of its own (`put(space, { id }) → /?__pt=<id>`) as a draft of its one space, kept in
95
+ `dir` across restarts; `drop(id)` forgets one, and anything else answers `404` saying where the pages are.
96
+ - **A space is chosen by host or credential** (`@plitzi/sdk-server`, docs): `getSpaceDeployment` is asked on every
97
+ request, and what a page fetches after it carries nothing of its address but the host — a space chosen by the page's
98
+ query or path lost everything it asked for, and `/_plitzi/space/<hash>.json` answered a bare `404`. The contract and
99
+ the README say so (the README offered "by path prefix"), and that `404` says why.
100
+
101
+ - **`plitzi explain on` says how to read what a trigger carries** (`@plitzi/sdk-authoring`): its doc sat above a
102
+ helper, so `explain` printed only the signature. It now leads with `[named('picked', on('onPick')), …('{{ picked.seat
103
+ }}')]`, and the CLI skill's plugin section says the same.
104
+
105
+ - Updated dependencies [6cdd5cf]
106
+ - @plitzi/sdk-auth@0.38.15
107
+ - @plitzi/sdk-event-bridge@0.38.15
108
+ - @plitzi/sdk-interactions@0.38.15
109
+ - @plitzi/sdk-navigation@0.38.15
110
+ - @plitzi/sdk-schema@0.38.15
111
+ - @plitzi/sdk-shared@0.38.15
112
+
3
113
  ## 0.38.14
4
114
 
5
115
  ### Patch Changes
@@ -62,11 +62,13 @@ var S = (e) => {
62
62
  me: R.me ?? "",
63
63
  members: R.members,
64
64
  messages: N,
65
- last: N.at(-1) ?? null
65
+ last: N.at(-1) ?? null,
66
+ retained: R.retained
66
67
  }), [
67
68
  R.connected,
68
69
  R.me,
69
70
  R.members,
71
+ R.retained,
70
72
  N
71
73
  ]), H = c(() => i(V), [V]);
72
74
  _({
@@ -93,7 +93,7 @@ var n = e()({
93
93
  },
94
94
  definition: {
95
95
  label: "Channel",
96
- description: "A realtime channel: every OTHER page on the same `topic` hears what one publishes, within milliseconds — the page that sent it too with `echo` (its `from` is then the `me` of the channel). The topic must match a channel the space declares in its settings (`channels`), e.g. `board:{{ id }}` under `board:{id}`. A topic of a private channel (`grant: true`) opens only with `grant`, bound to the grant the action that let this visitor in returned (its `realtime.grant` step). ITS DESCENDANTS bind to its source `channel_<id>`: `connected`, `members` (with the state each announced), `messages` (the last `keep`) and `last`. It fires `onMessage`; `onJoin` when somebody who came after this page has announced who they are, and `onLeave` when somebody goes — each with `from`, `user` and the `state` they announced; and answers `publish({ type, data, echo })` and `setPresence({ data })`; `presence` is the state this page announces (a name, a colour). Closed in the builder and anywhere without a server.",
96
+ description: "A realtime channel: every OTHER page on the same `topic` hears what one publishes, within milliseconds — the page that sent it too with `echo` (its `from` is then the `me` of the channel). The topic must match a channel the space declares in its settings (`channels`), e.g. `board:{{ id }}` under `board:{id}`. A topic of a private channel (`grant: true`) opens only with `grant`, bound to the grant the action that let this visitor in returned (its `realtime.grant` step). ITS DESCENDANTS bind to its source `channel_<id>`: `connected`, `members` (with the state each announced), `messages` (the last `keep`), `last` and — on a `retain: true` channel — `retained`, the latest data the server said of each type, there as the page joins (`retained.lists`). It fires `onMessage`; `onJoin` when somebody who came after this page has announced who they are, and `onLeave` when somebody goes — each with `from`, `user` and the `state` they announced; and answers `publish({ type, data, echo })` and `setPresence({ data })`; `presence` is the state this page announces (a name, a colour). Closed in the builder and anywhere without a server.",
97
97
  items: []
98
98
  },
99
99
  market: {
@@ -20,6 +20,11 @@ export type ChannelHandle = {
20
20
  /** This page's name on the channel — the `from` of what it sends. */
21
21
  me: string | undefined;
22
22
  members: RealtimeMember[];
23
+ /**
24
+ * What a `retain: true` channel keeps, by type: the latest data the server said of each — there as the page joins,
25
+ * and changed as it is said again. Empty on any other channel.
26
+ */
27
+ retained: Record<string, unknown>;
23
28
  /** Heard by every other page on the topic — and by this one too with `{ echo: true }`, once the server took it. */
24
29
  publish: (type: string, data: unknown, options?: RealtimePublishOptions) => Promise<boolean>;
25
30
  setPresence: (state: unknown) => void;
@@ -29,7 +34,8 @@ export type ChannelHandle = {
29
34
  * the speed of a cursor reads directly.
30
35
  *
31
36
  * Messages arrive through `onMessage` rather than as state, so a canvas redrawing sixty cursors a second does not
32
- * re-render the component sixty times a second. Closed — no connection, no members — where the page has no server to
37
+ * re-render the component sixty times a second — all but what a `retain: true` channel keeps, which is state
38
+ * (`retained`): a page that joins hears each kept message as if it had just been said. Closed — no connection, no members — where the page has no server to
33
39
  * connect to (the builder, an embed), and on the server.
34
40
  */
35
41
  declare const useChannel: (asked: string | undefined, { grant, presence, onMessage, onJoin, onLeave }?: UseChannelOptions) => ChannelHandle;
@@ -2,56 +2,64 @@ import { useCallback as e, useEffect as t, useMemo as n, useRef as r, useState a
2
2
  import { useCommonStore as a } from "@plitzi/sdk-shared/store";
3
3
  import { REVOKED_TYPE as o, isValidTopic as s, matchChannel as c, realtimeClientFor as l, trackPresence as u } from "@plitzi/sdk-shared/realtime";
4
4
  //#region src/realtime/useChannel.ts
5
- var d = (d, { grant: f, presence: p, onMessage: m, onJoin: h, onLeave: g } = {}) => {
6
- let [_] = a("schema.settings.channels"), v = d && (_ ? c(d, _) : s(d)) ? d : void 0, [y] = a("realtime.endpoint"), [b] = a("realtime.transport"), x = n(() => y && typeof window < "u" ? l(y, b) : void 0, [y, b]), [S, C] = i(!1), [w, T] = i([]), E = r(void 0), D = r({
7
- onMessage: m,
8
- onJoin: h,
9
- onLeave: g
5
+ var d = (e, t) => {
6
+ if (Object.hasOwn(e, t.type) && e[t.type].at > t.at) return e;
7
+ let n = Object.fromEntries(Object.entries(e).filter(([e]) => e !== t.type));
8
+ return t.data === null ? n : {
9
+ ...n,
10
+ [t.type]: t
11
+ };
12
+ }, f = (f, { grant: p, presence: m, onMessage: h, onJoin: g, onLeave: _ } = {}) => {
13
+ let [v] = a("schema.settings.channels"), y = f && (v ? c(f, v) : s(f)) ? f : void 0, [b] = a("realtime.endpoint"), [x] = a("realtime.transport"), S = n(() => b && typeof window < "u" ? l(b, x) : void 0, [b, x]), [C, w] = i(!1), [T, E] = i([]), [D, O] = i({}), k = r(void 0), A = r({
14
+ onMessage: h,
15
+ onJoin: g,
16
+ onLeave: _
10
17
  });
11
18
  t(() => {
12
- D.current = {
13
- onMessage: m,
14
- onJoin: h,
15
- onLeave: g
19
+ A.current = {
20
+ onMessage: h,
21
+ onJoin: g,
22
+ onLeave: _
16
23
  };
17
24
  }), t(() => {
18
- x && v && f && x.grant(v, f);
25
+ S && y && p && S.grant(y, p);
19
26
  }, [
20
- x,
21
- v,
22
- f
27
+ S,
28
+ y,
29
+ p
23
30
  ]), t(() => {
24
- if (!x || !v) return;
25
- let e = u(x, v, {
26
- onChange: T,
31
+ if (!S || !y) return;
32
+ let e = u(S, y, {
33
+ onChange: E,
27
34
  onMessage: (e) => {
28
- e.type === o && e.from === "server" && C(!1), D.current.onMessage?.(e);
35
+ e.type === o && e.from === "server" && w(!1), e.retained === !0 && e.from === "server" && O((t) => d(t, e)), A.current.onMessage?.(e);
29
36
  },
30
- onArrive: (e) => D.current.onJoin?.(e),
31
- onDepart: (e) => D.current.onLeave?.(e)
37
+ onArrive: (e) => A.current.onJoin?.(e),
38
+ onDepart: (e) => A.current.onLeave?.(e)
32
39
  });
33
- E.current = e, C(x.status === "open" && !x.refusal(v));
34
- let t = x.onStatus((e) => C(e === "open" && !x.refusal(v)));
40
+ k.current = e, w(S.status === "open" && !S.refusal(y));
41
+ let t = S.onStatus((e) => w(e === "open" && !S.refusal(y)));
35
42
  return () => {
36
- t(), e.stop(), E.current = void 0, T([]), C(!1);
43
+ t(), e.stop(), k.current = void 0, E([]), O({}), w(!1);
37
44
  };
38
- }, [x, v]);
39
- let O = p === void 0 ? void 0 : JSON.stringify(p);
45
+ }, [S, y]);
46
+ let j = m === void 0 ? void 0 : JSON.stringify(m);
40
47
  t(() => {
41
- O !== void 0 && E.current?.set(JSON.parse(O));
48
+ j !== void 0 && k.current?.set(JSON.parse(j));
42
49
  }, [
43
- O,
44
- x,
45
- v
50
+ j,
51
+ S,
52
+ y
46
53
  ]);
47
- let k = e((e, t, n) => x && v ? x.publish(v, e, t, n) : Promise.resolve(!1), [x, v]), A = e((e) => E.current?.set(e), []);
54
+ let M = e((e, t, n) => S && y ? S.publish(y, e, t, n) : Promise.resolve(!1), [S, y]), N = e((e) => k.current?.set(e), []), P = n(() => Object.fromEntries(Object.entries(D).map(([e, t]) => [e, t.data])), [D]);
48
55
  return {
49
- connected: S,
50
- me: S ? x?.me : void 0,
51
- members: w,
52
- publish: k,
53
- setPresence: A
56
+ connected: C,
57
+ me: C ? S?.me : void 0,
58
+ members: T,
59
+ retained: P,
60
+ publish: M,
61
+ setPresence: N
54
62
  };
55
63
  };
56
64
  //#endregion
57
- export { d as default };
65
+ export { f as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-elements",
3
- "version": "0.38.14",
3
+ "version": "0.38.15",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
6
  "dist"
@@ -1423,12 +1423,12 @@
1423
1423
  "@dr.pogodin/react-helmet": "^3.3.2",
1424
1424
  "@plitzi/nexus": "^1.4.0",
1425
1425
  "@plitzi/plitzi-ui": "^1.6.33",
1426
- "@plitzi/sdk-auth": "0.38.14",
1427
- "@plitzi/sdk-event-bridge": "0.38.14",
1428
- "@plitzi/sdk-interactions": "0.38.14",
1429
- "@plitzi/sdk-navigation": "0.38.14",
1430
- "@plitzi/sdk-schema": "0.38.14",
1431
- "@plitzi/sdk-shared": "0.38.14",
1426
+ "@plitzi/sdk-auth": "0.38.15",
1427
+ "@plitzi/sdk-event-bridge": "0.38.15",
1428
+ "@plitzi/sdk-interactions": "0.38.15",
1429
+ "@plitzi/sdk-navigation": "0.38.15",
1430
+ "@plitzi/sdk-schema": "0.38.15",
1431
+ "@plitzi/sdk-shared": "0.38.15",
1432
1432
  "clsx": "^2.1.1",
1433
1433
  "immer": "^11.1.21",
1434
1434
  "react-markdown": "^10.1.0",