Share Artifacts (publish local files and pasted content as links) (part 3)
The publish pipeline is in src/main/services/share-artifact/share-artifact-service.ts — single chokepoint for both the renderer IPC (SHAREPUBLISHARTIFACT) and the CLI POST /share/publish. Both routes pass through createShareArtifactService({ publisher }) so they share one storage publisher.
What it is
This is part 3 of the Share Artifacts (publish local files and pasted content as links) page. It carries the next stretch of the material on that page, moved here because a single page is capped at 40,000 characters.
Where to find it
Reach this part through Share Artifacts (publish local files and pasted content as links) — it lists every part and explains where the feature lives in the product. Everything below is reached from the same place.
How it behaves
Everything below is the behaviour, detail and edge cases that belong to this stretch of the Share Artifacts (publish local files and pasted content as links) page.
How it works
The publish pipeline is in src/main/services/share-artifact/share-artifact-service.ts — single chokepoint for both the renderer IPC (SHARE_PUBLISH_ARTIFACT) and the CLI POST /share/publish. Both routes pass through createShareArtifactService({ publisher }) so they share one storage publisher. After upload, publishArtifact() CONFIRMS the Firestore mirror doc landed (the public gate keys on it) and, on a permanent/persistent failure, fails the publish — rolling back the upload + the row — rather than return a URL that 404s (contract publish-confirms-the-mirror; the entry points no longer mirror publishes themselves).
The detector and HTML builder live at src/main/services/share/share-artifact-builder.ts. Detection is extension-first, then magic-byte-second; the builder uses marked + highlight.js for Markdown / code, base64-inlines images and PDFs, and HTML-escapes plain text. The page shell also embeds the Omniscio app icon as a data: favicon, sourced from the shared src/main/services/share/share-favicon.ts constant (the same icon used by conversation + digest shares).
Path validation against the active-workdirs + user-data allow-list is in src/main/services/share/share-path-validator.ts — symlinks resolve through fs.realpath before the check.
The share publisher is in src/main/services/firebase/firebase-publisher.ts — but it holds no Firebase key. It is a thin seam over share-relay-client.ts: every method delegates to the shareRelay Cloud Function (firebase/functions/src/share-relay.ts), authed by the signed-in user's global-auth Firebase ID token. The relay (holding the agentmc-share-uploader key in Secret Manager) mints the share token + short-lived v4 signed PUT URLs, and the app uploads the bytes straight to Cloud Storage (bucket agentmc-shares-artifacts, prefix shares/) — so a large video has no 32 MB function-relay cap, and the per-kind security headers (content-type / cache-control / content-disposition) are pinned on the signed URL server-side. The relay also OWNS the token (closing the client-minted-token overwrite hole) and tears shares down (unpublish deletes the objects + mirror doc + events in one ownerUid-gated call). Because publishing needs sign-in, a signed-out publish fails with sign-in-required ("sign in to share"), never silently. (This retired the desktop's Firebase admin "master" key for Shares — the old client-side GCS REST + google-auth-library JWT uploader is gone.)
The Firestore mirror — the shares/{token} doc the public gate reads, written on publish/revoke/delete — is in src/main/services/share/share-firestore-mirror.ts. It keeps the same public API but routes every write/read/delete through the same shareRelay function (relayMirrorWrite / relayMirrorRead / relayUnpublish); the client sends only the policy fields and never the server-owned ownerUid / jwtSecret / viewCount. (This subsumed the earlier client-side REST/JWT mirror — itself the 2026-06-29 fix for a packaged build where firebase-admin's gRPC auth presented the wrong identity to Firestore, every consumer 7 PERMISSION_DENIED, silently 404'ing every share for ~12h: postmortem.) The publish-time confirmation + fail-loud rollback live in share-publish-confirm.ts (it reads the mirror doc back via mirrorDocExists, so a write-ack without a serve-visible doc never returns success — the 2026-08-11 transient-relay incident); the self-heal backfill — on startup AND every ~30 min — in share-mirror-backfill.ts; the single deduped "cloud features degraded" alert (shared with the agent-email / support-chat consumers) in src/main/services/firebase/firebase-health-alert.ts.
DB schema is in src/main/db/queries-share.ts — insertSharePending → markSharePublished two-phase write, findActiveByContentHash for dedup, revokeShare / deleteShare for lifecycle.
The renderer entry points are src/renderer/src/components/ui/FilePeekOverlay.tsx (the Share2 button in the file-peek toolbar), src/renderer/src/features/shares/PastePublishModal.tsx (the paste modal, lazy-loaded from ShareManager.tsx per the heavy-modal lazy-load rule), and src/renderer/src/components/ui/agent-markdown-helpers.tsx (CopyableLink — the right-click menu on every file path / attachment link that Claude produces in chat). The three entry points all call the same IPC.SHARE_PUBLISH_ARTIFACT handler — the difference is only the payload shape (sourcePath vs pastedContent vs attachmentRef) and the origin discriminator that drives the sourceType telemetry split.
Telemetry fires from inside publishArtifact() on both branches — dedup-hit (reused: true) and fresh-publish (reused: false) — so the Stats view reflects all publish intents. The allow-list for the telemetry metadata is ['artifactType', 'reused', 'sourceType', 'autoStaged'] per the registry in src/shared/feature-registry/.
Auto-stage lives in the same file as autoStageExternal(sourcePath, userDataDir): if the first validateSharePath() returns valid: false with reason 'path outside allowed roots', without the internal refusedSensitive flag, AND sourceType !== 'cli', the service copies the source bytes to <userData>/published-pastes/<uuid>/<original base name> and re-validates. Two details are load-bearing. (1) The branch may not key on the reason string alone: a path refused because its NAME is credential-shaped reports that same string on purpose, and rescuing one would strip the name the deny-gate keys on — so a refused secret is refused on every road, before the gesture check and before any byte is copied (secret-refusal-is-never-rescued). (2) The staged copy keeps the source's REAL leaf name inside a fresh UUID directory, so the post-stage re-validation still sees what the file is (auto-stage-keeps-the-real-name); naming it <uuid><ext> after the old flat scheme destroyed exactly that. The one sweep reclaims an aged stage directory whole. Any failure of the stage step (ENOENT, EACCES, ENOSPC) collapses back to outside-allowlist so the closed error-string contract is preserved. The full invariant set + safe-change checklist lives in .claude/memory/contracts/share-artifacts-contract.md.
Related
The overview, the other parts, and everything else worth reading next all sit on Share Artifacts (publish local files and pasted content as links).
Last verified 2026-09-24