@estiva-app/interop 0.10.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -6,6 +6,57 @@ 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.11.0 — 2026-09-04
10
+
11
+ **A second identity shape: an event id, for an object that has no `d`.** No
12
+ existing manifest changes meaning; a pattern declaring `<id>` instead of `<d>`
13
+ is new, and `MatchedObjectUrl` gains `by` so a consumer knows which query to
14
+ make.
15
+
16
+ RFC 0.5 §7.6 recorded a message as *addressed by `nevent`, outside this
17
+ grammar*. PEE-17 needed a link to one, and following that literally would have
18
+ put bech32 in a URL — which §7.3 argues against on two grounds that apply to
19
+ `nevent` exactly as they do to `naddr`: a slice of it is checksum bytes rather
20
+ than the identity, and relay hints are part of the encoding, so one object has
21
+ more than one spelling.
22
+
23
+ **A raw event id has neither problem**, and resolves with `{ids: ["<id>"]}` —
24
+ one query, no index, the same property §7.2 chose the bare uuid for. So the rule
25
+ generalises rather than gaining an exception: the identity goes in the path,
26
+ unencoded, and everything before it is decoration.
27
+
28
+ - **`eventIdFromRef`** reads a 64-character id off a ref's tail.
29
+ - **`MatchedObjectUrl.by`** is `'d'` or `'id'` — the difference between
30
+ `{"#d": […]}` and `{ids: […]}`.
31
+ - **The pattern decides which**, by carrying `<id>` or `<d>`. Not sniffed from
32
+ the value: a uuid and a 64-hex string are distinguishable today, and a
33
+ consumer relying on that would be inferring an app's addressing model from a
34
+ character class.
35
+
36
+ Exactly 64 hex is required. A shorter run is a truncated id, which resolves to
37
+ nothing or — worse — to something nobody intended.
38
+
39
+ ## 0.10.1 — 2026-09-03
40
+
41
+ **`matchObjectUrl` matched no fragment route at all.** The parser's path capture
42
+ stopped at the `#`, so `https://ship.estiva.app/#/issue/<uuid>` reduced to no
43
+ segments and matched nothing — including patterns declaring exactly that shape.
44
+
45
+ That is not a corner. Ship served fragment routes until SHI-16 and declares them
46
+ still, so that links already sitting in other people's messages resolve rather
47
+ than rendering as plain text for ever. A consumer on 0.10.0 would have silently
48
+ failed every one of them.
49
+
50
+ Found by a production probe before any consumer was built on it, which is the
51
+ argument for probing the mechanism rather than the fixture.
52
+
53
+ - Path and fragment segments are concatenated rather than swapped, because an
54
+ app may be served under a sub-path *and* use a fragment — Ship's own dev URL
55
+ is `localhost:5190/ship/#/…`, where both halves carry meaning.
56
+ - A fragment route and a path route stay distinct shapes. Collapsing them would
57
+ let a pattern for `/issue/<slug>-<d>` claim `/#/issue/<d>`, and an app that
58
+ means different things by the two would resolve the wrong object.
59
+
9
60
  ## 0.10.0 — 2026-09-03
10
61
 
11
62
  **RFC 0.5 §7's URL grammar, which was accepted with amendments today.** No
