@peers-app/peers-sdk 0.27.4 → 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.
Files changed (92) hide show
  1. package/dist/context/user-context.d.ts +7 -1
  2. package/dist/context/user-context.js +4 -9
  3. package/dist/contracts/contract-proxy.d.ts +5 -0
  4. package/dist/contracts/contract-proxy.js +2 -2
  5. package/dist/contracts/contract-resolvers.d.ts +2 -2
  6. package/dist/contracts/contract-resolvers.js +14 -15
  7. package/dist/contracts/contract-transport.d.ts +2 -1
  8. package/dist/contracts/contract-transport.js +24 -2
  9. package/dist/contracts/system/network-contracts.d.ts +1 -1
  10. package/dist/contracts/tool-catalog-service.js +1 -1
  11. package/dist/contracts/tool-catalog.js +6 -2
  12. package/dist/data/assistants.d.ts +3 -3
  13. package/dist/data/authority-write-checks.d.ts +42 -0
  14. package/dist/data/authority-write-checks.js +324 -0
  15. package/dist/data/change-tracking.d.ts +4 -4
  16. package/dist/data/create-group.js +1 -0
  17. package/dist/data/devices.d.ts +27 -4
  18. package/dist/data/devices.js +31 -0
  19. package/dist/data/group-authority.d.ts +111 -0
  20. package/dist/data/group-authority.js +281 -0
  21. package/dist/data/group-members.d.ts +8 -2
  22. package/dist/data/group-members.js +41 -18
  23. package/dist/data/group-permissions.d.ts +15 -31
  24. package/dist/data/group-permissions.js +25 -95
  25. package/dist/data/group-resign.d.ts +38 -0
  26. package/dist/data/group-resign.js +71 -0
  27. package/dist/data/groups.d.ts +11 -0
  28. package/dist/data/groups.js +31 -13
  29. package/dist/data/index.d.ts +3 -0
  30. package/dist/data/index.js +3 -0
  31. package/dist/data/invites.d.ts +14 -0
  32. package/dist/data/orm/index.d.ts +1 -0
  33. package/dist/data/orm/index.js +1 -0
  34. package/dist/data/orm/sql.data-source.d.ts +2 -2
  35. package/dist/data/orm/sql.data-source.js +8 -9
  36. package/dist/data/orm/table-container.js +14 -8
  37. package/dist/data/orm/write-checks.d.ts +89 -0
  38. package/dist/data/orm/write-checks.js +115 -0
  39. package/dist/data/package-table-identity.d.ts +6 -0
  40. package/dist/data/package-table-identity.js +51 -0
  41. package/dist/data/package-version-resolver.d.ts +15 -0
  42. package/dist/data/package-version-resolver.js +58 -3
  43. package/dist/data/package-versions.d.ts +20 -14
  44. package/dist/data/package-versions.js +15 -9
  45. package/dist/data/packages.d.ts +6 -1
  46. package/dist/data/packages.js +15 -20
  47. package/dist/data/persistent-vars.js +2 -39
  48. package/dist/data/sync-recovery-queue.d.ts +4 -4
  49. package/dist/data/sync-recovery-queue.js +7 -1
  50. package/dist/data/tool-access-approvals.d.ts +3 -3
  51. package/dist/data/user-permissions.d.ts +33 -2
  52. package/dist/data/user-permissions.js +87 -10
  53. package/dist/data/users.d.ts +49 -12
  54. package/dist/data/users.js +21 -1
  55. package/dist/data/workflows.d.ts +1 -1
  56. package/dist/device/binary-peer-connection-v2.d.ts +8 -16
  57. package/dist/device/binary-peer-connection-v2.js +15 -31
  58. package/dist/device/connection.d.ts +35 -44
  59. package/dist/device/connection.js +188 -186
  60. package/dist/device/device.d.ts +1 -8
  61. package/dist/device/device.js +1 -30
  62. package/dist/device/frame-cipher.d.ts +1 -1
  63. package/dist/device/frame-cipher.js +1 -1
  64. package/dist/device/get-trust-level-fn.js +3 -6
  65. package/dist/device/socket.type.d.ts +6 -12
  66. package/dist/device-pairing/types.d.ts +26 -26
  67. package/dist/identity/user-row-change.d.ts +3 -1
  68. package/dist/identity/user-row-change.js +33 -18
  69. package/dist/invites/group-approval.js +23 -24
  70. package/dist/invites/invite-messages.d.ts +250 -28
  71. package/dist/invites/invite-token.d.ts +18 -0
  72. package/dist/invites/invite-token.js +2 -0
  73. package/dist/keys.d.ts +7 -0
  74. package/dist/keys.js +27 -0
  75. package/dist/package-installer/package-installer.d.ts +1 -1
  76. package/dist/package-installer/package-installer.js +8 -3
  77. package/dist/package-installer/package-propagation.d.ts +2 -0
  78. package/dist/package-installer/package-propagation.js +20 -38
  79. package/dist/package-installer/package-remote-checker.js +15 -3
  80. package/dist/package-loader/isolated-artifact.js +0 -2
  81. package/dist/package-loader/package-loader.js +6 -0
  82. package/dist/types/peer-device.d.ts +2 -0
  83. package/dist/types/workflow.d.ts +2 -2
  84. package/package.json +1 -1
  85. package/dist/data/package-permissions.d.ts +0 -14
  86. package/dist/data/package-permissions.js +0 -26
  87. package/dist/data/package-version-permissions.d.ts +0 -14
  88. package/dist/data/package-version-permissions.js +0 -30
  89. package/dist/device/binary-peer-connection.d.ts +0 -58
  90. package/dist/device/binary-peer-connection.js +0 -516
  91. package/dist/device/streamed-socket.d.ts +0 -23
  92. package/dist/device/streamed-socket.js +0 -255
