agent-comms 3.7.0 → 3.9.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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +5 -1
  4. package/dist/bridges/cc-peer/default-front.d.ts +20 -0
  5. package/dist/bridges/cc-peer/default-front.d.ts.map +1 -0
  6. package/dist/bridges/cc-peer/default-front.js +28 -0
  7. package/dist/bridges/cc-peer/default-front.js.map +1 -0
  8. package/dist/bridges/cc-peer/front-controller.d.ts +50 -0
  9. package/dist/bridges/cc-peer/front-controller.d.ts.map +1 -0
  10. package/dist/bridges/cc-peer/front-controller.js +84 -0
  11. package/dist/bridges/cc-peer/front-controller.js.map +1 -0
  12. package/dist/bridges/cc-peer/front-relay.d.ts +42 -0
  13. package/dist/bridges/cc-peer/front-relay.d.ts.map +1 -0
  14. package/dist/bridges/cc-peer/front-relay.js +43 -0
  15. package/dist/bridges/cc-peer/front-relay.js.map +1 -0
  16. package/dist/bridges/cc-peer/front-runtime.d.ts +18 -0
  17. package/dist/bridges/cc-peer/front-runtime.d.ts.map +1 -0
  18. package/dist/bridges/cc-peer/front-runtime.js +76 -0
  19. package/dist/bridges/cc-peer/front-runtime.js.map +1 -0
  20. package/dist/bridges/cc-peer/front.d.ts +32 -0
  21. package/dist/bridges/cc-peer/front.d.ts.map +1 -0
  22. package/dist/bridges/cc-peer/front.js +37 -0
  23. package/dist/bridges/cc-peer/front.js.map +1 -0
  24. package/dist/bridges/cc-peer/run.d.ts.map +1 -1
  25. package/dist/bridges/cc-peer/run.js +2 -0
  26. package/dist/bridges/cc-peer/run.js.map +1 -1
  27. package/dist/bridges/claude-code/channel.d.ts.map +1 -1
  28. package/dist/bridges/claude-code/channel.js +2 -0
  29. package/dist/bridges/claude-code/channel.js.map +1 -1
  30. package/dist/bridges/codex/tool.d.ts.map +1 -1
  31. package/dist/bridges/codex/tool.js +2 -0
  32. package/dist/bridges/codex/tool.js.map +1 -1
  33. package/dist/bridges/mcp/server.d.ts.map +1 -1
  34. package/dist/bridges/mcp/server.js +2 -0
  35. package/dist/bridges/mcp/server.js.map +1 -1
  36. package/dist/bridges/opencode/plugin.d.ts.map +1 -1
  37. package/dist/bridges/opencode/plugin.js +2 -0
  38. package/dist/bridges/opencode/plugin.js.map +1 -1
  39. package/dist/bridges/pi/extension.d.ts.map +1 -1
  40. package/dist/bridges/pi/extension.js +2 -0
  41. package/dist/bridges/pi/extension.js.map +1 -1
  42. package/dist/bridges/user/controller.d.ts.map +1 -1
  43. package/dist/bridges/user/controller.js +4 -0
  44. package/dist/bridges/user/controller.js.map +1 -1
  45. package/dist/core/bridge-mesh.d.ts +13 -2
  46. package/dist/core/bridge-mesh.d.ts.map +1 -1
  47. package/dist/core/bridge-mesh.js +27 -5
  48. package/dist/core/bridge-mesh.js.map +1 -1
  49. package/dist/core/delegation-policy.d.ts +20 -0
  50. package/dist/core/delegation-policy.d.ts.map +1 -0
  51. package/dist/core/delegation-policy.js +10 -0
  52. package/dist/core/delegation-policy.js.map +1 -0
  53. package/dist/core/device-membership-verification.d.ts +28 -0
  54. package/dist/core/device-membership-verification.d.ts.map +1 -0
  55. package/dist/core/device-membership-verification.js +43 -0
  56. package/dist/core/device-membership-verification.js.map +1 -0
  57. package/dist/core/device-membership.d.ts +42 -0
  58. package/dist/core/device-membership.d.ts.map +1 -0
  59. package/dist/core/device-membership.js +53 -0
  60. package/dist/core/device-membership.js.map +1 -0
  61. package/dist/core/identity-store.d.ts +18 -0
  62. package/dist/core/identity-store.d.ts.map +1 -1
  63. package/dist/core/identity-store.js +62 -0
  64. package/dist/core/identity-store.js.map +1 -1
  65. package/dist/core/index.d.ts +3 -1
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +2 -1
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/mesh-store.d.ts +4 -0
  70. package/dist/core/mesh-store.d.ts.map +1 -1
  71. package/dist/core/mesh-store.js +9 -0
  72. package/dist/core/mesh-store.js.map +1 -1
  73. package/dist/core/package-version.d.ts +6 -0
  74. package/dist/core/package-version.d.ts.map +1 -0
  75. package/dist/core/package-version.js +28 -0
  76. package/dist/core/package-version.js.map +1 -0
  77. package/dist/core/peer-lifecycle.d.ts +2 -0
  78. package/dist/core/peer-lifecycle.d.ts.map +1 -1
  79. package/dist/core/peer-lifecycle.js +1 -0
  80. package/dist/core/peer-lifecycle.js.map +1 -1
  81. package/dist/core/room-lifecycle.d.ts.map +1 -1
  82. package/dist/core/room-lifecycle.js +5 -4
  83. package/dist/core/room-lifecycle.js.map +1 -1
  84. package/dist/core/room-protocol.d.ts.map +1 -1
  85. package/dist/core/room-protocol.js +4 -3
  86. package/dist/core/room-protocol.js.map +1 -1
  87. package/dist/core/room-token-verification.d.ts +3 -0
  88. package/dist/core/room-token-verification.d.ts.map +1 -1
  89. package/dist/core/room-token-verification.js +4 -0
  90. package/dist/core/room-token-verification.js.map +1 -1
  91. package/dist/core/test/package-version.unit.test.d.ts +5 -0
  92. package/dist/core/test/package-version.unit.test.d.ts.map +1 -0
  93. package/dist/core/test/package-version.unit.test.js +30 -0
  94. package/dist/core/test/package-version.unit.test.js.map +1 -0
  95. package/dist/core/test/version-check.unit.test.d.ts +5 -0
  96. package/dist/core/test/version-check.unit.test.d.ts.map +1 -0
  97. package/dist/core/test/version-check.unit.test.js +176 -0
  98. package/dist/core/test/version-check.unit.test.js.map +1 -0
  99. package/dist/core/tool.d.ts +7 -1
  100. package/dist/core/tool.d.ts.map +1 -1
  101. package/dist/core/tool.js +34 -11
  102. package/dist/core/tool.js.map +1 -1
  103. package/dist/core/user-identity.d.ts +10 -0
  104. package/dist/core/user-identity.d.ts.map +1 -1
  105. package/dist/core/user-identity.js +60 -8
  106. package/dist/core/user-identity.js.map +1 -1
  107. package/dist/core/version-check.d.ts +36 -0
  108. package/dist/core/version-check.d.ts.map +1 -0
  109. package/dist/core/version-check.js +118 -0
  110. package/dist/core/version-check.js.map +1 -0
  111. package/dist/test/bridge-mesh-version-drift.test.d.ts +7 -0
  112. package/dist/test/bridge-mesh-version-drift.test.d.ts.map +1 -0
  113. package/dist/test/bridge-mesh-version-drift.test.js +98 -0
  114. package/dist/test/bridge-mesh-version-drift.test.js.map +1 -0
  115. package/dist/test/bridge-mesh.test.js +16 -2
  116. package/dist/test/bridge-mesh.test.js.map +1 -1
  117. package/dist/test/cc-peer-default-front.test.d.ts +2 -0
  118. package/dist/test/cc-peer-default-front.test.d.ts.map +1 -0
  119. package/dist/test/cc-peer-default-front.test.js +71 -0
  120. package/dist/test/cc-peer-default-front.test.js.map +1 -0
  121. package/dist/test/cc-peer-front-controller.test.d.ts +2 -0
  122. package/dist/test/cc-peer-front-controller.test.d.ts.map +1 -0
  123. package/dist/test/cc-peer-front-controller.test.js +206 -0
  124. package/dist/test/cc-peer-front-controller.test.js.map +1 -0
  125. package/dist/test/cc-peer-front-relay.test.d.ts +2 -0
  126. package/dist/test/cc-peer-front-relay.test.d.ts.map +1 -0
  127. package/dist/test/cc-peer-front-relay.test.js +177 -0
  128. package/dist/test/cc-peer-front-relay.test.js.map +1 -0
  129. package/dist/test/cc-peer-front.test.d.ts +2 -0
  130. package/dist/test/cc-peer-front.test.d.ts.map +1 -0
  131. package/dist/test/cc-peer-front.test.js +109 -0
  132. package/dist/test/cc-peer-front.test.js.map +1 -0
  133. package/dist/test/delegation-policy.test.d.ts +5 -0
  134. package/dist/test/delegation-policy.test.d.ts.map +1 -0
  135. package/dist/test/delegation-policy.test.js +52 -0
  136. package/dist/test/delegation-policy.test.js.map +1 -0
  137. package/dist/test/device-membership-verification.test.d.ts +2 -0
  138. package/dist/test/device-membership-verification.test.d.ts.map +1 -0
  139. package/dist/test/device-membership-verification.test.js +214 -0
  140. package/dist/test/device-membership-verification.test.js.map +1 -0
  141. package/dist/test/device-membership.test.d.ts +2 -0
  142. package/dist/test/device-membership.test.d.ts.map +1 -0
  143. package/dist/test/device-membership.test.js +201 -0
  144. package/dist/test/device-membership.test.js.map +1 -0
  145. package/dist/test/identity-store.test.js +120 -1
  146. package/dist/test/identity-store.test.js.map +1 -1
  147. package/dist/test/mesh-store-orchestration.test.js +27 -0
  148. package/dist/test/mesh-store-orchestration.test.js.map +1 -1
  149. package/dist/test/peer-lifecycle.test.js +12 -0
  150. package/dist/test/peer-lifecycle.test.js.map +1 -1
  151. package/dist/test/room-lifecycle.test.js +24 -1
  152. package/dist/test/room-lifecycle.test.js.map +1 -1
  153. package/dist/test/tool-version-reporting.test.d.ts +5 -0
  154. package/dist/test/tool-version-reporting.test.d.ts.map +1 -0
  155. package/dist/test/tool-version-reporting.test.js +67 -0
  156. package/dist/test/tool-version-reporting.test.js.map +1 -0
  157. package/dist/test/user-identity.test.js +54 -1
  158. package/dist/test/user-identity.test.js.map +1 -1
  159. package/package.json +1 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Unit tests for the cc-peer front's pure decision logic (bridges/cc-peer/front.ts) -- roster filtering, slot computation, and inbound message routing, all tested purely over data with no real cc-peer/MeshStore/filesystem involved.
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+ import { isCcPeerLibraryPeer, computeFrontSlot, selectSessionsToFront, matchInboundMessageSession, } from "../bridges/cc-peer/front.js";
6
+ function rosterEntry(overrides = {}) {
7
+ return {
8
+ pid: 111,
9
+ cwd: "/tmp/project",
10
+ version: "2.1.269",
11
+ messagingSocketPath: "/tmp/sock-111",
12
+ ...overrides,
13
+ };
14
+ }
15
+ /** A pid standing in for a real live agent-comms bridge holding a slot's lock -- arbitrary beyond "a plausible process id", used only to prove selectSessionsToFront treats "probe returned a pid" as "already fronted", not to assert anything about the value itself. */
16
+ const LIVE_LOCK_HOLDER_PID = 4242;
17
+ describe("isCcPeerLibraryPeer", () => {
18
+ it("is true for an entry carrying cc-peer's own literal version marker", () => {
19
+ expect(isCcPeerLibraryPeer(rosterEntry({ version: "cc-peer" }))).toBe(true);
20
+ });
21
+ it("is false for an entry reporting a real Claude Code version", () => {
22
+ expect(isCcPeerLibraryPeer(rosterEntry({ version: "2.1.269" }))).toBe(false);
23
+ });
24
+ });
25
+ describe("computeFrontSlot", () => {
26
+ it("uses the claude-code harness, matching the real claude-code bridge's own slot", () => {
27
+ expect(computeFrontSlot("/tmp/project")).toEqual({
28
+ harness: "claude-code",
29
+ cwd: "/tmp/project",
30
+ });
31
+ });
32
+ it("is distinct per cwd", () => {
33
+ expect(computeFrontSlot("/tmp/a")).not.toEqual(computeFrontSlot("/tmp/b"));
34
+ });
35
+ });
36
+ describe("selectSessionsToFront", () => {
37
+ it("excludes cc-peer-library peers regardless of slot state", () => {
38
+ const libraryPeer = rosterEntry({ pid: 1, version: "cc-peer" });
39
+ const selected = selectSessionsToFront([libraryPeer], () => undefined);
40
+ expect(selected).toEqual([]);
41
+ });
42
+ it("excludes a real session whose own slot is already held by a live bridge", () => {
43
+ const alreadyFronted = rosterEntry({ pid: 2, cwd: "/tmp/self-fronted" });
44
+ const selected = selectSessionsToFront([alreadyFronted], (slot) => slot.cwd === "/tmp/self-fronted" ? LIVE_LOCK_HOLDER_PID : undefined);
45
+ expect(selected).toEqual([]);
46
+ });
47
+ it("selects a real session whose own slot is unheld", () => {
48
+ const unfronted = rosterEntry({ pid: 3, cwd: "/tmp/unfronted" });
49
+ const selected = selectSessionsToFront([unfronted], () => undefined);
50
+ expect(selected).toEqual([unfronted]);
51
+ });
52
+ it("probes the slot computed from the entry's own cwd, not an unrelated one", () => {
53
+ const entry = rosterEntry({ pid: 4, cwd: "/tmp/watched" });
54
+ let probedSlot;
55
+ selectSessionsToFront([entry], (slot) => {
56
+ probedSlot = slot;
57
+ return undefined;
58
+ });
59
+ expect(probedSlot).toEqual({ harness: "claude-code", cwd: "/tmp/watched" });
60
+ });
61
+ it("handles a mixed roster, keeping only the unfronted real sessions", () => {
62
+ const libraryPeer = rosterEntry({ pid: 1, version: "cc-peer" });
63
+ const selfFronted = rosterEntry({ pid: 2, cwd: "/tmp/self-fronted" });
64
+ const unfronted = rosterEntry({ pid: 3, cwd: "/tmp/unfronted" });
65
+ const selected = selectSessionsToFront([libraryPeer, selfFronted, unfronted], (slot) => slot.cwd === "/tmp/self-fronted" ? LIVE_LOCK_HOLDER_PID : undefined);
66
+ expect(selected).toEqual([unfronted]);
67
+ });
68
+ });
69
+ describe("matchInboundMessageSession", () => {
70
+ it("matches a fronted session by the uds:<socketPath> envelope convention", () => {
71
+ const a = {
72
+ messagingSocketPath: "/tmp/sock-a",
73
+ label: "a",
74
+ };
75
+ const b = {
76
+ messagingSocketPath: "/tmp/sock-b",
77
+ label: "b",
78
+ };
79
+ const match = matchInboundMessageSession([a, b], {
80
+ from: "uds:/tmp/sock-b",
81
+ });
82
+ expect(match).toBe(b);
83
+ });
84
+ it("returns undefined when no fronted session's socket matches", () => {
85
+ const a = {
86
+ messagingSocketPath: "/tmp/sock-a",
87
+ label: "a",
88
+ };
89
+ const match = matchInboundMessageSession([a], {
90
+ from: "uds:/tmp/sock-unknown",
91
+ });
92
+ expect(match).toBeUndefined();
93
+ });
94
+ it("returns undefined when the message carries no from field at all", () => {
95
+ const a = {
96
+ messagingSocketPath: "/tmp/sock-a",
97
+ label: "a",
98
+ };
99
+ expect(matchInboundMessageSession([a], {})).toBeUndefined();
100
+ });
101
+ it("never matches a from value missing the uds: prefix, even with an otherwise identical path", () => {
102
+ const a = {
103
+ messagingSocketPath: "/tmp/sock-a",
104
+ label: "a",
105
+ };
106
+ expect(matchInboundMessageSession([a], { from: "/tmp/sock-a" })).toBeUndefined();
107
+ });
108
+ });
109
+ //# sourceMappingURL=cc-peer-front.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cc-peer-front.test.js","sourceRoot":"","sources":["../../src/test/cc-peer-front.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,0BAA0B,GAE3B,MAAM,6BAA6B,CAAC;AAErC,SAAS,WAAW,CAClB,YAAsD,EAAE;IAExD,OAAO;QACL,GAAG,EAAE,GAAG;QACR,GAAG,EAAE,cAAc;QACnB,OAAO,EAAE,SAAS;QAClB,mBAAmB,EAAE,eAAe;QACpC,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,2QAA2Q;AAC3Q,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CACnE,KAAK,CACN,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC;YAC/C,OAAO,EAAE,aAAa;YACtB,GAAG,EAAE,cAAc;SACpB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,WAAW,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,qBAAqB,CAAC,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvE,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,cAAc,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,CAAC,CAAC;QACzE,MAAM,QAAQ,GAAG,qBAAqB,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAChE,IAAI,CAAC,GAAG,KAAK,mBAAmB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,SAAS,CACpE,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,SAAS,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,qBAAqB,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrE,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,CAAC;QAC3D,IAAI,UAAwD,CAAC;QAC7D,qBAAqB,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE;YACtC,UAAU,GAAG,IAAI,CAAC;YAClB,OAAO,SAAS,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,WAAW,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QAChE,MAAM,WAAW,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,CAAC,CAAC;QACtE,MAAM,SAAS,GAAG,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,qBAAqB,CACpC,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,EACrC,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,GAAG,KAAK,mBAAmB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,SAAS,CACtE,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAOH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,GAAsB;YAC3B,mBAAmB,EAAE,aAAa;YAClC,KAAK,EAAE,GAAG;SACX,CAAC;QACF,MAAM,CAAC,GAAsB;YAC3B,mBAAmB,EAAE,aAAa;YAClC,KAAK,EAAE,GAAG;SACX,CAAC;QACF,MAAM,KAAK,GAAG,0BAA0B,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;YAC/C,IAAI,EAAE,iBAAiB;SACxB,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CAAC,GAAsB;YAC3B,mBAAmB,EAAE,aAAa;YAClC,KAAK,EAAE,GAAG;SACX,CAAC;QACF,MAAM,KAAK,GAAG,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE;YAC5C,IAAI,EAAE,uBAAuB;SAC9B,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,GAAsB;YAC3B,mBAAmB,EAAE,aAAa;YAClC,KAAK,EAAE,GAAG;SACX,CAAC;QACF,MAAM,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2FAA2F,EAAE,GAAG,EAAE;QACnG,MAAM,CAAC,GAAsB;YAC3B,mBAAmB,EAAE,aAAa;YAClC,KAAK,EAAE,GAAG;SACX,CAAC;QACF,MAAM,CACJ,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CACzD,CAAC,aAAa,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Unit tests for the issuer-side delegation policy (core/delegation-policy, agent-comms#163): deciding which capability verbs an issuer never lets a bearer redelegate, and how a caller narrows that decision per bearer.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=delegation-policy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delegation-policy.test.d.ts","sourceRoot":"","sources":["../../src/test/delegation-policy.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Unit tests for the issuer-side delegation policy (core/delegation-policy, agent-comms#163): deciding which capability verbs an issuer never lets a bearer redelegate, and how a caller narrows that decision per bearer.
3
+ */
4
+ import { test, expect } from "vitest";
5
+ import { resolveDelegationsRemaining } from "../core/delegation-policy.js";
6
+ const ALICE = "alice-device-id";
7
+ const BOB = "bob-device-id";
8
+ const REQUESTED_DELEGATIONS_REMAINING = 5;
9
+ const NO_DELEGATIONS_REMAINING = 0;
10
+ test("a capability on the policy's default nonDelegable set is always minted with delegationsRemaining 0", () => {
11
+ const policy = {
12
+ nonDelegable: new Set(["room:member"]),
13
+ };
14
+ expect(resolveDelegationsRemaining(policy, "room:member", ALICE, REQUESTED_DELEGATIONS_REMAINING)).toBe(NO_DELEGATIONS_REMAINING);
15
+ });
16
+ test("a capability absent from the default nonDelegable set passes the requested value through unchanged", () => {
17
+ const policy = {
18
+ nonDelegable: new Set(["room:member"]),
19
+ };
20
+ expect(resolveDelegationsRemaining(policy, "dm:send", ALICE, REQUESTED_DELEGATIONS_REMAINING)).toBe(REQUESTED_DELEGATIONS_REMAINING);
21
+ });
22
+ test("an undefined requested value passes through unchanged for a delegable capability", () => {
23
+ const policy = {
24
+ nonDelegable: new Set(["room:member"]),
25
+ };
26
+ expect(resolveDelegationsRemaining(policy, "dm:send", ALICE, undefined)).toBeUndefined();
27
+ });
28
+ test("a per-agent override entirely replaces the default nonDelegable set for that bearer, never merges with it", () => {
29
+ const policy = {
30
+ nonDelegable: new Set(["room:member"]),
31
+ perAgentOverrides: new Map([[ALICE, new Set(["dm:send"])]]),
32
+ };
33
+ // Alice's own override names dm:send, not room:member -- so room:member, present only in the policy's default set, no longer applies to her once her own override is in effect.
34
+ expect(resolveDelegationsRemaining(policy, "room:member", ALICE, REQUESTED_DELEGATIONS_REMAINING)).toBe(REQUESTED_DELEGATIONS_REMAINING);
35
+ expect(resolveDelegationsRemaining(policy, "dm:send", ALICE, REQUESTED_DELEGATIONS_REMAINING)).toBe(NO_DELEGATIONS_REMAINING);
36
+ });
37
+ test("a per-agent override for one bearer never affects a different bearer's own resolution", () => {
38
+ const policy = {
39
+ nonDelegable: new Set(["room:member"]),
40
+ perAgentOverrides: new Map([[ALICE, new Set(["dm:send"])]]),
41
+ };
42
+ expect(resolveDelegationsRemaining(policy, "room:member", BOB, REQUESTED_DELEGATIONS_REMAINING)).toBe(NO_DELEGATIONS_REMAINING);
43
+ expect(resolveDelegationsRemaining(policy, "dm:send", BOB, REQUESTED_DELEGATIONS_REMAINING)).toBe(REQUESTED_DELEGATIONS_REMAINING);
44
+ });
45
+ test("an empty per-agent override set makes every capability delegable for that bearer", () => {
46
+ const policy = {
47
+ nonDelegable: new Set(["room:member"]),
48
+ perAgentOverrides: new Map([[ALICE, new Set()]]),
49
+ };
50
+ expect(resolveDelegationsRemaining(policy, "room:member", ALICE, REQUESTED_DELEGATIONS_REMAINING)).toBe(REQUESTED_DELEGATIONS_REMAINING);
51
+ });
52
+ //# sourceMappingURL=delegation-policy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delegation-policy.test.js","sourceRoot":"","sources":["../../src/test/delegation-policy.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=device-membership-verification.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-membership-verification.test.d.ts","sourceRoot":"","sources":["../../src/test/device-membership-verification.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Unit tests for core/device-membership-verification: the group:member capability, its scope path derivation, and verifyDeviceMembership's own obligations on top of verifyCapabilityToken (obligation-shaped the same way room-token-verification.test.ts already covers room:member -- capability match, scope kind/path match, and chain root).
3
+ */
4
+ import { webcrypto } from "node:crypto";
5
+ import { describe, it, expect } from "vitest";
6
+ import { createNodeIdentity } from "wire-mesh-core/adapters/node-identity";
7
+ import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
8
+ import { mintCapabilityToken, mintRevocationEntry, } from "wire-mesh-core/domain/tokens";
9
+ import { createRevocationView } from "wire-mesh-core/domain/revocation-view";
10
+ import { DEVICE_MEMBER_CAPABILITY, userGroupPath, verifyDeviceMembership, } from "../core/device-membership-verification.js";
11
+ const ES256 = -7;
12
+ const HOUR_MS = 3_600_000;
13
+ function buf(bytes) {
14
+ return Uint8Array.from(bytes);
15
+ }
16
+ async function generateEs256Identity() {
17
+ const keyPair = await webcrypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
18
+ const publicKeyBytes = new Uint8Array(await webcrypto.subtle.exportKey("raw", keyPair.publicKey));
19
+ return createNodeIdentity(keyPair.privateKey, publicKeyBytes, ES256);
20
+ }
21
+ function fixedClock(atMs) {
22
+ return { now: () => atMs };
23
+ }
24
+ let issuedTokenIds = 0;
25
+ function nextTokenId() {
26
+ issuedTokenIds += 1;
27
+ return buf([issuedTokenIds]);
28
+ }
29
+ const NOW_MS = 1_893_456_000_000;
30
+ const EXPIRES_MS = NOW_MS + HOUR_MS;
31
+ async function mintDeviceMemberToken(issuer, bearer, groupPath, tokenId = nextTokenId()) {
32
+ const verdict = await mintCapabilityToken({
33
+ identity: issuer,
34
+ clock: fixedClock(NOW_MS),
35
+ tokenId,
36
+ bearer: bearer.deviceId,
37
+ capability: DEVICE_MEMBER_CAPABILITY,
38
+ scope: { kind: "group", path: groupPath },
39
+ expires: EXPIRES_MS,
40
+ delegationsRemaining: 0,
41
+ });
42
+ if (!verdict.ok)
43
+ throw new Error(`mint failed: ${verdict.reason}`);
44
+ return verdict.token;
45
+ }
46
+ describe("DEVICE_MEMBER_CAPABILITY / userGroupPath", () => {
47
+ it("is the registered group:member capability string", () => {
48
+ expect(DEVICE_MEMBER_CAPABILITY).toBe("group:member");
49
+ });
50
+ it("derives the group path as the user principal's own device-id hex", async () => {
51
+ const user = await generateEs256Identity();
52
+ expect(userGroupPath(user.deviceId)).toBe(deviceIdToHex(user.deviceId));
53
+ });
54
+ });
55
+ describe("verifyDeviceMembership", () => {
56
+ it("accepts a token rooted at the user principal for that principal's own group", async () => {
57
+ const user = await generateEs256Identity();
58
+ const device = await generateEs256Identity();
59
+ const groupPath = userGroupPath(user.deviceId);
60
+ const token = await mintDeviceMemberToken(user, device, groupPath);
61
+ const verdict = await verifyDeviceMembership(token, {
62
+ identity: user,
63
+ clock: fixedClock(NOW_MS),
64
+ revocation: createRevocationView(),
65
+ expectedBearer: device.deviceId,
66
+ userDeviceId: user.deviceId,
67
+ });
68
+ expect(verdict.ok).toBe(true);
69
+ if (verdict.ok) {
70
+ expect(verdict.claims.capability).toBe(DEVICE_MEMBER_CAPABILITY);
71
+ expect(verdict.claims.scope).toEqual({ kind: "group", path: groupPath });
72
+ }
73
+ });
74
+ it("refuses a token minted by someone other than the claimed user principal", async () => {
75
+ const user = await generateEs256Identity();
76
+ const impostor = await generateEs256Identity();
77
+ const device = await generateEs256Identity();
78
+ const groupPath = userGroupPath(user.deviceId);
79
+ // Minted by the impostor, scoped to claim it is the user's own group -- rootIssuer is the impostor, not the user, and must be refused.
80
+ const token = await mintDeviceMemberToken(impostor, device, groupPath);
81
+ const verdict = await verifyDeviceMembership(token, {
82
+ identity: user,
83
+ clock: fixedClock(NOW_MS),
84
+ revocation: createRevocationView(),
85
+ expectedBearer: device.deviceId,
86
+ userDeviceId: user.deviceId,
87
+ });
88
+ expect(verdict.ok).toBe(false);
89
+ if (!verdict.ok)
90
+ expect(verdict.reason).toBe("wrong_chain_root");
91
+ });
92
+ it("refuses a token scoped to a different user's group", async () => {
93
+ const user = await generateEs256Identity();
94
+ const otherUser = await generateEs256Identity();
95
+ const device = await generateEs256Identity();
96
+ const token = await mintDeviceMemberToken(user, device, userGroupPath(otherUser.deviceId));
97
+ const verdict = await verifyDeviceMembership(token, {
98
+ identity: user,
99
+ clock: fixedClock(NOW_MS),
100
+ revocation: createRevocationView(),
101
+ expectedBearer: device.deviceId,
102
+ userDeviceId: user.deviceId,
103
+ });
104
+ expect(verdict.ok).toBe(false);
105
+ if (!verdict.ok)
106
+ expect(verdict.reason).toBe("wrong_scope_path");
107
+ });
108
+ it("refuses a token with an unrelated capability even when scope kind/path line up", async () => {
109
+ const user = await generateEs256Identity();
110
+ const device = await generateEs256Identity();
111
+ const groupPath = userGroupPath(user.deviceId);
112
+ const verdict1 = await mintCapabilityToken({
113
+ identity: user,
114
+ clock: fixedClock(NOW_MS),
115
+ tokenId: nextTokenId(),
116
+ bearer: device.deviceId,
117
+ capability: "exec:pty",
118
+ scope: { kind: "group", path: groupPath },
119
+ expires: EXPIRES_MS,
120
+ });
121
+ if (!verdict1.ok)
122
+ throw new Error(`mint failed: ${verdict1.reason}`);
123
+ const verdict = await verifyDeviceMembership(verdict1.token, {
124
+ identity: user,
125
+ clock: fixedClock(NOW_MS),
126
+ revocation: createRevocationView(),
127
+ expectedBearer: device.deviceId,
128
+ userDeviceId: user.deviceId,
129
+ });
130
+ expect(verdict.ok).toBe(false);
131
+ if (!verdict.ok)
132
+ expect(verdict.reason).toBe("wrong_capability");
133
+ });
134
+ it("refuses a token with the right capability but the wrong scope kind", async () => {
135
+ const user = await generateEs256Identity();
136
+ const device = await generateEs256Identity();
137
+ const verdict1 = await mintCapabilityToken({
138
+ identity: user,
139
+ clock: fixedClock(NOW_MS),
140
+ tokenId: nextTokenId(),
141
+ bearer: device.deviceId,
142
+ capability: DEVICE_MEMBER_CAPABILITY,
143
+ scope: { kind: "room", path: userGroupPath(user.deviceId) },
144
+ expires: EXPIRES_MS,
145
+ });
146
+ if (!verdict1.ok)
147
+ throw new Error(`mint failed: ${verdict1.reason}`);
148
+ const verdict = await verifyDeviceMembership(verdict1.token, {
149
+ identity: user,
150
+ clock: fixedClock(NOW_MS),
151
+ revocation: createRevocationView(),
152
+ expectedBearer: device.deviceId,
153
+ userDeviceId: user.deviceId,
154
+ });
155
+ expect(verdict.ok).toBe(false);
156
+ if (!verdict.ok)
157
+ expect(verdict.reason).toBe("wrong_scope_kind");
158
+ });
159
+ it("refuses a token bearing a different device than the authenticated connection", async () => {
160
+ const user = await generateEs256Identity();
161
+ const device = await generateEs256Identity();
162
+ const impostorDevice = await generateEs256Identity();
163
+ const token = await mintDeviceMemberToken(user, device, userGroupPath(user.deviceId));
164
+ const verdict = await verifyDeviceMembership(token, {
165
+ identity: user,
166
+ clock: fixedClock(NOW_MS),
167
+ revocation: createRevocationView(),
168
+ expectedBearer: impostorDevice.deviceId,
169
+ userDeviceId: user.deviceId,
170
+ });
171
+ expect(verdict.ok).toBe(false);
172
+ if (!verdict.ok)
173
+ expect(verdict.reason).toBe("bearer_mismatch");
174
+ });
175
+ it("refuses an expired token", async () => {
176
+ const user = await generateEs256Identity();
177
+ const device = await generateEs256Identity();
178
+ const token = await mintDeviceMemberToken(user, device, userGroupPath(user.deviceId));
179
+ const verdict = await verifyDeviceMembership(token, {
180
+ identity: user,
181
+ clock: fixedClock(EXPIRES_MS + HOUR_MS),
182
+ revocation: createRevocationView(),
183
+ expectedBearer: device.deviceId,
184
+ userDeviceId: user.deviceId,
185
+ });
186
+ expect(verdict.ok).toBe(false);
187
+ if (!verdict.ok)
188
+ expect(verdict.reason).toBe("expired");
189
+ });
190
+ it("refuses a revoked token", async () => {
191
+ const user = await generateEs256Identity();
192
+ const device = await generateEs256Identity();
193
+ const tokenId = nextTokenId();
194
+ const token = await mintDeviceMemberToken(user, device, userGroupPath(user.deviceId), tokenId);
195
+ const revocation = createRevocationView();
196
+ const entry = await mintRevocationEntry({
197
+ identity: user,
198
+ tokenId,
199
+ revokedAt: NOW_MS,
200
+ });
201
+ await revocation.record(entry, { identity: user });
202
+ const verdict = await verifyDeviceMembership(token, {
203
+ identity: user,
204
+ clock: fixedClock(NOW_MS),
205
+ revocation,
206
+ expectedBearer: device.deviceId,
207
+ userDeviceId: user.deviceId,
208
+ });
209
+ expect(verdict.ok).toBe(false);
210
+ if (!verdict.ok)
211
+ expect(verdict.reason).toBe("revoked");
212
+ });
213
+ });
214
+ //# sourceMappingURL=device-membership-verification.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-membership-verification.test.js","sourceRoot":"","sources":["../../src/test/device-membership-verification.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAI7E,OAAO,EACL,wBAAwB,EACxB,aAAa,EACb,sBAAsB,GACvB,MAAM,2CAA2C,CAAC;AAEnD,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC;AACjB,MAAM,OAAO,GAAG,SAAS,CAAC;AAE1B,SAAS,GAAG,CAAC,KAAqC;IAChD,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,KAAK,UAAU,qBAAqB;IAClC,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,WAAW,CAChD,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,EACtC,IAAI,EACJ,CAAC,MAAM,EAAE,QAAQ,CAAC,CACnB,CAAC;IACF,MAAM,cAAc,GAAG,IAAI,UAAU,CACnC,MAAM,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAC3D,CAAC;IACF,OAAO,kBAAkB,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,IAAI,cAAc,GAAG,CAAC,CAAC;AACvB,SAAS,WAAW;IAClB,cAAc,IAAI,CAAC,CAAC;IACpB,OAAO,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,MAAM,GAAG,iBAAiB,CAAC;AACjC,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpC,KAAK,UAAU,qBAAqB,CAClC,MAAoB,EACpB,MAAoB,EACpB,SAAiB,EACjB,UAAmC,WAAW,EAAE;IAEhD,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC;QACxC,QAAQ,EAAE,MAAM;QAChB,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;QACzB,OAAO;QACP,MAAM,EAAE,MAAM,CAAC,QAAQ;QACvB,UAAU,EAAE,wBAAwB;QACpC,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;QACzC,OAAO,EAAE,UAAU;QACnB,oBAAoB,EAAE,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnE,OAAO,OAAO,CAAC,KAAK,CAAC;AACvB,CAAC;AAED,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,wBAAwB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAEnE,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE;YAClD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;YACjE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,uIAAuI;QACvI,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAEvE,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE;YAClD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,qBAAqB,CACvC,IAAI,EACJ,MAAM,EACN,aAAa,CAAC,SAAS,CAAC,QAAQ,CAAC,CAClC,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE;YAClD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gFAAgF,EAAE,KAAK,IAAI,EAAE;QAC9F,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC;YACzC,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,OAAO,EAAE,WAAW,EAAE;YACtB,MAAM,EAAE,MAAM,CAAC,QAAQ;YACvB,UAAU,EAAE,UAAU;YACtB,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;YACzC,OAAO,EAAE,UAAU;SACpB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAErE,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC3D,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC;YACzC,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,OAAO,EAAE,WAAW,EAAE;YACtB,MAAM,EAAE,MAAM,CAAC,QAAQ;YACvB,UAAU,EAAE,wBAAwB;YACpC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;YAC3D,OAAO,EAAE,UAAU;SACpB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAErE,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,QAAQ,CAAC,KAAK,EAAE;YAC3D,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;QAC5F,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,cAAc,GAAG,MAAM,qBAAqB,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,MAAM,qBAAqB,CACvC,IAAI,EACJ,MAAM,EACN,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAC7B,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE;YAClD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,cAAc,CAAC,QAAQ;YACvC,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;QACxC,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,qBAAqB,CACvC,IAAI,EACJ,MAAM,EACN,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAC7B,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE;YAClD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,UAAU,GAAG,OAAO,CAAC;YACvC,UAAU,EAAE,oBAAoB,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,KAAK,IAAI,EAAE;QACvC,MAAM,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,qBAAqB,EAAE,CAAC;QAC7C,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,MAAM,qBAAqB,CACvC,IAAI,EACJ,MAAM,EACN,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAC5B,OAAO,CACR,CAAC;QAEF,MAAM,UAAU,GAAG,oBAAoB,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC;YACtC,QAAQ,EAAE,IAAI;YACd,OAAO;YACP,SAAS,EAAE,MAAM;SAClB,CAAC,CAAC;QACH,MAAM,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAEnD,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE;YAClD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC;YACzB,UAAU;YACV,cAAc,EAAE,MAAM,CAAC,QAAQ;YAC/B,YAAY,EAAE,IAAI,CAAC,QAAQ;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=device-membership.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-membership.test.d.ts","sourceRoot":"","sources":["../../src/test/device-membership.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Unit tests for core/device-membership: admitDevice (mint + issued-grant bookkeeping) and removeDevice (revoke + forget the issued-grant record), the mint/revoke half of agent-comms#161 -- verifyDeviceMembership itself is covered separately in device-membership-verification.test.ts.
3
+ */
4
+ import * as fs from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import path from "node:path";
7
+ import { describe, it, expect } from "vitest";
8
+ import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
9
+ import { createSystemClock } from "wire-mesh-core/adapters/system-clock";
10
+ import { createRevocationView } from "wire-mesh-core/domain/revocation-view";
11
+ import { generateIdentity } from "../core/identity.js";
12
+ import { toIdentityPort } from "../core/wire-mesh-identity.js";
13
+ import { randomId } from "../core/random-id.js";
14
+ import { loadIssuedDeviceGrant, loadOrCreateUserIdentity, } from "../core/user-identity.js";
15
+ import { admitDevice, removeDevice } from "../core/device-membership.js";
16
+ import { DEVICE_MEMBER_CAPABILITY, userGroupPath, verifyDeviceMembership, } from "../core/device-membership-verification.js";
17
+ const TOKEN_TTL_MS = 60_000;
18
+ function tempDir() {
19
+ return fs.mkdtempSync(path.join(tmpdir(), "agent-comms-device-membership-test-"));
20
+ }
21
+ /** A real, persisted user-principal identity (not a bare in-memory keypair): admitDevice/removeDevice write through to the same user-identity.json this identity's own key material lives in, so the fixture must actually create that file first, the same way a real bridge process would via loadOrCreateUserIdentity. */
22
+ async function makeUser() {
23
+ const dir = tempDir();
24
+ const identity = await toIdentityPort(loadOrCreateUserIdentity({ dir }));
25
+ return { identity, userIdentityOptions: { dir } };
26
+ }
27
+ describe("admitDevice", () => {
28
+ it("mints a group:member token bearing the device, rooted at the user principal", async () => {
29
+ const { identity: user, userIdentityOptions } = await makeUser();
30
+ const device = await toIdentityPort(generateIdentity());
31
+ const clock = createSystemClock();
32
+ const verdict = await admitDevice({
33
+ userIdentity: user,
34
+ userIdentityOptions,
35
+ clock,
36
+ tokenId: randomId(),
37
+ deviceId: device.deviceId,
38
+ expires: clock.now() + TOKEN_TTL_MS,
39
+ });
40
+ expect(verdict.ok, `expected mint to succeed, got ${JSON.stringify(verdict)}`).toBe(true);
41
+ if (!verdict.ok)
42
+ return;
43
+ const memberVerdict = await verifyDeviceMembership(verdict.token, {
44
+ identity: user,
45
+ clock,
46
+ revocation: createRevocationView(),
47
+ expectedBearer: device.deviceId,
48
+ userDeviceId: user.deviceId,
49
+ });
50
+ expect(memberVerdict.ok).toBe(true);
51
+ });
52
+ it("records the issued grant's token-id under the user principal's own store, keyed by device hex", async () => {
53
+ const { identity: user, userIdentityOptions } = await makeUser();
54
+ const device = await toIdentityPort(generateIdentity());
55
+ const clock = createSystemClock();
56
+ const tokenId = randomId();
57
+ const verdict = await admitDevice({
58
+ userIdentity: user,
59
+ userIdentityOptions,
60
+ clock,
61
+ tokenId,
62
+ deviceId: device.deviceId,
63
+ expires: clock.now() + TOKEN_TTL_MS,
64
+ });
65
+ expect(verdict.ok).toBe(true);
66
+ const deviceHex = deviceIdToHex(device.deviceId);
67
+ expect(loadIssuedDeviceGrant(userIdentityOptions, deviceHex)).toEqual(tokenId);
68
+ });
69
+ it("does not record an issued grant when the mint itself refuses", async () => {
70
+ const { identity: user, userIdentityOptions } = await makeUser();
71
+ const device = await toIdentityPort(generateIdentity());
72
+ const clock = createSystemClock();
73
+ const verdict = await admitDevice({
74
+ userIdentity: user,
75
+ userIdentityOptions,
76
+ clock,
77
+ tokenId: randomId(),
78
+ deviceId: device.deviceId,
79
+ // Already expired -- mintCapabilityToken refuses outright.
80
+ expires: clock.now() - 1,
81
+ });
82
+ expect(verdict.ok).toBe(false);
83
+ const deviceHex = deviceIdToHex(device.deviceId);
84
+ expect(loadIssuedDeviceGrant(userIdentityOptions, deviceHex)).toBeUndefined();
85
+ });
86
+ });
87
+ describe("removeDevice", () => {
88
+ it("does nothing and returns undefined when this principal never admitted the device", async () => {
89
+ const { identity: user, userIdentityOptions } = await makeUser();
90
+ const device = await toIdentityPort(generateIdentity());
91
+ const clock = createSystemClock();
92
+ const entry = await removeDevice({
93
+ userIdentity: user,
94
+ userIdentityOptions,
95
+ clock,
96
+ revocation: createRevocationView(),
97
+ deviceId: device.deviceId,
98
+ });
99
+ expect(entry).toBeUndefined();
100
+ });
101
+ it("revokes an admitted device's own grant, records it locally, and forgets the issued-grant record", async () => {
102
+ const { identity: user, userIdentityOptions } = await makeUser();
103
+ const device = await toIdentityPort(generateIdentity());
104
+ const clock = createSystemClock();
105
+ const admitVerdict = await admitDevice({
106
+ userIdentity: user,
107
+ userIdentityOptions,
108
+ clock,
109
+ tokenId: randomId(),
110
+ deviceId: device.deviceId,
111
+ expires: clock.now() + TOKEN_TTL_MS,
112
+ });
113
+ if (!admitVerdict.ok)
114
+ throw new Error("expected admission to succeed");
115
+ const revocation = createRevocationView();
116
+ const entry = await removeDevice({
117
+ userIdentity: user,
118
+ userIdentityOptions,
119
+ clock,
120
+ revocation,
121
+ deviceId: device.deviceId,
122
+ });
123
+ expect(entry).not.toBeUndefined();
124
+ const deviceHex = deviceIdToHex(device.deviceId);
125
+ expect(loadIssuedDeviceGrant(userIdentityOptions, deviceHex)).toBeUndefined();
126
+ const verdict = await verifyDeviceMembership(admitVerdict.token, {
127
+ identity: user,
128
+ clock,
129
+ revocation,
130
+ expectedBearer: device.deviceId,
131
+ userDeviceId: user.deviceId,
132
+ });
133
+ expect(verdict.ok).toBe(false);
134
+ if (!verdict.ok)
135
+ expect(verdict.reason).toBe("revoked");
136
+ });
137
+ it("re-admitting after removal mints and records a genuinely fresh grant", async () => {
138
+ const { identity: user, userIdentityOptions } = await makeUser();
139
+ const device = await toIdentityPort(generateIdentity());
140
+ const clock = createSystemClock();
141
+ const first = await admitDevice({
142
+ userIdentity: user,
143
+ userIdentityOptions,
144
+ clock,
145
+ tokenId: randomId(),
146
+ deviceId: device.deviceId,
147
+ expires: clock.now() + TOKEN_TTL_MS,
148
+ });
149
+ if (!first.ok)
150
+ throw new Error("expected first admission to succeed");
151
+ const revocation = createRevocationView();
152
+ await removeDevice({
153
+ userIdentity: user,
154
+ userIdentityOptions,
155
+ clock,
156
+ revocation,
157
+ deviceId: device.deviceId,
158
+ });
159
+ const second = await admitDevice({
160
+ userIdentity: user,
161
+ userIdentityOptions,
162
+ clock,
163
+ tokenId: randomId(),
164
+ deviceId: device.deviceId,
165
+ expires: clock.now() + TOKEN_TTL_MS,
166
+ });
167
+ if (!second.ok)
168
+ throw new Error("expected re-admission to succeed");
169
+ const verdict = await verifyDeviceMembership(second.token, {
170
+ identity: user,
171
+ clock,
172
+ revocation,
173
+ expectedBearer: device.deviceId,
174
+ userDeviceId: user.deviceId,
175
+ });
176
+ expect(verdict.ok).toBe(true);
177
+ });
178
+ });
179
+ describe("userGroupPath / DEVICE_MEMBER_CAPABILITY re-exports", () => {
180
+ it("are usable together to compute the scope a real admission grant carries", async () => {
181
+ const { identity: user, userIdentityOptions } = await makeUser();
182
+ const device = await toIdentityPort(generateIdentity());
183
+ const clock = createSystemClock();
184
+ const verdict = await admitDevice({
185
+ userIdentity: user,
186
+ userIdentityOptions,
187
+ clock,
188
+ tokenId: randomId(),
189
+ deviceId: device.deviceId,
190
+ expires: clock.now() + TOKEN_TTL_MS,
191
+ });
192
+ if (!verdict.ok)
193
+ throw new Error("expected admission to succeed");
194
+ const [, , payload] = verdict.token;
195
+ expect(payload).not.toBeNull();
196
+ // The token's own scope is not re-decoded here (that is verifyDeviceMembership's job, exercised above); this asserts admitDevice used the module's own exported helpers rather than a second, independently-typed copy of the same capability/scope shape.
197
+ expect(DEVICE_MEMBER_CAPABILITY).toBe("group:member");
198
+ expect(userGroupPath(user.deviceId)).toBe(deviceIdToHex(user.deviceId));
199
+ });
200
+ });
201
+ //# sourceMappingURL=device-membership.test.js.map