@push.rocks/smartnftables 2.0.0 → 2.2.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.
package/changelog.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-15 - 2.2.0
4
+
5
+ ### Features
6
+
7
+ - Add exact transition release with full boot and namespace binding, terminal retry and joined cleanup, including recovery after policy interfaces disappear.
8
+
9
+ ## 2026-09-15 - 2.1.0
10
+
11
+ ### Features
12
+
13
+ - Expose a retained failure signal for unexpected policy process loss, ambiguous operations and unconfirmed cleanup while preserving exact recovery and retention semantics.
14
+
3
15
  ## 2026-09-14 - 2.0.0
4
16
 
5
17
  ### Breaking Changes
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "f87173a416bf8ff6aad2d59dcb8534bbc720325b51a78134ed84c758f1944881",
3
+ "binarySha256": "d997150e9cde38e1ba2c93c5b894c367f9b562a31291ee568ccd1e4a54034b61",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "2.0.0",
7
- "gitCommit": "baace9c25621df66a88552dcd1d2f5e63c3e477f",
6
+ "projectVersion": "2.2.0",
7
+ "gitCommit": "32e9f40066053a15f32031e796e3151140e98eca",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-14T22:07:32.392Z",
9
+ "builtAt": "2026-09-15T06:33:44.972Z",
10
10
  "tsrustVersion": "1.13.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_amd64_musl"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "206ed227d6c78079f1240f3a70fd2cf305258e84682b2affd97b92fcf3509b5a",
