@estiva-app/interop 0.9.0 → 0.9.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/CHANGELOG.md +35 -0
- package/README.md +109 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,41 @@ how a declaration is read, is a MAJOR — in `0.x`, a MINOR — even when no
|
|
|
6
6
|
TypeScript signature moved. A consumer upgrading must be able to tell whether
|
|
7
7
|
manifests already published still mean what they meant.
|
|
8
8
|
|
|
9
|
+
## 0.9.1 — 2026-09-03
|
|
10
|
+
|
|
11
|
+
**No behaviour change. The README was wrong, and this is the release that fixes
|
|
12
|
+
the page people are told to start from** (PRO-9).
|
|
13
|
+
|
|
14
|
+
- **§2's example did not compile, and its copy-paste path published an error
|
|
15
|
+
message.** It called `buildActionEvent({ object, actionId, value })`, which is
|
|
16
|
+
not the signature: the function takes the manifest, the kind, the address, the
|
|
17
|
+
object's author, the folder, the signer's pubkey and a clock, because this
|
|
18
|
+
package reaches for none of them. And it returns `UnsignedActionEvent | string`
|
|
19
|
+
where **the string is the refusal** — the example signed and published the
|
|
20
|
+
result without checking, so a reader following it would have put the words
|
|
21
|
+
"This app does not offer …" on the relay.
|
|
22
|
+
|
|
23
|
+
- **The content model was undocumented.** `ResolvedSlot.format` shipped in 0.7.0
|
|
24
|
+
and a body cannot be rendered safely without it: §13 forbids reading either
|
|
25
|
+
content model as the other, and the page said nothing about which one you
|
|
26
|
+
have. Now a table of the three values, `toRenderTree`, and the rule that the
|
|
27
|
+
format is decided by the declaration and never by looking at the body.
|
|
28
|
+
|
|
29
|
+
- **Object-creating actions were undocumented** — `control: 'form'`, `fields`,
|
|
30
|
+
`createsUnder`, and passing an object plus a `newId`, all shipped in 0.6.0.
|
|
31
|
+
With the producer's side too, since `required` is a list of names on `input`
|
|
32
|
+
rather than a flag on each property, which is the sort of thing a page is for.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`test/readme.test.ts` — the README, executed.** Every assertion is a claim
|
|
37
|
+
the page makes in prose, and it lives in `test/` so `npm run typecheck`
|
|
38
|
+
compiles it against the real declarations. The published example, pasted in
|
|
39
|
+
verbatim, fails that pass with `TS2353: 'object' does not exist in type`.
|
|
40
|
+
|
|
41
|
+
The page's examples had been run once, in a scratch project, when the package
|
|
42
|
+
was first published. That is a snapshot; this is a check.
|
|
43
|
+
|
|
9
44
|
## 0.9.0 — 2026-09-02
|
|
10
45
|
|
|
11
46
|
**Behaviour unchanged, and no export moved.** `contentFormatOf`,
|
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ What comes back:
|
|
|
55
55
|
title: { value: 'Billing entry' },
|
|
56
56
|
status: { value: 'In Progress', colour: 'blue' },
|
|
57
57
|
subtitle: { value: 'Settings entry point.' },
|
|
58
|
+
body: { value: '**Stripe Checkout** for billing.', format: 'marker' },
|
|
58
59
|
},
|
|
59
60
|
meta: [{ label: 'Assignee', value: 'abc…', isPubkey: true }],
|
|
60
61
|
children: [ /* … more of the same, if the owner declared a list */ ],
|
|
@@ -69,6 +70,36 @@ politeness, it is required: the slot set is closed but it *grows*, and producers
|
|
|
69
70
|
upgrade before consumers do. `title` is always present, so an object you only
|
|
70
71
|
half understand still renders as a named, resolvable thing.
|
|
71
72
|
|
|
73
|
+
### A `body` says which content model it is in — read it, do not guess
|
|
74
|
+
|
|
75
|
+
`body` is the one slot that carries structure, and it arrives in one of two
|
|
76
|
+
models that must never be read as each other. **The slot tells you which**, so
|
|
77
|
+
you never have to look at the text:
|
|
78
|
+
|
|
79
|
+
| `slot.body.format` | what you have |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `'marker'` | the marker dialect — `**bold**`, `# heading`, `> quote`, fenced code |
|
|
82
|
+
| `'blocks'` | a JSON block document |
|
|
83
|
+
| `'unknown'` | a model published after your app was written |
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { toRenderTree } from '@estiva-app/protocol'
|
|
87
|
+
|
|
88
|
+
// One tree, whichever model it came in. Marks decided, blocks decided,
|
|
89
|
+
// nothing left to parse — so you cannot render markup by accident.
|
|
90
|
+
const blocks = toRenderTree(object.slots.body.value, object.slots.body.format)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Never decide the model by inspecting the body.** A description that happens to
|
|
94
|
+
begin with `{` is marker text if its event carries no `content-format` tag, and
|
|
95
|
+
there are hundreds of those already published. `'unknown'` exists so you can
|
|
96
|
+
decline: rendering a body you do not understand as plain text is correct, and
|
|
97
|
+
guessing at it is not.
|
|
98
|
+
|
|
99
|
+
A slot that is *not* a body carries no `format` at all — that is how you tell
|
|
100
|
+
"this is prose in the marker dialect" from "this is a title, and models do not
|
|
101
|
+
apply to it".
|
|
102
|
+
|
|
72
103
|
### The states you must draw, and the one that catches everyone
|
|
73
104
|
|
|
74
105
|
A foreign object has more failure states than anything else on your screen,
|
|
@@ -94,16 +125,89 @@ An action is **an event to publish**, never an endpoint to call. There is no
|
|
|
94
125
|
server in the loop, and the owning app can be offline.
|
|
95
126
|
|
|
96
127
|
```ts
|
|
97
|
-
import { buildActionEvent } from '@estiva-app/interop'
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
128
|
+
import { buildActionEvent, resolveManifest } from '@estiva-app/interop'
|
|
129
|
+
|
|
130
|
+
const resolved = await resolveManifest(object.kind, query)
|
|
131
|
+
|
|
132
|
+
const built = buildActionEvent({
|
|
133
|
+
manifest: resolved.manifest,
|
|
134
|
+
kind: object.kind,
|
|
135
|
+
address: object.ref,
|
|
136
|
+
objectAuthor: pointer.pubkey,
|
|
137
|
+
folder, // the object's channel — see below
|
|
138
|
+
actionId: 'set-issue-status',
|
|
139
|
+
value: 'done',
|
|
140
|
+
pubkey: me, // whoever is about to sign
|
|
141
|
+
createdAtMs: Date.now(),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// A refusal is a string, and it is written for a person to read.
|
|
145
|
+
if (typeof built === 'string') return show(built)
|
|
146
|
+
|
|
147
|
+
await relay.publish(await signer.sign(built))
|
|
102
148
|
```
|
|
103
149
|
|
|
150
|
+
**Check the string.** `buildActionEvent` returns `UnsignedActionEvent | string`,
|
|
151
|
+
and the string is why it refused — an undeclared field, a required one left
|
|
152
|
+
empty, a value outside the declared vocabulary. Treat the result as an event
|
|
153
|
+
without checking and you will sign the refusal.
|
|
154
|
+
|
|
155
|
+
Everything it needs is passed in rather than reached for: this package opens no
|
|
156
|
+
socket, reads no clock and holds no identity, so the built event is a pure
|
|
157
|
+
function of its inputs and the whole suite runs against an array.
|
|
158
|
+
|
|
104
159
|
`object.actions` is already resolved against the owner's vocabularies, so a
|
|
105
160
|
`select` arrives with its options and the value it currently holds.
|
|
106
161
|
|
|
162
|
+
### Actions that create an object, not just change one
|
|
163
|
+
|
|
164
|
+
An action with `control: 'form'` makes a whole new object rather than setting a
|
|
165
|
+
field on this one. It arrives with the schema already resolved:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const action = object.actions.find((a) => a.control === 'form')
|
|
169
|
+
|
|
170
|
+
action.fields // [{ name: 'title', type: 'string', required: true }, …]
|
|
171
|
+
action.createsUnder // the address the new object will hang under
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Draw the fields, then pass an object rather than a scalar, plus an id for the
|
|
175
|
+
thing being made:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const built = buildActionEvent({
|
|
179
|
+
/* …as above… */
|
|
180
|
+
actionId: action.id,
|
|
181
|
+
value: { title: 'Payment fails on retry' },
|
|
182
|
+
newId: crypto.randomUUID(),
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Declaring one, from the producer's side — `required` is a list of names, not a
|
|
187
|
+
flag on each property:
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
{
|
|
191
|
+
"id": "add-issue", "label": "Add issue", "appliesTo": "31800",
|
|
192
|
+
"emits": { "kind": 31801, "setTag": "a", "toAddressOf": "self" },
|
|
193
|
+
"input": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"properties": { "title": { "type": "string" }, "note": { "type": "string" } },
|
|
196
|
+
"required": ["title"]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`newId` is **required** for an addressable kind and is supplied by you rather
|
|
202
|
+
than generated here — an object published without one has no address at all, so
|
|
203
|
+
nothing could reference it, comment on it or act on it afterwards.
|
|
204
|
+
|
|
205
|
+
Two rules worth knowing before you draw the form. **A property's name is the tag
|
|
206
|
+
its value is written to**, which is what lets a consumer build an event for an
|
|
207
|
+
app it has never seen. And **nothing can target an event's `content`** — an app
|
|
208
|
+
whose body lives there cannot have it filled from outside, which is why an
|
|
209
|
+
action may declare a `description` field that ends up in a tag.
|
|
210
|
+
|
|
107
211
|
Two consequences worth knowing before you ship it. **Validation is an honour
|
|
108
212
|
system** — nothing stops you publishing a status outside the declared
|
|
109
213
|
vocabulary, and a consumer that skips the check is the one putting junk in a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estiva-app/interop",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Render and act on another app's objects from its published NIP-89 manifest. The owner defines the projection; the consumer decides how it looks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|