@lanes-sh/link 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanes-sh/link",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "A self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://lanes.sh/link",
@@ -138,6 +138,57 @@ export function connectionsSharingCredential(
138
138
  .map((one) => `${one.provider}.${one.id}`);
139
139
  }
140
140
 
141
+ /**
142
+ * Take back the allow rules that named the provider, once nothing declares it.
143
+ *
144
+ * Not a tidy-up. `assertReferentialIntegrity` refuses an allow rule naming a
145
+ * provider with no connection, and `save` validates — so disconnecting the last
146
+ * connection of a provider *failed*, with a config error about a policy line the
147
+ * operator had not touched, and nothing removed. Every single-account provider
148
+ * was undisconnectable, which is most of them: the bug reproduced on `slack.*`
149
+ * and would have on `bunq.*`, while a profile with two Gmail accounts could
150
+ * disconnect one perfectly well.
151
+ *
152
+ * Symmetry is the argument for removing rather than warning. `connect` writes
153
+ * the row *and* the rule, and the pair is what `config-repair.ts` calls both
154
+ * halves or neither — a rule with nothing behind it grants nothing and is what
155
+ * that file exists to stop being written.
156
+ *
157
+ * `allow: ['*']` is untouched: it names no provider, so nothing about it becomes
158
+ * false. Narrower rules go with the wide one — `gmail.send_message` is as
159
+ * dangling as `gmail.*` once the last Gmail is gone, and the loader refuses it
160
+ * for the same reason.
161
+ *
162
+ * `deny` is left alone, deliberately. The loader permits a deny naming a
163
+ * provider with no connection, because denying something you have not connected
164
+ * yet is a reasonable thing to write ahead of time — and removing it here would
165
+ * silently re-permit whatever it covered if the account came back.
166
+ */
167
+ function dropProviderRules(document: ConfigDocument, config: Config, index: number): void {
168
+ const going = config.connections[index];
169
+ if (!going) return;
170
+
171
+ const stillDeclared = config.connections.some(
172
+ (one, i) => i !== index && one.provider === going.provider,
173
+ );
174
+ if (stillDeclared) return;
175
+
176
+ const rules = document.getIn(['policy', 'allow']) as { items?: unknown[] } | null;
177
+ const doomed: number[] = [];
178
+
179
+ (rules?.items ?? []).forEach((_rule, at) => {
180
+ const bare = document.getIn(['policy', 'allow', at]);
181
+ const capability =
182
+ typeof bare === 'string' ? bare : document.getIn(['policy', 'allow', at, 'capability']);
183
+ if (typeof capability !== 'string') return;
184
+
185
+ if (capability.split('.')[0] === going.provider) doomed.push(at);
186
+ });
187
+
188
+ // Descending, so each removal cannot move the index of one still to come.
189
+ for (const at of doomed.reverse()) document.removeFrom(['policy', 'allow'], at);
190
+ }
191
+
141
192
  export async function removeConnection(
142
193
  key: string,
143
194
  flags: DisconnectFlags,
@@ -162,6 +213,10 @@ export async function removeConnection(
162
213
  // running `connect`. The reverse — credential gone, declaration kept, edit
163
214
  // failed — is the same state, so ordering costs nothing either way; doing the
164
215
  // edit first means the file is right even if the store is unreachable.
216
+ // Both edits before the one `save`, so the file is never written in the
217
+ // state where the row is gone and the rule that named it is not — which is
218
+ // the state the loader refuses.
219
+ dropProviderRules(document, config, located.index);
165
220
  document.removeFrom(['connections'], located.index);
166
221
  await document.save();
167
222