@happyvertical/repos 0.85.4 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT.md CHANGED
@@ -27,7 +27,7 @@ pnpm --filter @happyvertical/repos clean
27
27
  ## Ecosystem Relationships
28
28
  - Provides: Standardized repository interface for GitHub, GitLab, Bitbucket, and Azure DevOps
29
29
  - Implements: none
30
- - Requires: @happyvertical/graphql, js-yaml
30
+ - Requires: @happyvertical/graphql, js-yaml, nostr-tools
31
31
  - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
32
  <!-- END AGENT:GENERATED -->
33
33
 
package/README.md CHANGED
@@ -123,6 +123,27 @@ an observation. The exported `createGitHubWebhookFixture()` helper creates exact
123
123
  deterministic bytes and signatures for duplicate, redelivery, delayed, and
124
124
  out-of-order integration scenarios.
125
125
 
126
+ ### Buzz forge relay
127
+
128
+ `BuzzRelayClient` polls configured Buzz/Nostr relays and normalizes supported
129
+ forge kinds into provider-neutral `ForgeEventEnvelope` values. Production
130
+ events are verified with `nostr-tools` before normalization; the
131
+ `allowUnverifiedFixtures` option is solely for deterministic test fixtures.
132
+ When `channelIds` is set, accepted events must have a matching `channel` or
133
+ `h` tag. Kind-7 approvals need pull-request metadata or their referenced
134
+ kind-1617 patch; pass a kind-39002 members event and `roleFloor` to enforce
135
+ channel roles.
136
+
137
+ ```typescript
138
+ import { BuzzRelayClient } from '@happyvertical/repos';
139
+
140
+ const buzz = new BuzzRelayClient({
141
+ relays: ['https://relay.example/buzz'],
142
+ channelIds: ['channel-hv'],
143
+ });
144
+ const events = await buzz.pollOnce();
145
+ ```
146
+
126
147
  ### Errors and provider metadata
127
148
 
128
149
  New forge APIs throw `ForgeError`, which exposes `code`, `provider`, `status`,
package/dist/index.d.ts CHANGED
@@ -4,6 +4,119 @@ export declare interface Branch {
4
4
  protected: boolean;
5
5
  }
6
6
 
7
+ export declare const BUZZ_FIXTURE_PUBKEYS: {
8
+ readonly owner: "2ad53df8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4f05";
9
+ readonly attestor: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
10
+ readonly reactor: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
11
+ };
12
+
13
+ /**
14
+ * Buzz/Nostr forge event kinds verified live against desktop-v0.5.3 (ADR-002).
15
+ * Kind numbers are load-bearing and must match the deployed relay.
16
+ */
17
+ export declare const BUZZ_FORGE_KINDS: {
18
+ readonly repositoryAnnouncement: 30617;
19
+ readonly refUpdate: 30618;
20
+ readonly patch: 1617;
21
+ readonly statusOpen: 1630;
22
+ readonly statusApplied: 1631;
23
+ readonly statusClosed: 1632;
24
+ readonly statusDraft: 1633;
25
+ readonly reaction: 7;
26
+ readonly channelMembers: 39002;
27
+ };
28
+
29
+ /** Channel role floor used when validating kind:7 approvals (ADR-002). */
30
+ export declare type BuzzChannelRole = 'owner' | 'admin' | 'member' | 'guest' | 'bot';
31
+
32
+ declare interface BuzzFixtureOptions {
33
+ /** Override event id after computing the NIP-01 digest (duplicate/replay cases). */
34
+ id?: string;
35
+ pubkey?: string;
36
+ created_at?: number;
37
+ tags?: readonly (readonly string[])[];
38
+ content?: string;
39
+ sig?: string;
40
+ }
41
+
42
+ export declare type BuzzForgeKind = (typeof BUZZ_FORGE_KINDS)[keyof typeof BUZZ_FORGE_KINDS];
43
+
44
+ /**
45
+ * Minimal buzz relay client: HTTP REQ polling plus an injectable event source
46
+ * for tests and WebSocket adapters.
47
+ *
48
+ * The client never mutates process-global state. Each subscribe/poll call is
49
+ * scoped to the constructed options.
50
+ */
51
+ export declare class BuzzRelayClient {
52
+ private readonly relays;
53
+ private readonly kinds;
54
+ private readonly channelIds;
55
+ private readonly fetchImpl;
56
+ private readonly pollIntervalMs;
57
+ private readonly seenIds;
58
+ constructor(options: BuzzRelayClientOptions);
59
+ /**
60
+ * Poll every configured relay once with a NIP-01-style HTTP REQ body and
61
+ * return newly observed, verified envelopes (deduped by event id).
62
+ */
63
+ pollOnce(options?: {
64
+ since?: number;
65
+ allowUnverifiedFixtures?: boolean;
66
+ membersEvent?: NostrForgeEvent;
67
+ referencedPatchEvent?: NostrForgeEvent;
68
+ }): Promise<ForgeEventEnvelope[]>;
69
+ /**
70
+ * Poll on an interval until `close()` is called. Useful for long-running
71
+ * Aedile adapters. Returns a subscription handle.
72
+ */
73
+ subscribe(onEvent: (envelope: ForgeEventEnvelope) => void | Promise<void>, options?: {
74
+ since?: number;
75
+ allowUnverifiedFixtures?: boolean;
76
+ membersEvent?: NostrForgeEvent;
77
+ referencedPatchEvent?: NostrForgeEvent;
78
+ onError?: (error: unknown) => void;
79
+ }): BuzzRelaySubscription;
80
+ /**
81
+ * Normalize a pre-fetched event list (tests / custom transports) without
82
+ * contacting a relay. Applies the same kind/channel filters and id dedupe.
83
+ */
84
+ ingestEvents(events: readonly NostrForgeEvent[], options?: {
85
+ allowUnverifiedFixtures?: boolean;
86
+ membersEvent?: NostrForgeEvent;
87
+ referencedPatchEvent?: NostrForgeEvent;
88
+ verify?: boolean;
89
+ }): ForgeEventEnvelope[];
90
+ /** Forget delivered ids (operator replay). */
91
+ resetSeen(): void;
92
+ private fetchEvents;
93
+ }
94
+
95
+ export declare interface BuzzRelayClientOptions {
96
+ /** WebSocket or HTTP relay endpoints. */
97
+ relays: readonly string[];
98
+ /** Optional filter limiting which kinds are accepted. */
99
+ kinds?: readonly number[];
100
+ /** Bound channel ids watched for forge activity. */
101
+ channelIds?: readonly string[];
102
+ /** Fetch implementation override for tests. */
103
+ fetch?: typeof fetch;
104
+ /** Poll interval in ms when using HTTP REQ polling. Default 5000. */
105
+ pollIntervalMs?: number;
106
+ }
107
+
108
+ export declare interface BuzzRelaySubscription {
109
+ close(): void;
110
+ }
111
+
112
+ export declare interface BuzzRoleResolution {
113
+ pubkey: string;
114
+ role: BuzzChannelRole;
115
+ }
116
+
117
+ /** Extract a channel id from common buzz tags when present. */
118
+ export declare function channelIdFromEvent(event: NostrForgeEvent): string | undefined;
119
+
7
120
  /** Normalized provider check run. */
