| … | … | @@ -20,7 +20,9 @@ const PLATFORMS = { |
| 20 | 20 | MATRIX: 3, |
| 21 | 21 | MASTODON: 4, |
| 22 | 22 | EMAIL: 5, |
| 23 | ANDROID: 6 | |
| 23 | ANDROID: 6, | |
| 24 | ATPROTO: 7, | |
| 25 | INSTAGRAM: 8 | |
| 24 | 26 | } |
| 25 | 27 | |
| 26 | 28 | // Reverse maps for display |
| … | … | @@ -0,0 +1,192 @@ |
| 1 | # AT Protocol Bridge | |
| 2 | ||
| 3 | Integration design for `bridge-atproto` — AT Protocol (Bluesky) as a Tier 2 bridge in hyper-social. | |
| 4 | ||
| 5 | ## AT Protocol Ecosystem (2026) | |
| 6 | ||
| 7 | ### Self-Hosting is Production-Ready | |
| 8 | ||
| 9 | - Official PDS runs on a **Raspberry Pi 3 B+** (1 core, 1GB RAM) | |
| 10 | - **Tranquil PDS** (Rust, single binary + Postgres) — passkeys, 2FA, SSO, `did:web`, granular OAuth scopes | |
| 11 | - **rsky** (Rust, Blacksky) — full independent relay + AppView + PDS at network scale | |
| 12 | - Cloudflare Tunnels eliminate static IP requirements | |
| 13 | - **IndieSky** working group (funded $50K by Free Our Feeds) coordinating independent infrastructure | |
| 14 | ||
| 15 | ### Relay Costs Collapsed | |
| 16 | ||
| 17 | **Sync v1.1** ("inductive firehose" / MST inversion) removed the requirement for relays to cache all network data locally: | |
| 18 | ||
| 19 | - Full-network relay: **$34/month** (2 vCPU, 12GB RAM, 30 Mbps) | |
| 20 | - Community members running relays on Raspberry Pis and $19/month VPS boxes | |
| 21 | - Rainbow fan-out servers re-broadcast firehose to many clients | |
| 22 | ||
| 23 | ### The "No AppView" Path | |
| 24 | ||
| 25 | - **Constellation** (Microcosm) — backlink index for the entire AT Proto firehose on a **Raspberry Pi 4B + SSD**, <2GB/day disk | |
| 26 | - **Red Dwarf** — Bluesky client using **zero AppView**, direct PDS queries + Constellation | |
| 27 | - Proves you don't need $500K infrastructure for a functional client | |
| 28 | ||
| 29 | ### Alternative PDS Implementations | |
| 30 | ||
| 31 | | Implementation | Language | Storage | Notable Features | | |
| 32 | |---------------|----------|---------|-----------------| | |
| 33 | | Reference PDS | TypeScript | SQLite | Official, Docker or bare metal | | |
| 34 | | Tranquil PDS | Rust | Postgres | Passkeys, 2FA, SSO, did:web, granular OAuth | | |
| 35 | | rsky-pds | Rust | Postgres + S3 | Part of full Blacksky stack | | |
| 36 | | Cocoon | Go | — | Community PDS | | |
| 37 | ||
| 38 | ### Protocol Developments | |
| 39 | ||
| 40 | - **Auth Scopes** — granular OAuth permissions per Lexicon type | |
| 41 | - **Lexicon publishing on-protocol** — schemas discoverable via DNS-like lookup | |
| 42 | - **IETF standardization** — working group charter published January 2026 | |
| 43 | - **Private/shared data** — next major protocol component (watching MLS at IETF) | |
| 44 | - Relays explicitly optional — clients can connect directly to PDSes via WebSocket | |
| 45 | ||
| 46 | ### P2P Hybrid Projects | |
| 47 | ||
| 48 | - **Roomy** — AT Proto for identity/discovery + Automerge CRDTs for P2P data sync (local-first, heading toward full P2P) | |
| 49 | - **Tangled** — social Git forge on AT Proto, self-hostable "knots", raised $4.5M (GitHub's ex-CEO backing) | |
| 50 | - Nobody has built AT Proto over Hypercore/Hyperswarm yet | |
| 51 | ||
| 52 | ### Data Model Compatibility | |
| 53 | ||
| 54 | AT Proto repos use DAG-CBOR, CID links, CAR files — the same IPLD primitives as IPFS. Repositories are content-addressed Merkle Search Trees with signed commits. This maps naturally to Hypercore/Hyperbee storage. | |
| 55 | ||
| 56 | ## Integration Design | |
| 57 | ||
| 58 | ### Approach: bridge-atproto (Tier 2 Bridge) | |
| 59 | ||
| 60 | Same pattern as bridge-irc, bridge-discord, bridge-mastodon. Independently deployable, composes through the hub. | |
| 61 | ||
| 62 | ``` | |
| 63 | bridge-atproto/ | |
| 64 | ├── lib/ | |
| 65 | │ ├── atproto-client.js # AT Proto PDS connection (XRPC/WebSocket) | |
| 66 | │ ├── keet-client.js # Keet room connection (reuse from other bridges) | |
| 67 | │ ├── adapter.js # AT Proto record ↔ bridge-envelope conversion | |
| 68 | │ ├── router.js # Message routing rules | |
| 69 | │ └── bridge.js # Orchestrator | |
| 70 | ├── test/ | |
| 71 | │ ├── adapter.test.js | |
| 72 | │ └── router.test.js | |
| 73 | ├── index.js | |
| 74 | └── package.json | |
| 75 | ``` | |
| 76 | ||
| 77 | ### Message Mapping | |
| 78 | ||
| 79 | ``` | |
| 80 | AT Proto post (app.bsky.feed.post) ←→ bridge-envelope ←→ Keet room message | |
| 81 | AT Proto like (app.bsky.feed.like) ←→ bridge-envelope ←→ Keet room reaction | |
| 82 | AT Proto follow (app.bsky.graph.follow) → bridge-envelope → Keet room notification | |
| 83 | AT Proto reply thread ←→ bridge-envelope ←→ Keet room thread | |
| 84 | ``` | |
| 85 | ||
| 86 | ### Credentials | |
| 87 | ||
| 88 | bridge-vault stores AT Proto OAuth tokens or app passwords. Same encrypted Hyperbee-backed storage as other bridges. | |
| 89 | ||
| 90 | ### Architecture Alignment | |
| 91 | ||
| 92 | | Concept | Hyper-Social | AT Protocol | | |
| 93 | |---------|-------------|-------------| | |
| 94 | | Data unit | Room message (`_hs:*` JSON) | Record (Lexicon-typed DAG-CBOR) | | |
| 95 | | Schema | Ad-hoc `_hs:` prefixed types | Formal Lexicons (NSIDs) | | |
| 96 | | Multi-writer | Autobase (keet-core) | MST repo per user + relay aggregation | | |
| 97 | | Identity | Keet keypair (ed25519) | DID (`did:plc`, `did:web`, `did:key`) | | |
| 98 | | Discovery | Hyperswarm DHT | DNS + DID resolution | | |
| 99 | | Indexing | MI/clustering on room membership | Relay firehose + AppView/Constellation | | |
| 100 | ||
| 101 | ### Identity Bridge | |
| 102 | ||
| 103 | Both systems use ed25519 keypairs. The identity graph (Phase 6) can link them: | |
| 104 | ||
| 105 | ``` | |
| 106 | Keet keypair ←→ did:key (same ed25519 curve) | |
| 107 | Room membership ←→ AT Proto social graph | |
| 108 | _hs:identity-link ←→ Bidirectional attestation | |
| 109 | ``` | |
| 110 | ||
| 111 | Tighter than IRC/Discord/Matrix identity linking, which is username-only mapping. | |
| 112 | ||
| 113 | ## Deeper Integration Paths | |
| 114 | ||
| 115 | These grow out of the bridge if there's a reason — none are prerequisites. | |
| 116 | ||
| 117 | ### hyper-feed as AT Proto Client | |
| 118 | ||
| 119 | hyper-feed subscribes to AT Proto PDSes directly (relays are optional): | |
| 120 | ||
| 121 | - Direct PDS queries for posts/profiles (Red Dwarf architecture) | |
| 122 | - Constellation-style backlink indexing on Autobase (community-scale) | |
| 123 | - Unified timeline: Keet room messages + AT Proto posts | |
| 124 | - Runs on a Raspberry Pi | |
| 125 | ||
| 126 | ### Lexicons as Room Metadata Schemas | |
| 127 | ||
| 128 | Publish hyper-social data types as AT Proto Lexicons: | |
| 129 | ||
| 130 | ```javascript | |
| 131 | // Current: ad-hoc room metadata | |
| 132 | { type: '_hs:room-meta', schema: 'wiki', title: '...' } | |
| 133 | ||
| 134 | // With Lexicons: formally typed, interoperable | |
| 135 | { type: '_hs:room-meta', lexicon: 'social.hypersocial.wiki.page', ... } | |
| 136 | ``` | |
| 137 | ||
| 138 | Wiki pages, kanban cards, and feed posts become first-class AT Proto records discoverable by any AT Proto client. | |
| 139 | ||
| 140 | ### Hyperbee-Backed PDS | |
| 141 | ||
| 142 | Store AT Proto repos directly in Hyperbee. MST keys map to sorted Hyperbee keys: | |
| 143 | ||
| 144 | ``` | |
| 145 | Hyperbee key: "app.bsky.feed.post/3k..." → value: DAG-CBOR record | |
| 146 | Hyperbee key: "app.bsky.graph.follow/3k..." → value: DAG-CBOR record | |
| 147 | ``` | |
| 148 | ||
| 149 | Replication over Hyperswarm instead of HTTPS WebSocket sync. HTTP gateway bridges to vanilla AT Proto network for federation. | |
| 150 | ||
| 151 | ### Full Topology | |
| 152 | ||
| 153 | ``` | |
| 154 | ┌─────────────┐ | |
| 155 | Keet rooms ────────►│ hyper-social │◄──────── AT Proto PDSes | |
| 156 | IRC channels ──────►│ (hub) │◄──────── Mastodon | |
| 157 | Discord ───────────►│ │◄──────── Matrix | |
| 158 | Email ─────────────►│ │ | |
| 159 | └──────┬──────┘ | |
| 160 | │ | |
| 161 | Autobase index | |
| 162 | (community-scale) | |
| 163 | ``` | |
| 164 | ||
| 165 | Every bridge enriches the same social graph. MI/clustering detects community structure across all connected networks. | |
| 166 | ||
| 167 | ## Phased Implementation | |
| 168 | ||
| 169 | | Phase | What | Effort | Depends On | | |
| 170 | |-------|------|--------|-----------| | |
| 171 | | 0 | `bridge-atproto` — Keet rooms ↔ AT Proto posts, bridge-envelope + bridge-vault | 1-2 weeks | Nothing (same pattern as existing bridges) | | |
| 172 | | 1 | AT Proto DID linking in identity graph | 1 week | Phase 6 of main roadmap | | |
| 173 | | 2 | hyper-feed ingests AT Proto posts directly from PDSes | 2-3 weeks | Phase 4 of main roadmap | | |
| 174 | | 3 | Publish hyper-social Lexicons — wiki, board, feed become AT Proto-native | 2 weeks | Lexicon publishing on-protocol | | |
| 175 | | 4 | Hyperbee-backed PDS — AT Proto repos in Hypercore, Hyperswarm replication, HTTP gateway | 1 month | Phase 2-3 | | |
| 176 | ||
| 177 | Phase 0 follows the exact pattern of the five existing bridges. Each subsequent phase deepens integration while staying backward-compatible with the evolutionary roadmap. | |
| 178 | ||
| 179 | ## References | |
| 180 | ||
| 181 | - [AT Protocol Self-Hosting Guide](https://atproto.com/guides/self-hosting) | |
| 182 | - [Relay Updates for Sync v1.1](https://atproto.com/blog/relay-updates-sync-v1-1) | |
| 183 | - [A Full-Network Relay for $34/month](https://whtwnd.com/bnewbold.net/3lo7a2a4qxg2l) | |
| 184 | - [Roomy Deep Dive](https://blog.muni.town/roomy-deep-dive/) — AT Proto + Automerge P2P hybrid | |
| 185 | - [Microcosm / Constellation](https://www.microcosm.blue/) — Raspberry Pi-scale backlink index | |
| 186 | - [Red Dwarf](https://reddwarf.app) — AppView-less Bluesky client | |
| 187 | - [Tangled](https://tangled.org) — Social Git forge on AT Proto | |
| 188 | - [Tranquil PDS](https://buttercup.wizardry.systems/) — Rust PDS with advanced auth | |
| 189 | - [Blacksky rsky](https://github.com/blacksky-algorithms/rsky) — Full Rust AT Proto stack | |
| 190 | - [IndieSky Working Group](https://atproto.wiki/en/working-groups/indiesky) | |
| 191 | - [2025 Protocol Roadmap](https://docs.bsky.app/blog/2025-protocol-roadmap-spring) | |
| 192 | - [Protocol Check-in Fall 2025](https://atproto.com/blog/protocol-check-in-fall-2025) |
| … | … | @@ -0,0 +1,700 @@ |
| 1 | # Community Moderation and Governance | |
| 2 | ||
| 3 | How named communities moderate themselves, scale governance, and maintain the boundary between room sovereignty and community sovereignty. Companion to [COMMUNITY-EMERGENCE.md](COMMUNITY-EMERGENCE.md) (how communities form) and [ROOMS-ARE-THE-COMPUTER.md](ROOMS-ARE-THE-COMPUTER.md) (the implementation architecture). | |
| 4 | ||
| 5 | ## the problem | |
| 6 | ||
| 7 | local MI clustering gives each user a personal view of their social graph. no moderation needed — it's your own folder organization. but the moment a human names a cluster and it becomes a shared community, three questions appear: | |
| 8 | ||
| 9 | - who moderates the community? | |
| 10 | - what exactly gets moderated? | |
| 11 | - how does governance scale beyond the founding group? | |
| 12 | ||
| 13 | these questions are entangled. the answer to each constrains the others. | |
| 14 | ||
| 15 | ## who moderates | |
| 16 | ||
| 17 | ### the namer seeds the context | |
| 18 | ||
| 19 | - when someone nucleates a cluster ("this is the testing community"), they carry implicit context about what "testing" means | |
| 20 | - they become initial admin not by authority but because they demonstrated enough understanding to name the pattern | |
| 21 | - this mirrors blacksky: fraser detected the statistical cluster from follow graphs, then named it. the naming act was political, not algorithmic | |
| 22 | ||
| 23 | ### context transfers through people, not rules | |
| 24 | ||
| 25 | - the namer invites moderators who share context | |
| 26 | - a "rust p2p" community's mods know what's on-topic without a written policy. a "berlin meetup" community's mods understand local norms | |
| 27 | - you can't export one community's mod team to another — the context doesn't serialize | |
| 28 | - blacksky's formulation: "context-aware moderators with shared understanding of anti-blackness, misogynoir, internalized racism" — things that keyword filters will never catch | |
| 29 | - moderation is a section of the community sheaf — locally consistent within the community, non-transferable across communities | |
| 30 | ||
| 31 | ### keet-core roles map directly | |
| 32 | ||
| 33 | the community-room uses existing keet-core roles. no new permission system: | |
| 34 | ||
| 35 | | room role | community meaning | | |
| 36 | |-----------|------------------| | |
| 37 | | admin | community founders — curate room listings, set norms, invite mods | | |
| 38 | | moderator | context-carriers — curate listings, flag rooms, can't change governance structure | | |
| 39 | | member | participants — browse listings, propose rooms for inclusion | | |
| 40 | | read-only | observers — see the community's room list, can't interact | | |
| 41 | ||
| 42 | ### the community-room's chat IS the meta-discussion space | |
| 43 | ||
| 44 | - "should we add this room?" | |
| 45 | - "this room's content doesn't fit our norms" | |
| 46 | - "let's discuss our moderation approach" | |
| 47 | - governance happens in conversation, not in a separate governance tool (at least initially) | |
| 48 | ||
| 49 | ## what gets moderated | |
| 50 | ||
| 51 | ### listings, not content | |
| 52 | ||
| 53 | the critical boundary: a community moderator can remove a room from the community listing. they CANNOT moderate inside that room. two separate scopes, two separate sheaf sections. | |
| 54 | ||
| 55 | ``` | |
| 56 | room admin: sees room data (messages, members, content) | |
| 57 | community admin: sees community data (which rooms listed, their metadata) | |
| 58 | nobody: sees the full graph | |
| 59 | ``` | |
| 60 | ||
| 61 | ### community admin powers | |
| 62 | ||
| 63 | **can do:** | |
| 64 | - add/remove `_hs:room-listing` messages in the community-room | |
| 65 | - set community metadata (topics, description, norms statement) | |
| 66 | - invite/ban community members (who can propose rooms) | |
| 67 | - pin governance decisions as messages | |
| 68 | ||
| 69 | **cannot do:** | |
| 70 | - read messages in listed rooms (unless they're also a member of that room independently) | |
| 71 | - see membership of listed rooms | |
| 72 | - moderate content inside listed rooms | |
| 73 | - track which community members joined which listed rooms | |
| 74 | ||
| 75 | ### the accountability gap is intentional | |
| 76 | ||
| 77 | if a listed room has toxic content, the community admin's only lever is delisting. they can't reach inside and fix it. the room admin must fix it or the room gets delisted. | |
| 78 | ||
| 79 | this prevents community governance from becoming surveillance. the room admin is a bartender — they see what happens in their bar, they don't follow you home, they don't know what other bars you go to. the community admin is a neighborhood association — they decide which bars are listed in the directory, they don't enter the bars. | |
| 80 | ||
| 81 | ### rooms in multiple communities | |
| 82 | ||
| 83 | a room listed in both "rust dev" and "berlin meetup" has two community-level governance contexts: | |
| 84 | - if "rust dev" delists it, "berlin meetup" still lists it | |
| 85 | - the room itself is unchanged | |
| 86 | - no single community has authority over a room | |
| 87 | - this is federation, not hierarchy | |
| 88 | ||
| 89 | ### moderation signals, not enforcement | |
| 90 | ||
| 91 | instead of enforcement transfer between communities, signal: | |
| 92 | ||
| 93 | ``` | |
| 94 | community A's moderator flags a room → signal published as _hs:mod-signal | |
| 95 | community B subscribes to A's signals → sees the signal | |
| 96 | community B decides independently → keep listing, add warning, or delist | |
| 97 | ``` | |
| 98 | ||
| 99 | - moderation reputation becomes emergent — communities that produce good signals get subscribed to, bad signals get ignored | |
| 100 | - no central moderator — a network of moderation services that communities opt into | |
| 101 | - same model as blacksky's labeler: produces labels, each room/user independently decides how to respond (off / warn / hide per label type) | |
| 102 | ||
| 103 | ### the _hs message types for moderation signals | |
| 104 | ||
| 105 | ```javascript | |
| 106 | // a moderation signal (posted in a moderation-room or community-room) | |
| 107 | {"_hs":"mod-signal", | |
| 108 | "target":"room:abc123", | |
| 109 | "signal":"off-topic", | |
| 110 | "reason":"crypto scam room, not related to testing", | |
| 111 | "signalBy":"member:moderator0", | |
| 112 | "signalAt":1708402400000} | |
| 113 | ||
| 114 | // subscribing to another community's signals = joining their moderation room | |
| 115 | // your client reads their _hs:mod-signal messages and surfaces them locally | |
| 116 | // you decide what to do — the signal is information, not enforcement | |
| 117 | ``` | |
| 118 | ||
| 119 | ### what transfers across community boundaries | |
| 120 | ||
| 121 | not enforcement. signal. | |
| 122 | ||
| 123 | ``` | |
| 124 | not: "room X is banned" (enforcement transfer) | |
| 125 | but: "room X was flagged for spam by the rust-dev community" (signal transfer) | |
| 126 | ||
| 127 | your community applies its OWN context to the signal. | |
| 128 | ``` | |
| 129 | ||
| 130 | this is critical because moderation context doesn't serialize. what's spam in a dev community might be on-topic in a crypto community. what's off-topic in one context is the main event in another. | |
| 131 | ||
| 132 | ## how governance scales | |
| 133 | ||
| 134 | ### why voting breaks | |
| 135 | ||
| 136 | majority voting produces winners and losers. the minority feels unheard. over time, losers disengage or split. this is why every discord server eventually fractures along governance lines. | |
| 137 | ||
| 138 | 52% vs 48% on a contentious issue doesn't resolve the tension — it suppresses the 48%. the 48% either comply resentfully, leave, or fork. none of these outcomes preserve community health. | |
| 139 | ||
| 140 | ### consensus-finding instead | |
| 141 | ||
| 142 | blacksky uses polis — a tool where: | |
| 143 | ||
| 144 | 1. members submit **statements** (not votes on predefined options) | |
| 145 | 2. members **agree/disagree/pass** on each other's statements | |
| 146 | 3. machine learning finds **clusters of agreement** — groups of people who tend to agree on similar things | |
| 147 | 4. output is a **consensus map**: which statements have broad agreement, which are divisive, where the fault lines are | |
| 148 | ||
| 149 | the output isn't "option A won with 52%." it's: | |
| 150 | - "here's what almost everyone agrees on" (consensus zone) | |
| 151 | - "here are two groups that disagree on X" (fault lines) | |
| 152 | - "here's a statement that bridges the groups" (potential compromise) | |
| 153 | ||
| 154 | minorities aren't outvoted — they're visible as a cluster with their own position. the governance tool surfaces the topology of opinion rather than flattening it into a binary. | |
| 155 | ||
| 156 | ### three phases of governance tooling | |
| 157 | ||
| 158 | governance can't be pre-installed. it must emerge in order: | |
| 159 | ||
| 160 | ``` | |
| 161 | statistical cluster → named community → identity forms → | |
| 162 | norms emerge → norms formalize → governance infrastructure | |
| 163 | ``` | |
| 164 | ||
| 165 | you can't impose governance on a community that hasn't formed identity. so the tooling phases match the community lifecycle: | |
| 166 | ||
| 167 | **phase 1: chat-based deliberation (small communities)** | |
| 168 | ||
| 169 | the community-room's chat IS the governance space. admins read the conversation, use context-judgment. this works for communities under ~50 active members — which is 90% of communities. | |
| 170 | ||
| 171 | ``` | |
| 172 | alice: "should we add the new crypto-testing room?" | |
| 173 | bob: "looks relevant, active overlap with our members" | |
| 174 | carol: "idk, last crypto room we added was full of scams" | |
| 175 | alice: "good point, let's wait and see if it stays legit" | |
| 176 | [admin decision: hold off for now] | |
| 177 | ``` | |
| 178 | ||
| 179 | no special tooling. the conversation IS the deliberation. the admin's decision is informed by the discussion but ultimately their call — benevolent dictator model, same as early blacksky. | |
| 180 | ||
| 181 | **phase 2: structured proposals (growing communities)** | |
| 182 | ||
| 183 | when chat-based deliberation gets noisy (too many voices, hard to track positions), structured `_hs:governance` messages: | |
| 184 | ||
| 185 | ```javascript | |
| 186 | // a proposal | |
| 187 | {"_hs":"governance", | |
| 188 | "type":"proposal", | |
| 189 | "text":"add the crypto-testing room to our community", | |
| 190 | "proposedBy":"member:alice0", | |
| 191 | "proposedAt":1708402400000} | |
| 192 | ||
| 193 | // positions (not votes — positions with reasons) | |
| 194 | {"_hs":"governance", | |
| 195 | "type":"position", | |
| 196 | "proposalId":"msg:abc123", | |
| 197 | "stance":"agree", | |
| 198 | "reason":"good fit, 60% member overlap with our existing rooms", | |
| 199 | "by":"member:bob0"} | |
| 200 | ||
| 201 | {"_hs":"governance", | |
| 202 | "type":"position", | |
| 203 | "proposalId":"msg:abc123", | |
| 204 | "stance":"disagree", | |
| 205 | "reason":"too many scam rooms in crypto space, quality risk", | |
| 206 | "by":"member:carol0"} | |
| 207 | ||
| 208 | // resolution (admin decision after reading positions) | |
| 209 | {"_hs":"governance", | |
| 210 | "type":"resolution", | |
| 211 | "proposalId":"msg:abc123", | |
| 212 | "decision":"deferred", | |
| 213 | "reason":"split opinion — revisit in 2 weeks after observing room quality", | |
| 214 | "resolvedBy":"member:admin0"} | |
| 215 | ``` | |
| 216 | ||
| 217 | the client renders these as structured cards — proposal at top, positions as a thread, resolution pinned. clearer than scrolling through chat. still admin-decided, but the reasoning is visible and auditable. | |
| 218 | ||
| 219 | **phase 3: consensus-finding (large communities)** | |
| 220 | ||
| 221 | polis-style dimensionality reduction across positions. the system collects agree/disagree/pass on statements, finds opinion clusters, surfaces consensus zones and fault lines. | |
| 222 | ||
| 223 | this is aspirational. most communities never reach the scale where this is needed. when they do, they've already built enough governance muscle through phases 1 and 2 to know what they need. | |
| 224 | ||
| 225 | ### the sequence cannot be shortcut | |
| 226 | ||
| 227 | from COMMUNITY-EMERGENCE.md: | |
| 228 | ||
| 229 | > you can't impose governance on a community that hasn't formed identity. you can't form identity around a cluster that hasn't been named. you can't name a cluster that doesn't exist in the data. the pipeline must run in order. | |
| 230 | ||
| 231 | building phase 3 tooling for a phase 1 community is wasted effort. building phase 1 tooling for a pre-community (unnamed MI cluster) is premature. the system should detect which phase a community is in and offer appropriate tools. | |
| 232 | ||
| 233 | in practice: start with chat. add structured proposals when someone asks for them. add consensus-finding when structured proposals get overwhelmed. each upgrade is a `_hs` message type — no protocol changes, no new infrastructure. | |
| 234 | ||
| 235 | ## the recursive loop | |
| 236 | ||
| 237 | governance modifies the graph it governs: | |
| 238 | ||
| 239 | ``` | |
| 240 | social graph → MI detection → communities → governance | |
| 241 | ↑ | | |
| 242 | └──── enforcement modifies graph ────────┘ | |
| 243 | ``` | |
| 244 | ||
| 245 | banning a room from a community changes the listing, which changes what members see, which might change which rooms they join, which changes the MI clusters, which might dissolve or reshape the community. | |
| 246 | ||
| 247 | this is a fixed-point problem. the language pipeline (from SOCIAL-GRAPH-NAVIGATION.md) has the same structure: grammar extracted from corpus determines how new sentences are parsed, which feeds back into the corpus. | |
| 248 | ||
| 249 | ### instability is health | |
| 250 | ||
| 251 | a community in stable equilibrium is dead or authoritarian. a living community's norms shift, people push back, deliberation happens, norms adjust. the social grammar is never final. the parse is always provisional. | |
| 252 | ||
| 253 | the MI clustering will detect when a community's internal structure no longer matches its name — rooms drifting apart, new sub-clusters forming, membership overlap declining. that's a signal to the humans that renegotiation is needed. | |
| 254 | ||
| 255 | ### expect communities to split, merge, dissolve, reform | |
| 256 | ||
| 257 | - **split**: a sub-cluster within "testing" becomes distinct enough to warrant its own community ("integration-testing" vs "unit-testing") | |
| 258 | - **merge**: two communities with 80%+ room overlap realize they're the same community with two names | |
| 259 | - **dissolve**: a community's rooms drift apart until the MI drops below threshold — the name no longer describes reality | |
| 260 | - **reform**: dissolved community members reconverge around a new pattern, nucleate again with a different name | |
| 261 | ||
| 262 | the system surfaces these dynamics. the humans decide what to do. | |
| 263 | ||
| 264 | ## the three sovereignties | |
| 265 | ||
| 266 | every governance decision exists in tension between three sovereignties: | |
| 267 | ||
| 268 | ``` | |
| 269 | protocol sovereignty keet/hypercore determines what's possible | |
| 270 | community sovereignty the named community determines what's acceptable | |
| 271 | individual sovereignty each person determines what's visible | |
| 272 | ``` | |
| 273 | ||
| 274 | ### where they conflict | |
| 275 | ||
| 276 | **at the room boundary (protocol vs community):** | |
| 277 | a community wants to delist a room, but the room admin disagrees. protocol sovereignty says: the room exists independently of any community. community sovereignty says: we decide what's in our directory. resolution: the community can delist, but can't destroy the room. the room continues to exist, just not in that community's listing. | |
| 278 | ||
| 279 | **at the identity layer (community vs individual):** | |
| 280 | the community wants consistent identity for accountability ("same person across rooms"). the individual wants context-dependent presentation ("different persona in different spaces"). resolution: identity linking is opt-in (from ROOMS-ARE-THE-COMPUTER.md §7). the community can require linked identity for membership, but can't force it on non-members. | |
| 281 | ||
| 282 | **at the governance layer (all three):** | |
| 283 | a community's deliberation produces a norm: "no X-type content." protocol provides the enforcement mechanism (room roles, listing management). community decides the norm. individuals decide whether to stay in the community. all three sovereignties are exercised, none is overridden. | |
| 284 | ||
| 285 | ### the tension is the feature | |
| 286 | ||
| 287 | from COMMUNITY-EMERGENCE.md: | |
| 288 | ||
| 289 | > non-trivial cohomology = real social tensions that can't be flattened. don't resolve them. surface them. let communities negotiate. | |
| 290 | ||
| 291 | a system that resolves all three sovereignties into a single hierarchy is authoritarian by design. the productive tension between protocol, community, and individual is what keeps the system healthy. | |
| 292 | ||
| 293 | ## the observation is an intervention | |
| 294 | ||
| 295 | detecting a community changes the community. naming "blacksky" changed how people self-identified and interacted. showing someone their social graph changes their social graph. | |
| 296 | ||
| 297 | the graph navigation system is not a neutral observation tool. it's a social intervention. the choice of what to surface, what to highlight, what to cluster — these are design decisions that shape the social space. | |
| 298 | ||
| 299 | ### design consequences | |
| 300 | ||
| 301 | - be explicit about what the tool shows and why | |
| 302 | - don't pretend algorithmic neutrality | |
| 303 | - surface the choices: "these rooms cluster because they share 70% of members" not just "group 1" | |
| 304 | - let users adjust: threshold sliders, feature weights, view modes | |
| 305 | - the view modes (topics, activity, people, category) are explicitly different lenses, not claims about ground truth | |
| 306 | ||
| 307 | ## connection to existing implementation | |
| 308 | ||
| 309 | ### what exists today (keet-daemon + godot-keet) | |
| 310 | ||
| 311 | - **local MI clustering** in `local-graph.js` — computes clusters from room membership, topics, category, activity, size | |
| 312 | - **view modes** in `group_bar.gd` — groups (manual), people (size quantiles), topics, category, activity | |
| 313 | - **graph visualization** in `graph_canvas.gd` — mindmap layout of clusters and rooms | |
| 314 | - **room metadata** via `_hs:room-meta` pinned messages — topics, category, type | |
| 315 | - **tag normalization** — `_normalizeTag()` strips non-alphanumeric edges, lowercases, so `_testing` and `Testing` cluster together | |
| 316 | ||
| 317 | ### what "groups" view becomes | |
| 318 | ||
| 319 | today: manually created registry rooms (autobase-based, being deprecated). | |
| 320 | tomorrow: named communities — community-rooms with `_hs:room-listing` messages, room roles for governance, chat for meta-discussion. | |
| 321 | ||
| 322 | the transition from "registry autobase" to "community-room" is the same transition described in ROOMS-ARE-THE-COMPUTER.md §Step 4. the community-room IS the registry room, with governance naturally provided by keet-core's role system. | |
| 323 | ||
| 324 | ### the nucleation prompt | |
| 325 | ||
| 326 | the bridge between local MI clustering (bottom-up) and named communities (top-down): | |
| 327 | ||
| 328 | ``` | |
| 329 | 1. local MI detects a high-overlap cluster | |
| 330 | 2. cluster has no matching named community | |
| 331 | 3. UI surfaces: "5 rooms with 80% member overlap — name this group?" | |
| 332 | 4. user names it → community-room created | |
| 333 | 5. user becomes admin → invites mods → governance begins | |
| 334 | ``` | |
| 335 | ||
| 336 | this is layer 2 → layer 4 from AUTO-DISCOVERY.md. the nucleation prompt is the midwife — it surfaces the opportunity, the human decides whether and how to nucleate. | |
| 337 | ||
| 338 | ### the moderation service as a room | |
| 339 | ||
| 340 | from ROOMS-ARE-THE-COMPUTER.md §8: | |
| 341 | ||
| 342 | a moderation service is a room whose messages are labels. subscribing = joining the room. labels are `_hs:mod-label` or `_hs:mod-signal` messages. the client reads labels and applies them according to user preference (off / warn / hide). | |
| 343 | ||
| 344 | multiple moderation services can overlap. users compose them. no central authority. moderation reputation is emergent — good labelers get subscribers, bad ones don't. | |
| 345 | ||
| 346 | ## the view frustum: composable dimension stacking | |
| 347 | ||
| 348 | ### the problem with flat views | |
| 349 | ||
| 350 | the current view modes are mutually exclusive — you pick activity OR people OR category. each gives a single-dimension clustering. but a single dimension is rarely enough to find what you're looking for: | |
| 351 | ||
| 352 | - activity alone splits rooms by message count, but "high activity" contains both dev rooms and social rooms | |
| 353 | - people alone splits by team size, but "small team" contains testing rooms and personal projects | |
| 354 | - category alone splits by topic, but "development" contains both active and dead rooms | |
| 355 | ||
| 356 | you need to stack dimensions: "show me active rooms, then within active, split by category." each added dimension narrows the view further. | |
| 357 | ||
| 358 | ### the frustum model | |
| 359 | ||
| 360 | a frustum is a truncated cone — wide at one end, narrow at the other. each dimension is a cut that narrows the view: | |
| 361 | ||
| 362 | ``` | |
| 363 | all rooms (30) | |
| 364 | └─ activity (primary): 3 clusters | |
| 365 | ├─ high (12 rooms) | |
| 366 | │ └─ category (secondary): 2 sub-clusters | |
| 367 | │ ├─ development (7 rooms) | |
| 368 | │ └─ testing (5 rooms) | |
| 369 | ├─ medium (10 rooms) | |
| 370 | │ └─ category (secondary): 3 sub-clusters | |
| 371 | │ ├─ social (4 rooms) | |
| 372 | │ ├─ development (3 rooms) | |
| 373 | │ └─ infrastructure (3 rooms) | |
| 374 | └─ low (8 rooms) | |
| 375 | └─ category (secondary): 1 cluster | |
| 376 | └─ misc (8 rooms) | |
| 377 | ``` | |
| 378 | ||
| 379 | the ordering matters. "activity → category" gives different results than "category → activity." the user controls the dimension stack — they decide what's primary, what refines. | |
| 380 | ||
| 381 | ### how it differs from combined features | |
| 382 | ||
| 383 | | approach | what it does | | |
| 384 | |----------|-------------| | |
| 385 | | combined MI | `MI = 0.6 * activity_MI + 0.4 * category_MI` → flat clusters, dimensions blended | | |
| 386 | | frustum | cluster by activity first, THEN within each cluster, sub-cluster by category → tree | | |
| 387 | ||
| 388 | combined MI loses the hierarchy — you can't tell whether a cluster formed because of activity similarity or category similarity. the frustum preserves each dimension as a distinct level. | |
| 389 | ||
| 390 | ### recursive sub-clustering algorithm | |
| 391 | ||
| 392 | ``` | |
| 393 | frustum(rooms, [dim1, dim2, dim3, ...], thresholds): | |
| 394 | if no dimensions left OR rooms.length < minSize: | |
| 395 | return leaf node {rooms} | |
| 396 | ||
| 397 | clusters = MI_cluster(rooms, dim1, thresholds[0]) | |
| 398 | ||
| 399 | for each cluster in clusters: | |
| 400 | cluster.children = frustum(cluster.rooms, [dim2, dim3, ...], thresholds[1:]) | |
| 401 | ||
| 402 | return clusters | |
| 403 | ``` | |
| 404 | ||
| 405 | each level: | |
| 406 | 1. extract features for the current dimension (activity, people, category, topics, etc.) | |
| 407 | 2. compute MI between rooms using only that dimension's features | |
| 408 | 3. cluster by MST + threshold cut | |
| 409 | 4. recurse into each cluster with the remaining dimensions | |
| 410 | ||
| 411 | the recursion stops when: | |
| 412 | - no more dimensions in the stack | |
| 413 | - a cluster is too small to sub-divide (< minSize rooms) | |
| 414 | - the sub-clustering produces no meaningful split (all rooms in one sub-cluster) | |
| 415 | ||
| 416 | ### the RPC interface | |
| 417 | ||
| 418 | ```javascript | |
| 419 | // current: single algo with flat features array | |
| 420 | graph.compute({ | |
| 421 | algo: { | |
| 422 | name: "activity", | |
| 423 | features: [{type: "activity", weight: 1.0}], | |
| 424 | threshold: 0.1, | |
| 425 | minSize: 2 | |
| 426 | } | |
| 427 | }) | |
| 428 | ||
| 429 | // frustum: ordered dimension stack | |
| 430 | graph.compute({ | |
| 431 | algo: { | |
| 432 | name: "frustum", | |
| 433 | frustum: [ | |
| 434 | {type: "activity", threshold: 0.1}, | |
| 435 | {type: "category", threshold: 0.1}, | |
| 436 | {type: "people", threshold: 0.1} | |
| 437 | ], | |
| 438 | minSize: 2 | |
| 439 | } | |
| 440 | }) | |
| 441 | ``` | |
| 442 | ||
| 443 | the `frustum` array replaces the `features` array. order matters — first element is primary dimension. each dimension can have its own threshold (coarser primary cuts, finer secondary cuts). | |
| 444 | ||
| 445 | ### the response: tree, not flat list | |
| 446 | ||
| 447 | ```javascript | |
| 448 | // current: flat cluster array | |
| 449 | { | |
| 450 | clusters: [ | |
| 451 | {id: 0, rooms: ["a", "b", "c"], label: "testing"}, | |
| 452 | {id: 1, rooms: ["d", "e"], label: "development"} | |
| 453 | ] | |
| 454 | } | |
| 455 | ||
| 456 | // frustum: nested tree | |
| 457 | { | |
| 458 | clusters: [ | |
| 459 | { | |
| 460 | id: 0, | |
| 461 | dimension: "activity", | |
| 462 | label: "5-12msgs", | |
| 463 | rooms: ["a", "b", "c", "d", "e"], | |
| 464 | children: [ | |
| 465 | { | |
| 466 | id: "0.0", | |
| 467 | dimension: "category", | |
| 468 | label: "testing", | |
| 469 | rooms: ["a", "b", "c"], | |
| 470 | children: [] | |
| 471 | }, | |
| 472 | { | |
| 473 | id: "0.1", | |
| 474 | dimension: "category", | |
| 475 | label: "development", | |
| 476 | rooms: ["d", "e"], | |
| 477 | children: [] | |
| 478 | } | |
| 479 | ] | |
| 480 | }, | |
| 481 | { | |
| 482 | id: 1, | |
| 483 | dimension: "activity", | |
| 484 | label: "50-100msgs", | |
| 485 | rooms: ["f", "g", "h"], | |
| 486 | children: [ | |
| 487 | { | |
| 488 | id: "1.0", | |
| 489 | dimension: "category", | |
| 490 | label: "social", | |
| 491 | rooms: ["f", "g", "h"], | |
| 492 | children: [] | |
| 493 | } | |
| 494 | ] | |
| 495 | } | |
| 496 | ] | |
| 497 | } | |
| 498 | ``` | |
| 499 | ||
| 500 | leaf clusters (children = []) are the actual groups shown in the UI. parent clusters provide the hierarchy for navigation and labeling. | |
| 501 | ||
| 502 | ### the UI: dimension chip stack | |
| 503 | ||
| 504 | instead of a single dropdown, the group bar header gets a stack of dimension chips: | |
| 505 | ||
| 506 | ``` | |
| 507 | [ Activity ▼ ] [ + Category ▼ ] [ + ] | |
| 508 | ``` | |
| 509 | ||
| 510 | - each chip is an OptionButton with the available dimensions | |
| 511 | - the `[+]` button adds another dimension to the stack | |
| 512 | - removing a chip (click X) collapses the frustum at that level | |
| 513 | - reordering chips (drag) changes the hierarchy | |
| 514 | - the group bar shows the tree: primary clusters as top-level icons, with expandable sub-groups | |
| 515 | ||
| 516 | ### group bar rendering | |
| 517 | ||
| 518 | ``` | |
| 519 | ┌─────────┐ | |
| 520 | │ ⌂ Home │ ← all rooms | |
| 521 | ├─────────┤ | |
| 522 | │ 5-12msg │ ← primary: activity cluster (collapsed) | |
| 523 | │ testing│ ← secondary: category sub-cluster | |
| 524 | │ dev │ ← secondary: category sub-cluster | |
| 525 | ├─────────┤ | |
| 526 | │ 50-100 │ ← primary: activity cluster (collapsed) | |
| 527 | │ social │ ← secondary: category sub-cluster | |
| 528 | ├─────────┤ | |
| 529 | │ empty │ ← primary: activity cluster (no sub-clusters) | |
| 530 | └─────────┘ | |
| 531 | ``` | |
| 532 | ||
| 533 | clicking a primary cluster icon selects all rooms in it (including all sub-clusters). clicking a secondary icon selects only that sub-cluster's rooms. the indentation communicates hierarchy. | |
| 534 | ||
| 535 | ### backwards compatibility | |
| 536 | ||
| 537 | - single-dimension algos still work exactly as today (frustum with 1 element = flat clustering) | |
| 538 | - the `features` array in algo config continues to work for the existing combined-MI mode | |
| 539 | - `frustum` array is a new alternative — if present, it takes precedence over `features` | |
| 540 | - the response includes a `tree` flag so the client knows whether to render flat or hierarchical | |
| 541 | ||
| 542 | ### implementation sketch: local-graph.js | |
| 543 | ||
| 544 | ```javascript | |
| 545 | async _runFrustum (roomIds, algo, dmSharedRooms) { | |
| 546 | const dims = algo.frustum | |
| 547 | const minSize = algo.minSize || 2 | |
| 548 | ||
| 549 | const tree = await this._frustumRecurse(roomIds, dims, 0, minSize) | |
| 550 | await this._labelFrustumTree(tree) | |
| 551 | ||
| 552 | // flatten leaf clusters for backwards-compat edges | |
| 553 | const leaves = this._collectLeaves(tree) | |
| 554 | const suggestions = this._buildSuggestions( | |
| 555 | await this._extractFeatures(roomIds, [{type: dims[0].type, weight: 1.0}]), | |
| 556 | dmSharedRooms | |
| 557 | ) | |
| 558 | ||
| 559 | return { clusters: tree, edges: [], suggestions, algo: algo.name, tree: true } | |
| 560 | } | |
| 561 | ||
| 562 | async _frustumRecurse (roomIds, dims, depth, minSize) { | |
| 563 | if (dims.length === 0 || roomIds.length < minSize) { | |
| 564 | return [] // leaf — rooms returned as-is to parent | |
| 565 | } | |
| 566 | ||
| 567 | const dim = dims[0] | |
| 568 | const remaining = dims.slice(1) | |
| 569 | ||
| 570 | // cluster by this dimension | |
| 571 | const features = await this._extractFeatures(roomIds, [{type: dim.type, weight: 1.0}]) | |
| 572 | const index = this._buildInvertedIndex(features) | |
| 573 | const { pairs, totalPairs } = this._computePairCounts(index) | |
| 574 | const edges = this._computeMI(pairs, features, totalPairs) | |
| 575 | const clusters = this._extractClusters(edges, roomIds, dim.threshold, minSize) | |
| 576 | ||
| 577 | // collect unclustered rooms | |
| 578 | const clustered = new Set() | |
| 579 | for (const c of clusters) for (const r of c.rooms) clustered.add(r) | |
| 580 | const unclustered = roomIds.filter(r => !clustered.has(r)) | |
| 581 | ||
| 582 | const result = [] | |
| 583 | for (let i = 0; i < clusters.length; i++) { | |
| 584 | const c = clusters[i] | |
| 585 | const children = await this._frustumRecurse(c.rooms, remaining, depth + 1, minSize) | |
| 586 | result.push({ | |
| 587 | id: c.id, | |
| 588 | dimension: dim.type, | |
| 589 | label: '', // filled by _labelFrustumTree | |
| 590 | rooms: c.rooms, | |
| 591 | children | |
| 592 | }) | |
| 593 | } | |
| 594 | ||
| 595 | // unclustered rooms become a catch-all node (no sub-clustering) | |
| 596 | if (unclustered.length > 0) { | |
| 597 | result.push({ | |
| 598 | id: 'other', | |
| 599 | dimension: dim.type, | |
| 600 | label: 'other', | |
| 601 | rooms: unclustered, | |
| 602 | children: [] | |
| 603 | }) | |
| 604 | } | |
| 605 | ||
| 606 | return result | |
| 607 | } | |
| 608 | ``` | |
| 609 | ||
| 610 | ### implementation sketch: group_bar.gd | |
| 611 | ||
| 612 | ```gdscript | |
| 613 | # dimension stack replaces single VIEW_MODES selector | |
| 614 | var _frustum_stack: Array[String] = [] # ["activity", "category"] | |
| 615 | ||
| 616 | const DIMENSIONS: Array[Dictionary] = [ | |
| 617 | {"type": "activity", "label": "Activity", "threshold": 0.1}, | |
| 618 | {"type": "category", "label": "Category", "threshold": 0.1}, | |
| 619 | {"type": "topics", "label": "Topics", "threshold": 0.3}, | |
| 620 | {"type": "size", "label": "People", "threshold": 0.1}, | |
| 621 | {"type": "creator", "label": "Creator", "threshold": 0.1}, | |
| 622 | ] | |
| 623 | ||
| 624 | func _build_frustum_algo() -> Dictionary: | |
| 625 | if _frustum_stack.is_empty(): | |
| 626 | return {} # groups mode — no clustering | |
| 627 | var frustum: Array = [] | |
| 628 | for dim_type: String in _frustum_stack: | |
| 629 | for d: Dictionary in DIMENSIONS: | |
| 630 | if d["type"] == dim_type: | |
| 631 | frustum.append({"type": d["type"], "threshold": d["threshold"]}) | |
| 632 | break | |
| 633 | return {"name": "frustum", "frustum": frustum, "minSize": 2} | |
| 634 | ||
| 635 | func _render_tree(clusters: Array, indent: int = 0) -> void: | |
| 636 | for cluster: Dictionary in clusters: | |
| 637 | var icon := _create_group_icon(cluster["id"], cluster["label"], | |
| 638 | cluster["dimension"], "", false) | |
| 639 | icon.custom_minimum_size.x = 60 - (indent * 4) # visual indent | |
| 640 | group_icons.add_child(icon) | |
| 641 | # recurse into children | |
| 642 | if cluster.has("children"): | |
| 643 | _render_tree(cluster["children"], indent + 1) | |
| 644 | ``` | |
| 645 | ||
| 646 | ## design principles (from this document) | |
| 647 | ||
| 648 | ``` | |
| 649 | 1. CONTEXT OVER RULES | |
| 650 | moderators share understanding, not policy documents. | |
| 651 | moderation context doesn't serialize across communities. | |
| 652 | ||
| 653 | 2. LISTINGS, NOT CONTENT | |
| 654 | community governance controls the directory. | |
| 655 | room governance controls the room. never conflate them. | |
| 656 | ||
| 657 | 3. SIGNAL, NOT ENFORCEMENT | |
| 658 | moderation crosses community boundaries as information. | |
| 659 | the receiving community decides what to do with the signal. | |
| 660 | ||
| 661 | 4. CONSENSUS, NOT MAJORITY | |
| 662 | surface the topology of opinion rather than flattening | |
| 663 | it into a binary. minorities are visible, not outvoted. | |
| 664 | ||
| 665 | 5. PHASE-APPROPRIATE TOOLING | |
| 666 | chat for small communities. structured proposals for growing | |
| 667 | ones. consensus-finding for large ones. don't pre-install. | |
| 668 | ||
| 669 | 6. THE ACCOUNTABILITY GAP IS INTENTIONAL | |
| 670 | community admins can't reach inside rooms. room admins | |
| 671 | can't force listing in communities. the gap prevents surveillance. | |
| 672 | ||
| 673 | 7. EXPECT LIFECYCLE | |
| 674 | communities split, merge, dissolve, reform. the system | |
| 675 | surfaces these dynamics. the humans decide what to do. | |
| 676 | ||
| 677 | 8. THREE SOVEREIGNTIES IN TENSION | |
| 678 | protocol, community, individual. the tension is the feature. | |
| 679 | resolving it into hierarchy is authoritarian by design. | |
| 680 | ``` | |
| 681 | ||
| 682 | ## connection to other documents | |
| 683 | ||
| 684 | - [COMMUNITY-EMERGENCE.md](COMMUNITY-EMERGENCE.md) — how communities form: phase transitions, nucleation, blacksky case study | |
| 685 | - [SOCIAL-GRAPH-NAVIGATION.md](SOCIAL-GRAPH-NAVIGATION.md) — the math: MI, MST, sheaf theory, connector profiles | |
| 686 | - [ROOMS-ARE-THE-COMPUTER.md](ROOMS-ARE-THE-COMPUTER.md) — the implementation: rooms as only data structure, `_hs` conventions, migration path | |
| 687 | - [AUTO-DISCOVERY.md](AUTO-DISCOVERY.md) — the five discovery layers: DHT → room-carried metadata → MI clustering → gossip → collaborative auto-registry | |
| 688 | ||
| 689 | ## appendix: frustum dimension types | |
| 690 | ||
| 691 | | dimension | what it measures | typical threshold | bucket method | | |
| 692 | |-----------|-----------------|-------------------|---------------| | |
| 693 | | activity | recent message count | 0.1 | quantile (6 buckets, descriptive labels like `act:5-12msgs`) | | |
| 694 | | category | MetadataBase category tag | 0.1 | exact match (normalized: `_testing` → `testing`) | | |
| 695 | | topics | MetadataBase topics array | 0.3 | set overlap (room can have multiple topics) | | |
| 696 | | size / people | member count | 0.1 | quantile (6 buckets, labels like `sz:3-5ppl`) | | |
| 697 | | creator | room admin identity | 0.1 | exact match on admin memberId | | |
| 698 | | members | membership overlap | 0.5 | set overlap (excludes local user) | | |
| 699 | ||
| 700 | dimensions with quantile bucketing (activity, size) produce equal-sized groups from the data distribution. dimensions with exact/set matching (category, topics, members) produce groups from shared metadata. the frustum can stack any combination. |
| … | … | @@ -0,0 +1,32 @@ |
| 1 | #!/usr/bin/env bare | |
| 2 | ||
| 3 | /** | |
| 4 | * hyper-social-web — POSSE Social Network Client | |
| 5 | * | |
| 6 | * Unified timeline and cross-posting for Mastodon, Bluesky, and Instagram. | |
| 7 | * Publish once, syndicate everywhere. | |
| 8 | */ | |
| 9 | ||
| 10 | await import('bare-node-runtime/global') | |
| 11 | ||
| 12 | import { createServer } from './lib/server.js' | |
| 13 | ||
| 14 | const args = process.argv.slice(2) | |
| 15 | ||
| 16 | const cmd = args[0] || 'serve' | |
| 17 | ||
| 18 | if (cmd === 'serve') { | |
| 19 | const portIdx = args.indexOf('--port') | |
| 20 | const port = portIdx !== -1 ? Number(args[portIdx + 1]) : 5050 | |
| 21 | ||
| 22 | const prefixIdx = args.indexOf('--prefix') | |
| 23 | const prefix = prefixIdx !== -1 ? args[prefixIdx + 1] : '' | |
| 24 | ||
| 25 | const storageIdx = args.indexOf('--storage') | |
| 26 | const storage = storageIdx !== -1 ? args[storageIdx + 1] : null | |
| 27 | ||
| 28 | const server = await createServer({ port, prefix, storage }) | |
| 29 | console.log(`[hyper-social-web] v0.1.0 listening on http://localhost:${port}${prefix}/`) | |
| 30 | } else { | |
| 31 | console.log('Usage: bare index.js serve [--port 5050] [--prefix /social] [--storage ./data]') | |
| 32 | } |
| … | … | @@ -0,0 +1,277 @@ |
| 1 | import https from 'bare-https' | |
| 2 | ||
| 3 | /** | |
| 4 | * Minimal AT Protocol client using raw XRPC HTTP calls. | |
| 5 | * No SDK dependency — just fetch + JSON + JWT. | |
| 6 | */ | |
| 7 | class AtprotoClient { | |
| 8 | constructor (opts = {}) { | |
| 9 | this.service = opts.service || 'https://bsky.social' | |
| 10 | this.identifier = opts.identifier // handle or DID | |
| 11 | this.password = opts.password // app password | |
| 12 | ||
| 13 | // Session state | |
| 14 | this.accessJwt = null | |
| 15 | this.refreshJwt = null | |
| 16 | this.did = null | |
| 17 | this.handle = null | |
| 18 | ||
| 19 | // Parse hostname from service URL | |
| 20 | const url = new URL(this.service) | |
| 21 | this.hostname = url.hostname | |
| 22 | this.basePath = url.pathname === '/' ? '' : url.pathname | |
| 23 | } | |
| 24 | ||
| 25 | _request (method, nsid, params, body) { | |
| 26 | return new Promise((resolve, reject) => { | |
| 27 | let path = `${this.basePath}/xrpc/${nsid}` | |
| 28 | ||
| 29 | // Query params for GET | |
| 30 | if (params && method === 'GET') { | |
| 31 | const qs = new URLSearchParams() | |
| 32 | for (const [k, v] of Object.entries(params)) { | |
| 33 | if (v !== undefined && v !== null) qs.set(k, String(v)) | |
| 34 | } | |
| 35 | const str = qs.toString() | |
| 36 | if (str) path += '?' + str | |
| 37 | } | |
| 38 | ||
| 39 | const opts = { | |
| 40 | hostname: this.hostname, | |
| 41 | port: 443, | |
| 42 | path, | |
| 43 | method, | |
| 44 | headers: { Accept: 'application/json' } | |
| 45 | } | |
| 46 | ||
| 47 | if (this.accessJwt) { | |
| 48 | opts.headers.Authorization = `Bearer ${this.accessJwt}` | |
| 49 | } | |
| 50 | ||
| 51 | let payload = '' | |
| 52 | if (body && method === 'POST') { | |
| 53 | payload = JSON.stringify(body) | |
| 54 | opts.headers['Content-Type'] = 'application/json' | |
| 55 | opts.headers['Content-Length'] = Buffer.byteLength(payload) | |
| 56 | } | |
| 57 | ||
| 58 | const req = https.request(opts, (res) => { | |
| 59 | let data = '' | |
| 60 | res.on('data', (chunk) => { data += chunk.toString() }) | |
| 61 | res.on('end', () => { | |
| 62 | if (res.statusCode >= 400) { | |
| 63 | // Check if token expired | |
| 64 | if (res.statusCode === 401 && this.refreshJwt) { | |
| 65 | return this._refreshSession() | |
| 66 | .then(() => this._request(method, nsid, params, body)) | |
| 67 | .then(resolve) | |
| 68 | .catch(reject) | |
| 69 | } | |
| 70 | return reject(new Error(`XRPC ${res.statusCode}: ${data.slice(0, 300)}`)) | |
| 71 | } | |
| 72 | try { resolve(JSON.parse(data)) } catch { resolve(data) } | |
| 73 | }) | |
| 74 | }) | |
| 75 | ||
| 76 | req.on('error', reject) | |
| 77 | req.setTimeout(15000, () => req.destroy(new Error('Request timeout'))) | |
| 78 | if (payload) req.write(payload) | |
| 79 | req.end() | |
| 80 | }) | |
| 81 | } | |
| 82 | ||
| 83 | // --- Session management --- | |
| 84 | ||
| 85 | async login () { | |
| 86 | const res = await this._requestNoAuth('POST', 'com.atproto.server.createSession', null, { | |
| 87 | identifier: this.identifier, | |
| 88 | password: this.password | |
| 89 | }) | |
| 90 | this.accessJwt = res.accessJwt | |
| 91 | this.refreshJwt = res.refreshJwt | |
| 92 | this.did = res.did | |
| 93 | this.handle = res.handle | |
| 94 | return res | |
| 95 | } | |
| 96 | ||
| 97 | async _refreshSession () { | |
| 98 | const oldRefresh = this.refreshJwt | |
| 99 | this.accessJwt = null | |
| 100 | ||
| 101 | const res = await this._requestWithToken('POST', 'com.atproto.server.refreshSession', null, null, oldRefresh) | |
| 102 | this.accessJwt = res.accessJwt | |
| 103 | this.refreshJwt = res.refreshJwt | |
| 104 | return res | |
| 105 | } | |
| 106 | ||
| 107 | _requestNoAuth (method, nsid, params, body) { | |
| 108 | const saved = this.accessJwt | |
| 109 | this.accessJwt = null | |
| 110 | return this._request(method, nsid, params, body).finally(() => { | |
| 111 | this.accessJwt = saved | |
| 112 | }) | |
| 113 | } | |
| 114 | ||
| 115 | _requestWithToken (method, nsid, params, body, token) { | |
| 116 | const saved = this.accessJwt | |
| 117 | this.accessJwt = token | |
| 118 | return this._request(method, nsid, params, body).finally(() => { | |
| 119 | this.accessJwt = saved | |
| 120 | }) | |
| 121 | } | |
| 122 | ||
| 123 | // --- Feed reading --- | |
| 124 | ||
| 125 | async getTimeline (limit = 30, cursor) { | |
| 126 | const res = await this._request('GET', 'app.bsky.feed.getTimeline', { limit, cursor }) | |
| 127 | return res.feed || [] | |
| 128 | } | |
| 129 | ||
| 130 | async getAuthorFeed (actor, limit = 30, cursor) { | |
| 131 | const res = await this._request('GET', 'app.bsky.feed.getAuthorFeed', { | |
| 132 | actor: actor || this.did, | |
| 133 | limit, | |
| 134 | cursor | |
| 135 | }) | |
| 136 | return res.feed || [] | |
| 137 | } | |
| 138 | ||
| 139 | async getProfile (actor) { | |
| 140 | return this._request('GET', 'app.bsky.actor.getProfile', { | |
| 141 | actor: actor || this.did | |
| 142 | }) | |
| 143 | } | |
| 144 | ||
| 145 | // --- Blob upload --- | |
| 146 | ||
| 147 | async uploadBlob (buffer, mimeType) { | |
| 148 | return new Promise((resolve, reject) => { | |
| 149 | const req = https.request({ | |
| 150 | hostname: this.hostname, | |
| 151 | port: 443, | |
| 152 | path: `${this.basePath}/xrpc/com.atproto.repo.uploadBlob`, | |
| 153 | method: 'POST', | |
| 154 | headers: { | |
| 155 | Authorization: `Bearer ${this.accessJwt}`, | |
| 156 | 'Content-Type': mimeType, | |
| 157 | 'Content-Length': buffer.length | |
| 158 | } | |
| 159 | }, (res) => { | |
| 160 | let data = '' | |
| 161 | res.on('data', (chunk) => { data += chunk.toString() }) | |
| 162 | res.on('end', () => { | |
| 163 | if (res.statusCode >= 400) { | |
| 164 | return reject(new Error(`Upload ${res.statusCode}: ${data.slice(0, 300)}`)) | |
| 165 | } | |
| 166 | try { resolve(JSON.parse(data)) } catch { reject(new Error('Invalid JSON')) } | |
| 167 | }) | |
| 168 | }) | |
| 169 | ||
| 170 | req.on('error', reject) | |
| 171 | req.setTimeout(60000, () => req.destroy(new Error('Upload timeout'))) | |
| 172 | req.write(buffer) | |
| 173 | req.end() | |
| 174 | }) | |
| 175 | } | |
| 176 | ||
| 177 | // --- Posting --- | |
| 178 | ||
| 179 | async post (text, opts = {}) { | |
| 180 | const now = new Date().toISOString() | |
| 181 | ||
| 182 | const record = { | |
| 183 | $type: 'app.bsky.feed.post', | |
| 184 | text, | |
| 185 | createdAt: now | |
| 186 | } | |
| 187 | ||
| 188 | // Facets (mentions, links, hashtags) — auto-detect | |
| 189 | const facets = detectFacets(text) | |
| 190 | if (facets.length > 0) record.facets = facets | |
| 191 | ||
| 192 | // Reply | |
| 193 | if (opts.replyTo) { | |
| 194 | record.reply = opts.replyTo | |
| 195 | } | |
| 196 | ||
| 197 | // Image embed | |
| 198 | if (opts.images && opts.images.length > 0) { | |
| 199 | record.embed = { | |
| 200 | $type: 'app.bsky.embed.images', | |
| 201 | images: opts.images.map(img => ({ | |
| 202 | alt: img.alt || '', | |
| 203 | image: img.blob, // { $type: 'blob', ref: { $link: cid }, mimeType, size } | |
| 204 | aspectRatio: img.width && img.height ? { width: img.width, height: img.height } : undefined | |
| 205 | })) | |
| 206 | } | |
| 207 | } | |
| 208 | ||
| 209 | return this._request('POST', 'com.atproto.repo.createRecord', null, { | |
| 210 | repo: this.did, | |
| 211 | collection: 'app.bsky.feed.post', | |
| 212 | record | |
| 213 | }) | |
| 214 | } | |
| 215 | ||
| 216 | // --- Notifications --- | |
| 217 | ||
| 218 | async getNotifications (limit = 20, cursor) { | |
| 219 | const res = await this._request('GET', 'app.bsky.notification.listNotifications', { limit, cursor }) | |
| 220 | return res.notifications || [] | |
| 221 | } | |
| 222 | ||
| 223 | // --- Search --- | |
| 224 | ||
| 225 | async searchPosts (query, limit = 25) { | |
| 226 | const res = await this._request('GET', 'app.bsky.feed.searchPosts', { q: query, limit }) | |
| 227 | return res.posts || [] | |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | /** | |
| 232 | * Auto-detect facets (links, mentions, hashtags) in text. | |
| 233 | * Returns AT Proto facet objects with byte offsets. | |
| 234 | */ | |
| 235 | function detectFacets (text) { | |
| 236 | const facets = [] | |
| 237 | const encoder = new TextEncoder() | |
| 238 | const bytes = encoder.encode(text) | |
| 239 | ||
| 240 | // URLs | |
| 241 | const urlRegex = /https?:\/\/[^\s<>)"']+/g | |
| 242 | let match | |
| 243 | while ((match = urlRegex.exec(text)) !== null) { | |
| 244 | const before = encoder.encode(text.slice(0, match.index)) | |
| 245 | const target = encoder.encode(match[0]) | |
| 246 | facets.push({ | |
| 247 | index: { byteStart: before.length, byteEnd: before.length + target.length }, | |
| 248 | features: [{ $type: 'app.bsky.richtext.facet#link', uri: match[0] }] | |
| 249 | }) | |
| 250 | } | |
| 251 | ||
| 252 | // Mentions (@handle.domain) | |
| 253 | const mentionRegex = /@([a-zA-Z0-9._-]+\.[a-zA-Z]{2,})/g | |
| 254 | while ((match = mentionRegex.exec(text)) !== null) { | |
| 255 | const before = encoder.encode(text.slice(0, match.index)) | |
| 256 | const target = encoder.encode(match[0]) | |
| 257 | facets.push({ | |
| 258 | index: { byteStart: before.length, byteEnd: before.length + target.length }, | |
| 259 | features: [{ $type: 'app.bsky.richtext.facet#mention', did: '' }] // DID resolved by AppView | |
| 260 | }) | |
| 261 | } | |
| 262 | ||
| 263 | // Hashtags (#tag) | |
| 264 | const tagRegex = /#([a-zA-Z0-9_]+)/g | |
| 265 | while ((match = tagRegex.exec(text)) !== null) { | |
| 266 | const before = encoder.encode(text.slice(0, match.index)) | |
| 267 | const target = encoder.encode(match[0]) | |
| 268 | facets.push({ | |
| 269 | index: { byteStart: before.length, byteEnd: before.length + target.length }, | |
| 270 | features: [{ $type: 'app.bsky.richtext.facet#tag', tag: match[1] }] | |
| 271 | }) | |
| 272 | } | |
| 273 | ||
| 274 | return facets | |
| 275 | } | |
| 276 | ||
| 277 | export { AtprotoClient, detectFacets } |
| … | … | @@ -0,0 +1,123 @@ |
| 1 | import https from 'bare-https' | |
| 2 | ||
| 3 | class MastodonClient { | |
| 4 | constructor (opts = {}) { | |
| 5 | this.instance = opts.instance | |
| 6 | this.token = opts.token | |
| 7 | } | |
| 8 | ||
| 9 | _request (method, path, body) { | |
| 10 | return new Promise((resolve, reject) => { | |
| 11 | const opts = { | |
| 12 | hostname: this.instance, | |
| 13 | port: 443, | |
| 14 | path, | |
| 15 | method, | |
| 16 | headers: { | |
| 17 | Authorization: `Bearer ${this.token}`, | |
| 18 | Accept: 'application/json' | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | let payload = '' | |
| 23 | if (body) { | |
| 24 | payload = JSON.stringify(body) | |
| 25 | opts.headers['Content-Type'] = 'application/json' | |
| 26 | opts.headers['Content-Length'] = Buffer.byteLength(payload) | |
| 27 | } | |
| 28 | ||
| 29 | const req = https.request(opts, (res) => { | |
| 30 | let data = '' | |
| 31 | res.on('data', (chunk) => { data += chunk.toString() }) | |
| 32 | res.on('end', () => { | |
| 33 | if (res.statusCode >= 400) { | |
| 34 | return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)) | |
| 35 | } | |
| 36 | try { resolve(JSON.parse(data)) } catch { reject(new Error('Invalid JSON')) } | |
| 37 | }) | |
| 38 | }) | |
| 39 | ||
| 40 | req.on('error', reject) | |
| 41 | req.setTimeout(15000, () => req.destroy(new Error('Request timeout'))) | |
| 42 | if (payload) req.write(payload) | |
| 43 | req.end() | |
| 44 | }) | |
| 45 | } | |
| 46 | ||
| 47 | async postStatus (text, opts = {}) { | |
| 48 | const body = { status: text } | |
| 49 | if (opts.visibility) body.visibility = opts.visibility | |
| 50 | if (opts.spoilerText) body.spoiler_text = opts.spoilerText | |
| 51 | if (opts.inReplyTo) body.in_reply_to_id = opts.inReplyTo | |
| 52 | if (opts.mediaIds && opts.mediaIds.length) body.media_ids = opts.mediaIds | |
| 53 | return this._request('POST', '/api/v1/statuses', body) | |
| 54 | } | |
| 55 | ||
| 56 | // Upload media via multipart/form-data | |
| 57 | async uploadMedia (buffer, mimeType, description) { | |
| 58 | return new Promise((resolve, reject) => { | |
| 59 | const boundary = '----HyperSocial' + Date.now().toString(36) | |
| 60 | const parts = [] | |
| 61 | ||
| 62 | // File part | |
| 63 | parts.push(`--${boundary}\r\n`) | |
| 64 | parts.push(`Content-Disposition: form-data; name="file"; filename="media.${mimeType.split('/')[1] || 'jpg'}"\r\n`) | |
| 65 | parts.push(`Content-Type: ${mimeType}\r\n\r\n`) | |
| 66 | const header = Buffer.from(parts.join('')) | |
| 67 | ||
| 68 | const footer = Buffer.from(`\r\n`) | |
| 69 | let descPart = Buffer.alloc(0) | |
| 70 | if (description) { | |
| 71 | descPart = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="description"\r\n\r\n${description}\r\n`) | |
| 72 | } | |
| 73 | const end = Buffer.from(`--${boundary}--\r\n`) | |
| 74 | ||
| 75 | const payload = Buffer.concat([header, buffer, footer, descPart, end]) | |
| 76 | ||
| 77 | const req = https.request({ | |
| 78 | hostname: this.instance, | |
| 79 | port: 443, | |
| 80 | path: '/api/v2/media', | |
| 81 | method: 'POST', | |
| 82 | headers: { | |
| 83 | Authorization: `Bearer ${this.token}`, | |
| 84 | Accept: 'application/json', | |
| 85 | 'Content-Type': `multipart/form-data; boundary=${boundary}`, | |
| 86 | 'Content-Length': payload.length | |
| 87 | } | |
| 88 | }, (res) => { | |
| 89 | let data = '' | |
| 90 | res.on('data', (chunk) => { data += chunk.toString() }) | |
| 91 | res.on('end', () => { | |
| 92 | if (res.statusCode >= 400) { | |
| 93 | return reject(new Error(`Upload ${res.statusCode}: ${data.slice(0, 200)}`)) | |
| 94 | } | |
| 95 | try { resolve(JSON.parse(data)) } catch { reject(new Error('Invalid JSON')) } | |
| 96 | }) | |
| 97 | }) | |
| 98 | ||
| 99 | req.on('error', reject) | |
| 100 | req.setTimeout(60000, () => req.destroy(new Error('Upload timeout'))) | |
| 101 | req.write(payload) | |
| 102 | req.end() | |
| 103 | }) | |
| 104 | } | |
| 105 | ||
| 106 | async homeTimeline (limit = 20, sinceId) { | |
| 107 | let path = `/api/v1/timelines/home?limit=${limit}` | |
| 108 | if (sinceId) path += `&since_id=${sinceId}` | |
| 109 | return this._request('GET', path) | |
| 110 | } | |
| 111 | ||
| 112 | async notifications (limit = 15, sinceId) { | |
| 113 | let path = `/api/v1/notifications?limit=${limit}` | |
| 114 | if (sinceId) path += `&since_id=${sinceId}` | |
| 115 | return this._request('GET', path) | |
| 116 | } | |
| 117 | ||
| 118 | async verifyCredentials () { | |
| 119 | return this._request('GET', '/api/v1/accounts/verify_credentials') | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | export { MastodonClient } |
| … | … | @@ -0,0 +1,84 @@ |
| 1 | import { randomBytes } from 'bare-crypto' | |
| 2 | ||
| 3 | const SOURCES = ['mastodon', 'atproto', 'instagram', 'local'] | |
| 4 | ||
| 5 | class FeedStore { | |
| 6 | constructor (bee) { | |
| 7 | this.bee = bee | |
| 8 | } | |
| 9 | ||
| 10 | _genId () { | |
| 11 | return randomBytes(8).toString('hex') | |
| 12 | } | |
| 13 | ||
| 14 | _padTimestamp (ts) { | |
| 15 | return String(ts).padStart(16, '0') | |
| 16 | } | |
| 17 | ||
| 18 | async add (opts = {}) { | |
| 19 | const id = opts.id || this._genId() | |
| 20 | const now = Date.now() | |
| 21 | ||
| 22 | const item = { | |
| 23 | id, | |
| 24 | source: opts.source || 'local', | |
| 25 | author: opts.author || 'unknown', | |
| 26 | authorId: opts.authorId || opts.author || 'unknown', | |
| 27 | text: opts.text || '', | |
| 28 | timestamp: opts.timestamp || now, | |
| 29 | type: opts.type || 'post', | |
| 30 | tags: opts.tags || [], | |
| 31 | url: opts.url || null, | |
| 32 | mediaUrl: opts.mediaUrl || null, | |
| 33 | mediaType: opts.mediaType || null, | |
| 34 | metadata: opts.metadata || {}, | |
| 35 | syndicated: opts.syndicated || null | |
| 36 | } | |
| 37 | ||
| 38 | const batch = this.bee.batch() | |
| 39 | await batch.put(`item/${id}`, item) | |
| 40 | await batch.put(`timeline/${this._padTimestamp(item.timestamp)}/${id}`, { id }) | |
| 41 | await batch.put(`source/${item.source}/${this._padTimestamp(item.timestamp)}/${id}`, { id }) | |
| 42 | await batch.put(`author/${item.authorId}/${this._padTimestamp(item.timestamp)}/${id}`, { id }) | |
| 43 | await batch.flush() | |
| 44 | ||
| 45 | return item | |
| 46 | } | |
| 47 | ||
| 48 | async get (id) { | |
| 49 | const node = await this.bee.get(`item/${id}`) | |
| 50 | return node ? node.value : null | |
| 51 | } | |
| 52 | ||
| 53 | async timeline (opts = {}) { | |
| 54 | const { limit = 50, before, after, source } = opts | |
| 55 | const items = [] | |
| 56 | ||
| 57 | const rangeOpts = {} | |
| 58 | if (source) { | |
| 59 | rangeOpts.gte = `source/${source}/` | |
| 60 | rangeOpts.lte = `source/${source}/~` | |
| 61 | if (before) rangeOpts.lt = `source/${source}/${this._padTimestamp(before)}/` | |
| 62 | } else { | |
| 63 | rangeOpts.gte = 'timeline/' | |
| 64 | rangeOpts.lte = 'timeline/~' | |
| 65 | if (before) rangeOpts.lt = `timeline/${this._padTimestamp(before)}/` | |
| 66 | if (after) rangeOpts.gt = `timeline/${this._padTimestamp(after)}/~` | |
| 67 | } | |
| 68 | ||
| 69 | // Read in reverse (newest first) | |
| 70 | rangeOpts.reverse = true | |
| 71 | ||
| 72 | for await (const node of this.bee.createReadStream(rangeOpts)) { | |
| 73 | if (!node.value) continue | |
| 74 | const item = await this.get(node.value.id) | |
| 75 | if (!item) continue | |
| 76 | items.push(item) | |
| 77 | if (items.length >= limit) break | |
| 78 | } | |
| 79 | ||
| 80 | return items | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | export { FeedStore, SOURCES } |
| … | … | @@ -0,0 +1,234 @@ |
| 1 | import { spawn } from 'bare-subprocess' | |
| 2 | import fs from 'bare-fs' | |
| 3 | import path from 'bare-path' | |
| 4 | import os from 'bare-os' | |
| 5 | import { randomBytes } from 'bare-crypto' | |
| 6 | import FFmpeg from 'bare-ffmpeg' | |
| 7 | ||
| 8 | const TEMP_DIR = path.join(os.tmpdir(), 'hyper-social-media') | |
| 9 | try { fs.mkdirSync(TEMP_DIR, { recursive: true }) } catch {} | |
| 10 | ||
| 11 | /** | |
| 12 | * Process an image for cross-platform POSSE publishing. | |
| 13 | * | |
| 14 | * - Resize to max 2048px on longest side | |
| 15 | * - Compress to JPEG 80% quality | |
| 16 | * - Strip EXIF/metadata (privacy) | |
| 17 | * - Target: under 1MB (Bluesky limit) | |
| 18 | * - Falls back to lower quality if still over 1MB | |
| 19 | * | |
| 20 | * @param {Buffer} input - Raw image data | |
| 21 | * @param {string} filename - Original filename (for extension detection) | |
| 22 | * @returns {{ buffer: Buffer, mimeType: string, width: number, height: number }} | |
| 23 | */ | |
| 24 | async function processImage (input, filename) { | |
| 25 | const id = randomBytes(8).toString('hex') | |
| 26 | const inputPath = path.join(TEMP_DIR, `${id}-input${extFor(filename)}`) | |
| 27 | const outputPath = path.join(TEMP_DIR, `${id}-output.jpg`) | |
| 28 | ||
| 29 | fs.writeFileSync(inputPath, input) | |
| 30 | ||
| 31 | try { | |
| 32 | // First pass: resize + compress to JPEG 80% | |
| 33 | await ffmpeg([ | |
| 34 | '-i', inputPath, | |
| 35 | '-vf', 'scale=2048:2048:force_original_aspect_ratio=decrease', | |
| 36 | '-q:v', '4', // JPEG quality ~80% | |
| 37 | '-map_metadata', '-1', // Strip all metadata | |
| 38 | '-y', outputPath | |
| 39 | ]) | |
| 40 | ||
| 41 | let output = fs.readFileSync(outputPath) | |
| 42 | ||
| 43 | // If still over 1MB (Bluesky limit), compress harder | |
| 44 | if (output.length > 1000000) { | |
| 45 | await ffmpeg([ | |
| 46 | '-i', inputPath, | |
| 47 | '-vf', 'scale=1600:1600:force_original_aspect_ratio=decrease', | |
| 48 | '-q:v', '8', // Lower quality | |
| 49 | '-map_metadata', '-1', | |
| 50 | '-y', outputPath | |
| 51 | ]) | |
| 52 | output = fs.readFileSync(outputPath) | |
| 53 | } | |
| 54 | ||
| 55 | // If STILL over 1MB, go aggressive | |
| 56 | if (output.length > 1000000) { | |
| 57 | await ffmpeg([ | |
| 58 | '-i', inputPath, | |
| 59 | '-vf', 'scale=1200:1200:force_original_aspect_ratio=decrease', | |
| 60 | '-q:v', '12', | |
| 61 | '-map_metadata', '-1', | |
| 62 | '-y', outputPath | |
| 63 | ]) | |
| 64 | output = fs.readFileSync(outputPath) | |
| 65 | } | |
| 66 | ||
| 67 | const dimensions = await getImageDimensions(outputPath) | |
| 68 | ||
| 69 | return { | |
| 70 | buffer: output, | |
| 71 | mimeType: 'image/jpeg', | |
| 72 | width: dimensions.width, | |
| 73 | height: dimensions.height | |
| 74 | } | |
| 75 | } finally { | |
| 76 | cleanup(inputPath, outputPath) | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Process a video for cross-platform POSSE publishing. | |
| 82 | * | |
| 83 | * - Re-encode to H.264 MP4 | |
| 84 | * - Cap duration at 60 seconds (Bluesky limit) | |
| 85 | * - Reasonable bitrate for web delivery | |
| 86 | * - Generate thumbnail | |
| 87 | * | |
| 88 | * @param {Buffer} input - Raw video data | |
| 89 | * @param {string} filename - Original filename | |
| 90 | * @returns {{ buffer: Buffer, mimeType: string, thumbnail: Buffer, duration: number }} | |
| 91 | */ | |
| 92 | async function processVideo (input, filename) { | |
| 93 | const id = randomBytes(8).toString('hex') | |
| 94 | const inputPath = path.join(TEMP_DIR, `${id}-input${extFor(filename)}`) | |
| 95 | const outputPath = path.join(TEMP_DIR, `${id}-output.mp4`) | |
| 96 | const thumbPath = path.join(TEMP_DIR, `${id}-thumb.jpg`) | |
| 97 | ||
| 98 | fs.writeFileSync(inputPath, input) | |
| 99 | ||
| 100 | try { | |
| 101 | // Transcode: H.264, AAC, max 60s, reasonable quality | |
| 102 | await ffmpeg([ | |
| 103 | '-i', inputPath, | |
| 104 | '-t', '60', // Cap at 60 seconds | |
| 105 | '-c:v', 'libx264', | |
| 106 | '-preset', 'medium', | |
| 107 | '-crf', '23', // Good quality/size balance | |
| 108 | '-c:a', 'aac', | |
| 109 | '-b:a', '128k', | |
| 110 | '-movflags', '+faststart', // Web-optimized | |
| 111 | '-vf', 'scale=1920:1920:force_original_aspect_ratio=decrease:force_divisible_by=2', | |
| 112 | '-map_metadata', '-1', | |
| 113 | '-y', outputPath | |
| 114 | ]) | |
| 115 | ||
| 116 | // Generate thumbnail at 1 second | |
| 117 | await ffmpeg([ | |
| 118 | '-i', inputPath, | |
| 119 | '-ss', '1', | |
| 120 | '-frames:v', '1', | |
| 121 | '-vf', 'scale=800:800:force_original_aspect_ratio=decrease', | |
| 122 | '-q:v', '4', | |
| 123 | '-y', thumbPath | |
| 124 | ]) | |
| 125 | ||
| 126 | const output = fs.readFileSync(outputPath) | |
| 127 | const thumbnail = fs.readFileSync(thumbPath) | |
| 128 | const duration = await getVideoDuration(outputPath) | |
| 129 | ||
| 130 | return { | |
| 131 | buffer: output, | |
| 132 | mimeType: 'video/mp4', | |
| 133 | thumbnail, | |
| 134 | duration | |
| 135 | } | |
| 136 | } finally { | |
| 137 | cleanup(inputPath, outputPath, thumbPath) | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | /** | |
| 142 | * Detect if a file is an image or video based on extension/mime. | |
| 143 | */ | |
| 144 | function detectMediaType (filename, mimeType) { | |
| 145 | const ext = (path.extname(filename) || '').toLowerCase() | |
| 146 | if (mimeType) { | |
| 147 | if (mimeType.startsWith('image/')) return 'image' | |
| 148 | if (mimeType.startsWith('video/')) return 'video' | |
| 149 | } | |
| 150 | if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.avif'].includes(ext)) return 'image' | |
| 151 | if (['.mp4', '.webm', '.mov', '.avi', '.mkv'].includes(ext)) return 'video' | |
| 152 | return null | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * Process any media file — routes to image or video processor. | |
| 157 | */ | |
| 158 | async function processMedia (input, filename, mimeType) { | |
| 159 | const type = detectMediaType(filename, mimeType) | |
| 160 | if (type === 'image') return { type: 'image', ...(await processImage(input, filename)) } | |
| 161 | if (type === 'video') return { type: 'video', ...(await processVideo(input, filename)) } | |
| 162 | throw new Error('Unsupported media type: ' + filename) | |
| 163 | } | |
| 164 | ||
| 165 | // --- Internal helpers --- | |
| 166 | ||
| 167 | function ffmpeg (args) { | |
| 168 | return new Promise((resolve, reject) => { | |
| 169 | const proc = spawn('ffmpeg', ['-hide_banner', '-loglevel', 'error', ...args]) | |
| 170 | let stderr = '' | |
| 171 | proc.stderr.on('data', (d) => { stderr += d.toString() }) | |
| 172 | proc.on('exit', (code) => { | |
| 173 | if (code !== 0) reject(new Error('ffmpeg failed: ' + stderr.slice(0, 500))) | |
| 174 | else resolve() | |
| 175 | }) | |
| 176 | proc.on('error', reject) | |
| 177 | }) | |
| 178 | } | |
| 179 | ||
| 180 | function getMediaInfo (filePath) { | |
| 181 | try { | |
| 182 | const io = new FFmpeg.IOContext(filePath, FFmpeg.constants.AVIO_FLAG_READ) | |
| 183 | const ctx = new FFmpeg.InputFormatContext(io) | |
| 184 | const info = { width: 0, height: 0, duration: 0 } | |
| 185 | ||
| 186 | for (const stream of ctx.streams) { | |
| 187 | const params = stream.codecpar | |
| 188 | if (params.codecType === FFmpeg.constants.AVMEDIA_TYPE_VIDEO) { | |
| 189 | info.width = params.width | |
| 190 | info.height = params.height | |
| 191 | } | |
| 192 | } | |
| 193 | ||
| 194 | // Duration in seconds (format context stores in microseconds or via stream timebase) | |
| 195 | ctx.destroy() | |
| 196 | return info | |
| 197 | } catch { | |
| 198 | return { width: 0, height: 0, duration: 0 } | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | function getImageDimensions (filePath) { | |
| 203 | const info = getMediaInfo(filePath) | |
| 204 | return { width: info.width, height: info.height } | |
| 205 | } | |
| 206 | ||
| 207 | function getVideoDuration (filePath) { | |
| 208 | // bare-ffmpeg duration access may vary — fall back to ffprobe subprocess | |
| 209 | return new Promise((resolve) => { | |
| 210 | const proc = spawn('ffprobe', [ | |
| 211 | '-v', 'quiet', '-print_format', 'json', '-show_format', filePath | |
| 212 | ]) | |
| 213 | let out = '' | |
| 214 | proc.stdout.on('data', (d) => { out += d.toString() }) | |
| 215 | proc.on('exit', () => { | |
| 216 | try { | |
| 217 | resolve(Math.round(parseFloat(JSON.parse(out).format?.duration || '0'))) | |
| 218 | } catch { resolve(0) } | |
| 219 | }) | |
| 220 | proc.on('error', () => resolve(0)) | |
| 221 | }) | |
| 222 | } | |
| 223 | ||
| 224 | function extFor (filename) { | |
| 225 | return path.extname(filename) || '.bin' | |
| 226 | } | |
| 227 | ||
| 228 | function cleanup (...files) { | |
| 229 | for (const f of files) { | |
| 230 | try { fs.unlinkSync(f) } catch {} | |
| 231 | } | |
| 232 | } | |
| 233 | ||
| 234 | export { processImage, processVideo, processMedia, detectMediaType } |
| … | … | @@ -0,0 +1,278 @@ |
| 1 | /** | |
| 2 | * POSSE Engine — Publish Own Site, Syndicate Elsewhere | |
| 3 | * | |
| 4 | * Compose once, publish to selected platforms. | |
| 5 | * Stores all posts and ingested timeline items in FeedStore. | |
| 6 | */ | |
| 7 | ||
| 8 | class POSSEEngine { | |
| 9 | constructor (feedStore) { | |
| 10 | this.feedStore = feedStore | |
| 11 | this.clients = new Map() // id -> { platform, client } | |
| 12 | } | |
| 13 | ||
| 14 | addClient (id, platform, client) { | |
| 15 | this.clients.set(id, { platform, client }) | |
| 16 | } | |
| 17 | ||
| 18 | removeClient (id) { | |
| 19 | this.clients.delete(id) | |
| 20 | } | |
| 21 | ||
| 22 | getClients (platform) { | |
| 23 | const result = [] | |
| 24 | for (const [id, entry] of this.clients) { | |
| 25 | if (!platform || entry.platform === platform) { | |
| 26 | result.push({ id, ...entry }) | |
| 27 | } | |
| 28 | } | |
| 29 | return result | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Publish content to selected platforms | |
| 34 | * @param {string} text - Post text | |
| 35 | * @param {string[]} targets - Platform names to publish to (e.g., ['mastodon', 'atproto']) | |
| 36 | * @param {object} opts - Additional options (media, replyTo, etc.) | |
| 37 | */ | |
| 38 | async publish (text, targets = [], opts = {}) { | |
| 39 | if (!text || !text.trim()) throw new Error('Text is required') | |
| 40 | ||
| 41 | const results = {} | |
| 42 | const errors = {} | |
| 43 | ||
| 44 | // media: { buffer, mimeType, width, height } from processMedia() | |
| 45 | const media = opts.media || null | |
| 46 | ||
| 47 | for (const target of targets) { | |
| 48 | const clients = this.getClients(target) | |
| 49 | if (clients.length === 0) { | |
| 50 | errors[target] = 'No connected account' | |
| 51 | continue | |
| 52 | } | |
| 53 | ||
| 54 | for (const { id, client } of clients) { | |
| 55 | try { | |
| 56 | if (target === 'mastodon') { | |
| 57 | const postOpts = { visibility: opts.visibility || 'public' } | |
| 58 | ||
| 59 | if (media) { | |
| 60 | const uploaded = await client.uploadMedia(media.buffer, media.mimeType, opts.altText || '') | |
| 61 | postOpts.mediaIds = [uploaded.id] | |
| 62 | } | |
| 63 | ||
| 64 | const status = await client.postStatus(text, postOpts) | |
| 65 | results[id] = { platform: target, id: status.id, url: status.url } | |
| 66 | } else if (target === 'atproto') { | |
| 67 | const postOpts = {} | |
| 68 | ||
| 69 | if (media) { | |
| 70 | const uploaded = await client.uploadBlob(media.buffer, media.mimeType) | |
| 71 | postOpts.images = [{ | |
| 72 | blob: uploaded.blob, | |
| 73 | alt: opts.altText || '', | |
| 74 | width: media.width, | |
| 75 | height: media.height | |
| 76 | }] | |
| 77 | } | |
| 78 | ||
| 79 | const record = await client.post(text, postOpts) | |
| 80 | results[id] = { platform: target, uri: record.uri, cid: record.cid } | |
| 81 | } | |
| 82 | } catch (err) { | |
| 83 | errors[id] = err.message | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | // Store in feed | |
| 89 | const item = await this.feedStore.add({ | |
| 90 | source: 'local', | |
| 91 | author: 'me', | |
| 92 | text, | |
| 93 | type: media ? 'media' : 'post', | |
| 94 | mediaUrl: media ? '(uploaded)' : null, | |
| 95 | mediaType: media ? media.mimeType : null, | |
| 96 | syndicated: results, | |
| 97 | metadata: { targets, errors: Object.keys(errors).length > 0 ? errors : undefined } | |
| 98 | }) | |
| 99 | ||
| 100 | return { item, results, errors } | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Refresh — pull latest posts from all connected platforms into feed store | |
| 105 | */ | |
| 106 | async refresh () { | |
| 107 | let count = 0 | |
| 108 | ||
| 109 | for (const [id, { platform, client }] of this.clients) { | |
| 110 | try { | |
| 111 | if (platform === 'mastodon') { | |
| 112 | count += await this._refreshMastodon(id, client) | |
| 113 | } else if (platform === 'atproto') { | |
| 114 | count += await this._refreshAtproto(id, client) | |
| 115 | } | |
| 116 | } catch (err) { | |
| 117 | console.error(`[posse] Refresh ${id} failed:`, err.message) | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | return count | |
| 122 | } | |
| 123 | ||
| 124 | async _refreshMastodon (accountId, client) { | |
| 125 | const statuses = await client.homeTimeline(30) | |
| 126 | let count = 0 | |
| 127 | ||
| 128 | for (const status of statuses) { | |
| 129 | const actual = status.reblog || status | |
| 130 | const author = actual.account?.acct || actual.account?.username || 'unknown' | |
| 131 | const text = stripHtml(actual.content || '') | |
| 132 | ||
| 133 | if (!text && !actual.media_attachments?.length) continue | |
| 134 | ||
| 135 | // Deduplicate by platform-specific ID | |
| 136 | const itemId = `mastodon:${actual.id}` | |
| 137 | const existing = await this.feedStore.get(itemId) | |
| 138 | if (existing) continue | |
| 139 | ||
| 140 | const attachment = actual.media_attachments?.[0] | |
| 141 | ||
| 142 | await this.feedStore.add({ | |
| 143 | id: itemId, | |
| 144 | source: 'mastodon', | |
| 145 | author, | |
| 146 | authorId: `mastodon:${actual.account?.id || author}`, | |
| 147 | text, | |
| 148 | timestamp: new Date(actual.created_at).getTime(), | |
| 149 | type: status.reblog ? 'boost' : (actual.in_reply_to_id ? 'reply' : 'post'), | |
| 150 | mediaUrl: attachment?.url || null, | |
| 151 | mediaType: attachment?.type || null, | |
| 152 | tags: (actual.tags || []).map(t => t.name), | |
| 153 | url: actual.url, | |
| 154 | metadata: { | |
| 155 | statusId: actual.id, | |
| 156 | avatar: actual.account?.avatar_static || actual.account?.avatar || null, | |
| 157 | displayName: actual.account?.display_name || '', | |
| 158 | spoilerText: actual.spoiler_text || '', | |
| 159 | isBoost: !!status.reblog, | |
| 160 | boostBy: status.reblog ? (status.account?.acct || '') : '', | |
| 161 | boostByAvatar: status.reblog ? (status.account?.avatar_static || status.account?.avatar || null) : null, | |
| 162 | visibility: actual.visibility | |
| 163 | } | |
| 164 | }) | |
| 165 | count++ | |
| 166 | } | |
| 167 | ||
| 168 | return count | |
| 169 | } | |
| 170 | ||
| 171 | async _refreshAtproto (accountId, client) { | |
| 172 | const feed = await client.getTimeline(30) | |
| 173 | let count = 0 | |
| 174 | ||
| 175 | for (const item of feed) { | |
| 176 | const post = item.post | |
| 177 | if (!post) continue | |
| 178 | ||
| 179 | const author = post.author?.handle || post.author?.did || 'unknown' | |
| 180 | const text = post.record?.text || '' | |
| 181 | if (!text) continue | |
| 182 | ||
| 183 | const itemId = `atproto:${post.uri}` | |
| 184 | const existing = await this.feedStore.get(itemId) | |
| 185 | if (existing) continue | |
| 186 | ||
| 187 | const embed = post.embed | |
| 188 | let mediaUrl = null | |
| 189 | let mediaType = null | |
| 190 | if (embed?.images?.[0]) { | |
| 191 | mediaUrl = embed.images[0].fullsize || embed.images[0].thumb | |
| 192 | mediaType = 'image' | |
| 193 | } | |
| 194 | ||
| 195 | await this.feedStore.add({ | |
| 196 | id: itemId, | |
| 197 | source: 'atproto', | |
| 198 | author, | |
| 199 | authorId: `atproto:${post.author?.did || author}`, | |
| 200 | text, | |
| 201 | timestamp: new Date(post.record?.createdAt || post.indexedAt).getTime(), | |
| 202 | type: item.reason?.$type === 'app.bsky.feed.defs#reasonRepost' ? 'boost' : (post.record?.reply ? 'reply' : 'post'), | |
| 203 | mediaUrl, | |
| 204 | mediaType, | |
| 205 | tags: [], | |
| 206 | url: `https://bsky.app/profile/${post.author?.handle}/post/${post.uri.split('/').pop()}`, | |
| 207 | metadata: { | |
| 208 | uri: post.uri, | |
| 209 | cid: post.cid, | |
| 210 | avatar: post.author?.avatar || null, | |
| 211 | displayName: post.author?.displayName || '', | |
| 212 | likeCount: post.likeCount || 0, | |
| 213 | repostCount: post.repostCount || 0, | |
| 214 | replyCount: post.replyCount || 0 | |
| 215 | } | |
| 216 | }) | |
| 217 | count++ | |
| 218 | } | |
| 219 | ||
| 220 | return count | |
| 221 | } | |
| 222 | ||
| 223 | /** | |
| 224 | * Get notifications from all connected platforms | |
| 225 | */ | |
| 226 | async notifications () { | |
| 227 | const all = [] | |
| 228 | ||
| 229 | for (const [id, { platform, client }] of this.clients) { | |
| 230 | try { | |
| 231 | if (platform === 'mastodon') { | |
| 232 | const notifs = await client.notifications(20) | |
| 233 | for (const n of notifs) { | |
| 234 | all.push({ | |
| 235 | platform: 'mastodon', | |
| 236 | type: n.type, // mention, reblog, favourite, follow | |
| 237 | author: n.account?.acct || 'unknown', | |
| 238 | text: n.status ? stripHtml(n.status.content || '') : '', | |
| 239 | timestamp: new Date(n.created_at).getTime() | |
| 240 | }) | |
| 241 | } | |
| 242 | } else if (platform === 'atproto') { | |
| 243 | const notifs = await client.getNotifications(20) | |
| 244 | for (const n of notifs) { | |
| 245 | all.push({ | |
| 246 | platform: 'atproto', | |
| 247 | type: n.reason, // like, repost, follow, mention, reply | |
| 248 | author: n.author?.handle || 'unknown', | |
| 249 | text: n.record?.text || '', | |
| 250 | timestamp: new Date(n.indexedAt).getTime() | |
| 251 | }) | |
| 252 | } | |
| 253 | } | |
| 254 | } catch (err) { | |
| 255 | console.error(`[posse] Notifications ${id} failed:`, err.message) | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | // Sort by timestamp descending | |
| 260 | all.sort((a, b) => b.timestamp - a.timestamp) | |
| 261 | return all.slice(0, 50) | |
| 262 | } | |
| 263 | } | |
| 264 | ||
| 265 | function stripHtml (html) { | |
| 266 | return html | |
| 267 | .replace(/<br\s*\/?>/gi, '\n') | |
| 268 | .replace(/<\/p>\s*<p>/gi, '\n\n') | |
| 269 | .replace(/<[^>]+>/g, '') | |
| 270 | .replace(/&/g, '&') | |
| 271 | .replace(/</g, '<') | |
| 272 | .replace(/>/g, '>') | |
| 273 | .replace(/"/g, '"') | |
| 274 | .replace(/'/g, "'") | |
| 275 | .trim() | |
| 276 | } | |
| 277 | ||
| 278 | export { POSSEEngine } |
| … | … | @@ -0,0 +1,380 @@ |
| 1 | import http from 'bare-http1' | |
| 2 | import fs from 'bare-fs' | |
| 3 | import path from 'bare-path' | |
| 4 | import os from 'bare-os' | |
| 5 | import Corestore from 'corestore' | |
| 6 | import Hyperbee from 'hyperbee' | |
| 7 | ||
| 8 | import { FeedStore } from './feed-store.js' | |
| 9 | import { POSSEEngine } from './posse.js' | |
| 10 | import { MastodonClient } from './clients/mastodon-client.js' | |
| 11 | import { AtprotoClient } from './clients/atproto-client.js' | |
| 12 | import { processMedia } from './media.js' | |
| 13 | ||
| 14 | const MIME = { | |
| 15 | '.html': 'text/html; charset=utf-8', | |
| 16 | '.css': 'text/css; charset=utf-8', | |
| 17 | '.js': 'application/javascript; charset=utf-8', | |
| 18 | '.json': 'application/json; charset=utf-8', | |
| 19 | '.png': 'image/png', | |
| 20 | '.svg': 'image/svg+xml', | |
| 21 | '.ico': 'image/x-icon' | |
| 22 | } | |
| 23 | ||
| 24 | function json (res, data, status = 200) { | |
| 25 | res.statusCode = status | |
| 26 | res.setHeader('Content-Type', 'application/json; charset=utf-8') | |
| 27 | res.end(JSON.stringify(data)) | |
| 28 | } | |
| 29 | ||
| 30 | function error (res, msg, status = 400) { | |
| 31 | json(res, { error: msg }, status) | |
| 32 | } | |
| 33 | ||
| 34 | async function bodyJson (req) { | |
| 35 | return new Promise((resolve, reject) => { | |
| 36 | let data = '' | |
| 37 | req.on('data', (chunk) => { data += chunk.toString() }) | |
| 38 | req.on('end', () => { | |
| 39 | try { resolve(JSON.parse(data)) } catch { reject(new Error('Invalid JSON')) } | |
| 40 | }) | |
| 41 | req.on('error', reject) | |
| 42 | }) | |
| 43 | } | |
| 44 | ||
| 45 | async function parseMultipart (req) { | |
| 46 | const contentType = req.headers['content-type'] || '' | |
| 47 | const boundaryMatch = contentType.match(/boundary=(.+)/) | |
| 48 | if (!boundaryMatch) throw new Error('No multipart boundary') | |
| 49 | ||
| 50 | const boundary = boundaryMatch[1] | |
| 51 | ||
| 52 | return new Promise((resolve, reject) => { | |
| 53 | const chunks = [] | |
| 54 | req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) | |
| 55 | req.on('end', () => { | |
| 56 | const body = Buffer.concat(chunks) | |
| 57 | const fields = {} | |
| 58 | let file = null | |
| 59 | ||
| 60 | const boundaryBuf = Buffer.from('--' + boundary) | |
| 61 | const parts = splitBuffer(body, boundaryBuf) | |
| 62 | ||
| 63 | for (const part of parts) { | |
| 64 | const headerEnd = bufferIndexOf(part, Buffer.from('\r\n\r\n')) | |
| 65 | if (headerEnd === -1) continue | |
| 66 | ||
| 67 | const headerStr = part.slice(0, headerEnd).toString() | |
| 68 | const content = part.slice(headerEnd + 4) | |
| 69 | ||
| 70 | // Strip trailing \r\n | |
| 71 | const trimmed = content.length > 2 && content[content.length - 2] === 13 && content[content.length - 1] === 10 | |
| 72 | ? content.slice(0, -2) : content | |
| 73 | ||
| 74 | const nameMatch = headerStr.match(/name="([^"]+)"/) | |
| 75 | const filenameMatch = headerStr.match(/filename="([^"]+)"/) | |
| 76 | const typeMatch = headerStr.match(/Content-Type:\s*(\S+)/i) | |
| 77 | ||
| 78 | if (!nameMatch) continue | |
| 79 | ||
| 80 | if (filenameMatch) { | |
| 81 | file = { | |
| 82 | fieldName: nameMatch[1], | |
| 83 | filename: filenameMatch[1], | |
| 84 | mimeType: typeMatch ? typeMatch[1] : 'application/octet-stream', | |
| 85 | buffer: trimmed | |
| 86 | } | |
| 87 | } else { | |
| 88 | fields[nameMatch[1]] = trimmed.toString() | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | resolve({ fields, file }) | |
| 93 | }) | |
| 94 | req.on('error', reject) | |
| 95 | }) | |
| 96 | } | |
| 97 | ||
| 98 | function splitBuffer (buf, delimiter) { | |
| 99 | const parts = [] | |
| 100 | let start = 0 | |
| 101 | while (true) { | |
| 102 | const idx = bufferIndexOf(buf, delimiter, start) | |
| 103 | if (idx === -1) { | |
| 104 | if (start < buf.length) parts.push(buf.slice(start)) | |
| 105 | break | |
| 106 | } | |
| 107 | if (idx > start) parts.push(buf.slice(start, idx)) | |
| 108 | start = idx + delimiter.length | |
| 109 | } | |
| 110 | return parts | |
| 111 | } | |
| 112 | ||
| 113 | function bufferIndexOf (buf, needle, start = 0) { | |
| 114 | for (let i = start; i <= buf.length - needle.length; i++) { | |
| 115 | let found = true | |
| 116 | for (let j = 0; j < needle.length; j++) { | |
| 117 | if (buf[i + j] !== needle[j]) { found = false; break } | |
| 118 | } | |
| 119 | if (found) return i | |
| 120 | } | |
| 121 | return -1 | |
| 122 | } | |
| 123 | ||
| 124 | async function createServer (opts = {}) { | |
| 125 | const { port = 5050, prefix = '', storage } = opts | |
| 126 | ||
| 127 | const appDir = path.dirname(path.dirname(new URL(import.meta.url).pathname)) | |
| 128 | const webDir = path.join(appDir, 'web') | |
| 129 | ||
| 130 | // Storage | |
| 131 | const storageDir = storage || path.join(os.homedir(), '.config', 'hyper-social-web') | |
| 132 | try { fs.mkdirSync(storageDir, { recursive: true }) } catch {} | |
| 133 | ||
| 134 | const store = new Corestore(path.join(storageDir, 'corestore')) | |
| 135 | await store.ready() | |
| 136 | ||
| 137 | const bee = new Hyperbee(store.get({ name: 'feed' }), { | |
| 138 | keyEncoding: 'utf-8', | |
| 139 | valueEncoding: 'json' | |
| 140 | }) | |
| 141 | await bee.ready() | |
| 142 | ||
| 143 | const feedStore = new FeedStore(bee) | |
| 144 | const posse = new POSSEEngine(feedStore) | |
| 145 | ||
| 146 | // Load saved accounts | |
| 147 | const accountsBee = new Hyperbee(store.get({ name: 'accounts' }), { | |
| 148 | keyEncoding: 'utf-8', | |
| 149 | valueEncoding: 'json' | |
| 150 | }) | |
| 151 | await accountsBee.ready() | |
| 152 | await loadAccounts(accountsBee, posse) | |
| 153 | ||
| 154 | // HTTP server | |
| 155 | const server = http.createServer(async (req, res) => { | |
| 156 | let url = req.url | |
| 157 | ||
| 158 | // Strip prefix (Apache ProxyPass keeps the prefix in the path) | |
| 159 | if (prefix && url.startsWith(prefix)) { | |
| 160 | url = url.slice(prefix.length) | |
| 161 | if (!url || url[0] !== '/') url = '/' + url | |
| 162 | } | |
| 163 | ||
| 164 | // CORS for local dev | |
| 165 | res.setHeader('Access-Control-Allow-Origin', '*') | |
| 166 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS') | |
| 167 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type') | |
| 168 | if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return } | |
| 169 | ||
| 170 | try { | |
| 171 | // --- API routes --- | |
| 172 | if (url.startsWith('/api/')) { | |
| 173 | await handleApi(url, req, res, { posse, feedStore, accountsBee, prefix }) | |
| 174 | return | |
| 175 | } | |
| 176 | ||
| 177 | // --- Static files --- | |
| 178 | await serveStatic(url, res, webDir) | |
| 179 | } catch (err) { | |
| 180 | console.error('[server] Error:', err.message) | |
| 181 | error(res, 'Internal server error', 500) | |
| 182 | } | |
| 183 | }) | |
| 184 | ||
| 185 | server.listen(port, '0.0.0.0') | |
| 186 | return { server, store, posse, feedStore } | |
| 187 | } | |
| 188 | ||
| 189 | async function handleApi (url, req, res, ctx) { | |
| 190 | const { posse, feedStore, accountsBee, prefix } = ctx | |
| 191 | const method = req.method | |
| 192 | ||
| 193 | // POST /api/post — POSSE publish (text only, JSON body) | |
| 194 | if (url === '/api/post' && method === 'POST') { | |
| 195 | const body = await bodyJson(req) | |
| 196 | const result = await posse.publish(body.text, body.targets || [], body) | |
| 197 | json(res, result) | |
| 198 | return | |
| 199 | } | |
| 200 | ||
| 201 | // POST /api/post-media — POSSE publish with media (multipart) | |
| 202 | if (url === '/api/post-media' && method === 'POST') { | |
| 203 | const { fields, file } = await parseMultipart(req) | |
| 204 | ||
| 205 | if (!file) { error(res, 'No file uploaded'); return } | |
| 206 | ||
| 207 | const text = fields.text || '' | |
| 208 | const targets = fields.targets ? JSON.parse(fields.targets) : [] | |
| 209 | const altText = fields.altText || '' | |
| 210 | ||
| 211 | if (!text.trim()) { error(res, 'Text is required'); return } | |
| 212 | if (targets.length === 0) { error(res, 'Select at least one platform'); return } | |
| 213 | ||
| 214 | // Process media (resize, compress, strip metadata) | |
| 215 | const processed = await processMedia(file.buffer, file.filename, file.mimeType) | |
| 216 | ||
| 217 | const result = await posse.publish(text, targets, { | |
| 218 | media: processed, | |
| 219 | altText | |
| 220 | }) | |
| 221 | ||
| 222 | json(res, result) | |
| 223 | return | |
| 224 | } | |
| 225 | ||
| 226 | // GET /api/timeline — unified timeline | |
| 227 | if (url.startsWith('/api/timeline')) { | |
| 228 | const params = new URL('http://x' + url).searchParams | |
| 229 | const source = params.get('source') || undefined | |
| 230 | const limit = Number(params.get('limit')) || 50 | |
| 231 | const before = params.get('before') ? Number(params.get('before')) : undefined | |
| 232 | ||
| 233 | const items = await feedStore.timeline({ source, limit, before }) | |
| 234 | json(res, { items }) | |
| 235 | return | |
| 236 | } | |
| 237 | ||
| 238 | // GET /api/accounts — list connected accounts | |
| 239 | if (url === '/api/accounts' && method === 'GET') { | |
| 240 | const accounts = [] | |
| 241 | for await (const node of accountsBee.createReadStream({ gte: 'account/', lte: 'account/~' })) { | |
| 242 | if (node.value) { | |
| 243 | const safe = { ...node.value } | |
| 244 | delete safe.token | |
| 245 | delete safe.password | |
| 246 | accounts.push(safe) | |
| 247 | } | |
| 248 | } | |
| 249 | json(res, { accounts }) | |
| 250 | return | |
| 251 | } | |
| 252 | ||
| 253 | // POST /api/accounts/connect — connect a new account | |
| 254 | if (url === '/api/accounts/connect' && method === 'POST') { | |
| 255 | const body = await bodyJson(req) | |
| 256 | const { platform, instance, handle, token, password } = body | |
| 257 | ||
| 258 | if (!platform) { error(res, 'platform required'); return } | |
| 259 | ||
| 260 | const id = `${platform}:${handle || instance || 'default'}` | |
| 261 | const account = { id, platform, instance, handle, token, password, connectedAt: Date.now() } | |
| 262 | ||
| 263 | await accountsBee.put(`account/${id}`, account) | |
| 264 | ||
| 265 | // Register client with POSSE engine | |
| 266 | await connectAccount(account, posse) | |
| 267 | ||
| 268 | const safe = { ...account } | |
| 269 | delete safe.token | |
| 270 | delete safe.password | |
| 271 | json(res, { account: safe }) | |
| 272 | return | |
| 273 | } | |
| 274 | ||
| 275 | // DELETE /api/accounts/:id | |
| 276 | if (url.startsWith('/api/accounts/') && method === 'DELETE') { | |
| 277 | const id = decodeURIComponent(url.slice('/api/accounts/'.length)) | |
| 278 | await accountsBee.del(`account/${id}`) | |
| 279 | posse.removeClient(id) | |
| 280 | json(res, { ok: true }) | |
| 281 | return | |
| 282 | } | |
| 283 | ||
| 284 | // GET /api/notifications | |
| 285 | if (url === '/api/notifications' && method === 'GET') { | |
| 286 | const notifications = await posse.notifications() | |
| 287 | json(res, { notifications }) | |
| 288 | return | |
| 289 | } | |
| 290 | ||
| 291 | // GET /api/feed/refresh — pull latest from all platforms | |
| 292 | if (url === '/api/feed/refresh' && method === 'GET') { | |
| 293 | const count = await posse.refresh() | |
| 294 | json(res, { refreshed: count }) | |
| 295 | return | |
| 296 | } | |
| 297 | ||
| 298 | // POST /api/feed/repull — clear feed and re-pull (to pick up avatars etc) | |
| 299 | if (url === '/api/feed/repull' && method === 'POST') { | |
| 300 | // Delete all timeline items | |
| 301 | let deleted = 0 | |
| 302 | for await (const node of feedStore.bee.createReadStream({ gte: 'item/', lte: 'item/~' })) { | |
| 303 | if (node.value?.id) { | |
| 304 | const item = node.value | |
| 305 | const batch = feedStore.bee.batch() | |
| 306 | await batch.del(`item/${item.id}`) | |
| 307 | await batch.del(`timeline/${feedStore._padTimestamp(item.timestamp)}/${item.id}`) | |
| 308 | await batch.del(`source/${item.source}/${feedStore._padTimestamp(item.timestamp)}/${item.id}`) | |
| 309 | await batch.del(`author/${item.authorId}/${feedStore._padTimestamp(item.timestamp)}/${item.id}`) | |
| 310 | await batch.flush() | |
| 311 | deleted++ | |
| 312 | } | |
| 313 | } | |
| 314 | // Re-pull | |
| 315 | const count = await posse.refresh() | |
| 316 | json(res, { deleted, refreshed: count }) | |
| 317 | return | |
| 318 | } | |
| 319 | ||
| 320 | error(res, 'Not found', 404) | |
| 321 | } | |
| 322 | ||
| 323 | async function serveStatic (url, res, webDir) { | |
| 324 | let filePath = url === '/' ? '/index.html' : url | |
| 325 | ||
| 326 | // SPA fallback — serve index.html for non-file paths | |
| 327 | const ext = path.extname(filePath) | |
| 328 | if (!ext) filePath = '/index.html' | |
| 329 | ||
| 330 | const fullPath = path.join(webDir, filePath) | |
| 331 | ||
| 332 | try { | |
| 333 | const content = fs.readFileSync(fullPath) | |
| 334 | const mime = MIME[path.extname(fullPath)] || 'application/octet-stream' | |
| 335 | res.setHeader('Content-Type', mime) | |
| 336 | res.end(content) | |
| 337 | } catch { | |
| 338 | // SPA fallback for any path | |
| 339 | try { | |
| 340 | const index = fs.readFileSync(path.join(webDir, 'index.html')) | |
| 341 | res.setHeader('Content-Type', 'text/html; charset=utf-8') | |
| 342 | res.end(index) | |
| 343 | } catch { | |
| 344 | res.statusCode = 404 | |
| 345 | res.end('Not found') | |
| 346 | } | |
| 347 | } | |
| 348 | } | |
| 349 | ||
| 350 | async function loadAccounts (accountsBee, posse) { | |
| 351 | for await (const node of accountsBee.createReadStream({ gte: 'account/', lte: 'account/~' })) { | |
| 352 | if (node.value) { | |
| 353 | try { | |
| 354 | await connectAccount(node.value, posse) | |
| 355 | } catch (err) { | |
| 356 | console.error(`[server] Failed to connect ${node.value.id}:`, err.message) | |
| 357 | } | |
| 358 | } | |
| 359 | } | |
| 360 | } | |
| 361 | ||
| 362 | async function connectAccount (account, posse) { | |
| 363 | if (account.platform === 'mastodon') { | |
| 364 | const client = new MastodonClient({ | |
| 365 | instance: account.instance, | |
| 366 | token: account.token | |
| 367 | }) | |
| 368 | posse.addClient(account.id, 'mastodon', client) | |
| 369 | } else if (account.platform === 'atproto') { | |
| 370 | const client = new AtprotoClient({ | |
| 371 | service: account.instance || 'https://bsky.social', | |
| 372 | identifier: account.handle, | |
| 373 | password: account.password | |
| 374 | }) | |
| 375 | await client.login() | |
| 376 | posse.addClient(account.id, 'atproto', client) | |
| 377 | } | |
| 378 | } | |
| 379 | ||
| 380 | export { createServer } |
| … | … | @@ -0,0 +1,1608 @@ |
| 1 | { | |
| 2 | "name": "hyper-social-web", | |
| 3 | "version": "0.1.0", | |
| 4 | "lockfileVersion": 3, | |
| 5 | "requires": true, | |
| 6 | "packages": { | |
| 7 | "": { | |
| 8 | "name": "hyper-social-web", | |
| 9 | "version": "0.1.0", | |
| 10 | "dependencies": { | |
| 11 | "b4a": "^1.8.0", | |
| 12 | "bare-crypto": "^1.13.4", | |
| 13 | "bare-ffmpeg": "^1.2.1", | |
| 14 | "bare-fs": "^4.5.6", | |
| 15 | "bare-http1": "^4.5.5", | |
| 16 | "bare-https": "^2.1.3", | |
| 17 | "bare-node-runtime": "^1.2.0", | |
| 18 | "bare-os": "^3.8.0", | |
| 19 | "bare-path": "^3.0.0", | |
| 20 | "bare-subprocess": "^5.2.3", | |
| 21 | "bridge-envelope": "file:../bridge-envelope", | |
| 22 | "bridge-vault": "file:../bridge-vault", | |
| 23 | "corestore": "^7.9.2", | |
| 24 | "hyper-feed": "file:../hyper-feed", | |
| 25 | "hyperbee": "^2.27.3" | |
| 26 | } | |
| 27 | }, | |
| 28 | "../bridge-envelope": { | |
| 29 | "version": "1.0.0", | |
| 30 | "license": "MIT", | |
| 31 | "dependencies": { | |
| 32 | "b4a": "^1.0.0", | |
| 33 | "compact-encoding": "^2.0.0" | |
| 34 | }, | |
| 35 | "devDependencies": { | |
| 36 | "brittle": "^3.0.0" | |
| 37 | } | |
| 38 | }, | |
| 39 | "../bridge-vault": { | |
| 40 | "version": "1.0.0", | |
| 41 | "license": "MIT", | |
| 42 | "dependencies": { | |
| 43 | "b4a": "^1.0.0", | |
| 44 | "compact-encoding": "^2.0.0", | |
| 45 | "corestore": "^6.0.0", | |
| 46 | "hyperbee": "^2.0.0", | |
| 47 | "hypercore": "^10.0.0", | |
| 48 | "sodium-native": "^4.0.0" | |
| 49 | }, | |
| 50 | "devDependencies": { | |
| 51 | "brittle": "^3.0.0" | |
| 52 | } | |
| 53 | }, | |
| 54 | "../hyper-feed": { | |
| 55 | "version": "1.0.0", | |
| 56 | "license": "MIT", | |
| 57 | "dependencies": { | |
| 58 | "corestore": "^6.0.0", | |
| 59 | "hyperbee": "^2.0.0" | |
| 60 | }, | |
| 61 | "devDependencies": { | |
| 62 | "brittle": "^3.0.0" | |
| 63 | } | |
| 64 | }, | |
| 65 | "node_modules/@hyperswarm/secret-stream": { | |
| 66 | "version": "6.9.1", | |
| 67 | "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz", | |
| 68 | "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==", | |
| 69 | "license": "Apache-2.0", | |
| 70 | "dependencies": { | |
| 71 | "b4a": "^1.1.0", | |
| 72 | "hypercore-crypto": "^3.3.1", | |
| 73 | "noise-curve-ed": "^2.0.1", | |
| 74 | "noise-handshake": "^4.0.0", | |
| 75 | "sodium-secretstream": "^1.1.0", | |
| 76 | "sodium-universal": "^5.0.0", | |
| 77 | "streamx": "^2.14.0", | |
| 78 | "timeout-refresh": "^2.0.0", | |
| 79 | "unslab": "^1.3.0" | |
| 80 | } | |
| 81 | }, | |
| 82 | "node_modules/b4a": { | |
| 83 | "version": "1.8.0", | |
| 84 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", | |
| 85 | "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", | |
| 86 | "license": "Apache-2.0", | |
| 87 | "peerDependencies": { | |
| 88 | "react-native-b4a": "*" | |
| 89 | }, | |
| 90 | "peerDependenciesMeta": { | |
| 91 | "react-native-b4a": { | |
| 92 | "optional": true | |
| 93 | } | |
| 94 | } | |
| 95 | }, | |
| 96 | "node_modules/bare-abort": { | |
| 97 | "version": "2.0.13", | |
| 98 | "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz", | |
| 99 | "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==", | |
| 100 | "license": "Apache-2.0" | |
| 101 | }, | |
| 102 | "node_modules/bare-abort-controller": { | |
| 103 | "version": "1.1.0", | |
| 104 | "resolved": "https://registry.npmjs.org/bare-abort-controller/-/bare-abort-controller-1.1.0.tgz", | |
| 105 | "integrity": "sha512-ycBxndojKqfYfGTX2NgZP1YnCCV3w5d4Co2BnKTN8ZKEmTyPPKMuEHrVKtsWlqBzzZws9UsLMcdmJf7a/37lWw==", | |
| 106 | "license": "Apache-2.0", | |
| 107 | "peer": true, | |
| 108 | "dependencies": { | |
| 109 | "bare-events": "^2.7.0" | |
| 110 | } | |
| 111 | }, | |
| 112 | "node_modules/bare-addon-resolve": { | |
| 113 | "version": "1.10.0", | |
| 114 | "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", | |
| 115 | "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", | |
| 116 | "license": "Apache-2.0", | |
| 117 | "dependencies": { | |
| 118 | "bare-module-resolve": "^1.10.0", | |
| 119 | "bare-semver": "^1.0.0" | |
| 120 | }, | |
| 121 | "peerDependencies": { | |
| 122 | "bare-url": "*" | |
| 123 | }, | |
| 124 | "peerDependenciesMeta": { | |
| 125 | "bare-url": { | |
| 126 | "optional": true | |
| 127 | } | |
| 128 | } | |
| 129 | }, | |
| 130 | "node_modules/bare-ansi-escapes": { | |
| 131 | "version": "2.2.3", | |
| 132 | "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz", | |
| 133 | "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==", | |
| 134 | "license": "Apache-2.0", | |
| 135 | "dependencies": { | |
| 136 | "bare-stream": "^2.6.5" | |
| 137 | }, | |
| 138 | "peerDependencies": { | |
| 139 | "bare-buffer": "*" | |
| 140 | }, | |
| 141 | "peerDependenciesMeta": { | |
| 142 | "bare-buffer": { | |
| 143 | "optional": true | |
| 144 | } | |
| 145 | } | |
| 146 | }, | |
| 147 | "node_modules/bare-assert": { | |
| 148 | "version": "1.2.0", | |
| 149 | "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz", | |
| 150 | "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==", | |
| 151 | "license": "Apache-2.0", | |
| 152 | "dependencies": { | |
| 153 | "bare-inspect": "^3.1.2" | |
| 154 | } | |
| 155 | }, | |
| 156 | "node_modules/bare-async-hooks": { | |
| 157 | "version": "0.0.0", | |
| 158 | "resolved": "https://registry.npmjs.org/bare-async-hooks/-/bare-async-hooks-0.0.0.tgz", | |
| 159 | "integrity": "sha512-xNfGwUobaomCGMGAqohAekS3uMCj+4tvI4AoOaJnO7NfpN+dvFdkC5xkeQtmZzs2vxf2TR5J6i5FDd1ImCZERw==", | |
| 160 | "license": "Apache-2.0" | |
| 161 | }, | |
| 162 | "node_modules/bare-buffer": { | |
| 163 | "version": "3.6.0", | |
| 164 | "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz", | |
| 165 | "integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==", | |
| 166 | "license": "Apache-2.0", | |
| 167 | "engines": { | |
| 168 | "bare": ">=1.20.0" | |
| 169 | } | |
| 170 | }, | |
| 171 | "node_modules/bare-bundle": { | |
| 172 | "version": "1.10.0", | |
| 173 | "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz", | |
| 174 | "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==", | |
| 175 | "license": "Apache-2.0", | |
| 176 | "peerDependencies": { | |
| 177 | "bare-buffer": "*", | |
| 178 | "bare-url": "*" | |
| 179 | }, | |
| 180 | "peerDependenciesMeta": { | |
| 181 | "bare-buffer": { | |
| 182 | "optional": true | |
| 183 | }, | |
| 184 | "bare-url": { | |
| 185 | "optional": true | |
| 186 | } | |
| 187 | } | |
| 188 | }, | |
| 189 | "node_modules/bare-channel": { | |
| 190 | "version": "5.2.3", | |
| 191 | "resolved": "https://registry.npmjs.org/bare-channel/-/bare-channel-5.2.3.tgz", | |
| 192 | "integrity": "sha512-2cRErqS4fzzr3ZUSd5W67ka70dZKJ1VWrCsvxPVC8vr3rWvd4aaCG5iCDas9+MFVaUIWvfw04+NH4+FHQDddMQ==", | |
| 193 | "license": "Apache-2.0", | |
| 194 | "dependencies": { | |
| 195 | "bare-events": "^2.0.0", | |
| 196 | "bare-stream": "^2.7.0", | |
| 197 | "bare-structured-clone": "^1.4.0" | |
| 198 | }, | |
| 199 | "engines": { | |
| 200 | "bare": ">=1.7.0" | |
| 201 | } | |
| 202 | }, | |
| 203 | "node_modules/bare-console": { | |
| 204 | "version": "6.1.0", | |
| 205 | "resolved": "https://registry.npmjs.org/bare-console/-/bare-console-6.1.0.tgz", | |
| 206 | "integrity": "sha512-BziGTEIyVh0zn78842mmpOLGTEFE7/S4A5Tb0d2iIu7bryVdoiF0przraYMetJckHxm0P5ITTZrYfRuYRr9n/Q==", | |
| 207 | "license": "Apache-2.0", | |
| 208 | "dependencies": { | |
| 209 | "bare-hrtime": "^2.0.0", | |
| 210 | "bare-logger": "^2.0.0", | |
| 211 | "bare-system-logger": "^1.0.2" | |
| 212 | } | |
| 213 | }, | |
| 214 | "node_modules/bare-crypto": { | |
| 215 | "version": "1.13.4", | |
| 216 | "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz", | |
| 217 | "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==", | |
| 218 | "license": "Apache-2.0", | |
| 219 | "dependencies": { | |
| 220 | "bare-assert": "^1.2.0", | |
| 221 | "bare-stream": "^2.6.3" | |
| 222 | }, | |
| 223 | "peerDependencies": { | |
| 224 | "bare-buffer": "*" | |
| 225 | }, | |
| 226 | "peerDependenciesMeta": { | |
| 227 | "bare-buffer": { | |
| 228 | "optional": true | |
| 229 | } | |
| 230 | } | |
| 231 | }, | |
| 232 | "node_modules/bare-debug-log": { | |
| 233 | "version": "2.0.0", | |
| 234 | "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz", | |
| 235 | "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==", | |
| 236 | "license": "Apache-2.0", | |
| 237 | "dependencies": { | |
| 238 | "bare-os": "^3.0.1" | |
| 239 | } | |
| 240 | }, | |
| 241 | "node_modules/bare-dgram": { | |
| 242 | "version": "1.0.1", | |
| 243 | "resolved": "https://registry.npmjs.org/bare-dgram/-/bare-dgram-1.0.1.tgz", | |
| 244 | "integrity": "sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==", | |
| 245 | "license": "Apache-2.0", | |
| 246 | "dependencies": { | |
| 247 | "bare-events": "^2.5.0", | |
| 248 | "udx-native": "^1.11.2" | |
| 249 | } | |
| 250 | }, | |
| 251 | "node_modules/bare-diagnostics-channel": { | |
| 252 | "version": "1.1.0", | |
| 253 | "resolved": "https://registry.npmjs.org/bare-diagnostics-channel/-/bare-diagnostics-channel-1.1.0.tgz", | |
| 254 | "integrity": "sha512-Reu+EQo+eLpB+a5p8UykFEdXndFaRaSgKV38uAMh/qhE2eTeJcdwAwo74hKYyeN8GD3DIFqC9ZlM4bnDc03CIg==", | |
| 255 | "license": "Apache-2.0" | |
| 256 | }, | |
| 257 | "node_modules/bare-dns": { | |
| 258 | "version": "2.1.4", | |
| 259 | "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz", | |
| 260 | "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==", | |
| 261 | "license": "Apache-2.0", | |
| 262 | "engines": { | |
| 263 | "bare": ">=1.7.0" | |
| 264 | } | |
| 265 | }, | |
| 266 | "node_modules/bare-encoding": { | |
| 267 | "version": "1.0.3", | |
| 268 | "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz", | |
| 269 | "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==", | |
| 270 | "license": "Apache-2.0", | |
| 271 | "peerDependencies": { | |
| 272 | "bare-buffer": "*" | |
| 273 | }, | |
| 274 | "peerDependenciesMeta": { | |
| 275 | "bare-buffer": { | |
| 276 | "optional": true | |
| 277 | } | |
| 278 | } | |
| 279 | }, | |
| 280 | "node_modules/bare-env": { | |
| 281 | "version": "3.0.0", | |
| 282 | "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz", | |
| 283 | "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==", | |
| 284 | "license": "Apache-2.0", | |
| 285 | "dependencies": { | |
| 286 | "bare-os": "^3.0.1" | |
| 287 | } | |
| 288 | }, | |
| 289 | "node_modules/bare-events": { | |
| 290 | "version": "2.8.2", | |
| 291 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", | |
| 292 | "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", | |
| 293 | "license": "Apache-2.0", | |
| 294 | "peerDependencies": { | |
| 295 | "bare-abort-controller": "*" | |
| 296 | }, | |
| 297 | "peerDependenciesMeta": { | |
| 298 | "bare-abort-controller": { | |
| 299 | "optional": true | |
| 300 | } | |
| 301 | } | |
| 302 | }, | |
| 303 | "node_modules/bare-fetch": { | |
| 304 | "version": "2.8.0", | |
| 305 | "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-2.8.0.tgz", | |
| 306 | "integrity": "sha512-xCBF6oKwzZmq+M6FGcAHv8Fn3XEXHeauLr6dfmr5IG07PFhabg1+r9eeVaIybPAqx8syF9M9+4r23QuDvh3Uvg==", | |
| 307 | "license": "Apache-2.0", | |
| 308 | "dependencies": { | |
| 309 | "bare-form-data": "^1.2.0", | |
| 310 | "bare-http1": "^4.5.2", | |
| 311 | "bare-https": "^2.0.0", | |
| 312 | "bare-stream": "^2.9.1", | |
| 313 | "bare-url": "^2.4.0", | |
| 314 | "bare-zlib": "^1.3.0" | |
| 315 | }, | |
| 316 | "peerDependencies": { | |
| 317 | "bare-abort-controller": "*", | |
| 318 | "bare-buffer": "*" | |
| 319 | }, | |
| 320 | "peerDependenciesMeta": { | |
| 321 | "bare-abort-controller": { | |
| 322 | "optional": true | |
| 323 | }, | |
| 324 | "bare-buffer": { | |
| 325 | "optional": true | |
| 326 | } | |
| 327 | } | |
| 328 | }, | |
| 329 | "node_modules/bare-ffmpeg": { | |
| 330 | "version": "1.2.1", | |
| 331 | "resolved": "https://registry.npmjs.org/bare-ffmpeg/-/bare-ffmpeg-1.2.1.tgz", | |
| 332 | "integrity": "sha512-lGZS7eRCEoCCw0Jnv1PPYl2N8SHuzfMDuKuw+fMDKWlauBZ7tnTtYOQrewLvDjww6TMXhtt2KMiw1OXSFU74Xw==", | |
| 333 | "license": "Apache-2.0" | |
| 334 | }, | |
| 335 | "node_modules/bare-form-data": { | |
| 336 | "version": "1.2.1", | |
| 337 | "resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.2.1.tgz", | |
| 338 | "integrity": "sha512-DFZhp5Tn0S7TjII/xhgLl2BNU6f7l5QbIW/YH7TjzQVnxLa8SaMSbl77jpy0ZR1oUnAo0a7NHJ9QAOU6PPr0Jg==", | |
| 339 | "license": "Apache-2.0", | |
| 340 | "dependencies": { | |
| 341 | "bare-buffer": "^3.6.0", | |
| 342 | "bare-stream": "^2.6.5" | |
| 343 | } | |
| 344 | }, | |
| 345 | "node_modules/bare-format": { | |
| 346 | "version": "1.0.2", | |
| 347 | "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz", | |
| 348 | "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==", | |
| 349 | "license": "Apache-2.0", | |
| 350 | "dependencies": { | |
| 351 | "bare-inspect": "^3.0.0" | |
| 352 | } | |
| 353 | }, | |
| 354 | "node_modules/bare-fs": { | |
| 355 | "version": "4.5.6", | |
| 356 | "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", | |
| 357 | "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", | |
| 358 | "license": "Apache-2.0", | |
| 359 | "dependencies": { | |
| 360 | "bare-events": "^2.5.4", | |
| 361 | "bare-path": "^3.0.0", | |
| 362 | "bare-stream": "^2.6.4", | |
| 363 | "bare-url": "^2.2.2", | |
| 364 | "fast-fifo": "^1.3.2" | |
| 365 | }, | |
| 366 | "engines": { | |
| 367 | "bare": ">=1.16.0" | |
| 368 | }, | |
| 369 | "peerDependencies": { | |
| 370 | "bare-buffer": "*" | |
| 371 | }, | |
| 372 | "peerDependenciesMeta": { | |
| 373 | "bare-buffer": { | |
| 374 | "optional": true | |
| 375 | } | |
| 376 | } | |
| 377 | }, | |
| 378 | "node_modules/bare-hrtime": { | |
| 379 | "version": "2.1.1", | |
| 380 | "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz", | |
| 381 | "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==", | |
| 382 | "license": "Apache-2.0" | |
| 383 | }, | |
| 384 | "node_modules/bare-http-parser": { | |
| 385 | "version": "1.1.3", | |
| 386 | "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz", | |
| 387 | "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==", | |
| 388 | "license": "Apache-2.0" | |
| 389 | }, | |
| 390 | "node_modules/bare-http1": { | |
| 391 | "version": "4.5.5", | |
| 392 | "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz", | |
| 393 | "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==", | |
| 394 | "license": "Apache-2.0", | |
| 395 | "dependencies": { | |
| 396 | "bare-events": "^2.6.0", | |
| 397 | "bare-http-parser": "^1.1.1", | |
| 398 | "bare-stream": "^2.10.0", | |
| 399 | "bare-tcp": "^2.2.0" | |
| 400 | }, | |
| 401 | "peerDependencies": { | |
| 402 | "bare-buffer": "*", | |
| 403 | "bare-url": "*" | |
| 404 | }, | |
| 405 | "peerDependenciesMeta": { | |
| 406 | "bare-buffer": { | |
| 407 | "optional": true | |
| 408 | }, | |
| 409 | "bare-url": { | |
| 410 | "optional": true | |
| 411 | } | |
| 412 | } | |
| 413 | }, | |
| 414 | "node_modules/bare-https": { | |
| 415 | "version": "2.1.3", | |
| 416 | "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz", | |
| 417 | "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==", | |
| 418 | "license": "Apache-2.0", | |
| 419 | "dependencies": { | |
| 420 | "bare-http1": "^4.4.0", | |
| 421 | "bare-tcp": "^2.2.0", | |
| 422 | "bare-tls": "^2.0.0" | |
| 423 | } | |
| 424 | }, | |
| 425 | "node_modules/bare-inspect": { | |
| 426 | "version": "3.1.4", | |
| 427 | "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz", | |
| 428 | "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==", | |
| 429 | "license": "Apache-2.0", | |
| 430 | "dependencies": { | |
| 431 | "bare-ansi-escapes": "^2.1.0", | |
| 432 | "bare-type": "^1.0.0" | |
| 433 | }, | |
| 434 | "engines": { | |
| 435 | "bare": ">=1.18.0" | |
| 436 | } | |
| 437 | }, | |
| 438 | "node_modules/bare-inspector": { | |
| 439 | "version": "5.0.0", | |
| 440 | "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-5.0.0.tgz", | |
| 441 | "integrity": "sha512-blObRFPBK6yn0/wZbrRgkGcOuLUQUYVvkKbsEp5p3bWJB+uNrAh+qUqOijQHS6mZjGvyGCE86VERp3EiUR2TyA==", | |
| 442 | "license": "Apache-2.0", | |
| 443 | "dependencies": { | |
| 444 | "bare-events": "^2.1.0", | |
| 445 | "bare-http1": "^4.0.0", | |
| 446 | "bare-stream": "^2.0.0", | |
| 447 | "bare-url": "^2.0.0", | |
| 448 | "bare-ws": "^2.0.0" | |
| 449 | }, | |
| 450 | "engines": { | |
| 451 | "bare": ">=1.2.0" | |
| 452 | }, | |
| 453 | "peerDependencies": { | |
| 454 | "bare-tcp": "*" | |
| 455 | }, | |
| 456 | "peerDependenciesMeta": { | |
| 457 | "bare-tcp": { | |
| 458 | "optional": true | |
| 459 | } | |
| 460 | } | |
| 461 | }, | |
| 462 | "node_modules/bare-logger": { | |
| 463 | "version": "2.0.3", | |
| 464 | "resolved": "https://registry.npmjs.org/bare-logger/-/bare-logger-2.0.3.tgz", | |
| 465 | "integrity": "sha512-U6K7NxHdeAHxEgHFDV8zXvgjgDZKQO6LlCrxQvUvlrZEVTnoezSImMWizi85rbZpVsbeI1ZWcLCRGnoIf5EV8Q==", | |
| 466 | "license": "Apache-2.0", | |
| 467 | "dependencies": { | |
| 468 | "bare-format": "^1.0.0" | |
| 469 | } | |
| 470 | }, | |
| 471 | "node_modules/bare-module": { | |
| 472 | "version": "6.1.3", | |
| 473 | "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz", | |
| 474 | "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==", | |
| 475 | "license": "Apache-2.0", | |
| 476 | "dependencies": { | |
| 477 | "bare-bundle": "^1.3.0", | |
| 478 | "bare-module-lexer": "^1.0.0", | |
| 479 | "bare-module-resolve": "^1.8.0", | |
| 480 | "bare-path": "^3.0.0", | |
| 481 | "bare-url": "^2.0.1" | |
| 482 | }, | |
| 483 | "engines": { | |
| 484 | "bare": ">=1.23.0" | |
| 485 | }, | |
| 486 | "peerDependencies": { | |
| 487 | "bare-buffer": "*" | |
| 488 | }, | |
| 489 | "peerDependenciesMeta": { | |
| 490 | "bare-buffer": { | |
| 491 | "optional": true | |
| 492 | } | |
| 493 | } | |
| 494 | }, | |
| 495 | "node_modules/bare-module-lexer": { | |
| 496 | "version": "1.4.7", | |
| 497 | "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz", | |
| 498 | "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==", | |
| 499 | "license": "Apache-2.0", | |
| 500 | "dependencies": { | |
| 501 | "require-addon": "^1.0.2" | |
| 502 | }, | |
| 503 | "peerDependencies": { | |
| 504 | "bare-buffer": "*" | |
| 505 | }, | |
| 506 | "peerDependenciesMeta": { | |
| 507 | "bare-buffer": { | |
| 508 | "optional": true | |
| 509 | } | |
| 510 | } | |
| 511 | }, | |
| 512 | "node_modules/bare-module-resolve": { | |
| 513 | "version": "1.12.1", | |
| 514 | "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", | |
| 515 | "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==", | |
| 516 | "license": "Apache-2.0", | |
| 517 | "dependencies": { | |
| 518 | "bare-semver": "^1.0.0" | |
| 519 | }, | |
| 520 | "peerDependencies": { | |
| 521 | "bare-url": "*" | |
| 522 | }, | |
| 523 | "peerDependenciesMeta": { | |
| 524 | "bare-url": { | |
| 525 | "optional": true | |
| 526 | } | |
| 527 | } | |
| 528 | }, | |
| 529 | "node_modules/bare-module-traverse": { | |
| 530 | "version": "2.0.1", | |
| 531 | "resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.0.1.tgz", | |
| 532 | "integrity": "sha512-1au+Og5p97T9b6Y7xmHZ7KtpW8vEYtz2jC2whmm+YJp46EaHfk26j91MmQhufdkR/8sdK1Q5p+P9A/Y5GrJg7Q==", | |
| 533 | "license": "Apache-2.0", | |
| 534 | "dependencies": { | |
| 535 | "bare-addon-resolve": "^1.5.0", | |
| 536 | "bare-module-lexer": "^1.4.0", | |
| 537 | "bare-module-resolve": "^1.7.0" | |
| 538 | }, | |
| 539 | "peerDependencies": { | |
| 540 | "bare-buffer": "*", | |
| 541 | "bare-url": "*" | |
| 542 | }, | |
| 543 | "peerDependenciesMeta": { | |
| 544 | "bare-buffer": { | |
| 545 | "optional": true | |
| 546 | }, | |
| 547 | "bare-url": { | |
| 548 | "optional": true | |
| 549 | } | |
| 550 | } | |
| 551 | }, | |
| 552 | "node_modules/bare-net": { | |
| 553 | "version": "2.3.1", | |
| 554 | "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz", | |
| 555 | "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==", | |
| 556 | "license": "Apache-2.0", | |
| 557 | "dependencies": { | |
| 558 | "bare-events": "^2.2.2", | |
| 559 | "bare-pipe": "^4.0.0", | |
| 560 | "bare-stream": "^2.0.0", | |
| 561 | "bare-tcp": "^2.0.0" | |
| 562 | } | |
| 563 | }, | |
| 564 | "node_modules/bare-node-runtime": { | |
| 565 | "version": "1.2.0", | |
| 566 | "resolved": "https://registry.npmjs.org/bare-node-runtime/-/bare-node-runtime-1.2.0.tgz", | |
| 567 | "integrity": "sha512-oCpkcKBLDuzhtga0Q/63Ds3Gj03I0jIf+VX4mDhSClpoBfz/wnDtl2d1NKQDAVNCoP4I617GrJkS3vwNO4dd7w==", | |
| 568 | "license": "Apache-2.0", | |
| 569 | "dependencies": { | |
| 570 | "bare-abort-controller": "^1.0.0", | |
| 571 | "bare-assert": "^1.1.0", | |
| 572 | "bare-async-hooks": "^0.0.0", | |
| 573 | "bare-buffer": "^3.3.1", | |
| 574 | "bare-console": "^6.0.1", | |
| 575 | "bare-crypto": "^1.11.2", | |
| 576 | "bare-dgram": "^1.0.1", | |
| 577 | "bare-diagnostics-channel": "^1.1.0", | |
| 578 | "bare-dns": "^2.1.4", | |
| 579 | "bare-events": "^2.7.0", | |
| 580 | "bare-fetch": "^2.5.0", | |
| 581 | "bare-fs": "^4.2.3", | |
| 582 | "bare-http1": "^4.0.4", | |
| 583 | "bare-https": "^2.0.0", | |
| 584 | "bare-inspector": "^5.0.0", | |
| 585 | "bare-module": "^6.1.2", | |
| 586 | "bare-net": "^2.0.2", | |
| 587 | "bare-os": "^3.6.2", | |
| 588 | "bare-path": "^3.0.0", | |
| 589 | "bare-performance": "^1.1.0", | |
| 590 | "bare-process": "^4.2.1", | |
| 591 | "bare-punycode": "^0.0.0", | |
| 592 | "bare-querystring": "^1.0.0", | |
| 593 | "bare-readline": "^1.1.0", | |
| 594 | "bare-repl": "^6.0.1", | |
| 595 | "bare-stream": "^2.7.0", | |
| 596 | "bare-string-decoder": "^1.0.0", | |
| 597 | "bare-subprocess": "^5.1.1", | |
| 598 | "bare-timers": "^3.0.3", | |
| 599 | "bare-tls": "^2.1.3", | |
| 600 | "bare-tty": "^5.0.3", | |
| 601 | "bare-url": "^2.2.2", | |
| 602 | "bare-utils": "^1.5.1", | |
| 603 | "bare-v8": "^1.0.1", | |
| 604 | "bare-vm": "^1.0.0", | |
| 605 | "bare-worker": "^4.0.0", | |
| 606 | "bare-ws": "^2.1.0", | |
| 607 | "bare-zlib": "^1.3.1" | |
| 608 | } | |
| 609 | }, | |
| 610 | "node_modules/bare-os": { | |
| 611 | "version": "3.8.0", | |
| 612 | "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", | |
| 613 | "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", | |
| 614 | "license": "Apache-2.0", | |
| 615 | "engines": { | |
| 616 | "bare": ">=1.14.0" | |
| 617 | } | |
| 618 | }, | |
| 619 | "node_modules/bare-path": { | |
| 620 | "version": "3.0.0", | |
| 621 | "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", | |
| 622 | "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", | |
| 623 | "license": "Apache-2.0", | |
| 624 | "dependencies": { | |
| 625 | "bare-os": "^3.0.1" | |
| 626 | } | |
| 627 | }, | |
| 628 | "node_modules/bare-performance": { | |
| 629 | "version": "1.3.0", | |
| 630 | "resolved": "https://registry.npmjs.org/bare-performance/-/bare-performance-1.3.0.tgz", | |
| 631 | "integrity": "sha512-ITlHSQwsnJSPjpagTtPo043LRkFgcrl9SgPsVUBXZvTeGWuNLUfMRwmwAS+35i8qnIEinySoLxwhVoJNZAIF2g==", | |
| 632 | "license": "Apache-2.0" | |
| 633 | }, | |
| 634 | "node_modules/bare-pipe": { | |
| 635 | "version": "4.1.5", | |
| 636 | "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz", | |
| 637 | "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==", | |
| 638 | "license": "Apache-2.0", | |
| 639 | "dependencies": { | |
| 640 | "bare-events": "^2.0.0", | |
| 641 | "bare-stream": "^2.0.0" | |
| 642 | }, | |
| 643 | "engines": { | |
| 644 | "bare": ">=1.16.0" | |
| 645 | } | |
| 646 | }, | |
| 647 | "node_modules/bare-process": { | |
| 648 | "version": "4.3.0", | |
| 649 | "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.3.0.tgz", | |
| 650 | "integrity": "sha512-HwtAdKbqS98V+jeWAagUdzkrJR4vtuPeeWlsoeD1YKTqM1hKUWM2eU+l2diUYeTqFfxY9b6J9SsBLuBukH0tMQ==", | |
| 651 | "license": "Apache-2.0", | |
| 652 | "dependencies": { | |
| 653 | "bare-abort": "^2.0.13", | |
| 654 | "bare-env": "^3.0.0", | |
| 655 | "bare-events": "^2.3.1", | |
| 656 | "bare-hrtime": "^2.0.0", | |
| 657 | "bare-os": "^3.7.1", | |
| 658 | "bare-pipe": "^4.0.0", | |
| 659 | "bare-signals": "^4.0.0", | |
| 660 | "bare-tty": "^5.0.0" | |
| 661 | } | |
| 662 | }, | |
| 663 | "node_modules/bare-punycode": { | |
| 664 | "version": "0.0.0", | |
| 665 | "resolved": "https://registry.npmjs.org/bare-punycode/-/bare-punycode-0.0.0.tgz", | |
| 666 | "integrity": "sha512-PC2Y6mGLytZPJCB9M7CvO5Zb6uWYVUZ+5t3L6vePFuFM6tdC6SsQ+sIsuf0Sa6LHBoWURX7I9yMMbPXMv7TIdQ==", | |
| 667 | "license": "Apache-2.0", | |
| 668 | "dependencies": { | |
| 669 | "punycode": "^2.3.1" | |
| 670 | } | |
| 671 | }, | |
| 672 | "node_modules/bare-querystring": { | |
| 673 | "version": "1.0.0", | |
| 674 | "resolved": "https://registry.npmjs.org/bare-querystring/-/bare-querystring-1.0.0.tgz", | |
| 675 | "integrity": "sha512-Kbqdjd4f1MxMtutXzraEKIXqXRfq9uWLwzUT5bLUh9TCiPznY9l6vHc4BUPNIa13DyH1VAlPy3iOxHzhqV2quQ==", | |
| 676 | "license": "Apache-2.0" | |
| 677 | }, | |
| 678 | "node_modules/bare-readline": { | |
| 679 | "version": "1.2.2", | |
| 680 | "resolved": "https://registry.npmjs.org/bare-readline/-/bare-readline-1.2.2.tgz", | |
| 681 | "integrity": "sha512-7MihQDnw80rxPyLJHZAUgFskFyGUj45FKJDIe57sW0iQyJIGroFA08mrtiMLMWUIgSd49hT7E97FCWvrc29p1Q==", | |
| 682 | "license": "Apache-2.0", | |
| 683 | "dependencies": { | |
| 684 | "bare-ansi-escapes": "^2.0.0", | |
| 685 | "bare-stream": "^2.0.0" | |
| 686 | } | |
| 687 | }, | |
| 688 | "node_modules/bare-realm": { | |
| 689 | "version": "2.0.1", | |
| 690 | "resolved": "https://registry.npmjs.org/bare-realm/-/bare-realm-2.0.1.tgz", | |
| 691 | "integrity": "sha512-kQbYU6AAQu4XBTQesuz2+PpjBx7MJzf5Qw3nSLzQCzbVglx7Ml85MtWEjUe1AeslXqrd+nFPHMEbL55ouISscA==", | |
| 692 | "license": "Apache-2.0", | |
| 693 | "engines": { | |
| 694 | "bare": ">=1.5.0" | |
| 695 | } | |
| 696 | }, | |
| 697 | "node_modules/bare-repl": { | |
| 698 | "version": "6.0.2", | |
| 699 | "resolved": "https://registry.npmjs.org/bare-repl/-/bare-repl-6.0.2.tgz", | |
| 700 | "integrity": "sha512-w2R+5LOrMl9UH/gcFjvdIpsB+SgyLr8cW4uJo5bAEzRbRpVA+ZTm8pMHYdj0uqIUBClP/6fkyW0MnB1XkxakmQ==", | |
| 701 | "license": "Apache-2.0", | |
| 702 | "dependencies": { | |
| 703 | "bare-inspect": "^3.0.0", | |
| 704 | "bare-module": "^6.0.1", | |
| 705 | "bare-os": "^3.0.1", | |
| 706 | "bare-path": "^3.0.0", | |
| 707 | "bare-pipe": "^4.0.0", | |
| 708 | "bare-readline": "^1.0.0", | |
| 709 | "bare-stream": "^2.0.0", | |
| 710 | "bare-tty": "^5.0.0" | |
| 711 | } | |
| 712 | }, | |
| 713 | "node_modules/bare-semver": { | |
| 714 | "version": "1.0.2", | |
| 715 | "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", | |
| 716 | "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", | |
| 717 | "license": "Apache-2.0" | |
| 718 | }, | |
| 719 | "node_modules/bare-signals": { | |
| 720 | "version": "4.2.0", | |
| 721 | "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz", | |
| 722 | "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==", | |
| 723 | "license": "Apache-2.0", | |
| 724 | "dependencies": { | |
| 725 | "bare-events": "^2.5.3", | |
| 726 | "bare-os": "^3.3.1" | |
| 727 | }, | |
| 728 | "engines": { | |
| 729 | "bare": ">=1.7.0" | |
| 730 | } | |
| 731 | }, | |
| 732 | "node_modules/bare-stream": { | |
| 733 | "version": "2.10.0", | |
| 734 | "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz", | |
| 735 | "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==", | |
| 736 | "license": "Apache-2.0", | |
| 737 | "dependencies": { | |
| 738 | "streamx": "^2.25.0", | |
| 739 | "teex": "^1.0.1" | |
| 740 | }, | |
| 741 | "peerDependencies": { | |
| 742 | "bare-buffer": "*", | |
| 743 | "bare-events": "*" | |
| 744 | }, | |
| 745 | "peerDependenciesMeta": { | |
| 746 | "bare-buffer": { | |
| 747 | "optional": true | |
| 748 | }, | |
| 749 | "bare-events": { | |
| 750 | "optional": true | |
| 751 | } | |
| 752 | } | |
| 753 | }, | |
| 754 | "node_modules/bare-string-decoder": { | |
| 755 | "version": "1.0.0", | |
| 756 | "resolved": "https://registry.npmjs.org/bare-string-decoder/-/bare-string-decoder-1.0.0.tgz", | |
| 757 | "integrity": "sha512-FjFvfHo88U7borNQSj9ijP2JTb1asqY6K28OZrix4dF9iZf5D2wi1+99CgLlHT3HtYqdwxRhDAiKu8rVstt5rQ==", | |
| 758 | "license": "Apache-2.0", | |
| 759 | "dependencies": { | |
| 760 | "text-decoder": "^1.2.3" | |
| 761 | }, | |
| 762 | "peerDependencies": { | |
| 763 | "bare-buffer": "*" | |
| 764 | }, | |
| 765 | "peerDependenciesMeta": { | |
| 766 | "bare-buffer": { | |
| 767 | "optional": true | |
| 768 | } | |
| 769 | } | |
| 770 | }, | |
| 771 | "node_modules/bare-structured-clone": { | |
| 772 | "version": "1.5.3", | |
| 773 | "resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.5.3.tgz", | |
| 774 | "integrity": "sha512-vC/YqGsp67ZeFnpyAskwaEIXtNscnCwFVKlSk0Oh2X3AqWT6H+7DC9vVI080FohUJeuUuB9UNx4UFdDcyQgZaw==", | |
| 775 | "license": "Apache-2.0", | |
| 776 | "dependencies": { | |
| 777 | "bare-type": "^1.1.0", | |
| 778 | "compact-encoding": "^2.15.0" | |
| 779 | }, | |
| 780 | "engines": { | |
| 781 | "bare": ">=1.2.0" | |
| 782 | }, | |
| 783 | "peerDependencies": { | |
| 784 | "bare-buffer": "*", | |
| 785 | "bare-url": "*" | |
| 786 | }, | |
| 787 | "peerDependenciesMeta": { | |
| 788 | "bare-buffer": { | |
| 789 | "optional": true | |
| 790 | }, | |
| 791 | "bare-url": { | |
| 792 | "optional": true | |
| 793 | } | |
| 794 | } | |
| 795 | }, | |
| 796 | "node_modules/bare-stylize": { | |
| 797 | "version": "0.0.1", | |
| 798 | "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz", | |
| 799 | "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==", | |
| 800 | "license": "Apache-2.0", | |
| 801 | "dependencies": { | |
| 802 | "bare-ansi-escapes": "^2.2.3", | |
| 803 | "bare-process": "^4.2.1" | |
| 804 | } | |
| 805 | }, | |
| 806 | "node_modules/bare-subprocess": { | |
| 807 | "version": "5.2.3", | |
| 808 | "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz", | |
| 809 | "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==", | |
| 810 | "license": "Apache-2.0", | |
| 811 | "dependencies": { | |
| 812 | "bare-env": "^3.0.0", | |
| 813 | "bare-events": "^2.5.4", | |
| 814 | "bare-os": "^3.0.1", | |
| 815 | "bare-pipe": "^4.0.0", | |
| 816 | "bare-url": "^2.2.2" | |
| 817 | }, | |
| 818 | "engines": { | |
| 819 | "bare": ">=1.7.0" | |
| 820 | }, | |
| 821 | "peerDependencies": { | |
| 822 | "bare-buffer": "*" | |
| 823 | }, | |
| 824 | "peerDependenciesMeta": { | |
| 825 | "bare-buffer": { | |
| 826 | "optional": true | |
| 827 | } | |
| 828 | } | |
| 829 | }, | |
| 830 | "node_modules/bare-system-logger": { | |
| 831 | "version": "1.0.3", | |
| 832 | "resolved": "https://registry.npmjs.org/bare-system-logger/-/bare-system-logger-1.0.3.tgz", | |
| 833 | "integrity": "sha512-HXTXZyhIIS4ZDYun4J9zKNQZDfLgEkkK8wy9nrTFJE5xq8APsOhDxuEsxP0YcNwC23DW1jaNpqD51zDeXPzIdg==", | |
| 834 | "license": "Apache-2.0", | |
| 835 | "dependencies": { | |
| 836 | "bare-logger": "^2.0.0" | |
| 837 | } | |
| 838 | }, | |
| 839 | "node_modules/bare-tcp": { | |
| 840 | "version": "2.2.7", | |
| 841 | "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz", | |
| 842 | "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==", | |
| 843 | "license": "Apache-2.0", | |
| 844 | "peer": true, | |
| 845 | "dependencies": { | |
| 846 | "bare-dns": "^2.0.4", | |
| 847 | "bare-events": "^2.5.4", | |
| 848 | "bare-stream": "^2.6.4" | |
| 849 | }, | |
| 850 | "engines": { | |
| 851 | "bare": ">=1.16.0" | |
| 852 | } | |
| 853 | }, | |
| 854 | "node_modules/bare-thread": { | |
| 855 | "version": "1.1.6", | |
| 856 | "resolved": "https://registry.npmjs.org/bare-thread/-/bare-thread-1.1.6.tgz", | |
| 857 | "integrity": "sha512-t6+mun2bpC6UlxyjWxn0bQYp8hM25XXRo6/UuoXx1mvYFZ6dHpp9nRyI0WWQk+n2Z5zb/g9hiUv6U+g4N8MQ3w==", | |
| 858 | "license": "Apache-2.0", | |
| 859 | "dependencies": { | |
| 860 | "bare-bundle": "^1.9.0", | |
| 861 | "bare-module-resolve": "^1.11.2", | |
| 862 | "bare-module-traverse": "^2.0.0" | |
| 863 | } | |
| 864 | }, | |
| 865 | "node_modules/bare-timers": { | |
| 866 | "version": "3.2.1", | |
| 867 | "resolved": "https://registry.npmjs.org/bare-timers/-/bare-timers-3.2.1.tgz", | |
| 868 | "integrity": "sha512-O+9e6Jol/BoYsQUzAFXG0gWUW1GWryFMblGcNBfi3smPrD3rJIKQwpw2FJARl2j/1VHne4a8YaHPK1cxDKfiYQ==", | |
| 869 | "license": "Apache-2.0", | |
| 870 | "engines": { | |
| 871 | "bare": ">=1.7.0" | |
| 872 | }, | |
| 873 | "peerDependencies": { | |
| 874 | "bare-abort-controller": "*" | |
| 875 | }, | |
| 876 | "peerDependenciesMeta": { | |
| 877 | "bare-abort-controller": { | |
| 878 | "optional": true | |
| 879 | } | |
| 880 | } | |
| 881 | }, | |
| 882 | "node_modules/bare-tls": { | |
| 883 | "version": "2.2.1", | |
| 884 | "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.1.tgz", | |
| 885 | "integrity": "sha512-hZ+ZqwrUO4dyH7/6WYkYWjgAFNJKjzwEYJiDaMnMs+eRleBDjQ3CvNZawpkw0Ar9jnM9NZK6+f6GqjkZ2FLGmQ==", | |
| 886 | "license": "Apache-2.0", | |
| 887 | "dependencies": { | |
| 888 | "bare-net": "^2.0.1", | |
| 889 | "bare-stream": "^2.6.4" | |
| 890 | }, | |
| 891 | "engines": { | |
| 892 | "bare": ">=1.7.0" | |
| 893 | } | |
| 894 | }, | |
| 895 | "node_modules/bare-tty": { | |
| 896 | "version": "5.0.3", | |
| 897 | "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz", | |
| 898 | "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==", | |
| 899 | "license": "Apache-2.0", | |
| 900 | "dependencies": { | |
| 901 | "bare-events": "^2.2.0", | |
| 902 | "bare-signals": "^4.0.0", | |
| 903 | "bare-stream": "^2.0.0" | |
| 904 | }, | |
| 905 | "engines": { | |
| 906 | "bare": ">=1.16.0" | |
| 907 | } | |
| 908 | }, | |
| 909 | "node_modules/bare-type": { | |
| 910 | "version": "1.1.0", | |
| 911 | "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz", | |
| 912 | "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==", | |
| 913 | "license": "Apache-2.0", | |
| 914 | "engines": { | |
| 915 | "bare": ">=1.2.0" | |
| 916 | } | |
| 917 | }, | |
| 918 | "node_modules/bare-url": { | |
| 919 | "version": "2.4.0", | |
| 920 | "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", | |
| 921 | "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", | |
| 922 | "license": "Apache-2.0", | |
| 923 | "dependencies": { | |
| 924 | "bare-path": "^3.0.0" | |
| 925 | } | |
| 926 | }, | |
| 927 | "node_modules/bare-utils": { | |
| 928 | "version": "1.6.0", | |
| 929 | "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz", | |
| 930 | "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==", | |
| 931 | "license": "Apache-2.0", | |
| 932 | "dependencies": { | |
| 933 | "bare-debug-log": "^2.0.0", | |
| 934 | "bare-encoding": "^1.0.0", | |
| 935 | "bare-format": "^1.0.0", | |
| 936 | "bare-inspect": "^3.0.0", | |
| 937 | "bare-stylize": "^0.0.1", | |
| 938 | "bare-type": "^1.0.6" | |
| 939 | } | |
| 940 | }, | |
| 941 | "node_modules/bare-v8": { | |
| 942 | "version": "1.0.1", | |
| 943 | "resolved": "https://registry.npmjs.org/bare-v8/-/bare-v8-1.0.1.tgz", | |
| 944 | "integrity": "sha512-/cR5ZvFWQRdtTZ4tx0j7TKvTWce8UnnLqm88fwHtJmfM7HODIBVjQGDT7KkDLeD2d/eHP2pzB71Y8/QyiMMKrQ==", | |
| 945 | "license": "Apache-2.0" | |
| 946 | }, | |
| 947 | "node_modules/bare-vm": { | |
| 948 | "version": "1.0.1", | |
| 949 | "resolved": "https://registry.npmjs.org/bare-vm/-/bare-vm-1.0.1.tgz", | |
| 950 | "integrity": "sha512-yLnbRvKt3AhRTmtfTIrYfdTHqGEfIJc+Fgb2DcHejE0HJ+p5adGxxPMvd3893Z7iXVYnalxukNARn4oJSZELHQ==", | |
| 951 | "license": "Apache-2.0", | |
| 952 | "dependencies": { | |
| 953 | "bare-realm": "^2.0.0" | |
| 954 | } | |
| 955 | }, | |
| 956 | "node_modules/bare-worker": { | |
| 957 | "version": "4.1.6", | |
| 958 | "resolved": "https://registry.npmjs.org/bare-worker/-/bare-worker-4.1.6.tgz", | |
| 959 | "integrity": "sha512-yvsektF7xNqlAyjJV+YYeBx+sGZ8Fhbo5Cjc/lpIdkM8Gd/e3MlaxTFg8NknkyvJ4oDJXqvp38Q+ZOn5Gyr+nQ==", | |
| 960 | "license": "Apache-2.0", | |
| 961 | "dependencies": { | |
| 962 | "bare-channel": "^5.1.5", | |
| 963 | "bare-events": "^2.2.1", | |
| 964 | "bare-module": "^6.0.1", | |
| 965 | "bare-thread": "^1.1.3" | |
| 966 | } | |
| 967 | }, | |
| 968 | "node_modules/bare-ws": { | |
| 969 | "version": "2.1.0", | |
| 970 | "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz", | |
| 971 | "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==", | |
| 972 | "license": "Apache-2.0", | |
| 973 | "dependencies": { | |
| 974 | "bare-crypto": "^1.2.0", | |
| 975 | "bare-events": "^2.3.1", | |
| 976 | "bare-http1": "^4.0.0", | |
| 977 | "bare-https": "^2.0.0", | |
| 978 | "bare-stream": "^2.1.2" | |
| 979 | }, | |
| 980 | "peerDependencies": { | |
| 981 | "bare-buffer": "*", | |
| 982 | "bare-url": "*" | |
| 983 | }, | |
| 984 | "peerDependenciesMeta": { | |
| 985 | "bare-buffer": { | |
| 986 | "optional": true | |
| 987 | }, | |
| 988 | "bare-url": { | |
| 989 | "optional": true | |
| 990 | } | |
| 991 | } | |
| 992 | }, | |
| 993 | "node_modules/bare-zlib": { | |
| 994 | "version": "1.3.1", | |
| 995 | "resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.3.1.tgz", | |
| 996 | "integrity": "sha512-VP93GFzhrTdWh9mXNocn7XsP/nF5JQluiiSsbTvsQ4yIYlhEHRMF9lQmZZDXwzK9PNYaVGUV1bdQuqp0Mj7MHw==", | |
| 997 | "license": "Apache-2.0", | |
| 998 | "dependencies": { | |
| 999 | "bare-stream": "^2.0.0" | |
| 1000 | } | |
| 1001 | }, | |
| 1002 | "node_modules/big-sparse-array": { | |
| 1003 | "version": "1.0.3", | |
| 1004 | "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz", | |
| 1005 | "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==", | |
| 1006 | "license": "MIT" | |
| 1007 | }, | |
| 1008 | "node_modules/bridge-envelope": { | |
| 1009 | "resolved": "../bridge-envelope", | |
| 1010 | "link": true | |
| 1011 | }, | |
| 1012 | "node_modules/bridge-vault": { | |
| 1013 | "resolved": "../bridge-vault", | |
| 1014 | "link": true | |
| 1015 | }, | |
| 1016 | "node_modules/codecs": { | |
| 1017 | "version": "3.1.0", | |
| 1018 | "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz", | |
| 1019 | "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==", | |
| 1020 | "license": "MIT", | |
| 1021 | "dependencies": { | |
| 1022 | "b4a": "^1.6.3" | |
| 1023 | } | |
| 1024 | }, | |
| 1025 | "node_modules/compact-encoding": { | |
| 1026 | "version": "2.19.1", | |
| 1027 | "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.1.tgz", | |
| 1028 | "integrity": "sha512-4J4FHc5UugLv2YMss1tL8OBZdgR0vVkV4EuXoAqFS4NbK4gz0Qp+E+VGUnwvDqmMOw2ce/H8duXh3rreC0Qccg==", | |
| 1029 | "license": "Apache-2.0", | |
| 1030 | "dependencies": { | |
| 1031 | "b4a": "^1.3.0" | |
| 1032 | } | |
| 1033 | }, | |
| 1034 | "node_modules/corestore": { | |
| 1035 | "version": "7.9.2", | |
| 1036 | "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz", | |
| 1037 | "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==", | |
| 1038 | "license": "MIT", | |
| 1039 | "dependencies": { | |
| 1040 | "b4a": "^1.6.7", | |
| 1041 | "hypercore": "^11.19.0", | |
| 1042 | "hypercore-crypto": "^3.4.2", | |
| 1043 | "hypercore-errors": "^1.4.0", | |
| 1044 | "hypercore-id-encoding": "^1.3.0", | |
| 1045 | "ready-resource": "^1.1.1", | |
| 1046 | "sodium-universal": "^5.0.1", | |
| 1047 | "which-runtime": "^1.2.1" | |
| 1048 | } | |
| 1049 | }, | |
| 1050 | "node_modules/debounceify": { | |
| 1051 | "version": "1.1.0", | |
| 1052 | "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz", | |
| 1053 | "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==", | |
| 1054 | "license": "MIT" | |
| 1055 | }, | |
| 1056 | "node_modules/device-file": { | |
| 1057 | "version": "2.3.1", | |
| 1058 | "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz", | |
| 1059 | "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==", | |
| 1060 | "license": "Apache-2.0", | |
| 1061 | "dependencies": { | |
| 1062 | "b4a": "^1.6.7", | |
| 1063 | "bare-fs": "^4.0.1", | |
| 1064 | "bare-path": "^3.0.0", | |
| 1065 | "fd-lock": "^2.1.0", | |
| 1066 | "fs-native-extensions": "^1.4.0", | |
| 1067 | "ready-resource": "^1.2.0" | |
| 1068 | } | |
| 1069 | }, | |
| 1070 | "node_modules/events-universal": { | |
| 1071 | "version": "1.0.1", | |
| 1072 | "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", | |
| 1073 | "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", | |
| 1074 | "license": "Apache-2.0", | |
| 1075 | "dependencies": { | |
| 1076 | "bare-events": "^2.7.0" | |
| 1077 | } | |
| 1078 | }, | |
| 1079 | "node_modules/fast-fifo": { | |
| 1080 | "version": "1.3.2", | |
| 1081 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", | |
| 1082 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", | |
| 1083 | "license": "MIT" | |
| 1084 | }, | |
| 1085 | "node_modules/fd-lock": { | |
| 1086 | "version": "2.1.1", | |
| 1087 | "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz", | |
| 1088 | "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==", | |
| 1089 | "license": "Apache-2.0", | |
| 1090 | "dependencies": { | |
| 1091 | "bare-fs": "^4.5.0", | |
| 1092 | "fs-native-extensions": "^1.4.4", | |
| 1093 | "ready-resource": "^1.2.0", | |
| 1094 | "resource-on-exit": "^1.0.0" | |
| 1095 | } | |
| 1096 | }, | |
| 1097 | "node_modules/flat-tree": { | |
| 1098 | "version": "1.13.0", | |
| 1099 | "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz", | |
| 1100 | "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==", | |
| 1101 | "license": "MIT" | |
| 1102 | }, | |
| 1103 | "node_modules/fs-native-extensions": { | |
| 1104 | "version": "1.4.5", | |
| 1105 | "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz", | |
| 1106 | "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==", | |
| 1107 | "license": "Apache-2.0", | |
| 1108 | "dependencies": { | |
| 1109 | "require-addon": "^1.1.0", | |
| 1110 | "which-runtime": "^1.2.0" | |
| 1111 | } | |
| 1112 | }, | |
| 1113 | "node_modules/generate-object-property": { | |
| 1114 | "version": "2.0.0", | |
| 1115 | "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz", | |
| 1116 | "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==", | |
| 1117 | "license": "MIT", | |
| 1118 | "dependencies": { | |
| 1119 | "is-property": "^1.0.0" | |
| 1120 | } | |
| 1121 | }, | |
| 1122 | "node_modules/generate-string": { | |
| 1123 | "version": "1.0.1", | |
| 1124 | "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz", | |
| 1125 | "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==", | |
| 1126 | "license": "MIT" | |
| 1127 | }, | |
| 1128 | "node_modules/hyper-feed": { | |
| 1129 | "resolved": "../hyper-feed", | |
| 1130 | "link": true | |
| 1131 | }, | |
| 1132 | "node_modules/hyperbee": { | |
| 1133 | "version": "2.27.3", | |
| 1134 | "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz", | |
| 1135 | "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==", | |
| 1136 | "license": "MIT", | |
| 1137 | "dependencies": { | |
| 1138 | "b4a": "^1.6.0", | |
| 1139 | "codecs": "^3.0.0", | |
| 1140 | "debounceify": "^1.0.0", | |
| 1141 | "hypercore-errors": "^1.0.0", | |
| 1142 | "mutexify": "^1.4.0", | |
| 1143 | "protocol-buffers-encodings": "^1.2.0", | |
| 1144 | "rache": "^1.0.0", | |
| 1145 | "ready-resource": "^1.0.0", | |
| 1146 | "resolve-reject-promise": "^1.1.0", | |
| 1147 | "safety-catch": "^1.0.2", | |
| 1148 | "streamx": "^2.12.4", | |
| 1149 | "unslab": "^1.2.0" | |
| 1150 | } | |
| 1151 | }, | |
| 1152 | "node_modules/hypercore": { | |
| 1153 | "version": "11.27.17", | |
| 1154 | "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.27.17.tgz", | |
| 1155 | "integrity": "sha512-u4GBBDN2EmOk/vzGOQqDOFTN3UdOuSAIbWik6ZboMHVr21n5rSM2rK/FXwc28kW7JUBtbfNKY28Hi86rVFMzYw==", | |
| 1156 | "license": "MIT", | |
| 1157 | "dependencies": { | |
| 1158 | "@hyperswarm/secret-stream": "^6.0.0", | |
| 1159 | "b4a": "^1.1.0", | |
| 1160 | "bare-events": "^2.2.0", | |
| 1161 | "big-sparse-array": "^1.0.3", | |
| 1162 | "compact-encoding": "^2.11.0", | |
| 1163 | "fast-fifo": "^1.3.0", | |
| 1164 | "flat-tree": "^1.9.0", | |
| 1165 | "hypercore-crypto": "^3.2.1", | |
| 1166 | "hypercore-errors": "^1.5.0", | |
| 1167 | "hypercore-id-encoding": "^1.2.0", | |
| 1168 | "hypercore-storage": "^2.0.0", | |
| 1169 | "is-options": "^1.0.1", | |
| 1170 | "nanoassert": "^2.0.0", | |
| 1171 | "protomux": "^3.5.0", | |
| 1172 | "quickbit-universal": "^2.2.0", | |
| 1173 | "random-array-iterator": "^1.0.0", | |
| 1174 | "safety-catch": "^1.0.1", | |
| 1175 | "sodium-universal": "^5.0.1", | |
| 1176 | "streamx": "^2.12.4", | |
| 1177 | "unslab": "^1.3.0", | |
| 1178 | "z32": "^1.0.0" | |
| 1179 | } | |
| 1180 | }, | |
| 1181 | "node_modules/hypercore-crypto": { | |
| 1182 | "version": "3.6.1", | |
| 1183 | "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz", | |
| 1184 | "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==", | |
| 1185 | "license": "MIT", | |
| 1186 | "dependencies": { | |
| 1187 | "b4a": "^1.6.6", | |
| 1188 | "compact-encoding": "^2.15.0", | |
| 1189 | "sodium-universal": "^5.0.0" | |
| 1190 | } | |
| 1191 | }, | |
| 1192 | "node_modules/hypercore-errors": { | |
| 1193 | "version": "1.5.0", | |
| 1194 | "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz", | |
| 1195 | "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==", | |
| 1196 | "license": "Apache-2.0", | |
| 1197 | "dependencies": { | |
| 1198 | "hypercore-id-encoding": "^1.3.0" | |
| 1199 | } | |
| 1200 | }, | |
| 1201 | "node_modules/hypercore-id-encoding": { | |
| 1202 | "version": "1.3.0", | |
| 1203 | "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz", | |
| 1204 | "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==", | |
| 1205 | "license": "Apache-2.0", | |
| 1206 | "dependencies": { | |
| 1207 | "b4a": "^1.5.3", | |
| 1208 | "z32": "^1.0.0" | |
| 1209 | } | |
| 1210 | }, | |
| 1211 | "node_modules/hypercore-storage": { | |
| 1212 | "version": "2.7.1", | |
| 1213 | "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.7.1.tgz", | |
| 1214 | "integrity": "sha512-eT3s227nS17c8+HWjl6ge0l1lPk9NDtBzQH///tOSZjCG4gxk0QnjhlJVS5q1dcnNB54Gs75JKceP798IymvTQ==", | |
| 1215 | "license": "Apache-2.0", | |
| 1216 | "dependencies": { | |
| 1217 | "b4a": "^1.6.7", | |
| 1218 | "bare-fs": "^4.0.1", | |
| 1219 | "bare-path": "^3.0.0", | |
| 1220 | "compact-encoding": "^2.16.0", | |
| 1221 | "device-file": "^2.1.2", | |
| 1222 | "flat-tree": "^1.12.1", | |
| 1223 | "hypercore-crypto": "^3.4.2", | |
| 1224 | "hyperschema": "^1.7.0", | |
| 1225 | "index-encoder": "^3.3.2", | |
| 1226 | "resolve-reject-promise": "^1.0.0", | |
| 1227 | "rocksdb-native": "^3.11.0", | |
| 1228 | "scope-lock": "^1.2.4", | |
| 1229 | "streamx": "^2.21.1" | |
| 1230 | } | |
| 1231 | }, | |
| 1232 | "node_modules/hyperschema": { | |
| 1233 | "version": "1.20.1", | |
| 1234 | "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz", | |
| 1235 | "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==", | |
| 1236 | "license": "Apache-2.0", | |
| 1237 | "dependencies": { | |
| 1238 | "bare-fs": "^4.0.1", | |
| 1239 | "compact-encoding": "^2.19.0", | |
| 1240 | "generate-object-property": "^2.0.0", | |
| 1241 | "generate-string": "^1.0.1" | |
| 1242 | } | |
| 1243 | }, | |
| 1244 | "node_modules/index-encoder": { | |
| 1245 | "version": "3.5.0", | |
| 1246 | "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz", | |
| 1247 | "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==", | |
| 1248 | "license": "Apache-2.0", | |
| 1249 | "dependencies": { | |
| 1250 | "b4a": "^1.6.4" | |
| 1251 | } | |
| 1252 | }, | |
| 1253 | "node_modules/is-options": { | |
| 1254 | "version": "1.0.2", | |
| 1255 | "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz", | |
| 1256 | "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==", | |
| 1257 | "license": "MIT", | |
| 1258 | "dependencies": { | |
| 1259 | "b4a": "^1.1.1" | |
| 1260 | } | |
| 1261 | }, | |
| 1262 | "node_modules/is-property": { | |
| 1263 | "version": "1.0.2", | |
| 1264 | "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", | |
| 1265 | "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", | |
| 1266 | "license": "MIT" | |
| 1267 | }, | |
| 1268 | "node_modules/mutexify": { | |
| 1269 | "version": "1.4.0", | |
| 1270 | "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", | |
| 1271 | "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==", | |
| 1272 | "license": "MIT", | |
| 1273 | "dependencies": { | |
| 1274 | "queue-tick": "^1.0.0" | |
| 1275 | } | |
| 1276 | }, | |
| 1277 | "node_modules/nanoassert": { | |
| 1278 | "version": "2.0.0", | |
| 1279 | "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", | |
| 1280 | "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", | |
| 1281 | "license": "ISC" | |
| 1282 | }, | |
| 1283 | "node_modules/noise-curve-ed": { | |
| 1284 | "version": "2.1.0", | |
| 1285 | "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz", | |
| 1286 | "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==", | |
| 1287 | "license": "ISC", | |
| 1288 | "dependencies": { | |
| 1289 | "b4a": "^1.1.0", | |
| 1290 | "nanoassert": "^2.0.0", | |
| 1291 | "sodium-universal": "^5.0.0" | |
| 1292 | } | |
| 1293 | }, | |
| 1294 | "node_modules/noise-handshake": { | |
| 1295 | "version": "4.2.0", | |
| 1296 | "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz", | |
| 1297 | "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==", | |
| 1298 | "license": "Apache-2.0", | |
| 1299 | "dependencies": { | |
| 1300 | "b4a": "^1.1.0", | |
| 1301 | "nanoassert": "^2.0.0", | |
| 1302 | "sodium-universal": "^5.0.0" | |
| 1303 | } | |
| 1304 | }, | |
| 1305 | "node_modules/protocol-buffers-encodings": { | |
| 1306 | "version": "1.2.0", | |
| 1307 | "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz", | |
| 1308 | "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==", | |
| 1309 | "license": "MIT", | |
| 1310 | "dependencies": { | |
| 1311 | "b4a": "^1.6.0", | |
| 1312 | "signed-varint": "^2.0.1", | |
| 1313 | "varint": "5.0.0" | |
| 1314 | } | |
| 1315 | }, | |
| 1316 | "node_modules/protomux": { | |
| 1317 | "version": "3.10.1", | |
| 1318 | "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz", | |
| 1319 | "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==", | |
| 1320 | "license": "MIT", | |
| 1321 | "dependencies": { | |
| 1322 | "b4a": "^1.3.1", | |
| 1323 | "compact-encoding": "^2.5.1", | |
| 1324 | "queue-tick": "^1.0.0", | |
| 1325 | "safety-catch": "^1.0.1", | |
| 1326 | "unslab": "^1.3.0" | |
| 1327 | } | |
| 1328 | }, | |
| 1329 | "node_modules/punycode": { | |
| 1330 | "version": "2.3.1", | |
| 1331 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", | |
| 1332 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", | |
| 1333 | "license": "MIT", | |
| 1334 | "engines": { | |
| 1335 | "node": ">=6" | |
| 1336 | } | |
| 1337 | }, | |
| 1338 | "node_modules/queue-tick": { | |
| 1339 | "version": "1.0.1", | |
| 1340 | "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", | |
| 1341 | "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", | |
| 1342 | "license": "MIT" | |
| 1343 | }, | |
| 1344 | "node_modules/quickbit-native": { | |
| 1345 | "version": "2.4.8", | |
| 1346 | "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz", | |
| 1347 | "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==", | |
| 1348 | "license": "Apache-2.0", | |
| 1349 | "optional": true, | |
| 1350 | "dependencies": { | |
| 1351 | "require-addon": "^1.1.0" | |
| 1352 | } | |
| 1353 | }, | |
| 1354 | "node_modules/quickbit-universal": { | |
| 1355 | "version": "2.2.0", | |
| 1356 | "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz", | |
| 1357 | "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==", | |
| 1358 | "license": "ISC", | |
| 1359 | "dependencies": { | |
| 1360 | "b4a": "^1.6.0", | |
| 1361 | "simdle-universal": "^1.1.0" | |
| 1362 | }, | |
| 1363 | "optionalDependencies": { | |
| 1364 | "quickbit-native": "^2.2.0" | |
| 1365 | } | |
| 1366 | }, | |
| 1367 | "node_modules/rache": { | |
| 1368 | "version": "1.0.0", | |
| 1369 | "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz", | |
| 1370 | "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==", | |
| 1371 | "license": "Apache-2.0" | |
| 1372 | }, | |
| 1373 | "node_modules/random-array-iterator": { | |
| 1374 | "version": "1.0.0", | |
| 1375 | "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz", | |
| 1376 | "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==", | |
| 1377 | "license": "MIT" | |
| 1378 | }, | |
| 1379 | "node_modules/ready-resource": { | |
| 1380 | "version": "1.2.0", | |
| 1381 | "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz", | |
| 1382 | "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==", | |
| 1383 | "license": "MIT", | |
| 1384 | "dependencies": { | |
| 1385 | "bare-events": "^2.2.0" | |
| 1386 | } | |
| 1387 | }, | |
| 1388 | "node_modules/refcounter": { | |
| 1389 | "version": "1.0.0", | |
| 1390 | "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz", | |
| 1391 | "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==", | |
| 1392 | "license": "Apache-2.0" | |
| 1393 | }, | |
| 1394 | "node_modules/require-addon": { | |
| 1395 | "version": "1.2.0", | |
| 1396 | "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", | |
| 1397 | "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", | |
| 1398 | "license": "Apache-2.0", | |
| 1399 | "dependencies": { | |
| 1400 | "bare-addon-resolve": "^1.3.0" | |
| 1401 | }, | |
| 1402 | "engines": { | |
| 1403 | "bare": ">=1.10.0" | |
| 1404 | } | |
| 1405 | }, | |
| 1406 | "node_modules/resolve-reject-promise": { | |
| 1407 | "version": "1.1.0", | |
| 1408 | "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz", | |
| 1409 | "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==", | |
| 1410 | "license": "MIT" | |
| 1411 | }, | |
| 1412 | "node_modules/resource-on-exit": { | |
| 1413 | "version": "1.0.0", | |
| 1414 | "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz", | |
| 1415 | "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==", | |
| 1416 | "license": "Apache-2.0" | |
| 1417 | }, | |
| 1418 | "node_modules/rocksdb-native": { | |
| 1419 | "version": "3.15.0", | |
| 1420 | "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz", | |
| 1421 | "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==", | |
| 1422 | "license": "Apache-2.0", | |
| 1423 | "dependencies": { | |
| 1424 | "compact-encoding": "^2.15.0", | |
| 1425 | "ready-resource": "^1.0.0", | |
| 1426 | "refcounter": "^1.0.0", | |
| 1427 | "require-addon": "^1.0.2", | |
| 1428 | "resolve-reject-promise": "^1.1.0", | |
| 1429 | "signal-promise": "^1.0.3", | |
| 1430 | "streamx": "^2.16.1" | |
| 1431 | }, | |
| 1432 | "engines": { | |
| 1433 | "bare": ">=1.16.0" | |
| 1434 | } | |
| 1435 | }, | |
| 1436 | "node_modules/safety-catch": { | |
| 1437 | "version": "1.0.3", | |
| 1438 | "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz", | |
| 1439 | "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==", | |
| 1440 | "license": "MIT" | |
| 1441 | }, | |
| 1442 | "node_modules/scope-lock": { | |
| 1443 | "version": "1.2.4", | |
| 1444 | "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz", | |
| 1445 | "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==", | |
| 1446 | "license": "Apache-2.0" | |
| 1447 | }, | |
| 1448 | "node_modules/signal-promise": { | |
| 1449 | "version": "1.0.3", | |
| 1450 | "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz", | |
| 1451 | "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==", | |
| 1452 | "license": "MIT" | |
| 1453 | }, | |
| 1454 | "node_modules/signed-varint": { | |
| 1455 | "version": "2.0.1", | |
| 1456 | "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz", | |
| 1457 | "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==", | |
| 1458 | "license": "MIT", | |
| 1459 | "dependencies": { | |
| 1460 | "varint": "~5.0.0" | |
| 1461 | } | |
| 1462 | }, | |
| 1463 | "node_modules/simdle-native": { | |
| 1464 | "version": "1.3.9", | |
| 1465 | "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz", | |
| 1466 | "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==", | |
| 1467 | "license": "Apache-2.0", | |
| 1468 | "optional": true, | |
| 1469 | "dependencies": { | |
| 1470 | "b4a": "^1.6.0", | |
| 1471 | "require-addon": "^1.1.0" | |
| 1472 | } | |
| 1473 | }, | |
| 1474 | "node_modules/simdle-universal": { | |
| 1475 | "version": "1.1.2", | |
| 1476 | "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz", | |
| 1477 | "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==", | |
| 1478 | "license": "ISC", | |
| 1479 | "dependencies": { | |
| 1480 | "b4a": "^1.6.0" | |
| 1481 | }, | |
| 1482 | "optionalDependencies": { | |
| 1483 | "simdle-native": "^1.1.1" | |
| 1484 | } | |
| 1485 | }, | |
| 1486 | "node_modules/sodium-native": { | |
| 1487 | "version": "5.1.0", | |
| 1488 | "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", | |
| 1489 | "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", | |
| 1490 | "license": "MIT", | |
| 1491 | "dependencies": { | |
| 1492 | "bare-assert": "^1.2.0", | |
| 1493 | "require-addon": "^1.1.0", | |
| 1494 | "which-runtime": "^1.2.1" | |
| 1495 | }, | |
| 1496 | "engines": { | |
| 1497 | "bare": ">=1.16.0" | |
| 1498 | } | |
| 1499 | }, | |
| 1500 | "node_modules/sodium-secretstream": { | |
| 1501 | "version": "1.2.0", | |
| 1502 | "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz", | |
| 1503 | "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==", | |
| 1504 | "license": "MIT", | |
| 1505 | "dependencies": { | |
| 1506 | "b4a": "^1.1.1", | |
| 1507 | "sodium-universal": "^5.0.0" | |
| 1508 | } | |
| 1509 | }, | |
| 1510 | "node_modules/sodium-universal": { | |
| 1511 | "version": "5.0.1", | |
| 1512 | "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz", | |
| 1513 | "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==", | |
| 1514 | "license": "MIT", | |
| 1515 | "dependencies": { | |
| 1516 | "sodium-native": "^5.0.1" | |
| 1517 | }, | |
| 1518 | "peerDependencies": { | |
| 1519 | "sodium-javascript": "~0.8.0" | |
| 1520 | }, | |
| 1521 | "peerDependenciesMeta": { | |
| 1522 | "sodium-javascript": { | |
| 1523 | "optional": true | |
| 1524 | } | |
| 1525 | } | |
| 1526 | }, | |
| 1527 | "node_modules/streamx": { | |
| 1528 | "version": "2.25.0", | |
| 1529 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", | |
| 1530 | "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", | |
| 1531 | "license": "MIT", | |
| 1532 | "dependencies": { | |
| 1533 | "events-universal": "^1.0.0", | |
| 1534 | "fast-fifo": "^1.3.2", | |
| 1535 | "text-decoder": "^1.1.0" | |
| 1536 | } | |
| 1537 | }, | |
| 1538 | "node_modules/teex": { | |
| 1539 | "version": "1.0.1", | |
| 1540 | "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", | |
| 1541 | "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", | |
| 1542 | "license": "MIT", | |
| 1543 | "dependencies": { | |
| 1544 | "streamx": "^2.12.5" | |
| 1545 | } | |
| 1546 | }, | |
| 1547 | "node_modules/text-decoder": { | |
| 1548 | "version": "1.2.7", | |
| 1549 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", | |
| 1550 | "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", | |
| 1551 | "license": "Apache-2.0", | |
| 1552 | "dependencies": { | |
| 1553 | "b4a": "^1.6.4" | |
| 1554 | } | |
| 1555 | }, | |
| 1556 | "node_modules/timeout-refresh": { | |
| 1557 | "version": "2.0.1", | |
| 1558 | "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz", | |
| 1559 | "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==", | |
| 1560 | "license": "MIT" | |
| 1561 | }, | |
| 1562 | "node_modules/udx-native": { | |
| 1563 | "version": "1.19.2", | |
| 1564 | "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.19.2.tgz", | |
| 1565 | "integrity": "sha512-RNYh+UhfryCsF5hE2ZOuIqcZ+qdipXK3UsarwxWJwsUQZFE3ybwz0mPjwb5ev1PMBcjFahWiepS/q0wwL51c2g==", | |
| 1566 | "license": "Apache-2.0", | |
| 1567 | "dependencies": { | |
| 1568 | "b4a": "^1.5.0", | |
| 1569 | "bare-events": "^2.2.0", | |
| 1570 | "require-addon": "^1.1.0", | |
| 1571 | "streamx": "^2.22.0" | |
| 1572 | }, | |
| 1573 | "engines": { | |
| 1574 | "bare": ">=1.17.4" | |
| 1575 | } | |
| 1576 | }, | |
| 1577 | "node_modules/unslab": { | |
| 1578 | "version": "1.3.0", | |
| 1579 | "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz", | |
| 1580 | "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==", | |
| 1581 | "license": "Apache-2.0", | |
| 1582 | "dependencies": { | |
| 1583 | "b4a": "^1.6.6" | |
| 1584 | } | |
| 1585 | }, | |
| 1586 | "node_modules/varint": { | |
| 1587 | "version": "5.0.0", | |
| 1588 | "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz", | |
| 1589 | "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==", | |
| 1590 | "license": "MIT" | |
| 1591 | }, | |
| 1592 | "node_modules/which-runtime": { | |
| 1593 | "version": "1.3.2", | |
| 1594 | "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz", | |
| 1595 | "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", | |
| 1596 | "license": "Apache-2.0" | |
| 1597 | }, | |
| 1598 | "node_modules/z32": { | |
| 1599 | "version": "1.1.0", | |
| 1600 | "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz", | |
| 1601 | "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==", | |
| 1602 | "license": "MIT", | |
| 1603 | "dependencies": { | |
| 1604 | "b4a": "^1.5.3" | |
| 1605 | } | |
| 1606 | } | |
| 1607 | } | |
| 1608 | } |
| … | … | @@ -0,0 +1,28 @@ |
| 1 | { | |
| 2 | "name": "hyper-social-web", | |
| 3 | "version": "0.1.0", | |
| 4 | "description": "POSSE social network client — unified timeline and cross-posting for Mastodon, Bluesky, and Instagram", | |
| 5 | "type": "module", | |
| 6 | "main": "index.js", | |
| 7 | "scripts": { | |
| 8 | "start": "bare index.js serve", | |
| 9 | "test": "brittle test/*.test.js" | |
| 10 | }, | |
| 11 | "dependencies": { | |
| 12 | "b4a": "^1.8.0", | |
| 13 | "bare-crypto": "^1.13.4", | |
| 14 | "bare-ffmpeg": "^1.2.1", | |
| 15 | "bare-fs": "^4.5.6", | |
| 16 | "bare-http1": "^4.5.5", | |
| 17 | "bare-https": "^2.1.3", | |
| 18 | "bare-node-runtime": "^1.2.0", | |
| 19 | "bare-os": "^3.8.0", | |
| 20 | "bare-path": "^3.0.0", | |
| 21 | "bare-subprocess": "^5.2.3", | |
| 22 | "bridge-envelope": "file:../bridge-envelope", | |
| 23 | "bridge-vault": "file:../bridge-vault", | |
| 24 | "corestore": "^7.9.2", | |
| 25 | "hyper-feed": "file:../hyper-feed", | |
| 26 | "hyperbee": "^2.27.3" | |
| 27 | } | |
| 28 | } |
| … | … | @@ -0,0 +1,205 @@ |
| 1 | * { margin: 0; padding: 0; box-sizing: border-box; } | |
| 2 | ||
| 3 | :root { | |
| 4 | --bg: #0d1117; | |
| 5 | --bg-card: #161b22; | |
| 6 | --bg-input: #0d1117; | |
| 7 | --border: #30363d; | |
| 8 | --text: #e6edf3; | |
| 9 | --text-muted: #8b949e; | |
| 10 | --accent: #58a6ff; | |
| 11 | --accent-hover: #79c0ff; | |
| 12 | --green: #3fb950; | |
| 13 | --red: #f85149; | |
| 14 | --mastodon: #6364ff; | |
| 15 | --atproto: #0085ff; | |
| 16 | --instagram: #e1306c; | |
| 17 | --local: #3fb950; | |
| 18 | --radius: 8px; | |
| 19 | } | |
| 20 | ||
| 21 | body { | |
| 22 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; | |
| 23 | background: var(--bg); | |
| 24 | color: var(--text); | |
| 25 | line-height: 1.5; | |
| 26 | } | |
| 27 | ||
| 28 | #app { | |
| 29 | max-width: 640px; | |
| 30 | margin: 0 auto; | |
| 31 | padding: 0 16px; | |
| 32 | } | |
| 33 | ||
| 34 | /* Header */ | |
| 35 | header { | |
| 36 | display: flex; | |
| 37 | align-items: center; | |
| 38 | justify-content: space-between; | |
| 39 | padding: 16px 0; | |
| 40 | border-bottom: 1px solid var(--border); | |
| 41 | margin-bottom: 16px; | |
| 42 | } | |
| 43 | ||
| 44 | header h1 { font-size: 18px; font-weight: 600; } | |
| 45 | header h1 a { color: var(--text); text-decoration: none; } | |
| 46 | ||
| 47 | nav { display: flex; gap: 8px; } | |
| 48 | nav a { | |
| 49 | color: var(--text-muted); | |
| 50 | text-decoration: none; | |
| 51 | padding: 6px 12px; | |
| 52 | border-radius: var(--radius); | |
| 53 | font-size: 14px; | |
| 54 | } | |
| 55 | nav a:hover { color: var(--text); background: var(--bg-card); } | |
| 56 | nav a.active { color: var(--accent); background: var(--bg-card); } | |
| 57 | ||
| 58 | /* Buttons */ | |
| 59 | .btn { | |
| 60 | background: var(--bg-card); | |
| 61 | color: var(--text); | |
| 62 | border: 1px solid var(--border); | |
| 63 | padding: 8px 16px; | |
| 64 | border-radius: var(--radius); | |
| 65 | cursor: pointer; | |
| 66 | font-size: 14px; | |
| 67 | } | |
| 68 | .btn:hover { border-color: var(--accent); } | |
| 69 | .btn-primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600; } | |
| 70 | .btn-primary:hover { background: var(--accent-hover); } | |
| 71 | .btn-small { padding: 4px 10px; font-size: 12px; } | |
| 72 | .btn-block { display: block; width: 100%; margin-top: 12px; } | |
| 73 | .btn-danger { border-color: var(--red); color: var(--red); } | |
| 74 | ||
| 75 | /* Filters */ | |
| 76 | .filters { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; } | |
| 77 | .filter-btn { | |
| 78 | background: var(--bg-card); | |
| 79 | color: var(--text-muted); | |
| 80 | border: 1px solid var(--border); | |
| 81 | padding: 4px 12px; | |
| 82 | border-radius: 20px; | |
| 83 | cursor: pointer; | |
| 84 | font-size: 13px; | |
| 85 | } | |
| 86 | .filter-btn:hover { color: var(--text); border-color: var(--text-muted); } | |
| 87 | .filter-btn.active { color: var(--accent); border-color: var(--accent); } | |
| 88 | ||
| 89 | /* Feed */ | |
| 90 | .feed { display: flex; flex-direction: column; gap: 2px; } | |
| 91 | ||
| 92 | .post { | |
| 93 | background: var(--bg-card); | |
| 94 | border: 1px solid var(--border); | |
| 95 | border-radius: var(--radius); | |
| 96 | padding: 14px; | |
| 97 | } | |
| 98 | .post-header { | |
| 99 | display: flex; | |
| 100 | align-items: center; | |
| 101 | gap: 8px; | |
| 102 | margin-bottom: 6px; | |
| 103 | } | |
| 104 | .post-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } | |
| 105 | .post-avatar-placeholder { width: 40px; height: 40px; border-radius: 50%; background: var(--border); flex-shrink: 0; } | |
| 106 | .post-author-info { display: flex; flex-direction: column; gap: 0; min-width: 0; } | |
| 107 | .post-displayname { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| 108 | .post-author { font-size: 13px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| 109 | .post-platform { | |
| 110 | font-size: 11px; | |
| 111 | padding: 2px 6px; | |
| 112 | border-radius: 4px; | |
| 113 | font-weight: 600; | |
| 114 | text-transform: uppercase; | |
| 115 | } | |
| 116 | .post-platform.mastodon { background: var(--mastodon); color: #fff; } | |
| 117 | .post-platform.atproto { background: var(--atproto); color: #fff; } | |
| 118 | .post-platform.instagram { background: var(--instagram); color: #fff; } | |
| 119 | .post-platform.local { background: var(--local); color: #000; } | |
| 120 | .post-time { color: var(--text-muted); font-size: 12px; margin-left: auto; } | |
| 121 | .post-text { font-size: 14px; white-space: pre-wrap; word-wrap: break-word; } | |
| 122 | .post-media { margin-top: 8px; } | |
| 123 | .post-media img { max-width: 100%; border-radius: var(--radius); } | |
| 124 | .post-meta { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: var(--text-muted); } | |
| 125 | .post-syndicated { margin-top: 6px; font-size: 11px; color: var(--text-muted); } | |
| 126 | ||
| 127 | .post-type-boost { border-left: 3px solid var(--text-muted); } | |
| 128 | .post-type-reply { border-left: 3px solid var(--accent); } | |
| 129 | ||
| 130 | /* Compose */ | |
| 131 | .compose-box { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; } | |
| 132 | .compose-box textarea { | |
| 133 | width: 100%; | |
| 134 | background: var(--bg-input); | |
| 135 | color: var(--text); | |
| 136 | border: 1px solid var(--border); | |
| 137 | border-radius: var(--radius); | |
| 138 | padding: 10px; | |
| 139 | font-size: 14px; | |
| 140 | font-family: inherit; | |
| 141 | resize: vertical; | |
| 142 | } | |
| 143 | .compose-box textarea:focus { outline: none; border-color: var(--accent); } | |
| 144 | .compose-media { margin: 10px 0; } | |
| 145 | #media-preview { display: flex; align-items: center; gap: 8px; padding: 8px; background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); } | |
| 146 | #preview-name { font-size: 13px; color: var(--text-muted); flex: 1; } | |
| 147 | #alt-text { width: 100%; background: var(--bg-input); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; font-size: 13px; } | |
| 148 | #alt-text:focus { outline: none; border-color: var(--accent); } | |
| 149 | .compose-targets { margin: 12px 0; display: flex; gap: 16px; } | |
| 150 | .compose-targets label { font-size: 14px; display: flex; align-items: center; gap: 4px; } | |
| 151 | .compose-actions { display: flex; justify-content: space-between; align-items: center; } | |
| 152 | #char-count { font-size: 13px; color: var(--text-muted); } | |
| 153 | ||
| 154 | /* Settings */ | |
| 155 | .connect-form { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; } | |
| 156 | .connect-form select, .connect-form input { | |
| 157 | display: block; | |
| 158 | width: 100%; | |
| 159 | background: var(--bg-input); | |
| 160 | color: var(--text); | |
| 161 | border: 1px solid var(--border); | |
| 162 | border-radius: var(--radius); | |
| 163 | padding: 8px 10px; | |
| 164 | font-size: 14px; | |
| 165 | margin-bottom: 8px; | |
| 166 | } | |
| 167 | .connect-form select:focus, .connect-form input:focus { outline: none; border-color: var(--accent); } | |
| 168 | .connect-form .btn { margin-top: 8px; } | |
| 169 | ||
| 170 | .account-card { | |
| 171 | background: var(--bg-card); | |
| 172 | border: 1px solid var(--border); | |
| 173 | border-radius: var(--radius); | |
| 174 | padding: 12px; | |
| 175 | margin-bottom: 8px; | |
| 176 | display: flex; | |
| 177 | align-items: center; | |
| 178 | justify-content: space-between; | |
| 179 | } | |
| 180 | .account-info { display: flex; align-items: center; gap: 8px; } | |
| 181 | ||
| 182 | /* Notifications */ | |
| 183 | .notification { | |
| 184 | background: var(--bg-card); | |
| 185 | border: 1px solid var(--border); | |
| 186 | border-radius: var(--radius); | |
| 187 | padding: 10px 14px; | |
| 188 | font-size: 14px; | |
| 189 | } | |
| 190 | .notification .notif-type { font-weight: 600; text-transform: capitalize; } | |
| 191 | ||
| 192 | /* Result messages */ | |
| 193 | .result { margin-top: 10px; padding: 8px 12px; border-radius: var(--radius); font-size: 13px; } | |
| 194 | .result.success { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); } | |
| 195 | .result.error { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid rgba(248,81,73,0.3); } | |
| 196 | ||
| 197 | h2 { font-size: 18px; margin-bottom: 12px; } | |
| 198 | h3 { font-size: 15px; margin: 16px 0 8px; color: var(--text-muted); } | |
| 199 | ||
| 200 | /* Responsive */ | |
| 201 | @media (max-width: 480px) { | |
| 202 | header { flex-direction: column; gap: 8px; } | |
| 203 | nav { flex-wrap: wrap; justify-content: center; } | |
| 204 | .compose-targets { flex-direction: column; gap: 8px; } | |
| 205 | } |
| … | … | @@ -0,0 +1,105 @@ |
| 1 | <!DOCTYPE html> | |
| 2 | <html lang="en"> | |
| 3 | <head> | |
| 4 | <meta charset="utf-8"> | |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 6 | <title>hyper-social</title> | |
| 7 | <link rel="stylesheet" href="css/app.css"> | |
| 8 | </head> | |
| 9 | <body> | |
| 10 | <div id="app"> | |
| 11 | <header> | |
| 12 | <h1><a href="#/">hyper-social</a></h1> | |
| 13 | <nav> | |
| 14 | <a href="#/" class="nav-link active" data-page="timeline">Timeline</a> | |
| 15 | <a href="#/compose" class="nav-link" data-page="compose">Compose</a> | |
| 16 | <a href="#/notifications" class="nav-link" data-page="notifications">Notifications</a> | |
| 17 | <a href="#/settings" class="nav-link" data-page="settings">Settings</a> | |
| 18 | </nav> | |
| 19 | </header> | |
| 20 | ||
| 21 | <main> | |
| 22 | <!-- Timeline filters --> | |
| 23 | <div id="page-timeline" class="page"> | |
| 24 | <div class="filters"> | |
| 25 | <button class="filter-btn active" data-source="">All</button> | |
| 26 | <button class="filter-btn" data-source="mastodon">Mastodon</button> | |
| 27 | <button class="filter-btn" data-source="atproto">Bluesky</button> | |
| 28 | <button class="filter-btn" data-source="instagram">Instagram</button> | |
| 29 | <button class="filter-btn" data-source="local">My Posts</button> | |
| 30 | </div> | |
| 31 | <button id="refresh-btn" class="btn btn-small">Refresh</button> | |
| 32 | <div id="timeline" class="feed"></div> | |
| 33 | <button id="load-more" class="btn btn-block" style="display:none">Load more</button> | |
| 34 | </div> | |
| 35 | ||
| 36 | <!-- Compose (POSSE) --> | |
| 37 | <div id="page-compose" class="page" style="display:none"> | |
| 38 | <h2>Compose</h2> | |
| 39 | <div class="compose-box"> | |
| 40 | <textarea id="compose-text" placeholder="What's on your mind?" rows="4"></textarea> | |
| 41 | <div class="compose-media"> | |
| 42 | <input type="file" id="media-input" accept="image/*,video/*" style="display:none"> | |
| 43 | <button id="attach-btn" class="btn btn-small">Attach image/video</button> | |
| 44 | <div id="media-preview" style="display:none"> | |
| 45 | <img id="preview-img" style="max-width:200px;max-height:150px;border-radius:6px"> | |
| 46 | <span id="preview-name"></span> | |
| 47 | <button id="remove-media" class="btn btn-small btn-danger">Remove</button> | |
| 48 | </div> | |
| 49 | <input type="text" id="alt-text" placeholder="Alt text (accessibility)" style="display:none;margin-top:6px"> | |
| 50 | </div> | |
| 51 | <div class="compose-targets"> | |
| 52 | <label><input type="checkbox" name="target" value="mastodon" checked> Mastodon</label> | |
| 53 | <label><input type="checkbox" name="target" value="atproto" checked> Bluesky</label> | |
| 54 | <label><input type="checkbox" name="target" value="instagram" disabled> Instagram</label> | |
| 55 | </div> | |
| 56 | <div class="compose-actions"> | |
| 57 | <span id="char-count">0</span> | |
| 58 | <button id="publish-btn" class="btn btn-primary">Publish</button> | |
| 59 | </div> | |
| 60 | <div id="publish-result" class="result" style="display:none"></div> | |
| 61 | </div> | |
| 62 | </div> | |
| 63 | ||
| 64 | <!-- Notifications --> | |
| 65 | <div id="page-notifications" class="page" style="display:none"> | |
| 66 | <h2>Notifications</h2> | |
| 67 | <div id="notifications" class="feed"></div> | |
| 68 | </div> | |
| 69 | ||
| 70 | <!-- Settings --> | |
| 71 | <div id="page-settings" class="page" style="display:none"> | |
| 72 | <h2>Connected Accounts</h2> | |
| 73 | <div id="accounts-list"></div> | |
| 74 | ||
| 75 | <h3>Add Account</h3> | |
| 76 | <div class="connect-form"> | |
| 77 | <select id="connect-platform"> | |
| 78 | <option value="mastodon">Mastodon</option> | |
| 79 | <option value="atproto">Bluesky</option> | |
| 80 | </select> | |
| 81 | ||
| 82 | <div id="mastodon-fields" class="platform-fields"> | |
| 83 | <input type="text" id="masto-instance" placeholder="Instance (e.g., mastodon.social)"> | |
| 84 | <input type="password" id="masto-token" placeholder="Access token"> | |
| 85 | </div> | |
| 86 | ||
| 87 | <div id="atproto-fields" class="platform-fields" style="display:none"> | |
| 88 | <input type="text" id="bsky-handle" placeholder="Handle (e.g., you.bsky.social)"> | |
| 89 | <input type="password" id="bsky-password" placeholder="App password"> | |
| 90 | <input type="text" id="bsky-service" placeholder="Service (default: https://bsky.social)" value="https://bsky.social"> | |
| 91 | </div> | |
| 92 | ||
| 93 | <button id="connect-btn" class="btn btn-primary">Connect</button> | |
| 94 | <div id="connect-result" class="result" style="display:none"></div> | |
| 95 | </div> | |
| 96 | </div> | |
| 97 | </main> | |
| 98 | </div> | |
| 99 | ||
| 100 | <script src="js/api.js"></script> | |
| 101 | <script src="js/timeline.js"></script> | |
| 102 | <script src="js/composer.js"></script> | |
| 103 | <script src="js/app.js"></script> | |
| 104 | </body> | |
| 105 | </html> |
| … | … | @@ -0,0 +1,61 @@ |
| 1 | /** | |
| 2 | * API client for hyper-social-web backend | |
| 3 | */ | |
| 4 | const API = { | |
| 5 | // Base path — empty for direct access, or prefix like '/social' behind reverse proxy | |
| 6 | // Detected from the path that loaded index.html | |
| 7 | base: (function () { | |
| 8 | const p = window.location.pathname.replace(/\/index\.html$/, '').replace(/\/+$/, '') | |
| 9 | return p || '' | |
| 10 | })(), | |
| 11 | ||
| 12 | async post (text, targets) { | |
| 13 | const res = await fetch(this.base + '/api/post', { | |
| 14 | method: 'POST', | |
| 15 | headers: { 'Content-Type': 'application/json' }, | |
| 16 | body: JSON.stringify({ text, targets }) | |
| 17 | }) | |
| 18 | return res.json() | |
| 19 | }, | |
| 20 | ||
| 21 | async timeline (opts = {}) { | |
| 22 | const params = new URLSearchParams() | |
| 23 | if (opts.source) params.set('source', opts.source) | |
| 24 | if (opts.limit) params.set('limit', opts.limit) | |
| 25 | if (opts.before) params.set('before', opts.before) | |
| 26 | const qs = params.toString() | |
| 27 | const res = await fetch(this.base + '/api/timeline' + (qs ? '?' + qs : '')) | |
| 28 | return res.json() | |
| 29 | }, | |
| 30 | ||
| 31 | async refresh () { | |
| 32 | const res = await fetch(this.base + '/api/feed/refresh') | |
| 33 | return res.json() | |
| 34 | }, | |
| 35 | ||
| 36 | async accounts () { | |
| 37 | const res = await fetch(this.base + '/api/accounts') | |
| 38 | return res.json() | |
| 39 | }, | |
| 40 | ||
| 41 | async connect (data) { | |
| 42 | const res = await fetch(this.base + '/api/accounts/connect', { | |
| 43 | method: 'POST', | |
| 44 | headers: { 'Content-Type': 'application/json' }, | |
| 45 | body: JSON.stringify(data) | |
| 46 | }) | |
| 47 | return res.json() | |
| 48 | }, | |
| 49 | ||
| 50 | async disconnect (id) { | |
| 51 | const res = await fetch(this.base + '/api/accounts/' + encodeURIComponent(id), { | |
| 52 | method: 'DELETE' | |
| 53 | }) | |
| 54 | return res.json() | |
| 55 | }, | |
| 56 | ||
| 57 | async notifications () { | |
| 58 | const res = await fetch(this.base + '/api/notifications') | |
| 59 | return res.json() | |
| 60 | } | |
| 61 | } |
| … | … | @@ -0,0 +1,199 @@ |
| 1 | /** | |
| 2 | * hyper-social-web — SPA router and initialization | |
| 3 | */ | |
| 4 | ||
| 5 | let currentSource = '' | |
| 6 | let oldestTimestamp = null | |
| 7 | ||
| 8 | // --- Router --- | |
| 9 | function route () { | |
| 10 | const hash = window.location.hash || '#/' | |
| 11 | const pages = document.querySelectorAll('.page') | |
| 12 | const links = document.querySelectorAll('.nav-link') | |
| 13 | ||
| 14 | pages.forEach(p => { p.style.display = 'none' }) | |
| 15 | links.forEach(l => l.classList.remove('active')) | |
| 16 | ||
| 17 | if (hash.startsWith('#/compose')) { | |
| 18 | document.getElementById('page-compose').style.display = 'block' | |
| 19 | document.querySelector('[data-page="compose"]').classList.add('active') | |
| 20 | } else if (hash.startsWith('#/notifications')) { | |
| 21 | document.getElementById('page-notifications').style.display = 'block' | |
| 22 | document.querySelector('[data-page="notifications"]').classList.add('active') | |
| 23 | loadNotifications() | |
| 24 | } else if (hash.startsWith('#/settings')) { | |
| 25 | document.getElementById('page-settings').style.display = 'block' | |
| 26 | document.querySelector('[data-page="settings"]').classList.add('active') | |
| 27 | loadAccounts() | |
| 28 | } else { | |
| 29 | document.getElementById('page-timeline').style.display = 'block' | |
| 30 | document.querySelector('[data-page="timeline"]').classList.add('active') | |
| 31 | loadTimeline() | |
| 32 | } | |
| 33 | } | |
| 34 | ||
| 35 | // --- Timeline --- | |
| 36 | async function loadTimeline (append) { | |
| 37 | const container = document.getElementById('timeline') | |
| 38 | const loadMoreBtn = document.getElementById('load-more') | |
| 39 | ||
| 40 | if (!append) { | |
| 41 | container.innerHTML = '<div style="color:var(--text-muted);padding:20px;text-align:center">Loading...</div>' | |
| 42 | oldestTimestamp = null | |
| 43 | } | |
| 44 | ||
| 45 | const opts = { limit: 30 } | |
| 46 | if (currentSource) opts.source = currentSource | |
| 47 | if (append && oldestTimestamp) opts.before = oldestTimestamp | |
| 48 | ||
| 49 | let data | |
| 50 | try { | |
| 51 | console.log('[timeline] fetching', API.base + '/api/timeline', opts) | |
| 52 | data = await API.timeline(opts) | |
| 53 | console.log('[timeline] got', data.items ? data.items.length : 0, 'items') | |
| 54 | } catch (err) { | |
| 55 | console.error('[timeline] fetch error:', err) | |
| 56 | container.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Error loading timeline: ' + err.message + '</div>' | |
| 57 | return | |
| 58 | } | |
| 59 | ||
| 60 | const items = data.items || [] | |
| 61 | ||
| 62 | if (!append) container.innerHTML = '' | |
| 63 | ||
| 64 | if (items.length === 0 && !append) { | |
| 65 | container.innerHTML = '<div style="color:var(--text-muted);padding:40px;text-align:center">No posts yet. Connect an account in Settings, then Refresh.</div>' | |
| 66 | loadMoreBtn.style.display = 'none' | |
| 67 | return | |
| 68 | } | |
| 69 | ||
| 70 | for (const item of items) { | |
| 71 | container.appendChild(renderPost(item)) | |
| 72 | oldestTimestamp = item.timestamp | |
| 73 | } | |
| 74 | ||
| 75 | loadMoreBtn.style.display = items.length >= 30 ? 'block' : 'none' | |
| 76 | } | |
| 77 | ||
| 78 | // --- Notifications --- | |
| 79 | async function loadNotifications () { | |
| 80 | const container = document.getElementById('notifications') | |
| 81 | container.innerHTML = '<div style="color:var(--text-muted);padding:20px;text-align:center">Loading...</div>' | |
| 82 | ||
| 83 | const data = await API.notifications() | |
| 84 | const notifs = data.notifications || [] | |
| 85 | container.innerHTML = '' | |
| 86 | ||
| 87 | if (notifs.length === 0) { | |
| 88 | container.innerHTML = '<div style="color:var(--text-muted);padding:40px;text-align:center">No notifications</div>' | |
| 89 | return | |
| 90 | } | |
| 91 | ||
| 92 | for (const n of notifs) { | |
| 93 | container.appendChild(renderNotification(n)) | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | // --- Accounts --- | |
| 98 | async function loadAccounts () { | |
| 99 | const container = document.getElementById('accounts-list') | |
| 100 | container.innerHTML = '' | |
| 101 | ||
| 102 | const data = await API.accounts() | |
| 103 | const accounts = data.accounts || [] | |
| 104 | ||
| 105 | if (accounts.length === 0) { | |
| 106 | container.innerHTML = '<div style="color:var(--text-muted);padding:12px">No accounts connected</div>' | |
| 107 | return | |
| 108 | } | |
| 109 | ||
| 110 | for (const account of accounts) { | |
| 111 | container.appendChild(renderAccountCard(account)) | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | async function disconnectAccount (id) { | |
| 116 | if (!confirm('Disconnect this account?')) return | |
| 117 | await API.disconnect(id) | |
| 118 | loadAccounts() | |
| 119 | } | |
| 120 | ||
| 121 | // --- Init --- | |
| 122 | document.addEventListener('DOMContentLoaded', () => { | |
| 123 | initComposer() | |
| 124 | ||
| 125 | // Filter buttons | |
| 126 | document.querySelectorAll('.filter-btn').forEach(btn => { | |
| 127 | btn.addEventListener('click', () => { | |
| 128 | document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')) | |
| 129 | btn.classList.add('active') | |
| 130 | currentSource = btn.dataset.source | |
| 131 | loadTimeline() | |
| 132 | }) | |
| 133 | }) | |
| 134 | ||
| 135 | // Refresh button | |
| 136 | document.getElementById('refresh-btn').addEventListener('click', async () => { | |
| 137 | const btn = document.getElementById('refresh-btn') | |
| 138 | btn.textContent = 'Refreshing...' | |
| 139 | btn.disabled = true | |
| 140 | try { | |
| 141 | const data = await API.refresh() | |
| 142 | btn.textContent = `Refreshed (${data.refreshed} new)` | |
| 143 | loadTimeline() | |
| 144 | } catch (err) { | |
| 145 | btn.textContent = 'Error' | |
| 146 | } | |
| 147 | setTimeout(() => { btn.textContent = 'Refresh'; btn.disabled = false }, 2000) | |
| 148 | }) | |
| 149 | ||
| 150 | // Load more | |
| 151 | document.getElementById('load-more').addEventListener('click', () => loadTimeline(true)) | |
| 152 | ||
| 153 | // Platform fields toggle | |
| 154 | document.getElementById('connect-platform').addEventListener('change', (e) => { | |
| 155 | document.getElementById('mastodon-fields').style.display = e.target.value === 'mastodon' ? 'block' : 'none' | |
| 156 | document.getElementById('atproto-fields').style.display = e.target.value === 'atproto' ? 'block' : 'none' | |
| 157 | }) | |
| 158 | ||
| 159 | // Connect account | |
| 160 | document.getElementById('connect-btn').addEventListener('click', async () => { | |
| 161 | const platform = document.getElementById('connect-platform').value | |
| 162 | const resultEl = document.getElementById('connect-result') | |
| 163 | let data | |
| 164 | ||
| 165 | if (platform === 'mastodon') { | |
| 166 | data = { | |
| 167 | platform: 'mastodon', | |
| 168 | instance: document.getElementById('masto-instance').value.trim(), | |
| 169 | token: document.getElementById('masto-token').value.trim() | |
| 170 | } | |
| 171 | if (!data.instance || !data.token) { showResult(resultEl, 'Instance and token required', 'error'); return } | |
| 172 | data.handle = data.instance | |
| 173 | } else if (platform === 'atproto') { | |
| 174 | data = { | |
| 175 | platform: 'atproto', | |
| 176 | handle: document.getElementById('bsky-handle').value.trim(), | |
| 177 | password: document.getElementById('bsky-password').value.trim(), | |
| 178 | instance: document.getElementById('bsky-service').value.trim() || 'https://bsky.social' | |
| 179 | } | |
| 180 | if (!data.handle || !data.password) { showResult(resultEl, 'Handle and app password required', 'error'); return } | |
| 181 | } | |
| 182 | ||
| 183 | try { | |
| 184 | const res = await API.connect(data) | |
| 185 | if (res.error) { | |
| 186 | showResult(resultEl, res.error, 'error') | |
| 187 | } else { | |
| 188 | showResult(resultEl, 'Connected: ' + (res.account?.id || 'success'), 'success') | |
| 189 | loadAccounts() | |
| 190 | } | |
| 191 | } catch (err) { | |
| 192 | showResult(resultEl, err.message, 'error') | |
| 193 | } | |
| 194 | }) | |
| 195 | ||
| 196 | // Route | |
| 197 | window.addEventListener('hashchange', route) | |
| 198 | route() | |
| 199 | }) |
| … | … | @@ -0,0 +1,133 @@ |
| 1 | /** | |
| 2 | * POSSE Composer — text + media upload | |
| 3 | */ | |
| 4 | ||
| 5 | let selectedFile = null | |
| 6 | ||
| 7 | function initComposer () { | |
| 8 | const textarea = document.getElementById('compose-text') | |
| 9 | const charCount = document.getElementById('char-count') | |
| 10 | const publishBtn = document.getElementById('publish-btn') | |
| 11 | const resultEl = document.getElementById('publish-result') | |
| 12 | const mediaInput = document.getElementById('media-input') | |
| 13 | const attachBtn = document.getElementById('attach-btn') | |
| 14 | const mediaPreview = document.getElementById('media-preview') | |
| 15 | const previewImg = document.getElementById('preview-img') | |
| 16 | const previewName = document.getElementById('preview-name') | |
| 17 | const removeMedia = document.getElementById('remove-media') | |
| 18 | const altText = document.getElementById('alt-text') | |
| 19 | ||
| 20 | textarea.addEventListener('input', () => { | |
| 21 | charCount.textContent = textarea.value.length | |
| 22 | }) | |
| 23 | ||
| 24 | // Attach media | |
| 25 | attachBtn.addEventListener('click', () => mediaInput.click()) | |
| 26 | ||
| 27 | mediaInput.addEventListener('change', () => { | |
| 28 | const file = mediaInput.files[0] | |
| 29 | if (!file) return | |
| 30 | ||
| 31 | selectedFile = file | |
| 32 | previewName.textContent = file.name + ' (' + formatSize(file.size) + ')' | |
| 33 | altText.style.display = 'block' | |
| 34 | ||
| 35 | if (file.type.startsWith('image/')) { | |
| 36 | const url = URL.createObjectURL(file) | |
| 37 | previewImg.src = url | |
| 38 | previewImg.style.display = 'block' | |
| 39 | } else { | |
| 40 | previewImg.style.display = 'none' | |
| 41 | } | |
| 42 | ||
| 43 | mediaPreview.style.display = 'flex' | |
| 44 | attachBtn.style.display = 'none' | |
| 45 | }) | |
| 46 | ||
| 47 | removeMedia.addEventListener('click', () => { | |
| 48 | selectedFile = null | |
| 49 | mediaInput.value = '' | |
| 50 | mediaPreview.style.display = 'none' | |
| 51 | attachBtn.style.display = 'inline-block' | |
| 52 | altText.style.display = 'none' | |
| 53 | altText.value = '' | |
| 54 | previewImg.src = '' | |
| 55 | }) | |
| 56 | ||
| 57 | // Publish | |
| 58 | publishBtn.addEventListener('click', async () => { | |
| 59 | const text = textarea.value.trim() | |
| 60 | if (!text) return | |
| 61 | ||
| 62 | const targets = [] | |
| 63 | document.querySelectorAll('input[name="target"]:checked').forEach(cb => { | |
| 64 | targets.push(cb.value) | |
| 65 | }) | |
| 66 | ||
| 67 | if (targets.length === 0) { | |
| 68 | showResult(resultEl, 'Select at least one platform', 'error') | |
| 69 | return | |
| 70 | } | |
| 71 | ||
| 72 | publishBtn.disabled = true | |
| 73 | publishBtn.textContent = selectedFile ? 'Processing & publishing...' : 'Publishing...' | |
| 74 | ||
| 75 | try { | |
| 76 | let data | |
| 77 | ||
| 78 | if (selectedFile) { | |
| 79 | // Multipart upload with media | |
| 80 | const form = new FormData() | |
| 81 | form.append('text', text) | |
| 82 | form.append('targets', JSON.stringify(targets)) | |
| 83 | form.append('altText', altText.value || '') | |
| 84 | form.append('file', selectedFile) | |
| 85 | ||
| 86 | const res = await fetch(API.base + '/api/post-media', { | |
| 87 | method: 'POST', | |
| 88 | body: form | |
| 89 | }) | |
| 90 | data = await res.json() | |
| 91 | } else { | |
| 92 | // Text-only | |
| 93 | data = await API.post(text, targets) | |
| 94 | } | |
| 95 | ||
| 96 | if (data.error) { | |
| 97 | showResult(resultEl, data.error, 'error') | |
| 98 | } else if (data.errors && Object.keys(data.errors).length > 0) { | |
| 99 | const errMsg = Object.entries(data.errors).map(([k, v]) => `${k}: ${v}`).join(', ') | |
| 100 | showResult(resultEl, 'Partial: ' + errMsg, 'error') | |
| 101 | } else { | |
| 102 | showResult(resultEl, 'Published to ' + targets.join(', '), 'success') | |
| 103 | textarea.value = '' | |
| 104 | charCount.textContent = '0' | |
| 105 | // Clear media | |
| 106 | selectedFile = null | |
| 107 | mediaInput.value = '' | |
| 108 | mediaPreview.style.display = 'none' | |
| 109 | attachBtn.style.display = 'inline-block' | |
| 110 | altText.style.display = 'none' | |
| 111 | altText.value = '' | |
| 112 | } | |
| 113 | } catch (err) { | |
| 114 | showResult(resultEl, err.message, 'error') | |
| 115 | } | |
| 116 | ||
| 117 | publishBtn.disabled = false | |
| 118 | publishBtn.textContent = 'Publish' | |
| 119 | }) | |
| 120 | } | |
| 121 | ||
| 122 | function formatSize (bytes) { | |
| 123 | if (bytes < 1024) return bytes + ' B' | |
| 124 | if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' | |
| 125 | return (bytes / (1024 * 1024)).toFixed(1) + ' MB' | |
| 126 | } | |
| 127 | ||
| 128 | function showResult (el, msg, type) { | |
| 129 | el.style.display = 'block' | |
| 130 | el.className = 'result ' + type | |
| 131 | el.textContent = msg | |
| 132 | setTimeout(() => { el.style.display = 'none' }, 5000) | |
| 133 | } |
| … | … | @@ -0,0 +1,113 @@ |
| 1 | /** | |
| 2 | * Timeline rendering | |
| 3 | */ | |
| 4 | ||
| 5 | function renderPost (item) { | |
| 6 | const el = document.createElement('div') | |
| 7 | el.className = 'post' + (item.type === 'boost' ? ' post-type-boost' : '') + (item.type === 'reply' ? ' post-type-reply' : '') | |
| 8 | ||
| 9 | const platformLabel = { | |
| 10 | mastodon: 'Mastodon', | |
| 11 | atproto: 'Bluesky', | |
| 12 | instagram: 'Instagram', | |
| 13 | local: 'Local' | |
| 14 | } | |
| 15 | ||
| 16 | const time = new Date(item.timestamp) | |
| 17 | const timeStr = time.toLocaleDateString() + ' ' + time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) | |
| 18 | ||
| 19 | let boostInfo = '' | |
| 20 | if (item.type === 'boost' && item.metadata?.boostBy) { | |
| 21 | boostInfo = `<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px">${item.metadata.boostBy} reposted</div>` | |
| 22 | } | |
| 23 | ||
| 24 | let mediaHtml = '' | |
| 25 | if (item.mediaUrl) { | |
| 26 | if (item.mediaType === 'image' || item.mediaType === 'gifv') { | |
| 27 | mediaHtml = `<div class="post-media"><img src="${escapeHtml(item.mediaUrl)}" loading="lazy" alt=""></div>` | |
| 28 | } else if (item.mediaType === 'video') { | |
| 29 | mediaHtml = `<div class="post-media"><a href="${escapeHtml(item.mediaUrl)}" target="_blank">[video]</a></div>` | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | let metaHtml = '' | |
| 34 | if (item.metadata) { | |
| 35 | const parts = [] | |
| 36 | if (item.metadata.likeCount) parts.push(item.metadata.likeCount + ' likes') | |
| 37 | if (item.metadata.repostCount) parts.push(item.metadata.repostCount + ' reposts') | |
| 38 | if (item.metadata.replyCount) parts.push(item.metadata.replyCount + ' replies') | |
| 39 | if (parts.length > 0) metaHtml = `<div class="post-meta">${parts.join(' · ')}</div>` | |
| 40 | } | |
| 41 | ||
| 42 | let syndicatedHtml = '' | |
| 43 | if (item.syndicated) { | |
| 44 | const platforms = Object.values(item.syndicated).map(s => s.platform).join(', ') | |
| 45 | syndicatedHtml = `<div class="post-syndicated">Published to: ${platforms}</div>` | |
| 46 | } | |
| 47 | ||
| 48 | let urlHtml = '' | |
| 49 | if (item.url) { | |
| 50 | urlHtml = `<div style="margin-top:4px"><a href="${escapeHtml(item.url)}" target="_blank" style="color:var(--accent);font-size:12px">View on ${platformLabel[item.source] || item.source}</a></div>` | |
| 51 | } | |
| 52 | ||
| 53 | const avatar = item.metadata?.avatar || '' | |
| 54 | const displayName = item.metadata?.displayName || '' | |
| 55 | const avatarHtml = avatar ? `<img class="post-avatar" src="${escapeHtml(avatar)}" alt="">` : `<div class="post-avatar post-avatar-placeholder"></div>` | |
| 56 | ||
| 57 | el.innerHTML = ` | |
| 58 | ${boostInfo} | |
| 59 | <div class="post-header"> | |
| 60 | ${avatarHtml} | |
| 61 | <div class="post-author-info"> | |
| 62 | ${displayName ? `<span class="post-displayname">${escapeHtml(displayName)}</span>` : ''} | |
| 63 | <span class="post-author">${escapeHtml(item.author)}</span> | |
| 64 | </div> | |
| 65 | <span class="post-platform ${item.source}">${platformLabel[item.source] || item.source}</span> | |
| 66 | <span class="post-time">${timeStr}</span> | |
| 67 | </div> | |
| 68 | <div class="post-text">${escapeHtml(item.text)}</div> | |
| 69 | ${mediaHtml} | |
| 70 | ${metaHtml} | |
| 71 | ${syndicatedHtml} | |
| 72 | ${urlHtml} | |
| 73 | ` | |
| 74 | return el | |
| 75 | } | |
| 76 | ||
| 77 | function renderNotification (notif) { | |
| 78 | const el = document.createElement('div') | |
| 79 | el.className = 'notification' | |
| 80 | ||
| 81 | const platformLabel = { mastodon: 'Mastodon', atproto: 'Bluesky' } | |
| 82 | const time = new Date(notif.timestamp) | |
| 83 | const timeStr = time.toLocaleDateString() + ' ' + time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) | |
| 84 | ||
| 85 | el.innerHTML = ` | |
| 86 | <div class="post-header"> | |
| 87 | <span class="notif-type">${escapeHtml(notif.type)}</span> | |
| 88 | <span class="post-platform ${notif.platform}">${platformLabel[notif.platform] || notif.platform}</span> | |
| 89 | <span class="post-time">${timeStr}</span> | |
| 90 | </div> | |
| 91 | <div><strong>${escapeHtml(notif.author)}</strong></div> | |
| 92 | ${notif.text ? `<div class="post-text">${escapeHtml(notif.text)}</div>` : ''} | |
| 93 | ` | |
| 94 | return el | |
| 95 | } | |
| 96 | ||
| 97 | function renderAccountCard (account) { | |
| 98 | const el = document.createElement('div') | |
| 99 | el.className = 'account-card' | |
| 100 | el.innerHTML = ` | |
| 101 | <div class="account-info"> | |
| 102 | <span class="post-platform ${account.platform}">${account.platform}</span> | |
| 103 | <span>${escapeHtml(account.handle || account.instance || account.id)}</span> | |
| 104 | </div> | |
| 105 | <button class="btn btn-small btn-danger" onclick="disconnectAccount('${escapeHtml(account.id)}')">Disconnect</button> | |
| 106 | ` | |
| 107 | return el | |
| 108 | } | |
| 109 | ||
| 110 | function escapeHtml (str) { | |
| 111 | if (!str) return '' | |
| 112 | return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | |
| 113 | } |