3
+ "binarySha256": "14a625bdb893667872dad6b339aa77f64c6d7e538158bff3e4b92e5684d70e61",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "2.0.0",
7
- "gitCommit": "baace9c25621df66a88552dcd1d2f5e63c3e477f",
6
+ "projectVersion": "2.2.0",
7
+ "gitCommit": "32e9f40066053a15f32031e796e3151140e98eca",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-09-14T22:07:32.519Z",
9
+ "builtAt": "2026-09-15T06:33:49.548Z",
10
10
  "tsrustVersion": "1.13.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_arm64_musl"
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '2.0.0',
6
+ version: '2.2.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLG1IQUFtSDtDQUNqSSxDQUFBIn0=
@@ -1,4 +1,4 @@
1
- import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy, IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftPolicy } from './managed.types.js';
1
+ import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy, IPreparedManagedNftPolicy, IManagedNftStatus, IManagedNftTransitionRelease, TManagedNftPolicy } from './managed.types.js';
2
2
  export declare class ManagedNftablesError extends Error {
3
3
  readonly code: string;
4
4
  constructor(code: string);
@@ -10,6 +10,10 @@ export declare class ManagedNftables<TPolicy extends TManagedNftPolicy = IManage
10
10
  #private;
11
11
  constructor(options: IManagedNftOptions);
12
12
  get state(): string;
13
+ /** Retained loss of owner confidence, including idle process death. It never
14
+ * resets or proves withdrawal/deletion; explicit recovery and joined close
15
+ * remain the caller's responsibility. Orderly close does not abort it. */
16
+ get failureSignal(): AbortSignal;
13
17
  /** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
14
18
  start(): Promise<IManagedNftStatus<TPolicy>>;
15
19
  prepare(policy: TPolicy): Promise<IPreparedManagedNftPolicy<TPolicy>>;
@@ -18,6 +22,14 @@ export declare class ManagedNftables<TPolicy extends TManagedNftPolicy = IManage
18
22
  release(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{
19
23
  released: true;
20
24
  }>;
25
+ /** Terminal release when an apply acknowledgement was lost. Retain the full
26
+ * original identity and transition first. The native owner accepts only an
27
+ * absent table or the exact previous/target graph; it never recreates policy
28
+ * or requires surviving interfaces. Exact retries and inspection remain
29
+ * available; close() retries admitted cleanup, closeRetaining() only joins. */
30
+ releaseTransition(request: IManagedNftTransitionRelease<TPolicy>): Promise<{
31
+ released: true;
32
+ }>;
21
33
  /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
22
34
  * joins the native process. Retain the full applied value before this call.
23
35
  * After any failure close() only terminates; it never deletes the table. */
@@ -67,6 +67,9 @@ export class ManagedNftables {
67
67
  #retaining = false;
68
68
  #detachExpected = null;
69
69
  #detach = null;
70
+ #transitionRelease = null;
71
+ #failure = new AbortController();
72
+ #processLost = false;
70
73
  constructor(options) {
71
74
  this.#options = snapshot(options);
72
75
  if (Object.keys(this.#options).some((key) => !['ownerId', 'instanceId', 'tableName', 'binaryPath', 'networkNamespaceFd'].includes(key))
@@ -81,6 +84,16 @@ export class ManagedNftables {
81
84
  throw new ManagedNftablesError('INVALID');
82
85
  }
83
86
  get state() { return this.#state; }
87
+ /** Retained loss of owner confidence, including idle process death. It never
88
+ * resets or proves withdrawal/deletion; explicit recovery and joined close
89
+ * remain the caller's responsibility. Orderly close does not abort it. */
90
+ get failureSignal() { return this.#failure.signal; }
91
+ #fail() {
92
+ if (this.#state !== 'closing')
93
+ this.#state = 'failed-owned';
94
+ if (!this.#failure.signal.aborted)
95
+ this.#failure.abort(new ManagedNftablesError('OWNER_LOST'));
96
+ }
84
97
  /** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
85
98
  start() {
86
99
  if (this.#state !== 'new' || this.#retaining)
@@ -97,11 +110,12 @@ export class ManagedNftables {
97
110
  cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
98
111
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
99
112
  readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
100
- maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, detachPolicy: 1, closeOwner: 1 },
113
+ maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, releasePolicyTransition: 1, detachPolicy: 1, closeOwner: 1 },
101
114
  });
102
115
  this.#bridge.on('exit', () => {
116
+ this.#processLost = true;
103
117
  if (!['closing', 'stopped'].includes(this.#state))
104
- this.#state = 'failed-owned';
118
+ this.#fail();
105
119
  });
106
120
  if (!await this.#bridge.spawn())
107
121
  throw new ManagedNftablesError('UNAVAILABLE');
@@ -112,7 +126,7 @@ export class ManagedNftables {
112
126
  this.#state = 'ready';
113
127
  return status;
114
128
  })();
115
- return this.#track(work);
129
+ return this.#track(work, 'openOwner');
116
130
  }
117
131
  prepare(policy) {
118
132
  return this.#invoke('preparePolicy', policy);
@@ -124,11 +138,19 @@ export class ManagedNftables {
124
138
  release(expected) {
125
139
  return this.#invoke('releasePolicy', expected);
126
140
  }
141
+ /** Terminal release when an apply acknowledgement was lost. Retain the full
142
+ * original identity and transition first. The native owner accepts only an
143
+ * absent table or the exact previous/target graph; it never recreates policy
144
+ * or requires surviving interfaces. Exact retries and inspection remain
145
+ * available; close() retries admitted cleanup, closeRetaining() only joins. */
146
+ releaseTransition(request) {
147
+ return this.#invoke('releasePolicyTransition', request);
148
+ }
127
149
  /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
128
150
  * joins the native process. Retain the full applied value before this call.
129
151
  * After any failure close() only terminates; it never deletes the table. */
130
152
  detach(expected) {
131
- if (this.#closeRetainingSelected || this.#closeMode === 'delete')
153
+ if (this.#closeRetainingSelected || this.#closeMode === 'delete' || this.#transitionRelease)
132
154
  return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
133
155
  this.#retaining = true;
134
156
  let captured;
@@ -151,7 +173,7 @@ export class ManagedNftables {
151
173
  throw new ManagedNftablesError('PROTOCOL');
152
174
  this.#nativeMayOwn = false;
153
175
  return result;
154
- }));
176
+ }), 'detachPolicy');
155
177
  const detached = (async () => { const result = await work; await this.close(); return result; })();
156
178
  this.#detach = detached;
157
179
  void detached.catch(() => { this.#detach = null; });
@@ -183,8 +205,13 @@ export class ManagedNftables {
183
205
  await this.#work?.catch(() => undefined);
184
206
  if (this.#bridge) {
185
207
  if (this.#nativeMayOwn && !retaining) {
186
- const result = await this.#bridge.sendCommand('closeOwner', {});
187
- if (result.released !== true)
208
+ // Even native validation may have rejected before selecting its own
209
+ // terminal intent. Retry this exact request; generic close must not
210
+ // delete an earlier applied graph outside the requested transition.
211
+ const result = this.#transitionRelease
212
+ ? await this.#bridge.sendCommand('releasePolicyTransition', this.#transitionRelease.request)
213
+ : await this.#bridge.sendCommand('closeOwner', {});
214
+ if (result.released !== true || this.#transitionRelease && Object.keys(result).length !== 1)
188
215
  throw new ManagedNftablesError('PROTOCOL');
189
216
  }
190
217
  await this.#bridge.terminate({ gracePeriodMs: 1000 });
@@ -194,6 +221,7 @@ export class ManagedNftables {
194
221
  })().catch(() => {
195
222
  this.#state = 'failed-owned';
196
223
  this.#close = null;
224
+ this.#fail();
197
225
  throw new ManagedNftablesError('CLEANUP_UNCONFIRMED');
198
226
  });
199
227
  return this.#close;
@@ -208,23 +236,48 @@ export class ManagedNftables {
208
236
  catch {
209
237
  return Promise.reject(new ManagedNftablesError('INVALID'));
210
238
  }
211
- if (method === 'reconcilePolicy' || method === 'releasePolicy')
239
+ if (this.#transitionRelease && (method !== 'inspectPolicy' && method !== 'releasePolicyTransition'
240
+ || method === 'releasePolicyTransition' && !plugins.util.isDeepStrictEqual(this.#transitionRelease.request, captured))) {
241
+ return Promise.reject(new ManagedNftablesError('CONFLICT'));
242
+ }
243
+ if (method === 'releasePolicyTransition')
244
+ this.#transitionRelease = { request: captured };
245
+ if (method === 'reconcilePolicy' || method === 'releasePolicy' || method === 'releasePolicyTransition')
212
246
  this.#nativeMayOwn = true;
213
247
  return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
214
248
  const capturedResult = snapshot(result);
215
- if (method === 'releasePolicy')
249
+ if (method === 'releasePolicy' || method === 'releasePolicyTransition') {
250
+ if (method === 'releasePolicyTransition' && (capturedResult?.released !== true
251
+ || Object.keys(capturedResult).length !== 1))
252
+ throw new ManagedNftablesError('PROTOCOL');
216
253
  this.#nativeMayOwn = false;
217
- if (this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy'
218
- || method === 'inspectPolicy' && capturedResult.error === null))
254
+ }
255
+ if (method === 'inspectPolicy') {
256
+ const status = capturedResult;
257
+ if (status.state === 'failed-owned' || status.error !== null)
258
+ this.#fail();
259
+ }
260
+ if (!this.#processLost && this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy' || method === 'releasePolicyTransition'
261
+ || method === 'inspectPolicy' && capturedResult.error === null
262
+ && capturedResult.state !== 'failed-owned'))
219
263
  this.#state = 'ready';
220
264
  return capturedResult;
221
- }));
265
+ }), method);
222
266
  }
223
- #track(work) {
267
+ #track(work, method) {
224
268
  const owned = work.catch((error) => {
225
269
  if (this.#state !== 'closing')
226
270
  this.#state = 'failed-owned';
227
271
  const code = error instanceof plugins.smartrust.RustBridgeRequestError ? error.responseErrorCode : undefined;
272
+ // Only a native compiler rejection or explicit INVALID response proves
273
+ // effect-free validation. Other native errors can follow lost ownership
274
+ // or admitted kernel work. Recovery stays available; confidence is lost.
275
+ const validation = error instanceof plugins.smartrust.RustBridgeRequestError
276
+ && error.code === 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE'
277
+ && (method === 'preparePolicy' || code === 'INVALID');
278
+ if (!validation && (this.#state !== 'closing'
279
+ || ['reconcilePolicy', 'releasePolicy', 'releasePolicyTransition', 'detachPolicy'].includes(method)))
280
+ this.#fail();
228
281
  throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
229
282
  });
230
283
  this.#work = owned;
@@ -232,4 +285,4 @@ export class ManagedNftables {
232
285
  return owned;
233
286
  }
234
287
  }
235
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5tYW5hZ2VkbmZ0YWJsZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLm1hbmFnZWRuZnRhYmxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUl4QyxNQUFNLE9BQU8sb0JBQXFCLFNBQVEsS0FBSztJQUM3QyxZQUE0QixJQUFZO1FBQUksS0FBSyxDQUFDLG9CQUFvQixJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBQW5ELFNBQUksR0FBSixJQUFJLENBQVE7SUFBd0MsQ0FBQztDQUNsRjtBQUVELG9GQUFvRjtBQUNwRixNQUFNLFVBQVUsUUFBUSxDQUFJLEtBQVE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBQ3BCLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBYSxFQUFVLEVBQUU7UUFDdkMsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLE9BQU8sSUFBSSxDQUFDLFdBQVcsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsT0FBTztZQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUM3SCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUMsQ0FBQztJQUNGLE1BQU0sSUFBSSxHQUFHLENBQUMsS0FBYyxFQUFFLEtBQWEsRUFBVyxFQUFFO1FBQ3RELElBQUksRUFBRSxLQUFLLEdBQUcsTUFBTSxJQUFJLEtBQUssR0FBRyxFQUFFO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzlFLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUFFLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3BELElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxTQUFTO1lBQUUsT0FBTyxL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
288
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5tYW5hZ2VkbmZ0YWJsZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9jbGFzc2VzLm1hbmFnZWRuZnRhYmxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUl4QyxNQUFNLE9BQU8sb0JBQXFCLFNBQVEsS0FBSztJQUM3QyxZQUE0QixJQUFZO1FBQUksS0FBSyxDQUFDLG9CQUFvQixJQUFJLEdBQUcsQ0FBQyxDQUFDO1FBQW5ELFNBQUksR0FBSixJQUFJLENBQVE7SUFBd0MsQ0FBQztDQUNsRjtBQUVELG9GQUFvRjtBQUNwRixNQUFNLFVBQVUsUUFBUSxDQUFJLEtBQVE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2QsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBQ3BCLE1BQU0sTUFBTSxHQUFHLENBQUMsS0FBYSxFQUFVLEVBQUU7UUFDdkMsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLE9BQU8sSUFBSSxDQUFDLFdBQVcsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsT0FBTztZQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUM3SCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUMsQ0FBQztJQUNGLE1BQU0sSUFBSSxHQUFHLENBQUMsS0FBYyxFQUFFLEtBQWEsRUFBVyxFQUFFO1FBQ3RELElBQUksRUFBRSxLQUFLLEdBQUcsTUFBTSxJQUFJLEtBQUssR0FBRyxFQUFFO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzlFLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtZQUFFLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3BELElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxTQUFTO1lBQUUsT0FBTyxLQUFLLENBQUM7UUFDL0QsSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLGFBQWEsQ0FBQyxLQUFLLENBQUM7WUFBRSxPQUFPLEtBQUssQ0FBQztRQUMzRSxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzlHLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbkMsSUFBSSxNQUFNLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzdILE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDcEMsSUFBSSxJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sR0FBRyxLQUFLO1lBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQzVFLEtBQUssTUFBTSxHQUFHLElBQUksSUFBSTtZQUFFLElBQUksT0FBTyxHQUFHLEtBQUssUUFBUTtnQkFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDakUsTUFBTSxXQUFXLEdBQUcsTUFBTSxDQUFDLHlCQUF5QixDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzVELE1BQU0sTUFBTSxHQUF3QyxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQ3BFLElBQUksS0FBSyxJQUFJLElBQUksQ0FBQyxNQUFNLEtBQU0sS0FBbUIsQ0FBQyxNQUFNLEdBQUcsQ0FBQztZQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUN4RyxLQUFLLE1BQU0sR0FBRyxJQUFJLElBQUksRUFBRSxDQUFDO1lBQ3ZCLElBQUksS0FBSyxJQUFJLEdBQUcsS0FBSyxRQUFRO2dCQUFFLFNBQVM7WUFDeEMsTUFBTSxVQUFVLEdBQUcsV0FBVyxDQUFDLEdBQWEsQ0FBQyxDQUFDO1lBQzlDLElBQUksT0FBTyxHQUFHLEtBQUssUUFBUSxJQUFJLENBQUMsVUFBVSxDQUFDLFVBQVUsSUFBSSxDQUFDLENBQUMsT0FBTyxJQUFJLFVBQVUsQ0FBQzttQkFDNUUsS0FBSyxJQUFJLENBQUMsbUJBQW1CLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztnQkFBRSxNQUFNLElBQUksb0JBQW9CLENBQUMsU0FBUyxDQUFDLENBQUM7WUFDeEYsTUFBTSxDQUFDLGNBQWMsQ0FBQyxNQUFNLEVBQUUsR0FBRyxFQUFFLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxVQUFVLENBQUMsS0FBSyxFQUFFLEtBQUssR0FBRyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLFlBQVksRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDekksQ0FBQztRQUNELE9BQU8sTUFBTSxDQUFDO0lBQ2hCLENBQUMsQ0FBQztJQUNGLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFNLENBQUM7SUFDbkMsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQUMsR0FBRyxPQUFPO1FBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ25HLE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUM7QUFFRCxzR0FBc0c7QUFDdEcsTUFBTSxPQUFPLGVBQWU7SUFDakIsUUFBUSxDQUErQjtJQUNoRCxPQUFPLEdBQXNFLElBQUksQ0FBQztJQUNsRixNQUFNLEdBQTBFLEtBQUssQ0FBQztJQUN0RixLQUFLLEdBQTRCLElBQUksQ0FBQztJQUN0QyxNQUFNLEdBQXlCLElBQUksQ0FBQztJQUNwQyxVQUFVLEdBQStCLElBQUksQ0FBQztJQUM5Qyx1QkFBdUIsR0FBRyxLQUFLLENBQUM7SUFDaEMsYUFBYSxHQUFHLEtBQUssQ0FBQztJQUN0QixVQUFVLEdBQUcsS0FBSyxDQUFDO0lBQ25CLGVBQWUsR0FBNkMsSUFBSSxDQUFDO0lBQ2pFLE9BQU8sR0FBdUMsSUFBSSxDQUFDO0lBQ25ELGtCQUFrQixHQUE4RCxJQUFJLENBQUM7SUFDNUUsUUFBUSxHQUFHLElBQUksZUFBZSxFQUFFLENBQUM7SUFDMUMsWUFBWSxHQUFHLEtBQUssQ0FBQztJQUVyQixZQUFZLE9BQTJCO1FBQ3JDLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2xDLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUyxFQUFFLFlBQVksRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixDQUFDLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDO2VBQ2xJLENBQUMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVUsRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLHVCQUF1QixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztlQUM5SixJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxNQUFNLEdBQUcsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQztlQUNwRixJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVUsS0FBSyxTQUFTLElBQUksQ0FBQyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQUUsQ0FBQztZQUMvSSxNQUFNLElBQUksb0JBQW9CLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDNUMsQ0FBQztRQUNELElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxrQkFBa0IsS0FBSyxTQUFTLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxLQUFLLE9BQU87ZUFDOUUsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLGtCQUFrQixHQUFHLENBQUM7ZUFDM0YsSUFBSSxDQUFDLFFBQVEsQ0FBQyxrQkFBa0IsR0FBRyxXQUFXLENBQUM7WUFBRSxNQUFNLElBQUksb0JBQW9CLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDbEcsQ0FBQztJQUVELElBQVcsS0FBSyxLQUFhLE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFFbEQ7OzhFQUUwRTtJQUMxRSxJQUFXLGFBQWEsS0FBa0IsT0FBTyxJQUFJLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFFeEUsS0FBSztRQUNILElBQUksSUFBSSxDQUFDLE1BQU0sS0FBSyxTQUFTO1lBQUUsSUFBSSxDQUFDLE1BQU0sR0FBRyxjQUFjLENBQUM7UUFDNUQsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLE9BQU87WUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLG9CQUFvQixDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFDakcsQ0FBQztJQUVELHVGQUF1RjtJQUNoRixLQUFLO1FBQ1YsSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLEtBQUssSUFBSSxJQUFJLENBQUMsVUFBVTtZQUFFLE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FBQyxJQUFJLG9CQUFvQixDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUM7UUFDN0csSUFBSSxDQUFDLE1BQU0sR0FBRyxVQUFVLENBQUM7UUFDekIsTUFBTSxJQUFJLEdBQUcsQ0FBQyxLQUFLLElBQUksRUFBRTtZQUN2QixJQUFJLE9BQU8sQ0FBQyxRQUFRLEtBQUssT0FBTyxJQUFJLENBQUMsQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUM7Z0JBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLHNCQUFzQixDQUFDLENBQUM7WUFDckksTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsSUFBSSxHQUFHLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztZQUN4RSxJQUFJLENBQUMsT0FBTyxHQUFHLElBQUksT0FBTyxDQUFDLFNBQVMsQ0FBQyxVQUFVLENBQStCO2dCQUM1RSxVQUFVLEVBQUUsZUFBZSxFQUFFLFVBQVUsRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVU7dUJBQzVELE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxXQUFXLEVBQUUsdUJBQXVCLE9BQU8sQ0FBQyxJQUFJLEtBQUssS0FBSyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE9BQU8sT0FBTyxDQUFDO2dCQUNuSCxVQUFVLEVBQUUsRUFBRSxFQUFFLGdCQUFnQixFQUFFLEtBQUs7Z0JBQ3ZDLE9BQU8sRUFBRSxDQUFDLGNBQWMsRUFBRSxHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxrQkFBa0IsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyx3QkFBd0IsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDO2dCQUNySCx3QkFBd0IsRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLGtCQUFrQixLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQUM7Z0JBQ3pILGNBQWMsRUFBRSxJQUFJLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxFQUFFLGNBQWMsRUFBRSxPQUFPO2dCQUN2RSwwQkFBMEIsRUFBRSxFQUFFLGFBQWEsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFLENBQUMsRUFBRSxlQUFlLEVBQUUsQ0FBQyxFQUFFLGFBQWEsRUFBRSxDQUFDLEVBQUUsYUFBYSxFQUFFLENBQUMsRUFBRSx1QkFBdUIsRUFBRSxDQUFDLEVBQUUsWUFBWSxFQUFFLENBQUMsRUFBRSxVQUFVLEVBQUUsQ0FBQyxFQUFFO2FBQ25MLENBQUMsQ0FBQztZQUNILElBQUksQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxHQUFHLEVBQUU7Z0JBQzNCLElBQUksQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDO2dCQUN6QixJQUFJLENBQUMsQ0FBQyxTQUFTLEVBQUUsU0FBUyxDQUFDLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUM7b0JBQUUsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ2xFLENBQUMsQ0FBQyxDQUFDO1lBQ0gsSUFBSSxDQUFDLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUU7Z0JBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLGFBQWEsQ0FBQyxDQUFDO1lBQy9FLE1BQU0sRUFBRSxPQUFPLEVBQUUsVUFBVSxFQUFFLFNBQVMsRUFBRSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUM7WUFDekQsTUFBTSxNQUFNLEdBQUcsUUFBUSxDQUFDLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsV0FBVyxFQUFFLEVBQUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDLENBQUM7WUFDekcsSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLFVBQVU7Z0JBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLGFBQWEsQ0FBQyxDQUFDO1lBQzlFLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDO1lBQUMsT0FBTyxNQUFNLENBQUM7UUFDdkMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUNMLE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsV0FBVyxDQUFDLENBQUM7SUFDeEMsQ0FBQztJQUVNLE9BQU8sQ0FBQyxNQUFlO1FBQzVCLE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxlQUFlLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDL0MsQ0FBQztJQUNNLFNBQVMsQ0FBQyxVQUEwQztRQUN6RCxPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMsaUJBQWlCLEVBQUUsVUFBVSxDQUFDLENBQUM7SUFDckQsQ0FBQztJQUNNLE9BQU8sS0FBMEMsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLGVBQWUsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDNUYsT0FBTyxDQUFDLFFBQTJDO1FBQ3hELE9BQU8sSUFBSSxDQUFDLE9BQU8sQ0FBQyxlQUFlLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDakQsQ0FBQztJQUVEOzs7O21GQUkrRTtJQUN4RSxpQkFBaUIsQ0FBQyxPQUE4QztRQUNyRSxPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMseUJBQXlCLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDMUQsQ0FBQztJQUVEOztnRkFFNEU7SUFDckUsTUFBTSxDQUFDLFFBQTJDO1FBQ3ZELElBQUksSUFBSSxDQUFDLHVCQUF1QixJQUFJLElBQUksQ0FBQyxVQUFVLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxrQkFBa0I7WUFBRSxPQUFPLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxvQkFBb0IsQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDO1FBQzVKLElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDO1FBQ3ZCLElBQUksUUFBMkMsQ0FBQztRQUNoRCxJQUFJLENBQUM7WUFDSCxRQUFRLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1lBQzlCLElBQUksSUFBSSxDQUFDLGVBQWUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLGVBQWUsRUFBRSxRQUFRLENBQUM7Z0JBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFVBQVUsQ0FBQyxDQUFDO1lBQ3hJLElBQUksSUFBSSxDQUFDLE9BQU87Z0JBQUUsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDO1lBQ3RDLElBQUksQ0FBQyxDQUFDLE9BQU8sRUFBRSxjQUFjLENBQUMsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sSUFBSSxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksQ0FBQyxNQUFNO2dCQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxhQUFhLENBQUMsQ0FBQztZQUNsSixJQUFJLENBQUMsZUFBZSxHQUFHLFFBQVEsQ0FBQztRQUNsQyxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUFDLE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLFlBQVksb0JBQW9CLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxvQkFBb0IsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO1FBQUMsQ0FBQztRQUMvSCxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLGNBQWMsRUFBRSxRQUFRLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRTtZQUN6RixNQUFNLE1BQU0sR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDL0IsSUFBSSxNQUFNLEVBQUUsUUFBUSxLQUFLLElBQUksSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sS0FBSyxDQUFDO2dCQUFFLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxVQUFVLENBQUMsQ0FBQztZQUM5RyxJQUFJLENBQUMsYUFBYSxHQUFHLEtBQUssQ0FBQztZQUMzQixPQUFPLE1BQU0sQ0FBQztRQUNoQixDQUFDLENBQUMsRUFBRSxjQUFjLENBQUMsQ0FBQztRQUNwQixNQUFNLFFBQVEsR0FBRyxDQUFDLEtBQUssSUFBSSxFQUFFLEdBQUcsTUFBTSxNQUFNLEdBQUcsTUFBTSxJQUFJLENBQUMsQ0FBQyxNQUFNLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLE9BQU8sTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUNuRyxJQUFJLENBQUMsT0FBTyxHQUFHLFFBQVEsQ0FBQztRQUN4QixLQUFLLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNwRCxPQUFPLFFBQVEsQ0FBQztJQUNsQixDQUFDO0lBRUQ7Ozs7MkVBSXVFO0lBQ2hFLGNBQWM7UUFDbkIsSUFBSSxJQUFJLENBQUMsVUFBVSxLQUFLLFFBQVE7WUFBRSxPQUFPLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxvQkFBb0IsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDO1FBQzlGLElBQUksQ0FBQyx1QkFBdUIsR0FBRyxJQUFJLENBQUM7UUFDcEMsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLENBQUM7UUFDdkIsT0FBTyxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDdEIsQ0FBQztJQUVEO21HQUMrRjtJQUN4RixLQUFLO1FBQ1YsSUFBSSxJQUFJLENBQUMsTUFBTTtZQUFFLE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQztRQUNwQyxJQUFJLElBQUksQ0FBQyxNQUFNLEtBQUssU0FBUztZQUFFLE9BQU8sT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ3hELElBQUksQ0FBQyxVQUFVLEtBQUssSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUM7UUFDMUQsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLFVBQVUsS0FBSyxRQUFRLENBQUM7UUFDL0MsSUFBSSxDQUFDLE1BQU0sR0FBRyxTQUFTLENBQUM7UUFDeEIsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLEtBQUssSUFBSSxFQUFFO1lBQ3hCLE1BQU0sSUFBSSxDQUFDLEtBQUssRUFBRSxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUM7WUFDekMsSUFBSSxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ2pCLElBQUksSUFBSSxDQUFDLGFBQWEsSUFBSSxDQUFDLFNBQVMsRUFBRSxDQUFDO29CQUNyQyxvRUFBb0U7b0JBQ3BFLG9FQUFvRTtvQkFDcEUsb0VBQW9FO29CQUNwRSxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsa0JBQWtCO3dCQUNwQyxDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyx5QkFBeUIsRUFBRSxJQUFJLENBQUMsa0JBQWtCLENBQUMsT0FBTyxDQUFDO3dCQUM1RixDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxZQUFZLEVBQUUsRUFBRSxDQUFDLENBQUM7b0JBQ3JELElBQUksTUFBTSxDQUFDLFFBQVEsS0FBSyxJQUFJLElBQUksSUFBSSxDQUFDLGtCQUFrQixJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxLQUFLLENBQUM7d0JBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFVBQVUsQ0FBQyxDQUFDO2dCQUMxSSxDQUFDO2dCQUNELE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsRUFBRSxhQUFhLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztZQUN4RCxDQUFDO1lBQ0QsSUFBSSxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUM7WUFBQyxJQUFJLENBQUMsTUFBTSxHQUFHLFNBQVMsQ0FBQztRQUMvQyxDQUFDLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDZCxJQUFJLENBQUMsTUFBTSxHQUFHLGNBQWMsQ0FBQztZQUFDLElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDO1lBQ2pELElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNiLE1BQU0sSUFBSSxvQkFBb0IsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO1FBQ3hELENBQUMsQ0FBQyxDQUFDO1FBQ0gsT0FBTyxJQUFJLENBQUMsTUFBTSxDQUFDO0lBQ3JCLENBQUM7SUFFRCxPQUFPLENBQStDLE1BQVMsRUFBRSxNQUFpRDtRQUNoSCxJQUFJLElBQUksQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDLE9BQU8sRUFBRSxjQUFjLENBQUMsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sSUFBSSxJQUFJLENBQUMsS0FBSztZQUFFLE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FBQyxJQUFJLG9CQUFvQixDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUM7UUFDdkssSUFBSSxRQUFtRCxDQUFDO1FBQ3hELElBQUksQ0FBQztZQUFDLFFBQVEsR0FBRyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUM7UUFBQyxDQUFDO1FBQUMsTUFBTSxDQUFDO1lBQUMsT0FBTyxPQUFPLENBQUMsTUFBTSxDQUFDLElBQUksb0JBQW9CLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztRQUFDLENBQUM7UUFDMUcsSUFBSSxJQUFJLENBQUMsa0JBQWtCLElBQUksQ0FBQyxNQUFNLEtBQUssZUFBZSxJQUFJLE1BQU0sS0FBSyx5QkFBeUI7ZUFDN0YsTUFBTSxLQUFLLHlCQUF5QixJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsa0JBQWtCLENBQUMsT0FBTyxFQUFFLFFBQVEsQ0FBQyxDQUFDLEVBQUUsQ0FBQztZQUN6SCxPQUFPLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxvQkFBb0IsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDO1FBQzlELENBQUM7UUFDRCxJQUFJLE1BQU0sS0FBSyx5QkFBeUI7WUFBRSxJQUFJLENBQUMsa0JBQWtCLEdBQUcsRUFBRSxPQUFPLEVBQUUsUUFBaUQsRUFBRSxDQUFDO1FBQ25JLElBQUksTUFBTSxLQUFLLGlCQUFpQixJQUFJLE1BQU0sS0FBSyxlQUFlLElBQUksTUFBTSxLQUFLLHlCQUF5QjtZQUFFLElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxDQUFDO1FBQ2xJLE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFdBQVcsQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUU7WUFDNUUsTUFBTSxjQUFjLEdBQUcsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ3hDLElBQUksTUFBTSxLQUFLLGVBQWUsSUFBSSxNQUFNLEtBQUsseUJBQXlCLEVBQUUsQ0FBQztnQkFDdkUsSUFBSSxNQUFNLEtBQUsseUJBQXlCLElBQUksQ0FBRSxjQUFxQyxFQUFFLFFBQVEsS0FBSyxJQUFJO3VCQUNqRyxNQUFNLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDLE1BQU0sS0FBSyxDQUFDLENBQUM7b0JBQUUsTUFBTSxJQUFJLG9CQUFvQixDQUFDLFVBQVUsQ0FBQyxDQUFDO2dCQUMzRixJQUFJLENBQUMsYUFBYSxHQUFHLEtBQUssQ0FBQztZQUM3QixDQUFDO1lBQ0QsSUFBSSxNQUFNLEtBQUssZUFBZSxFQUFFLENBQUM7Z0JBQy9CLE1BQU0sTUFBTSxHQUFHLGNBQTRDLENBQUM7Z0JBQzVELElBQUksTUFBTSxDQUFDLEtBQUssS0FBSyxjQUFjLElBQUksTUFBTSxDQUFDLEtBQUssS0FBSyxJQUFJO29CQUFFLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUM3RSxDQUFDO1lBQ0QsSUFBSSxDQUFDLElBQUksQ0FBQyxZQUFZLElBQUksSUFBSSxDQUFDLE1BQU0sS0FBSyxTQUFTLElBQUksQ0FBQyxNQUFNLEtBQUssaUJBQWlCLElBQUksTUFBTSxLQUFLLGVBQWUsSUFBSSxNQUFNLEtBQUsseUJBQXlCO21CQUNySixNQUFNLEtBQUssZUFBZSxJQUFLLGNBQTZDLENBQUMsS0FBSyxLQUFLLElBQUk7dUJBQ3hGLGNBQTZDLENBQUMsS0FBSyxLQUFLLGNBQWMsQ0FBQztnQkFBRSxJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQztZQUN2RyxPQUFPLGNBQWMsQ0FBQztRQUN4QixDQUFDLENBQUMsRUFBRSxNQUFNLENBQUMsQ0FBQztJQUNkLENBQUM7SUFDRCxNQUFNLENBQUksSUFBZ0IsRUFBRSxNQUEwQztRQUNwRSxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUU7WUFDakMsSUFBSSxJQUFJLENBQUMsTUFBTSxLQUFLLFNBQVM7Z0JBQUUsSUFBSSxDQUFDLE1BQU0sR0FBRyxjQUFjLENBQUM7WUFDNUQsTUFBTSxJQUFJLEdBQUcsS0FBSyxZQUFZLE9BQU8sQ0FBQyxTQUFTLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDO1lBQzdHLHVFQUF1RTtZQUN2RSx3RUFBd0U7WUFDeEUseUVBQXlFO1lBQ3pFLE1BQU0sVUFBVSxHQUFHLEtBQUssWUFBWSxPQUFPLENBQUMsU0FBUyxDQUFDLHNCQUFzQjttQkFDdkUsS0FBSyxDQUFDLElBQUksS0FBSyx1Q0FBdUM7bUJBQ3RELENBQUMsTUFBTSxLQUFLLGVBQWUsSUFBSSxJQUFJLEtBQUssU0FBUyxDQUFDLENBQUM7WUFDeEQsSUFBSSxDQUFDLFVBQVUsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLEtBQUssU0FBUzttQkFDeEMsQ0FBQyxpQkFBaUIsRUFBRSxlQUFlLEVBQUUseUJBQXlCLEVBQUUsY0FBYyxDQUFDLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDO2dCQUFFLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNySCxNQUFNLElBQUksb0JBQW9CLENBQUMsT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLGdCQUFnQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxhQUFhLENBQUMsQ0FBQztRQUNqSCxDQUFDLENBQUMsQ0FBQztRQUNILElBQUksQ0FBQyxLQUFLLEdBQUcsS0FBSyxDQUFDO1FBQ25CLEtBQUssS0FBSyxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQzVFLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztDQUNGIn0=
@@ -51,6 +51,13 @@ export interface IManagedNftTransition<TPolicy extends TManagedNftPolicy = IMana
51
51
  previous: IAppliedManagedNftPolicy<TPolicy> | null;
52
52
  target: IPreparedManagedNftPolicy<TPolicy>;
53
53
  }
54
+ /** Persist the original boot/namespace identity and full transition before cleanup.
55
+ * This releases only its exact owned table; it does not prove packet drain or
56
+ * authorize address reuse. Surviving policy interfaces are not required. */
57
+ export interface IManagedNftTransitionRelease<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
58
+ identity: IManagedNftIdentity;
59
+ transition: IManagedNftTransition<TPolicy>;
60
+ }
54
61
  export interface IManagedNftStatus<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
55
62
  identity: IManagedNftIdentity;
56
63
  state: 'idle' | 'applied' | 'failed-owned' | 'released' | 'retained';
@@ -96,6 +103,12 @@ export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftP
96
103
  released: true;
97
104
  };
