@peers-app/peers-ui 0.27.5 → 0.28.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.
@@ -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,7 @@ 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
+ /** Packages with authorized navigation metadata available in the current context. */
6
7
  exports.allPackages = (0, peers_sdk_1.observable)([]);
7
8
  let allRoutesLoaded = false;
8
9
  const prefsSubscribed = new Set();
@@ -46,7 +47,10 @@ async function enrichWithPvAppNavs(pkg) {
46
47
  if (!pvId)
47
48
  return pkg;
48
49
  try {
49
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
50
+ const dataContext = await (0, peers_sdk_1.getDefaultDataContext)();
51
+ const pv = await (0, peers_sdk_1.PackageVersions)(dataContext).get(pvId);
52
+ if (pv)
53
+ await (0, peers_sdk_1.assertPackageVersionRunnable)(pkg, pv, dataContext);
50
54
  if (pv?.appNavs) {
51
55
  return { ...pkg, appNavs: pv.appNavs };
52
56
  }
@@ -56,6 +60,7 @@ async function enrichWithPvAppNavs(pkg) {
56
60
  }
57
61
  return pkg;
58
62
  }
63
+ /** Load package routes and subscribe to changes in the current context. */
59
64
  const loadAllRoutes = async () => {
60
65
  if (allRoutesLoaded)
61
66
  return;
@@ -125,9 +130,15 @@ function loadRoutesBundle(pkg, forceRefresh) {
125
130
  /* use group default */
126
131
  }
127
132
  let routesBundleFileId;
133
+ let selected;
128
134
  if (pvId) {
129
135
  try {
130
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
136
+ const dataContext = await (0, peers_sdk_1.getDefaultDataContext)();
137
+ const pv = await (0, peers_sdk_1.PackageVersions)(dataContext).get(pvId);
138
+ if (pv) {
139
+ await (0, peers_sdk_1.assertPackageVersionRunnable)(pkg, pv, dataContext);
140
+ selected = { pv, dataContext };
141
+ }
131
142
  routesBundleFileId = pv?.routesBundleFileId;
132
143
  }
133
144
  catch {
@@ -139,6 +150,8 @@ function loadRoutesBundle(pkg, forceRefresh) {
139
150
  bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(routesBundleFileId);
140
151
  }
141
152
  if (bundleCode) {
153
+ if (selected?.dataContext.groupId)
154
+ (0, peers_sdk_1.assertPackageBundleIntegrity)(selected.pv, "routes", bundleCode);
142
155
  const exportRoutes = (peerRoutes) => {
143
156
  const rw = window;
144
157
  peerRoutes.routes.forEach((route) => {
@@ -182,7 +182,7 @@ const UILoader = (args) => {
182
182
  return (0, jsx_runtime_1.jsx)(UIAsyncLoader, { peersUIId: peersUIId, props: props }, peersUIId);
183
183
  };
184
184
  const uiLoadingPromises = {};
185
- async function resolveUiBundleFileId(pkg) {
185
+ async function resolveUiBundleVersion(pkg) {
186
186
  let pvId = pkg.activePackageVersionId;
187
187
  try {
188
188
  const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
@@ -195,8 +195,11 @@ async function resolveUiBundleFileId(pkg) {
195
195
  if (!pvId)
196
196
  return undefined;
197
197
  try {
198
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
199
- return pv?.uiBundleFileId;
198
+ const dataContext = await (0, peers_sdk_1.getDefaultDataContext)();
199
+ const pv = await (0, peers_sdk_1.PackageVersions)(dataContext).get(pvId);
200
+ if (pv)
201
+ await (0, peers_sdk_1.assertPackageVersionRunnable)(pkg, pv, dataContext);
202
+ return pv ? { pv, dataContext } : undefined;
200
203
  }
201
204
  catch {
202
205
  return undefined;
@@ -208,12 +211,15 @@ function loadUIBundle(pkg, forceRefresh) {
208
211
  const sTime = Date.now();
209
212
  console.log(`loading ui bundle for ${pkg.name}`);
210
213
  importPromise = (async () => {
211
- const uiBundleFileId = await resolveUiBundleFileId(pkg);
212
- if (!uiBundleFileId) {
214
+ const selected = await resolveUiBundleVersion(pkg);
215
+ const uiBundleFileId = selected?.pv.uiBundleFileId;
216
+ if (!selected || !uiBundleFileId) {
213
217
  return;
214
218
  }
215
219
  const bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(uiBundleFileId);
216
220
  if (bundleCode) {
221
+ if (selected.dataContext.groupId)
222
+ (0, peers_sdk_1.assertPackageBundleIntegrity)(selected.pv, "ui", bundleCode);
217
223
  const exportUIs = (peerUIs) => {
218
224
  peerUIs?.uis?.forEach((ui) => {
219
225
  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.0",
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.0",
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.0",
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",