@uuaid/pillar 2.0.1 → 2.0.3

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/HOSTING-LIMITS.md CHANGED
@@ -310,6 +310,21 @@ front of each one.
310
310
  clients retry after 2 s). Stored envelopes stay in SQLite. Leave disk for a
311
311
  256 MiB store plus its WAL file.
312
312
 
313
+ **Secure delete (from 2.0.3).** Expired envelopes are zeroed on disk by
314
+ default. `vacuum()` runs with SQLite `secure_delete` on and follows each
315
+ expiry pass that deleted anything with `wal_checkpoint(TRUNCATE)`, so neither
316
+ the database file nor the WAL keeps a deleted envelope's bytes. Turn it off
317
+ per broker with `--secure-delete off` (`CARRIER_SECURE_DELETE=off` for
318
+ `pillar-carrier`, `SEED_SECURE_DELETE=off` for `pillar-seed`); any other value
319
+ than on/off refuses to start. It is one setting for the whole store: SQLite
320
+ sets it per connection, so it cannot differ by tenant. Two things it does not
321
+ do:
322
+ - Bytes deleted before you upgraded stay in free pages. Run
323
+ `sqlite3 <db> 'VACUUM;'` once, with the carrier stopped, to clear them.
324
+ - A checkpoint that stays busy (another process holding the database open)
325
+ is logged as `secure-delete: WAL checkpoint still busy` and retried at the
326
+ next hourly pass; until then the WAL can still hold those bytes.
327
+
313
328
  **Relay host.** Back up `PUSH_RELAY_SEED_PATH`. Every poll token names the
314
329
  relay's key, so losing that file invalidates all of them
315
330
  (apps/pillar-push/src/config.ts:19-22). Watch `fcmKey.daysLeft` on
@@ -343,6 +358,7 @@ or an exception message. A parameterised reason such as
343
358
  | `keyId-sender-mismatch` | 400 | Sign with `transportSignature.keyId` equal to the sender UUAID, or send under a delegation |
344
359
  | `publicKey-sender-mismatch` | 400 | Send from the UUAID your signing key derives, or attach the principal's delegation |
345
360
  | `bad-signature` | 400, 401 | Sign again with this identity's own key over the exact bytes; the signature does not verify |
361
+ | `weak-publicKey` (from 2.0.3) | 400 | The sender key is a small-order or non-canonical Ed25519 point, which can "sign" anything; use a real identity key |
346
362
  | `not-object`, `bad-version <v>`, `missing-or-bad-alg`, `alg-not-active:<alg>`, `bad-publicKey`, `bad-signature-encoding`, `missing-enc`, `enc-alg-not-supported:<alg>`, `enc-alg-not-active:<alg>`, `bad-epk`, `bad-iv`, `bad-ct`, `bad-canonical`, `verify-threw` | 400 (`verify-threw` also 401) | Re-seal the envelope with `seal()` from `@uuaid/pillar` or `@uuaid/pillar-client`; this one does not verify |
347
363
  | `delegation-invalid:<why>` | 403 (send), 401 (inbox) | Mint a fresh delegation from the principal, then resend |
348
364
  | `missing-auth-headers` | 401 | Send `x-pillar-pubkey`, `x-pillar-ts` and `x-pillar-sig` |
@@ -160,8 +160,8 @@ The handle prefix in the Pillar ecosystem is **`...` (three dots)** — NOT
160
160
 
161
161
  - `...<name>` — a directory-resolved handle: three dots, then
162
162
  `[a-z0-9-]+` (case-insensitive). Matched case-insensitively against the
163
- carrier directory's agent `name` and `role` fields (e.g. `...thoth`,
164
- `...dbdash`). Resolution prefers an exact name/role match, then a prefix
163
+ carrier directory's agent `name` and `role` fields (e.g. `...my-agent`,
164
+ `...helper`). Resolution prefers an exact name/role match, then a prefix
165
165
  match, then a substring match; the first non-empty tier wins. Multiple
166
166
  matches → the client shows a picker; zero matches → a clear
167
167
  "no agent found" error.
package/bin/carrier.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  * carrier — run a standalone carrier server (the 24/7 post office).
4
4
  *
