vibeshare-live 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ import { ControlState, WriteArbiter, RelayHandle } from 'vibelive-cli';
2
+ import { ConsentLedger } from '@pooriaarab/vibe-core';
3
+
4
+ /**
5
+ * The access gate — the correctness-critical piece of vibeshare.
6
+ *
7
+ * Background (docs/spec.md · "Access / safety"): a shared link carries an access
8
+ * policy. **Spectators** are read-only: they see the live agent output stream but
9
+ * can never drive the wrapped agent. **Invite** viewers may be promoted to a full
10
+ * vibelive participant (request control → drive).
11
+ *
12
+ * The invariant vibeshare adds on top of vibelive:
13
+ *
14
+ * > A spectator NEVER obtains the write token.
15
+ *
16
+ * We do NOT enforce this in the UI only. We enforce it by never letting a
17
+ * spectator reach the write-arbitration state machine: the gate swallows their
18
+ * `requestControl` and returns `null` (denied) without touching the arbiter, so a
19
+ * spectator is never the driver and never enters the FIFO queue. The same
20
+ * `WriteArbiter` vibelive uses to guarantee "never two concurrent writers" is the
21
+ * witness we assert against (see `src/access.test.ts`): after a spectator request,
22
+ * `arbiter.isDriver(spectatorId) === false` and the queue does not contain them.
23
+ *
24
+ * This module is pure logic over an injected arbiter — no IO, no network — so the
25
+ * invariant is fully unit-testable.
26
+ */
27
+
28
+ /** Who a share link lets its holders do. */
29
+ type AccessMode = 'spectate' | 'invite';
30
+ /** Default access when a share is created without an explicit policy. */
31
+ declare const DEFAULT_ACCESS: AccessMode;
32
+ /** The role a particular viewer currently holds inside a share. */
33
+ type ViewerRole = 'spectator' | 'participant';
34
+ /** Denial reasons surfaced to callers / tests. */
35
+ type DenialReason = 'spectator' | 'not-promoted' | 'unknown';
36
+ /** Result of a (possibly-denied) control request. */
37
+ type ControlRequestResult = {
38
+ readonly ok: true;
39
+ readonly state: ControlState;
40
+ } | {
41
+ readonly ok: false;
42
+ readonly reason: DenialReason;
43
+ };
44
+ interface AccessGateOptions {
45
+ /** The real vibelive write-arbiter the gate mediates. */
46
+ readonly arbiter: WriteArbiter;
47
+ /** The share's access policy. */
48
+ readonly access?: AccessMode;
49
+ /**
50
+ * Optional passphrase. When set, `verifyPassphrase` must pass before a viewer is
51
+ * admitted at all (a second factor on top of the capability URL).
52
+ */
53
+ readonly passphrase?: string;
54
+ }
55
+ /**
56
+ * The access gate. Owns the per-viewer role bookkeeping and mediates every write
57
+ * request through the injected vibelive {@link WriteArbiter}. Stateless w.r.t. the
58
+ * network — the CLI/MCP wire viewers in and call `requestControl`/`approve`.
59
+ */
60
+ interface AccessGate {
61
+ /** This share's access policy. */
62
+ readonly access: AccessMode;
63
+ /** True iff a passphrase is required to join this share. */
64
+ readonly hasPassphrase: boolean;
65
+ /** Admit a new viewer (recorded at the share's default role). Idempotent. */
66
+ admit(viewer: {
67
+ readonly id: string;
68
+ readonly name: string;
69
+ }, passphrase?: string): boolean;
70
+ /** Remove a viewer (e.g. on disconnect / kick). Releases the token if they hold it. */
71
+ remove(viewerId: string): void;
72
+ /** True iff this viewer id is known to the gate. */
73
+ has(viewerId: string): boolean;
74
+ /** This viewer's current role, or `undefined` if unknown. */
75
+ role(viewerId: string): ViewerRole | undefined;
76
+ /**
77
+ * Promote an invite viewer into a full participant (eligible to drive).
78
+ * Only meaningful under `access: 'invite'`. Returns false if the viewer is
79
+ * unknown or the share is spectate-only.
80
+ */
81
+ promote(viewerId: string): boolean;
82
+ /**
83
+ * Request the write token on behalf of a viewer.
84
+ *
85
+ * - Spectators (any share) → denied (`reason: 'spectator'`), arbiter untouched.
86
+ * - Invite viewers not yet promoted → denied (`reason: 'not-promoted'`).
87
+ * - Promoted invite viewers → forwards to the arbiter and returns its snapshot.
88
+ *
89
+ * This is the single chokepoint that upholds "a spectator never obtains the
90
+ * write token" against the real vibelive arbiter.
91
+ */
92
+ requestControl(viewerId: string): ControlRequestResult;
93
+ }
94
+ declare function createAccessGate(options: AccessGateOptions): AccessGate;
95
+
96
+ /**
97
+ * The share orchestrator — the public library surface.
98
+ *
99
+ * vibeshare is the URL/access layer ON TOP of vibelive's engine: it does not
100
+ * reimplement transport. {@link createShare} takes an already-running vibelive
101
+ * relay (the host's local/LAN ws server) and mints a capability URL + access gate
102
+ * + expiry around it. Spectators connect to the relay as ordinary vibelive
103
+ * participants (they get the ordered output fan-out) but the access gate ensures
104
+ * they can never drive the wrapped agent.
105
+ *
106
+ * Consent: vibeshare gates fanning session output off the host machine behind the
107
+ * `share:session` scope from `@pooriaarab/vibe-core`. `createShare` refuses to
108
+ * mint a share unless that grant is present (the CLI grants it; a bare library
109
+ * caller must too), which is the same local-first enforcement vibelive uses.
110
+ */
111
+
112
+ /** The consent scope that gates sharing a session off the host machine. */
113
+ declare const SHARE_SESSION_SCOPE = "share:session";
114
+ /** Expiry specification: a preset, or a raw duration in milliseconds. */
115
+ type ExpirySpec = '1h' | '24h' | number;
116
+ /**
117
+ * Parse an expiry spec into a duration in milliseconds, or `null` for "never".
118
+ *
119
+ * Pure and total — unit-tested directly. `undefined` → never expires.
120
+ */
121
+ declare function parseExpiry(spec: ExpirySpec | undefined): number | null;
122
+ /** A viewer of a share, derived from the live vibelive roster + the access gate. */
123
+ interface Viewer {
124
+ readonly id: string;
125
+ readonly name: string;
126
+ /** Current role — spectators can't drive; participants can. */
127
+ readonly role: ViewerRole;
128
+ readonly joinedAt: number;
129
+ }
130
+ /** A snapshot of a share's audience. */
131
+ interface ViewerRoster {
132
+ readonly viewers: readonly Viewer[];
133
+ /** Invite viewers awaiting host promotion (join requests). */
134
+ readonly pending: readonly Viewer[];
135
+ }
136
+ /** Why a share tore down. Surfaced via {@link ShareOptions.onRevoke}. */
137
+ type RevokeReason = 'manual' | 'expired';
138
+ interface ShareOptions {
139
+ /** The running vibelive relay whose session is being shared. */
140
+ readonly session: RelayHandle;
141
+ /** Access policy for link holders. Defaults to spectate (read-only). */
142
+ readonly access?: AccessMode;
143
+ /** Optional expiry (auto-revokes the share after this elapses). */
144
+ readonly expiry?: ExpirySpec;
145
+ /** Optional passphrase — a second factor on top of the capability URL. */
146
+ readonly passphrase?: string;
147
+ /**
148
+ * Optional explicit consent ledger. Defaults to the relay's own ledger, which
149
+ * vibelive seeds with `share:session` granted. If you pass a different ledger it
150
+ * must hold the grant or `createShare` throws.
151
+ */
152
+ readonly consent?: ConsentLedger;
153
+ /** Called exactly once when the share tears down (manual revoke or expiry). */
154
+ readonly onRevoke?: (reason: RevokeReason) => void;
155
+ }
156
+ interface ShareHandle {
157
+ /** The unguessable capability id backing this share. */
158
+ readonly id: string;
159
+ /** The human-facing share URL (`https://vibeshare.stream/s/<id>`). */
160
+ readonly url: string;
161
+ /** This share's access policy. */
162
+ readonly access: AccessMode;
163
+ /** True once revoked (manually or by expiry). */
164
+ readonly revoked: boolean;
165
+ /** The underlying access gate (exposed for tests + advanced callers). */
166
+ readonly gate: AccessGate;
167
+ /** The local/LAN relay URL viewers actually connect over. */
168
+ readonly relayUrl: string;
169
+ /** Live audience snapshot: connected viewers + pending join requests. */
170
+ viewers(): ViewerRoster;
171
+ /**
172
+ * Promote a pending invite viewer into a participant (lets them request control).
173
+ * Invite shares only. Returns false if unknown / spectate share.
174
+ */
175
+ approve(viewerId: string): boolean;
176
+ /** Remove a viewer (disconnect handling / kick). Releases the token if held. */
177
+ removeViewer(viewerId: string): void;
178
+ /** Tear down the share: clear the expiry timer and close the vibelive relay. */
179
+ revoke(): Promise<void>;
180
+ }
181
+ /** Error thrown when consent for `share:session` has not been granted. */
182
+ declare class ConsentError extends Error {
183
+ constructor();
184
+ }
185
+ declare const HOST_PARTICIPANT_NAME = "host";
186
+ declare function createShare(options: ShareOptions): ShareHandle;
187
+
188
+ export { type AccessMode as A, ConsentError as C, DEFAULT_ACCESS as D, type ExpirySpec as E, HOST_PARTICIPANT_NAME as H, type RevokeReason as R, SHARE_SESSION_SCOPE as S, type Viewer as V, type AccessGate as a, type AccessGateOptions as b, type ControlRequestResult as c, type DenialReason as d, type ShareHandle as e, type ShareOptions as f, type ViewerRole as g, type ViewerRoster as h, createAccessGate as i, createShare as j, parseExpiry as p };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "vibeshare-live",
3
+ "version": "0.1.0",
4
+ "description": "Share your live agent coding session by URL — spectate read-only or invite into the vibelive multiplayer engine. Open source, CLI-first, e2e p2p. Local-first.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Pooria Arab",
8
+ "bin": { "vibeshare": "./dist/cli.js" },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
12
+ "files": ["dist"],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts src/cli.ts src/mcp.ts --format esm --dts --clean",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
18
+ },
19
+ "dependencies": {
20
+ "@pooriaarab/vibe-core": "^0.1.0",
21
+ "vibelive-cli": "^0.1.0",
22
+ "@modelcontextprotocol/sdk": "^1.0.0",
23
+ "ws": "^8.16.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.0",
27
+ "@types/ws": "^8.5.0",
28
+ "tsup": "^8.0.0",
29
+ "typescript": "^5.4.0",
30
+ "vitest": "^1.6.0"
31
+ },
32
+ "homepage": "https://github.com/pooriaarab/vibeshare#readme",
33
+ "repository": { "type": "git", "url": "git+https://github.com/pooriaarab/vibeshare.git" },
34
+ "bugs": { "url": "https://github.com/pooriaarab/vibeshare/issues" },
35
+ "keywords": ["vibe-suite", "vibeshare", "claude-code", "codex", "multiplayer", "cli", "mcp", "share", "local-first"],
36
+ "publishConfig": { "access": "public" },
37
+ "engines": { "node": ">=18" }
38
+ }