wire-mesh-core 1.35.1 → 1.36.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.
@@ -0,0 +1,127 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_generated_protocol = require("../generated/protocol.cjs");
3
+ const require_domain_data_sync = require("./data-sync.cjs");
4
+ const require_domain_group_key = require("./group-key.cjs");
5
+ const require_domain_room = require("./room.cjs");
6
+ let cbor2 = require("cbor2");
7
+ //#region src/domain/notice-board.ts
8
+ /**
9
+ * The application-ready noticeboard (spec/room.cddl's own noticeboard extension of core/room, wire-mesh#36): composes core/data's oplog replication (data-sync.ts), room-notice signing and verification (room.ts), and the room.rekey content-key machinery (group-key.ts, room-rekey.ts) into the one primitive an application actually wires -- post an encrypted notice to your own log, ingest a replicated peer entry, read back a room's notices verified and (where this side holds the epoch key) decrypted.
10
+ *
11
+ * Deliberately NOT included here, matching data-sync.ts's own "primitive vs. policy" split: when to send the returned data-have frame, which peers' logs to track, how to fan a catch-up request out -- all application policy. The board only guarantees what its pieces each already guarantee individually: own-log appends are monotonic, peer ingests are gap-rejecting, and nothing is ever surfaced unverified.
12
+ *
13
+ * readPeerNotices' honest read-confidentiality behaviour: a notice this side cannot decrypt (no key for its epoch -- a pre-join notice, or a post-kick epoch) still verifies and still appears in the read, with plaintext undefined. Authenticity and confidentiality are independent properties; the board never conflates them.
14
+ */
15
+ /** The opaque byte length a caller-omitted noticeId gets: 16 random bytes, the same sizing token-id already uses. */
16
+ const GENERATED_NOTICE_ID_BYTE_LENGTH = 16;
17
+ /** Serialises a minted notice to the canonical CBOR bytes an oplog entry carries. */
18
+ function noticeToEntryBytes(notice) {
19
+ return new Uint8Array((0, cbor2.encode)(notice, cbor2.cdeEncodeOptions));
20
+ }
21
+ function createNoticeBoard(options) {
22
+ const { identity, storage, clock, revocation, roomKeys } = options;
23
+ async function postEncryptedNotice(post) {
24
+ const keyEpoch = await roomKeys.currentEpoch(post.room);
25
+ if (keyEpoch === void 0) throw new Error(`no content key held for room ${post.room}: room.rekey this room before posting encrypted notices`);
26
+ const contentKey = await roomKeys.get(post.room, keyEpoch);
27
+ if (contentKey === void 0) throw new Error(`content key store is inconsistent: epoch ${String(keyEpoch)} is current for room ${post.room} but holds no key`);
28
+ const noticeId = post.noticeId ?? crypto.getRandomValues(new Uint8Array(GENERATED_NOTICE_ID_BYTE_LENGTH));
29
+ const ciphertext = await require_domain_group_key.encryptNoticeContent(contentKey, post.plaintext);
30
+ const notice = await require_domain_room.createRoomNotice({
31
+ identity,
32
+ clock,
33
+ room: post.room,
34
+ token: post.token,
35
+ noticeId,
36
+ contentType: require_domain_group_key.encryptedContentType(post.contentType),
37
+ content: ciphertext,
38
+ keyEpoch
39
+ });
40
+ const { haveFrame: dataHave } = await require_domain_data_sync.appendOwnEntry({
41
+ identity,
42
+ storage
43
+ }, noticeToEntryBytes(notice));
44
+ return {
45
+ dataHave,
46
+ notice,
47
+ keyEpoch
48
+ };
49
+ }
50
+ async function ingestPeerEntry(peer, entry) {
51
+ const fromSeq = await require_domain_data_sync.headSeqFor(storage, peer);
52
+ if (!(await require_domain_data_sync.handleDataEntries(storage, {
53
+ type: "data-entries",
54
+ peer,
55
+ "from-seq": fromSeq,
56
+ entries: [entry]
57
+ })).ok) throw new Error(`gap rejecting entry for peer log (head ${String(fromSeq)})`);
58
+ }
59
+ async function readLog(peer, room) {
60
+ const entries = await require_domain_data_sync.readEntries(storage, peer, 0);
61
+ const out = [];
62
+ for (const entry of entries) out.push(await readOne(entry, room));
63
+ return out;
64
+ }
65
+ async function readOne(entry, room) {
66
+ const notice = decodeNotice(entry);
67
+ if (notice === void 0) return {
68
+ verified: false,
69
+ reason: "malformed"
70
+ };
71
+ const verdict = await require_domain_room.verifyRoomNotice(notice, {
72
+ identity,
73
+ clock,
74
+ revocation,
75
+ expectedRoom: room
76
+ });
77
+ if (!verdict.ok) return {
78
+ verified: false,
79
+ reason: verdict.reason
80
+ };
81
+ const claims = verdict.claims;
82
+ if (!require_domain_group_key.isEncryptedContentType(claims["content-type"])) return {
83
+ verified: true,
84
+ contentType: claims["content-type"],
85
+ plaintext: claims.content,
86
+ claims
87
+ };
88
+ const keyEpoch = claims["key-epoch"];
89
+ if (keyEpoch === void 0) return {
90
+ verified: false,
91
+ reason: "key_epoch_mismatch"
92
+ };
93
+ const contentKey = await roomKeys.get(claims.room, keyEpoch);
94
+ const base = {
95
+ verified: true,
96
+ contentType: require_domain_group_key.plaintextContentType(claims["content-type"]),
97
+ claims
98
+ };
99
+ if (contentKey === void 0) return base;
100
+ try {
101
+ const plaintext = await require_domain_group_key.decryptNoticeContent(contentKey, claims.content);
102
+ return {
103
+ ...base,
104
+ plaintext
105
+ };
106
+ } catch {
107
+ return base;
108
+ }
109
+ }
110
+ function decodeNotice(entry) {
111
+ try {
112
+ const decoded = (0, cbor2.decode)(entry, cbor2.cdeDecodeOptions);
113
+ const parsed = require_generated_protocol.roomNoticeSchema.safeParse(decoded);
114
+ return parsed.success ? parsed.data : void 0;
115
+ } catch {
116
+ return;
117
+ }
118
+ }
119
+ return {
120
+ postEncryptedNotice,
121
+ ingestPeerEntry,
122
+ readOwnNotices: async (room) => readLog(identity.deviceId, room),
123
+ readPeerNotices: async (peer, room) => readLog(peer, room)
124
+ };
125
+ }
126
+ //#endregion
127
+ exports.createNoticeBoard = createNoticeBoard;
@@ -0,0 +1,60 @@
1
+ import { D as DeviceId, Et as RoomNoticeClaims, T as DataHaveFrame, Tt as RoomNotice, p as CapabilityToken } from "../protocol-gbXtNeOc.cjs";
2
+ import { KeyValueStorage } from "../ports/storage.cjs";
3
+ import { t as IdentityPort } from "../identity-B4XBWkqg.cjs";
4
+ import { t as Clock } from "../clock-DiSx-WKM.cjs";
5
+ import { a as RevocationCheck } from "../tokens-C9Kwy0Gk.cjs";
6
+ import { RoomNoticeVerdictReason } from "./room.cjs";
7
+ //#region src/domain/notice-board.d.ts
8
+ /**
9
+ * Where an application keeps the content keys its createRoomRekeyHandler unwraps, and where the owner's own side stores every epoch key it generated. Room-keyed and epoch-keyed, because a reader must locate exactly the epoch a notice names, never guess.
10
+ */
11
+ export interface RoomKeyStore {
12
+ get: (room: string, epoch: number) => Promise<Uint8Array | undefined>;
13
+ set: (room: string, epoch: number, key: Uint8Array) => void;
14
+ /** The highest epoch held for a room -- what postEncryptedNotice stamps a new notice with. */
15
+ currentEpoch: (room: string) => Promise<number | undefined>;
16
+ }
17
+ /** One notice as read back from a log: always verified-or-not first, decrypted only when both the signature and the epoch key are held. */
18
+ export interface NoticeBoardEntry {
19
+ /** Whether verifyRoomNotice passed every obligation. False means nothing else on this entry is populated except reason -- unverified content is never surfaced, not even as ciphertext. */
20
+ verified: boolean;
21
+ /** Why verification failed, when it did. */
22
+ reason?: RoomNoticeVerdictReason;
23
+ /** The notice's own TRUE (suffix-stripped) content-type, on a verified entry. */
24
+ contentType?: string;
25
+ /** The decrypted content, present iff verified AND this side holds the epoch key (or the notice was never encrypted). Undefined means "valid notice, can't read it" -- the confidentiality property, not an error. */
26
+ plaintext?: Uint8Array;
27
+ /** The fully verified claims, on a verified entry -- posted-at, poster, refs, everything a UI renders around the content itself. */
28
+ claims?: RoomNoticeClaims;
29
+ }
30
+ export interface CreateNoticeBoardOptions {
31
+ identity: IdentityPort;
32
+ storage: KeyValueStorage;
33
+ clock: Clock;
34
+ revocation: RevocationCheck;
35
+ roomKeys: RoomKeyStore;
36
+ }
37
+ export interface PostEncryptedNoticeOptions {
38
+ room: string;
39
+ /** The poster's own room:member token, embedded in full in the notice. */
40
+ token: CapabilityToken;
41
+ /** The notice's true content-type; the +aes256gcm suffix is applied here, never by the caller. */
42
+ contentType: string;
43
+ plaintext: Uint8Array;
44
+ /** Caller-supplied notice id, matching createRoomNotice's own convention; fresh random bytes when omitted. */
45
+ noticeId?: Uint8Array<ArrayBuffer>;
46
+ }
47
+ export interface PostedNotice {
48
+ /** The data-have frame announcing the new entry -- the application decides when and to whom to actually send it. */
49
+ dataHave: DataHaveFrame;
50
+ notice: RoomNotice;
51
+ /** The epoch the notice was encrypted under. */
52
+ keyEpoch: number;
53
+ }
54
+ export declare function createNoticeBoard(options: Readonly<CreateNoticeBoardOptions>): {
55
+ postEncryptedNotice: (post: Readonly<PostEncryptedNoticeOptions>) => Promise<PostedNotice>;
56
+ ingestPeerEntry: (peer: DeviceId, entry: Uint8Array<ArrayBuffer>) => Promise<void>;
57
+ readOwnNotices: (room: string) => Promise<NoticeBoardEntry[]>;
58
+ readPeerNotices: (peer: DeviceId, room: string) => Promise<NoticeBoardEntry[]>;
59
+ };
60
+ //#endregion
@@ -0,0 +1,60 @@
1
+ import { D as DeviceId, Et as RoomNoticeClaims, T as DataHaveFrame, Tt as RoomNotice, p as CapabilityToken } from "../protocol-gbXtNeOc.mjs";
2
+ import { KeyValueStorage } from "../ports/storage.mjs";
3
+ import { t as IdentityPort } from "../identity-CIb9dGkJ.mjs";
4
+ import { t as Clock } from "../clock-DiSx-WKM.mjs";
5
+ import { a as RevocationCheck } from "../tokens-k172l0Iy.mjs";
6
+ import { RoomNoticeVerdictReason } from "./room.mjs";
7
+ //#region src/domain/notice-board.d.ts
8
+ /**
9
+ * Where an application keeps the content keys its createRoomRekeyHandler unwraps, and where the owner's own side stores every epoch key it generated. Room-keyed and epoch-keyed, because a reader must locate exactly the epoch a notice names, never guess.
10
+ */
11
+ export interface RoomKeyStore {
12
+ get: (room: string, epoch: number) => Promise<Uint8Array | undefined>;
13
+ set: (room: string, epoch: number, key: Uint8Array) => void;
14
+ /** The highest epoch held for a room -- what postEncryptedNotice stamps a new notice with. */
15
+ currentEpoch: (room: string) => Promise<number | undefined>;
16
+ }
17
+ /** One notice as read back from a log: always verified-or-not first, decrypted only when both the signature and the epoch key are held. */
18
+ export interface NoticeBoardEntry {
19
+ /** Whether verifyRoomNotice passed every obligation. False means nothing else on this entry is populated except reason -- unverified content is never surfaced, not even as ciphertext. */
20
+ verified: boolean;
21
+ /** Why verification failed, when it did. */
22
+ reason?: RoomNoticeVerdictReason;
23
+ /** The notice's own TRUE (suffix-stripped) content-type, on a verified entry. */
24
+ contentType?: string;
25
+ /** The decrypted content, present iff verified AND this side holds the epoch key (or the notice was never encrypted). Undefined means "valid notice, can't read it" -- the confidentiality property, not an error. */
26
+ plaintext?: Uint8Array;
27
+ /** The fully verified claims, on a verified entry -- posted-at, poster, refs, everything a UI renders around the content itself. */
28
+ claims?: RoomNoticeClaims;
29
+ }
30
+ export interface CreateNoticeBoardOptions {
31
+ identity: IdentityPort;
32
+ storage: KeyValueStorage;
33
+ clock: Clock;
34
+ revocation: RevocationCheck;
35
+ roomKeys: RoomKeyStore;
36
+ }
37
+ export interface PostEncryptedNoticeOptions {
38
+ room: string;
39
+ /** The poster's own room:member token, embedded in full in the notice. */
40
+ token: CapabilityToken;
41
+ /** The notice's true content-type; the +aes256gcm suffix is applied here, never by the caller. */
42
+ contentType: string;
43
+ plaintext: Uint8Array;
44
+ /** Caller-supplied notice id, matching createRoomNotice's own convention; fresh random bytes when omitted. */
45
+ noticeId?: Uint8Array<ArrayBuffer>;
46
+ }
47
+ export interface PostedNotice {
48
+ /** The data-have frame announcing the new entry -- the application decides when and to whom to actually send it. */
49
+ dataHave: DataHaveFrame;
50
+ notice: RoomNotice;
51
+ /** The epoch the notice was encrypted under. */
52
+ keyEpoch: number;
53
+ }
54
+ export declare function createNoticeBoard(options: Readonly<CreateNoticeBoardOptions>): {
55
+ postEncryptedNotice: (post: Readonly<PostEncryptedNoticeOptions>) => Promise<PostedNotice>;
56
+ ingestPeerEntry: (peer: DeviceId, entry: Uint8Array<ArrayBuffer>) => Promise<void>;
57
+ readOwnNotices: (room: string) => Promise<NoticeBoardEntry[]>;
58
+ readPeerNotices: (peer: DeviceId, room: string) => Promise<NoticeBoardEntry[]>;
59
+ };
60
+ //#endregion
@@ -0,0 +1,126 @@
1
+ import { roomNoticeSchema } from "../generated/protocol.mjs";
2
+ import { appendOwnEntry, handleDataEntries, headSeqFor, readEntries } from "./data-sync.mjs";
3
+ import { decryptNoticeContent, encryptNoticeContent, encryptedContentType, isEncryptedContentType, plaintextContentType } from "./group-key.mjs";
4
+ import { createRoomNotice, verifyRoomNotice } from "./room.mjs";
5
+ import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
6
+ //#region src/domain/notice-board.ts
7
+ /**
8
+ * The application-ready noticeboard (spec/room.cddl's own noticeboard extension of core/room, wire-mesh#36): composes core/data's oplog replication (data-sync.ts), room-notice signing and verification (room.ts), and the room.rekey content-key machinery (group-key.ts, room-rekey.ts) into the one primitive an application actually wires -- post an encrypted notice to your own log, ingest a replicated peer entry, read back a room's notices verified and (where this side holds the epoch key) decrypted.
9
+ *
10
+ * Deliberately NOT included here, matching data-sync.ts's own "primitive vs. policy" split: when to send the returned data-have frame, which peers' logs to track, how to fan a catch-up request out -- all application policy. The board only guarantees what its pieces each already guarantee individually: own-log appends are monotonic, peer ingests are gap-rejecting, and nothing is ever surfaced unverified.
11
+ *
12
+ * readPeerNotices' honest read-confidentiality behaviour: a notice this side cannot decrypt (no key for its epoch -- a pre-join notice, or a post-kick epoch) still verifies and still appears in the read, with plaintext undefined. Authenticity and confidentiality are independent properties; the board never conflates them.
13
+ */
14
+ /** The opaque byte length a caller-omitted noticeId gets: 16 random bytes, the same sizing token-id already uses. */
15
+ const GENERATED_NOTICE_ID_BYTE_LENGTH = 16;
16
+ /** Serialises a minted notice to the canonical CBOR bytes an oplog entry carries. */
17
+ function noticeToEntryBytes(notice) {
18
+ return new Uint8Array(encode(notice, cdeEncodeOptions));
19
+ }
20
+ function createNoticeBoard(options) {
21
+ const { identity, storage, clock, revocation, roomKeys } = options;
22
+ async function postEncryptedNotice(post) {
23
+ const keyEpoch = await roomKeys.currentEpoch(post.room);
24
+ if (keyEpoch === void 0) throw new Error(`no content key held for room ${post.room}: room.rekey this room before posting encrypted notices`);
25
+ const contentKey = await roomKeys.get(post.room, keyEpoch);
26
+ if (contentKey === void 0) throw new Error(`content key store is inconsistent: epoch ${String(keyEpoch)} is current for room ${post.room} but holds no key`);
27
+ const noticeId = post.noticeId ?? crypto.getRandomValues(new Uint8Array(GENERATED_NOTICE_ID_BYTE_LENGTH));
28
+ const ciphertext = await encryptNoticeContent(contentKey, post.plaintext);
29
+ const notice = await createRoomNotice({
30
+ identity,
31
+ clock,
32
+ room: post.room,
33
+ token: post.token,
34
+ noticeId,
35
+ contentType: encryptedContentType(post.contentType),
36
+ content: ciphertext,
37
+ keyEpoch
38
+ });
39
+ const { haveFrame: dataHave } = await appendOwnEntry({
40
+ identity,
41
+ storage
42
+ }, noticeToEntryBytes(notice));
43
+ return {
44
+ dataHave,
45
+ notice,
46
+ keyEpoch
47
+ };
48
+ }
49
+ async function ingestPeerEntry(peer, entry) {
50
+ const fromSeq = await headSeqFor(storage, peer);
51
+ if (!(await handleDataEntries(storage, {
52
+ type: "data-entries",
53
+ peer,
54
+ "from-seq": fromSeq,
55
+ entries: [entry]
56
+ })).ok) throw new Error(`gap rejecting entry for peer log (head ${String(fromSeq)})`);
57
+ }
58
+ async function readLog(peer, room) {
59
+ const entries = await readEntries(storage, peer, 0);
60
+ const out = [];
61
+ for (const entry of entries) out.push(await readOne(entry, room));
62
+ return out;
63
+ }
64
+ async function readOne(entry, room) {
65
+ const notice = decodeNotice(entry);
66
+ if (notice === void 0) return {
67
+ verified: false,
68
+ reason: "malformed"
69
+ };
70
+ const verdict = await verifyRoomNotice(notice, {
71
+ identity,
72
+ clock,
73
+ revocation,
74
+ expectedRoom: room
75
+ });
76
+ if (!verdict.ok) return {
77
+ verified: false,
78
+ reason: verdict.reason
79
+ };
80
+ const claims = verdict.claims;
81
+ if (!isEncryptedContentType(claims["content-type"])) return {
82
+ verified: true,
83
+ contentType: claims["content-type"],
84
+ plaintext: claims.content,
85
+ claims
86
+ };
87
+ const keyEpoch = claims["key-epoch"];
88
+ if (keyEpoch === void 0) return {
89
+ verified: false,
90
+ reason: "key_epoch_mismatch"
91
+ };
92
+ const contentKey = await roomKeys.get(claims.room, keyEpoch);
93
+ const base = {
94
+ verified: true,
95
+ contentType: plaintextContentType(claims["content-type"]),
96
+ claims
97
+ };
98
+ if (contentKey === void 0) return base;
99
+ try {
100
+ const plaintext = await decryptNoticeContent(contentKey, claims.content);
101
+ return {
102
+ ...base,
103
+ plaintext
104
+ };
105
+ } catch {
106
+ return base;
107
+ }
108
+ }
109
+ function decodeNotice(entry) {
110
+ try {
111
+ const decoded = decode(entry, cdeDecodeOptions);
112
+ const parsed = roomNoticeSchema.safeParse(decoded);
113
+ return parsed.success ? parsed.data : void 0;
114
+ } catch {
115
+ return;
116
+ }
117
+ }
118
+ return {
119
+ postEncryptedNotice,
120
+ ingestPeerEntry,
121
+ readOwnNotices: async (room) => readLog(identity.deviceId, room),
122
+ readPeerNotices: async (peer, room) => readLog(peer, room)
123
+ };
124
+ }
125
+ //#endregion
126
+ export { createNoticeBoard };
@@ -1,26 +1,2 @@
1
- import { B as IdentityKey, D as DeviceId, It as TokenClaims, p as CapabilityToken } from "../protocol-gbXtNeOc.cjs";
2
- import { l as TokenVerdictReason, u as VerifyCapabilityTokenOptions } from "../tokens-C9Kwy0Gk.cjs";
3
- //#region src/domain/room-token-verification.d.ts
4
- /** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
5
- export declare const ROOM_MEMBER_CAPABILITY = "room:member";
6
- export type RoomTokenVerdictReason = TokenVerdictReason | "wrong_scope_kind" | "wrong_scope_path" | "wrong_chain_root";
7
- export type RoomTokenVerdict = {
8
- ok: true;
9
- claims: TokenClaims;
10
- /** The chain's own certified root issuer-key -- for a named room, this is exactly the room's rightful owner (obligation 1 just confirmed the chain terminates there), and for a DM it's the verifying identity's own key. Lets a caller (e.g. room.rekey's own handler) derive an ECDH shared secret against the room's real owner with no separate live-sender identity check needed. */
11
- rootIssuerKey: IdentityKey;
12
- } | {
13
- ok: false;
14
- reason: RoomTokenVerdictReason;
15
- };
16
- export interface VerifyRoomTokenOptions extends Omit<VerifyCapabilityTokenOptions, "expectedBearer"> {
17
- /** The peer identity actually authenticated on the arriving connection (obligation 2) -- never a relay-asserted or gossip-derived value. Mandatory here: every room verb requires a bearer, unlike verifyCapabilityToken's own optional field for callers presenting a token to authorise themselves rather than a specific counterparty. */
18
- expectedBearer: DeviceId;
19
- /** The room path this request claims to act on (obligation 3) -- must equal the token's own scope.path, and its own shape determines the chain root obligation 1 requires. */
20
- roomPath: string;
21
- }
22
- /**
23
- * Verifies a `room:member` capability token against all six of core/room's verifier obligations. Delegates obligations 2/4/5 to verifyCapabilityToken directly; adds obligation 3 (scope.kind/scope.path must match the room being acted on) and obligation 1 (the chain's root must be the room's own owner for a named room, or the verifying identity itself for a DM -- never either named participant directly, since a DM token minted by anyone other than the verifier would let a sender self-issue authority to message a stranger unsolicited).
24
- */
25
- export declare function verifyRoomToken(token: CapabilityToken, options: Readonly<VerifyRoomTokenOptions>): Promise<RoomTokenVerdict>;
26
- //#endregion
1
+ import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-BY9AwZQ-.cjs";
2
+ export { ROOM_MEMBER_CAPABILITY, RoomTokenVerdict, RoomTokenVerdictReason, VerifyRoomTokenOptions, verifyRoomToken };
@@ -1,26 +1,2 @@
1
- import { B as IdentityKey, D as DeviceId, It as TokenClaims, p as CapabilityToken } from "../protocol-gbXtNeOc.mjs";
2
- import { l as TokenVerdictReason, u as VerifyCapabilityTokenOptions } from "../tokens-k172l0Iy.mjs";
3
- //#region src/domain/room-token-verification.d.ts
4
- /** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
5
- export declare const ROOM_MEMBER_CAPABILITY = "room:member";
6
- export type RoomTokenVerdictReason = TokenVerdictReason | "wrong_scope_kind" | "wrong_scope_path" | "wrong_chain_root";
7
- export type RoomTokenVerdict = {
8
- ok: true;
9
- claims: TokenClaims;
10
- /** The chain's own certified root issuer-key -- for a named room, this is exactly the room's rightful owner (obligation 1 just confirmed the chain terminates there), and for a DM it's the verifying identity's own key. Lets a caller (e.g. room.rekey's own handler) derive an ECDH shared secret against the room's real owner with no separate live-sender identity check needed. */
11
- rootIssuerKey: IdentityKey;
12
- } | {
13
- ok: false;
14
- reason: RoomTokenVerdictReason;
15
- };
16
- export interface VerifyRoomTokenOptions extends Omit<VerifyCapabilityTokenOptions, "expectedBearer"> {
17
- /** The peer identity actually authenticated on the arriving connection (obligation 2) -- never a relay-asserted or gossip-derived value. Mandatory here: every room verb requires a bearer, unlike verifyCapabilityToken's own optional field for callers presenting a token to authorise themselves rather than a specific counterparty. */
18
- expectedBearer: DeviceId;
19
- /** The room path this request claims to act on (obligation 3) -- must equal the token's own scope.path, and its own shape determines the chain root obligation 1 requires. */
20
- roomPath: string;
21
- }
22
- /**
23
- * Verifies a `room:member` capability token against all six of core/room's verifier obligations. Delegates obligations 2/4/5 to verifyCapabilityToken directly; adds obligation 3 (scope.kind/scope.path must match the room being acted on) and obligation 1 (the chain's root must be the room's own owner for a named room, or the verifying identity itself for a DM -- never either named participant directly, since a DM token minted by anyone other than the verifier would let a sender self-issue authority to message a stranger unsolicited).
24
- */
25
- export declare function verifyRoomToken(token: CapabilityToken, options: Readonly<VerifyRoomTokenOptions>): Promise<RoomTokenVerdict>;
26
- //#endregion
1
+ import { a as verifyRoomToken, i as VerifyRoomTokenOptions, n as RoomTokenVerdict, r as RoomTokenVerdictReason, t as ROOM_MEMBER_CAPABILITY } from "../room-token-verification-CYKEP_h6.mjs";
2
+ export { ROOM_MEMBER_CAPABILITY, RoomTokenVerdict, RoomTokenVerdictReason, VerifyRoomTokenOptions, verifyRoomToken };
@@ -2,7 +2,7 @@ import { Dt as RoomPath, Et as RoomNoticeClaims, Tt as RoomNotice, p as Capabili
2
2
  import { t as IdentityPort } from "../identity-B4XBWkqg.cjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.cjs";
4
4
  import { a as RevocationCheck } from "../tokens-C9Kwy0Gk.cjs";
5
- import { RoomTokenVerdictReason } from "./room-token-verification.cjs";
5
+ import { r as RoomTokenVerdictReason } from "../room-token-verification-BY9AwZQ-.cjs";
6
6
  //#region src/domain/room.d.ts
7
7
  export type RoomNoticeVerdictReason = "malformed" | "wrong_room" | "bad_signature" | "wrong_poster" | "content_expired" | "key_epoch_mismatch" | RoomTokenVerdictReason;
8
8
  export type RoomNoticeVerdict = {
@@ -2,7 +2,7 @@ import { Dt as RoomPath, Et as RoomNoticeClaims, Tt as RoomNotice, p as Capabili
2
2
  import { t as IdentityPort } from "../identity-CIb9dGkJ.mjs";
3
3
  import { t as Clock } from "../clock-DiSx-WKM.mjs";
4
4
  import { a as RevocationCheck } from "../tokens-k172l0Iy.mjs";
5
- import { RoomTokenVerdictReason } from "./room-token-verification.mjs";
5
+ import { r as RoomTokenVerdictReason } from "../room-token-verification-CYKEP_h6.mjs";
6
6
  //#region src/domain/room.d.ts
7
7
  export type RoomNoticeVerdictReason = "malformed" | "wrong_room" | "bad_signature" | "wrong_poster" | "content_expired" | "key_epoch_mismatch" | RoomTokenVerdictReason;
8
8
  export type RoomNoticeVerdict = {
@@ -0,0 +1,27 @@
1
+ import { B as IdentityKey, D as DeviceId, It as TokenClaims, p as CapabilityToken } from "./protocol-gbXtNeOc.cjs";
2
+ import { l as TokenVerdictReason, u as VerifyCapabilityTokenOptions } from "./tokens-C9Kwy0Gk.cjs";
3
+ //#region src/domain/room-token-verification.d.ts
4
+ /** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
5
+ declare const ROOM_MEMBER_CAPABILITY = "room:member";
6
+ type RoomTokenVerdictReason = TokenVerdictReason | "wrong_scope_kind" | "wrong_scope_path" | "wrong_chain_root";
7
+ type RoomTokenVerdict = {
8
+ ok: true;
9
+ claims: TokenClaims;
10
+ /** The chain's own certified root issuer-key -- for a named room, this is exactly the room's rightful owner (obligation 1 just confirmed the chain terminates there), and for a DM it's the verifying identity's own key. Lets a caller (e.g. room.rekey's own handler) derive an ECDH shared secret against the room's real owner with no separate live-sender identity check needed. */
11
+ rootIssuerKey: IdentityKey;
12
+ } | {
13
+ ok: false;
14
+ reason: RoomTokenVerdictReason;
15
+ };
16
+ interface VerifyRoomTokenOptions extends Omit<VerifyCapabilityTokenOptions, "expectedBearer"> {
17
+ /** The peer identity actually authenticated on the arriving connection (obligation 2) -- never a relay-asserted or gossip-derived value. Mandatory here: every room verb requires a bearer, unlike verifyCapabilityToken's own optional field for callers presenting a token to authorise themselves rather than a specific counterparty. */
18
+ expectedBearer: DeviceId;
19
+ /** The room path this request claims to act on (obligation 3) -- must equal the token's own scope.path, and its own shape determines the chain root obligation 1 requires. */
20
+ roomPath: string;
21
+ }
22
+ /**
23
+ * Verifies a `room:member` capability token against all six of core/room's verifier obligations. Delegates obligations 2/4/5 to verifyCapabilityToken directly; adds obligation 3 (scope.kind/scope.path must match the room being acted on) and obligation 1 (the chain's root must be the room's own owner for a named room, or the verifying identity itself for a DM -- never either named participant directly, since a DM token minted by anyone other than the verifier would let a sender self-issue authority to message a stranger unsolicited).
24
+ */
25
+ declare function verifyRoomToken(token: CapabilityToken, options: Readonly<VerifyRoomTokenOptions>): Promise<RoomTokenVerdict>;
26
+ //#endregion
27
+ export { verifyRoomToken as a, VerifyRoomTokenOptions as i, RoomTokenVerdict as n, RoomTokenVerdictReason as r, ROOM_MEMBER_CAPABILITY as t };
@@ -0,0 +1,27 @@
1
+ import { B as IdentityKey, D as DeviceId, It as TokenClaims, p as CapabilityToken } from "./protocol-gbXtNeOc.mjs";
2
+ import { l as TokenVerdictReason, u as VerifyCapabilityTokenOptions } from "./tokens-k172l0Iy.mjs";
3
+ //#region src/domain/room-token-verification.d.ts
4
+ /** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
5
+ declare const ROOM_MEMBER_CAPABILITY = "room:member";
6
+ type RoomTokenVerdictReason = TokenVerdictReason | "wrong_scope_kind" | "wrong_scope_path" | "wrong_chain_root";
7
+ type RoomTokenVerdict = {
8
+ ok: true;
9
+ claims: TokenClaims;
10
+ /** The chain's own certified root issuer-key -- for a named room, this is exactly the room's rightful owner (obligation 1 just confirmed the chain terminates there), and for a DM it's the verifying identity's own key. Lets a caller (e.g. room.rekey's own handler) derive an ECDH shared secret against the room's real owner with no separate live-sender identity check needed. */
11
+ rootIssuerKey: IdentityKey;
12
+ } | {
13
+ ok: false;
14
+ reason: RoomTokenVerdictReason;
15
+ };
16
+ interface VerifyRoomTokenOptions extends Omit<VerifyCapabilityTokenOptions, "expectedBearer"> {
17
+ /** The peer identity actually authenticated on the arriving connection (obligation 2) -- never a relay-asserted or gossip-derived value. Mandatory here: every room verb requires a bearer, unlike verifyCapabilityToken's own optional field for callers presenting a token to authorise themselves rather than a specific counterparty. */
18
+ expectedBearer: DeviceId;
19
+ /** The room path this request claims to act on (obligation 3) -- must equal the token's own scope.path, and its own shape determines the chain root obligation 1 requires. */
20
+ roomPath: string;
21
+ }
22
+ /**
23
+ * Verifies a `room:member` capability token against all six of core/room's verifier obligations. Delegates obligations 2/4/5 to verifyCapabilityToken directly; adds obligation 3 (scope.kind/scope.path must match the room being acted on) and obligation 1 (the chain's root must be the room's own owner for a named room, or the verifying identity itself for a DM -- never either named participant directly, since a DM token minted by anyone other than the verifier would let a sender self-issue authority to message a stranger unsolicited).
24
+ */
25
+ declare function verifyRoomToken(token: CapabilityToken, options: Readonly<VerifyRoomTokenOptions>): Promise<RoomTokenVerdict>;
26
+ //#endregion
27
+ export { verifyRoomToken as a, VerifyRoomTokenOptions as i, RoomTokenVerdict as n, RoomTokenVerdictReason as r, ROOM_MEMBER_CAPABILITY as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "1.35.1",
3
+ "version": "1.36.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
6
6
  "repository": {
@@ -126,6 +126,10 @@
126
126
  "import": "./dist/domain/mesh-session.mjs",
127
127
  "require": "./dist/domain/mesh-session.cjs"
128
128
  },
129
+ "./domain/notice-board": {
130
+ "import": "./dist/domain/notice-board.mjs",
131
+ "require": "./dist/domain/notice-board.cjs"
132
+ },
129
133
  "./domain/relay-hub": {
130
134
  "import": "./dist/domain/relay-hub.mjs",
131
135
  "require": "./dist/domain/relay-hub.cjs"