@@ -1,7 +1,7 @@
1
1
  import { DataContext } from "../context/data-context";
2
2
  import { type IUser } from "../data";
3
3
  import type { DataSourceFactory, IDataChangedEvent, Table } from "../data/orm";
4
- import { type IPublicPrivateKeys } from "../keys";
4
+ import type { IPublicPrivateKeys } from "../keys";
5
5
  import { type Observable } from "../observable";
6
6
  /** Coordinates personal and group data contexts for one signed-in user. */
7
7
  export declare class UserContext {
@@ -69,6 +69,12 @@ export declare class UserContext {
69
69
  type: "peers-services" | "https";
70
70
  url: string;
71
71
  }[] | undefined;
72
+ anchorsSignature?: string | undefined;
73
+ keySuccession?: {
74
+ publicKey: string;
75
+ signature: string;
76
+ issuedAt: string;
77
+ }[] | undefined;
72
78
  } | undefined>;
73
79
  /**
74
80
  * Sign and persist this user's identity, then copy it into every loaded
@@ -6,7 +6,6 @@ const data_context_1 = require("../context/data-context");
6
6
  const data_1 = require("../data");
7
7
  const user_permissions_1 = require("../data/user-permissions");
8
8
  const events_1 = require("../events");
9
- const keys_1 = require("../keys");
10
9
  const observable_1 = require("../observable");
11
10
  const utils_1 = require("../utils");
12
11
  /** Coordinates personal and group data contexts for one signed-in user. */
@@ -278,16 +277,12 @@ class UserContext {
278
277
  console.error("Group data context is missing group id. Something went wrong");
279
278
  continue;
280
279
  }
281
- const groupObject = await (0, data_1.Groups)(dataContext).get(dataContext.groupId);
280
+ // Copy the exact row the authority read verified, rather than a row read before an
281
+ // asynchronous rebuild that may have verified a different, subsequently synced row.
282
+ const { getGroupAuthority } = await Promise.resolve().then(() => require("../data/group-authority"));
283
+ const { group: groupObject } = await getGroupAuthority(this, dataContext.groupId);
282
284
  const myGroupObject = await (0, data_1.Groups)(this.userDataContext).get(dataContext.groupId);
