@peers-app/peers-ui 0.27.5 → 0.28.1

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.
@@ -21,6 +21,7 @@ function ToolAccessApprovalCard(props) {
21
21
  const userContext = await (0, peers_sdk_1.getUserContext)();
22
22
  return (0, peers_sdk_1.getCurrentUserAccessLevel)(userContext.getDataContext(approval.dataContextId));
23
23
  }, (0, peers_sdk_1.recordAccessLevel)({ accessLevel: 100 }), [approval.dataContextId]);
24
+ const isGroup = (0, hooks_1.usePromise)(async () => !!(await (0, peers_sdk_1.getUserContext)()).getDataContext(approval.dataContextId).groupId, true, [approval.dataContextId]);
24
25
  const required = approval.requiredAccessLevel;
25
26
  const current = userLevel ?? 100;
26
27
  const pending = approval.status === peers_sdk_1.ToolAccessApprovalStatus.pending;
@@ -53,7 +54,7 @@ function ToolAccessApprovalCard(props) {
53
54
  setBusy(false);
54
55
  }
55
56
  }
56
- return ((0, jsx_runtime_1.jsx)("div", { className: "card my-2 bg-body-secondary border-secondary-subtle", "data-testid": "tool-access-approval-card", children: (0, jsx_runtime_1.jsx)("div", { className: "card-body py-2 px-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-start gap-2", children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${statusStyle.icon} ${statusStyle.iconClass} mt-1`, "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1 min-w-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold text-body", children: pending ? "Approval required" : `Access ${approval.status}` }), (0, jsx_runtime_1.jsxs)("div", { className: "small text-body", children: [(0, jsx_runtime_1.jsx)("code", { className: "text-body", children: approval.toolName }), " needs", " ", (0, peers_sdk_1.getAccessLevelLabel)(required), " (", required, "). You have ", (0, peers_sdk_1.getAccessLevelLabel)(current), " ", "(", current, ")."] }), argsPreview && ((0, jsx_runtime_1.jsx)("pre", { className: "small mb-2 mt-2 p-2 bg-body-tertiary border rounded text-body", style: { whiteSpace: "pre-wrap" }, children: argsPreview })), (0, jsx_runtime_1.jsxs)("div", { className: "text-body-secondary small mt-1", children: ["Expires ", approval.expiresAt.toLocaleTimeString(), expired ? " (expired)" : ""] }), approval.resultSummary && ((0, jsx_runtime_1.jsx)("div", { className: "small text-body mt-1", children: approval.resultSummary })), error && (0, jsx_runtime_1.jsx)("div", { className: "text-danger small mt-1", children: error }), pending && !expired && ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex flex-wrap gap-1 mt-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-primary", disabled: busy, onClick: () => decide("once"), children: "Allow once" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", disabled: busy, onClick: () => decide("thread"), children: ["Allow in thread (", GRANT_TTL_MINUTES, " min)"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", disabled: busy, onClick: () => decide("context"), children: ["Allow in this context (", GRANT_TTL_MINUTES, " min)"] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-danger", disabled: busy, onClick: () => decide(undefined, true), children: "Deny" })] }))] })] }) }) }));
57
+ return ((0, jsx_runtime_1.jsx)("div", { className: "card my-2 bg-body-secondary border-secondary-subtle", "data-testid": "tool-access-approval-card", children: (0, jsx_runtime_1.jsx)("div", { className: "card-body py-2 px-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-start gap-2", children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${statusStyle.icon} ${statusStyle.iconClass} mt-1`, "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1 min-w-0", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold text-body", children: pending ? "Approval required" : `Access ${approval.status}` }), (0, jsx_runtime_1.jsxs)("div", { className: "small text-body", children: [(0, jsx_runtime_1.jsx)("code", { className: "text-body", children: approval.toolName }), " needs", " ", (0, peers_sdk_1.getAccessLevelLabel)(required), " (", required, "). You have ", (0, peers_sdk_1.getAccessLevelLabel)(current), " ", "(", current, ")."] }), argsPreview && ((0, jsx_runtime_1.jsx)("pre", { className: "small mb-2 mt-2 p-2 bg-body-tertiary border rounded text-body", style: { whiteSpace: "pre-wrap" }, children: argsPreview })), (0, jsx_runtime_1.jsxs)("div", { className: "text-body-secondary small mt-1", children: ["Expires ", approval.expiresAt.toLocaleTimeString(), expired ? " (expired)" : ""] }), approval.resultSummary && ((0, jsx_runtime_1.jsx)("div", { className: "small text-body mt-1", children: approval.resultSummary })), error && (0, jsx_runtime_1.jsx)("div", { className: "text-danger small mt-1", children: error }), pending && !expired && ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex flex-wrap gap-1 mt-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-primary", disabled: busy, onClick: () => decide("once"), children: "Allow once" }), !isGroup && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", disabled: busy, onClick: () => decide("thread"), children: ["Allow in thread (", GRANT_TTL_MINUTES, " min)"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", disabled: busy, onClick: () => decide("context"), children: ["Allow in this context (", GRANT_TTL_MINUTES, " min)"] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-danger", disabled: busy, onClick: () => decide(undefined, true), children: "Deny" })] }))] })] }) }) }));
57
58
  }
58
59
  function meaningfulArgsPreview(preview) {
59
60
  const trimmed = preview?.trim();
package/dist/globals.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare const groups: import("@peers-app/peers-sdk").Observable<{
30
30
  founderUserId: string;
31
31
  disabled?: boolean | undefined;
32
32
  iconClassName?: string | undefined;
33
+ signedAt?: number | undefined;
33
34
  publicRole?: import("@peers-app/peers-sdk").GroupMemberRole | undefined;
34
35
  }[]>;
35
36
  /**
@@ -110,8 +110,9 @@ const GroupMembersUI = (props) => {
110
110
  try {
111
111
  const member = await groupMembersTable.get(memberId);
112
112
  if (member) {
113
- member.role = newRole;
114
- await groupMembersTable.signAndSave(member);
113
+ // Without `publicKey`, signing binds the grant to the member's key as this device knows it.
114
+ const { publicKey: _granted, ...rest } = member;
115
+ await groupMembersTable.signAndSave({ ...rest, role: newRole });
115
116
  setRefreshKey((prev) => prev + 1);
116
117
  }
117
118
  }
@@ -23,6 +23,9 @@ function statusLabel(entry) {
23
23
  if (entry.parkedReason === "unresolvable") {
24
24
  return "Parked, needs a decision";
25
25
  }
26
+ if (entry.parkedReason === "authorization") {
27
+ return "Parked, not authorized";
28
+ }
26
29
  return "Parked";
27
30
  }
28
31
  function statusExplanation(entry) {
@@ -38,6 +41,9 @@ function statusExplanation(entry) {
38
41
  if (entry.parkedReason === "unresolvable") {
39
42
  return "An automatic fix could not be applied safely — the other record was missing, more than one row matched, or the write failed. The rows were left as they are. Choose an action when you know which version should remain.";
40
43
  }
44
+ if (entry.parkedReason === "authorization") {
45
+ return "The change isn't signed by someone allowed to make it — for example, a membership or package release that needs an Admin. This device kept its current row. It checks again on its own whenever the group's members or roles change, so a change that was only waiting for its signer's membership to arrive is written then.";
46
+ }
41
47
  return "This entry is waiting and is not being sent to other devices. Retry tries it again. The other actions clear it.";
42
48
  }
43
49
  /** Show a validation payload as indented JSON when the error ends with one. */
@@ -4,5 +4,7 @@ import "../ui-defaults";
4
4
  export type IPackageWithNavs = IPackage & {
5
5
  appNavs?: IAppNav[];
6
6
  };
7
+ /** Packages with authorized navigation metadata available in the current context. */
7
8
  export declare const allPackages: import("@peers-app/peers-sdk").Observable<IPackageWithNavs[]>;
9
+ /** Load package routes and subscribe to changes in the current context. */
8
10
  export declare const loadAllRoutes: () => Promise<true | undefined>;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadAllRoutes = exports.allPackages = void 0;
4
4
  const peers_sdk_1 = require("@peers-app/peers-sdk");
5
5
  require("../ui-defaults");
6
+ const runnable_version_1 = require("./runnable-version");
7
+ /** Packages with authorized navigation metadata available in the current context. */
6
8
  exports.allPackages = (0, peers_sdk_1.observable)([]);
7
9
  let allRoutesLoaded = false;
8
10
  const prefsSubscribed = new Set();
@@ -34,28 +36,10 @@ function subscribeToPrefs(packageId) {
34
36
  * Enrich an IPackage with appNavs from the device's active PackageVersion record.
35
37
  */
36
38
  async function enrichWithPvAppNavs(pkg) {
37
- let pvId = pkg.activePackageVersionId;
38
- try {
39
- const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
40
- await pvar.loadingPromise;
41
- pvId = pvar()?.activePackageVersionId ?? pvId;
42
- }
43
- catch {
44
- /* use group default */
45
- }
46
- if (!pvId)
47
- return pkg;
48
- try {
49
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
50
- if (pv?.appNavs) {
51
- return { ...pkg, appNavs: pv.appNavs };
52
- }
53
- }
54
- catch {
55
- /* PV not found */
56
- }
57
- return pkg;
39
+ const appNavs = (await (0, runnable_version_1.resolveRunnableVersion)(pkg))?.pv.appNavs;
40
+ return appNavs ? { ...pkg, appNavs } : pkg;
58
41
  }
42
+ /** Load package routes and subscribe to changes in the current context. */
59
43
  const loadAllRoutes = async () => {
60
44
  if (allRoutesLoaded)
61
45
  return;
@@ -115,30 +99,15 @@ function loadRoutesBundle(pkg, forceRefresh) {
115
99
  console.log(`waiting for registerPeersUIRoute to be defined on the window object`);
116
100
  await new Promise((resolve) => setTimeout(resolve, 20));
117
101
  }
118
- let pvId = pkg.activePackageVersionId;
119
- try {
120
- const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
121
- await pvar.loadingPromise;
122
- pvId = pvar()?.activePackageVersionId ?? pvId;
123
- }
124
- catch {
125
- /* use group default */
126
- }
127
- let routesBundleFileId;
128
- if (pvId) {
129
- try {
130
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
131
- routesBundleFileId = pv?.routesBundleFileId;
132
- }
133
- catch {
134
- /* no version record */
135
- }
136
- }
102
+ const selected = await (0, runnable_version_1.resolveRunnableVersion)(pkg);
103
+ const routesBundleFileId = selected?.pv.routesBundleFileId;
137
104
  let bundleCode = "";
138
105
  if (routesBundleFileId) {
139
106
  bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(routesBundleFileId);
140
107
  }
141
108
  if (bundleCode) {
109
+ if (selected?.dataContext.groupId)
110
+ (0, peers_sdk_1.assertPackageBundleIntegrity)(selected.pv, "routes", bundleCode);
142
111
  const exportRoutes = (peerRoutes) => {
143
112
  const rw = window;
144
113
  peerRoutes.routes.forEach((route) => {
@@ -0,0 +1,12 @@
1
+ import { type DataContext, type IPackage, type IPackageVersion } from "@peers-app/peers-sdk";
2
+ /**
3
+ * The version of `pkg` this device runs, when it may run here: the device's own selection, else
4
+ * the group's, checked with `assertPackageVersionRunnable`. A refusal is logged and gives
5
+ * undefined, so the package's app links, routes and screens are left out rather than loaded.
6
+ *
7
+ * @param pkg - The package.
8
+ */
9
+ export declare function resolveRunnableVersion(pkg: IPackage): Promise<{
10
+ pv: IPackageVersion;
11
+ dataContext: DataContext;
12
+ } | undefined>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRunnableVersion = resolveRunnableVersion;
4
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
+ /**
6
+ * The version of `pkg` this device runs, when it may run here: the device's own selection, else
7
+ * the group's, checked with `assertPackageVersionRunnable`. A refusal is logged and gives
8
+ * undefined, so the package's app links, routes and screens are left out rather than loaded.
9
+ *
10
+ * @param pkg - The package.
11
+ */
12
+ async function resolveRunnableVersion(pkg) {
13
+ let pvId = pkg.activePackageVersionId;
14
+ try {
15
+ const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
16
+ await pvar.loadingPromise;
17
+ pvId = pvar()?.activePackageVersionId ?? pvId;
18
+ }
19
+ catch {
20
+ /* use group default */
21
+ }
22
+ if (!pvId)
23
+ return undefined;
24
+ try {
25
+ const dataContext = await (0, peers_sdk_1.getDefaultDataContext)();
26
+ const pv = await (0, peers_sdk_1.PackageVersions)(dataContext).get(pvId);
27
+ if (!pv)
28
+ return undefined;
29
+ await (0, peers_sdk_1.assertPackageVersionRunnable)(pkg, pv, dataContext);
30
+ return { pv, dataContext };
31
+ }
32
+ catch (error) {
33
+ console.warn(`Not showing ${pkg.name}: ${error instanceof Error ? error.message : String(error)}`);
34
+ return undefined;
35
+ }
36
+ }
@@ -44,6 +44,7 @@ const globals_1 = require("../globals");
44
44
  const hooks_1 = require("../hooks");
45
45
  require("../ui-defaults");
46
46
  const routes_loader_1 = require("./routes-loader");
47
+ const runnable_version_1 = require("./runnable-version");
47
48
  const peersUIs = {};
48
49
  class UIErrorBoundary extends react_1.Component {
49
50
  constructor(props) {
@@ -182,38 +183,21 @@ const UILoader = (args) => {
182
183
  return (0, jsx_runtime_1.jsx)(UIAsyncLoader, { peersUIId: peersUIId, props: props }, peersUIId);
183
184
  };
184
185
  const uiLoadingPromises = {};
185
- async function resolveUiBundleFileId(pkg) {
186
- let pvId = pkg.activePackageVersionId;
187
- try {
188
- const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
189
- await pvar.loadingPromise;
190
- pvId = pvar()?.activePackageVersionId ?? pvId;
191
- }
192
- catch {
193
- /* use group default */
194
- }
195
- if (!pvId)
196
- return undefined;
197
- try {
198
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
199
- return pv?.uiBundleFileId;
200
- }
201
- catch {
202
- return undefined;
203
- }
204
- }
205
186
  function loadUIBundle(pkg, forceRefresh) {
206
187
  let importPromise = uiLoadingPromises[pkg.packageId];
207
188
  if (!importPromise || forceRefresh) {
208
189
  const sTime = Date.now();
209
190
  console.log(`loading ui bundle for ${pkg.name}`);
210
191
  importPromise = (async () => {
211
- const uiBundleFileId = await resolveUiBundleFileId(pkg);
212
- if (!uiBundleFileId) {
192
+ const selected = await (0, runnable_version_1.resolveRunnableVersion)(pkg);
193
+ const uiBundleFileId = selected?.pv.uiBundleFileId;
194
+ if (!selected || !uiBundleFileId) {
213
195
  return;
214
196
  }
215
197
  const bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(uiBundleFileId);
216
198
  if (bundleCode) {
199
+ if (selected.dataContext.groupId)
200
+ (0, peers_sdk_1.assertPackageBundleIntegrity)(selected.pv, "ui", bundleCode);
217
201
  const exportUIs = (peerUIs) => {
218
202
  peerUIs?.uis?.forEach((ui) => {
219
203
  peersUIs[ui.peersUIId] = ui;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.27.5",
3
+ "version": "0.28.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -36,7 +36,7 @@
36
36
  "lint:fix": "biome check --write ."
37
37
  },
38
38
  "peerDependencies": {
39
- "@peers-app/peers-sdk": "^0.27.5",
39
+ "@peers-app/peers-sdk": "^0.28.1",
40
40
  "bootstrap": "^5.3.3",
41
41
  "react": "^18.0.0",
42
42
  "react-dom": "^18.0.0"
@@ -47,7 +47,7 @@
47
47
  "@babel/preset-react": "^7.24.1",
48
48
  "@babel/preset-typescript": "^7.27.1",
49
49
  "@biomejs/biome": "2.3.11",
50
- "@peers-app/peers-sdk": "0.27.5",
50
+ "@peers-app/peers-sdk": "0.28.1",
51
51
  "@testing-library/dom": "^10.4.0",
52
52
  "@testing-library/jest-dom": "^6.6.3",
53
53
  "@testing-library/react": "^16.3.0",