sh3-core 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Shell.svelte +19 -0
- package/dist/api.d.ts +5 -1
- package/dist/api.js +6 -1
- package/dist/app/admin/ApiKeysView.svelte +16 -27
- package/dist/app/admin/SystemView.svelte +149 -11
- package/dist/documents/backends.d.ts +8 -0
- package/dist/documents/backends.js +87 -0
- package/dist/documents/backends.test.d.ts +1 -0
- package/dist/documents/backends.test.js +33 -0
- package/dist/documents/browse.d.ts +12 -0
- package/dist/documents/browse.js +19 -0
- package/dist/documents/browse.test.d.ts +1 -0
- package/dist/documents/browse.test.js +41 -0
- package/dist/documents/http-backend.d.ts +4 -0
- package/dist/documents/http-backend.js +14 -0
- package/dist/documents/sync/index.d.ts +1 -2
- package/dist/documents/sync/index.js +0 -2
- package/dist/documents/sync/observer.d.ts +3 -0
- package/dist/documents/sync/observer.js +45 -0
- package/dist/documents/sync/registry.d.ts +3 -0
- package/dist/documents/sync/registry.js +8 -1
- package/dist/documents/sync/registry.test.js +11 -0
- package/dist/documents/types.d.ts +18 -0
- package/dist/documents/types.js +6 -1
- package/dist/keys/ConsentDialog.svelte +176 -0
- package/dist/keys/ConsentDialog.svelte.d.ts +3 -0
- package/dist/keys/client.d.ts +13 -0
- package/dist/keys/client.js +65 -0
- package/dist/keys/client.test.d.ts +1 -0
- package/dist/keys/client.test.js +44 -0
- package/dist/keys/consent.svelte.d.ts +16 -0
- package/dist/keys/consent.svelte.js +29 -0
- package/dist/keys/consent.test.d.ts +1 -0
- package/dist/keys/consent.test.js +53 -0
- package/dist/keys/revocation-bus.svelte.d.ts +35 -0
- package/dist/keys/revocation-bus.svelte.js +92 -0
- package/dist/keys/revocation-bus.test.d.ts +1 -0
- package/dist/keys/revocation-bus.test.js +95 -0
- package/dist/keys/types.d.ts +32 -0
- package/dist/keys/types.js +13 -0
- package/dist/layout/inspection.d.ts +17 -0
- package/dist/layout/inspection.js +53 -0
- package/dist/server-shard/types.d.ts +21 -2
- package/dist/server-sync.d.ts +6 -0
- package/dist/server-sync.js +634 -0
- package/dist/server-sync.js.map +7 -0
- package/dist/sh3core-shard/ShellHome.svelte +140 -63
- package/dist/sh3core-shard/sh3coreShard.svelte.js +12 -1
- package/dist/shards/activate-browse.test.d.ts +1 -0
- package/dist/shards/activate-browse.test.js +36 -0
- package/dist/shards/activate-on-key-revoked.test.d.ts +1 -0
- package/dist/shards/activate-on-key-revoked.test.js +60 -0
- package/dist/shards/activate-sync-registry.test.d.ts +1 -0
- package/dist/shards/activate-sync-registry.test.js +42 -0
- package/dist/shards/activate-tenantid.test.d.ts +1 -0
- package/dist/shards/activate-tenantid.test.js +21 -0
- package/dist/shards/activate.svelte.d.ts +12 -0
- package/dist/shards/activate.svelte.js +55 -3
- package/dist/shards/types.d.ts +42 -0
- package/dist/shards/types.js +1 -1
- package/dist/shell/views/KeysAndPeers.svelte +110 -0
- package/dist/shell/views/KeysAndPeers.svelte.d.ts +3 -0
- package/dist/shell-shard/Terminal.svelte +0 -11
- package/dist/shell-shard/manifest.js +1 -1
- package/dist/shell-shard/shellShard.svelte.js +52 -4
- package/dist/shell-shard/toolbar/Toolbar.svelte +11 -32
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +0 -2
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +29 -62
- package/dist/shell-shard/verbs/index.js +3 -1
- package/dist/shell-shard/verbs/views.d.ts +2 -0
- package/dist/shell-shard/verbs/views.js +103 -2
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +77 -0
- package/dist/testing.js.map +7 -0
- package/dist/verbs/types.d.ts +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/documents/journal-hook.ts", "../src/documents/notifications.ts", "../src/documents/sync/hash.ts", "../src/documents/sync/types.ts", "../src/documents/sync/serialization.ts", "../src/documents/sync/journal.ts", "../src/documents/sync/tombstones.ts", "../src/documents/sync/conflicts.ts", "../src/documents/sync/engine.ts", "../src/documents/sync/registry.ts", "../src/documents/sync/singleton.ts", "../src/documents/sync/handle.ts"],
|
|
4
|
+
"sourcesContent": ["/*\n * Journal appender hook \u2014 lets the sync engine subscribe to regular\n * shard writes/deletes without creating an import cycle between the\n * document handle and the sync subsystem.\n */\n\nimport type { JournalEntry } from './sync/types';\n\ntype Appender = (entry: Omit<JournalEntry, 'seq' | 'ts'>) => Promise<void>;\n\nlet appender: Appender | null = null;\n\nexport function setJournalAppender(fn: Appender): void {\n appender = fn;\n}\n\nexport function clearJournalAppender(): void {\n appender = null;\n}\n\nexport async function notifyJournal(entry: Omit<JournalEntry, 'seq' | 'ts'>): Promise<void> {\n if (appender) await appender(entry);\n}\n", "/*\n * Document change notification emitter \u2014 in-process pub/sub.\n *\n * A single global emitter connects document handles so that a write\n * from one handle (e.g. an editor shard saving a file) is visible to\n * another handle watching the same scope (e.g. a preview shard).\n *\n * Cross-tab sync (BroadcastChannel) is deferred \u2014 this emitter is\n * in-process only.\n */\n\nimport type { DocumentChange } from './types';\n\ntype Listener = (change: DocumentChange) => void;\n\nclass DocumentChangeEmitter {\n #listeners = new Set<Listener>();\n\n subscribe(fn: Listener): () => void {\n this.#listeners.add(fn);\n return () => {\n this.#listeners.delete(fn);\n };\n }\n\n emit(change: DocumentChange): void {\n for (const fn of this.#listeners) {\n try {\n fn(change);\n } catch (e) {\n console.error('SH3: document change listener threw', e);\n }\n }\n }\n}\n\nexport const documentChanges = new DocumentChangeEmitter();\n", "/*\n * Content hashing for the sync subsystem. sha-256 via SubtleCrypto,\n * truncated to 16 hex chars \u2014 enough for collision resistance at\n * per-user document scale while keeping manifest entries small.\n */\n\nexport async function hashContent(content: string | ArrayBuffer): Promise<string> {\n const buf =\n typeof content === 'string' ? new TextEncoder().encode(content) : new Uint8Array(content);\n const digest = await crypto.subtle.digest('SHA-256', buf);\n const hex = Array.from(new Uint8Array(digest))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return hex.slice(0, 16);\n}\n", "/*\n * Document Sync API types. See docs/superpowers/specs/2026-04-14-document-sync-api-design.md.\n */\n\n/** Permission string required in a shard manifest to obtain ctx.sync(). */\nexport const PERMISSION_DOCUMENTS_SYNC = 'documents:sync';\n\n/** Reserved shardId used to persist sync metadata (journal, tombstones, cursors, grants). */\nexport const SYNC_RESERVED_SHARD_ID = '__sync__';\n\nexport type SyncScope =\n | { kind: 'shard'; shardId: string }\n | { kind: 'tenant' }\n | { kind: 'path'; shardId: string; prefix: string };\n\nexport interface ManifestEntry {\n path: string;\n shardId: string;\n hash: string;\n size: number;\n lastModified: number;\n tombstone?: { deletedAt: number };\n}\n\nexport interface ApplyEntry {\n path: string;\n shardId: string;\n op: 'upsert' | 'delete';\n content?: string | ArrayBuffer;\n remoteHash: string;\n remoteMtime: number;\n}\n\nexport interface ApplyOpts {\n onConflict?: ConflictPolicy;\n expectedLocalHash?: string;\n}\n\nexport type ApplyOutcome =\n | { status: 'applied'; newHash: string }\n | { status: 'skipped-identical' }\n | { status: 'conflict'; resolution: ConflictResolution };\n\nexport interface ApplyBatchResult {\n applied: Array<{ path: string; shardId: string; newHash: string }>;\n skipped: Array<{ path: string; shardId: string; reason: 'identical' }>;\n conflicts: ConflictResolution[];\n}\n\nexport interface ConflictResolution {\n path: string;\n shardId: string;\n localHash: string;\n remoteHash: string;\n conflictArtifactPath: string;\n base?: { hash: string };\n}\n\nexport interface ConflictContext {\n path: string;\n shardId: string;\n localHash: string;\n remoteHash: string;\n baseHash?: string;\n}\n\nexport type ConflictPolicy =\n | 'default'\n | 'remote-wins'\n | 'local-wins'\n | 'keep-both'\n | ((ctx: ConflictContext) => Promise<'remote-wins' | 'local-wins' | 'keep-both'>);\n\nexport interface JournalEntry {\n seq: number;\n ts: number;\n shardId: string;\n path: string;\n op: 'upsert' | 'delete';\n hash: string | null;\n}\n\nexport interface ChangePage {\n entries: JournalEntry[];\n nextCursor: string;\n hasMore: boolean;\n truncated?: boolean;\n}\n\nexport interface GrantRecord {\n connectorId: string;\n scope: SyncScope;\n grantedAt: number;\n}\n\nexport interface SyncHandle {\n readonly connectorId: string;\n grantedScopes(): Promise<SyncScope[]>;\n getManifest(scope: SyncScope): Promise<ManifestEntry[]>;\n changesSince(scope: SyncScope, cursor?: string): Promise<ChangePage>;\n ack(scope: SyncScope, cursor: string): Promise<void>;\n apply(scope: SyncScope, entry: ApplyEntry, opts?: ApplyOpts): Promise<ApplyOutcome>;\n applyBatch(scope: SyncScope, manifest: ApplyEntry[], opts?: ApplyOpts): Promise<ApplyBatchResult>;\n forget(scope: SyncScope, path: string): Promise<void>;\n}\n\nexport class ScopeNotGrantedError extends Error {\n constructor(public readonly scope: SyncScope) {\n super(`Scope not granted: ${JSON.stringify(scope)}`);\n this.name = 'ScopeNotGrantedError';\n }\n}\n\nexport class ScopeRevokedError extends Error {\n constructor(public readonly scope: SyncScope) {\n super(`Scope revoked during operation: ${JSON.stringify(scope)}`);\n this.name = 'ScopeRevokedError';\n }\n}\n\nexport class TenantMismatchError extends Error {\n constructor() {\n super('Sync handle tenantId does not match current session');\n this.name = 'TenantMismatchError';\n }\n}\n", "/*\n * JSON storage helpers on top of DocumentBackend. All sync metadata\n * lives under the reserved shardId SYNC_RESERVED_SHARD_ID, scoped per\n * tenant by the backend. Callers pass tenantId explicitly; tenant\n * scoping is enforced at the SyncHandle/engine layer.\n */\n\nimport type { DocumentBackend } from '../types';\nimport { SYNC_RESERVED_SHARD_ID } from './types';\n\nexport async function readJson<T>(\n backend: DocumentBackend,\n tenantId: string,\n path: string,\n): Promise<T | null> {\n const raw = await backend.read(tenantId, SYNC_RESERVED_SHARD_ID, path);\n if (raw === null) return null;\n const str = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);\n return JSON.parse(str) as T;\n}\n\nexport async function writeJson(\n backend: DocumentBackend,\n tenantId: string,\n path: string,\n value: unknown,\n): Promise<void> {\n await backend.write(tenantId, SYNC_RESERVED_SHARD_ID, path, JSON.stringify(value));\n}\n\nexport async function deletePath(\n backend: DocumentBackend,\n tenantId: string,\n path: string,\n): Promise<void> {\n await backend.delete(tenantId, SYNC_RESERVED_SHARD_ID, path);\n}\n\nexport async function listJsonPaths(\n backend: DocumentBackend,\n tenantId: string,\n prefix: string,\n): Promise<string[]> {\n const all = await backend.list(tenantId, SYNC_RESERVED_SHARD_ID);\n return all.map((m) => m.path).filter((p) => p.startsWith(prefix));\n}\n", "/*\n * Change journal for the sync subsystem.\n *\n * Append-only per-tenant sequence of JournalEntry, stored as JSON\n * segments under journal/<n>.json. Cursors are opaque \"<seq>:<version>\"\n * strings; version matches the journal's truncation epoch. A cursor with\n * an older version whose seq < oldestSeq means the connector has fallen\n * behind retained history.\n */\n\nimport type { DocumentBackend } from '../types';\nimport type { ChangePage, JournalEntry, SyncScope } from './types';\nimport { readJson, writeJson, deletePath, listJsonPaths } from './serialization';\n\ninterface JournalMeta {\n currentSeq: number;\n currentSegment: number;\n oldestSegment: number;\n version: number; // bumped on truncation\n}\n\nconst META_PATH = 'journal/meta.json';\nconst SEGMENT_PREFIX = 'journal/seg-';\nconst CURSOR_PREFIX = 'cursors/';\nconst DEFAULT_SEGMENT_SIZE = 500;\nconst DEFAULT_PAGE_SIZE = 100;\n\nfunction segmentPath(i: number): string {\n return `${SEGMENT_PREFIX}${i}.json`;\n}\n\nfunction cursorPath(connectorId: string): string {\n return `${CURSOR_PREFIX}${encodeURIComponent(connectorId)}.json`;\n}\n\nfunction matchesScope(entry: JournalEntry, scope: SyncScope): boolean {\n switch (scope.kind) {\n case 'tenant': return true;\n case 'shard': return entry.shardId === scope.shardId;\n case 'path': return entry.shardId === scope.shardId && entry.path.startsWith(scope.prefix);\n }\n}\n\nexport class Journal {\n #meta: JournalMeta = { currentSeq: 0, currentSegment: 0, oldestSegment: 0, version: 0 };\n #segmentSize: number;\n #pageSize: number;\n\n constructor(\n private backend: DocumentBackend,\n private tenantId: string,\n opts: { segmentSize?: number; pageSize?: number } = {},\n ) {\n this.#segmentSize = opts.segmentSize ?? DEFAULT_SEGMENT_SIZE;\n this.#pageSize = opts.pageSize ?? DEFAULT_PAGE_SIZE;\n }\n\n async init(): Promise<void> {\n const meta = await readJson<JournalMeta>(this.backend, this.tenantId, META_PATH);\n if (meta) this.#meta = meta;\n }\n\n static encodeCursor(seq: number, version: number): string {\n return `${seq}:${version}`;\n }\n\n static decodeCursor(cursor: string): { seq: number; version: number } | null {\n const m = /^(\\d+):(\\d+)$/.exec(cursor);\n if (!m) return null;\n return { seq: Number(m[1]), version: Number(m[2]) };\n }\n\n async append(\n entry: Omit<JournalEntry, 'seq' | 'ts'>,\n ): Promise<JournalEntry> {\n const seq = this.#meta.currentSeq + 1;\n const full: JournalEntry = { ...entry, seq, ts: Date.now() };\n const segIdx = this.#meta.currentSegment;\n const current = (await readJson<JournalEntry[]>(this.backend, this.tenantId, segmentPath(segIdx))) ?? [];\n current.push(full);\n await writeJson(this.backend, this.tenantId, segmentPath(segIdx), current);\n this.#meta.currentSeq = seq;\n if (current.length >= this.#segmentSize) {\n this.#meta.currentSegment = segIdx + 1;\n }\n await writeJson(this.backend, this.tenantId, META_PATH, this.#meta);\n return full;\n }\n\n async oldestRetainedSeq(): Promise<number> {\n const first = await readJson<JournalEntry[]>(this.backend, this.tenantId, segmentPath(this.#meta.oldestSegment));\n if (!first || first.length === 0) return 0;\n return first[0].seq;\n }\n\n async changesSince(scope: SyncScope, cursor?: string): Promise<ChangePage> {\n let startSeq = 0;\n if (cursor) {\n const decoded = Journal.decodeCursor(cursor);\n if (!decoded) return { entries: [], nextCursor: cursor, hasMore: false };\n const oldest = await this.oldestRetainedSeq();\n if (decoded.version < this.#meta.version && decoded.seq < oldest) {\n return { entries: [], nextCursor: cursor, hasMore: false, truncated: true };\n }\n startSeq = decoded.seq;\n }\n\n const out: JournalEntry[] = [];\n let lastSeq = startSeq;\n let hasMore = false;\n\n for (let seg = this.#meta.oldestSegment; seg <= this.#meta.currentSegment; seg++) {\n const entries = (await readJson<JournalEntry[]>(this.backend, this.tenantId, segmentPath(seg))) ?? [];\n for (const e of entries) {\n if (e.seq <= startSeq) continue;\n if (!matchesScope(e, scope)) { lastSeq = e.seq; continue; }\n if (out.length >= this.#pageSize) { hasMore = true; break; }\n out.push(e);\n lastSeq = e.seq;\n }\n if (hasMore) break;\n }\n\n return {\n entries: out,\n nextCursor: Journal.encodeCursor(lastSeq, this.#meta.version),\n hasMore,\n };\n }\n\n async getCursor(connectorId: string): Promise<string | null> {\n return readJson<string>(this.backend, this.tenantId, cursorPath(connectorId));\n }\n\n async ackCursor(connectorId: string, cursor: string): Promise<void> {\n await writeJson(this.backend, this.tenantId, cursorPath(connectorId), cursor);\n }\n\n async dropCursor(connectorId: string): Promise<void> {\n await deletePath(this.backend, this.tenantId, cursorPath(connectorId));\n }\n\n async listCursors(): Promise<Array<{ connectorId: string; cursor: string }>> {\n const paths = await listJsonPaths(this.backend, this.tenantId, CURSOR_PREFIX);\n const out: Array<{ connectorId: string; cursor: string }> = [];\n for (const p of paths) {\n const cursor = await readJson<string>(this.backend, this.tenantId, p);\n if (cursor === null) continue;\n const id = decodeURIComponent(p.slice(CURSOR_PREFIX.length, -'.json'.length));\n out.push({ connectorId: id, cursor });\n }\n return out;\n }\n\n async minSeqAckedByAll(connectorIds: string[]): Promise<number> {\n if (connectorIds.length === 0) return 0;\n let min = Infinity;\n for (const id of connectorIds) {\n const c = await this.getCursor(id);\n const decoded = c ? Journal.decodeCursor(c) : null;\n const seq = decoded ? decoded.seq : 0;\n if (seq < min) min = seq;\n }\n return min === Infinity ? 0 : min;\n }\n\n /** Test-only: simulate truncating all segments whose entries are <= uptoSeq. */\n async __truncateForTest(uptoSeq: number): Promise<void> {\n for (let seg = this.#meta.oldestSegment; seg <= this.#meta.currentSegment; seg++) {\n const entries = (await readJson<JournalEntry[]>(this.backend, this.tenantId, segmentPath(seg))) ?? [];\n const last = entries[entries.length - 1];\n if (last && last.seq <= uptoSeq) {\n await deletePath(this.backend, this.tenantId, segmentPath(seg));\n this.#meta.oldestSegment = seg + 1;\n } else break;\n }\n this.#meta.version += 1;\n await writeJson(this.backend, this.tenantId, META_PATH, this.#meta);\n }\n}\n", "/*\n * Tombstone store \u2014 records deletion metadata so sync connectors can\n * distinguish \"never existed\" from \"was deleted.\" Backed by JSON docs\n * under the reserved sync shardId. GC is driven by the engine, not here.\n */\n\nimport type { DocumentBackend } from '../types';\nimport { readJson, writeJson, deletePath, listJsonPaths } from './serialization';\n\nconst PREFIX = 'tombstones/';\n\nfunction key(shardId: string, path: string): string {\n return `${PREFIX}${shardId}__${encodeURIComponent(path)}.json`;\n}\n\nexport interface TombstoneRecord {\n deletedAt: number;\n lastHash: string;\n}\n\nexport interface TombstoneEntry extends TombstoneRecord {\n shardId: string;\n path: string;\n}\n\nexport class TombstoneStore {\n constructor(\n private backend: DocumentBackend,\n private tenantId: string,\n ) {}\n\n async record(shardId: string, path: string, lastHash: string, deletedAt: number): Promise<void> {\n await writeJson(this.backend, this.tenantId, key(shardId, path), {\n deletedAt,\n lastHash,\n } satisfies TombstoneRecord);\n }\n\n async get(shardId: string, path: string): Promise<TombstoneRecord | null> {\n return readJson<TombstoneRecord>(this.backend, this.tenantId, key(shardId, path));\n }\n\n async clear(shardId: string, path: string): Promise<void> {\n await deletePath(this.backend, this.tenantId, key(shardId, path));\n }\n\n async listByShard(shardId: string): Promise<TombstoneEntry[]> {\n const prefix = `${PREFIX}${shardId}__`;\n const paths = await listJsonPaths(this.backend, this.tenantId, prefix);\n const out: TombstoneEntry[] = [];\n for (const p of paths) {\n const rec = await readJson<TombstoneRecord>(this.backend, this.tenantId, p);\n if (!rec) continue;\n const originalPath = decodeURIComponent(p.slice(prefix.length, -'.json'.length));\n out.push({ shardId, path: originalPath, ...rec });\n }\n return out;\n }\n\n async listAll(): Promise<TombstoneEntry[]> {\n const paths = await listJsonPaths(this.backend, this.tenantId, PREFIX);\n const out: TombstoneEntry[] = [];\n for (const p of paths) {\n const rec = await readJson<TombstoneRecord>(this.backend, this.tenantId, p);\n if (!rec) continue;\n const rest = p.slice(PREFIX.length, -'.json'.length);\n const sep = rest.indexOf('__');\n if (sep < 0) continue;\n const shardId = rest.slice(0, sep);\n const path = decodeURIComponent(rest.slice(sep + 2));\n out.push({ shardId, path, ...rec });\n }\n return out;\n }\n}\n", "/*\n * Conflict resolution \u2014 dispatches the caller-supplied (or default)\n * policy, writes .sync-conflict-* artifacts when required, and tracks\n * per-(connectorId, path) base hashes for three-way comparisons.\n */\n\nimport type { DocumentBackend } from '../types';\nimport type { ConflictPolicy, ConflictResolution } from './types';\nimport { readJson, writeJson } from './serialization';\n\ninterface ConflictInput {\n connectorId: string;\n shardId: string;\n path: string;\n localHash: string;\n remoteHash: string;\n remoteContent?: string | ArrayBuffer;\n baseHash?: string;\n}\n\nexport type ConflictAction =\n | { action: 'apply-remote'; asPath?: string }\n | { action: 'skip' }\n | { action: 'conflict'; resolution: ConflictResolution };\n\nconst BASES_PREFIX = 'bases/';\n\nfunction baseKey(connectorId: string, shardId: string, path: string): string {\n return `${BASES_PREFIX}${encodeURIComponent(connectorId)}__${shardId}__${encodeURIComponent(path)}.json`;\n}\n\nfunction isArtifactName(name: string): boolean {\n return /\\.sync-conflict-[^.]+-\\d+$/.test(name);\n}\n\nexport class ConflictManager {\n constructor(\n private backend: DocumentBackend,\n private tenantId: string,\n ) {}\n\n async resolve(policy: ConflictPolicy, input: ConflictInput): Promise<ConflictAction> {\n const p = typeof policy === 'function' ? await policy({\n path: input.path, shardId: input.shardId,\n localHash: input.localHash, remoteHash: input.remoteHash, baseHash: input.baseHash,\n }) : policy;\n\n switch (p) {\n case 'remote-wins': return { action: 'apply-remote' };\n case 'local-wins': return { action: 'skip' };\n case 'keep-both': {\n const asPath = `${input.path}.incoming-${input.connectorId}-${Date.now()}`;\n return { action: 'apply-remote', asPath };\n }\n case 'default':\n default: {\n const ts = Date.now();\n const artifact = `${input.path}.sync-conflict-${input.connectorId}-${ts}`;\n if (input.remoteContent !== undefined) {\n await this.backend.write(this.tenantId, input.shardId, artifact, input.remoteContent);\n }\n const resolution: ConflictResolution = {\n path: input.path,\n shardId: input.shardId,\n localHash: input.localHash,\n remoteHash: input.remoteHash,\n conflictArtifactPath: artifact,\n base: input.baseHash ? { hash: input.baseHash } : undefined,\n };\n return { action: 'conflict', resolution };\n }\n }\n }\n\n async getBaseHash(connectorId: string, shardId: string, path: string): Promise<string | null> {\n return readJson<string>(this.backend, this.tenantId, baseKey(connectorId, shardId, path));\n }\n\n async setBaseHash(connectorId: string, shardId: string, path: string, hash: string): Promise<void> {\n await writeJson(this.backend, this.tenantId, baseKey(connectorId, shardId, path), hash);\n }\n\n async listConflicts(shardId: string): Promise<ConflictResolution[]> {\n const metas = await this.backend.list(this.tenantId, shardId);\n const out: ConflictResolution[] = [];\n for (const m of metas) {\n const name = m.path;\n if (!isArtifactName(name)) continue;\n const m2 = /^(.*)\\.sync-conflict-([^-]+)-(\\d+)$/.exec(name);\n if (!m2) continue;\n const originalPath = m2[1];\n out.push({\n path: originalPath,\n shardId,\n localHash: '',\n remoteHash: '',\n conflictArtifactPath: name,\n });\n }\n return out;\n }\n}\n", "/*\n * SyncEngine \u2014 orchestrates manifest generation, apply pipeline, and\n * change notifications. Composes Journal, TombstoneStore, and\n * ConflictManager over a DocumentBackend. One instance per tenant.\n */\n\nimport type { DocumentBackend, DocumentChange } from '../types';\nimport { documentChanges } from '../notifications';\nimport type {\n ApplyBatchResult, ApplyEntry, ApplyOpts, ApplyOutcome,\n ChangePage, ConflictResolution, ManifestEntry, SyncScope,\n} from './types';\nimport { hashContent } from './hash';\nimport { Journal } from './journal';\nimport { TombstoneStore } from './tombstones';\nimport { ConflictManager } from './conflicts';\n\nfunction scopeCovers(scope: SyncScope, shardId: string, path: string): boolean {\n switch (scope.kind) {\n case 'tenant': return true;\n case 'shard': return scope.shardId === shardId;\n case 'path': return scope.shardId === shardId && path.startsWith(scope.prefix);\n }\n}\n\nexport class SyncEngine {\n #journal: Journal;\n #tombstones: TombstoneStore;\n #conflicts: ConflictManager;\n\n constructor(\n private backend: DocumentBackend,\n private tenantId: string,\n opts: { segmentSize?: number } = {},\n ) {\n this.#journal = new Journal(backend, tenantId, { segmentSize: opts.segmentSize });\n this.#tombstones = new TombstoneStore(backend, tenantId);\n this.#conflicts = new ConflictManager(backend, tenantId);\n }\n\n async init(): Promise<void> {\n await this.#journal.init();\n }\n\n get journal(): Journal { return this.#journal; }\n\n async getManifest(_connectorId: string, scope: SyncScope): Promise<ManifestEntry[]> {\n const shardIds = await this.#shardsInScope(scope);\n const entries: ManifestEntry[] = [];\n for (const shardId of shardIds) {\n const metas = await this.backend.list(this.tenantId, shardId);\n for (const m of metas) {\n if (!scopeCovers(scope, shardId, m.path)) continue;\n if (m.path.startsWith('.') || /\\.sync-conflict-/.test(m.path)) continue;\n const raw = await this.backend.read(this.tenantId, shardId, m.path);\n if (raw === null) continue;\n const hash = await hashContent(raw);\n entries.push({ path: m.path, shardId, hash, size: m.size, lastModified: m.lastModified });\n }\n const tombs = await this.#tombstones.listByShard(shardId);\n for (const t of tombs) {\n if (!scopeCovers(scope, shardId, t.path)) continue;\n entries.push({\n path: t.path, shardId, hash: t.lastHash, size: 0, lastModified: t.deletedAt,\n tombstone: { deletedAt: t.deletedAt },\n });\n }\n }\n return entries;\n }\n\n async changesSince(scope: SyncScope, cursor?: string): Promise<ChangePage> {\n return this.#journal.changesSince(scope, cursor);\n }\n\n async ack(connectorId: string, _scope: SyncScope, cursor: string): Promise<void> {\n // Scope is informational \u2014 cursors are connector-wide, not scope-specific.\n await this.#journal.ackCursor(connectorId, cursor);\n }\n\n async apply(\n connectorId: string,\n scope: SyncScope,\n entry: ApplyEntry,\n opts: ApplyOpts = {},\n ): Promise<ApplyOutcome> {\n if (!scopeCovers(scope, entry.shardId, entry.path)) {\n throw new Error(`ApplyEntry {${entry.shardId}:${entry.path}} falls outside scope`);\n }\n if (entry.op === 'upsert') return this.#applyUpsert(connectorId, entry, opts);\n return this.#applyDelete(connectorId, entry);\n }\n\n async applyBatch(\n connectorId: string,\n scope: SyncScope,\n manifest: ApplyEntry[],\n opts: ApplyOpts = {},\n ): Promise<ApplyBatchResult> {\n const applied: ApplyBatchResult['applied'] = [];\n const skipped: ApplyBatchResult['skipped'] = [];\n const conflicts: ConflictResolution[] = [];\n for (const e of manifest) {\n const out = await this.apply(connectorId, scope, e, opts);\n if (out.status === 'applied') applied.push({ path: e.path, shardId: e.shardId, newHash: out.newHash });\n else if (out.status === 'skipped-identical') skipped.push({ path: e.path, shardId: e.shardId, reason: 'identical' });\n else conflicts.push(out.resolution);\n }\n return { applied, skipped, conflicts };\n }\n\n async forget(scope: SyncScope, path: string): Promise<void> {\n const shardIds = await this.#shardsInScope(scope);\n for (const shardId of shardIds) {\n if (!scopeCovers(scope, shardId, path)) continue;\n await this.#tombstones.clear(shardId, path);\n }\n }\n\n // ----- internals -----\n\n async #applyUpsert(\n connectorId: string,\n entry: ApplyEntry,\n opts: ApplyOpts,\n ): Promise<ApplyOutcome> {\n const existing = await this.backend.read(this.tenantId, entry.shardId, entry.path);\n const existed = existing !== null;\n const localHash = existed ? await hashContent(existing) : null;\n\n if (localHash !== null && localHash === entry.remoteHash) {\n return { status: 'skipped-identical' };\n }\n\n const conflictTriggered =\n existed && (\n (opts.expectedLocalHash !== undefined && opts.expectedLocalHash !== localHash) ||\n (opts.expectedLocalHash === undefined && await this.#hasDivergedBase(connectorId, entry, localHash!))\n );\n\n if (conflictTriggered) {\n const baseHash = (await this.#conflicts.getBaseHash(connectorId, entry.shardId, entry.path)) ?? undefined;\n const action = await this.#conflicts.resolve(opts.onConflict ?? 'default', {\n connectorId,\n shardId: entry.shardId,\n path: entry.path,\n localHash: localHash!,\n remoteHash: entry.remoteHash,\n remoteContent: entry.content,\n baseHash,\n });\n if (action.action === 'skip') return { status: 'skipped-identical' };\n if (action.action === 'conflict') return { status: 'conflict', resolution: action.resolution };\n // apply-remote \u2014 fall through to write (possibly under asPath)\n const writePath = action.asPath ?? entry.path;\n return this.#writeAndRecord(connectorId, entry, writePath, existed);\n }\n\n return this.#writeAndRecord(connectorId, entry, entry.path, existed);\n }\n\n async #writeAndRecord(\n connectorId: string,\n entry: ApplyEntry,\n writePath: string,\n existed: boolean,\n ): Promise<ApplyOutcome> {\n if (entry.content === undefined) {\n throw new Error(`Upsert without content for ${entry.shardId}:${entry.path}`);\n }\n await this.backend.write(this.tenantId, entry.shardId, writePath, entry.content);\n await this.#tombstones.clear(entry.shardId, writePath);\n const newHash = await hashContent(entry.content);\n await this.#conflicts.setBaseHash(connectorId, entry.shardId, writePath, newHash);\n await this.#journal.append({ shardId: entry.shardId, path: writePath, op: 'upsert', hash: newHash });\n this.#emit({ type: existed && writePath === entry.path ? 'update' : 'create', path: writePath, tenantId: this.tenantId, shardId: entry.shardId });\n return { status: 'applied', newHash };\n }\n\n async #applyDelete(connectorId: string, entry: ApplyEntry): Promise<ApplyOutcome> {\n const existing = await this.backend.read(this.tenantId, entry.shardId, entry.path);\n if (existing === null) {\n // Already gone. Record a tombstone anyway so manifests reflect the delete.\n await this.#tombstones.record(entry.shardId, entry.path, entry.remoteHash, Date.now());\n await this.#journal.append({ shardId: entry.shardId, path: entry.path, op: 'delete', hash: null });\n return { status: 'applied', newHash: '' };\n }\n const lastHash = await hashContent(existing);\n await this.backend.delete(this.tenantId, entry.shardId, entry.path);\n await this.#tombstones.record(entry.shardId, entry.path, lastHash, Date.now());\n await this.#conflicts.setBaseHash(connectorId, entry.shardId, entry.path, ''); // clear base\n await this.#journal.append({ shardId: entry.shardId, path: entry.path, op: 'delete', hash: null });\n this.#emit({ type: 'delete', path: entry.path, tenantId: this.tenantId, shardId: entry.shardId });\n return { status: 'applied', newHash: '' };\n }\n\n async #hasDivergedBase(connectorId: string, entry: ApplyEntry, localHash: string): Promise<boolean> {\n const base = await this.#conflicts.getBaseHash(connectorId, entry.shardId, entry.path);\n if (!base) return false; // no prior base \u2192 treat as first-seen, apply\n return base !== localHash; // local moved since we last wrote it\n }\n\n async #shardsInScope(scope: SyncScope): Promise<string[]> {\n if (scope.kind === 'tenant') {\n // Listing all shards under a tenant isn't part of DocumentBackend's\n // contract. For now, we rely on callers passing {kind:'shard'} or\n // {kind:'path'}; {kind:'tenant'} traversal is driven by registry\n // listing at the handle layer. Return empty here as a placeholder\n // \u2014 the handle expands tenant-scope into per-shard calls.\n return [];\n }\n return [scope.shardId];\n }\n\n #emit(change: DocumentChange): void {\n documentChanges.emit(change);\n }\n}\n", "/*\n * Grant registry \u2014 public surface (revoke/list/listConflicts) for\n * connectors, plus __grantInternal used only by the core-owned\n * SyncGrantPicker component. Connectors do not import __grantInternal.\n */\n\nimport type { DocumentBackend } from '../types';\nimport type { ConflictResolution, GrantRecord, SyncScope } from './types';\nimport { readJson, writeJson, deletePath, listJsonPaths } from './serialization';\nimport { ConflictManager } from './conflicts';\n\nconst GRANTS_PREFIX = 'grants/';\n\nfunction grantPath(connectorId: string): string {\n return `${GRANTS_PREFIX}${encodeURIComponent(connectorId)}.json`;\n}\n\nfunction scopesEqual(a: SyncScope, b: SyncScope): boolean {\n if (a.kind !== b.kind) return false;\n if (a.kind === 'tenant') return true;\n if (a.kind === 'shard' && b.kind === 'shard') return a.shardId === b.shardId;\n if (a.kind === 'path' && b.kind === 'path')\n return a.shardId === b.shardId && a.prefix === b.prefix;\n return false;\n}\n\nexport async function __grantInternal(\n backend: DocumentBackend,\n tenantId: string,\n connectorId: string,\n scope: SyncScope,\n): Promise<void> {\n const existing = (await readJson<GrantRecord[]>(backend, tenantId, grantPath(connectorId))) ?? [];\n if (existing.some((g) => scopesEqual(g.scope, scope))) return;\n existing.push({ connectorId, scope, grantedAt: Date.now() });\n await writeJson(backend, tenantId, grantPath(connectorId), existing);\n}\n\nexport interface SyncRegistry {\n list(connectorId?: string): Promise<GrantRecord[]>;\n revoke(connectorId: string, scope: SyncScope): Promise<void>;\n /** Per-shard conflict enumeration. */\n listConflicts(shardId: string): Promise<ConflictResolution[]>;\n /** Tenant-wide conflict enumeration (fans out over every shard). */\n listConflicts(): Promise<ConflictResolution[]>;\n listAllConnectorIds(): Promise<string[]>;\n}\n\nexport function createSyncRegistry(backend: DocumentBackend, tenantId: string): SyncRegistry {\n const conflicts = new ConflictManager(backend, tenantId);\n\n async function readGrants(connectorId: string): Promise<GrantRecord[]> {\n return (await readJson<GrantRecord[]>(backend, tenantId, grantPath(connectorId))) ?? [];\n }\n\n return {\n async list(connectorId) {\n if (connectorId) return readGrants(connectorId);\n const paths = await listJsonPaths(backend, tenantId, GRANTS_PREFIX);\n const out: GrantRecord[] = [];\n for (const p of paths) {\n const arr = await readJson<GrantRecord[]>(backend, tenantId, p);\n if (arr) out.push(...arr);\n }\n return out;\n },\n\n async revoke(connectorId, scope) {\n const grants = await readGrants(connectorId);\n const next = grants.filter((g) => !scopesEqual(g.scope, scope));\n if (next.length === 0) await deletePath(backend, tenantId, grantPath(connectorId));\n else await writeJson(backend, tenantId, grantPath(connectorId), next);\n },\n\n async listConflicts(shardId?: string): Promise<ConflictResolution[]> {\n if (shardId !== undefined) return conflicts.listConflicts(shardId);\n const shards = await backend.listAllShards(tenantId);\n const out: ConflictResolution[] = [];\n for (const s of shards) {\n out.push(...(await conflicts.listConflicts(s)));\n }\n return out;\n },\n\n async listAllConnectorIds() {\n const paths = await listJsonPaths(backend, tenantId, GRANTS_PREFIX);\n return paths.map((p) =>\n decodeURIComponent(p.slice(GRANTS_PREFIX.length, -'.json'.length)),\n );\n },\n };\n}\n", "/*\n * Per-tenant SyncEngine singleton. Lazily initialised on first\n * ctx.sync() call. The journal appender hook is registered once\n * when the engine is first built so regular shard writes feed\n * the journal for all future connectors.\n */\n\nimport type { DocumentBackend } from '../types';\nimport { setJournalAppender } from '../journal-hook';\nimport { SyncEngine } from './engine';\nimport { createSyncRegistry, type SyncRegistry } from './registry';\n\ninterface Bundle {\n engine: SyncEngine;\n registry: SyncRegistry;\n}\n\nconst bundles = new Map<string, Bundle>();\n\nexport async function getSyncBundle(backend: DocumentBackend, tenantId: string): Promise<Bundle> {\n const existing = bundles.get(tenantId);\n if (existing) return existing;\n const engine = new SyncEngine(backend, tenantId);\n await engine.init();\n const registry = createSyncRegistry(backend, tenantId);\n setJournalAppender(async (e) => { await engine.journal.append(e); });\n const bundle = { engine, registry };\n bundles.set(tenantId, bundle);\n return bundle;\n}\n\n/** Test-only reset. */\nexport function __resetSyncBundlesForTest(): void {\n bundles.clear();\n}\n", "/*\n * SyncHandle factory \u2014 binds tenantId + connectorId, validates scope\n * grants per call, and fans {kind:'tenant'} scopes across the granted\n * shard/path scopes before delegating to the engine.\n */\n\nimport type { SyncEngine } from './engine';\nimport type { SyncRegistry } from './registry';\nimport {\n ScopeNotGrantedError,\n type ApplyBatchResult,\n type ManifestEntry, type SyncHandle, type SyncScope,\n} from './types';\n\nfunction scopeContains(parent: SyncScope, child: SyncScope): boolean {\n if (parent.kind === 'tenant') return true;\n if (parent.kind === 'shard') {\n if (child.kind === 'shard') return parent.shardId === child.shardId;\n if (child.kind === 'path') return parent.shardId === child.shardId;\n return false;\n }\n // parent.kind === 'path'\n if (child.kind === 'path')\n return child.shardId === parent.shardId && child.prefix.startsWith(parent.prefix);\n return false;\n}\n\ninterface SyncHandleDeps {\n tenantId: string;\n connectorId: string;\n engine: SyncEngine;\n registry: SyncRegistry;\n}\n\nexport function createSyncHandle(deps: SyncHandleDeps): SyncHandle {\n const { connectorId, engine, registry } = deps;\n\n async function currentGrants(): Promise<SyncScope[]> {\n const records = await registry.list(connectorId);\n return records.map((r) => r.scope);\n }\n\n async function requireScope(requested: SyncScope): Promise<SyncScope[]> {\n const grants = await currentGrants();\n const matching = grants.filter((g) => scopeContains(g, requested));\n if (matching.length === 0) throw new ScopeNotGrantedError(requested);\n if (requested.kind === 'tenant') {\n // Expand into the set of sub-scopes the connector is granted.\n const concrete = grants.filter((g) => g.kind === 'shard' || g.kind === 'path');\n return concrete.length > 0 ? concrete : [requested];\n }\n return [requested];\n }\n\n return {\n connectorId,\n\n async grantedScopes() {\n return currentGrants();\n },\n\n async getManifest(scope) {\n const concreteScopes = await requireScope(scope);\n const out: ManifestEntry[] = [];\n if (scope.kind === 'tenant' && concreteScopes[0]?.kind !== 'tenant') {\n for (const s of concreteScopes) out.push(...await engine.getManifest(connectorId, s));\n } else {\n out.push(...await engine.getManifest(connectorId, scope));\n }\n return out;\n },\n\n async changesSince(scope, cursor) {\n await requireScope(scope);\n return engine.changesSince(scope, cursor);\n },\n\n async ack(scope, cursor) {\n await requireScope(scope);\n await engine.ack(connectorId, scope, cursor);\n },\n\n async apply(scope, entry, opts) {\n await requireScope(scope);\n return engine.apply(connectorId, scope, entry, opts);\n },\n\n async applyBatch(scope, manifest, opts): Promise<ApplyBatchResult> {\n await requireScope(scope);\n return engine.applyBatch(connectorId, scope, manifest, opts);\n },\n\n async forget(scope, path) {\n await requireScope(scope);\n await engine.forget(scope, path);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/*
|
|
3
|
-
* Shell home — the view shown when no app is active.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Shell home — the view shown when no app is active.
|
|
4
|
+
*
|
|
5
|
+
* Layout: a title header, a filter bar, then one grid per visible
|
|
6
|
+
* section (User apps always, Admin apps when elevated). Each app is
|
|
7
|
+
* rendered as a square card; the whole card is the launch action.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
|
|
9
11
|
import ShellTitle from './ShellTitle.svelte';
|
|
10
12
|
|
|
13
|
+
let filter = $state('');
|
|
14
|
+
|
|
11
15
|
const apps = $derived(listRegisteredApps());
|
|
12
|
-
const userApps = $derived(apps.filter(m => !m.admin));
|
|
13
|
-
const adminApps = $derived(apps.filter(m => m.admin));
|
|
14
16
|
const elevated = $derived(isAdmin());
|
|
17
|
+
|
|
18
|
+
function matches(m: { id: string; label: string }, q: string): boolean {
|
|
19
|
+
if (!q) return true;
|
|
20
|
+
const needle = q.toLowerCase();
|
|
21
|
+
return m.label.toLowerCase().includes(needle) || m.id.toLowerCase().includes(needle);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const userApps = $derived(apps.filter((m) => !m.admin && matches(m, filter)));
|
|
25
|
+
const adminApps = $derived(apps.filter((m) => m.admin && matches(m, filter)));
|
|
26
|
+
const totalVisible = $derived(userApps.length + (elevated ? adminApps.length : 0));
|
|
15
27
|
</script>
|
|
16
28
|
|
|
17
29
|
<div class="shell-home">
|
|
@@ -26,54 +38,68 @@
|
|
|
26
38
|
</div>
|
|
27
39
|
</header>
|
|
28
40
|
|
|
41
|
+
<div class="shell-home-filter">
|
|
42
|
+
<input
|
|
43
|
+
type="search"
|
|
44
|
+
placeholder="Filter apps…"
|
|
45
|
+
bind:value={filter}
|
|
46
|
+
aria-label="Filter apps by name"
|
|
47
|
+
class="shell-home-filter-input"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
29
51
|
{#if userApps.length > 0}
|
|
30
52
|
<section class="shell-home-section">
|
|
31
53
|
<h2 class="shell-home-section-title">Apps</h2>
|
|
32
|
-
<
|
|
54
|
+
<div class="shell-home-grid">
|
|
33
55
|
{#each userApps as manifest (manifest.id)}
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
class="shell-home-card"
|
|
59
|
+
onclick={() => launchApp(manifest.id)}
|
|
60
|
+
title="Launch {manifest.label}"
|
|
61
|
+
>
|
|
62
|
+
<div class="shell-home-card-label">{manifest.label}</div>
|
|
63
|
+
<div class="shell-home-card-meta">
|
|
64
|
+
<span class="shell-home-card-id">{manifest.id}</span>
|
|
65
|
+
<span class="shell-home-card-version">v{manifest.version}</span>
|
|
38
66
|
</div>
|
|
39
|
-
|
|
40
|
-
type="button"
|
|
41
|
-
class="shell-home-launch"
|
|
42
|
-
onclick={() => launchApp(manifest.id)}
|
|
43
|
-
>
|
|
44
|
-
Launch
|
|
45
|
-
</button>
|
|
46
|
-
</li>
|
|
67
|
+
</button>
|
|
47
68
|
{/each}
|
|
48
|
-
</
|
|
69
|
+
</div>
|
|
49
70
|
</section>
|
|
50
71
|
{/if}
|
|
51
72
|
|
|
52
73
|
{#if elevated && adminApps.length > 0}
|
|
53
74
|
<section class="shell-home-section">
|
|
54
75
|
<h2 class="shell-home-section-title">Admin</h2>
|
|
55
|
-
<
|
|
76
|
+
<div class="shell-home-grid">
|
|
56
77
|
{#each adminApps as manifest (manifest.id)}
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="shell-home-card"
|
|
81
|
+
onclick={() => launchApp(manifest.id)}
|
|
82
|
+
title="Launch {manifest.label}"
|
|
83
|
+
>
|
|
84
|
+
<div class="shell-home-card-label">{manifest.label}</div>
|
|
85
|
+
<div class="shell-home-card-meta">
|
|
86
|
+
<span class="shell-home-card-id">{manifest.id}</span>
|
|
87
|
+
<span class="shell-home-card-version">v{manifest.version}</span>
|
|
61
88
|
</div>
|
|
62
|
-
|
|
63
|
-
type="button"
|
|
64
|
-
class="shell-home-launch"
|
|
65
|
-
onclick={() => launchApp(manifest.id)}
|
|
66
|
-
>
|
|
67
|
-
Launch
|
|
68
|
-
</button>
|
|
69
|
-
</li>
|
|
89
|
+
</button>
|
|
70
90
|
{/each}
|
|
71
|
-
</
|
|
91
|
+
</div>
|
|
72
92
|
</section>
|
|
73
93
|
{/if}
|
|
74
94
|
|
|
75
|
-
{#if
|
|
76
|
-
<p class="shell-home-empty">
|
|
95
|
+
{#if totalVisible === 0}
|
|
96
|
+
<p class="shell-home-empty">
|
|
97
|
+
{#if apps.length === 0}
|
|
98
|
+
No apps registered.
|
|
99
|
+
{:else}
|
|
100
|
+
No apps match “{filter}”.
|
|
101
|
+
{/if}
|
|
102
|
+
</p>
|
|
77
103
|
{/if}
|
|
78
104
|
</div>
|
|
79
105
|
|
|
@@ -93,7 +119,7 @@
|
|
|
93
119
|
}
|
|
94
120
|
.shell-home-header {
|
|
95
121
|
text-align: center;
|
|
96
|
-
margin-bottom:
|
|
122
|
+
margin-bottom: 24px;
|
|
97
123
|
display: flex;
|
|
98
124
|
flex-direction: column;
|
|
99
125
|
align-items: center;
|
|
@@ -136,14 +162,38 @@
|
|
|
136
162
|
position: relative;
|
|
137
163
|
top: -1px;
|
|
138
164
|
}
|
|
165
|
+
.shell-home-filter {
|
|
166
|
+
width: 100%;
|
|
167
|
+
max-width: 720px;
|
|
168
|
+
margin-bottom: 24px;
|
|
169
|
+
}
|
|
170
|
+
.shell-home-filter-input {
|
|
171
|
+
width: 100%;
|
|
172
|
+
padding: 10px 14px;
|
|
173
|
+
font: inherit;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
color: var(--shell-fg);
|
|
176
|
+
background: var(--shell-bg-elevated);
|
|
177
|
+
border: 1px solid var(--shell-border);
|
|
178
|
+
border-radius: var(--shell-radius-md);
|
|
179
|
+
outline: none;
|
|
180
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
181
|
+
}
|
|
182
|
+
.shell-home-filter-input::placeholder {
|
|
183
|
+
color: var(--shell-fg-muted);
|
|
184
|
+
}
|
|
185
|
+
.shell-home-filter-input:focus {
|
|
186
|
+
border-color: var(--shell-accent);
|
|
187
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 25%, transparent);
|
|
188
|
+
}
|
|
139
189
|
.shell-home-empty {
|
|
140
190
|
color: var(--shell-fg-muted);
|
|
141
191
|
font-style: italic;
|
|
142
192
|
}
|
|
143
193
|
.shell-home-section {
|
|
144
194
|
width: 100%;
|
|
145
|
-
max-width:
|
|
146
|
-
margin-bottom:
|
|
195
|
+
max-width: 720px;
|
|
196
|
+
margin-bottom: 28px;
|
|
147
197
|
}
|
|
148
198
|
.shell-home-section-title {
|
|
149
199
|
font-size: 13px;
|
|
@@ -153,40 +203,67 @@
|
|
|
153
203
|
color: var(--shell-fg-subtle);
|
|
154
204
|
margin: 0 0 12px;
|
|
155
205
|
}
|
|
156
|
-
.shell-home-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
206
|
+
.shell-home-grid {
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
|
209
|
+
gap: 10px;
|
|
210
|
+
}
|
|
211
|
+
.shell-home-card {
|
|
212
|
+
aspect-ratio: 1 / 1;
|
|
160
213
|
display: flex;
|
|
161
214
|
flex-direction: column;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
display: grid;
|
|
166
|
-
grid-template-columns: 1fr auto;
|
|
167
|
-
grid-template-rows: auto auto;
|
|
168
|
-
gap: 4px 16px;
|
|
169
|
-
align-items: center;
|
|
170
|
-
padding: 14px 18px;
|
|
215
|
+
justify-content: space-between;
|
|
216
|
+
text-align: left;
|
|
217
|
+
padding: 10px;
|
|
171
218
|
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
172
219
|
border: 1px solid var(--shell-border);
|
|
173
220
|
border-radius: var(--shell-radius-md);
|
|
221
|
+
color: inherit;
|
|
222
|
+
font: inherit;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
225
|
+
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
226
|
+
}
|
|
227
|
+
.shell-home-card:hover {
|
|
228
|
+
border-color: var(--shell-accent);
|
|
229
|
+
transform: translateY(-1px);
|
|
230
|
+
box-shadow:
|
|
231
|
+
0 6px 14px rgba(0, 0, 0, 0.3),
|
|
232
|
+
0 0 0 1px color-mix(in srgb, var(--shell-accent) 35%, transparent),
|
|
233
|
+
0 4px 12px color-mix(in srgb, var(--shell-accent) 18%, transparent);
|
|
174
234
|
}
|
|
175
|
-
.shell-home-
|
|
176
|
-
|
|
177
|
-
|
|
235
|
+
.shell-home-card:focus-visible {
|
|
236
|
+
outline: none;
|
|
237
|
+
border-color: var(--shell-accent);
|
|
238
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 40%, transparent);
|
|
239
|
+
}
|
|
240
|
+
.shell-home-card:active {
|
|
241
|
+
transform: translateY(0);
|
|
242
|
+
}
|
|
243
|
+
.shell-home-card-label {
|
|
178
244
|
font-weight: 600;
|
|
245
|
+
font-size: 12px;
|
|
246
|
+
line-height: 1.2;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
display: -webkit-box;
|
|
249
|
+
-webkit-box-orient: vertical;
|
|
250
|
+
-webkit-line-clamp: 2;
|
|
251
|
+
line-clamp: 2;
|
|
179
252
|
}
|
|
180
|
-
.shell-home-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
253
|
+
.shell-home-card-meta {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
gap: 1px;
|
|
257
|
+
font-size: 9px;
|
|
184
258
|
color: var(--shell-fg-subtle);
|
|
259
|
+
min-width: 0;
|
|
185
260
|
}
|
|
186
|
-
.shell-home-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
261
|
+
.shell-home-card-id {
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
text-overflow: ellipsis;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
.shell-home-card-version {
|
|
267
|
+
color: var(--shell-fg-muted);
|
|
191
268
|
}
|
|
192
269
|
</style>
|
|
@@ -23,13 +23,17 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { mount, unmount } from 'svelte';
|
|
25
25
|
import ShellHome from './ShellHome.svelte';
|
|
26
|
+
import KeysAndPeers from '../shell/views/KeysAndPeers.svelte';
|
|
26
27
|
import { VERSION } from '../version';
|
|
27
28
|
export const sh3coreShard = {
|
|
28
29
|
manifest: {
|
|
29
30
|
id: '__sh3core__',
|
|
30
31
|
label: 'SH3 Core',
|
|
31
32
|
version: VERSION,
|
|
32
|
-
views: [
|
|
33
|
+
views: [
|
|
34
|
+
{ id: 'sh3core:home', label: 'Home' },
|
|
35
|
+
{ id: 'shell:keys-and-peers', label: 'Keys & Peers' },
|
|
36
|
+
],
|
|
33
37
|
},
|
|
34
38
|
activate(ctx) {
|
|
35
39
|
const factory = {
|
|
@@ -43,7 +47,14 @@ export const sh3coreShard = {
|
|
|
43
47
|
};
|
|
44
48
|
},
|
|
45
49
|
};
|
|
50
|
+
const keysFactory = {
|
|
51
|
+
mount(container, _context) {
|
|
52
|
+
const instance = mount(KeysAndPeers, { target: container });
|
|
53
|
+
return { unmount() { unmount(instance); } };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
46
56
|
ctx.registerView('sh3core:home', factory);
|
|
57
|
+
ctx.registerView('shell:keys-and-peers', keysFactory);
|
|
47
58
|
},
|
|
48
59
|
autostart() {
|
|
49
60
|
// Intentionally empty. Defining this field is what puts the sh3core
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
+
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
6
|
+
describe('ctx.browse permission gating', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
__resetShardRegistryForTest();
|
|
9
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
10
|
+
__setTenantId('tenant-a');
|
|
11
|
+
});
|
|
12
|
+
it('is undefined when permission is absent', async () => {
|
|
13
|
+
let captured = null;
|
|
14
|
+
registerShard({
|
|
15
|
+
manifest: { id: 'no-browse', label: 'n', version: '0.0.0', views: [] },
|
|
16
|
+
activate(ctx) { captured = ctx; },
|
|
17
|
+
});
|
|
18
|
+
await activateShard('no-browse');
|
|
19
|
+
expect(captured.browse).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
it('is defined when documents:browse is declared', async () => {
|
|
22
|
+
var _a, _b, _c;
|
|
23
|
+
let captured = null;
|
|
24
|
+
registerShard({
|
|
25
|
+
manifest: {
|
|
26
|
+
id: 'has-browse', label: 'b', version: '0.0.0', views: [],
|
|
27
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE],
|
|
28
|
+
},
|
|
29
|
+
activate(ctx) { captured = ctx; },
|
|
30
|
+
});
|
|
31
|
+
await activateShard('has-browse');
|
|
32
|
+
expect(typeof ((_a = captured.browse) === null || _a === void 0 ? void 0 : _a.listDocuments)).toBe('function');
|
|
33
|
+
expect(typeof ((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.watchDocuments)).toBe('function');
|
|
34
|
+
expect(typeof ((_c = captured.browse) === null || _c === void 0 ? void 0 : _c.listShards)).toBe('function');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, deactivateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
+
import { emit } from '../keys/revocation-bus.svelte';
|
|
6
|
+
describe('onKeyRevoked hook wiring', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
__resetShardRegistryForTest();
|
|
9
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
10
|
+
__setTenantId('tenant-a');
|
|
11
|
+
});
|
|
12
|
+
it('fires onKeyRevoked when the bus emits for the shard', async () => {
|
|
13
|
+
const received = [];
|
|
14
|
+
registerShard({
|
|
15
|
+
manifest: { id: 'hook-shard', label: 'h', version: '0.0.0', views: [] },
|
|
16
|
+
activate() { },
|
|
17
|
+
onKeyRevoked(id) { received.push(id); },
|
|
18
|
+
});
|
|
19
|
+
await activateShard('hook-shard');
|
|
20
|
+
emit('hook-shard', 'key-abc');
|
|
21
|
+
// Handler is async; give microtasks a chance to flush.
|
|
22
|
+
await Promise.resolve();
|
|
23
|
+
expect(received).toEqual(['key-abc']);
|
|
24
|
+
});
|
|
25
|
+
it('does not fire for a different shardId', async () => {
|
|
26
|
+
const received = [];
|
|
27
|
+
registerShard({
|
|
28
|
+
manifest: { id: 'shard-x', label: 'x', version: '0.0.0', views: [] },
|
|
29
|
+
activate() { },
|
|
30
|
+
onKeyRevoked(id) { received.push(id); },
|
|
31
|
+
});
|
|
32
|
+
await activateShard('shard-x');
|
|
33
|
+
emit('shard-y', 'key-other');
|
|
34
|
+
await Promise.resolve();
|
|
35
|
+
expect(received).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
it('does not fire after deactivation', async () => {
|
|
38
|
+
const received = [];
|
|
39
|
+
registerShard({
|
|
40
|
+
manifest: { id: 'shard-deact', label: 'd', version: '0.0.0', views: [] },
|
|
41
|
+
activate() { },
|
|
42
|
+
onKeyRevoked(id) { received.push(id); },
|
|
43
|
+
});
|
|
44
|
+
await activateShard('shard-deact');
|
|
45
|
+
deactivateShard('shard-deact');
|
|
46
|
+
emit('shard-deact', 'key-gone');
|
|
47
|
+
await Promise.resolve();
|
|
48
|
+
expect(received).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
it('does not subscribe when onKeyRevoked is absent', async () => {
|
|
51
|
+
// Should not throw — just silently skips subscribing.
|
|
52
|
+
registerShard({
|
|
53
|
+
manifest: { id: 'no-hook', label: 'n', version: '0.0.0', views: [] },
|
|
54
|
+
activate() { },
|
|
55
|
+
});
|
|
56
|
+
await expect(activateShard('no-hook')).resolves.toBeUndefined();
|
|
57
|
+
// Emitting for a shard with no listener is a no-op.
|
|
58
|
+
expect(() => emit('no-hook', 'k')).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { __resetSyncBundlesForTest } from '../documents/sync/singleton';
|
|
5
|
+
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
6
|
+
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
7
|
+
describe('ctx.syncRegistry', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
__resetShardRegistryForTest();
|
|
10
|
+
__resetSyncBundlesForTest();
|
|
11
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
12
|
+
__setTenantId('tenant-a');
|
|
13
|
+
});
|
|
14
|
+
it('is undefined without documents:browse', async () => {
|
|
15
|
+
let captured = null;
|
|
16
|
+
registerShard({
|
|
17
|
+
manifest: { id: 'no-obs', label: 'n', version: '0.0.0', views: [] },
|
|
18
|
+
activate(ctx) { captured = ctx; },
|
|
19
|
+
});
|
|
20
|
+
await activateShard('no-obs');
|
|
21
|
+
expect(captured.syncRegistry).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
it('is available under documents:browse', async () => {
|
|
24
|
+
let captured = null;
|
|
25
|
+
registerShard({
|
|
26
|
+
manifest: {
|
|
27
|
+
id: 'obs', label: 'o', version: '0.0.0', views: [],
|
|
28
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE],
|
|
29
|
+
},
|
|
30
|
+
activate(ctx) { captured = ctx; },
|
|
31
|
+
});
|
|
32
|
+
await activateShard('obs');
|
|
33
|
+
const reg = captured.syncRegistry();
|
|
34
|
+
expect(typeof reg.list).toBe('function');
|
|
35
|
+
expect(typeof reg.listConflicts).toBe('function');
|
|
36
|
+
expect(typeof reg.listAllConnectorIds).toBe('function');
|
|
37
|
+
expect(typeof reg.revoke).toBe('function');
|
|
38
|
+
// Functional smoke: empty registry should return []
|
|
39
|
+
expect(await reg.list()).toEqual([]);
|
|
40
|
+
expect(await reg.listConflicts()).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
+
describe('ctx.tenantId', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
__resetShardRegistryForTest();
|
|
8
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
9
|
+
__setTenantId('tenant-a');
|
|
10
|
+
});
|
|
11
|
+
it('is present unconditionally on ctx', async () => {
|
|
12
|
+
let captured = null;
|
|
13
|
+
const shard = {
|
|
14
|
+
manifest: { id: 'test-tenantid', label: 't', version: '0.0.0', views: [] },
|
|
15
|
+
activate(ctx) { captured = ctx; },
|
|
16
|
+
};
|
|
17
|
+
registerShard(shard);
|
|
18
|
+
await activateShard('test-tenantid');
|
|
19
|
+
expect(captured.tenantId).toBe('tenant-a');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -50,6 +50,18 @@ export declare function isActive(id: string): boolean;
|
|
|
50
50
|
* Used by lifecycle.ts to pass context to `shard.resume()`.
|
|
51
51
|
*/
|
|
52
52
|
export declare function getShardContext(id: string): ShardContext | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Enumerate every view declared as `standalone` across the currently
|
|
55
|
+
* active shards. Intended for the `views --standalone` verb and any
|
|
56
|
+
* launcher UI that wants to surface "summonable" primitives. Only
|
|
57
|
+
* pulls from `activeShards` — registered-but-inactive shards aren't
|
|
58
|
+
* ready to mount.
|
|
59
|
+
*/
|
|
60
|
+
export declare function listStandaloneViews(): Array<{
|
|
61
|
+
shardId: string;
|
|
62
|
+
viewId: string;
|
|
63
|
+
label: string;
|
|
64
|
+
}>;
|
|
53
65
|
/**
|
|
54
66
|
* Test-only reset. Tears down any active shard entries (without running
|
|
55
67
|
* deactivate hooks — tests should run deactivate explicitly if they care)
|