283
285
  if (groupObject && !(0, lodash_1.isEqual)(groupObject, myGroupObject)) {
284
- try {
285
- (0, keys_1.verifyObjectSignature)(groupObject);
286
- }
287
- catch (err) {
288
- console.error(`Error verifying group object from group ${dataContext.groupId}:`, err);
289
- continue;
290
- }
291
286
  await (0, data_1.Groups)(this.userDataContext).save(groupObject);
292
287
  }
293
288
  }
@@ -428,6 +428,11 @@ export interface IContractCallContext {
428
428
  workflow?: IWorkflowRunContext;
429
429
  /** User id of the caller (e.g. the remote device's user), if known. */
430
430
  callerUserId?: string;
431
+ /**
432
+ * Signing key a remote caller authenticated with. Role lookups for `callerUserId` pass it, so a
433
+ * role counts only for the key it was granted to. Absent for same-device callers.
434
+ */
435
+ callerPublicKey?: string;
431
436
  /** User id of the local device executing the call. */
432
437
  localUserId?: string;
433
438
  /**
@@ -534,8 +534,8 @@ async function assertContractToolAccess(tool, call, context) {
534
534
  }
535
535
  const userLevel = userId
536
536
  ? context.dataContext
537
- ? await (0, group_permissions_1.getUserAccessLevel)(context.dataContext, userId)
538
- : (0, access_level_1.accessLevelFromGroupRole)(await (0, group_permissions_1.getUserRole)(call.dataContextId || context.localUserId || userId, userId))
537
+ ? await (0, group_permissions_1.getUserAccessLevel)(context.dataContext, userId, context.callerPublicKey)
538
+ : (0, access_level_1.accessLevelFromGroupRole)(await (0, group_permissions_1.getUserRole)(call.dataContextId || context.localUserId || userId, userId, undefined, context.callerPublicKey))
539
539
  : access_level_1.AccessLevel.None;
540
540
  return (0, action_dispatch_1.authorizeToolDispatch)({ ...context.authority, userId, userAccessLevel: userLevel }, required, mode, strict);
541
541
  }
@@ -71,8 +71,8 @@ export declare function registerContractResolver(contractId: string, version: nu
71
71
  */
72
72
  export declare function getContractResolver(contractId: string, version: number, providerPackageId?: string, dataContext?: DataContext): ContractResolver | undefined;
73
73
  /**
74
- * Verify that a connection- or message-authenticated remote identity matches a valid
75
- * self-signed Users record in the provider's personal or shared-group data.
74
+ * Verify that a connection- or message-authenticated remote identity matches the user's
75
+ * identity on record, preferring the personal context over shared-group data.
76
76
  *
77
77
  * Unsigned discovery stubs, malformed signatures, and key mismatches fail closed. This is
78
78
  * the identity half of remote authorization; trust and role decisions build on it.
@@ -160,8 +160,8 @@ function getContractResolver(contractId, version, providerPackageId, dataContext
160
160
  return onlyScopedResolver;
161
161
  }
162
162
  /**
163
- * Verify that a connection- or message-authenticated remote identity matches a valid
164
- * self-signed Users record in the provider's personal or shared-group data.
163
+ * Verify that a connection- or message-authenticated remote identity matches the user's
164
+ * identity on record, preferring the personal context over shared-group data.
165
165
  *
166
166
  * Unsigned discovery stubs, malformed signatures, and key mismatches fail closed. This is
167
167
  * the identity half of remote authorization; trust and role decisions build on it.
@@ -173,19 +173,15 @@ async function verifyRemoteContractIdentity(userContext, remoteIdentity) {
173
173
  const identityContexts = [userContext.userDataContext, ...userContext.groupDataContexts.values()];
174
174
  for (const dataContext of identityContexts) {
175
175
  const user = await (0, users_1.Users)(dataContext).get(remoteIdentity.userId);
176
- if (!user?.signature ||
177
- user.userId !== remoteIdentity.userId ||
178
- user.publicKey !== remoteIdentity.publicKey ||
179
- user.publicBoxKey !== remoteIdentity.publicBoxKey) {
176
+ if (!user)
180
177
  continue;
181
- }
182
- try {
183
- (0, user_permissions_1.verifyUserSignature)(user);
184
- return true;
185
- }
186
- catch {
187
- // A different context may contain the valid signed record.
188
- }
178
+ // A member can supply a self-signed namesake in a group. It must never override an
179
+ // identity already pinned in personal contacts (especially the device owner's identity).
180
+ if (!(0, user_permissions_1.sameUserIdentity)(user, remoteIdentity))
181
+ return false;
182
+ if (!user.signature)
183
+ continue;
184
+ return (0, user_permissions_1.isSelfSignedUser)(user);
189
185
  }
190
186
  return false;
191
187
  }
@@ -269,6 +265,7 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
269
265
  dataContextId,
270
266
  context: {
271
267
  callerUserId: remoteIdentity.userId,
268
+ callerPublicKey: remoteIdentity.publicKey,
272
269
  localUserId: userContext.userId,
273
270
  callerDeviceId: remoteIdentity.deviceId,
274
271
  // Never create a context the caller names: only this device's own groups.
@@ -286,7 +283,7 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
286
283
  }
287
284
  const dataContext = userContext.getDataContext(dataContextId);
288
285
  await dataContext.packageLoader?.catalogReady;
289
- const callerLevel = await (0, group_permissions_1.getUserAccessLevel)(dataContext, remoteIdentity.userId);
286
+ const callerLevel = await (0, group_permissions_1.getUserAccessLevel)(dataContext, remoteIdentity.userId, remoteIdentity.publicKey);
290
287
  // Policy belongs to the executing provider, not the latest registration of this shape.
291
288
  // Select after the async role lookup and retain the selection through endpoint resolution.
292
289
  const registry = dataContext.packageLoader?.contractRegistry;
@@ -323,6 +320,7 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
323
320
  providerPackageId: selected?.providerPackageId,
324
321
  context: {
325
322
  callerUserId: remoteIdentity.userId,
323
+ callerPublicKey: remoteIdentity.publicKey,
326
324
  localUserId: userContext.userId,
327
325
  callerDeviceId: remoteIdentity.deviceId,
328
326
  dataContext,
@@ -490,6 +488,7 @@ function createUserContextContractProviderRouter(transport, opts) {
490
488
  const { userContext, remoteIdentity } = opts;
491
489
  const context = {
492
490
  callerUserId: remoteIdentity.userId,
491
+ callerPublicKey: remoteIdentity.publicKey,
493
492
  localUserId: userContext.userId,
494
493
  callerDeviceId: remoteIdentity.deviceId,
495
494
  dataContext: userContext.userDataContext,
@@ -63,7 +63,8 @@ export declare const CONTRACT_REQUEST_CHANNEL = "contractCall";
63
63
  export declare const CONTRACT_NOTIFY_CHANNEL = "contractNotify";
64
64
  /**
65
65
  * Wrap a duplex RPC emitter (e.g. a device {@link Connection}) as an
66
- * {@link IContractTransport}. `emit` delegates directly. `on` uses one adapter-owned
66
+ * {@link IContractTransport}. `emit` delegates directly and rejects with an `Error`
67
+ * carrying the remote message, whatever the emitter rejected with. `on` uses one adapter-owned
67
68
  * dispatcher per emitter/channel and returns an unsubscribe that removes only the caller's
68
69
  * handler from that dispatcher; it never uses a coarse `removeAllListeners`.
69
70
  *
@@ -33,9 +33,24 @@ exports.CONTRACT_REQUEST_CHANNEL = "contractCall";
33
33
  */
34
34
  exports.CONTRACT_NOTIFY_CHANNEL = "contractNotify";
35
35
  const connectionDispatcherStates = new WeakMap();
36
+ /**
37
+ * Turn a failed emit into an `Error`. A device connection rejects with the remote handler's
38
+ * `{ error, errorType }` envelope, or with a string when the call times out.
39
+ */
40
+ function toError(error) {
41
+ if (error instanceof Error)
42
+ return error;
43
+ if (error &&
44
+ typeof error === "object" &&
45
+ typeof error.error === "string") {
46
+ return new Error(error.error);
47
+ }
48
+ return new Error(typeof error === "string" ? error : JSON.stringify(error));
49
+ }
36
50
  /**
37
51
  * Wrap a duplex RPC emitter (e.g. a device {@link Connection}) as an
38
- * {@link IContractTransport}. `emit` delegates directly. `on` uses one adapter-owned
52
+ * {@link IContractTransport}. `emit` delegates directly and rejects with an `Error`
53
+ * carrying the remote message, whatever the emitter rejected with. `on` uses one adapter-owned
39
54
  * dispatcher per emitter/channel and returns an unsubscribe that removes only the caller's
40
55
  * handler from that dispatcher; it never uses a coarse `removeAllListeners`.
41
56
  *
@@ -53,7 +68,14 @@ const connectionDispatcherStates = new WeakMap();
53
68
  */
54
69
  function connectionContractTransport(emitter) {
55
70
  return {
56
- emit: (channel, ...args) => emitter.emit(channel, ...args),
71
+ emit: async (channel, ...args) => {
72
+ try {
73
+ return await emitter.emit(channel, ...args);
74
+ }
75
+ catch (error) {
76
+ throw toError(error);
77
+ }
78
+ },
57
79
  on: (channel, handler) => {
58
80
  let channelStates = connectionDispatcherStates.get(emitter);
59
81
  if (!channelStates) {
@@ -199,7 +199,7 @@ export interface INetworkSyncRecoveryEntry {
199
199
  attemptCount: number;
200
200
  latestError: string;
201
201
  status: "retrying" | "parked";
202
- parkedReason?: "schema" | "unique-conflict" | "unresolvable";
202
+ parkedReason?: "schema" | "unique-conflict" | "unresolvable" | "authorization";
203
203
  stuck: boolean;
204
204
  /** True when this device has a row that "Keep this device's version" can snapshot. */
205
205
  localRowExists: boolean;
@@ -172,7 +172,7 @@ async function projectToolCatalogAccess(dataContext, entry, callerContext) {
172
172
  ? runtime.getEffectiveToolLevel?.(entry.target, provider)
173
173
  : (0, tool_access_1.recordAccessLevel)((await liveResolver(dataContext, entry.target, provider)?.({ dataContext }))?.tools?.[entry.target.toolName]?.tool);
174
174
  if (accessLevel !== undefined) {
175
- const callerAccessLevel = await (0, group_permissions_1.getUserAccessLevel)(dataContext, userId);
175
+ const callerAccessLevel = await (0, group_permissions_1.getUserAccessLevel)(dataContext, userId, callerContext.callerPublicKey);
176
176
  entry.access = {
177
177
  accessLevel,
178
178
  callerAccessLevel,
@@ -179,13 +179,17 @@ function validateToolCatalogWrite(existing, candidate, recordId = candidate?.too
179
179
  !isMissingTargetQuarantine(existing) &&
180
180
  isMissingTargetQuarantine(candidate))
181
181
  throw new Error(`Quarantined tool identity: ${candidate.toolId}`);
182
- // Quarantine preserves the original evidence byte-for-byte; it cannot retarget it.
182
+ // Quarantine preserves the original evidence byte-for-byte; it cannot retarget it. That
183
+ // includes an existing quarantine's reason: a peer's resolve patch whose `catalogReason`
184
+ // half arrives first must park, not erase the missing-target reason its other half needs.
183
185
  if (existing &&
184
186
  candidate.catalogState === "quarantined" &&
185
187
  existing.contractId === candidate.contractId &&
186
188
  existing.contractVersion === candidate.contractVersion &&
187
189
  existing.toolName === candidate.toolName &&
188
- existing.catalogIdentity === candidate.catalogIdentity)
190
+ existing.catalogIdentity === candidate.catalogIdentity &&
191
+ (existing.catalogState !== "quarantined" ||
192
+ (existing.catalogReason ?? undefined) === (candidate.catalogReason ?? undefined)))
189
193
  return;
190
194
  // Changing quarantined identity evidence also requires an approved repair, even
191
195
  // when the candidate stays quarantined and would be resolved by reconciliation.
@@ -22,8 +22,8 @@ export declare const assistantSchema: z.ZodObject<{
22
22
  }, "strip", z.ZodTypeAny, {
23
23
  name: string;
24
24
  accessLevel: number;
25
- assistantId: string;
26
25
  createdAt: Date;
26
+ assistantId: string;
27
27
  assistantRunnerToolId: string;
28
28
  assistantRunnerConfig: {} & {
29
29
  [k: string]: any;
@@ -56,8 +56,8 @@ export type IAssistant = z.infer<typeof assistantSchema>;
56
56
  export declare function Assistants(dataContext?: DataContext): import("..").Table<{
57
57
  name: string;
58
58
  accessLevel: number;
59
- assistantId: string;
60
59
  createdAt: Date;
60
+ assistantId: string;
61
61
  assistantRunnerToolId: string;
62
62
  assistantRunnerConfig: {} & {
63
63
  [k: string]: any;
@@ -87,8 +87,8 @@ export declare const groupPrimaryAssistantVar: import("./persistent-vars").Persi
87
87
  export declare const getPrimaryAssistant: (dataContext?: DataContext) => Promise<{
88
88
  name: string;
89
89
  accessLevel: number;
90
- assistantId: string;
91
90
  createdAt: Date;
91
+ assistantId: string;
92
92
  assistantRunnerToolId: string;
93
93
  assistantRunnerConfig: {} & {
94
94
  [k: string]: any;
@@ -0,0 +1,42 @@
1
+ import type { IGroupMember } from "./group-members";
2
+ import type { IGroup } from "./groups";
3
+ import { type ITableWriteCheckContext } from "./orm/write-checks";
4
+ import type { IPackageVersion } from "./package-versions";
5
+ import type { IPackage } from "./packages";
6
+ /**
7
+ * A membership must be signed by a member who counts as at least Admin, at least the role
8
+ * granted, and at least the member's current role. Only the founder writes the founder's row.
9
+ * A membership is removed by a signed soft delete; a hard delete is allowed only for a
10
+ * duplicate row (the user has another membership row), as unique-conflict cleanup writes.
11
+ */
12
+ export declare function checkGroupMember(existing: IGroupMember | undefined, candidate: IGroupMember | undefined, context: ITableWriteCheckContext): Promise<void>;
13
+ /**
14
+ * The group row must be signed by the founder or an Admin who counts, and only the founder can
15
+ * name a different founder. Sync never deletes it.
16
+ */
17
+ export declare function checkGroup(existing: IGroup | undefined, candidate: IGroup | undefined, context: ITableWriteCheckContext): Promise<void>;
18
+ /**
19
+ * A Writer may add a package; changing an existing one beyond its name and description (the
20
+ * active version, follow policy, update URL, publisher key, repository, disabled) needs an
21
+ * Admin. Deletes stay open to any member, as the table allows locally.
22
+ */
23
+ export declare function checkPackageWrite(existing: IPackage | undefined, candidate: IPackage | undefined, context: ITableWriteCheckContext): Promise<void>;
24
+ /**
25
+ * Whether an Admin's device may act on a package row by itself: install releases from its
26
+ * `updateUrl`, copy releases signed by its publisher key in from other contexts, and select one
27
+ * by its follow policy. Each of those signs the result with the Admin's key, so the row must be
28
+ * one an Admin signed: a Writer may add a package row, or delete one and add it again, with any
29
+ * publisher key and update URL. A peers-core row instead qualifies only while it names the
30
+ * peers-core publisher key (or a revoked one, which is re-pinned to it before any release is
31
+ * checked), whoever signed it. Always true outside a group.
32
+ *
33
+ * @param pkg - The package row in `dataContext`.
34
+ * @param dataContext - The context the row belongs to.
35
+ */
36
+ export declare function isPackageRowAdminSigned(pkg: IPackage, dataContext: ITableWriteCheckContext["dataContext"]): Promise<boolean>;
37
+ /**
38
+ * A dev version needs a Writer's signature; any other tag needs an Admin's, so no member can
39
+ * choose the code other members' devices run. peers-core versions signed by its publisher are
40
+ * accepted from anyone, since every device installs the bundled release.
41
+ */
42
+ export declare function checkPackageVersion(existing: IPackageVersion | undefined, candidate: IPackageVersion | undefined, context: ITableWriteCheckContext): Promise<void>;