5
5
  * carrier --port 8799 --host 127.0.0.1 --db /opt/pillar-carrier/carrier.db
6
+ * carrier ... --secure-delete off # keep deleted envelope bytes (default: on)
6
7
  *
7
8
  * Defaults to loopback (RedTeam F5) — expose only behind a TLS-terminating
8
9
  * proxy (nginx/Caddy). Pass --host 0.0.0.0 deliberately only if the carrier
@@ -11,7 +12,7 @@
11
12
  * about who exists — pure store-and-forward.
12
13
  */
13
14
  import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
14
- import { CarrierServer } from "../src/net/carrier-server.mjs";
15
+ import { CarrierServer, parseSecureDelete } from "../src/net/carrier-server.mjs";
15
16
 
16
17
  function parseArgs(argv) {
17
18
  const args = {};
@@ -35,17 +36,29 @@ const ttlDays = Number(args.ttl ?? process.env.CARRIER_TTL_DAYS ?? 14);
35
36
  const tierIssuers = String(args["tier-issuers"] ?? process.env.CARRIER_TIER_ISSUERS ?? "")
36
37
  .split(",").map(s => s.trim()).filter(s => /^[0-9a-f]{64}$/i.test(s));
37
38
 
38
- const server = new CarrierServer({ dbPath, ttlDays, trustedTierIssuers: tierIssuers });
39
+ // Secure delete of expired envelopes: on unless this broker turns it off.
40
+ let secureDelete;
41
+ try {
42
+ secureDelete = parseSecureDelete(args["secure-delete"] ?? process.env.CARRIER_SECURE_DELETE);
43
+ } catch (e) {
44
+ console.error(`carrier: ${e.message}`);
45
+ process.exit(2);
46
+ }
47
+
48
+ const server = new CarrierServer({ dbPath, ttlDays, trustedTierIssuers: tierIssuers, secureDelete });
39
49
  const addr = await server.listen(port, host);
40
50
  console.log(`carrier listening on ${addr.address}:${addr.port}`);
41
51
  console.log(` db: ${dbPath}`);
42
52
  console.log(` ttl: ${ttlDays} days`);
43
53
  console.log(` tiers: ${tierIssuers.length ? `${tierIssuers.length} trusted issuer(s)` : "disabled (community budgets for all)"}`);
54
+ console.log(` secure-delete: ${secureDelete ? "on (expired envelopes are zeroed; WAL truncated after each expiry pass)" : "off (expired envelope bytes stay on disk until reused)"}`);
44
55
 
45
56
  // Vacuum expired envelopes hourly.
46
57
  const vacuumTimer = setInterval(() => {
47
58
  const n = server.vacuum();
48
59
  if (n > 0) console.log(`vacuumed ${n} expired envelopes`);
60
+ const v = server.lastVacuum;
61
+ if (v?.scrubbed === false) console.error(`secure-delete: WAL checkpoint still busy after ${v.attempts} tries (busy=${v.checkpoint?.busy}); deleted bytes may remain in the WAL until the next pass`);
49
62
  }, 3600_000);
50
63
 
51
64
  const shutdown = async () => {
@@ -47,7 +47,7 @@ function parse(argv) {
47
47
  }
48
48
 
49
49
  function homeDir(args) {
50
- return args.home ?? process.env.PILLAR_HOME ?? join(homedir(), ".pillar-fleet", "zz-master");
50
+ return args.home ?? process.env.PILLAR_HOME ?? join(homedir(), ".pillar");
51
51
  }
52
52
 
53
53
  function loadHome(args) {
@@ -27,8 +27,8 @@
27
27
  * Carrier-only by default (outbound HTTPS only — runs on any machine or
28
28
  * webserver; no inbound ports, no libp2p). Pass --libp2p to also listen.
29
29
  *
30
- * pillar-responder --home ~/.pillar-fleet/zz-master-responder \
31
- * --reply-text "zz-master is away — left a message" \
30
+ * pillar-responder --home ~/.pillar-responder \
31
+ * --reply-text "my-agent is away — left a message" \
32
32
  * --forward-to uuaid:foundation:agent:… --forward-pubkey <hex>
33
33
  */
34
34
  import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
@@ -43,7 +43,7 @@ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22
43
43
  import { mkdirSync } from "node:fs";
44
44
  import { homedir } from "node:os";
45
45
  import { join } from "node:path";
46
- import { CarrierServer } from "../src/net/carrier-server.mjs";
46
+ import { CarrierServer, parseSecureDelete } from "../src/net/carrier-server.mjs";
47
47
  import { Keychain } from "../src/identity/keychain.mjs";
48
48
 
49
49
  function parseArgs(argv) {
@@ -68,6 +68,14 @@ const publicUrl = args["public-url"] ?? process.env.SEED_PUBLIC_URL ?? null;
68
68
  const registryUrl = typeof args.announce === "string" ? args.announce.replace(/\/+$/, "") : null;
69
69
  const operator = args.operator ?? process.env.SEED_OPERATOR ?? null;
70
70
  const region = args.region ?? process.env.SEED_REGION ?? null;
71
+ // Secure delete of expired envelopes: on unless this seed turns it off.
72
+ let secureDelete;
73
+ try {
74
+ secureDelete = parseSecureDelete(args["secure-delete"] ?? process.env.SEED_SECURE_DELETE);
75
+ } catch (e) {
76
+ console.error(`pillar-seed: ${e.message}`);
77
+ process.exit(2);
78
+ }
71
79
 
72
80
  /**
73
81
  * The seed's identity. Distinct from any agent keychain on the box: it names
@@ -114,12 +122,13 @@ if (args.status) {
114
122
  process.exit(0);
115
123
  }
116
124
 
117
- const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl });
125
+ const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl, secureDelete });
118
126
  const addr = await server.listen(port, host);
119
127
  console.log(`pillar-seed listening on http://${addr.address}:${addr.port}`);
120
128
  console.log(` uuaid: ${keychain._identity.uuaid}`);
121
129
  console.log(` db: ${dbPath}`);
122
130
  console.log(` ttl: ${ttlDays} days`);
131
+ console.log(` secure-delete: ${secureDelete ? "on (expired envelopes are zeroed; WAL truncated after each expiry pass)" : "off (expired envelope bytes stay on disk until reused)"}`);
123
132
  if (publicUrl) console.log(` public url: ${publicUrl}`);
124
133
 
125
134
  // Boot self-check on the address we actually serve — verify before claim.
@@ -171,6 +180,8 @@ if (registryUrl) {
171
180
  const vacuumTimer = setInterval(() => {
172
181
  const n = server.vacuum();
173
182
  if (n > 0) console.log(`vacuumed ${n} expired envelopes`);
183
+ const v = server.lastVacuum;
184
+ if (v?.scrubbed === false) console.error(`secure-delete: WAL checkpoint still busy after ${v.attempts} tries (busy=${v.checkpoint?.busy}); deleted bytes may remain in the WAL until the next pass`);
174
185
  }, 3600_000);
175
186
 
176
187
  const shutdown = async () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uuaid/pillar",
3
- "version": "2.0.1",
4
- "description": "Pillar (Protocol for Inter-agent Linked Ledger And Relay) \u2014 the secure delivery layer of UACP, the Universal Agent Communication Protocol. UACP = UUAID (identity) + Envelope (signed, encrypted container) + Pillar (relay, mailbox, routing, store-and-forward). Reference implementation of the UACP delivery layer; includes VDA-1 (IAASO-3901, in ballot) delivery attestation and a blind escrow for the Zilligon ZGN / EVM rails.",
3
+ "version": "2.0.3",
4
+ "description": "Pillar (Protocol for Inter-agent Linked Ledger And Relay) — the secure delivery layer of UACP, the Universal Agent Communication Protocol. UACP = UUAID (identity) + Envelope (signed, encrypted container) + Pillar (relay, mailbox, routing, store-and-forward). Reference implementation of the UACP delivery layer; includes VDA-1 (IAASO-3901, in ballot) delivery attestation and a blind escrow for the Zilligon ZGN / EVM rails.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "src/index.mjs",
@@ -24,11 +24,10 @@ function _ser(v) {
24
24
  if (typeof v === "bigint") return v.toString();
25
25
  if (Array.isArray(v)) return "[" + v.map(_ser).join(",") + "]";
26
26
  if (typeof v === "object") {
27
- // UTF-16 code unit sort — the RFC 8785 requirement.
28
- const keys = Object.keys(v).sort((a, b) => {
29
- const ab = Buffer.from(a, "utf16le"), bb = Buffer.from(b, "utf16le");
30
- return Buffer.compare(ab, bb);
31
- });
27
+ // RFC 8785 §3.2.3: keys sort by their UTF-16 code units, which is exactly
28
+ // JavaScript's default string order. (Comparing UTF-16LE *bytes* is not: it
29
+ // puts "Ā" (U+0100) before "a", and disagreed with @uuaid/pillar-client.)
30
+ const keys = Object.keys(v).sort(); // @rule jcs.code-unit-order
32
31
  return "{" + keys.map(k => JSON.stringify(k) + ":" + _ser(v[k])).join(",") + "}";
33
32
  }
34
33
  throw new Error("JCS: unsupported type " + typeof v);
@@ -12,9 +12,43 @@
12
12
  import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "node:fs";
13
13
  import { dirname } from "node:path";
14
14
  import { randomBytes, createHash, generateKeyPairSync, sign, verify, createPublicKey, createPrivateKey, hkdfSync, createCipheriv, createDecipheriv } from "node:crypto";
15
+ import { ed25519 } from "@noble/curves/ed25519.js";
15
16
 
16
17
  const KEYCHAIN_VERSION = 1;
17
18
 
19
+ /**
20
+ * RFC 8032 / FIPS 186-5 rule for an Ed25519 public key: the canonical encoding
21
+ * of a point that is not of small order and lies in the prime-order subgroup.
22
+ *
23
+ * OpenSSL's Ed25519 verify (node `crypto.verify`) does not check this: it uses
24
+ * the non-cofactored equation, and 5 of the 8 small-order points verify as
25
+ * keys. With the identity point as the key, R = identity and S = 0 verifies on
26
+ * every message, so anyone could forge a signature "from" that key's uuaid
27
+ * (found 2026-10-06). The same rule as `isStrictEd25519PublicKey` in
28
+ * @uuaid/pillar-client.
29
+ *
30
+ * The answer depends on the key alone, so it is cached (bounded).
31
+ */
32
+ const STRICT_KEY_CACHE_MAX = 4096;
33
+ const strictKeyCache = new Map();
34
+
35
+ export function isStrictEd25519PublicKey(publicKeyHex) {
36
+ if (typeof publicKeyHex !== "string" || !/^[0-9a-f]{64}$/i.test(publicKeyHex)) return false;
37
+ const hex = publicKeyHex.toLowerCase();
38
+ const cached = strictKeyCache.get(hex);
39
+ if (cached !== undefined) return cached;
40
+ let ok = false;
41
+ try {
42
+ const point = ed25519.Point.fromHex(hex, false);
43
+ ok = point.toHex() === hex && !point.isSmallOrder() && point.isTorsionFree();
44
+ } catch (_e) {
45
+ ok = false;
46
+ }
47
+ if (strictKeyCache.size >= STRICT_KEY_CACHE_MAX) strictKeyCache.delete(strictKeyCache.keys().next().value);
48
+ strictKeyCache.set(hex, ok);
49
+ return ok;
50
+ }
51
+
18
52
  /**
19
53
  * Derive a stable, publishable local id from a raw Ed25519 public key. This
20
54
  * makes the UUAID self-authenticating: given the UUAID, anyone can
@@ -149,6 +183,7 @@ export class Keychain {
149
183
  }
150
184
 
151
185
  static verifyDetached(publicKeyHex, data, signatureBytes) {
186
+ if (!isStrictEd25519PublicKey(publicKeyHex)) return false; // @rule keychain.strict-key
152
187
  const spkiPrefix = Buffer.from("302a300506032b6570032100", "hex");
153
188
  const der = Buffer.concat([spkiPrefix, Buffer.from(publicKeyHex, "hex")]);
154
189
  const pub = createPublicKey({ key: der, format: "der", type: "spki" });
@@ -81,11 +81,39 @@ const RATE_MAP_HARD_CAP = 10_000; // F2: bound memory under key spray
81
81
  const COMMUNITY_DB_BYTES = Math.floor(MAX_DB_BYTES * 0.85); // last 15% of the store is reserved
82
82
  const COMMUNITY_CONCURRENT_WAITS = 48; // of MAX_CONCURRENT_WAITS
83
83
  const MAX_PRIORITY_GOOD_PER_MIN = 600; // priority senders bill to their own per-IP bucket
84
+ const CHECKPOINT_ATTEMPTS = 3; // TRUNCATE checkpoint tries per vacuum pass
85
+ const CHECKPOINT_BUSY_MS = 250; // per try; a busy pass is retried next hour
86
+
87
+ /**
88
+ * Secure delete for the envelope store: one switch per broker (--secure-delete
89
+ * on|off, CARRIER_SECURE_DELETE / SEED_SECURE_DELETE). ON by default.
90
+ *
91
+ * A plain DELETE leaves an expired envelope's bytes readable in the database
92
+ * file, and, while the database is open, in the WAL (PC-4 delete-residue probe,
93
+ * 2026-10-08). ON sets PRAGMA secure_delete, which zeroes deleted content, and
94
+ * vacuum() follows every expiry delete with wal_checkpoint(TRUNCATE), which
95
+ * empties the WAL of the frames that still hold it. OFF keeps SQLite's default:
96
+ * deleted bytes stay until the page is reused. secure_delete is per connection,
97
+ * so this is a per-broker setting, never a per-tenant one.
98
+ *
99
+ * Returns true or false; throws on anything that is not a recognised spelling,
100
+ * so a typo never silently turns the scrub off.
101
+ */
102
+ export function parseSecureDelete(value) {
103
+ if (value === undefined || value === null || value === true) return true;
104
+ const v = String(value).trim().toLowerCase();
105
+ if (["on", "1", "true", "yes"].includes(v)) return true;
106
+ if (["off", "0", "false", "no"].includes(v)) return false;
107
+ throw new Error(`secure-delete must be on or off, not "${value}"`);
108
+ }
84
109
 
85
110
  export class CarrierServer {
86
- constructor({ dbPath = "carrier.db", ttlDays = 14, cryptoInventory, trustedTierIssuers = [], keychain = null, publicUrl = null } = {}) {
111
+ constructor({ dbPath = "carrier.db", ttlDays = 14, cryptoInventory, trustedTierIssuers = [], keychain = null, publicUrl = null, secureDelete = true } = {}) {
87
112
  this.db = new Database(dbPath);
88
113
  this.db.pragma("journal_mode = WAL");
114
+ this.secureDelete = secureDelete !== false;
115
+ this.db.pragma(`secure_delete = ${this.secureDelete ? "ON" : "OFF"}`);
116
+ this.lastVacuum = null;
89
117
  this.db.exec(`
90
118
  create table if not exists envelopes (
91
119
  seq integer primary key autoincrement,
@@ -235,10 +263,35 @@ export class CarrierServer {
235
263
  return true;
236
264
  }
237
265
 
238
- /** Remove envelopes older than TTL. Call periodically. */
266
+ /**
267
+ * Remove envelopes older than TTL. Call periodically. Returns the number
268
+ * deleted. With secure delete on, a pass that deleted anything then runs
269
+ * wal_checkpoint(TRUNCATE) until it reports busy=0 (at most
270
+ * CHECKPOINT_ATTEMPTS tries); `lastVacuum` records the outcome, and
271
+ * `lastVacuum.scrubbed` is false when the checkpoint never completed.
272
+ */
239
273
  vacuum() {
240
274
  const cutoff = new Date(Date.now() - this.ttlDays * 86400_000).toISOString();
241
275
  const r = this.db.prepare("delete from envelopes where created_at < ?").run(cutoff);
276
+ const report = { deleted: r.changes, secureDelete: this.secureDelete, checkpoint: null, attempts: 0, scrubbed: null };
277
+ if (this.secureDelete && r.changes > 0) {
278
+ // vacuum() runs on the event loop: wait at most CHECKPOINT_BUSY_MS per
279
+ // try for a reader to finish, never the connection's 5 s default.
280
+ const busyTimeout = this.db.pragma("busy_timeout", { simple: true });
281
+ this.db.pragma(`busy_timeout = ${CHECKPOINT_BUSY_MS}`);
282
+ try {
283
+ while (report.attempts < CHECKPOINT_ATTEMPTS) {
284
+ report.attempts++;
285
+ const [cp] = this.db.pragma("wal_checkpoint(TRUNCATE)");
286
+ report.checkpoint = cp;
287
+ if (cp && cp.busy === 0) break;
288
+ }
289
+ } finally {
290
+ this.db.pragma(`busy_timeout = ${busyTimeout}`);
291
+ }
292
+ report.scrubbed = report.checkpoint?.busy === 0;
293
+ }
294
+ this.lastVacuum = report;
242
295
  return r.changes;
243
296
  }
244
297
 
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { createHash, randomBytes } from "node:crypto";
30
30
  import { jcs } from "../identity/jcs.mjs";
31
- import { Keychain } from "../identity/keychain.mjs";
31
+ import { Keychain, isStrictEd25519PublicKey } from "../identity/keychain.mjs";
32
32
  import { sealPayload, openPayload, E2E_ALG } from "../crypto/e2e.mjs";
33
33
  import { verifyDelegationDoc, delegationCoversKind, CAPABILITY_ONLY_KINDS } from "./delegation.mjs";
34
34
 
@@ -112,6 +112,7 @@ export function open(envelope, { cryptoInventory } = {}) {
112
112
  }
113
113
  if (sig.keyId !== envelope.sender) return { ok: false, reason: "keyId-sender-mismatch" };
114
114
  if (typeof sig.publicKey !== "string" || !/^[0-9a-f]{64}$/i.test(sig.publicKey)) return { ok: false, reason: "bad-publicKey" };
115
+ if (!isStrictEd25519PublicKey(sig.publicKey)) return { ok: false, reason: "weak-publicKey" }; // @rule envelope.open-strict-key
115
116
  if (typeof sig.signature !== "string" || !/^[0-9a-f]{128}$/i.test(sig.signature)) return { ok: false, reason: "bad-signature-encoding" };
116
117
  // Structural checks on the encryption block — an envelope without one is invalid.
117
118
  const enc = envelope.enc;
@@ -172,6 +173,7 @@ function openDelegated(envelope, { cryptoInventory } = {}) {
172
173
  const reject = (reason) => ({ ok: false, reason: `delegation-invalid:${reason}`, code: 403 });
173
174
  const sig = envelope.transportSignature;
174
175
  if (typeof sig.publicKey !== "string" || !/^[0-9a-f]{64}$/i.test(sig.publicKey)) return { ok: false, reason: "bad-publicKey" };
176
+ if (!isStrictEd25519PublicKey(sig.publicKey)) return { ok: false, reason: "weak-publicKey" }; // @rule envelope.open-delegated-strict-key
175
177
  if (typeof sig.signature !== "string" || !/^[0-9a-f]{128}$/i.test(sig.signature)) return { ok: false, reason: "bad-signature-encoding" };
176
178
  // Structural checks on the encryption block — same rules as the legacy path.
177
179
  const enc = envelope.enc;
@@ -32,6 +32,7 @@ export const REFUSAL_FIXES = Object.freeze({
32
32
  "keyId-sender-mismatch": "sign with transportSignature.keyId equal to the sender UUAID, or send under a delegation",
33
33
  "publicKey-sender-mismatch": "send from the UUAID your signing key derives, or attach the principal's delegation",
34
34
  "bad-signature": "sign again with this identity's own key over the exact bytes; the signature does not verify",
35
+ "weak-publicKey": "use a real Ed25519 identity key; this key is a small-order or non-canonical point that can sign anything, so no signature under it counts",
35
36
  "delegation-invalid": "mint a fresh delegation from the principal, then resend",
36
37
  // Carrier, envelope shape and crypto (envelope.mjs open()): one fix for all
37
38
  "not-object": RESEAL,