@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 +281 -0
- package/dist/index.cjs +913 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +744 -0
- package/dist/index.d.ts +744 -0
- package/dist/index.js +882 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
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
|