package/dist/index.d.ts CHANGED
@@ -33,5 +33,5 @@ export { resolveManifest, resolveForeignObject, resolveForeignEvent, resolveFold
33
33
  * question: those resolve an object you already hold, these turn a string
34
34
  * somebody pasted into one you can hold.
35
35
  */
36
- export { slugify, objectRef, identifierFromRef, urlPatternsOf, matchObjectUrl, type UrlPattern, type MatchedObjectUrl, } from './objectUrl.js';
36
+ export { slugify, objectRef, identifierFromRef, eventIdFromRef, urlPatternsOf, matchObjectUrl, type UrlPattern, type MatchedObjectUrl, } from './objectUrl.js';
37
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,SAAS,EACT,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,aAAa,EAClB,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAA;AAExB;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,SAAS,EACT,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,aAAa,EAClB,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAA;AAExB;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -33,5 +33,5 @@ export { resolveManifest, resolveForeignObject, resolveForeignEvent, resolveFold
33
33
  * question: those resolve an object you already hold, these turn a string
34
34
  * somebody pasted into one you can hold.
35
35
  */
36
- export { slugify, objectRef, identifierFromRef, urlPatternsOf, matchObjectUrl, } from './objectUrl.js';
36
+ export { slugify, objectRef, identifierFromRef, eventIdFromRef, urlPatternsOf, matchObjectUrl, } from './objectUrl.js';
37
37
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,SAAS,EACT,kBAAkB,EAClB,qBAAqB,EAErB,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EAenB,cAAc,GAEf,MAAM,iBAAiB,CAAA;AAExB;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,aAAa,EACb,cAAc,GAGf,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,SAAS,EACT,kBAAkB,EAClB,qBAAqB,EAErB,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EAenB,cAAc,GAEf,MAAM,iBAAiB,CAAA;AAExB;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,GAGf,MAAM,gBAAgB,CAAA"}
@@ -48,6 +48,22 @@ export declare function objectRef(identifier: string, title?: string): string;
48
48
  * rather than silently miss.
49
49
  */
50
50
  export declare function identifierFromRef(ref: string): string | null;
51
+ /**
52
+ * The event id out of a ref, or null.
53
+ *
54
+ * **Why an event id in a URL rather than an `nevent`.** §7.3 rejects bech32 in
55
+ * a URL on two grounds, and both apply to `nevent` exactly as they do to
56
+ * `naddr`: a slice of it is checksum bytes rather than the identity, and relay
57
+ * hints are part of the encoding, so one object has more than one spelling.
58
+ *
59
+ * A raw event id has neither problem. It is the whole identity of an event that
60
+ * has no `d`, it is stable because nothing optional is encoded into it, and it
61
+ * resolves with `{ids: ["<id>"]}` — one query, no index, exactly the property
62
+ * §7.2 chose the bare uuid for. So the rule generalises rather than gaining an
63
+ * exception: **the identity goes in the path, unencoded, and everything before
64
+ * it is decoration.**
65
+ */
66
+ export declare function eventIdFromRef(ref: string): string | null;
51
67
  /**
52
68
  * A URL shape an app says it serves — RFC 0.5 §7.5's `urls`, read off the
53
69
  * manifest event the way `web` is.
@@ -78,7 +94,17 @@ export declare function urlPatternsOf(event: {
78
94
  }): UrlPattern[];
79
95
  /** What a pasted URL turned out to name. */
80
96
  export interface MatchedObjectUrl {
97
+ /** A `d` for an addressable object, or an event id for one without. */
81
98
  identifier: string;
99
+ /**
100
+ * How to resolve `identifier`: by `#d` for an addressable object, by `ids`
101
+ * for an event that has none.
102
+ *
103
+ * Declared by the pattern's placeholder — `<d>` or `<id>` — rather than
104
+ * guessed from the string, because a consumer that inspected the value would
105
+ * be deciding a kind's addressing model from the shape of a hex string.
106
+ */
107
+ by: 'd' | 'id';
82
108
  /** Present only when the matched pattern named one. */
83
109
  kind?: number;
84
110
  }
@@ -1 +1 @@
1
- {"version":3,"file":"objectUrl.d.ts","sourceRoot":"","sources":["../src/objectUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAkBH;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5D;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAA;CAAE,GAAG,UAAU,EAAE,CAQvE;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAuB3F"}
1
+ {"version":3,"file":"objectUrl.d.ts","sourceRoot":"","sources":["../src/objectUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA2BH;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAA;CAAE,GAAG,UAAU,EAAE,CAQvE;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;;;OAOG;IACH,EAAE,EAAE,GAAG,GAAG,IAAI,CAAA;IACd,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAmC3F"}
package/dist/objectUrl.js CHANGED
@@ -40,6 +40,14 @@
40
40
  * label that may itself contain hyphens, digits and hex.
41
41
  */
42
42
  const TRAILING_UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
43
+ /**
44
+ * A 64-character event id at the very end of a ref.
45
+ *
46
+ * The second identity shape, for an object that has no `d` — a `kind:9`
47
+ * message is the case in hand, and §7.6 recorded it as outside the grammar
48
+ * until this. Anchored at the end for the same positional reason as the uuid.
49
+ */
50
+ const TRAILING_EVENT_ID_RE = /(?:^|[^0-9a-f])([0-9a-f]{64})$/i;
43
51
  /**
44
52
  * Slugs are cut here. Long enough to stay recognisable in a chat client's link
45
53
  * preview, short enough that the uuid is not pushed off the end of a rendered
@@ -77,6 +85,25 @@ export function identifierFromRef(ref) {
77
85
  const match = TRAILING_UUID_RE.exec(ref);
78
86
  return match ? match[0].toLowerCase() : null;
79
87
  }
88
+ /**
89
+ * The event id out of a ref, or null.
90
+ *
91
+ * **Why an event id in a URL rather than an `nevent`.** §7.3 rejects bech32 in
92
+ * a URL on two grounds, and both apply to `nevent` exactly as they do to
93
+ * `naddr`: a slice of it is checksum bytes rather than the identity, and relay
94
+ * hints are part of the encoding, so one object has more than one spelling.
95
+ *
96
+ * A raw event id has neither problem. It is the whole identity of an event that
97
+ * has no `d`, it is stable because nothing optional is encoded into it, and it
98
+ * resolves with `{ids: ["<id>"]}` — one query, no index, exactly the property
99
+ * §7.2 chose the bare uuid for. So the rule generalises rather than gaining an
100
+ * exception: **the identity goes in the path, unencoded, and everything before
101
+ * it is decoration.**
102
+ */
103
+ export function eventIdFromRef(ref) {
104
+ const match = TRAILING_EVENT_ID_RE.exec(ref);
105
+ return match ? match[1].toLowerCase() : null;
106
+ }
80
107
  /** The `urls` shapes a manifest event declares. */
81
108
  export function urlPatternsOf(event) {
82
109
  const out = [];
@@ -109,6 +136,9 @@ export function matchObjectUrl(url, patterns) {
109
136
  continue;
110
137
  if (shape.scheme !== target.scheme || shape.host !== target.host)
111
138
  continue;
139
+ // A fragment route and a path route are different shapes, not one.
140
+ if (shape.fragmented !== target.fragmented)
141
+ continue;
112
142
  // The pattern's path up to its final segment: `/issue/` out of
113
143
  // `/issue/<slug>-<d>`. Compared literally and at equal depth, so `/issues`
114
144
  // never matches `/issue/…` and a nested route never matches a shallower one.
@@ -119,10 +149,20 @@ export function matchObjectUrl(url, patterns) {
119
149
  .every((segment, i) => segment === target.segments[i]);
120
150
  if (!prefixMatches)
121
151
  continue;
122
- const identifier = identifierFromRef(target.segments[target.segments.length - 1] ?? '');
152
+ const tail = target.segments[target.segments.length - 1] ?? '';
153
+ /*
154
+ The pattern says which identity it carries. `<id>` is an event id, for a
155
+ kind with no `d`; anything else is the `<d>` uuid this grammar started
156
+ with. Read from the declaration rather than sniffed from the value: a
157
+ 64-hex string and a uuid are distinguishable today, and a consumer that
158
+ relied on that would be inferring an app's addressing model from a
159
+ character class.
160
+ */
161
+ const wantsEventId = shape.segments[shape.segments.length - 1]?.includes('<id>') ?? false;
162
+ const identifier = wantsEventId ? eventIdFromRef(tail) : identifierFromRef(tail);
123
163
  if (!identifier)
124
164
  continue;
125
- return { identifier, ...(kind === undefined ? {} : { kind }) };
165
+ return { identifier, by: wantsEventId ? 'id' : 'd', ...(kind === undefined ? {} : { kind }) };
126
166
  }
127
167
  return null;
128
168
  }
@@ -145,14 +185,34 @@ export function matchObjectUrl(url, patterns) {
145
185
  * declare.
146
186
  */
147
187
  function splitUrl(raw) {
148
- const match = /^(https?):\/\/([^/?#]+)([^?#]*)/i.exec(raw.trim());
188
+ const match = /^(https?):\/\/([^/?#]+)([^?#]*)(?:\?[^#]*)?(?:#(.*))?$/i.exec(raw.trim());
149
189
  if (!match)
150
190
  return null;
151
- const [, scheme, host, path] = match;
191
+ const [, scheme, host, path, fragment] = match;
192
+ /*
193
+ A fragment route counts as path.
194
+
195
+ Ship served `#/issue/<d>` until SHI-16 and still declares those shapes, so
196
+ that links already sitting in other people's messages resolve rather than
197
+ rendering as plain text for ever. A parser that stopped at the `#` — this
198
+ one did — matched none of them, which a production probe caught before any
199
+ consumer was built on it.
200
+
201
+ Segments from the path and the fragment are concatenated rather than
202
+ swapped, because an app may be served under a sub-path *and* use a
203
+ fragment: Ship's own dev URL is `localhost:5190/ship/#/…`, where both
204
+ halves carry meaning.
205
+
206
+ `fragmented` is kept so the two do not collapse into each other. Without it
207
+ a pattern for `/issue/<slug>-<d>` would also claim `/#/issue/<d>`, and an
208
+ app that means different things by the two would resolve the wrong object.
209
+ */
210
+ const fragmentPath = fragment && fragment.startsWith('/') ? fragment : '';
152
211
  return {
153
212
  scheme: scheme.toLowerCase(),
154
213
  host: host.toLowerCase(),
155
- segments: (path ?? '').split('/').filter(Boolean),
214
+ segments: [...(path ?? '').split('/'), ...fragmentPath.split('/')].filter(Boolean),
215
+ fragmented: fragmentPath !== '',
156
216
  };
157
217
  }
158
218
  //# sourceMappingURL=objectUrl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"objectUrl.js","sourceRoot":"","sources":["../src/objectUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,gEAAgE,CAAA;AAEzF;;;;GAIG;AACH,MAAM,eAAe,GAAG,EAAE,CAAA;AAE1B;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC;SACzB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;AACxB,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,SAAS,CAAC,UAAkB,EAAE,KAAc;IAC1D,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACxC,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAA;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACxC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AAC9C,CAAC;AA2BD,mDAAmD;AACnD,MAAM,UAAU,aAAa,CAAC,KAA2B;IACvD,MAAM,GAAG,GAAiB,EAAE,CAAA;IAC5B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAQ;QAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAChD,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IAC5E,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AASD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,QAAsB;IAChE,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK;YAAE,SAAQ;QACpB,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI;YAAE,SAAQ;QAE1E,+DAA+D;QAC/D,2EAA2E;QAC3E,6EAA6E;QAC7E,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM;YAAE,SAAQ;QAC9D,MAAM,aAAa,GAAG,KAAK,CAAC,QAAQ;aACjC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;aACZ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;QACxD,IAAI,CAAC,aAAa;YAAE,SAAQ;QAE5B,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;QACvF,IAAI,CAAC,UAAU;YAAE,SAAQ;QACzB,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IAChE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,QAAQ,CAAC,GAAW;IAC3B,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,KAAK,CAAA;IACpC,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE;QAC5B,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;QACxB,QAAQ,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;KAClD,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"objectUrl.js","sourceRoot":"","sources":["../src/objectUrl.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/interop",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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",
package/src/index.ts CHANGED
@@ -75,6 +75,7 @@ export {
75
75
  slugify,
76
76
  objectRef,
77
77
  identifierFromRef,
78
+ eventIdFromRef,
78
79
  urlPatternsOf,
79
80
  matchObjectUrl,
80
81
  type UrlPattern,
package/src/objectUrl.ts CHANGED
@@ -42,6 +42,15 @@
42
42
  */
43
43
  const TRAILING_UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
44
44
 
45
+ /**
46
+ * A 64-character event id at the very end of a ref.
47
+ *
48
+ * The second identity shape, for an object that has no `d` — a `kind:9`
49
+ * message is the case in hand, and §7.6 recorded it as outside the grammar
50
+ * until this. Anchored at the end for the same positional reason as the uuid.
51
+ */
52
+ const TRAILING_EVENT_ID_RE = /(?:^|[^0-9a-f])([0-9a-f]{64})$/i
53
+
45
54
  /**
46
55
  * Slugs are cut here. Long enough to stay recognisable in a chat client's link
47
56
  * preview, short enough that the uuid is not pushed off the end of a rendered
@@ -83,6 +92,26 @@ export function identifierFromRef(ref: string): string | null {
83
92
  return match ? match[0].toLowerCase() : null
84
93
  }
85
94
 
95
+ /**
96
+ * The event id out of a ref, or null.
97
+ *
98
+ * **Why an event id in a URL rather than an `nevent`.** §7.3 rejects bech32 in
99
+ * a URL on two grounds, and both apply to `nevent` exactly as they do to
100
+ * `naddr`: a slice of it is checksum bytes rather than the identity, and relay
101
+ * hints are part of the encoding, so one object has more than one spelling.
102
+ *
103
+ * A raw event id has neither problem. It is the whole identity of an event that
104
+ * has no `d`, it is stable because nothing optional is encoded into it, and it
105
+ * resolves with `{ids: ["<id>"]}` — one query, no index, exactly the property
106
+ * §7.2 chose the bare uuid for. So the rule generalises rather than gaining an
107
+ * exception: **the identity goes in the path, unencoded, and everything before
108
+ * it is decoration.**
109
+ */
110
+ export function eventIdFromRef(ref: string): string | null {
111
+ const match = TRAILING_EVENT_ID_RE.exec(ref)
112
+ return match ? match[1].toLowerCase() : null
113
+ }
114
+
86
115
  /**
87
116
  * A URL shape an app says it serves — RFC 0.5 §7.5's `urls`, read off the
88
117
  * manifest event the way `web` is.
@@ -121,7 +150,17 @@ export function urlPatternsOf(event: { tags: string[][] }): UrlPattern[] {
121
150
 
122
151
  /** What a pasted URL turned out to name. */
123
152
  export interface MatchedObjectUrl {
153
+ /** A `d` for an addressable object, or an event id for one without. */
124
154
  identifier: string
155
+ /**
156
+ * How to resolve `identifier`: by `#d` for an addressable object, by `ids`
157
+ * for an event that has none.
158
+ *
159
+ * Declared by the pattern's placeholder — `<d>` or `<id>` — rather than
160
+ * guessed from the string, because a consumer that inspected the value would
161
+ * be deciding a kind's addressing model from the shape of a hex string.
162
+ */
163
+ by: 'd' | 'id'
125
164
  /** Present only when the matched pattern named one. */
126
165
  kind?: number
127
166
  }
@@ -145,6 +184,8 @@ export function matchObjectUrl(url: string, patterns: UrlPattern[]): MatchedObje
145
184
  const shape = splitUrl(pattern)
146
185
  if (!shape) continue
147
186
  if (shape.scheme !== target.scheme || shape.host !== target.host) continue
187
+ // A fragment route and a path route are different shapes, not one.
188
+ if (shape.fragmented !== target.fragmented) continue
148
189
 
149
190
  // The pattern's path up to its final segment: `/issue/` out of
150
191
  // `/issue/<slug>-<d>`. Compared literally and at equal depth, so `/issues`
@@ -155,9 +196,19 @@ export function matchObjectUrl(url: string, patterns: UrlPattern[]): MatchedObje
155
196
  .every((segment, i) => segment === target.segments[i])
156
197
  if (!prefixMatches) continue
157
198
 
158
- const identifier = identifierFromRef(target.segments[target.segments.length - 1] ?? '')
199
+ const tail = target.segments[target.segments.length - 1] ?? ''
200
+ /*
201
+ The pattern says which identity it carries. `<id>` is an event id, for a
202
+ kind with no `d`; anything else is the `<d>` uuid this grammar started
203
+ with. Read from the declaration rather than sniffed from the value: a
204
+ 64-hex string and a uuid are distinguishable today, and a consumer that
205
+ relied on that would be inferring an app's addressing model from a
206
+ character class.
207
+ */
208
+ const wantsEventId = shape.segments[shape.segments.length - 1]?.includes('<id>') ?? false
209
+ const identifier = wantsEventId ? eventIdFromRef(tail) : identifierFromRef(tail)
159
210
  if (!identifier) continue
160
- return { identifier, ...(kind === undefined ? {} : { kind }) }
211
+ return { identifier, by: wantsEventId ? 'id' : 'd', ...(kind === undefined ? {} : { kind }) }
161
212
  }
162
213
  return null
163
214
  }
@@ -180,13 +231,34 @@ export function matchObjectUrl(url: string, patterns: UrlPattern[]): MatchedObje
180
231
  * refusal local to the parser rather than a consequence of what apps happen to
181
232
  * declare.
182
233
  */
183
- function splitUrl(raw: string): { scheme: string; host: string; segments: string[] } | null {
184
- const match = /^(https?):\/\/([^/?#]+)([^?#]*)/i.exec(raw.trim())
234
+ function splitUrl(raw: string): { scheme: string; host: string; segments: string[]; fragmented: boolean } | null {
235
+ const match = /^(https?):\/\/([^/?#]+)([^?#]*)(?:\?[^#]*)?(?:#(.*))?$/i.exec(raw.trim())
185
236
  if (!match) return null
186
- const [, scheme, host, path] = match
237
+ const [, scheme, host, path, fragment] = match
238
+
239
+ /*
240
+ A fragment route counts as path.
241
+
242
+ Ship served `#/issue/<d>` until SHI-16 and still declares those shapes, so
243
+ that links already sitting in other people's messages resolve rather than
244
+ rendering as plain text for ever. A parser that stopped at the `#` — this
245
+ one did — matched none of them, which a production probe caught before any
246
+ consumer was built on it.
247
+
248
+ Segments from the path and the fragment are concatenated rather than
249
+ swapped, because an app may be served under a sub-path *and* use a
250
+ fragment: Ship's own dev URL is `localhost:5190/ship/#/…`, where both
251
+ halves carry meaning.
252
+
253
+ `fragmented` is kept so the two do not collapse into each other. Without it
254
+ a pattern for `/issue/<slug>-<d>` would also claim `/#/issue/<d>`, and an
255
+ app that means different things by the two would resolve the wrong object.
256
+ */
257
+ const fragmentPath = fragment && fragment.startsWith('/') ? fragment : ''
187
258
  return {
188
259
  scheme: scheme.toLowerCase(),
189
260
  host: host.toLowerCase(),
190
- segments: (path ?? '').split('/').filter(Boolean),
261
+ segments: [...(path ?? '').split('/'), ...fragmentPath.split('/')].filter(Boolean),
262
+ fragmented: fragmentPath !== '',
191
263
  }
192
264
  }