@singi-labs/sifa-sdk 0.19.102 → 0.19.103
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/dist/cards/resolve-card-url.js +43 -4
- package/dist/cards/resolve-card-url.js.map +1 -1
- package/dist/index.cjs +55 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/stream/index.js +2 -0
- package/dist/stream/index.js.map +1 -1
- package/dist/stream/timeline-worthy.js +20 -0
- package/dist/stream/timeline-worthy.js.map +1 -0
- package/dist/stream/to-stream-card-vm.js +11 -2
- package/dist/stream/to-stream-card-vm.js.map +1 -1
- package/package.json +1 -1
|
@@ -54,16 +54,55 @@ function parseAtUri(atUri) {
|
|
|
54
54
|
if (!match || !match[1] || !match[2]) return null;
|
|
55
55
|
return { did: match[1], rkey: match[2] };
|
|
56
56
|
}
|
|
57
|
+
var TANGLED = "https://tangled.sh";
|
|
58
|
+
function tangledRepoBase(ref) {
|
|
59
|
+
const value = stringOrNull(ref);
|
|
60
|
+
if (!value) return null;
|
|
61
|
+
if (value.startsWith("did:")) return `${TANGLED}/${value}`;
|
|
62
|
+
const parsed = parseAtUri(value);
|
|
63
|
+
if (parsed && TANGLED_REPO_SLUG.test(parsed.rkey))
|
|
64
|
+
return `${TANGLED}/${parsed.did}/${parsed.rkey}`;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function resolveTangledUrl(collection, record, rkey, owner) {
|
|
68
|
+
if (collection === "sh.tangled.repo.pull" || collection === "sh.tangled.repo.issue") {
|
|
69
|
+
const target = record.target && typeof record.target === "object" ? record.target : {};
|
|
70
|
+
const base = tangledRepoBase(target.repoDid) ?? tangledRepoBase(target.repo) ?? tangledRepoBase(record.repoDid) ?? tangledRepoBase(record.repo);
|
|
71
|
+
if (!base) return null;
|
|
72
|
+
const list = collection === "sh.tangled.repo.pull" ? "pulls" : "issues";
|
|
73
|
+
return { url: `${base}/${list}`, ownRecord: false };
|
|
74
|
+
}
|
|
75
|
+
if (collection === "sh.tangled.feed.star") {
|
|
76
|
+
const starred = tangledRepoBase(record.subjectDid) ?? tangledRepoBase(record.subject);
|
|
77
|
+
return starred ? { url: starred, ownRecord: false } : null;
|
|
78
|
+
}
|
|
79
|
+
const repoDid = tangledRepoBase(record.repoDid);
|
|
80
|
+
if (repoDid) return { url: repoDid, ownRecord: true };
|
|
81
|
+
const repoName = stringOrNull(record.name) ?? (collection === "sh.tangled.repo" ? rkey : null);
|
|
82
|
+
if (repoName && owner && TANGLED_REPO_SLUG.test(repoName)) {
|
|
83
|
+
return { url: `${TANGLED}/${owner}/${repoName}`, ownRecord: true };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
57
87
|
function resolveCardHealth(item) {
|
|
58
88
|
const { collection, record, uri, rkey, authorDid, authorHandle } = item;
|
|
59
89
|
const appId = getAppIdForCollection(collection);
|
|
60
90
|
if (collection.startsWith("sh.tangled.")) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
}
|
|
91
|
+
const tangled = resolveTangledUrl(collection, record, rkey, authorHandle ?? authorDid);
|
|
92
|
+
if (tangled) return tangled.ownRecord ? recordHealth(tangled.url) : urlHealth(tangled.url);
|
|
93
|
+
if (!authorHandle && authorDid) return urlHealth(`${TANGLED}/${authorDid}`);
|
|
65
94
|
return patternHealth("tangled", { handle: authorHandle, did: authorDid, rkey }, collection);
|
|
66
95
|
}
|
|
96
|
+
if (collection === "app.userinput.discussion" && authorDid) {
|
|
97
|
+
return recordHealth(`https://userinput.app/d/${authorDid}/${rkey}`);
|
|
98
|
+
}
|
|
99
|
+
if (collection === "app.userinput.reply") {
|
|
100
|
+
const subject = record.subject;
|
|
101
|
+
const subjectUri = subject != null && typeof subject === "object" ? stringOrNull(subject.uri) : null;
|
|
102
|
+
const parsed = subjectUri?.includes("/app.userinput.discussion/") ? parseAtUri(subjectUri) : null;
|
|
103
|
+
if (parsed) return urlHealth(`https://userinput.app/d/${parsed.did}/${parsed.rkey}`);
|
|
104
|
+
return { url: null, strategy: "none" };
|
|
105
|
+
}
|
|
67
106
|
if (collection.startsWith("com.kipclip.") || collection.startsWith("community.lexicon.bookmarks.")) {
|
|
68
107
|
const subject = stringOrNull(record.subject);
|
|
69
108
|
if (subject) return urlHealth(subject);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cards/resolve-card-url.ts"],"sourcesContent":["import { APP_URL_PATTERNS, COLLECTION_TO_APP } from './app-url-patterns.js';\n\n/**\n * An activity item as consumed by the Sifa activity-card components and\n * the sifa-api external-URL health scanner. The shape mirrors the props\n * the cards already use, so the resolver returns the same URL the UI\n * renders.\n */\nexport interface ActivityItemForUrl {\n /** Collection NSID, e.g. \"sh.tangled.graph.repo\". */\n collection: string;\n /** The raw record from the PDS / API. */\n record: Record<string, unknown>;\n /** Full at-uri of the record (used by collections that derive URL from uri). */\n uri: string;\n /** Record key. */\n rkey: string;\n /** DID of the author of the record. */\n authorDid: string;\n /** Optional handle (cards prefer handle in URL paths when available). */\n authorHandle?: string;\n}\n\n/**\n * Map a collection NSID to its app id, mirroring the sifa-web prefix map.\n *\n * Falls back to the first two NSID segments (e.g. \"org.hyperboards\") when\n * the prefix is not registered, matching the existing card behaviour.\n */\nexport function getAppIdForCollection(collection: string): string {\n for (const [prefix, appId] of COLLECTION_TO_APP) {\n if (collection.startsWith(prefix)) return appId;\n }\n const parts = collection.split('.');\n if (parts.length >= 2) return `${parts[0]}.${parts[1]}`;\n return collection;\n}\n\n/**\n * How the activity-card health scanner should verify a card is still live.\n *\n * - `record`: the URL is a first-party permalink that renders THIS record\n * (e.g. a Bluesky post at bsky.app, a tangled repo, a smokesignal event).\n * The authoritative liveness check is \"does the record still exist on its\n * PDS\" — `com.atproto.repo.getRecord` on the item's own at-uri — NOT an\n * HTTP probe of the rendering app. Rendering apps (bsky.app, tangled.sh)\n * frequently answer HEAD with 404/405 while serving GET fine, which\n * false-positives every permalink as broken.\n * - `url`: the URL is a foreign or derived target — a bookmarked page, an\n * external publisher, a *different* record's page, or a profile page.\n * Its reachability is independent of this record, so probe the URL.\n * - `none`: the card has no clickable URL; nothing to check.\n */\nexport type CardHealthStrategy = 'record' | 'url' | 'none';\n\nexport interface CardHealth {\n /** The clickable URL the card renders. Null when non-clickable. */\n url: string | null;\n /** How the scanner should verify liveness. See {@link CardHealthStrategy}. */\n strategy: CardHealthStrategy;\n}\n\n/** Build a `url`-strategy result (foreign/derived/profile target). */\nfunction urlHealth(url: string | null): CardHealth {\n return url ? { url, strategy: 'url' } : { url: null, strategy: 'none' };\n}\n\n/** Build a `record`-strategy result (self-permalink of this record). */\nfunction recordHealth(url: string | null): CardHealth {\n return url ? { url, strategy: 'record' } : { url: null, strategy: 'none' };\n}\n\nfunction interpolate(pattern: string, vars: Record<string, string | undefined>): string | null {\n let result = pattern;\n for (const [key, value] of Object.entries(vars)) {\n if (result.includes(`{${key}}`)) {\n if (!value) return null;\n result = result.replaceAll(`{${key}}`, encodeURIComponent(value));\n }\n }\n return result;\n}\n\n/**\n * Whether the per-item `urlPattern` for `appId` is appropriate for `collection`.\n *\n * For bluesky, the per-item URL pattern only matches `app.bsky.feed.post` —\n * other `app.bsky.*` collections (`actor.status`, `graph.cancellation`, etc.)\n * use rkeys that would 404 at `bsky.app/profile/{handle}/post/{rkey}`. Fall\n * back to the profile URL for those. See sifa-web#1070 / sifa-web#1073.\n *\n * When `collection` is undefined we err on the safe side for bluesky and\n * prefer the profile URL.\n */\nfunction shouldUseItemPattern(appId: string, collection: string | undefined): boolean {\n if (appId === 'bluesky') {\n return collection === 'app.bsky.feed.post';\n }\n return true;\n}\n\n/**\n * Resolve a pattern-based URL, reporting which tier produced it:\n * - `item`: a per-item permalink (renders this specific record) -> record-checkable.\n * - `profile`: the app's profile page (not this record) -> url-checkable.\n * - `null`: no pattern applied.\n */\nfunction patternHealth(\n appId: string,\n vars: { handle?: string; did?: string; rkey?: string },\n collection?: string,\n): CardHealth {\n const patterns = APP_URL_PATTERNS[appId];\n if (!patterns) return { url: null, strategy: 'none' };\n\n if (patterns.urlPattern && shouldUseItemPattern(appId, collection)) {\n const url = interpolate(patterns.urlPattern, vars);\n if (url) return recordHealth(url);\n }\n if (patterns.profileUrlPattern) {\n const url = interpolate(patterns.profileUrlPattern, vars);\n if (url) return urlHealth(url);\n }\n return { url: null, strategy: 'none' };\n}\n\n/**\n * Valid tangled repo slug: alphanumeric, dot, dash, underscore. No whitespace,\n * slashes, or URL-encodable special characters. Multi-segment aggregate `name`\n * values produce 404 URLs, so callers fall back to the profile URL when\n * invalid. See sifa-web#1071 / sifa-web#1072.\n */\nconst TANGLED_REPO_SLUG = /^[a-zA-Z0-9._-]+$/;\n\nfunction stringOrNull(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n// at://did:plc:xxx/community.lexicon.calendar.event/rkey\n// -> { did: \"did:plc:xxx\", rkey: \"rkey\" }\nfunction parseAtUri(atUri: string): { did: string; rkey: string } | null {\n const match = atUri.match(/^at:\\/\\/(did:[^/]+)\\/[^/]+\\/(.+)$/);\n if (!match || !match[1] || !match[2]) return null;\n return { did: match[1], rkey: match[2] };\n}\n\n/**\n * Resolve the canonical clickable URL for an activity item **and** how its\n * liveness should be checked — the single source of truth shared by:\n * - sifa-web activity cards (which render `.url`), and\n * - the sifa-api link-health scanner (which switches on `.strategy`: probe the\n * record's PDS for `record`-strategy cards, probe the URL for `url`).\n *\n * See {@link CardHealthStrategy} for why self-permalinks (bsky posts, etc.)\n * must be verified by record existence rather than an HTTP probe of the app.\n *\n * Resolution order:\n * 1. Per-collection bespoke logic (tangled, kipclip, margin, smokesignal\n * rsvp, standard documents). These mirror the inline logic in the\n * individual card components.\n * 2. Generic `record.url` field (used by hyperboards and similar).\n * 3. Pattern-based per-item / profile URL from the registry.\n */\nexport function resolveCardHealth(item: ActivityItemForUrl): CardHealth {\n const { collection, record, uri, rkey, authorDid, authorHandle } = item;\n const appId = getAppIdForCollection(collection);\n\n // --- Per-collection bespoke logic (mirrors the card components) ---\n\n // Tangled: prefer per-repo URL `https://tangled.sh/{handle}/{slug}`. The slug\n // is `record.name` (legacy format) or — when `name` is absent — the record's\n // rkey (newer repos use the slug as the rkey). The rkey fallback is scoped to\n // `sh.tangled.repo`; other tangled collections (e.g. feed.star) use TID rkeys\n // that are not repo slugs. Only when the slug is valid — multi-segment\n // aggregate names (whitespace, slashes, special chars) would 404. See\n // sifa-web#1071. The repo record itself is the card's subject, so it is\n // verified by the record's existence.\n if (collection.startsWith('sh.tangled.')) {\n const repoName = stringOrNull(record.name) ?? (collection === 'sh.tangled.repo' ? rkey : null);\n if (repoName && authorHandle && TANGLED_REPO_SLUG.test(repoName)) {\n return recordHealth(`https://tangled.sh/${authorHandle}/${repoName}`);\n }\n return patternHealth('tangled', { handle: authorHandle, did: authorDid, rkey }, collection);\n }\n\n // Kipclip / community.lexicon.bookmarks: the bookmark subject IS the URL —\n // a foreign page whose reachability is independent of the bookmark record.\n if (\n collection.startsWith('com.kipclip.') ||\n collection.startsWith('community.lexicon.bookmarks.')\n ) {\n const subject = stringOrNull(record.subject);\n if (subject) return urlHealth(subject);\n return patternHealth('kipclip', { handle: authorHandle, did: authorDid, rkey });\n }\n\n // Margin bookmark: card returns null entirely without record.source\n if (collection === 'at.margin.bookmark') {\n return urlHealth(stringOrNull(record.source));\n }\n\n // Margin note / annotation: both are W3C web annotations whose `target.source`\n // is the annotated page URL. Link to that source; fall back to the margin app.\n if (collection === 'at.margin.annotation' || collection === 'at.margin.note') {\n const target = record.target;\n if (target != null && typeof target === 'object') {\n const source = stringOrNull((target as Record<string, unknown>).source);\n if (source) return urlHealth(source);\n }\n return urlHealth(APP_URL_PATTERNS.margin?.profileUrlPattern ?? null);\n }\n\n // Community-calendar RSVP: parse the linked event uri. The URL points at a\n // *different* record (the event), so probe the URL, not this RSVP record.\n // Rendered via atmo.rsvp, which resolves any community.lexicon.calendar event\n // by did+rkey (Smoke Signal is decommissioned; its permalinks are retired).\n if (collection === 'community.lexicon.calendar.rsvp') {\n const subject = record.subject;\n if (subject != null && typeof subject === 'object') {\n const subjectUri = stringOrNull((subject as Record<string, unknown>).uri);\n if (subjectUri) {\n const parsed = parseAtUri(subjectUri);\n if (parsed) {\n return urlHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n }\n }\n return { url: null, strategy: 'none' };\n }\n\n // Community-calendar event itself: use item's own uri -> self-permalink on\n // atmo.rsvp (same rationale as the RSVP block above).\n if (collection === 'community.lexicon.calendar.event') {\n const parsed = parseAtUri(uri);\n if (parsed) {\n return recordHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmo.rsvp event itself: use item's own uri -> self-permalink.\n if (collection === 'quest.atmo.event') {\n const parsed = parseAtUri(uri);\n if (parsed) {\n return recordHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmo.rsvp checkin: parse the linked event uri (record.event) and link to\n // that event's page (mirrors smokesignal rsvp). The URL points at a\n // *different* record (the event), so probe the URL. The checkin's own rkey is\n // not addressable, so without a valid event reference it's non-clickable.\n if (collection === 'quest.atmo.checkin') {\n const eventUri = stringOrNull(record.event);\n if (eventUri) {\n const parsed = parseAtUri(eventUri);\n if (parsed) {\n return urlHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmoBB forum (atmobb.app): the universal viewer renders any thread at\n // /t/{thread-owner-did}/{thread-rkey}. A discussion reply has no own URL — it\n // lives inside its parent thread, so parse record.thread.uri and link there.\n // That URL points at a *different* record (the thread), so probe the URL.\n if (collection === 'app.atmobb.discussion.reply') {\n const thread = record.thread;\n if (thread != null && typeof thread === 'object') {\n const threadUri = stringOrNull((thread as Record<string, unknown>).uri);\n if (threadUri) {\n const parsed = parseAtUri(threadUri);\n if (parsed) {\n return urlHealth(`https://atmobb.app/t/${parsed.did}/${parsed.rkey}`);\n }\n }\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmoBB thread itself: build the per-thread URL from the item's own uri — a\n // self-permalink for this record (record-checkable). Other app.atmobb.*\n // collections (actor.profile, forum.*) are excluded upstream by sifa-api and\n // never reach the resolver.\n if (collection === 'app.atmobb.discussion.thread') {\n const parsed = parseAtUri(uri);\n if (parsed) {\n return recordHealth(`https://atmobb.app/t/${parsed.did}/${parsed.rkey}`);\n }\n return { url: null, strategy: 'none' };\n }\n\n // Kut membership: a `social.kut.community.membership` record has no page of\n // its own. Link to the community it joined, parsed from record.community.uri.\n // Kut renders communities at /community/{slug}, where the slug is the\n // community-profile record's rkey (verified live). The community is a\n // *different* record, so probe the URL.\n if (collection === 'social.kut.community.membership') {\n const community = record.community;\n const communityUri =\n typeof community === 'string'\n ? community\n : // Narrow the object branch to read an optional `uri`. The membership\n // lexicon carries `community` as a bare at-uri string today; the cast\n // tolerates a future strongRef `{ uri, cid }` shape without assuming it.\n community != null && typeof community === 'object'\n ? stringOrNull((community as Record<string, unknown>).uri)\n : null;\n if (communityUri) {\n const parsed = parseAtUri(communityUri);\n if (parsed) {\n return urlHealth(`https://kut.social/community/${encodeURIComponent(parsed.rkey)}`);\n }\n }\n return urlHealth(APP_URL_PATTERNS.kut?.profileUrlPattern ?? null);\n }\n\n // atstore reviews: no per-review URL exists on atstore.fyi, and user profile\n // pages don't exist either. Deep-link to the reviewed product instead.\n // The slug comes from record.listingMeta (enriched by sifa-api from the\n // review's `subject` at-uri → fyi.atstore.listing.detail record). The product\n // page is a *different* subject, so probe the URL.\n if (collection === 'fyi.atstore.listing.review') {\n const listingMeta = record.listingMeta;\n if (listingMeta != null && typeof listingMeta === 'object') {\n const slug = stringOrNull((listingMeta as Record<string, unknown>).slug);\n if (slug) return urlHealth(`https://atstore.fyi/products/${encodeURIComponent(slug)}`);\n }\n return urlHealth(APP_URL_PATTERNS.atstore?.profileUrlPattern ?? null);\n }\n\n // Crate content: link to the canonical published location the maker\n // recorded (a YouTube/podcast/personal-site URL) — a foreign page. Crate is\n // an authoring dashboard with no public per-record viewer, so `note`\n // records — which carry no canonicalUrl — render non-clickable (null).\n if (collection === 'social.crate.content') {\n return urlHealth(stringOrNull(record.canonicalUrl));\n }\n if (collection === 'social.crate.note') {\n return { url: null, strategy: 'none' };\n }\n\n // Standard documents: siteUrl + path (or just siteUrl) — an external site.\n if (collection.startsWith('site.standard.')) {\n const siteUrl = stringOrNull(record.siteUrl);\n const path = stringOrNull(record.path);\n if (siteUrl && path) return urlHealth(`${siteUrl}${path}`);\n if (siteUrl) return urlHealth(siteUrl);\n // Fall through to generic record.url / patterns\n }\n\n // Kich recipe: always link to the Kich recipe page. record.url is the\n // *source* the recipe was imported from (a YouTube video, a blog), which the\n // card shows as secondary attribution — not the primary link. Must run before\n // the generic record.url fallback below, which would otherwise hijack it. The\n // recipe page is a first-party permalink for this record.\n if (collection === 'io.kich.recipe.recipe') {\n return patternHealth('kich', { handle: authorHandle, did: authorDid, rkey }, collection);\n }\n\n // recipe.exchange: same shape as Kich — the record's attribution.url points\n // at the original source (a blog/video), not the app. Always link to the\n // recipe.exchange page. Must run before the generic fallback below.\n if (collection === 'exchange.recipe.recipe') {\n return patternHealth('recipe', { handle: authorHandle, did: authorDid, rkey }, collection);\n }\n\n // atmob paste (atmob.in): the SPA viewer renders any paste by its full at-uri\n // via a `?uri=` query param (verified in the app's app.js). Both\n // paste.document and paste.bundle share that viewer, so build the URL from the\n // item's own uri — a self-permalink for this record (record-checkable).\n if (collection.startsWith('in.atmob.paste.')) {\n if (uri) return recordHealth(`https://atmob.in/?uri=${encodeURIComponent(uri)}`);\n return { url: null, strategy: 'none' };\n }\n\n // --- Generic fallbacks ---\n\n // record.url is a common ad-hoc per-item URL (hyperboards, etc.). It is an\n // arbitrary foreign URL, so probe it directly.\n const recordUrl = stringOrNull(record.url);\n if (recordUrl) return urlHealth(recordUrl);\n\n // Pattern-based fallback. Pass `collection` so app-specific guards (bluesky\n // restricting /post/{rkey} to app.bsky.feed.post) can apply. The tier\n // (per-item vs profile) decides record- vs url-strategy.\n return patternHealth(appId, { handle: authorHandle, did: authorDid, rkey }, collection);\n}\n\n/**\n * Resolve the canonical clickable URL for an activity item — the same URL\n * the activity-card UI in sifa-web renders. Thin wrapper over\n * {@link resolveCardHealth}; returns `null` when no link is appropriate.\n */\nexport function resolveCardUrl(item: ActivityItemForUrl): string | null {\n return resolveCardHealth(item).url;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/cards/resolve-card-url.ts"],"sourcesContent":["import { APP_URL_PATTERNS, COLLECTION_TO_APP } from './app-url-patterns.js';\n\n/**\n * An activity item as consumed by the Sifa activity-card components and\n * the sifa-api external-URL health scanner. The shape mirrors the props\n * the cards already use, so the resolver returns the same URL the UI\n * renders.\n */\nexport interface ActivityItemForUrl {\n /** Collection NSID, e.g. \"sh.tangled.graph.repo\". */\n collection: string;\n /** The raw record from the PDS / API. */\n record: Record<string, unknown>;\n /** Full at-uri of the record (used by collections that derive URL from uri). */\n uri: string;\n /** Record key. */\n rkey: string;\n /** DID of the author of the record. */\n authorDid: string;\n /** Optional handle (cards prefer handle in URL paths when available). */\n authorHandle?: string;\n}\n\n/**\n * Map a collection NSID to its app id, mirroring the sifa-web prefix map.\n *\n * Falls back to the first two NSID segments (e.g. \"org.hyperboards\") when\n * the prefix is not registered, matching the existing card behaviour.\n */\nexport function getAppIdForCollection(collection: string): string {\n for (const [prefix, appId] of COLLECTION_TO_APP) {\n if (collection.startsWith(prefix)) return appId;\n }\n const parts = collection.split('.');\n if (parts.length >= 2) return `${parts[0]}.${parts[1]}`;\n return collection;\n}\n\n/**\n * How the activity-card health scanner should verify a card is still live.\n *\n * - `record`: the URL is a first-party permalink that renders THIS record\n * (e.g. a Bluesky post at bsky.app, a tangled repo, a smokesignal event).\n * The authoritative liveness check is \"does the record still exist on its\n * PDS\" — `com.atproto.repo.getRecord` on the item's own at-uri — NOT an\n * HTTP probe of the rendering app. Rendering apps (bsky.app, tangled.sh)\n * frequently answer HEAD with 404/405 while serving GET fine, which\n * false-positives every permalink as broken.\n * - `url`: the URL is a foreign or derived target — a bookmarked page, an\n * external publisher, a *different* record's page, or a profile page.\n * Its reachability is independent of this record, so probe the URL.\n * - `none`: the card has no clickable URL; nothing to check.\n */\nexport type CardHealthStrategy = 'record' | 'url' | 'none';\n\nexport interface CardHealth {\n /** The clickable URL the card renders. Null when non-clickable. */\n url: string | null;\n /** How the scanner should verify liveness. See {@link CardHealthStrategy}. */\n strategy: CardHealthStrategy;\n}\n\n/** Build a `url`-strategy result (foreign/derived/profile target). */\nfunction urlHealth(url: string | null): CardHealth {\n return url ? { url, strategy: 'url' } : { url: null, strategy: 'none' };\n}\n\n/** Build a `record`-strategy result (self-permalink of this record). */\nfunction recordHealth(url: string | null): CardHealth {\n return url ? { url, strategy: 'record' } : { url: null, strategy: 'none' };\n}\n\nfunction interpolate(pattern: string, vars: Record<string, string | undefined>): string | null {\n let result = pattern;\n for (const [key, value] of Object.entries(vars)) {\n if (result.includes(`{${key}}`)) {\n if (!value) return null;\n result = result.replaceAll(`{${key}}`, encodeURIComponent(value));\n }\n }\n return result;\n}\n\n/**\n * Whether the per-item `urlPattern` for `appId` is appropriate for `collection`.\n *\n * For bluesky, the per-item URL pattern only matches `app.bsky.feed.post` —\n * other `app.bsky.*` collections (`actor.status`, `graph.cancellation`, etc.)\n * use rkeys that would 404 at `bsky.app/profile/{handle}/post/{rkey}`. Fall\n * back to the profile URL for those. See sifa-web#1070 / sifa-web#1073.\n *\n * When `collection` is undefined we err on the safe side for bluesky and\n * prefer the profile URL.\n */\nfunction shouldUseItemPattern(appId: string, collection: string | undefined): boolean {\n if (appId === 'bluesky') {\n return collection === 'app.bsky.feed.post';\n }\n return true;\n}\n\n/**\n * Resolve a pattern-based URL, reporting which tier produced it:\n * - `item`: a per-item permalink (renders this specific record) -> record-checkable.\n * - `profile`: the app's profile page (not this record) -> url-checkable.\n * - `null`: no pattern applied.\n */\nfunction patternHealth(\n appId: string,\n vars: { handle?: string; did?: string; rkey?: string },\n collection?: string,\n): CardHealth {\n const patterns = APP_URL_PATTERNS[appId];\n if (!patterns) return { url: null, strategy: 'none' };\n\n if (patterns.urlPattern && shouldUseItemPattern(appId, collection)) {\n const url = interpolate(patterns.urlPattern, vars);\n if (url) return recordHealth(url);\n }\n if (patterns.profileUrlPattern) {\n const url = interpolate(patterns.profileUrlPattern, vars);\n if (url) return urlHealth(url);\n }\n return { url: null, strategy: 'none' };\n}\n\n/**\n * Valid tangled repo slug: alphanumeric, dot, dash, underscore. No whitespace,\n * slashes, or URL-encodable special characters. Multi-segment aggregate `name`\n * values produce 404 URLs, so callers fall back to the profile URL when\n * invalid. See sifa-web#1071 / sifa-web#1072.\n */\nconst TANGLED_REPO_SLUG = /^[a-zA-Z0-9._-]+$/;\n\nfunction stringOrNull(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n// at://did:plc:xxx/community.lexicon.calendar.event/rkey\n// -> { did: \"did:plc:xxx\", rkey: \"rkey\" }\nfunction parseAtUri(atUri: string): { did: string; rkey: string } | null {\n const match = atUri.match(/^at:\\/\\/(did:[^/]+)\\/[^/]+\\/(.+)$/);\n if (!match || !match[1] || !match[2]) return null;\n return { did: match[1], rkey: match[2] };\n}\n\nconst TANGLED = 'https://tangled.sh';\n\n/** A Tangled repo reference (a repo DID, or an `at://` repo record uri) as its tangled.sh base URL. */\nfunction tangledRepoBase(ref: unknown): string | null {\n const value = stringOrNull(ref);\n if (!value) return null;\n if (value.startsWith('did:')) return `${TANGLED}/${value}`;\n const parsed = parseAtUri(value);\n if (parsed && TANGLED_REPO_SLUG.test(parsed.rkey))\n return `${TANGLED}/${parsed.did}/${parsed.rkey}`;\n return null;\n}\n\n/**\n * The per-item tangled.sh URL for a Tangled record, or null when none can be\n * built. `ownRecord` is true when the URL is the record's own page (a repo).\n */\nfunction resolveTangledUrl(\n collection: string,\n record: Record<string, unknown>,\n rkey: string,\n owner: string,\n): { url: string; ownRecord: boolean } | null {\n if (collection === 'sh.tangled.repo.pull' || collection === 'sh.tangled.repo.issue') {\n const target =\n record.target && typeof record.target === 'object'\n ? (record.target as Record<string, unknown>)\n : {};\n const base =\n tangledRepoBase(target.repoDid) ??\n tangledRepoBase(target.repo) ??\n tangledRepoBase(record.repoDid) ??\n tangledRepoBase(record.repo);\n if (!base) return null;\n const list = collection === 'sh.tangled.repo.pull' ? 'pulls' : 'issues';\n return { url: `${base}/${list}`, ownRecord: false };\n }\n if (collection === 'sh.tangled.feed.star') {\n const starred = tangledRepoBase(record.subjectDid) ?? tangledRepoBase(record.subject);\n return starred ? { url: starred, ownRecord: false } : null;\n }\n const repoDid = tangledRepoBase(record.repoDid);\n if (repoDid) return { url: repoDid, ownRecord: true };\n const repoName = stringOrNull(record.name) ?? (collection === 'sh.tangled.repo' ? rkey : null);\n if (repoName && owner && TANGLED_REPO_SLUG.test(repoName)) {\n return { url: `${TANGLED}/${owner}/${repoName}`, ownRecord: true };\n }\n return null;\n}\n\n/**\n * Resolve the canonical clickable URL for an activity item **and** how its\n * liveness should be checked — the single source of truth shared by:\n * - sifa-web activity cards (which render `.url`), and\n * - the sifa-api link-health scanner (which switches on `.strategy`: probe the\n * record's PDS for `record`-strategy cards, probe the URL for `url`).\n *\n * See {@link CardHealthStrategy} for why self-permalinks (bsky posts, etc.)\n * must be verified by record existence rather than an HTTP probe of the app.\n *\n * Resolution order:\n * 1. Per-collection bespoke logic (tangled, kipclip, margin, smokesignal\n * rsvp, standard documents). These mirror the inline logic in the\n * individual card components.\n * 2. Generic `record.url` field (used by hyperboards and similar).\n * 3. Pattern-based per-item / profile URL from the registry.\n */\nexport function resolveCardHealth(item: ActivityItemForUrl): CardHealth {\n const { collection, record, uri, rkey, authorDid, authorHandle } = item;\n const appId = getAppIdForCollection(collection);\n\n // --- Per-collection bespoke logic (mirrors the card components) ---\n\n // Tangled. tangled.sh resolves both DIDs and handles in the path, so a link\n // never depends on the author handle being known:\n // - a repo: by its repo DID when the record carries one, else owner + slug\n // (`record.name`, or the rkey for `sh.tangled.repo`, whose newer records\n // use the slug as the rkey). Only valid slugs: multi-segment aggregate\n // names would 404 (sifa-web#1071).\n // - a pull / issue: the target repo's pulls / issues list. The record carries\n // no PR or issue number, so the single item is not addressable.\n // - a star: the starred repo.\n // Anything else falls back to the owner's profile.\n if (collection.startsWith('sh.tangled.')) {\n const tangled = resolveTangledUrl(collection, record, rkey, authorHandle ?? authorDid);\n // A repo link is this record's own page; a pull / issue / star links to a\n // different record (the target repo), so probe that page itself.\n if (tangled) return tangled.ownRecord ? recordHealth(tangled.url) : urlHealth(tangled.url);\n if (!authorHandle && authorDid) return urlHealth(`${TANGLED}/${authorDid}`);\n return patternHealth('tangled', { handle: authorHandle, did: authorDid, rkey }, collection);\n }\n\n // User Input: a discussion has its own page; a reply links to the discussion\n // it belongs to (its `subject`).\n if (collection === 'app.userinput.discussion' && authorDid) {\n return recordHealth(`https://userinput.app/d/${authorDid}/${rkey}`);\n }\n if (collection === 'app.userinput.reply') {\n const subject = record.subject;\n const subjectUri =\n subject != null && typeof subject === 'object'\n ? stringOrNull((subject as Record<string, unknown>).uri)\n : null;\n const parsed = subjectUri?.includes('/app.userinput.discussion/')\n ? parseAtUri(subjectUri)\n : null;\n if (parsed) return urlHealth(`https://userinput.app/d/${parsed.did}/${parsed.rkey}`);\n return { url: null, strategy: 'none' };\n }\n\n // Kipclip / community.lexicon.bookmarks: the bookmark subject IS the URL —\n // a foreign page whose reachability is independent of the bookmark record.\n if (\n collection.startsWith('com.kipclip.') ||\n collection.startsWith('community.lexicon.bookmarks.')\n ) {\n const subject = stringOrNull(record.subject);\n if (subject) return urlHealth(subject);\n return patternHealth('kipclip', { handle: authorHandle, did: authorDid, rkey });\n }\n\n // Margin bookmark: card returns null entirely without record.source\n if (collection === 'at.margin.bookmark') {\n return urlHealth(stringOrNull(record.source));\n }\n\n // Margin note / annotation: both are W3C web annotations whose `target.source`\n // is the annotated page URL. Link to that source; fall back to the margin app.\n if (collection === 'at.margin.annotation' || collection === 'at.margin.note') {\n const target = record.target;\n if (target != null && typeof target === 'object') {\n const source = stringOrNull((target as Record<string, unknown>).source);\n if (source) return urlHealth(source);\n }\n return urlHealth(APP_URL_PATTERNS.margin?.profileUrlPattern ?? null);\n }\n\n // Community-calendar RSVP: parse the linked event uri. The URL points at a\n // *different* record (the event), so probe the URL, not this RSVP record.\n // Rendered via atmo.rsvp, which resolves any community.lexicon.calendar event\n // by did+rkey (Smoke Signal is decommissioned; its permalinks are retired).\n if (collection === 'community.lexicon.calendar.rsvp') {\n const subject = record.subject;\n if (subject != null && typeof subject === 'object') {\n const subjectUri = stringOrNull((subject as Record<string, unknown>).uri);\n if (subjectUri) {\n const parsed = parseAtUri(subjectUri);\n if (parsed) {\n return urlHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n }\n }\n return { url: null, strategy: 'none' };\n }\n\n // Community-calendar event itself: use item's own uri -> self-permalink on\n // atmo.rsvp (same rationale as the RSVP block above).\n if (collection === 'community.lexicon.calendar.event') {\n const parsed = parseAtUri(uri);\n if (parsed) {\n return recordHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmo.rsvp event itself: use item's own uri -> self-permalink.\n if (collection === 'quest.atmo.event') {\n const parsed = parseAtUri(uri);\n if (parsed) {\n return recordHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmo.rsvp checkin: parse the linked event uri (record.event) and link to\n // that event's page (mirrors smokesignal rsvp). The URL points at a\n // *different* record (the event), so probe the URL. The checkin's own rkey is\n // not addressable, so without a valid event reference it's non-clickable.\n if (collection === 'quest.atmo.checkin') {\n const eventUri = stringOrNull(record.event);\n if (eventUri) {\n const parsed = parseAtUri(eventUri);\n if (parsed) {\n return urlHealth(`https://atmo.rsvp/p/${parsed.did}/e/${parsed.rkey}`);\n }\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmoBB forum (atmobb.app): the universal viewer renders any thread at\n // /t/{thread-owner-did}/{thread-rkey}. A discussion reply has no own URL — it\n // lives inside its parent thread, so parse record.thread.uri and link there.\n // That URL points at a *different* record (the thread), so probe the URL.\n if (collection === 'app.atmobb.discussion.reply') {\n const thread = record.thread;\n if (thread != null && typeof thread === 'object') {\n const threadUri = stringOrNull((thread as Record<string, unknown>).uri);\n if (threadUri) {\n const parsed = parseAtUri(threadUri);\n if (parsed) {\n return urlHealth(`https://atmobb.app/t/${parsed.did}/${parsed.rkey}`);\n }\n }\n }\n return { url: null, strategy: 'none' };\n }\n\n // atmoBB thread itself: build the per-thread URL from the item's own uri — a\n // self-permalink for this record (record-checkable). Other app.atmobb.*\n // collections (actor.profile, forum.*) are excluded upstream by sifa-api and\n // never reach the resolver.\n if (collection === 'app.atmobb.discussion.thread') {\n const parsed = parseAtUri(uri);\n if (parsed) {\n return recordHealth(`https://atmobb.app/t/${parsed.did}/${parsed.rkey}`);\n }\n return { url: null, strategy: 'none' };\n }\n\n // Kut membership: a `social.kut.community.membership` record has no page of\n // its own. Link to the community it joined, parsed from record.community.uri.\n // Kut renders communities at /community/{slug}, where the slug is the\n // community-profile record's rkey (verified live). The community is a\n // *different* record, so probe the URL.\n if (collection === 'social.kut.community.membership') {\n const community = record.community;\n const communityUri =\n typeof community === 'string'\n ? community\n : // Narrow the object branch to read an optional `uri`. The membership\n // lexicon carries `community` as a bare at-uri string today; the cast\n // tolerates a future strongRef `{ uri, cid }` shape without assuming it.\n community != null && typeof community === 'object'\n ? stringOrNull((community as Record<string, unknown>).uri)\n : null;\n if (communityUri) {\n const parsed = parseAtUri(communityUri);\n if (parsed) {\n return urlHealth(`https://kut.social/community/${encodeURIComponent(parsed.rkey)}`);\n }\n }\n return urlHealth(APP_URL_PATTERNS.kut?.profileUrlPattern ?? null);\n }\n\n // atstore reviews: no per-review URL exists on atstore.fyi, and user profile\n // pages don't exist either. Deep-link to the reviewed product instead.\n // The slug comes from record.listingMeta (enriched by sifa-api from the\n // review's `subject` at-uri → fyi.atstore.listing.detail record). The product\n // page is a *different* subject, so probe the URL.\n if (collection === 'fyi.atstore.listing.review') {\n const listingMeta = record.listingMeta;\n if (listingMeta != null && typeof listingMeta === 'object') {\n const slug = stringOrNull((listingMeta as Record<string, unknown>).slug);\n if (slug) return urlHealth(`https://atstore.fyi/products/${encodeURIComponent(slug)}`);\n }\n return urlHealth(APP_URL_PATTERNS.atstore?.profileUrlPattern ?? null);\n }\n\n // Crate content: link to the canonical published location the maker\n // recorded (a YouTube/podcast/personal-site URL) — a foreign page. Crate is\n // an authoring dashboard with no public per-record viewer, so `note`\n // records — which carry no canonicalUrl — render non-clickable (null).\n if (collection === 'social.crate.content') {\n return urlHealth(stringOrNull(record.canonicalUrl));\n }\n if (collection === 'social.crate.note') {\n return { url: null, strategy: 'none' };\n }\n\n // Standard documents: siteUrl + path (or just siteUrl) — an external site.\n if (collection.startsWith('site.standard.')) {\n const siteUrl = stringOrNull(record.siteUrl);\n const path = stringOrNull(record.path);\n if (siteUrl && path) return urlHealth(`${siteUrl}${path}`);\n if (siteUrl) return urlHealth(siteUrl);\n // Fall through to generic record.url / patterns\n }\n\n // Kich recipe: always link to the Kich recipe page. record.url is the\n // *source* the recipe was imported from (a YouTube video, a blog), which the\n // card shows as secondary attribution — not the primary link. Must run before\n // the generic record.url fallback below, which would otherwise hijack it. The\n // recipe page is a first-party permalink for this record.\n if (collection === 'io.kich.recipe.recipe') {\n return patternHealth('kich', { handle: authorHandle, did: authorDid, rkey }, collection);\n }\n\n // recipe.exchange: same shape as Kich — the record's attribution.url points\n // at the original source (a blog/video), not the app. Always link to the\n // recipe.exchange page. Must run before the generic fallback below.\n if (collection === 'exchange.recipe.recipe') {\n return patternHealth('recipe', { handle: authorHandle, did: authorDid, rkey }, collection);\n }\n\n // atmob paste (atmob.in): the SPA viewer renders any paste by its full at-uri\n // via a `?uri=` query param (verified in the app's app.js). Both\n // paste.document and paste.bundle share that viewer, so build the URL from the\n // item's own uri — a self-permalink for this record (record-checkable).\n if (collection.startsWith('in.atmob.paste.')) {\n if (uri) return recordHealth(`https://atmob.in/?uri=${encodeURIComponent(uri)}`);\n return { url: null, strategy: 'none' };\n }\n\n // --- Generic fallbacks ---\n\n // record.url is a common ad-hoc per-item URL (hyperboards, etc.). It is an\n // arbitrary foreign URL, so probe it directly.\n const recordUrl = stringOrNull(record.url);\n if (recordUrl) return urlHealth(recordUrl);\n\n // Pattern-based fallback. Pass `collection` so app-specific guards (bluesky\n // restricting /post/{rkey} to app.bsky.feed.post) can apply. The tier\n // (per-item vs profile) decides record- vs url-strategy.\n return patternHealth(appId, { handle: authorHandle, did: authorDid, rkey }, collection);\n}\n\n/**\n * Resolve the canonical clickable URL for an activity item — the same URL\n * the activity-card UI in sifa-web renders. Thin wrapper over\n * {@link resolveCardHealth}; returns `null` when no link is appropriate.\n */\nexport function resolveCardUrl(item: ActivityItemForUrl): string | null {\n return resolveCardHealth(item).url;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -4499,16 +4499,55 @@ function parseAtUri(atUri) {
|
|
|
4499
4499
|
if (!match || !match[1] || !match[2]) return null;
|
|
4500
4500
|
return { did: match[1], rkey: match[2] };
|
|
4501
4501
|
}
|
|
4502
|
+
var TANGLED = "https://tangled.sh";
|
|
4503
|
+
function tangledRepoBase(ref) {
|
|
4504
|
+
const value = stringOrNull(ref);
|
|
4505
|
+
if (!value) return null;
|
|
4506
|
+
if (value.startsWith("did:")) return `${TANGLED}/${value}`;
|
|
4507
|
+
const parsed3 = parseAtUri(value);
|
|
4508
|
+
if (parsed3 && TANGLED_REPO_SLUG.test(parsed3.rkey))
|
|
4509
|
+
return `${TANGLED}/${parsed3.did}/${parsed3.rkey}`;
|
|
4510
|
+
return null;
|
|
4511
|
+
}
|
|
4512
|
+
function resolveTangledUrl(collection, record, rkey, owner) {
|
|
4513
|
+
if (collection === "sh.tangled.repo.pull" || collection === "sh.tangled.repo.issue") {
|
|
4514
|
+
const target = record.target && typeof record.target === "object" ? record.target : {};
|
|
4515
|
+
const base = tangledRepoBase(target.repoDid) ?? tangledRepoBase(target.repo) ?? tangledRepoBase(record.repoDid) ?? tangledRepoBase(record.repo);
|
|
4516
|
+
if (!base) return null;
|
|
4517
|
+
const list = collection === "sh.tangled.repo.pull" ? "pulls" : "issues";
|
|
4518
|
+
return { url: `${base}/${list}`, ownRecord: false };
|
|
4519
|
+
}
|
|
4520
|
+
if (collection === "sh.tangled.feed.star") {
|
|
4521
|
+
const starred = tangledRepoBase(record.subjectDid) ?? tangledRepoBase(record.subject);
|
|
4522
|
+
return starred ? { url: starred, ownRecord: false } : null;
|
|
4523
|
+
}
|
|
4524
|
+
const repoDid = tangledRepoBase(record.repoDid);
|
|
4525
|
+
if (repoDid) return { url: repoDid, ownRecord: true };
|
|
4526
|
+
const repoName = stringOrNull(record.name) ?? (collection === "sh.tangled.repo" ? rkey : null);
|
|
4527
|
+
if (repoName && owner && TANGLED_REPO_SLUG.test(repoName)) {
|
|
4528
|
+
return { url: `${TANGLED}/${owner}/${repoName}`, ownRecord: true };
|
|
4529
|
+
}
|
|
4530
|
+
return null;
|
|
4531
|
+
}
|
|
4502
4532
|
function resolveCardHealth(item) {
|
|
4503
4533
|
const { collection, record, uri, rkey, authorDid, authorHandle } = item;
|
|
4504
4534
|
const appId = getAppIdForCollection(collection);
|
|
4505
4535
|
if (collection.startsWith("sh.tangled.")) {
|
|
4506
|
-
const
|
|
4507
|
-
if (
|
|
4508
|
-
|
|
4509
|
-
}
|
|
4536
|
+
const tangled = resolveTangledUrl(collection, record, rkey, authorHandle ?? authorDid);
|
|
4537
|
+
if (tangled) return tangled.ownRecord ? recordHealth(tangled.url) : urlHealth(tangled.url);
|
|
4538
|
+
if (!authorHandle && authorDid) return urlHealth(`${TANGLED}/${authorDid}`);
|
|
4510
4539
|
return patternHealth("tangled", { handle: authorHandle, did: authorDid, rkey }, collection);
|
|
4511
4540
|
}
|
|
4541
|
+
if (collection === "app.userinput.discussion" && authorDid) {
|
|
4542
|
+
return recordHealth(`https://userinput.app/d/${authorDid}/${rkey}`);
|
|
4543
|
+
}
|
|
4544
|
+
if (collection === "app.userinput.reply") {
|
|
4545
|
+
const subject = record.subject;
|
|
4546
|
+
const subjectUri = subject != null && typeof subject === "object" ? stringOrNull(subject.uri) : null;
|
|
4547
|
+
const parsed3 = subjectUri?.includes("/app.userinput.discussion/") ? parseAtUri(subjectUri) : null;
|
|
4548
|
+
if (parsed3) return urlHealth(`https://userinput.app/d/${parsed3.did}/${parsed3.rkey}`);
|
|
4549
|
+
return { url: null, strategy: "none" };
|
|
4550
|
+
}
|
|
4512
4551
|
if (collection.startsWith("com.kipclip.") || collection.startsWith("community.lexicon.bookmarks.")) {
|
|
4513
4552
|
const subject = stringOrNull(record.subject);
|
|
4514
4553
|
if (subject) return urlHealth(subject);
|
|
@@ -5300,6 +5339,15 @@ var TITLE_BY_VERB = {
|
|
|
5300
5339
|
received: () => "Received",
|
|
5301
5340
|
styledCharacter: () => "Styled a character"
|
|
5302
5341
|
};
|
|
5342
|
+
function resolveVerb(collection, record) {
|
|
5343
|
+
if (collection === "blog.pckt.mini.post") {
|
|
5344
|
+
const reply = asRecord(record?.reply);
|
|
5345
|
+
for (const ref of [reply?.parent, reply?.root]) {
|
|
5346
|
+
if (nonBlankString(asRecord(ref)?.uri)?.startsWith("at://")) return "replied";
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
return verbForCollection(collection);
|
|
5350
|
+
}
|
|
5303
5351
|
function buildTitle(verb, label, hasSubject) {
|
|
5304
5352
|
return TITLE_BY_VERB[verb](label, hasSubject);
|
|
5305
5353
|
}
|
|
@@ -5907,9 +5955,9 @@ function applyGeneric(vm, item, record) {
|
|
|
5907
5955
|
return vm;
|
|
5908
5956
|
}
|
|
5909
5957
|
function toStreamCardVM(item, options = {}) {
|
|
5910
|
-
const verb = verbForCollection(item.collection);
|
|
5911
|
-
const source = buildSource(item, options);
|
|
5912
5958
|
const record = asRecord(item.record);
|
|
5959
|
+
const verb = resolveVerb(item.collection, record);
|
|
5960
|
+
const source = buildSource(item, options);
|
|
5913
5961
|
const timestamp = (record ? asNonEmptyString(record.createdAt) : void 0) ?? item.indexedAt;
|
|
5914
5962
|
const vm = {
|
|
5915
5963
|
uri: item.uri,
|
|
@@ -7748,7 +7796,7 @@ function describeSifaRecord(collection, value) {
|
|
|
7748
7796
|
}
|
|
7749
7797
|
|
|
7750
7798
|
// src/index.ts
|
|
7751
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
7799
|
+
var SIFA_SDK_VERSION = "0.19.103";
|
|
7752
7800
|
|
|
7753
7801
|
exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
|
|
7754
7802
|
exports.ACTIVITY_VERBS = ACTIVITY_VERBS;
|