98
105
  };
106
+ releasePolicyTransition: {
107
+ params: IManagedNftTransitionRelease<TPolicy>;
108
+ result: {
109
+ released: true;
110
+ };
111
+ };
99
112
  detachPolicy: {
100
113
  params: IAppliedManagedNftPolicy<TPolicy>;
101
114
  result: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartnftables",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
6
6
  "main": "dist_ts/index.js",
package/readme.md CHANGED
@@ -109,10 +109,12 @@ grants remain outstanding.
109
109
  | Method | Behavior |
110
110
  | --- | --- |
111
111
  | `start()` | Starts one private native process and returns idle status. |
112
+ | `failureSignal` | Stable per-instance AbortSignal for lost owner confidence, including an unexpected idle process exit. It never resets or confirms cleanup. |
112
113
  | `prepare(policy)` | Validates, canonicalizes and hashes an inert complete policy without kernel writes. |
113
114
  | `reconcile({ previous, target })` | Creates or replaces the entire policy in one kernel transaction. `previous` is null initially, otherwise the full prior applied result. |
114
115
  | `inspect()` | Reads the current owned graph and reports `enforced`, pending intent and a bounded error code. |
115
116
  | `release(applied)` | Verifies the exact retained graph and confirms deletion; repeats are idempotent. |
117
+ | `releaseTransition({ identity, transition })` | Selects terminal cleanup using the original full identity and transition when the applied result is unavailable. Accepts absence or deletes the exact previous/target graph without requiring surviving interfaces. |
116
118
  | `detach(applied)` | Selects terminal retention, verifies the same graph with PERSIST-only flags after dropping socket ownership, then joins the native process. Returns `{ detached: true }`; retain the full applied result. |
117
119
  | `closeRetaining()` | Stops command admission, joins admitted operations and terminates the native process without requesting deletion or confirming retention. Use when the exact applied result is unavailable. |
118
120
  | `close()` | Stops command admission, joins admitted policy operations, confirms owned-table deletion unless retention was selected, then joins the native process. Failure retains the owner for explicit recovery. |
@@ -156,6 +158,22 @@ invalidates the old boot receipt; the caller must fence its old link/workload
156
158
  lifecycle before starting a new owner instance. Cleanup failures and uncertain
157
159
  operations must not be reported as successful release or used to transfer authority.
158
160
 
161
+ Subscribe to `failureSignal` before starting or admitting policy work, and check
162
+ `failureSignal.aborted` when attaching to an existing instance. Unexpected child
163
+ exit, ambiguous operation failures, native inspection reporting failed ownership,
164
+ and unconfirmed cleanup abort the signal once with `ManagedNftablesError` code
165
+ `OWNER_LOST`. Local input rejection, effect-free native compiler validation and
166
+ native `INVALID` responses do not abort it. Other native errors can occur after
167
+ kernel effects or ownership loss and conservatively abort it. Successful orderly
168
+ close, including a cancelled startup, does not signal an unexpected failure;
169
+ ambiguous mutations still signal loss while close is draining them.
170
+
171
+ The caller must fence packet admission on loss. Recovery operations and exact
172
+ retries remain available as documented, but successful recovery never resets the
173
+ signal. It does not monitor foreign kernel changes while idle or prove policy
174
+ withdrawal: PERSIST policy can remain after process death. Still await the chosen
175
+ joined cleanup method and retain unresolved policy/allocation authority.
176
+
159
177
  For an orderly restart that must preserve policy, persist the complete applied
160
178
  result and call `await owner.detach(applied)` before calling `close()`. The native
161
179
  operation validates the exact receipt, graph, interfaces, boot and namespace. It
@@ -185,6 +203,35 @@ may have left no graph; a lost reply may have left the original or target graph.
185
203
  Keep the complete original transition durably and recover it through a fresh owner
186
204
  with identical options. Never fabricate an applied receipt to request detachment.
187
205
 
206
+ When cleanup is required instead of recovering enforcement, persist a
207
+ `IManagedNftTransitionRelease` containing the identity returned by the original
208
+ `start()` and the full original transition. After fencing workload packet
209
+ admission and joining the old owner, start a fresh owner with the same options:
210
+
211
+ ```typescript
212
+ // Both values come from the caller's durable journal, before the lost apply ACK.
213
+ const cleanup = { identity: originalIdentity, transition: originalTransition };
214
+ await recoveryOwner.releaseTransition(cleanup);
215
+ await recoveryOwner.close();
216
+ ```
217
+
218
+ `releaseTransition()` validates the original boot and namespace, canonical target,
219
+ complete previous result when present, table identity and entire rule graph. It
220
+ accepts an absent table, the previous graph, or the target graph; it never creates
221
+ or replaces policy. Deleted or changed interfaces do not prevent this cleanup.
222
+ Orphan adoption checks the graph before and after claiming OWNER, binds the prior
223
+ or already-observed handle, and rejects another live owner. Deletion uses the exact
224
+ handle and a generation-fenced batch, followed by an absence check.
225
+
226
+ Once admitted, only that captured cleanup body may be retried. Other policy writes,
227
+ preparation and detachment reject; inspection never claims enforcement. An ordinary
228
+ `close()` retries the selected cleanup before joining the child. `closeRetaining()`
229
+ only joins, allowing a fresh process to retry the durably retained cleanup after
230
+ an ambiguous acknowledgement. Local inert-input or busy rejection does not select
231
+ cleanup. A successful release acknowledges that table operation at observation
232
+ time, including an exact retry after a lost delete ACK. It does not prove packet
233
+ drain, clear conntrack, release allocations, or cross a changed boot/namespace.
234
+
188
235
  After `closeRetaining()`, all policy commands, including `detach()`, reject.
189
236
  Concurrent cleanup calls share the same join. A failed join keeps the native
190
237
  owner and retention choice for retry through `closeRetaining()` or `close()`.
package/rust/src/main.rs CHANGED
@@ -91,6 +91,10 @@ fn dispatch(owner: &mut Option<owner::Owner>, request: Request) -> Result<serde_
91
91
  .release(decode(request.params)?)?;
92
92
  Ok(serde_json::json!({"released":true}))
93
93
  }