8
121
  export declare interface CheckRun {
9
122
  id: string;
@@ -47,6 +160,37 @@ export declare interface CommitStatus {
47
160
  raw?: unknown;
48
161
  }
49
162
 
163
+ /**
164
+ * Compute the NIP-01 event id for a Nostr event (sha256 of serialized fields).
165
+ * Used by fixtures and verification when the caller supplies an unsigned skeleton.
166
+ */
167
+ export declare function computeNostrEventId(event: Omit<NostrForgeEvent, 'id' | 'sig'>): string;
168
+
169
+ export declare function createApprovalFixture(options?: BuzzFixtureOptions & {
170
+ content?: string;
171
+ headSha?: string;
172
+ targetEventId?: string;
173
+ }): NostrForgeEvent;
174
+
175
+ /**
176
+ * Deterministic sequences for convergence suites (duplicate / delayed /
177
+ * out-of-order / replay). Each sequence is an ordered list of Nostr events;
178
+ * consumers may reshuffle delivery while asserting identical projection state.
179
+ */
180
+ export declare function createBuzzConvergenceSequences(): {
181
+ canonical: NostrForgeEvent[];
182
+ duplicate: NostrForgeEvent[];
183
+ delayed: NostrForgeEvent[];
184
+ outOfOrder: NostrForgeEvent[];
185
+ replay: NostrForgeEvent[];
186
+ };
187
+
188
+ /**
189
+ * Build a deterministic unsigned-then-id-bound Nostr event for tests.
190
+ * Signature bytes are placeholders; pair with `allowUnverifiedFixtures: true`.
191
+ */
192
+ export declare function createBuzzFixtureEvent(kind: number, options?: BuzzFixtureOptions): NostrForgeEvent;
193
+
50
194
  /** Input for publishing a provider check run. */
51
195
  export declare interface CreateCheckRunInput {
52
196
  name: string;
@@ -115,6 +259,19 @@ export declare interface CreateIssueInput {
115
259
  assignees?: string[];
116
260
  }
117
261
 
262
+ export declare function createMembersFixture(options?: BuzzFixtureOptions & {
263
+ members?: readonly {
264
+ pubkey: string;
265
+ role: string;
266
+ }[];
267
+ }): NostrForgeEvent;
268
+
269
+ export declare function createPatchFixture(options?: BuzzFixtureOptions & {
270
+ headSha?: string;
271
+ number?: number;
272
+ repoId?: string;
273
+ }): NostrForgeEvent;
274
+
118
275
  export declare interface CreatePRInput {
119
276
  title: string;
120
277
  body?: string;
@@ -123,6 +280,27 @@ export declare interface CreatePRInput {
123
280
  draft?: boolean;
124
281
  }
125
282
 
283
+ export declare function createRefUpdateFixture(options?: BuzzFixtureOptions & {
284
+ ref?: string;
285
+ head?: string;
286
+ previous?: string;
287
+ repoId?: string;
288
+ owner?: string;
289
+ }): NostrForgeEvent;
290
+
291
+ export declare function createRepositoryAnnouncementFixture(options?: BuzzFixtureOptions & {
292
+ owner?: string;
293
+ repoId?: string;
294
+ channelId?: string;
295
+ }): NostrForgeEvent;
296
+
297
+ export declare function createStatusFixture(options?: BuzzFixtureOptions & {
298
+ kind?: typeof BUZZ_FORGE_KINDS.statusOpen | typeof BUZZ_FORGE_KINDS.statusApplied | typeof BUZZ_FORGE_KINDS.statusClosed | typeof BUZZ_FORGE_KINDS.statusDraft;
299
+ context?: string;
300
+ headSha?: string;
301
+ conclusion?: string;
302
+ }): NostrForgeEvent;
303
+
126
304
  /**
127
305
  * Detect which template an issue was created from based on labels
128
306
  *
@@ -884,6 +1062,17 @@ export declare function loadIssueTemplate(yamlPath: string): Promise<IssueTempla
884
1062
 
885
1063
  export declare type MergeMethod = 'merge' | 'squash' | 'rebase';
886
1064
 
1065
+ /**
1066
+ * Normalize one already-verified Nostr forge event into a provider-neutral
1067
+ * envelope. Signature verification is the caller's responsibility (or use
1068
+ * {@link verifyAndNormalizeBuzzEvent}).
1069
+ */
1070
+ export declare function normalizeBuzzEvent(event: NostrForgeEvent, receivedAt?: Date, options?: {
1071
+ roleFloor?: BuzzChannelRole;
1072
+ membersEvent?: NostrForgeEvent;
1073
+ referencedPatchEvent?: NostrForgeEvent;
1074
+ }): ForgeEventEnvelope;
1075
+
887
1076
  /**
888
1077
  * Normalizes one already-verified GitHub payload.
889
1078
  * @param deliveryId Stable provider delivery identity.
@@ -894,6 +1083,17 @@ export declare type MergeMethod = 'merge' | 'squash' | 'rebase';
894
1083
  */
895
1084
  export declare function normalizeGitHubWebhook(deliveryId: string, event: string, raw: unknown, receivedAt?: Date): ForgeEventEnvelope;
896
1085
 
1086
+ /** Minimal Nostr event shape consumed by the buzz forge adapter. */
1087
+ export declare interface NostrForgeEvent {
1088
+ id: string;
1089
+ pubkey: string;
1090
+ created_at: number;
1091
+ kind: number;
1092
+ tags: readonly (readonly string[])[];
1093
+ content: string;
1094
+ sig: string;
1095
+ }
1096
+
897
1097
  /**
898
1098
  * Parse an issue body into field values
899
1099
  *
@@ -1027,6 +1227,15 @@ export declare enum RepositoryErrorCode {
1027
1227
  UNKNOWN = "UNKNOWN"
1028
1228
  }
1029
1229
 
1230
+ /**
1231
+ * Resolve a reactor's channel role from a kind:39002 members list.
1232
+ * Returns null when the pubkey is absent from the membership event.
1233
+ */
1234
+ export declare function resolveBuzzChannelRole(membersEvent: NostrForgeEvent, pubkey: string): BuzzRoleResolution | null;
1235
+
1236
+ /** True when the reactor's role rank is at or above the configured floor. */
1237
+ export declare function roleMeetsFloor(role: BuzzChannelRole, floor: BuzzChannelRole): boolean;
1238
+
1030
1239
  export declare interface SearchFilters {
1031
1240
  state?: 'open' | 'closed' | 'all';
1032
1241
  labels?: string[];
@@ -1088,4 +1297,26 @@ export declare interface User {
1088
1297
  url?: string;
1089
1298
  }
1090
1299
 
1300
+ /**
1301
+ * Verify then normalize a buzz forge event. Rejects invalid signatures without
1302
+ * producing an observation.
1303
+ */
1304
+ export declare function verifyAndNormalizeBuzzEvent(event: NostrForgeEvent, receivedAt?: Date, options?: {
1305
+ roleFloor?: BuzzChannelRole;
1306
+ membersEvent?: NostrForgeEvent;
1307
+ referencedPatchEvent?: NostrForgeEvent;
1308
+ allowUnverifiedFixtures?: boolean;
1309
+ }): ForgeEventEnvelope;
1310
+
1311
+ /**
1312
+ * Structural signature verification for Nostr events.
1313
+ *
1314
+ * When `nostr-tools` is available it performs full Schnorr verification.
1315
+ * Otherwise it fails closed unless `allowUnverifiedFixtures` is set (tests
1316
+ * that inject pre-trusted fixture events with matching id digests).
1317
+ */
1318
+ export declare function verifyNostrEventSignature(event: NostrForgeEvent, options?: {
1319
+ allowUnverifiedFixtures?: boolean;
1320
+ }): void;
1321
+
1091
1322
  export { }