@nostr-games/inventory 0.1.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/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # @nostr-games/inventory
2
+
3
+ Framework-independent TypeScript library implementing two Nostr event kinds for
4
+ games:
5
+
6
+ - **`kind:31632`** — Game Item Definition
7
+ - **`kind:31633`** — Game Inventory
8
+
9
+ It provides constants, types, parsers, builders, validators and helpers for
10
+ these kinds. It is pure, has no import-time side effects, no framework or
11
+ application dependencies, and works in both browser and Node environments.
12
+
13
+ > Status: `private` (not published to npm). This is the protocol layer only.
14
+ > No React, hooks, UI, relay clients, signers, encryption, grants, placement,
15
+ > equipment, persistence, or publishing are included in this phase.
16
+
17
+ ## Design principles
18
+
19
+ - **TypeScript-first**, strict compiler settings, no `any` casts.
20
+ - **Pure functions**, no global/module side effects.
21
+ - **Tags are the source of truth.** `content` is optional metadata and never
22
+ required to reconstruct an item or inventory.
23
+ - **No heavy dependencies.** The only Nostr type used is a minimal local
24
+ structural `NostrEvent` interface, so any compatible event object (e.g. from
25
+ `nostr-tools`) can be passed in without adding a dependency.
26
+
27
+ ## Install (local / workspace only)
28
+
29
+ ```bash
30
+ pnpm install
31
+ pnpm run build
32
+ ```
33
+
34
+ ## Public API
35
+
36
+ ### Constants
37
+
38
+ ```ts
39
+ KIND_GAME_ITEM_DEFINITION; // 31632
40
+ KIND_GAME_INVENTORY; // 31633
41
+ ```
42
+
43
+ ### Nostr types
44
+
45
+ ```ts
46
+ interface NostrEvent {
47
+ id;
48
+ pubkey;
49
+ created_at;
50
+ kind;
51
+ tags;
52
+ content;
53
+ sig;
54
+ }
55
+ interface UnsignedEventTemplate<K> {
56
+ kind: K;
57
+ content: string;
58
+ tags: string[][];
59
+ }
60
+ ```
61
+
62
+ Builders return an `UnsignedEventTemplate`. They never create `id` or `sig`
63
+ and never sign. `pubkey` / `created_at` are added by your signing layer.
64
+
65
+ ### Address helpers
66
+
67
+ ```ts
68
+ buildAddressableEventAddress(kind, pubkey, identifier): string
69
+ parseAddressableEventAddress(address, options?): AddressableEventAddress | null
70
+
71
+ buildGameItemAddress(pubkey, itemId): string
72
+ parseGameItemAddress(address, options?): GameItemAddress | null
73
+
74
+ buildGameInventoryAddress(ownerPubkey, inventoryId): string
75
+ parseGameInventoryAddress(address, options?): GameInventoryAddress | null
76
+
77
+ getDTag(event | tags): string | undefined
78
+ ```
79
+
80
+ Addresses are `<kind>:<pubkey>:<d>`. **The `d` component may contain colons**
81
+ (e.g. `blobbi:food:carrot`), so parsing splits only on the first two colons and
82
+ treats the remainder as the identifier.
83
+
84
+ By default the pubkey segment may be any non-empty string (the specs use
85
+ placeholder pubkeys like `pubkey123`). Pass `{ requireHexPubkey: true }` to
86
+ require a 64-char lowercase-hex pubkey.
87
+
88
+ ### Kind 31632 — Game Item Definition
89
+
90
+ ```ts
91
+ parseGameItemDefinition(event, options?): GameItemDefinition | null
92
+ parseGameItemDefinitionResult(event, options?): ParseResult<GameItemDefinition>
93
+ buildGameItemDefinitionEvent(input): UnsignedEventTemplate<31632>
94
+ validateGameItemDefinition(event, options?): ItemDefinitionValidationResult
95
+ ```
96
+
97
+ ### Kind 31633 — Game Inventory
98
+
99
+ ```ts
100
+ parseGameInventory(event, options?): GameInventory | null
101
+ parseGameInventoryResult(event, options?): ParseResult<GameInventory>
102
+ buildGameInventoryEvent(input): UnsignedEventTemplate<31633>
103
+ validateGameInventory(event, options?): InventoryValidationResult
104
+ ```
105
+
106
+ ### Quantity helpers
107
+
108
+ ```ts
109
+ parseInventoryQuantity(raw): number | null // positive integer strings only
110
+ getInventoryItemQuantity(inventory, itemAddress): number
111
+ setInventoryItemQuantity(inventory, itemAddress, quantity, relay?): GameInventory
112
+ addInventoryItemQuantity(inventory, itemAddress, amount, relay?): GameInventory
113
+ removeInventoryItemQuantity(inventory, itemAddress, amount): GameInventory
114
+ ```
115
+
116
+ All inventory helpers are **immutable**: they return a new `GameInventory` and
117
+ never mutate the input.
118
+
119
+ ## Parsing behavior: modes and results
120
+
121
+ Every parser accepts a `mode`:
122
+
123
+ - **`permissive`** (default) — follows the specs' "SHOULD tolerate" rules.
124
+ Unknown tags are kept, invalid item tags are ignored, invalid JSON `content`
125
+ does not block tag parsing, duplicate inventory items resolve using the
126
+ recommended default (`last` valid quantity).
127
+ - **`strict`** — rejects the event where the specs allow rejection: invalid
128
+ JSON `content`, and duplicate item references (see below).
129
+
130
+ "MUST reject" conditions (wrong kind, missing/empty `d`, missing required
131
+ `31632` tags) reject the event in **both** modes.
132
+
133
+ Parsers come in two flavors so failures are never silently hidden:
134
+
135
+ - `parseX(...) => X | null` — convenience.
136
+ - `parseXResult(...) => ParseResult<X>` — structured result exposing:
137
+ - `ok: false` + `error` for a **rejected event**;
138
+ - `ok: true` + `value` for a valid event, plus `warnings[]` describing
139
+ **valid events with invalid tags that were ignored** and other recoverable
140
+ issues (e.g. `invalid-quantity`, `malformed-address`,
141
+ `wrong-referenced-kind`, `invalid-json-content`, `duplicate-item`).
142
+
143
+ ### Content JSON
144
+
145
+ For both kinds, `content` is preserved verbatim on the parsed object
146
+ (`.content`). If it is non-empty valid JSON it is also exposed as
147
+ `.contentJson`. Invalid JSON in permissive mode yields a warning, not a
148
+ rejection. JSON is only required if you opt in via `requireJsonContent` (or use
149
+ `strict` mode). Builders accept either a preserialized string or any
150
+ JSON-serializable value for `content`.
151
+
152
+ ## Duplicate inventory items
153
+
154
+ The 31633 spec allows three strategies for duplicate `a` tag addresses. This
155
+ library implements all three explicitly via `duplicateStrategy`:
156
+
157
+ | Strategy | Behavior | Default in |
158
+ | -------- | ---------------------------- | --------------- |
159
+ | `last` | keep the last valid quantity | permissive mode |
160
+ | `sum` | sum all valid quantities | (opt-in) |
161
+ | `strict` | reject the inventory | strict mode |
162
+
163
+ `last` is the spec's recommended default, so it is the default in permissive
164
+ parsing and in `buildGameInventoryEvent`.
165
+
166
+ ## Builders
167
+
168
+ Builders:
169
+
170
+ - validate input before generating anything (throw on empty `id`/`name`/`type`
171
+ or invalid item addresses);
172
+ - produce unsigned templates (`{ kind, content, tags }`), never `id`/`sig`;
173
+ - normalize quantities (floor, clamp) and **omit zero-quantity items**;
174
+ - emit tags in a **stable, deterministic order** to simplify testing;
175
+ - never duplicate the tags they manage; `extraTags` are appended verbatim.
176
+
177
+ ## Example
178
+
179
+ ```ts
180
+ import {
181
+ buildGameItemDefinitionEvent,
182
+ buildGameItemAddress,
183
+ buildGameInventoryEvent,
184
+ parseGameInventory,
185
+ addInventoryItemQuantity,
186
+ } from "@nostr-games/inventory";
187
+
188
+ // Define an item
189
+ const carrot = buildGameItemDefinitionEvent({
190
+ id: "blobbi:food:carrot",
191
+ name: "Carrot",
192
+ type: "consumable",
193
+ category: "food",
194
+ topics: ["edible"],
195
+ content: { description: "A crunchy carrot." },
196
+ });
197
+
198
+ // Start from an empty inventory event, add 3 carrots, rebuild.
199
+ const carrotAddr = buildGameItemAddress(
200
+ "<issuer-pubkey>",
201
+ "blobbi:food:carrot",
202
+ );
203
+ const empty = buildGameInventoryEvent({ id: "game:blobbi" });
204
+ const inv = parseGameInventory({
205
+ ...empty,
206
+ id: "",
207
+ pubkey: "<owner>",
208
+ created_at: 0,
209
+ sig: "",
210
+ })!;
211
+ const updated = addInventoryItemQuantity(inv, carrotAddr, 3);
212
+ const nextEvent = buildGameInventoryEvent({
213
+ id: updated.id,
214
+ items: updated.items,
215
+ });
216
+ ```
217
+
218
+ ## Ambiguities found in the specifications
219
+
220
+ These were resolved explicitly rather than silently:
221
+
222
+ 1. **`d` values contain colons.** The recommended `d` format
223
+ `namespace:category:slug` means full addresses have more than three
224
+ colon-separated parts. Address parsing therefore treats everything after the
225
+ second colon as the identifier.
226
+ 2. **Pubkey format.** The spec examples use non-hex placeholders. Address
227
+ parsing accepts any non-empty pubkey by default; strict hex validation is
228
+ opt-in (`requireHexPubkey`).
229
+ 3. **`based_on` marker vs. quantity slot.** In `31632`, an `a` tag's index-3
230
+ slot is the `based_on` marker; in `31633` the same slot is the quantity.
231
+ These are handled per-kind and never conflated.
232
+ 4. **Ignore vs. reject.** For `31633`, malformed/invalid item references are
233
+ _ignored_ (parse still succeeds) while missing/empty `d` or wrong `kind`
234
+ _rejects_ the whole event. These are surfaced as warnings vs. errors.
235
+ 5. **Duplicate items.** Default `last`, with `sum` and `strict` available, per
236
+ the spec's three allowed strategies.
237
+ 6. **Invalid JSON `content`.** Only rejected when JSON is required
238
+ (`requireJsonContent` / strict mode); otherwise a warning.
239
+
240
+ ## Project structure
241
+
242
+ ```
243
+ src/
244
+ index.ts # public barrel export
245
+ nostr/event.ts # NostrEvent, UnsignedEventTemplate
246
+ common/
247
+ constants.ts # KIND_* constants
248
+ address.ts # addressable address parse/build
249
+ tags.ts # getDTag, getTagValue(s)
250
+ json.ts # content JSON parse/serialize
251
+ result.ts # ParseMode / ParseResult / warnings
252
+ kinds/
253
+ game-item-definition/ # kind 31632
254
+ { types, constants via common, address, validate, parse, build, index }
255
+ game-inventory/ # kind 31633
256
+ { types, address, quantity, validate, parse, build, helpers, index }
257
+ test/ # vitest suites
258
+ ```
259
+
260
+ ## Scripts
261
+
262
+ ```bash
263
+ pnpm run typecheck # tsc --noEmit
264
+ pnpm run lint # eslint
265
+ pnpm run format # prettier --write
266
+ pnpm run test # vitest run
267
+ pnpm run build # tsup (ESM + CJS + d.ts)
268
+ pnpm run check # all of the above
269
+ ```
270
+
271
+ ## Tooling
272
+
273
+ - **pnpm** — package manager
274
+ - **TypeScript** (strict) — language + typecheck
275
+ - **tsup** (esbuild) — build to ESM + CJS + type declarations
276
+ - **vitest** — tests
277
+ - **ESLint** (typescript-eslint, type-checked) + **Prettier** — lint/format
278
+
279
+ ## License
280
+
281
+ MIT