94
+ "releasePolicyTransition" => {
95
+ owner.as_mut().ok_or(Error::Conflict)?.release_transition(decode(request.params)?)?;
96
+ Ok(serde_json::json!({"released":true}))
97
+ }
94
98
  "detachPolicy" => {
95
99
  owner.as_mut().ok_or(Error::Conflict)?.detach(decode(request.params)?)?;
96
100
  Ok(serde_json::json!({"detached":true}))
@@ -5,7 +5,7 @@ impl Owner {
5
5
  /// orphan after an ACK was lost, including through a fresh native process.
6
6
  pub fn detach(&mut self, expected: Applied) -> Result<()> {
7
7
  self.validate_receipt(&expected)?;
8
- if self.released || self.pending.is_some() {
8
+ if self.released || self.pending.is_some() || self.transition_release.is_some() {
9
9
  return Err(Error::Conflict);
10
10
  }
11
11
  if let Some(retention) = &self.retention {
package/rust/src/owner.rs CHANGED
@@ -44,6 +44,12 @@ pub struct Transition {
44
44
  pub previous: Option<Applied>,
45
45
  pub target: Prepared,
46
46
  }
47
+ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
48
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
49
+ pub struct TransitionRelease {
50
+ pub identity: Identity,
51
+ pub transition: Transition,
52
+ }
47
53
  #[derive(Clone, Debug, Serialize)]
48
54
  #[serde(rename_all = "camelCase")]
49
55
  pub struct Status {
@@ -69,6 +75,7 @@ pub struct Owner {
69
75
  error: Option<Error>,
70
76
  released: bool,
71
77
  retention: Option<Retention>,
78
+ transition_release: Option<TransitionRelease>,
72
79
  }
73
80
 
74
81
  struct Retention {
@@ -78,6 +85,8 @@ struct Retention {
78
85
 
79
86
  #[path = "owner.detach.rs"]
80
87
  mod detach;
88
+ #[path = "owner.transitionrelease.rs"]
89
+ mod transitionrelease;
81
90
 
82
91
  pub(crate) fn context() -> Result<(String, String, String)> {
83
92
  let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
@@ -164,6 +173,7 @@ impl Owner {
164
173
  error: None,
165
174
  released: false,
166
175
  retention: None,
176
+ transition_release: None,
167
177
  })
168
178
  }
169
179
  fn socket(&mut self) -> Result<&mut Socket> {
@@ -360,7 +370,7 @@ impl Owner {
360
370
  })
361
371
  }
362
372
  pub fn reconcile(&mut self, transition: Transition) -> Result<Applied> {
363
- if self.released || self.retention.is_some() {
373
+ if self.released || self.retention.is_some() || self.transition_release.is_some() {
364
374
  return Err(Error::Conflict);
365
375
  }
366
376
  transition.target.validate()?;
@@ -463,6 +473,9 @@ impl Owner {
463
473
  self.applied(&graph, transition.target.clone())
464
474
  }
465
475
  pub fn inspect(&mut self) -> Status {
476
+ if self.transition_release.is_some() {
477
+ return self.inspect_transition_release();
478
+ }
466
479
  if self.retention.is_some() {
467
480
  return self.inspect_retained();
468
481
  }
@@ -503,7 +516,7 @@ impl Owner {
503
516
  }
504
517
  }
505
518
  pub fn release(&mut self, expected: Applied) -> Result<()> {
506
- if self.retention.is_some() {
519
+ if self.retention.is_some() || self.transition_release.is_some() {
507
520
  return Err(Error::Conflict);
508
521
  }
509
522
  self.validate_receipt(&expected)?;
@@ -561,6 +574,9 @@ impl Owner {
561
574
  }
562
575
  }
563
576
  pub fn close(&mut self) -> Result<()> {
577
+ if let Some(request) = self.transition_release.clone() {
578
+ return self.release_transition(request);
579
+ }
564
580
  if self.retention.is_some() {
565
581
  return Err(Error::Conflict);
566
582
  }
@@ -0,0 +1,107 @@
1
+ use super::*;
2
+
3
+ impl Owner {
4
+ /// Terminal cleanup of an exact caller-retained transition. Unlike reconcile,
5
+ /// this never creates/replaces policy and does not require surviving interfaces.
6
+ pub fn release_transition(&mut self, request: TransitionRelease) -> Result<()> {
7
+ if self.retention.is_some() || request.identity != self.identity {
8
+ return Err(Error::Conflict);
9
+ }
10
+ request.transition.target.validate()?;
11
+ if let Some(previous) = &request.transition.previous {
12
+ self.validate_receipt(previous)?;
13
+ if previous.prepared.policy.kind() != request.transition.target.policy.kind()
14
+ || previous.prepared.policy.revision()
15
+ >= request.transition.target.policy.revision()
16
+ {
17
+ return Err(Error::Conflict);
18
+ }
19
+ }
20
+ if let Some(selected) = &self.transition_release {
21
+ if selected != &request {
22
+ return Err(Error::Conflict);
23
+ }
24
+ if self.released {
25
+ return Ok(());
26
+ }
27
+ } else {
28
+ if self.released
29
+ || self
30
+ .pending
31
+ .as_ref()
32
+ .is_some_and(|pending| pending != &request.transition)
33
+ || self.applied.as_ref().is_some_and(|applied| {
34
+ Some(applied) != request.transition.previous.as_ref()
35
+ && applied.prepared != request.transition.target
36
+ })
37
+ {
38
+ return Err(Error::Conflict);
39
+ }
40
+ // Capture terminal intent before any query, ownership claim or delete.
41
+ self.transition_release = Some(request.clone());
42
+ }
43
+ let result = self.release_transition_inner(&request.transition);
44
+ match result {
45
+ Ok(()) => {
46
+ self.released = true;
47
+ self.pending = None;
48
+ self.error = None;
49
+ self.socket = None;
50
+ Ok(())
51
+ }
52
+ Err(error) => {
53
+ self.error = Some(error);
54
+ Err(error)
55
+ }
56
+ }
57
+ }
58
+
59
+ fn release_transition_inner(&mut self, transition: &Transition) -> Result<()> {
60
+ let Some(graph) = self.graph()? else {
61
+ return Ok(());
62
+ };
63
+ // Also bind any handle this process already observed before an ambiguous
64
+ // delete. A replacement under the same name must never inherit authority.
65
+ if let Some(applied) = &self.applied {
66
+ self.binding(&graph, Some(applied))?;
67
+ }
68
+ let graph = self.adopt(graph, transition)?;
69
+ let prepared = if self.matches(&graph, &transition.target)? {
70
+ transition.target.clone()
71
+ } else {
72
+ transition
73
+ .previous
74
+ .as_ref()
75
+ .ok_or(Error::Conflict)?
76
+ .prepared
77
+ .clone()
78
+ };
79
+ self.applied = Some(self.applied(&graph, prepared)?);
80
+ self.socket()?.batch(
81
+ graph.generation,
82
+ vec![(2, 0, vec![Attr::u64(4, wire::handle(&graph.table, 4)?)])],
83
+ )?;
84
+ if self.graph()?.is_some() {
85
+ return Err(Error::Conflict);
86
+ }
87
+ Ok(())
88
+ }
89
+
90
+ pub(super) fn inspect_transition_release(&self) -> Status {
91
+ // Cleanup intent cannot grant enforcement, including after a failed
92
+ // attempt that acquired OWNER on an otherwise valid persistent graph.
93
+ Status {
94
+ identity: self.identity.clone(),
95
+ state: if self.released {
96
+ "released"
97
+ } else {
98
+ "failed-owned"
99
+ }
100
+ .into(),
101
+ enforced: false,
102
+ applied: self.applied.clone(),
103
+ pending: self.pending.clone(),
104
+ error: self.error.map(|error| error.code().into()),
105
+ }
106
+ }
107
+ }
@@ -27,6 +27,9 @@ mod poolguard_tests;
27
27
  #[path = "owner_detach_tests.rs"]
28
28
  mod detach_tests;
29
29
 
30
+ #[path = "owner_transitionrelease_tests.rs"]
31
+ mod transitionrelease_tests;
32
+
30
33
  #[path = "owner_coexistence_tests.rs"]
31
34
  mod coexistence_tests;
32
35
 
@@ -0,0 +1,327 @@
1
+ use super::namespace_tests::NativeOwner;
2
+ use super::*;
3
+ use serde_json::json;
4
+ use std::io::Write;
5
+ use std::time::{Duration, Instant};
6
+
7
+ fn target(revision: u64) -> Prepared {
8
+ let mut policy = crate::egress::tests::pool_guard();
9
+ policy["revision"] = json!(revision);
10
+ serde_json::from_value::<crate::egress::Policy>(policy)
11
+ .unwrap()
12
+ .prepare()
13
+ .unwrap()
14
+ .into()
15
+ }
16
+ fn request(owner: &Owner, transition: Transition) -> TransitionRelease {
17
+ TransitionRelease {
18
+ identity: owner.identity.clone(),
19
+ transition,
20
+ }
21
+ }
22
+
23
+ #[test]
24
+ fn transition_release_validates_closed_identity_and_body_before_admission() {
25
+ let mut owner = Owner::new(options("release_input")).unwrap();
26
+ let command = request(
27
+ &owner,
28
+ Transition {
29
+ previous: None,
30
+ target: target(1),
31
+ },
32
+ );
33
+ for field in ["bootId", "namespaceDevice", "namespaceInode", "instanceId"] {
34
+ let mut invalid = serde_json::to_value(&command).unwrap();
35
+ invalid["identity"][field] = json!("invalid");
36
+ assert_eq!(
37
+ owner.release_transition(serde_json::from_value(invalid).unwrap()),
38
+ Err(Error::Conflict)
39
+ );
40
+ assert!(owner.transition_release.is_none());
41
+ }
42
+ let mut invalid = command.clone();
43
+ invalid.transition.target.digest = format!("sha256:{}", "0".repeat(64));
44
+ assert!(owner.release_transition(invalid).is_err());
45
+ assert!(owner.transition_release.is_none());
46
+ for pointer in ["", "/identity", "/transition", "/transition/target"] {
47
+ let mut invalid = serde_json::to_value(&command).unwrap();
48
+ invalid.pointer_mut(pointer).unwrap()["extra"] = json!(true);
49
+ assert!(serde_json::from_value::<TransitionRelease>(invalid).is_err());
50
+ }
51
+ let mut invalid = serde_json::to_value(&command).unwrap();
52
+ invalid["transition"]
53
+ .as_object_mut()
54
+ .unwrap()
55
+ .remove("previous");
56
+ assert!(serde_json::from_value::<TransitionRelease>(invalid).is_err());
57
+ assert!(owner.socket.is_none());
58
+ }
59
+
60
+ #[test]
61
+ #[ignore = "requires disposable isolated native qualification guest"]
62
+ fn release_original_transition_after_interfaces_disappear_never_recreates_policy() {
63
+ isolated();
64
+ for replacement in [false, true] {
65
+ for delivered in [false, true] {
66
+ let endpoint = endpoint("a", 72);
67
+ let policy = crate::policy::Policy {
68
+ schema_version: 1,
69
+ revision: 1,
70
+ endpoints: vec![endpoint],
71
+ rules: vec![],
72
+ };
73
+ let mut owner = Owner::new(options("release_links")).unwrap();
74
+ let previous = replacement.then(|| {
75
+ owner
76
+ .reconcile(Transition {
77
+ previous: None,
78
+ target: policy.clone().prepare().unwrap().into(),
79
+ })
80
+ .unwrap()
81
+ });
82
+ let mut next = policy;
83
+ if replacement {
84
+ next.revision = 2;
85
+ }
86
+ let command = request(
87
+ &owner,
88
+ Transition {
89
+ previous,
90
+ target: next.prepare().unwrap().into(),
91
+ },
92
+ );
93
+ if delivered {
94
+ owner.reconcile(command.transition.clone()).unwrap();
95
+ }
96
+ // Only the original command crosses this process boundary.
97
+ drop(owner);
98
+ ip(&["link", "del", "worka"]);
99
+ ip(&["netns", "del", "a"]);
100
+ let mut fresh = Owner::new(options("release_links")).unwrap();
101
+ assert!(fresh.reconcile(command.transition.clone()).is_err());
102
+ if replacement && delivered {
103
+ // Model a retained native pending command after an ambiguous batch.
104
+ fresh.pending = Some(command.transition.clone());
105
+ }
106
+ fresh.release_transition(command.clone()).unwrap();
107
+ let status = fresh.inspect();
108
+ assert_eq!(status.state, "released");
109
+ assert!(!status.enforced);
110
+ assert!(fresh.graph().unwrap().is_none());
111
+ fresh.release_transition(command.clone()).unwrap();
112
+ assert_eq!(
113
+ fresh.reconcile(command.transition.clone()),
114
+ Err(Error::Conflict)
115
+ );
116
+ let mut different = command;
117
+ different.transition.target = target(3);
118
+ assert_eq!(fresh.release_transition(different), Err(Error::Conflict));
119
+ fresh.close().unwrap();
120
+ }
121
+ }
122
+ }
123
+
124
+ #[test]
125
+ #[ignore = "requires disposable isolated native qualification guest"]
126
+ fn release_transition_rejects_foreign_owner_and_keeps_exact_terminal_choice() {
127
+ isolated();
128
+ let mut active = Owner::new(options("release_foreign")).unwrap();
129
+ let command = request(
130
+ &active,
131
+ Transition {
132
+ previous: None,
133
+ target: target(1),
134
+ },
135
+ );
136
+ let applied = active.reconcile(command.transition.clone()).unwrap();
137
+ let mut fresh = Owner::new(options("release_foreign")).unwrap();
138
+ assert_eq!(
139
+ fresh.release_transition(command.clone()),
140
+ Err(Error::ForeignOwner)
141
+ );
142
+ assert_eq!(fresh.inspect().state, "failed-owned");
143
+ assert!(!fresh.inspect().enforced);
144
+ assert_eq!(
145
+ fresh.reconcile(command.transition.clone()),
146
+ Err(Error::Conflict)
147
+ );
148
+ assert_eq!(fresh.detach(applied.clone()), Err(Error::Conflict));
149
+ assert_eq!(fresh.release(applied), Err(Error::Conflict));
150
+ assert!(active.inspect().enforced);
151
+ drop(active);
152
+ // close may retry only the already-selected exact release operation.
153
+ fresh.close().unwrap();
154
+ assert!(fresh.graph().unwrap().is_none());
155
+ fresh.release_transition(command).unwrap();
156
+ }
157
+
158
+ #[test]
159
+ #[ignore = "requires disposable isolated native qualification guest"]
160
+ fn release_transition_binds_previous_handle_and_complete_graph() {
161
+ isolated();
162
+ for fault in ["handle", "body", "pending"] {
163
+ let mut active = Owner::new(options("release_binding")).unwrap();
164
+ let previous = active
165
+ .reconcile(Transition {
166
+ previous: None,
167
+ target: target(1),
168
+ })
169
+ .unwrap();
170
+ let command = request(
171
+ &active,
172
+ Transition {
173
+ previous: Some(previous.clone()),
174
+ target: target(2),
175
+ },
176
+ );
177
+ drop(active);
178
+ let mut fresh = Owner::new(options("release_binding")).unwrap();
179
+ let mut invalid = command.clone();
180
+ if fault == "handle" {
181
+ invalid
182
+ .transition
183
+ .previous
184
+ .as_mut()
185
+ .unwrap()
186
+ .receipt
187
+ .table_handle = "18446744073709551615".into();
188
+ } else if fault == "pending" {
189
+ fresh.pending = Some(Transition {
190
+ previous: Some(previous.clone()),
191
+ target: target(3),
192
+ });
193
+ } else {
194
+ let mut foreign = Socket::open().unwrap();
195
+ let generation = foreign.generation().unwrap();
196
+ foreign
197
+ .batch(
198
+ generation,
199
+ vec![(
200
+ 3,
201
+ 0x600,
202
+ vec![
203
+ Attr::string(1, "snft_release_binding"),
204
+ Attr::string(3, "foreign"),
205
+ ],
206
+ )],
207
+ )
208
+ .unwrap();
209
+ }
210
+ assert_eq!(fresh.release_transition(invalid), Err(Error::Conflict));
211
+ assert!(fresh.graph().unwrap().is_some());
212
+ assert!(!fresh.inspect().enforced);
213
+ drop(fresh);
214
+ // Disposable fixture cleanup never enters the production API.
215
+ let mut cleanup = Socket::open().unwrap();
216
+ let generation = cleanup.generation().unwrap();
217
+ cleanup
218
+ .batch(
219
+ generation,
220
+ vec![(2, 0, vec![Attr::string(1, "snft_release_binding")])],
221
+ )
222
+ .unwrap();
223
+ }
224
+ }
225
+
226
+ #[test]
227
+ #[ignore = "requires disposable isolated native qualification guest"]
228
+ fn release_transition_retry_never_deletes_a_replacement_for_its_observed_handle() {
229
+ isolated();
230
+ let mut owner = Owner::new(options("release_replaced")).unwrap();
231
+ let command = request(
232
+ &owner,
233
+ Transition {
234
+ previous: None,
235
+ target: target(1),
236
+ },
237
+ );
238
+ let first = owner.reconcile(command.transition.clone()).unwrap();
239
+ // Stop at the exact production boundary after deletion, before its final
240
+ // observation. Retained state must still bind the handle already acquired.
241
+ owner.transition_release = Some(command.clone());
242
+ let graph = owner.graph().unwrap().unwrap();
243
+ owner
244
+ .socket()
245
+ .unwrap()
246
+ .batch(
247
+ graph.generation,
248
+ vec![(
249
+ 2,
250
+ 0,
251
+ vec![Attr::u64(4, wire::handle(&graph.table, 4).unwrap())],
252
+ )],
253
+ )
254
+ .unwrap();
255
+ let mut replacement = Owner::new(options("release_replaced")).unwrap();
256
+ let second = replacement.reconcile(command.transition.clone()).unwrap();
257
+ assert_ne!(first.receipt.table_handle, second.receipt.table_handle);
258
+ drop(replacement);
259
+ assert_eq!(owner.release_transition(command), Err(Error::Conflict));
260
+ let graph = owner.graph().unwrap().unwrap();
261
+ assert_eq!(
262
+ wire::handle(&graph.table, 4).unwrap().to_string(),
263
+ second.receipt.table_handle
264
+ );
265
+ assert_eq!(owner.applied, Some(first));
266
+ assert!(!owner.inspect().enforced);
267
+ drop(owner);
268
+ Owner::new(options("release_replaced"))
269
+ .unwrap()
270
+ .release(second)
271
+ .unwrap();
272
+ }
273
+
274
+ #[test]
275
+ #[ignore = "requires disposable isolated native qualification guest"]
276
+ fn release_transition_recovers_lost_delete_ack_across_actual_native_processes() {
277
+ isolated();
278
+ let namespace = std::fs::File::open("/proc/thread-self/ns/net").unwrap();
279
+ let mut native = NativeOwner::start(&namespace);
280
+ let status = native.call(
281
+ "openOwner",
282
+ serde_json::to_value(options("release_ack")).unwrap(),
283
+ );
284
+ let command = TransitionRelease {
285
+ identity: serde_json::from_value(status["identity"].clone()).unwrap(),
286
+ transition: Transition {
287
+ previous: None,
288
+ target: target(1),
289
+ },
290
+ };
291
+ native.call(
292
+ "reconcilePolicy",
293
+ serde_json::to_value(&command.transition).unwrap(),
294
+ );
295
+ writeln!(
296
+ native.child.stdin.as_mut().unwrap(),
297
+ "{}",
298
+ json!({
299
+ "id":"lost-release", "method":"releasePolicyTransition", "params":command,
300
+ })
301
+ )
302
+ .unwrap();
303
+ let mut observer = Owner::new(options("release_ack")).unwrap();
304
+ let until = Instant::now() + Duration::from_secs(3);
305
+ while observer.graph().unwrap().is_some() {
306
+ assert!(Instant::now() < until);
307
+ std::thread::sleep(Duration::from_millis(5));
308
+ }
309
+ // Never read or retain the release response.
310
+ drop(native);
311
+ let mut fresh = NativeOwner::start(&namespace);
312
+ fresh.call(
313
+ "openOwner",
314
+ serde_json::to_value(options("release_ack")).unwrap(),
315
+ );
316
+ assert_eq!(
317
+ fresh.call(
318
+ "releasePolicyTransition",
319
+ serde_json::to_value(&command).unwrap()
320
+ ),
321
+ json!({"released":true})
322
+ );
323
+ let status = fresh.call("inspectPolicy", json!({}));
324
+ assert_eq!(status["state"], "released");
325
+ assert_eq!(status["enforced"], false);
326
+ assert!(observer.graph().unwrap().is_none());
327
+ }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '2.0.0',
6
+ version: '2.2.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  }
@@ -1,6 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { IManagedNftOptions, IManagedNftPolicy, IManagedNftTransition, IAppliedManagedNftPolicy,
3
- IPreparedManagedNftPolicy, IManagedNftStatus, TManagedNftCommands, TManagedNftPolicy } from './managed.types.js';
3
+ IPreparedManagedNftPolicy, IManagedNftStatus, IManagedNftTransitionRelease, TManagedNftCommands, TManagedNftPolicy } from './managed.types.js';
4
4
 
5
5
  export class ManagedNftablesError extends Error {
6
6
  constructor(public readonly code: string) { super(`Managed nftables ${code}.`); }
@@ -55,6 +55,9 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
55
55
  #retaining = false;
56
56
  #detachExpected: IAppliedManagedNftPolicy<TPolicy> | null = null;
57
57
  #detach: Promise<{ detached: true }> | null = null;
58
+ #transitionRelease: { request: IManagedNftTransitionRelease<TPolicy> } | null = null;
59
+ readonly #failure = new AbortController();
60
+ #processLost = false;
58
61
 
59
62
  constructor(options: IManagedNftOptions) {
60
63
  this.#options = snapshot(options);
@@ -71,6 +74,16 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
71
74
 
72
75
  public get state(): string { return this.#state; }
73
76
 
77
+ /** Retained loss of owner confidence, including idle process death. It never
78
+ * resets or proves withdrawal/deletion; explicit recovery and joined close
79
+ * remain the caller's responsibility. Orderly close does not abort it. */
80
+ public get failureSignal(): AbortSignal { return this.#failure.signal; }
81
+
82
+ #fail(): void {
83
+ if (this.#state !== 'closing') this.#state = 'failed-owned';
84
+ if (!this.#failure.signal.aborted) this.#failure.abort(new ManagedNftablesError('OWNER_LOST'));
85
+ }
86
+
74
87
  /** Starts only the compiler/IPC owner. The returned idle status is not enforcement. */
75
88
  public start(): Promise<IManagedNftStatus<TPolicy>> {
76
89
  if (this.#state !== 'new' || this.#retaining) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
@@ -85,10 +98,11 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
85
98
  cliArgs: ['--management', ...(this.#options.networkNamespaceFd === undefined ? [] : ['--network-namespace-fd', '3'])],
86
99
  inheritedFileDescriptors: this.#options.networkNamespaceFd === undefined ? undefined : [this.#options.networkNamespaceFd],
87
100
  readyTimeoutMs: 3000, requestTimeoutMs: 30_000, maxPayloadSize: 262_144,
88
- maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, detachPolicy: 1, closeOwner: 1 },
101
+ maxPendingRequestsByMethod: { preparePolicy: 1, openOwner: 1, reconcilePolicy: 1, inspectPolicy: 1, releasePolicy: 1, releasePolicyTransition: 1, detachPolicy: 1, closeOwner: 1 },
89
102
  });
90
103
  this.#bridge.on('exit', () => {
91
- if (!['closing', 'stopped'].includes(this.#state)) this.#state = 'failed-owned';
104
+ this.#processLost = true;
105
+ if (!['closing', 'stopped'].includes(this.#state)) this.#fail();
92
106
  });
93
107
  if (!await this.#bridge.spawn()) throw new ManagedNftablesError('UNAVAILABLE');
94
108
  const { ownerId, instanceId, tableName } = this.#options;
@@ -96,7 +110,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
96
110
  if (this.#state !== 'starting') throw new ManagedNftablesError('UNAVAILABLE');
97
111
  this.#state = 'ready'; return status;
98
112
  })();
99
- return this.#track(work);
113
+ return this.#track(work, 'openOwner');
100
114
  }
101
115
 
102
116
  public prepare(policy: TPolicy): Promise<IPreparedManagedNftPolicy<TPolicy>> {
@@ -110,11 +124,20 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
110
124
  return this.#invoke('releasePolicy', expected);
111
125
  }
112
126
 
127
+ /** Terminal release when an apply acknowledgement was lost. Retain the full
128
+ * original identity and transition first. The native owner accepts only an
129
+ * absent table or the exact previous/target graph; it never recreates policy
130
+ * or requires surviving interfaces. Exact retries and inspection remain
131
+ * available; close() retries admitted cleanup, closeRetaining() only joins. */
132
+ public releaseTransition(request: IManagedNftTransitionRelease<TPolicy>): Promise<{ released: true }> {
133
+ return this.#invoke('releasePolicyTransition', request);
134
+ }
135
+
113
136
  /** Permanently selects retention cleanup, verifies exact PERSIST retention, and
114
137
  * joins the native process. Retain the full applied value before this call.
115
138
  * After any failure close() only terminates; it never deletes the table. */
116
139
  public detach(expected: IAppliedManagedNftPolicy<TPolicy>): Promise<{ detached: true }> {
117
- if (this.#closeRetainingSelected || this.#closeMode === 'delete') return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
140
+ if (this.#closeRetainingSelected || this.#closeMode === 'delete' || this.#transitionRelease) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
118
141
  this.#retaining = true;
119
142
  let captured: IAppliedManagedNftPolicy<TPolicy>;
120
143
  try {
@@ -129,7 +152,7 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
129
152
  if (result?.detached !== true || Object.keys(result).length !== 1) throw new ManagedNftablesError('PROTOCOL');
130
153
  this.#nativeMayOwn = false;
131
154
  return result;
132
- }));
155
+ }), 'detachPolicy');
133
156
  const detached = (async () => { const result = await work; await this.close(); return result; })();
134
157
  this.#detach = detached;
135
158
  void detached.catch(() => { this.#detach = null; });
@@ -160,14 +183,20 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
160
183
  await this.#work?.catch(() => undefined);
161
184
  if (this.#bridge) {
162
185
  if (this.#nativeMayOwn && !retaining) {
163
- const result = await this.#bridge.sendCommand('closeOwner', {});
164
- if (result.released !== true) throw new ManagedNftablesError('PROTOCOL');
186
+ // Even native validation may have rejected before selecting its own
187
+ // terminal intent. Retry this exact request; generic close must not
188
+ // delete an earlier applied graph outside the requested transition.
189
+ const result = this.#transitionRelease
190
+ ? await this.#bridge.sendCommand('releasePolicyTransition', this.#transitionRelease.request)
191
+ : await this.#bridge.sendCommand('closeOwner', {});
192
+ if (result.released !== true || this.#transitionRelease && Object.keys(result).length !== 1) throw new ManagedNftablesError('PROTOCOL');
165
193
  }
166
194
  await this.#bridge.terminate({ gracePeriodMs: 1000 });
167
195
  }
168
196
  this.#bridge = null; this.#state = 'stopped';
169
197
  })().catch(() => {
170
198
  this.#state = 'failed-owned'; this.#close = null;
199
+ this.#fail();
171
200
  throw new ManagedNftablesError('CLEANUP_UNCONFIRMED');
172
201
  });
173
202
  return this.#close;
@@ -177,19 +206,41 @@ export class ManagedNftables<TPolicy extends TManagedNftPolicy = IManagedNftPoli
177
206
  if (this.#retaining || !['ready', 'failed-owned'].includes(this.#state) || !this.#bridge || this.#work) return Promise.reject(new ManagedNftablesError('UNAVAILABLE'));
178
207
  let captured: TManagedNftCommands<TPolicy>[K]['params'];
179
208
  try { captured = snapshot(params); } catch { return Promise.reject(new ManagedNftablesError('INVALID')); }
180
- if (method === 'reconcilePolicy' || method === 'releasePolicy') this.#nativeMayOwn = true;
209
+ if (this.#transitionRelease && (method !== 'inspectPolicy' && method !== 'releasePolicyTransition'
210
+ || method === 'releasePolicyTransition' && !plugins.util.isDeepStrictEqual(this.#transitionRelease.request, captured))) {
211
+ return Promise.reject(new ManagedNftablesError('CONFLICT'));
212
+ }
213
+ if (method === 'releasePolicyTransition') this.#transitionRelease = { request: captured as IManagedNftTransitionRelease<TPolicy> };
214
+ if (method === 'reconcilePolicy' || method === 'releasePolicy' || method === 'releasePolicyTransition') this.#nativeMayOwn = true;
181
215
  return this.#track(this.#bridge.sendCommand(method, captured).then((result) => {
182
216
  const capturedResult = snapshot(result);
183
- if (method === 'releasePolicy') this.#nativeMayOwn = false;
184
- if (this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy'
185
- || method === 'inspectPolicy' && (capturedResult as IManagedNftStatus<TPolicy>).error === null)) this.#state = 'ready';
217
+ if (method === 'releasePolicy' || method === 'releasePolicyTransition') {
218
+ if (method === 'releasePolicyTransition' && ((capturedResult as { released: true })?.released !== true
219
+ || Object.keys(capturedResult).length !== 1)) throw new ManagedNftablesError('PROTOCOL');
220
+ this.#nativeMayOwn = false;
221
+ }
222
+ if (method === 'inspectPolicy') {
223
+ const status = capturedResult as IManagedNftStatus<TPolicy>;
224
+ if (status.state === 'failed-owned' || status.error !== null) this.#fail();
225
+ }
226
+ if (!this.#processLost && this.#state !== 'closing' && (method === 'reconcilePolicy' || method === 'releasePolicy' || method === 'releasePolicyTransition'
227
+ || method === 'inspectPolicy' && (capturedResult as IManagedNftStatus<TPolicy>).error === null
228
+ && (capturedResult as IManagedNftStatus<TPolicy>).state !== 'failed-owned')) this.#state = 'ready';
186
229
  return capturedResult;
187
- }));
230
+ }), method);
188
231
  }
189
- #track<T>(work: Promise<T>): Promise<T> {
232
+ #track<T>(work: Promise<T>, method: keyof TManagedNftCommands<TPolicy>): Promise<T> {
190
233
  const owned = work.catch((error) => {
191
234
  if (this.#state !== 'closing') this.#state = 'failed-owned';
192
235
  const code = error instanceof plugins.smartrust.RustBridgeRequestError ? error.responseErrorCode : undefined;
236
+ // Only a native compiler rejection or explicit INVALID response proves
237
+ // effect-free validation. Other native errors can follow lost ownership
238
+ // or admitted kernel work. Recovery stays available; confidence is lost.
239
+ const validation = error instanceof plugins.smartrust.RustBridgeRequestError
240
+ && error.code === 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE'
241
+ && (method === 'preparePolicy' || code === 'INVALID');
242
+ if (!validation && (this.#state !== 'closing'
243
+ || ['reconcilePolicy', 'releasePolicy', 'releasePolicyTransition', 'detachPolicy'].includes(method))) this.#fail();
193
244
  throw new ManagedNftablesError(typeof code === 'string' && /^[A-Z_]{1,64}$/.test(code) ? code : 'UNAVAILABLE');
194
245
  });
195
246
  this.#work = owned;
@@ -60,6 +60,14 @@ export interface IManagedNftTransition<TPolicy extends TManagedNftPolicy = IMana
60
60
  target: IPreparedManagedNftPolicy<TPolicy>;
61
61
  }
62
62
 
63
+ /** Persist the original boot/namespace identity and full transition before cleanup.
64
+ * This releases only its exact owned table; it does not prove packet drain or
65
+ * authorize address reuse. Surviving policy interfaces are not required. */
66
+ export interface IManagedNftTransitionRelease<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
67
+ identity: IManagedNftIdentity;
68
+ transition: IManagedNftTransition<TPolicy>;
69
+ }
70
+
63
71
  export interface IManagedNftStatus<TPolicy extends TManagedNftPolicy = IManagedNftPolicy> {
64
72
  identity: IManagedNftIdentity;
65
73
  state: 'idle' | 'applied' | 'failed-owned' | 'released' | 'retained';
@@ -91,6 +99,7 @@ export type TManagedNftCommands<TPolicy extends TManagedNftPolicy = IManagedNftP
91
99
  reconcilePolicy: { params: IManagedNftTransition<TPolicy>; result: IAppliedManagedNftPolicy<TPolicy> };
92
100
  inspectPolicy: { params: Record<string, never>; result: IManagedNftStatus<TPolicy> };
93
101
  releasePolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { released: true } };
102
+ releasePolicyTransition: { params: IManagedNftTransitionRelease<TPolicy>; result: { released: true } };
94
103
  detachPolicy: { params: IAppliedManagedNftPolicy<TPolicy>; result: { detached: true } };
95
104
  closeOwner: { params: Record<string, never>; result: { released: true } };
96
105
  };