@phosphorco/bb-inbox-rows 0.0.0-stage → 0.1.1

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/CONTRACT.md ADDED
@@ -0,0 +1,17 @@
1
+ # Inbox rows v1
2
+
3
+ Inbox owns root/visibility/archive/lifecycle validation, identity binding, indexed source reads and canonical sender/body models. The package owns bounded DTOs, fixed RPC methods, injected registration/client adapters and conformance. A host owns discovery (listed plugins plus fixed describe), one shared hydration resource, navigation, selection, fallback, error isolation, freshness and close cancellation. No feature storage, React renderer or polling is provided here.
4
+
5
+ `METHODS.describe = inboxRowsV1Describe`, null input. Negotiate the tolerant protocol/versions envelope before parsing v1 details. `METHODS.read = inboxRowsV1Read` takes `{version:1, occurrences:[{key,threadId}], archiveMode:'active'|'include-archived', viewAs?:string|null}`. There are 1–16 occurrences; duplicate keys are invalid, distinct keys for the same thread are allowed. Omitted/null viewAs selects the actual actor through Inbox's existing identity binding; a non-null subject selects a read lens, never a new actor.
6
+
7
+ Response `{version:1, ownerKey:string|null, revision:string, rows:[...]}` echoes the resolved subject and source snapshot revision. Every requested occurrence appears exactly once with the same thread ID. A ready row is `{key,threadId,state:'ready',revision,data:RowData}`; an unavailable row has `state:'unavailable',reason` (not-found/not-top-level/excluded/timeout/source-error/oversize). Ready data threadId must match. Non-null requested subject must echo exactly. Missing and unsolicited occurrences fail the client boundary.
8
+
9
+ RowData contains `v:1,threadId,title,groupPath,tags,tagOverflow,owners,ownerOverflow,ownerOverflowLabel,participants,participantOverflow,senderDescription,status:{tone,label},time:{at,text,title}`. Face entries contain `{key,name:string|null,imageUrl:string|null,label,external:boolean}`. Ordered owner and participant arrays each contain at most two portraits. Missing owner profiles retain a visible unknown owner. Owners are always shown in hosted search. Owner keys are removed from participants; participant duplicates collapse in canonical order. Overflow represents remaining canonical identities. Attribution and recent-sender descriptions follow native Inbox. Status is the thread's own lifecycle/attention, never a family aggregate. Time includes an absolute epoch ms and preformatted text; inert bodies schedule no updates.
10
+
11
+ Inputs are strict at all boundaries. Outputs are additive at every evolving object boundary, retaining unknown fields. Version 1 enums are closed. Changed meaning/input requires a new wire version and fixed methods. Canonical `JSON.stringify` UTF-8 bounds are checked before schema decoding (parsed transport JSON, not raw HTTP/preparse allocation): input 16KiB, describe 2KiB, each complete presentation stamp `{pluginId:ROW_PLUGIN_ID,schema:ROW_SCHEMA,data}` 4KiB, response 60KiB. Presentation depth remains below context-recognition's depth-eight limit. Oversized stamps become unavailable individually without removing portraits. If the complete batch exceeds 60KiB, ready rows become unavailable in reverse occurrence order until it fits. A malformed row becomes unavailable; no malformed ready stamp reaches the host.
12
+
13
+ Supplier deadline 3500ms; caller deadline 4500ms, below hosted owner's 5000ms. Supplier registration admits at most two in-flight source reads, no queue; a noncooperative source continues occupying its slot after timeout. One client admits one read at a time; cancelled/nonsettling calls do not open additional remote work. Caller abort fences local delivery, not proof of remote cancellation. Source adapters must respect signals between bounded stages. ID reads deduplicate <=16 IDs; source repeats explicit parent and lifecycle-owner root checks, hidden/deleted/archive checks. Unknown lineage is unavailable. No full owner/lifecycle tables or family traversal. Sender history <=25 events/ID, concurrency <=2; profile keys are batched, <=200 per call. Supplier-specific total profile and table bounds belong in its conformance evidence.
14
+
15
+ Inbox registers `inbox-sidebar-nerd/thread-row@1` lazily in its content-script context, with `methods:[inboxRowsV1Read]`. `assertRowPresentation` rejects missing/mismatched read allowlists even on permissive hosts. Host creates one owner-bound presentation client from that exact negotiated entry, validates the response, then renders each data stamp/revision through context-recognition 0.5.2. The actual shared body is inert: no per-row proxy, root Inbox, controls, observers or timers. Host owns role=option, stable accessible name and usable fallback. Registration disappearance or renderer/source failure must preserve those host responsibilities.
16
+
17
+ Root and /bb contain no SDK runtime imports; /bb injects registration and transport. /testing uses the public SDK fake host and invokes the real registered methods. The /testing entry uses the SDK testing entry and its documented optional testing peers (better-sqlite3, cron-parser, hono). Inject createHost to reuse an installed SDK fake host and its SQLite adaptation; the default uses the package-resolved SDK. Package tests and supplier conformance establish source behavior only. Actual browser/body parity, packaged leaf resolution, reload and CSS proof remain adopter obligations.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Phosphor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @phosphorco/bb-inbox-rows
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A small published contract for bounded Inbox row hydration. It ships schemas and byte checks (root), injected public-SDK/identity registration and caller transport (`/bb`), and real-handler conformance (`/testing`). No React or source storage.
4
+
5
+ Read [CONTRACT.md](CONTRACT.md) for ownership, wire shapes and work limits. Hosts discover via the listed plugin API and `inboxRowsV1Describe`, then create one batch client; each occurrence keeps a native-navigable fallback. Inbox owns the separately registered lazy presentation and genuine shared native body.
package/dist/bb.js ADDED
@@ -0,0 +1,222 @@
1
+ // src/bb.ts
2
+ import { z as z2 } from "zod";
3
+
4
+ // src/index.ts
5
+ import { z } from "zod";
6
+ var PROTOCOL = "phosphor.bb-inbox-rows";
7
+ var METHODS = Object.freeze({ describe: "inboxRowsV1Describe", read: "inboxRowsV1Read" });
8
+ var ROW_PLUGIN_ID = "inbox-sidebar-nerd";
9
+ var ROW_SCHEMA = "inbox-sidebar-nerd/thread-row@1";
10
+ var LIMITS = Object.freeze({ occurrences: 16, stampBytes: 4096, responseBytes: 60 * 1024, inputBytes: 16 * 1024, describeBytes: 2048, supplierMs: 3500, callMs: 4500, senderEvents: 25, senderConcurrency: 2, profileKeys: 200 });
11
+ var token = z.string().min(1).max(256);
12
+ var key = z.string().min(1).max(512);
13
+ var text = z.string().max(1024);
14
+ var count = z.number().int().min(0).max(1e6);
15
+ var time = z.number().finite().nonnegative();
16
+ var faceSchema = z.object({ key, name: text.nullable(), imageUrl: z.string().max(2048).nullable(), label: text, external: z.boolean() }).passthrough();
17
+ var rowDataSchema = z.object({
18
+ v: z.literal(1),
19
+ threadId: token,
20
+ title: text,
21
+ groupPath: text.nullable(),
22
+ tags: z.array(z.string().max(256)).max(3),
23
+ tagOverflow: count,
24
+ owners: z.array(faceSchema).max(2),
25
+ ownerOverflow: count,
26
+ ownerOverflowLabel: text,
27
+ participants: z.array(faceSchema).max(2),
28
+ participantOverflow: count,
29
+ senderDescription: text,
30
+ status: z.object({ tone: z.enum(["done", "snoozed", "waiting-for-reset", "failed", "needs-you", "working", "woke", "ready", "quiet"]), label: text }).passthrough(),
31
+ time: z.object({ at: time, text: z.string().max(128), title: z.string().max(256) }).passthrough()
32
+ }).passthrough();
33
+ var readInputSchema = z.object({ version: z.literal(1), occurrences: z.array(z.object({ key: token, threadId: token }).strict()).min(1).max(LIMITS.occurrences), archiveMode: z.enum(["active", "include-archived"]), viewAs: key.nullable().optional() }).strict().superRefine((v, ctx) => {
34
+ if (new Set(v.occurrences.map((r) => r.key)).size !== v.occurrences.length)
35
+ ctx.addIssue({ code: "custom", message: "Duplicate occurrence key" });
36
+ });
37
+ var unavailableReasonSchema = z.enum(["not-found", "not-top-level", "excluded", "timeout", "source-error", "oversize"]);
38
+ var ready = z.object({ key: token, threadId: token, state: z.literal("ready"), revision: token, data: rowDataSchema }).passthrough();
39
+ var unavailable = z.object({ key: token, threadId: token, state: z.literal("unavailable"), reason: unavailableReasonSchema }).passthrough();
40
+ var readOutputSchema = z.object({ version: z.literal(1), ownerKey: key.nullable(), revision: token, rows: z.array(z.discriminatedUnion("state", [ready, unavailable])).min(1).max(LIMITS.occurrences) }).passthrough();
41
+ var describeEnvelopeSchema = z.object({ protocol: z.literal(PROTOCOL), versions: z.array(z.number().int().positive()).max(16) }).passthrough();
42
+ var describeSchema = describeEnvelopeSchema.extend({ schema: z.literal(ROW_SCHEMA), readMethod: z.literal(METHODS.read), limits: z.object({ occurrences: z.literal(16), stampBytes: z.literal(4096), responseBytes: z.literal(61440), supplierMs: z.literal(3500) }).passthrough() });
43
+ var DESCRIPTION = { protocol: PROTOCOL, versions: [1], schema: ROW_SCHEMA, readMethod: METHODS.read, limits: { occurrences: 16, stampBytes: 4096, responseBytes: 61440, supplierMs: 3500 } };
44
+ function canonicalBytes(value) {
45
+ const json = JSON.stringify(value);
46
+ if (json === undefined)
47
+ throw Error("Expected JSON");
48
+ return new TextEncoder().encode(json).byteLength;
49
+ }
50
+ function parseBounded(schema, raw, max) {
51
+ if (canonicalBytes(raw) > max)
52
+ throw RangeError("Inbox rows byte limit");
53
+ return schema.parse(raw);
54
+ }
55
+ function parseReadInput(raw) {
56
+ return parseBounded(readInputSchema, raw, LIMITS.inputBytes);
57
+ }
58
+ function rowPresentation(raw) {
59
+ return { pluginId: ROW_PLUGIN_ID, schema: ROW_SCHEMA, data: raw };
60
+ }
61
+ function parseRowData(raw) {
62
+ if (canonicalBytes({ pluginId: ROW_PLUGIN_ID, schema: ROW_SCHEMA, data: raw }) > LIMITS.stampBytes)
63
+ throw RangeError("Inbox stamp byte limit");
64
+ const visit = (value, depth) => {
65
+ if (depth > 8)
66
+ throw RangeError("Inbox stamp depth limit");
67
+ if (value && typeof value === "object")
68
+ for (const child of Object.values(value))
69
+ visit(child, depth + 1);
70
+ };
71
+ visit(raw, 0);
72
+ return rowDataSchema.parse(raw);
73
+ }
74
+ function validateReadOutput(input, output) {
75
+ if (input.viewAs != null && output.ownerKey !== input.viewAs)
76
+ throw Error("Inbox row owner lens mismatch");
77
+ if (output.rows.length !== input.occurrences.length)
78
+ throw Error("Missing Inbox row occurrence");
79
+ const requested = new Map(input.occurrences.map((o) => [o.key, o.threadId])), seen = new Set;
80
+ for (const row of output.rows) {
81
+ if (seen.has(row.key) || requested.get(row.key) !== row.threadId)
82
+ throw Error("Unexpected Inbox row occurrence");
83
+ seen.add(row.key);
84
+ if (row.state === "ready") {
85
+ if (row.data.threadId !== row.threadId)
86
+ throw Error("Inbox row data identity mismatch");
87
+ parseRowData(row.data);
88
+ }
89
+ }
90
+ return output;
91
+ }
92
+ function parseReadOutput(input, raw) {
93
+ return validateReadOutput(input, parseBounded(readOutputSchema, raw, LIMITS.responseBytes));
94
+ }
95
+ function boundReadOutput(input, raw) {
96
+ const rows = raw.rows.map((row) => {
97
+ if (row.state === "unavailable")
98
+ return row;
99
+ try {
100
+ parseRowData(row.data);
101
+ return row;
102
+ } catch {
103
+ let oversized = false;
104
+ try {
105
+ oversized = canonicalBytes(rowPresentation(row.data)) > LIMITS.stampBytes;
106
+ } catch {}
107
+ return { key: row.key, threadId: row.threadId, state: "unavailable", reason: oversized ? "oversize" : "source-error" };
108
+ }
109
+ });
110
+ const output = { ...raw, rows };
111
+ for (let index = rows.length - 1;canonicalBytes(output) > LIMITS.responseBytes && index >= 0; index--) {
112
+ const row = rows[index];
113
+ if (row.state === "ready")
114
+ rows[index] = { key: row.key, threadId: row.threadId, state: "unavailable", reason: "oversize" };
115
+ }
116
+ return parseReadOutput(input, output);
117
+ }
118
+
119
+ // src/bb.ts
120
+ function sdkSchema(schema) {
121
+ return { "~standard": { version: 1, vendor: "bb-inbox-rows", jsonSchema: { input: () => schema.toJSONSchema({ io: "input", unrepresentable: "any" }), output: () => schema.toJSONSchema({ io: "output", unrepresentable: "any" }) }, validate(value) {
122
+ const r = schema.safeParse(value);
123
+ return r.success ? { value: r.data } : { issues: r.error.issues };
124
+ } } };
125
+ }
126
+ var inboxRowsRpcContract = {
127
+ [METHODS.describe]: { input: sdkSchema(z2.null()), output: sdkSchema(z2.unknown()) },
128
+ [METHODS.read]: { input: sdkSchema(readInputSchema), output: sdkSchema(z2.unknown()) }
129
+ };
130
+ function createInboxRowsHandlers(supplier) {
131
+ let pending = 0;
132
+ return {
133
+ describe(raw = null) {
134
+ z2.null().parse(raw);
135
+ return DESCRIPTION;
136
+ },
137
+ async read(raw, context) {
138
+ const input = parseReadInput(raw);
139
+ const failed = (reason) => ({ version: 1, ownerKey: input.viewAs ?? null, revision: "unavailable", rows: input.occurrences.map((o) => ({ ...o, state: "unavailable", reason })) });
140
+ if (pending >= 2)
141
+ return failed("source-error");
142
+ const controller = new AbortController;
143
+ let timer;
144
+ pending++;
145
+ const flight = Promise.resolve().then(() => supplier.read(input, context, controller.signal)).finally(() => {
146
+ pending--;
147
+ });
148
+ const deadline = new Promise((resolve) => {
149
+ timer = setTimeout(() => {
150
+ controller.abort();
151
+ resolve(failed("timeout"));
152
+ }, LIMITS.supplierMs);
153
+ });
154
+ try {
155
+ return boundReadOutput(input, await Promise.race([flight, deadline]));
156
+ } catch {
157
+ return failed(controller.signal.aborted ? "timeout" : "source-error");
158
+ } finally {
159
+ clearTimeout(timer);
160
+ controller.abort();
161
+ }
162
+ }
163
+ };
164
+ }
165
+ function registerInboxRowsSupplier(register, supplier) {
166
+ const h = createInboxRowsHandlers(supplier);
167
+ return register(inboxRowsRpcContract, { [METHODS.describe]: (input) => h.describe(input), [METHODS.read]: (input, context) => h.read(input, context) });
168
+ }
169
+ function createInboxRowsClient(transport, pluginId) {
170
+ let pending = false;
171
+ async function call(method, input, outer) {
172
+ if (pending)
173
+ throw Error("Inbox row request still outstanding");
174
+ outer?.throwIfAborted();
175
+ pending = true;
176
+ const controller = new AbortController, signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
177
+ let timer, abort;
178
+ const flight = Promise.resolve().then(() => {
179
+ signal.throwIfAborted();
180
+ return transport({ pluginId, method, input, signal });
181
+ }).finally(() => {
182
+ pending = false;
183
+ });
184
+ const deadline = new Promise((_, reject) => {
185
+ timer = setTimeout(() => {
186
+ reject(Error("Inbox row read timed out"));
187
+ controller.abort();
188
+ }, LIMITS.callMs);
189
+ });
190
+ const aborted = new Promise((_, reject) => {
191
+ abort = () => reject(new DOMException("Aborted", "AbortError"));
192
+ signal.addEventListener("abort", abort, { once: true });
193
+ });
194
+ try {
195
+ return await Promise.race([flight, deadline, aborted]);
196
+ } finally {
197
+ clearTimeout(timer);
198
+ if (abort)
199
+ signal.removeEventListener("abort", abort);
200
+ controller.abort();
201
+ }
202
+ }
203
+ return {
204
+ pluginId,
205
+ async describe(signal) {
206
+ const raw = await call(METHODS.describe, null, signal), envelope = parseBounded(describeEnvelopeSchema, raw, LIMITS.describeBytes);
207
+ if (!envelope.versions.includes(1))
208
+ throw Error("Unsupported Inbox rows version");
209
+ return parseBounded(describeSchema, raw, LIMITS.describeBytes);
210
+ },
211
+ async read(raw, signal) {
212
+ const input = parseReadInput(raw);
213
+ return parseReadOutput(input, await call(METHODS.read, input, signal));
214
+ }
215
+ };
216
+ }
217
+ export {
218
+ registerInboxRowsSupplier,
219
+ inboxRowsRpcContract,
220
+ createInboxRowsHandlers,
221
+ createInboxRowsClient
222
+ };
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ // src/index.ts
2
+ import { z } from "zod";
3
+ var PROTOCOL = "phosphor.bb-inbox-rows";
4
+ var METHODS = Object.freeze({ describe: "inboxRowsV1Describe", read: "inboxRowsV1Read" });
5
+ var ROW_PLUGIN_ID = "inbox-sidebar-nerd";
6
+ var ROW_SCHEMA = "inbox-sidebar-nerd/thread-row@1";
7
+ var LIMITS = Object.freeze({ occurrences: 16, stampBytes: 4096, responseBytes: 60 * 1024, inputBytes: 16 * 1024, describeBytes: 2048, supplierMs: 3500, callMs: 4500, senderEvents: 25, senderConcurrency: 2, profileKeys: 200 });
8
+ var token = z.string().min(1).max(256);
9
+ var key = z.string().min(1).max(512);
10
+ var text = z.string().max(1024);
11
+ var count = z.number().int().min(0).max(1e6);
12
+ var time = z.number().finite().nonnegative();
13
+ var faceSchema = z.object({ key, name: text.nullable(), imageUrl: z.string().max(2048).nullable(), label: text, external: z.boolean() }).passthrough();
14
+ var rowDataSchema = z.object({
15
+ v: z.literal(1),
16
+ threadId: token,
17
+ title: text,
18
+ groupPath: text.nullable(),
19
+ tags: z.array(z.string().max(256)).max(3),
20
+ tagOverflow: count,
21
+ owners: z.array(faceSchema).max(2),
22
+ ownerOverflow: count,
23
+ ownerOverflowLabel: text,
24
+ participants: z.array(faceSchema).max(2),
25
+ participantOverflow: count,
26
+ senderDescription: text,
27
+ status: z.object({ tone: z.enum(["done", "snoozed", "waiting-for-reset", "failed", "needs-you", "working", "woke", "ready", "quiet"]), label: text }).passthrough(),
28
+ time: z.object({ at: time, text: z.string().max(128), title: z.string().max(256) }).passthrough()
29
+ }).passthrough();
30
+ var readInputSchema = z.object({ version: z.literal(1), occurrences: z.array(z.object({ key: token, threadId: token }).strict()).min(1).max(LIMITS.occurrences), archiveMode: z.enum(["active", "include-archived"]), viewAs: key.nullable().optional() }).strict().superRefine((v, ctx) => {
31
+ if (new Set(v.occurrences.map((r) => r.key)).size !== v.occurrences.length)
32
+ ctx.addIssue({ code: "custom", message: "Duplicate occurrence key" });
33
+ });
34
+ var unavailableReasonSchema = z.enum(["not-found", "not-top-level", "excluded", "timeout", "source-error", "oversize"]);
35
+ var ready = z.object({ key: token, threadId: token, state: z.literal("ready"), revision: token, data: rowDataSchema }).passthrough();
36
+ var unavailable = z.object({ key: token, threadId: token, state: z.literal("unavailable"), reason: unavailableReasonSchema }).passthrough();
37
+ var readOutputSchema = z.object({ version: z.literal(1), ownerKey: key.nullable(), revision: token, rows: z.array(z.discriminatedUnion("state", [ready, unavailable])).min(1).max(LIMITS.occurrences) }).passthrough();
38
+ var describeEnvelopeSchema = z.object({ protocol: z.literal(PROTOCOL), versions: z.array(z.number().int().positive()).max(16) }).passthrough();
39
+ var describeSchema = describeEnvelopeSchema.extend({ schema: z.literal(ROW_SCHEMA), readMethod: z.literal(METHODS.read), limits: z.object({ occurrences: z.literal(16), stampBytes: z.literal(4096), responseBytes: z.literal(61440), supplierMs: z.literal(3500) }).passthrough() });
40
+ var DESCRIPTION = { protocol: PROTOCOL, versions: [1], schema: ROW_SCHEMA, readMethod: METHODS.read, limits: { occurrences: 16, stampBytes: 4096, responseBytes: 61440, supplierMs: 3500 } };
41
+ function canonicalBytes(value) {
42
+ const json = JSON.stringify(value);
43
+ if (json === undefined)
44
+ throw Error("Expected JSON");
45
+ return new TextEncoder().encode(json).byteLength;
46
+ }
47
+ function parseBounded(schema, raw, max) {
48
+ if (canonicalBytes(raw) > max)
49
+ throw RangeError("Inbox rows byte limit");
50
+ return schema.parse(raw);
51
+ }
52
+ function parseReadInput(raw) {
53
+ return parseBounded(readInputSchema, raw, LIMITS.inputBytes);
54
+ }
55
+ function rowPresentation(raw) {
56
+ return { pluginId: ROW_PLUGIN_ID, schema: ROW_SCHEMA, data: raw };
57
+ }
58
+ function parseRowData(raw) {
59
+ if (canonicalBytes({ pluginId: ROW_PLUGIN_ID, schema: ROW_SCHEMA, data: raw }) > LIMITS.stampBytes)
60
+ throw RangeError("Inbox stamp byte limit");
61
+ const visit = (value, depth) => {
62
+ if (depth > 8)
63
+ throw RangeError("Inbox stamp depth limit");
64
+ if (value && typeof value === "object")
65
+ for (const child of Object.values(value))
66
+ visit(child, depth + 1);
67
+ };
68
+ visit(raw, 0);
69
+ return rowDataSchema.parse(raw);
70
+ }
71
+ function validateReadOutput(input, output) {
72
+ if (input.viewAs != null && output.ownerKey !== input.viewAs)
73
+ throw Error("Inbox row owner lens mismatch");
74
+ if (output.rows.length !== input.occurrences.length)
75
+ throw Error("Missing Inbox row occurrence");
76
+ const requested = new Map(input.occurrences.map((o) => [o.key, o.threadId])), seen = new Set;
77
+ for (const row of output.rows) {
78
+ if (seen.has(row.key) || requested.get(row.key) !== row.threadId)
79
+ throw Error("Unexpected Inbox row occurrence");
80
+ seen.add(row.key);
81
+ if (row.state === "ready") {
82
+ if (row.data.threadId !== row.threadId)
83
+ throw Error("Inbox row data identity mismatch");
84
+ parseRowData(row.data);
85
+ }
86
+ }
87
+ return output;
88
+ }
89
+ function parseReadOutput(input, raw) {
90
+ return validateReadOutput(input, parseBounded(readOutputSchema, raw, LIMITS.responseBytes));
91
+ }
92
+ function boundReadOutput(input, raw) {
93
+ const rows = raw.rows.map((row) => {
94
+ if (row.state === "unavailable")
95
+ return row;
96
+ try {
97
+ parseRowData(row.data);
98
+ return row;
99
+ } catch {
100
+ let oversized = false;
101
+ try {
102
+ oversized = canonicalBytes(rowPresentation(row.data)) > LIMITS.stampBytes;
103
+ } catch {}
104
+ return { key: row.key, threadId: row.threadId, state: "unavailable", reason: oversized ? "oversize" : "source-error" };
105
+ }
106
+ });
107
+ const output = { ...raw, rows };
108
+ for (let index = rows.length - 1;canonicalBytes(output) > LIMITS.responseBytes && index >= 0; index--) {
109
+ const row = rows[index];
110
+ if (row.state === "ready")
111
+ rows[index] = { key: row.key, threadId: row.threadId, state: "unavailable", reason: "oversize" };
112
+ }
113
+ return parseReadOutput(input, output);
114
+ }
115
+ function assertRowPresentation(entry) {
116
+ if (entry.schema !== ROW_SCHEMA || !entry.methods.includes(METHODS.read))
117
+ throw Error("Inbox row registration must allow the fixed batch read method");
118
+ }
119
+ export {
120
+ validateReadOutput,
121
+ unavailableReasonSchema,
122
+ rowPresentation,
123
+ rowDataSchema,
124
+ readOutputSchema,
125
+ readInputSchema,
126
+ parseRowData,
127
+ parseReadOutput,
128
+ parseReadInput,
129
+ parseBounded,
130
+ describeSchema,
131
+ describeEnvelopeSchema,
132
+ canonicalBytes,
133
+ boundReadOutput,
134
+ assertRowPresentation,
135
+ ROW_SCHEMA,
136
+ ROW_PLUGIN_ID,
137
+ PROTOCOL,
138
+ METHODS,
139
+ LIMITS,
140
+ DESCRIPTION
141
+ };
@@ -0,0 +1,203 @@
1
+ // src/testing.ts
2
+ import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
3
+
4
+ // src/bb.ts
5
+ import { z as z2 } from "zod";
6
+
7
+ // src/index.ts
8
+ import { z } from "zod";
9
+ var PROTOCOL = "phosphor.bb-inbox-rows";
10
+ var METHODS = Object.freeze({ describe: "inboxRowsV1Describe", read: "inboxRowsV1Read" });
11
+ var ROW_PLUGIN_ID = "inbox-sidebar-nerd";
12
+ var ROW_SCHEMA = "inbox-sidebar-nerd/thread-row@1";
13
+ var LIMITS = Object.freeze({ occurrences: 16, stampBytes: 4096, responseBytes: 60 * 1024, inputBytes: 16 * 1024, describeBytes: 2048, supplierMs: 3500, callMs: 4500, senderEvents: 25, senderConcurrency: 2, profileKeys: 200 });
14
+ var token = z.string().min(1).max(256);
15
+ var key = z.string().min(1).max(512);
16
+ var text = z.string().max(1024);
17
+ var count = z.number().int().min(0).max(1e6);
18
+ var time = z.number().finite().nonnegative();
19
+ var faceSchema = z.object({ key, name: text.nullable(), imageUrl: z.string().max(2048).nullable(), label: text, external: z.boolean() }).passthrough();
20
+ var rowDataSchema = z.object({
21
+ v: z.literal(1),
22
+ threadId: token,
23
+ title: text,
24
+ groupPath: text.nullable(),
25
+ tags: z.array(z.string().max(256)).max(3),
26
+ tagOverflow: count,
27
+ owners: z.array(faceSchema).max(2),
28
+ ownerOverflow: count,
29
+ ownerOverflowLabel: text,
30
+ participants: z.array(faceSchema).max(2),
31
+ participantOverflow: count,
32
+ senderDescription: text,
33
+ status: z.object({ tone: z.enum(["done", "snoozed", "waiting-for-reset", "failed", "needs-you", "working", "woke", "ready", "quiet"]), label: text }).passthrough(),
34
+ time: z.object({ at: time, text: z.string().max(128), title: z.string().max(256) }).passthrough()
35
+ }).passthrough();
36
+ var readInputSchema = z.object({ version: z.literal(1), occurrences: z.array(z.object({ key: token, threadId: token }).strict()).min(1).max(LIMITS.occurrences), archiveMode: z.enum(["active", "include-archived"]), viewAs: key.nullable().optional() }).strict().superRefine((v, ctx) => {
37
+ if (new Set(v.occurrences.map((r) => r.key)).size !== v.occurrences.length)
38
+ ctx.addIssue({ code: "custom", message: "Duplicate occurrence key" });
39
+ });
40
+ var unavailableReasonSchema = z.enum(["not-found", "not-top-level", "excluded", "timeout", "source-error", "oversize"]);
41
+ var ready = z.object({ key: token, threadId: token, state: z.literal("ready"), revision: token, data: rowDataSchema }).passthrough();
42
+ var unavailable = z.object({ key: token, threadId: token, state: z.literal("unavailable"), reason: unavailableReasonSchema }).passthrough();
43
+ var readOutputSchema = z.object({ version: z.literal(1), ownerKey: key.nullable(), revision: token, rows: z.array(z.discriminatedUnion("state", [ready, unavailable])).min(1).max(LIMITS.occurrences) }).passthrough();
44
+ var describeEnvelopeSchema = z.object({ protocol: z.literal(PROTOCOL), versions: z.array(z.number().int().positive()).max(16) }).passthrough();
45
+ var describeSchema = describeEnvelopeSchema.extend({ schema: z.literal(ROW_SCHEMA), readMethod: z.literal(METHODS.read), limits: z.object({ occurrences: z.literal(16), stampBytes: z.literal(4096), responseBytes: z.literal(61440), supplierMs: z.literal(3500) }).passthrough() });
46
+ var DESCRIPTION = { protocol: PROTOCOL, versions: [1], schema: ROW_SCHEMA, readMethod: METHODS.read, limits: { occurrences: 16, stampBytes: 4096, responseBytes: 61440, supplierMs: 3500 } };
47
+ function canonicalBytes(value) {
48
+ const json = JSON.stringify(value);
49
+ if (json === undefined)
50
+ throw Error("Expected JSON");
51
+ return new TextEncoder().encode(json).byteLength;
52
+ }
53
+ function parseBounded(schema, raw, max) {
54
+ if (canonicalBytes(raw) > max)
55
+ throw RangeError("Inbox rows byte limit");
56
+ return schema.parse(raw);
57
+ }
58
+ function parseReadInput(raw) {
59
+ return parseBounded(readInputSchema, raw, LIMITS.inputBytes);
60
+ }
61
+ function parseRowData(raw) {
62
+ if (canonicalBytes({ pluginId: ROW_PLUGIN_ID, schema: ROW_SCHEMA, data: raw }) > LIMITS.stampBytes)
63
+ throw RangeError("Inbox stamp byte limit");
64
+ const visit = (value, depth) => {
65
+ if (depth > 8)
66
+ throw RangeError("Inbox stamp depth limit");
67
+ if (value && typeof value === "object")
68
+ for (const child of Object.values(value))
69
+ visit(child, depth + 1);
70
+ };
71
+ visit(raw, 0);
72
+ return rowDataSchema.parse(raw);
73
+ }
74
+ function validateReadOutput(input, output) {
75
+ if (input.viewAs != null && output.ownerKey !== input.viewAs)
76
+ throw Error("Inbox row owner lens mismatch");
77
+ if (output.rows.length !== input.occurrences.length)
78
+ throw Error("Missing Inbox row occurrence");
79
+ const requested = new Map(input.occurrences.map((o) => [o.key, o.threadId])), seen = new Set;
80
+ for (const row of output.rows) {
81
+ if (seen.has(row.key) || requested.get(row.key) !== row.threadId)
82
+ throw Error("Unexpected Inbox row occurrence");
83
+ seen.add(row.key);
84
+ if (row.state === "ready") {
85
+ if (row.data.threadId !== row.threadId)
86
+ throw Error("Inbox row data identity mismatch");
87
+ parseRowData(row.data);
88
+ }
89
+ }
90
+ return output;
91
+ }
92
+ function parseReadOutput(input, raw) {
93
+ return validateReadOutput(input, parseBounded(readOutputSchema, raw, LIMITS.responseBytes));
94
+ }
95
+ function assertRowPresentation(entry) {
96
+ if (entry.schema !== ROW_SCHEMA || !entry.methods.includes(METHODS.read))
97
+ throw Error("Inbox row registration must allow the fixed batch read method");
98
+ }
99
+
100
+ // src/bb.ts
101
+ function sdkSchema(schema) {
102
+ return { "~standard": { version: 1, vendor: "bb-inbox-rows", jsonSchema: { input: () => schema.toJSONSchema({ io: "input", unrepresentable: "any" }), output: () => schema.toJSONSchema({ io: "output", unrepresentable: "any" }) }, validate(value) {
103
+ const r = schema.safeParse(value);
104
+ return r.success ? { value: r.data } : { issues: r.error.issues };
105
+ } } };
106
+ }
107
+ var inboxRowsRpcContract = {
108
+ [METHODS.describe]: { input: sdkSchema(z2.null()), output: sdkSchema(z2.unknown()) },
109
+ [METHODS.read]: { input: sdkSchema(readInputSchema), output: sdkSchema(z2.unknown()) }
110
+ };
111
+ function createInboxRowsClient(transport, pluginId) {
112
+ let pending = false;
113
+ async function call(method, input, outer) {
114
+ if (pending)
115
+ throw Error("Inbox row request still outstanding");
116
+ outer?.throwIfAborted();
117
+ pending = true;
118
+ const controller = new AbortController, signal = outer ? AbortSignal.any([outer, controller.signal]) : controller.signal;
119
+ let timer, abort;
120
+ const flight = Promise.resolve().then(() => {
121
+ signal.throwIfAborted();
122
+ return transport({ pluginId, method, input, signal });
123
+ }).finally(() => {
124
+ pending = false;
125
+ });
126
+ const deadline = new Promise((_, reject) => {
127
+ timer = setTimeout(() => {
128
+ reject(Error("Inbox row read timed out"));
129
+ controller.abort();
130
+ }, LIMITS.callMs);
131
+ });
132
+ const aborted = new Promise((_, reject) => {
133
+ abort = () => reject(new DOMException("Aborted", "AbortError"));
134
+ signal.addEventListener("abort", abort, { once: true });
135
+ });
136
+ try {
137
+ return await Promise.race([flight, deadline, aborted]);
138
+ } finally {
139
+ clearTimeout(timer);
140
+ if (abort)
141
+ signal.removeEventListener("abort", abort);
142
+ controller.abort();
143
+ }
144
+ }
145
+ return {
146
+ pluginId,
147
+ async describe(signal) {
148
+ const raw = await call(METHODS.describe, null, signal), envelope = parseBounded(describeEnvelopeSchema, raw, LIMITS.describeBytes);
149
+ if (!envelope.versions.includes(1))
150
+ throw Error("Unsupported Inbox rows version");
151
+ return parseBounded(describeSchema, raw, LIMITS.describeBytes);
152
+ },
153
+ async read(raw, signal) {
154
+ const input = parseReadInput(raw);
155
+ return parseReadOutput(input, await call(METHODS.read, input, signal));
156
+ }
157
+ };
158
+ }
159
+
160
+ // src/testing.ts
161
+ async function runInboxRowsSupplierConformance(options) {
162
+ const host = (options.createHost ?? createFakePluginHost)({ pluginId: options.pluginId }), checks = [];
163
+ const check = async (name, fn) => {
164
+ try {
165
+ await fn();
166
+ checks.push({ name, passed: true });
167
+ } catch (e) {
168
+ checks.push({ name, passed: false, error: String(e) });
169
+ }
170
+ };
171
+ try {
172
+ await check("real-registration", () => options.register(host.bb));
173
+ const client = createInboxRowsClient(async (c) => {
174
+ if (c.pluginId !== options.pluginId)
175
+ throw Error("Wrong owner");
176
+ return host.harness.behavior.callRpc(c.method, c.input);
177
+ }, options.pluginId);
178
+ await check("version-negotiation", () => client.describe());
179
+ await check("presentation-read-allowlist", () => assertRowPresentation(options.presentation));
180
+ await check("strict-input", async () => {
181
+ for (const input of [{ version: 1, occurrences: [{ key: "k", threadId: "t" }], archiveMode: "active", extra: true }, { version: 1, occurrences: [{ key: "k", threadId: "t", extra: true }], archiveMode: "active" }, { version: 1, occurrences: [{ key: "k", threadId: "t" }, { key: "k", threadId: "t2" }], archiveMode: "active" }]) {
182
+ let rejected = false;
183
+ try {
184
+ await host.harness.behavior.callRpc(METHODS.read, input);
185
+ } catch (e) {
186
+ rejected = e.name === "ZodError" || e.code === "invalid_input";
187
+ }
188
+ if (!rejected)
189
+ throw Error("Malformed input was not rejected with validation evidence");
190
+ }
191
+ });
192
+ for (const [i, c] of options.cases.entries())
193
+ await check(`read:${i}`, async () => c.check(await client.read(c.input)));
194
+ if (!options.cases.length)
195
+ checks.push({ name: "supplier-owned-witnesses", passed: false });
196
+ return { pluginId: options.pluginId, proof: "source-conformance", passed: checks.every((c) => c.passed), checks };
197
+ } finally {
198
+ await host.harness.lifecycle.dispose();
199
+ }
200
+ }
201
+ export {
202
+ runInboxRowsSupplierConformance
203
+ };
@@ -0,0 +1,134 @@
1
+ import type { StandardSchemaV1 } from '@get-bb/plugin-sdk';
2
+ import { METHODS, type ReadInput, type ReadOutput } from './index.js';
3
+ export declare const inboxRowsRpcContract: {
4
+ inboxRowsV1Describe: {
5
+ input: StandardSchemaV1<null, null>;
6
+ output: StandardSchemaV1<unknown, unknown>;
7
+ };
8
+ inboxRowsV1Read: {
9
+ input: StandardSchemaV1<{
10
+ version: 1;
11
+ occurrences: {
12
+ key: string;
13
+ threadId: string;
14
+ }[];
15
+ archiveMode: "active" | "include-archived";
16
+ viewAs?: string | null | undefined;
17
+ }, {
18
+ version: 1;
19
+ occurrences: {
20
+ key: string;
21
+ threadId: string;
22
+ }[];
23
+ archiveMode: "active" | "include-archived";
24
+ viewAs?: string | null | undefined;
25
+ }>;
26
+ output: StandardSchemaV1<unknown, unknown>;
27
+ };
28
+ };
29
+ export interface InboxRowsSupplier<Context = void> {
30
+ read(input: ReadInput, context: Context | undefined, signal: AbortSignal): Promise<ReadOutput>;
31
+ }
32
+ export declare function createInboxRowsHandlers<Context>(supplier: InboxRowsSupplier<Context>): {
33
+ describe(raw?: unknown): {
34
+ readonly protocol: "phosphor.bb-inbox-rows";
35
+ readonly versions: readonly [1];
36
+ readonly schema: "inbox-sidebar-nerd/thread-row@1";
37
+ readonly readMethod: "inboxRowsV1Read";
38
+ readonly limits: {
39
+ readonly occurrences: 16;
40
+ readonly stampBytes: 4096;
41
+ readonly responseBytes: 61440;
42
+ readonly supplierMs: 3500;
43
+ };
44
+ };
45
+ read(raw: unknown, context?: Context): Promise<ReadOutput>;
46
+ };
47
+ /** Inject the public SDK registrar, or an identity binding's registrar preserving invocation. */
48
+ export declare function registerInboxRowsSupplier<Context, Result>(register: (contract: typeof inboxRowsRpcContract, handlers: {
49
+ [METHODS.describe]: (input: unknown) => unknown;
50
+ [METHODS.read]: (input: unknown, context?: Context) => Promise<ReadOutput>;
51
+ }) => Result, supplier: InboxRowsSupplier<Context>): Result;
52
+ export type InboxRowsTransport = (call: {
53
+ pluginId: string;
54
+ method: string;
55
+ input: unknown;
56
+ signal: AbortSignal;
57
+ }) => Promise<unknown>;
58
+ /** One shared caller resource. Selection changes must not invoke this client. */
59
+ export declare function createInboxRowsClient(transport: InboxRowsTransport, pluginId: string): {
60
+ pluginId: string;
61
+ describe(signal?: AbortSignal): Promise<{
62
+ [x: string]: unknown;
63
+ protocol: "phosphor.bb-inbox-rows";
64
+ versions: number[];
65
+ schema: "inbox-sidebar-nerd/thread-row@1";
66
+ readMethod: "inboxRowsV1Read";
67
+ limits: {
68
+ [x: string]: unknown;
69
+ occurrences: 16;
70
+ stampBytes: 4096;
71
+ responseBytes: 61440;
72
+ supplierMs: 3500;
73
+ };
74
+ }>;
75
+ read(raw: ReadInput, signal?: AbortSignal): Promise<{
76
+ [x: string]: unknown;
77
+ version: 1;
78
+ ownerKey: string | null;
79
+ revision: string;
80
+ rows: ({
81
+ [x: string]: unknown;
82
+ key: string;
83
+ threadId: string;
84
+ state: "ready";
85
+ revision: string;
86
+ data: {
87
+ [x: string]: unknown;
88
+ v: 1;
89
+ threadId: string;
90
+ title: string;
91
+ groupPath: string | null;
92
+ tags: string[];
93
+ tagOverflow: number;
94
+ owners: {
95
+ [x: string]: unknown;
96
+ key: string;
97
+ name: string | null;
98
+ imageUrl: string | null;
99
+ label: string;
100
+ external: boolean;
101
+ }[];
102
+ ownerOverflow: number;
103
+ ownerOverflowLabel: string;
104
+ participants: {
105
+ [x: string]: unknown;
106
+ key: string;
107
+ name: string | null;
108
+ imageUrl: string | null;
109
+ label: string;
110
+ external: boolean;
111
+ }[];
112
+ participantOverflow: number;
113
+ senderDescription: string;
114
+ status: {
115
+ [x: string]: unknown;
116
+ tone: "done" | "snoozed" | "waiting-for-reset" | "failed" | "needs-you" | "working" | "woke" | "ready" | "quiet";
117
+ label: string;
118
+ };
119
+ time: {
120
+ [x: string]: unknown;
121
+ at: number;
122
+ text: string;
123
+ title: string;
124
+ };
125
+ };
126
+ } | {
127
+ [x: string]: unknown;
128
+ key: string;
129
+ threadId: string;
130
+ state: "unavailable";
131
+ reason: "not-found" | "not-top-level" | "excluded" | "timeout" | "source-error" | "oversize";
132
+ })[];
133
+ }>;
134
+ };
@@ -0,0 +1,244 @@
1
+ import { z } from 'zod';
2
+ export declare const PROTOCOL = "phosphor.bb-inbox-rows";
3
+ export declare const METHODS: Readonly<{
4
+ describe: "inboxRowsV1Describe";
5
+ read: "inboxRowsV1Read";
6
+ }>;
7
+ export declare const ROW_PLUGIN_ID = "inbox-sidebar-nerd";
8
+ export declare const ROW_SCHEMA = "inbox-sidebar-nerd/thread-row@1";
9
+ export declare const LIMITS: Readonly<{
10
+ occurrences: 16;
11
+ stampBytes: 4096;
12
+ responseBytes: number;
13
+ inputBytes: number;
14
+ describeBytes: 2048;
15
+ supplierMs: 3500;
16
+ callMs: 4500;
17
+ senderEvents: 25;
18
+ senderConcurrency: 2;
19
+ profileKeys: 200;
20
+ }>;
21
+ /** Every evolving output object preserves additive fields; raw bytes are checked before decoding. */
22
+ export declare const rowDataSchema: z.ZodObject<{
23
+ v: z.ZodLiteral<1>;
24
+ threadId: z.ZodString;
25
+ title: z.ZodString;
26
+ groupPath: z.ZodNullable<z.ZodString>;
27
+ tags: z.ZodArray<z.ZodString>;
28
+ tagOverflow: z.ZodNumber;
29
+ owners: z.ZodArray<z.ZodObject<{
30
+ key: z.ZodString;
31
+ name: z.ZodNullable<z.ZodString>;
32
+ imageUrl: z.ZodNullable<z.ZodString>;
33
+ label: z.ZodString;
34
+ external: z.ZodBoolean;
35
+ }, z.core.$loose>>;
36
+ ownerOverflow: z.ZodNumber;
37
+ ownerOverflowLabel: z.ZodString;
38
+ participants: z.ZodArray<z.ZodObject<{
39
+ key: z.ZodString;
40
+ name: z.ZodNullable<z.ZodString>;
41
+ imageUrl: z.ZodNullable<z.ZodString>;
42
+ label: z.ZodString;
43
+ external: z.ZodBoolean;
44
+ }, z.core.$loose>>;
45
+ participantOverflow: z.ZodNumber;
46
+ senderDescription: z.ZodString;
47
+ status: z.ZodObject<{
48
+ tone: z.ZodEnum<{
49
+ done: "done";
50
+ snoozed: "snoozed";
51
+ "waiting-for-reset": "waiting-for-reset";
52
+ failed: "failed";
53
+ "needs-you": "needs-you";
54
+ working: "working";
55
+ woke: "woke";
56
+ ready: "ready";
57
+ quiet: "quiet";
58
+ }>;
59
+ label: z.ZodString;
60
+ }, z.core.$loose>;
61
+ time: z.ZodObject<{
62
+ at: z.ZodNumber;
63
+ text: z.ZodString;
64
+ title: z.ZodString;
65
+ }, z.core.$loose>;
66
+ }, z.core.$loose>;
67
+ export type RowData = z.infer<typeof rowDataSchema>;
68
+ export declare const readInputSchema: z.ZodObject<{
69
+ version: z.ZodLiteral<1>;
70
+ occurrences: z.ZodArray<z.ZodObject<{
71
+ key: z.ZodString;
72
+ threadId: z.ZodString;
73
+ }, z.core.$strict>>;
74
+ archiveMode: z.ZodEnum<{
75
+ active: "active";
76
+ "include-archived": "include-archived";
77
+ }>;
78
+ viewAs: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
+ }, z.core.$strict>;
80
+ export type ReadInput = z.infer<typeof readInputSchema>;
81
+ export declare const unavailableReasonSchema: z.ZodEnum<{
82
+ "not-found": "not-found";
83
+ "not-top-level": "not-top-level";
84
+ excluded: "excluded";
85
+ timeout: "timeout";
86
+ "source-error": "source-error";
87
+ oversize: "oversize";
88
+ }>;
89
+ export type UnavailableReason = z.infer<typeof unavailableReasonSchema>;
90
+ export declare const readOutputSchema: z.ZodObject<{
91
+ version: z.ZodLiteral<1>;
92
+ ownerKey: z.ZodNullable<z.ZodString>;
93
+ revision: z.ZodString;
94
+ rows: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
95
+ key: z.ZodString;
96
+ threadId: z.ZodString;
97
+ state: z.ZodLiteral<"ready">;
98
+ revision: z.ZodString;
99
+ data: z.ZodObject<{
100
+ v: z.ZodLiteral<1>;
101
+ threadId: z.ZodString;
102
+ title: z.ZodString;
103
+ groupPath: z.ZodNullable<z.ZodString>;
104
+ tags: z.ZodArray<z.ZodString>;
105
+ tagOverflow: z.ZodNumber;
106
+ owners: z.ZodArray<z.ZodObject<{
107
+ key: z.ZodString;
108
+ name: z.ZodNullable<z.ZodString>;
109
+ imageUrl: z.ZodNullable<z.ZodString>;
110
+ label: z.ZodString;
111
+ external: z.ZodBoolean;
112
+ }, z.core.$loose>>;
113
+ ownerOverflow: z.ZodNumber;
114
+ ownerOverflowLabel: z.ZodString;
115
+ participants: z.ZodArray<z.ZodObject<{
116
+ key: z.ZodString;
117
+ name: z.ZodNullable<z.ZodString>;
118
+ imageUrl: z.ZodNullable<z.ZodString>;
119
+ label: z.ZodString;
120
+ external: z.ZodBoolean;
121
+ }, z.core.$loose>>;
122
+ participantOverflow: z.ZodNumber;
123
+ senderDescription: z.ZodString;
124
+ status: z.ZodObject<{
125
+ tone: z.ZodEnum<{
126
+ done: "done";
127
+ snoozed: "snoozed";
128
+ "waiting-for-reset": "waiting-for-reset";
129
+ failed: "failed";
130
+ "needs-you": "needs-you";
131
+ working: "working";
132
+ woke: "woke";
133
+ ready: "ready";
134
+ quiet: "quiet";
135
+ }>;
136
+ label: z.ZodString;
137
+ }, z.core.$loose>;
138
+ time: z.ZodObject<{
139
+ at: z.ZodNumber;
140
+ text: z.ZodString;
141
+ title: z.ZodString;
142
+ }, z.core.$loose>;
143
+ }, z.core.$loose>;
144
+ }, z.core.$loose>, z.ZodObject<{
145
+ key: z.ZodString;
146
+ threadId: z.ZodString;
147
+ state: z.ZodLiteral<"unavailable">;
148
+ reason: z.ZodEnum<{
149
+ "not-found": "not-found";
150
+ "not-top-level": "not-top-level";
151
+ excluded: "excluded";
152
+ timeout: "timeout";
153
+ "source-error": "source-error";
154
+ oversize: "oversize";
155
+ }>;
156
+ }, z.core.$loose>], "state">>;
157
+ }, z.core.$loose>;
158
+ export type ReadOutput = z.infer<typeof readOutputSchema>;
159
+ export declare const describeEnvelopeSchema: z.ZodObject<{
160
+ protocol: z.ZodLiteral<"phosphor.bb-inbox-rows">;
161
+ versions: z.ZodArray<z.ZodNumber>;
162
+ }, z.core.$loose>;
163
+ export declare const describeSchema: z.ZodObject<{
164
+ protocol: z.ZodLiteral<"phosphor.bb-inbox-rows">;
165
+ versions: z.ZodArray<z.ZodNumber>;
166
+ schema: z.ZodLiteral<"inbox-sidebar-nerd/thread-row@1">;
167
+ readMethod: z.ZodLiteral<"inboxRowsV1Read">;
168
+ limits: z.ZodObject<{
169
+ occurrences: z.ZodLiteral<16>;
170
+ stampBytes: z.ZodLiteral<4096>;
171
+ responseBytes: z.ZodLiteral<61440>;
172
+ supplierMs: z.ZodLiteral<3500>;
173
+ }, z.core.$loose>;
174
+ }, z.core.$loose>;
175
+ export declare const DESCRIPTION: {
176
+ readonly protocol: "phosphor.bb-inbox-rows";
177
+ readonly versions: readonly [1];
178
+ readonly schema: "inbox-sidebar-nerd/thread-row@1";
179
+ readonly readMethod: "inboxRowsV1Read";
180
+ readonly limits: {
181
+ readonly occurrences: 16;
182
+ readonly stampBytes: 4096;
183
+ readonly responseBytes: 61440;
184
+ readonly supplierMs: 3500;
185
+ };
186
+ };
187
+ /** Canonical transport JSON size, including Unicode, escapes and additive fields. Not raw HTTP bytes. */
188
+ export declare function canonicalBytes(value: unknown): number;
189
+ export declare function parseBounded<T>(schema: z.ZodType<T>, raw: unknown, max: number): T;
190
+ export declare function parseReadInput(raw: unknown): ReadInput;
191
+ export declare function rowPresentation(raw: RowData): {
192
+ pluginId: string;
193
+ schema: string;
194
+ data: {
195
+ [x: string]: unknown;
196
+ v: 1;
197
+ threadId: string;
198
+ title: string;
199
+ groupPath: string | null;
200
+ tags: string[];
201
+ tagOverflow: number;
202
+ owners: {
203
+ [x: string]: unknown;
204
+ key: string;
205
+ name: string | null;
206
+ imageUrl: string | null;
207
+ label: string;
208
+ external: boolean;
209
+ }[];
210
+ ownerOverflow: number;
211
+ ownerOverflowLabel: string;
212
+ participants: {
213
+ [x: string]: unknown;
214
+ key: string;
215
+ name: string | null;
216
+ imageUrl: string | null;
217
+ label: string;
218
+ external: boolean;
219
+ }[];
220
+ participantOverflow: number;
221
+ senderDescription: string;
222
+ status: {
223
+ [x: string]: unknown;
224
+ tone: "done" | "snoozed" | "waiting-for-reset" | "failed" | "needs-you" | "working" | "woke" | "ready" | "quiet";
225
+ label: string;
226
+ };
227
+ time: {
228
+ [x: string]: unknown;
229
+ at: number;
230
+ text: string;
231
+ title: string;
232
+ };
233
+ };
234
+ };
235
+ export declare function parseRowData(raw: unknown): RowData;
236
+ export declare function validateReadOutput(input: ReadInput, output: ReadOutput): ReadOutput;
237
+ export declare function parseReadOutput(input: ReadInput, raw: unknown): ReadOutput;
238
+ /** Deterministic occurrence-order admission. A stamp never loses portraits to fit. */
239
+ export declare function boundReadOutput(input: ReadInput, raw: ReadOutput): ReadOutput;
240
+ /** Presentation metadata is data-only; registration still belongs to Inbox's content script. */
241
+ export declare function assertRowPresentation(entry: {
242
+ schema: string;
243
+ methods: readonly string[];
244
+ }): void;
@@ -0,0 +1,36 @@
1
+ import type { BbPluginApi } from '@get-bb/plugin-sdk';
2
+ import { createFakePluginHost } from '@get-bb/plugin-sdk/testing';
3
+ import { type ReadInput, type ReadOutput } from './index.js';
4
+ type FakeHost = ReturnType<typeof createFakePluginHost>;
5
+ export interface InboxRowsConformanceHost {
6
+ bb: Pick<BbPluginApi, 'rpc' | 'storage'>;
7
+ harness: {
8
+ behavior: Pick<FakeHost['harness']['behavior'], 'callRpc'>;
9
+ lifecycle: Pick<FakeHost['harness']['lifecycle'], 'dispose'>;
10
+ };
11
+ }
12
+ export declare function runInboxRowsSupplierConformance(options: {
13
+ pluginId: string;
14
+ createHost?: (options: {
15
+ pluginId: string;
16
+ }) => InboxRowsConformanceHost;
17
+ register: (bb: InboxRowsConformanceHost['bb']) => unknown | Promise<unknown>;
18
+ cases: readonly {
19
+ input: ReadInput;
20
+ check: (output: ReadOutput) => unknown;
21
+ }[];
22
+ presentation: {
23
+ schema: string;
24
+ methods: readonly string[];
25
+ };
26
+ }): Promise<{
27
+ pluginId: string;
28
+ proof: "source-conformance";
29
+ passed: boolean;
30
+ checks: {
31
+ name: string;
32
+ passed: boolean;
33
+ error?: string;
34
+ }[];
35
+ }>;
36
+ export {};
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
- "name": "@phosphorco/bb-inbox-rows",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@phosphorco/bb-inbox-rows", "version": "0.1.1", "description": "Bounded inert Inbox row projections between independently installed BB plugins.",
3
+ "type": "module", "license": "MIT", "author": "Phosphor",
4
+ "repository": {"type":"git","url":"git+https://github.com/phosphorco/bb-community-plugins.git","directory":"packages/bb-inbox-rows"},
5
+ "exports": {
6
+ ".":{"types":"./dist/types/index.d.ts","import":"./dist/index.js"},
7
+ "./bb":{"types":"./dist/types/bb.d.ts","import":"./dist/bb.js"},
8
+ "./testing":{"types":"./dist/types/testing.d.ts","import":"./dist/testing.js"}
9
+ },
10
+ "files":["dist/","README.md","CONTRACT.md","LICENSE"], "publishConfig":{"access":"public"},
11
+ "dependencies":{"zod":"^4.3.6"},
12
+ "peerDependencies":{"@get-bb/plugin-sdk":"^0.6.31"}, "peerDependenciesMeta":{"@get-bb/plugin-sdk":{"optional":true}},
13
+ "devDependencies":{"@get-bb/plugin-sdk":"0.6.52","@types/node":"^22","bun":"1.3.14","typescript":"^5.9.3"},
14
+ "scripts":{"build":"bun tools/build.mjs && tsc --project tsconfig.declarations.json","test":"npm run build && node --test test/*.test.mjs","typecheck":"tsc --noEmit"}
15
+ }