openclaw-nostr 2026.9.2 → 2026.9.4
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 +9 -0
- package/README.md +8 -3
- package/node_modules/cascadia-ts/cascadia.ts +95 -10
- package/node_modules/cascadia-ts/dist/cascadia.d.ts +39 -0
- package/node_modules/cascadia-ts/dist/cascadia.js +78 -10
- package/package.json +6 -4
- package/src/channel-directory.ts +5 -3
- package/src/channel.ts +62 -24
- package/src/config-schema.ts +7 -2
- package/src/fleet-tasks.ts +13 -1
- package/src/metrics.ts +11 -2
- package/src/nip46-signer.ts +32 -12
- package/src/nostr-bus-nip29.ts +19 -6
- package/src/nostr-bus-relay.auth-patch.test.ts +29 -6
- package/src/nostr-bus-relay.ts +35 -22
- package/src/nostr-bus-types.ts +5 -0
- package/src/nostr-bus.ts +28 -4
- package/src/nostr-capabilities-extended.test.ts +45 -0
- package/src/nostr-capabilities.ts +78 -1
- package/src/nostr-extras.ts +2 -1
- package/src/nostr-profile-http.ts +1 -1
- package/src/nostr-state-store.ts +3 -1
- package/src/nostr-taskflow-tool.test.ts +26 -0
- package/src/nostr-taskflow-tool.ts +51 -23
- package/src/setup-surface.ts +49 -6
- package/src/types.test.ts +28 -0
- package/src/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased (Cascadia Fork)
|
|
4
4
|
|
|
5
|
+
### Fixes — plugin fails to load on openclaw >= 2026.8 (ocn-o66w)
|
|
6
|
+
- **`ERR_PACKAGE_PATH_NOT_EXPORTED: './plugin-sdk/zod'`**: openclaw retired the `openclaw/plugin-sdk/zod` exports subpath (upstream `dceb2c343c4`, an early retirement of the August 15 compat batch), so gateways running openclaw 2026.8.x or newer failed to load the whole plugin at `index.ts`. The two importers (`src/config-schema.ts`, `src/nostr-profile-http.ts`) now import `z` from the `zod` package directly, as the upstream migration guide instructs, and `zod` is declared as a runtime dependency pinned to `4.4.3` to match the host's copy. Because openclaw inlines its own zod into `dist`, the plugin-built config schema is structurally identical but nominally distinct to TypeScript at the `buildChannelConfigSchema` boundary; the two host call sites carry a documented cast. Runtime behavior is unchanged.
|
|
7
|
+
|
|
8
|
+
### Fixes — Blossom auth helper
|
|
9
|
+
- **Blossom uploads now have a BUD-11 auth helper**: added `buildBlossomAuthEvent`, `getBlossomAuthToken`, bus-level `getBlossomAuthToken`, and channel-level `getNostrBlossomAuthToken` for kind:24242 Blossom auth events with `t`, `x`, `expiration`, and optional `server` tags. Updated docs/comments that previously pointed Blossom upload callers at generic NIP-98 kind:27235 tokens, which BUD-11-only Blossom servers reject before storage.
|
|
10
|
+
|
|
11
|
+
### Fixes — relay AUTH regression in 2026.9.2 (ocn-a4wg)
|
|
12
|
+
- **Upgrade from 2026.9.2.** That release swallowed every `relay.auth()` rejection and returned `false`, on the mistaken premise that nostr-tools never handles it. It handles it in two of three call sites: `SimplePool.subscribeMap` retries a CLOSED `auth-required:` via `auth().then(re-subscribe).catch(stop)`, and `SimplePool.publish` awaits it before republishing. Returning `false` made both treat a **failed** AUTH as success, so a NIP-29 room with `auth:true` re-subscribed unauthenticated in a loop — `closed: auth-required` → reconnect → repeat — with the relay's real refusal reason suppressed, which reads exactly like `auth:true` being ignored. `auth()` now rejects again; the returned promise carries a no-op `.catch()`, which marks it handled for Node's unhandled-rejection detection without consuming it, so the unawaited call in `AbstractRelay._onmessage` still cannot crash the process (the ocn-n5g guarantee) while awaiting callers keep their retry semantics.
|
|
13
|
+
|
|
5
14
|
### Fixes — relay AUTH refusal crashed the process (ocn-n5g, ocn-uao2)
|
|
6
15
|
- **A relay refusing NIP-42 AUTH no longer takes the gateway down (ocn-n5g)**: a relay answering our AUTH event with `["OK", <id>, false, "error: relay needs serviceUrl to be configured before AUTH can work"]` — a misconfigured relay that can never authenticate anyone — rejected the auth promise inside nostr-tools' `AbstractRelay._onmessage`, which invokes `this.auth(this.onauth)` without awaiting or catching it. The rejection escaped as an unhandled rejection and killed the process (stability bundle written), with a stack containing no frames of ours. Our prototype patch previously swallowed only `SendingOnClosedConnection` and `"auth timed out"`, so relay-side refusals sailed through. It now swallows every auth rejection and returns `false` — since the sole caller never handles the promise, rethrowing can only ever crash — and clears the memoized `authPromise` so a later challenge retries instead of replaying a cached rejection forever. The patch moved to `nostr-bus-relay.ts` and is now installed by the fleet-tasks pool too, which sets `automaticallyAuth` but never ran it and carried the same latent crash.
|
|
7
16
|
- **Failing relays are now named in the logs (ocn-uao2)**: the swallowed failure was reported as a bare string, so the offending relay was unidentifiable. Failures now carry a structured report (relay URL, kind, relay-supplied reason, occurrence and suppressed counts) and are classified `closed_connection` / `timed_out` / `no_challenge` / `relay_refused`, separating self-healing races from a relay that will never authenticate. Refusals route to `onError` so they are always visible in production logs; races route to `onDebug` and are lifted to `warn`. Repeats are throttled per relay+kind (first immediately, then at most one line per minute carrying the suppressed count) because a refusing relay re-refuses on every reconnect. AUTH sends dropped by the closed-connection guard, previously swallowed in silence, are now named too. Emits `relay.auth.rejected` tagged by relay and kind.
|
package/README.md
CHANGED
|
@@ -989,6 +989,7 @@ import {
|
|
|
989
989
|
getNostrRelayCapabilities,
|
|
990
990
|
// Auth
|
|
991
991
|
getNostrHttpAuthToken,
|
|
992
|
+
getNostrBlossomAuthToken,
|
|
992
993
|
// Media
|
|
993
994
|
publishNostrFileMetadata,
|
|
994
995
|
// Social
|
|
@@ -1041,8 +1042,12 @@ await bus.sendChannelMessage({
|
|
|
1041
1042
|
relayUrl: "wss://relay.example",
|
|
1042
1043
|
});
|
|
1043
1044
|
|
|
1044
|
-
// Generate
|
|
1045
|
-
const token = await bus.
|
|
1045
|
+
// Generate BUD-11 auth token for a Blossom upload
|
|
1046
|
+
const token = await bus.getBlossomAuthToken({
|
|
1047
|
+
verb: "upload",
|
|
1048
|
+
hashes: ["<sha256>"],
|
|
1049
|
+
server: "https://media.example.com",
|
|
1050
|
+
});
|
|
1046
1051
|
```
|
|
1047
1052
|
|
|
1048
1053
|
### Tier 3 Extras (Namespace Imports)
|
|
@@ -1117,7 +1122,7 @@ pnpm vitest run --config vitest.extensions.config.ts extensions/nostr/src/nostr-
|
|
|
1117
1122
|
- Event signatures are verified before processing
|
|
1118
1123
|
- Use environment variables for keys, never commit to config files
|
|
1119
1124
|
- Consider using `allowlist` mode in production
|
|
1120
|
-
- NIP-98 tokens are signed with the bus's key — scope them to specific URLs
|
|
1125
|
+
- NIP-98 and BUD-11 Blossom tokens are signed with the bus's key — scope them to specific URLs/servers and hashes
|
|
1121
1126
|
- NIP-44 conversation keys are derived from the bus's secret key
|
|
1122
1127
|
- HTTP mutation endpoints (PUT, POST, DELETE) are restricted to loopback addresses
|
|
1123
1128
|
- **NIP-46**: The client secret (`nip46Secret`) is distinct from the identity key — store it via `NOSTR_NIP46_SECRET` env var or a `SecretRef` (`env`, `file`, `exec`), never in raw config
|
|
@@ -116,32 +116,32 @@ export const KindMeta: Record<number, {
|
|
|
116
116
|
[CTXVM_SERVER_ANNOUNCEMENT]: {
|
|
117
117
|
symbol: "CTXVM_SERVER_ANNOUNCEMENT",
|
|
118
118
|
family: "ContextVM",
|
|
119
|
-
eventForm: "
|
|
120
|
-
requiredTags: [],
|
|
119
|
+
eventForm: "regular",
|
|
120
|
+
requiredTags: ["p"],
|
|
121
121
|
},
|
|
122
122
|
[CTXVM_TOOLS_ANNOUNCEMENT]: {
|
|
123
123
|
symbol: "CTXVM_TOOLS_ANNOUNCEMENT",
|
|
124
124
|
family: "ContextVM",
|
|
125
|
-
eventForm: "
|
|
126
|
-
requiredTags: [],
|
|
125
|
+
eventForm: "regular",
|
|
126
|
+
requiredTags: ["p"],
|
|
127
127
|
},
|
|
128
128
|
[CTXVM_RESOURCES_ANNOUNCEMENT]: {
|
|
129
129
|
symbol: "CTXVM_RESOURCES_ANNOUNCEMENT",
|
|
130
130
|
family: "ContextVM",
|
|
131
|
-
eventForm: "
|
|
132
|
-
requiredTags: [],
|
|
131
|
+
eventForm: "regular",
|
|
132
|
+
requiredTags: ["p"],
|
|
133
133
|
},
|
|
134
134
|
[CTXVM_RESOURCE_TEMPLATES_ANNOUNCEMENT]: {
|
|
135
135
|
symbol: "CTXVM_RESOURCE_TEMPLATES_ANNOUNCEMENT",
|
|
136
136
|
family: "ContextVM",
|
|
137
|
-
eventForm: "
|
|
138
|
-
requiredTags: [],
|
|
137
|
+
eventForm: "regular",
|
|
138
|
+
requiredTags: ["p"],
|
|
139
139
|
},
|
|
140
140
|
[CTXVM_PROMPTS_ANNOUNCEMENT]: {
|
|
141
141
|
symbol: "CTXVM_PROMPTS_ANNOUNCEMENT",
|
|
142
142
|
family: "ContextVM",
|
|
143
|
-
eventForm: "
|
|
144
|
-
requiredTags: [],
|
|
143
|
+
eventForm: "regular",
|
|
144
|
+
requiredTags: ["p"],
|
|
145
145
|
},
|
|
146
146
|
[NIP59_EPHEMERAL_GIFT_WRAP]: {
|
|
147
147
|
symbol: "NIP59_EPHEMERAL_GIFT_WRAP",
|
|
@@ -1443,6 +1443,91 @@ export function validateCascadiaLoomWorkerV1Payload(payload: CascadiaLoomWorkerV
|
|
|
1443
1443
|
return errors;
|
|
1444
1444
|
}
|
|
1445
1445
|
|
|
1446
|
+
export interface CascadiaMaintenancePressureResultV1Mount {
|
|
1447
|
+
path: string;
|
|
1448
|
+
total_bytes: number;
|
|
1449
|
+
free_bytes: number;
|
|
1450
|
+
used_pct: number;
|
|
1451
|
+
total_inodes: number;
|
|
1452
|
+
free_inodes: number;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
export function validateCascadiaMaintenancePressureResultV1Mount(payload: CascadiaMaintenancePressureResultV1Mount): string[] {
|
|
1456
|
+
const errors: string[] = [];
|
|
1457
|
+
if (payload.path === undefined || payload.path === null || payload.path === '') errors.push("cascadia.maintenance.pressure-result.v1.mounts[].path is required");
|
|
1458
|
+
if (payload.total_bytes === undefined || payload.total_bytes === null) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_bytes is required");
|
|
1459
|
+
if (payload.total_bytes !== undefined && payload.total_bytes < 0) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_bytes must be >= 0");
|
|
1460
|
+
if (payload.free_bytes === undefined || payload.free_bytes === null) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_bytes is required");
|
|
1461
|
+
if (payload.free_bytes !== undefined && payload.free_bytes < 0) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_bytes must be >= 0");
|
|
1462
|
+
if (payload.used_pct === undefined || payload.used_pct === null) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].used_pct is required");
|
|
1463
|
+
if (payload.used_pct !== undefined && payload.used_pct < 0) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].used_pct must be >= 0");
|
|
1464
|
+
if (payload.used_pct !== undefined && payload.used_pct > 100) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].used_pct must be <= 100");
|
|
1465
|
+
if (payload.total_inodes === undefined || payload.total_inodes === null) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_inodes is required");
|
|
1466
|
+
if (payload.total_inodes !== undefined && payload.total_inodes < 0) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_inodes must be >= 0");
|
|
1467
|
+
if (payload.free_inodes === undefined || payload.free_inodes === null) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_inodes is required");
|
|
1468
|
+
if (payload.free_inodes !== undefined && payload.free_inodes < 0) errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_inodes must be >= 0");
|
|
1469
|
+
return errors;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** Bounded fleet hygiene disk and inode pressure result payload
|
|
1473
|
+
* Schema: cascadia.maintenance.pressure-result.v1
|
|
1474
|
+
* CUE: schemas/control-plane/maintenance-pressure-result.cue (MaintenancePressureResult, MountPressure)
|
|
1475
|
+
*/
|
|
1476
|
+
export interface CascadiaMaintenancePressureResultV1Payload {
|
|
1477
|
+
mounts: CascadiaMaintenancePressureResultV1Mount[];
|
|
1478
|
+
sampled_at: string;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
export function validateCascadiaMaintenancePressureResultV1Payload(payload: CascadiaMaintenancePressureResultV1Payload): string[] {
|
|
1482
|
+
const errors: string[] = [];
|
|
1483
|
+
if (payload.mounts === undefined || payload.mounts === null) errors.push("cascadia.maintenance.pressure-result.v1.mounts is required");
|
|
1484
|
+
if (payload.mounts !== undefined) payload.mounts.forEach((item) => errors.push(...validateCascadiaMaintenancePressureResultV1Mount(item)));
|
|
1485
|
+
if (payload.sampled_at === undefined || payload.sampled_at === null || payload.sampled_at === '') errors.push("cascadia.maintenance.pressure-result.v1.sampled_at is required");
|
|
1486
|
+
return errors;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
export interface CascadiaMaintenanceScanResultV1Candidate {
|
|
1490
|
+
id: string;
|
|
1491
|
+
path: string;
|
|
1492
|
+
class: "dup-clone" | "orphan-clone" | "misplaced-backup" | "cruft";
|
|
1493
|
+
reason?: string;
|
|
1494
|
+
canonical?: string;
|
|
1495
|
+
blocked?: "dirty-tree" | "orphan" | "in-use" | "policy" | "protected-root";
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export function validateCascadiaMaintenanceScanResultV1Candidate(payload: CascadiaMaintenanceScanResultV1Candidate): string[] {
|
|
1499
|
+
const errors: string[] = [];
|
|
1500
|
+
if (payload.id === undefined || payload.id === null || payload.id === '') errors.push("cascadia.maintenance.scan-result.v1.candidates[].id is required");
|
|
1501
|
+
if (payload.path === undefined || payload.path === null || payload.path === '') errors.push("cascadia.maintenance.scan-result.v1.candidates[].path is required");
|
|
1502
|
+
if (payload.class === undefined || payload.class === null) errors.push("cascadia.maintenance.scan-result.v1.candidates[].class is required");
|
|
1503
|
+
if (payload.class !== undefined && !["dup-clone", "orphan-clone", "misplaced-backup", "cruft"].includes(payload.class as string)) errors.push("cascadia.maintenance.scan-result.v1.candidates[].class must be one of: dup-clone, orphan-clone, misplaced-backup, cruft");
|
|
1504
|
+
if (payload.blocked !== undefined && !["dirty-tree", "orphan", "in-use", "policy", "protected-root"].includes(payload.blocked as string)) errors.push("cascadia.maintenance.scan-result.v1.candidates[].blocked must be one of: dirty-tree, orphan, in-use, policy, protected-root");
|
|
1505
|
+
return errors;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/** Bounded fleet hygiene scan result payload
|
|
1509
|
+
* Schema: cascadia.maintenance.scan-result.v1
|
|
1510
|
+
* CUE: schemas/control-plane/maintenance-scan-result.cue (MaintenanceScanResult, MaintenanceCandidate)
|
|
1511
|
+
*/
|
|
1512
|
+
export interface CascadiaMaintenanceScanResultV1Payload {
|
|
1513
|
+
candidates: CascadiaMaintenanceScanResultV1Candidate[];
|
|
1514
|
+
scanned_at: string;
|
|
1515
|
+
roots?: string[];
|
|
1516
|
+
total_candidates: number;
|
|
1517
|
+
truncated?: boolean;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
export function validateCascadiaMaintenanceScanResultV1Payload(payload: CascadiaMaintenanceScanResultV1Payload): string[] {
|
|
1521
|
+
const errors: string[] = [];
|
|
1522
|
+
if (payload.candidates === undefined || payload.candidates === null) errors.push("cascadia.maintenance.scan-result.v1.candidates is required");
|
|
1523
|
+
if (payload.candidates !== undefined && payload.candidates.length > 500) errors.push("cascadia.maintenance.scan-result.v1.candidates must contain at most 500 items");
|
|
1524
|
+
if (payload.candidates !== undefined) payload.candidates.forEach((item) => errors.push(...validateCascadiaMaintenanceScanResultV1Candidate(item)));
|
|
1525
|
+
if (payload.scanned_at === undefined || payload.scanned_at === null || payload.scanned_at === '') errors.push("cascadia.maintenance.scan-result.v1.scanned_at is required");
|
|
1526
|
+
if (payload.total_candidates === undefined || payload.total_candidates === null) errors.push("cascadia.maintenance.scan-result.v1.total_candidates is required");
|
|
1527
|
+
if (payload.total_candidates !== undefined && payload.total_candidates < 0) errors.push("cascadia.maintenance.scan-result.v1.total_candidates must be >= 0");
|
|
1528
|
+
return errors;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1446
1531
|
/** Fleet hygiene (Swabbie) maintenance command payload
|
|
1447
1532
|
* Schema: cascadia.maintenance.v1
|
|
1448
1533
|
* CUE: schemas/control-plane/maintenance.cue (Maintenance)
|
|
@@ -675,6 +675,45 @@ export interface CascadiaLoomWorkerV1Payload {
|
|
|
675
675
|
pricing?: Record<string, unknown>;
|
|
676
676
|
}
|
|
677
677
|
export declare function validateCascadiaLoomWorkerV1Payload(payload: CascadiaLoomWorkerV1Payload): string[];
|
|
678
|
+
export interface CascadiaMaintenancePressureResultV1Mount {
|
|
679
|
+
path: string;
|
|
680
|
+
total_bytes: number;
|
|
681
|
+
free_bytes: number;
|
|
682
|
+
used_pct: number;
|
|
683
|
+
total_inodes: number;
|
|
684
|
+
free_inodes: number;
|
|
685
|
+
}
|
|
686
|
+
export declare function validateCascadiaMaintenancePressureResultV1Mount(payload: CascadiaMaintenancePressureResultV1Mount): string[];
|
|
687
|
+
/** Bounded fleet hygiene disk and inode pressure result payload
|
|
688
|
+
* Schema: cascadia.maintenance.pressure-result.v1
|
|
689
|
+
* CUE: schemas/control-plane/maintenance-pressure-result.cue (MaintenancePressureResult, MountPressure)
|
|
690
|
+
*/
|
|
691
|
+
export interface CascadiaMaintenancePressureResultV1Payload {
|
|
692
|
+
mounts: CascadiaMaintenancePressureResultV1Mount[];
|
|
693
|
+
sampled_at: string;
|
|
694
|
+
}
|
|
695
|
+
export declare function validateCascadiaMaintenancePressureResultV1Payload(payload: CascadiaMaintenancePressureResultV1Payload): string[];
|
|
696
|
+
export interface CascadiaMaintenanceScanResultV1Candidate {
|
|
697
|
+
id: string;
|
|
698
|
+
path: string;
|
|
699
|
+
class: "dup-clone" | "orphan-clone" | "misplaced-backup" | "cruft";
|
|
700
|
+
reason?: string;
|
|
701
|
+
canonical?: string;
|
|
702
|
+
blocked?: "dirty-tree" | "orphan" | "in-use" | "policy" | "protected-root";
|
|
703
|
+
}
|
|
704
|
+
export declare function validateCascadiaMaintenanceScanResultV1Candidate(payload: CascadiaMaintenanceScanResultV1Candidate): string[];
|
|
705
|
+
/** Bounded fleet hygiene scan result payload
|
|
706
|
+
* Schema: cascadia.maintenance.scan-result.v1
|
|
707
|
+
* CUE: schemas/control-plane/maintenance-scan-result.cue (MaintenanceScanResult, MaintenanceCandidate)
|
|
708
|
+
*/
|
|
709
|
+
export interface CascadiaMaintenanceScanResultV1Payload {
|
|
710
|
+
candidates: CascadiaMaintenanceScanResultV1Candidate[];
|
|
711
|
+
scanned_at: string;
|
|
712
|
+
roots?: string[];
|
|
713
|
+
total_candidates: number;
|
|
714
|
+
truncated?: boolean;
|
|
715
|
+
}
|
|
716
|
+
export declare function validateCascadiaMaintenanceScanResultV1Payload(payload: CascadiaMaintenanceScanResultV1Payload): string[];
|
|
678
717
|
/** Fleet hygiene (Swabbie) maintenance command payload
|
|
679
718
|
* Schema: cascadia.maintenance.v1
|
|
680
719
|
* CUE: schemas/control-plane/maintenance.cue (Maintenance)
|
|
@@ -88,32 +88,32 @@ export const KindMeta = {
|
|
|
88
88
|
[CTXVM_SERVER_ANNOUNCEMENT]: {
|
|
89
89
|
symbol: "CTXVM_SERVER_ANNOUNCEMENT",
|
|
90
90
|
family: "ContextVM",
|
|
91
|
-
eventForm: "
|
|
92
|
-
requiredTags: [],
|
|
91
|
+
eventForm: "regular",
|
|
92
|
+
requiredTags: ["p"],
|
|
93
93
|
},
|
|
94
94
|
[CTXVM_TOOLS_ANNOUNCEMENT]: {
|
|
95
95
|
symbol: "CTXVM_TOOLS_ANNOUNCEMENT",
|
|
96
96
|
family: "ContextVM",
|
|
97
|
-
eventForm: "
|
|
98
|
-
requiredTags: [],
|
|
97
|
+
eventForm: "regular",
|
|
98
|
+
requiredTags: ["p"],
|
|
99
99
|
},
|
|
100
100
|
[CTXVM_RESOURCES_ANNOUNCEMENT]: {
|
|
101
101
|
symbol: "CTXVM_RESOURCES_ANNOUNCEMENT",
|
|
102
102
|
family: "ContextVM",
|
|
103
|
-
eventForm: "
|
|
104
|
-
requiredTags: [],
|
|
103
|
+
eventForm: "regular",
|
|
104
|
+
requiredTags: ["p"],
|
|
105
105
|
},
|
|
106
106
|
[CTXVM_RESOURCE_TEMPLATES_ANNOUNCEMENT]: {
|
|
107
107
|
symbol: "CTXVM_RESOURCE_TEMPLATES_ANNOUNCEMENT",
|
|
108
108
|
family: "ContextVM",
|
|
109
|
-
eventForm: "
|
|
110
|
-
requiredTags: [],
|
|
109
|
+
eventForm: "regular",
|
|
110
|
+
requiredTags: ["p"],
|
|
111
111
|
},
|
|
112
112
|
[CTXVM_PROMPTS_ANNOUNCEMENT]: {
|
|
113
113
|
symbol: "CTXVM_PROMPTS_ANNOUNCEMENT",
|
|
114
114
|
family: "ContextVM",
|
|
115
|
-
eventForm: "
|
|
116
|
-
requiredTags: [],
|
|
115
|
+
eventForm: "regular",
|
|
116
|
+
requiredTags: ["p"],
|
|
117
117
|
},
|
|
118
118
|
[NIP59_EPHEMERAL_GIFT_WRAP]: {
|
|
119
119
|
symbol: "NIP59_EPHEMERAL_GIFT_WRAP",
|
|
@@ -945,6 +945,74 @@ export function validateCascadiaLoomWorkerV1Payload(payload) {
|
|
|
945
945
|
errors.push("cascadia.loom.worker.v1.capacity is required");
|
|
946
946
|
return errors;
|
|
947
947
|
}
|
|
948
|
+
export function validateCascadiaMaintenancePressureResultV1Mount(payload) {
|
|
949
|
+
const errors = [];
|
|
950
|
+
if (payload.path === undefined || payload.path === null || payload.path === '')
|
|
951
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].path is required");
|
|
952
|
+
if (payload.total_bytes === undefined || payload.total_bytes === null)
|
|
953
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_bytes is required");
|
|
954
|
+
if (payload.total_bytes !== undefined && payload.total_bytes < 0)
|
|
955
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_bytes must be >= 0");
|
|
956
|
+
if (payload.free_bytes === undefined || payload.free_bytes === null)
|
|
957
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_bytes is required");
|
|
958
|
+
if (payload.free_bytes !== undefined && payload.free_bytes < 0)
|
|
959
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_bytes must be >= 0");
|
|
960
|
+
if (payload.used_pct === undefined || payload.used_pct === null)
|
|
961
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].used_pct is required");
|
|
962
|
+
if (payload.used_pct !== undefined && payload.used_pct < 0)
|
|
963
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].used_pct must be >= 0");
|
|
964
|
+
if (payload.used_pct !== undefined && payload.used_pct > 100)
|
|
965
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].used_pct must be <= 100");
|
|
966
|
+
if (payload.total_inodes === undefined || payload.total_inodes === null)
|
|
967
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_inodes is required");
|
|
968
|
+
if (payload.total_inodes !== undefined && payload.total_inodes < 0)
|
|
969
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].total_inodes must be >= 0");
|
|
970
|
+
if (payload.free_inodes === undefined || payload.free_inodes === null)
|
|
971
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_inodes is required");
|
|
972
|
+
if (payload.free_inodes !== undefined && payload.free_inodes < 0)
|
|
973
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts[].free_inodes must be >= 0");
|
|
974
|
+
return errors;
|
|
975
|
+
}
|
|
976
|
+
export function validateCascadiaMaintenancePressureResultV1Payload(payload) {
|
|
977
|
+
const errors = [];
|
|
978
|
+
if (payload.mounts === undefined || payload.mounts === null)
|
|
979
|
+
errors.push("cascadia.maintenance.pressure-result.v1.mounts is required");
|
|
980
|
+
if (payload.mounts !== undefined)
|
|
981
|
+
payload.mounts.forEach((item) => errors.push(...validateCascadiaMaintenancePressureResultV1Mount(item)));
|
|
982
|
+
if (payload.sampled_at === undefined || payload.sampled_at === null || payload.sampled_at === '')
|
|
983
|
+
errors.push("cascadia.maintenance.pressure-result.v1.sampled_at is required");
|
|
984
|
+
return errors;
|
|
985
|
+
}
|
|
986
|
+
export function validateCascadiaMaintenanceScanResultV1Candidate(payload) {
|
|
987
|
+
const errors = [];
|
|
988
|
+
if (payload.id === undefined || payload.id === null || payload.id === '')
|
|
989
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates[].id is required");
|
|
990
|
+
if (payload.path === undefined || payload.path === null || payload.path === '')
|
|
991
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates[].path is required");
|
|
992
|
+
if (payload.class === undefined || payload.class === null)
|
|
993
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates[].class is required");
|
|
994
|
+
if (payload.class !== undefined && !["dup-clone", "orphan-clone", "misplaced-backup", "cruft"].includes(payload.class))
|
|
995
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates[].class must be one of: dup-clone, orphan-clone, misplaced-backup, cruft");
|
|
996
|
+
if (payload.blocked !== undefined && !["dirty-tree", "orphan", "in-use", "policy", "protected-root"].includes(payload.blocked))
|
|
997
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates[].blocked must be one of: dirty-tree, orphan, in-use, policy, protected-root");
|
|
998
|
+
return errors;
|
|
999
|
+
}
|
|
1000
|
+
export function validateCascadiaMaintenanceScanResultV1Payload(payload) {
|
|
1001
|
+
const errors = [];
|
|
1002
|
+
if (payload.candidates === undefined || payload.candidates === null)
|
|
1003
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates is required");
|
|
1004
|
+
if (payload.candidates !== undefined && payload.candidates.length > 500)
|
|
1005
|
+
errors.push("cascadia.maintenance.scan-result.v1.candidates must contain at most 500 items");
|
|
1006
|
+
if (payload.candidates !== undefined)
|
|
1007
|
+
payload.candidates.forEach((item) => errors.push(...validateCascadiaMaintenanceScanResultV1Candidate(item)));
|
|
1008
|
+
if (payload.scanned_at === undefined || payload.scanned_at === null || payload.scanned_at === '')
|
|
1009
|
+
errors.push("cascadia.maintenance.scan-result.v1.scanned_at is required");
|
|
1010
|
+
if (payload.total_candidates === undefined || payload.total_candidates === null)
|
|
1011
|
+
errors.push("cascadia.maintenance.scan-result.v1.total_candidates is required");
|
|
1012
|
+
if (payload.total_candidates !== undefined && payload.total_candidates < 0)
|
|
1013
|
+
errors.push("cascadia.maintenance.scan-result.v1.total_candidates must be >= 0");
|
|
1014
|
+
return errors;
|
|
1015
|
+
}
|
|
948
1016
|
export function validateCascadiaMaintenanceV1Payload(payload) {
|
|
949
1017
|
const errors = [];
|
|
950
1018
|
return errors;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-nostr",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.4",
|
|
4
4
|
"description": "Nostr channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,14 +16,16 @@
|
|
|
16
16
|
"CHANGELOG.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tsc
|
|
19
|
+
"build": "tsc -p tsconfig.json --noCheck",
|
|
20
20
|
"test:bootstrap": "node --test test/nostr-claw-bootstrap.test.mjs",
|
|
21
|
-
"test:scanner": "vitest run test/runtime-security-scanner.test.ts"
|
|
21
|
+
"test:scanner": "vitest run test/runtime-security-scanner.test.ts",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"cascadia-ts": "file:../cascadia-nips/generated/typescript",
|
|
25
26
|
"nostr-tools": "2.23.3",
|
|
26
|
-
"ws": "^8.18.0"
|
|
27
|
+
"ws": "^8.18.0",
|
|
28
|
+
"zod": "4.4.3"
|
|
27
29
|
},
|
|
28
30
|
"bundledDependencies": [
|
|
29
31
|
"cascadia-ts"
|
package/src/channel-directory.ts
CHANGED
|
@@ -40,7 +40,7 @@ type RoomConfigLike = {
|
|
|
40
40
|
type NostrChannelCfgLike = {
|
|
41
41
|
channels?: {
|
|
42
42
|
nostr?: {
|
|
43
|
-
allowFrom?: readonly string[];
|
|
43
|
+
allowFrom?: readonly (string | number)[];
|
|
44
44
|
profile?: { name?: string; displayName?: string };
|
|
45
45
|
nostr_channels?: Record<string, RoomConfigLike | undefined>;
|
|
46
46
|
};
|
|
@@ -82,8 +82,10 @@ function toNpub(pubkeyHex: string): string {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// See normalizeAllowEntry in nostr-taskflow-tool.ts: allowFrom entries may be
|
|
86
|
+
// numbers, so stringify before trimming.
|
|
87
|
+
function normalizeEntryPubkey(value: string | number): string | undefined {
|
|
88
|
+
const trimmed = String(value).trim();
|
|
87
89
|
if (!trimmed || trimmed === "*") {
|
|
88
90
|
return undefined;
|
|
89
91
|
}
|
package/src/channel.ts
CHANGED
|
@@ -104,6 +104,7 @@ import type {
|
|
|
104
104
|
ReactionTarget,
|
|
105
105
|
DeletionOptions,
|
|
106
106
|
RepostOptions,
|
|
107
|
+
BlossomAuthTokenOptions,
|
|
107
108
|
FileMetadataObject,
|
|
108
109
|
} from "./nostr-capabilities.js";
|
|
109
110
|
import {
|
|
@@ -356,7 +357,7 @@ type NostrGroupInboundContextParams = {
|
|
|
356
357
|
roomConfig?: NostrRoomChannelMap[string];
|
|
357
358
|
preflight?: NostrGroupPreflightResult;
|
|
358
359
|
cfg?: unknown;
|
|
359
|
-
accountAllowFrom?: readonly string[];
|
|
360
|
+
accountAllowFrom?: readonly (string | number)[];
|
|
360
361
|
commandHelpers?: NostrGroupPreflightCommandHelpers;
|
|
361
362
|
/** P3: a pending-commitment reminder injected as untrusted context for this turn. */
|
|
362
363
|
commitmentReminder?: string;
|
|
@@ -373,7 +374,8 @@ type NostrChannelInboundEventContextParams = {
|
|
|
373
374
|
messageIdFull?: string;
|
|
374
375
|
timestamp?: number;
|
|
375
376
|
from: string;
|
|
376
|
-
|
|
377
|
+
/** Optional: system-authored room events have no actor pubkey. */
|
|
378
|
+
sender?: {
|
|
377
379
|
id?: string;
|
|
378
380
|
name?: string;
|
|
379
381
|
displayLabel?: string;
|
|
@@ -509,12 +511,12 @@ function buildChannelInboundEventContextCompat(params: NostrChannelInboundEventC
|
|
|
509
511
|
ConversationLabel: params.conversation.label,
|
|
510
512
|
GroupSubject: params.conversation.kind !== "direct" ? params.conversation.label : undefined,
|
|
511
513
|
UntrustedStructuredContext: params.supplemental?.untrustedContext,
|
|
512
|
-
SenderName: params.sender
|
|
513
|
-
SenderId: params.sender
|
|
514
|
-
SenderIsBot: params.sender
|
|
515
|
-
SenderUsername: params.sender
|
|
516
|
-
SenderTag: params.sender
|
|
517
|
-
MemberRoleIds: params.sender
|
|
514
|
+
SenderName: params.sender?.name ?? params.sender?.displayLabel,
|
|
515
|
+
SenderId: params.sender?.id,
|
|
516
|
+
SenderIsBot: params.sender?.isBot,
|
|
517
|
+
SenderUsername: params.sender?.username,
|
|
518
|
+
SenderTag: params.sender?.tag,
|
|
519
|
+
MemberRoleIds: params.sender?.roles,
|
|
518
520
|
Timestamp: params.timestamp,
|
|
519
521
|
Provider: params.provider ?? params.channel,
|
|
520
522
|
Surface: params.surface ?? params.provider ?? params.channel,
|
|
@@ -922,8 +924,11 @@ async function assertBackedNostrRoomCommitment(params: {
|
|
|
922
924
|
}),
|
|
923
925
|
);
|
|
924
926
|
const assessment = await assessCommitment(params.text, roomKey, (reference) => {
|
|
927
|
+
// `effective` is a FleetTaskEventSnapshot; the status lives on its `task`
|
|
928
|
+
// payload. Reading it off the snapshot yielded undefined, so every
|
|
929
|
+
// referenced task — closed ones included — counted as still active.
|
|
925
930
|
const task = activeFleetTaskServices.get(params.accountId)?.getTask(reference)?.effective;
|
|
926
|
-
if (task && task.status !== "closed") return true;
|
|
931
|
+
if (task && task.task.status !== "closed") return true;
|
|
927
932
|
return resolveNostrTaskFlowBackingActive(
|
|
928
933
|
getNostrRuntime(),
|
|
929
934
|
params.accountId,
|
|
@@ -1297,8 +1302,11 @@ function localIsNostrAbortRequestText(text: string): boolean {
|
|
|
1297
1302
|
return /^(?:abort|cancel|stop)\b/i.test(text.trim());
|
|
1298
1303
|
}
|
|
1299
1304
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1305
|
+
// allowFrom entries may be numbers (the host's shared AllowFromListSchema is
|
|
1306
|
+
// ZodArray<ZodUnion<[ZodString, ZodNumber]>>), so stringify before trimming —
|
|
1307
|
+
// .trim() on a number throws, and this path gates control-command authorization.
|
|
1308
|
+
function normalizeNostrAllowEntry(value: string | number): string | undefined {
|
|
1309
|
+
const trimmed = String(value).trim();
|
|
1302
1310
|
if (!trimmed || trimmed === "*") {
|
|
1303
1311
|
return undefined;
|
|
1304
1312
|
}
|
|
@@ -1306,7 +1314,7 @@ function normalizeNostrAllowEntry(value: string): string | undefined {
|
|
|
1306
1314
|
}
|
|
1307
1315
|
|
|
1308
1316
|
function nostrAllowListExplicitlyIncludesSender(
|
|
1309
|
-
allowFrom: readonly string[] | undefined,
|
|
1317
|
+
allowFrom: readonly (string | number)[] | undefined,
|
|
1310
1318
|
senderPubkey: string,
|
|
1311
1319
|
): boolean {
|
|
1312
1320
|
const normalizedSender = senderPubkey.toLowerCase();
|
|
@@ -1317,7 +1325,7 @@ function resolveNostrCommandAuthorized(params: {
|
|
|
1317
1325
|
hasControlCommand: boolean;
|
|
1318
1326
|
senderPubkey: string;
|
|
1319
1327
|
roomConfig?: NostrRoomChannelMap[string];
|
|
1320
|
-
accountAllowFrom?: readonly string[];
|
|
1328
|
+
accountAllowFrom?: readonly (string | number)[];
|
|
1321
1329
|
}): boolean {
|
|
1322
1330
|
if (!params.hasControlCommand) {
|
|
1323
1331
|
return false;
|
|
@@ -1446,7 +1454,7 @@ export function resolveNostrGroupPreflight(params: {
|
|
|
1446
1454
|
route: Pick<NostrGroupRouteFacts, "agentId">;
|
|
1447
1455
|
meta: InboundGroupMessageMeta;
|
|
1448
1456
|
roomConfig?: NostrRoomChannelMap[string];
|
|
1449
|
-
accountAllowFrom?: readonly string[];
|
|
1457
|
+
accountAllowFrom?: readonly (string | number)[];
|
|
1450
1458
|
commandHelpers?: NostrGroupPreflightCommandHelpers;
|
|
1451
1459
|
/** True when the sender is a peer for implicit-mention suppression (P1); may include unknownAsPeer. */
|
|
1452
1460
|
senderIsPeer?: boolean;
|
|
@@ -1612,13 +1620,17 @@ function buildNostrGroupMembershipInboundEventContext(params: {
|
|
|
1612
1620
|
messageIdFull: params.meta.eventId,
|
|
1613
1621
|
timestamp: params.meta.createdAt ? params.meta.createdAt * 1000 : Date.now(),
|
|
1614
1622
|
from: params.meta.actorPubkey ? `nostr:${params.meta.actorPubkey}` : "nostr:system",
|
|
1615
|
-
sender
|
|
1623
|
+
// Omit `sender` entirely when there is no actor, rather than setting it to
|
|
1624
|
+
// undefined: the host's param type does not accept an explicit undefined.
|
|
1625
|
+
...(params.meta.actorPubkey
|
|
1616
1626
|
? {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1627
|
+
sender: {
|
|
1628
|
+
id: params.meta.actorPubkey,
|
|
1629
|
+
name: nip19.npubEncode(params.meta.actorPubkey),
|
|
1630
|
+
displayLabel: nip19.npubEncode(params.meta.actorPubkey),
|
|
1631
|
+
},
|
|
1620
1632
|
}
|
|
1621
|
-
:
|
|
1633
|
+
: {}),
|
|
1622
1634
|
conversation: {
|
|
1623
1635
|
kind: "group",
|
|
1624
1636
|
id: roomKey,
|
|
@@ -2199,7 +2211,10 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
2199
2211
|
unsend: true,
|
|
2200
2212
|
},
|
|
2201
2213
|
reload: { configPrefixes: ["channels.nostr"] },
|
|
2202
|
-
|
|
2214
|
+
// See config-schema.ts: bridges the host-bundled vs. package zod type seam.
|
|
2215
|
+
configSchema: buildChannelConfigSchema(
|
|
2216
|
+
NostrConfigSchema as unknown as Parameters<typeof buildChannelConfigSchema>[0],
|
|
2217
|
+
),
|
|
2203
2218
|
|
|
2204
2219
|
config: {
|
|
2205
2220
|
listAccountIds: (cfg) => listNostrAccountIds(cfg),
|
|
@@ -2868,7 +2883,7 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
2868
2883
|
);
|
|
2869
2884
|
}
|
|
2870
2885
|
}
|
|
2871
|
-
nip46SessionState = sessionState;
|
|
2886
|
+
nip46SessionState = sessionState ?? undefined;
|
|
2872
2887
|
|
|
2873
2888
|
ctx.setStatus({
|
|
2874
2889
|
accountId: account.accountId,
|
|
@@ -3226,6 +3241,10 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
3226
3241
|
hasControlCommand: runtime.channel.text?.hasControlCommand,
|
|
3227
3242
|
shouldHandleTextCommands: runtime.channel.commands?.shouldHandleTextCommands,
|
|
3228
3243
|
};
|
|
3244
|
+
const accountAllowFrom = resolveNostrAccount({
|
|
3245
|
+
cfg,
|
|
3246
|
+
accountId: account.accountId,
|
|
3247
|
+
}).config.allowFrom;
|
|
3229
3248
|
// Classify the sender via the kind:0 bot index. isPeerAgent is a
|
|
3230
3249
|
// synchronous cache read that schedules a background fetch on a
|
|
3231
3250
|
// miss — safe on the admission-gate path. A DEFINITIVE known bot
|
|
@@ -3248,7 +3267,7 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
3248
3267
|
roomConfig,
|
|
3249
3268
|
route,
|
|
3250
3269
|
meta,
|
|
3251
|
-
accountAllowFrom
|
|
3270
|
+
accountAllowFrom,
|
|
3252
3271
|
commandHelpers,
|
|
3253
3272
|
senderIsPeer,
|
|
3254
3273
|
senderIsKnownBot,
|
|
@@ -3614,7 +3633,6 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
3614
3633
|
route: prepared.route,
|
|
3615
3634
|
meta,
|
|
3616
3635
|
cfg: prepared.cfg,
|
|
3617
|
-
accountAllowFrom: account.config.allowFrom,
|
|
3618
3636
|
commandHelpers: prepared.commandHelpers,
|
|
3619
3637
|
preflight: prepared.preflight,
|
|
3620
3638
|
commitmentReminder,
|
|
@@ -4304,7 +4322,6 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
4304
4322
|
route,
|
|
4305
4323
|
meta,
|
|
4306
4324
|
cfg,
|
|
4307
|
-
accountAllowFrom: account.config.allowFrom,
|
|
4308
4325
|
commandHelpers,
|
|
4309
4326
|
preflight,
|
|
4310
4327
|
commitmentReminder,
|
|
@@ -5448,6 +5465,27 @@ export async function getNostrHttpAuthToken(
|
|
|
5448
5465
|
return bus.getNip98Token(url, method);
|
|
5449
5466
|
}
|
|
5450
5467
|
|
|
5468
|
+
// ============================================================================
|
|
5469
|
+
// BUD-11: Blossom Auth
|
|
5470
|
+
// ============================================================================
|
|
5471
|
+
|
|
5472
|
+
/**
|
|
5473
|
+
* Generate a BUD-11 Blossom auth token.
|
|
5474
|
+
* @param opts - Blossom operation, optional hash scopes, expiration, and server
|
|
5475
|
+
* @param accountId - Account ID (defaults to "default")
|
|
5476
|
+
* @returns Base64url encoded kind:24242 auth event without the "Nostr " prefix
|
|
5477
|
+
*/
|
|
5478
|
+
export async function getNostrBlossomAuthToken(
|
|
5479
|
+
opts: BlossomAuthTokenOptions,
|
|
5480
|
+
accountId: string = DEFAULT_ACCOUNT_ID,
|
|
5481
|
+
): Promise<string> {
|
|
5482
|
+
const bus = activeBuses.get(accountId);
|
|
5483
|
+
if (!bus) {
|
|
5484
|
+
throw nostrBusNotRunningError(accountId);
|
|
5485
|
+
}
|
|
5486
|
+
return bus.getBlossomAuthToken(opts);
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5451
5489
|
// ============================================================================
|
|
5452
5490
|
// NIP-94: File Metadata
|
|
5453
5491
|
// ============================================================================
|
package/src/config-schema.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
DmPolicySchema,
|
|
5
5
|
MarkdownConfigSchema,
|
|
6
6
|
} from "openclaw/plugin-sdk/channel-config-primitives";
|
|
7
|
-
import { z } from "
|
|
7
|
+
import { z } from "zod";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Validates https:// URLs only (no javascript:, data:, file:, etc.)
|
|
@@ -686,4 +686,9 @@ export type NostrConfig = z.infer<typeof NostrConfigSchema>;
|
|
|
686
686
|
/**
|
|
687
687
|
* JSON Schema for Control UI (converted from Zod)
|
|
688
688
|
*/
|
|
689
|
-
|
|
689
|
+
// The host bundles its own zod copy into openclaw/dist, so a schema built with
|
|
690
|
+
// the `zod` package is structurally identical but nominally distinct to TS.
|
|
691
|
+
// Runtime is unaffected (both are zod 4.4.3); the cast bridges the type seam.
|
|
692
|
+
export const nostrChannelConfigSchema = buildChannelConfigSchema(
|
|
693
|
+
NostrConfigSchema as unknown as Parameters<typeof buildChannelConfigSchema>[0],
|
|
694
|
+
);
|