@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 +51 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/objectUrl.d.ts +26 -0
- package/dist/objectUrl.d.ts.map +1 -1
- package/dist/objectUrl.js +65 -5
- package/dist/objectUrl.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/objectUrl.ts +78 -6
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/objectUrl.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/objectUrl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objectUrl.d.ts","sourceRoot":"","sources":["../src/objectUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;
|
|
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
|
|
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?):\/\/([^/?#]+)([^?#]*)
|
|
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
|
package/dist/objectUrl.js.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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
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
|
|
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?):\/\/([^/?#]+)([^?#]*)
|
|
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
|
}
|