sh3-core 0.8.2 → 0.9.1

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.
Files changed (102) hide show
  1. package/dist/api.d.ts +4 -7
  2. package/dist/api.js +2 -4
  3. package/dist/app/store/InstalledView.svelte +55 -1
  4. package/dist/app/store/PermissionConfirmModal.svelte +232 -0
  5. package/dist/app/store/PermissionConfirmModal.svelte.d.ts +17 -0
  6. package/dist/app/store/StoreView.svelte +119 -5
  7. package/dist/app/store/storeShard.svelte.d.ts +10 -1
  8. package/dist/app/store/storeShard.svelte.js +51 -7
  9. package/dist/app/store/storeShard.svelte.test.js +34 -0
  10. package/dist/apps/types.d.ts +3 -5
  11. package/dist/documents/backends.d.ts +2 -0
  12. package/dist/documents/backends.js +6 -0
  13. package/dist/documents/browse.d.ts +31 -1
  14. package/dist/documents/browse.js +18 -2
  15. package/dist/documents/browse.test.js +81 -0
  16. package/dist/documents/handle.js +13 -5
  17. package/dist/documents/handle.test.js +55 -0
  18. package/dist/documents/http-backend.d.ts +11 -4
  19. package/dist/documents/http-backend.js +37 -11
  20. package/dist/documents/index.d.ts +2 -1
  21. package/dist/documents/index.js +1 -1
  22. package/dist/documents/sync-types.d.ts +45 -0
  23. package/dist/documents/sync-types.js +11 -0
  24. package/dist/documents/types.d.ts +69 -2
  25. package/dist/documents/types.js +32 -2
  26. package/dist/keys/ConsentDialog.svelte +4 -4
  27. package/dist/keys/consent.test.js +4 -3
  28. package/dist/keys/types.d.ts +4 -2
  29. package/dist/registry/client.js +3 -0
  30. package/dist/registry/installer.d.ts +4 -1
  31. package/dist/registry/installer.js +25 -11
  32. package/dist/registry/permission-descriptions.d.ts +21 -0
  33. package/dist/registry/permission-descriptions.js +67 -0
  34. package/dist/registry/permission-descriptions.test.js +86 -0
  35. package/dist/registry/schema.js +19 -6
  36. package/dist/registry/types.d.ts +17 -5
  37. package/dist/server-shard/types.d.ts +55 -8
  38. package/dist/shards/activate-browse.test.js +87 -3
  39. package/dist/shards/activate.svelte.js +9 -31
  40. package/dist/shards/types.d.ts +0 -15
  41. package/dist/shell/views/KeysAndPeers.svelte +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +2 -10
  45. package/dist/documents/journal-hook.d.ts +0 -6
  46. package/dist/documents/journal-hook.js +0 -16
  47. package/dist/documents/sync/activate-integration.test.js +0 -37
  48. package/dist/documents/sync/components/DocumentSyncExplorer.svelte +0 -99
  49. package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +0 -15
  50. package/dist/documents/sync/components/SyncGrantPicker.svelte +0 -70
  51. package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +0 -12
  52. package/dist/documents/sync/conflicts.d.ts +0 -30
  53. package/dist/documents/sync/conflicts.js +0 -77
  54. package/dist/documents/sync/conflicts.test.js +0 -71
  55. package/dist/documents/sync/engine.d.ts +0 -19
  56. package/dist/documents/sync/engine.js +0 -188
  57. package/dist/documents/sync/engine.test.d.ts +0 -1
  58. package/dist/documents/sync/engine.test.js +0 -169
  59. package/dist/documents/sync/handle.d.ts +0 -11
  60. package/dist/documents/sync/handle.js +0 -79
  61. package/dist/documents/sync/handle.test.js +0 -56
  62. package/dist/documents/sync/hash.d.ts +0 -1
  63. package/dist/documents/sync/hash.js +0 -13
  64. package/dist/documents/sync/hash.test.d.ts +0 -1
  65. package/dist/documents/sync/hash.test.js +0 -20
  66. package/dist/documents/sync/index.d.ts +0 -5
  67. package/dist/documents/sync/index.js +0 -10
  68. package/dist/documents/sync/journal.d.ts +0 -30
  69. package/dist/documents/sync/journal.js +0 -179
  70. package/dist/documents/sync/journal.test.d.ts +0 -1
  71. package/dist/documents/sync/journal.test.js +0 -87
  72. package/dist/documents/sync/observer.d.ts +0 -3
  73. package/dist/documents/sync/observer.js +0 -45
  74. package/dist/documents/sync/registry.d.ts +0 -13
  75. package/dist/documents/sync/registry.js +0 -73
  76. package/dist/documents/sync/registry.test.d.ts +0 -1
  77. package/dist/documents/sync/registry.test.js +0 -53
  78. package/dist/documents/sync/serialization.d.ts +0 -5
  79. package/dist/documents/sync/serialization.js +0 -24
  80. package/dist/documents/sync/serialization.test.d.ts +0 -1
  81. package/dist/documents/sync/serialization.test.js +0 -26
  82. package/dist/documents/sync/singleton.d.ts +0 -11
  83. package/dist/documents/sync/singleton.js +0 -26
  84. package/dist/documents/sync/tombstones.d.ts +0 -19
  85. package/dist/documents/sync/tombstones.js +0 -58
  86. package/dist/documents/sync/tombstones.test.d.ts +0 -1
  87. package/dist/documents/sync/tombstones.test.js +0 -37
  88. package/dist/documents/sync/types.d.ts +0 -116
  89. package/dist/documents/sync/types.js +0 -27
  90. package/dist/documents/sync/write-hook.test.d.ts +0 -1
  91. package/dist/documents/sync/write-hook.test.js +0 -36
  92. package/dist/server-sync.d.ts +0 -6
  93. package/dist/server-sync.js +0 -634
  94. package/dist/server-sync.js.map +0 -7
  95. package/dist/shards/activate-sync-registry.test.d.ts +0 -1
  96. package/dist/shards/activate-sync-registry.test.js +0 -42
  97. package/dist/testing.d.ts +0 -3
  98. package/dist/testing.js +0 -77
  99. package/dist/testing.js.map +0 -7
  100. /package/dist/{documents/sync/activate-integration.test.d.ts → app/store/storeShard.svelte.test.d.ts} +0 -0
  101. /package/dist/documents/{sync/handle.test.d.ts → handle.test.d.ts} +0 -0
  102. /package/dist/{documents/sync/conflicts.test.d.ts → registry/permission-descriptions.test.d.ts} +0 -0
@@ -1,26 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { MemoryDocumentBackend } from '../backends';
3
- import { readJson, writeJson, listJsonPaths } from './serialization';
4
- import { SYNC_RESERVED_SHARD_ID } from './types';
5
- describe('sync serialization', () => {
6
- let backend;
7
- beforeEach(() => { backend = new MemoryDocumentBackend(); });
8
- it('writes and reads JSON round-trip', async () => {
9
- await writeJson(backend, 'tenant-a', 'foo/bar.json', { hello: 'world', n: 7 });
10
- const out = await readJson(backend, 'tenant-a', 'foo/bar.json');
11
- expect(out).toEqual({ hello: 'world', n: 7 });
12
- });
13
- it('returns null when a path is absent', async () => {
14
- expect(await readJson(backend, 'tenant-a', 'missing.json')).toBeNull();
15
- });
16
- it('lists JSON paths under a prefix', async () => {
17
- await writeJson(backend, 't', 'grants/a.json', { a: 1 });
18
- await writeJson(backend, 't', 'grants/b.json', { b: 2 });
19
- await writeJson(backend, 't', 'other/c.json', { c: 3 });
20
- const paths = await listJsonPaths(backend, 't', 'grants/');
21
- expect(paths.sort()).toEqual(['grants/a.json', 'grants/b.json']);
22
- });
23
- it('uses SYNC_RESERVED_SHARD_ID', () => {
24
- expect(SYNC_RESERVED_SHARD_ID).toBe('__sync__');
25
- });
26
- });
@@ -1,11 +0,0 @@
1
- import type { DocumentBackend } from '../types';
2
- import { SyncEngine } from './engine';
3
- import { type SyncRegistry } from './registry';
4
- interface Bundle {
5
- engine: SyncEngine;
6
- registry: SyncRegistry;
7
- }
8
- export declare function getSyncBundle(backend: DocumentBackend, tenantId: string): Promise<Bundle>;
9
- /** Test-only reset. */
10
- export declare function __resetSyncBundlesForTest(): void;
11
- export {};
@@ -1,26 +0,0 @@
1
- /*
2
- * Per-tenant SyncEngine singleton. Lazily initialised on first
3
- * ctx.sync() call. The journal appender hook is registered once
4
- * when the engine is first built so regular shard writes feed
5
- * the journal for all future connectors.
6
- */
7
- import { setJournalAppender } from '../journal-hook';
8
- import { SyncEngine } from './engine';
9
- import { createSyncRegistry } from './registry';
10
- const bundles = new Map();
11
- export async function getSyncBundle(backend, tenantId) {
12
- const existing = bundles.get(tenantId);
13
- if (existing)
14
- return existing;
15
- const engine = new SyncEngine(backend, tenantId);
16
- await engine.init();
17
- const registry = createSyncRegistry(backend, tenantId);
18
- setJournalAppender(async (e) => { await engine.journal.append(e); });
19
- const bundle = { engine, registry };
20
- bundles.set(tenantId, bundle);
21
- return bundle;
22
- }
23
- /** Test-only reset. */
24
- export function __resetSyncBundlesForTest() {
25
- bundles.clear();
26
- }
@@ -1,19 +0,0 @@
1
- import type { DocumentBackend } from '../types';
2
- export interface TombstoneRecord {
3
- deletedAt: number;
4
- lastHash: string;
5
- }
6
- export interface TombstoneEntry extends TombstoneRecord {
7
- shardId: string;
8
- path: string;
9
- }
10
- export declare class TombstoneStore {
11
- private backend;
12
- private tenantId;
13
- constructor(backend: DocumentBackend, tenantId: string);
14
- record(shardId: string, path: string, lastHash: string, deletedAt: number): Promise<void>;
15
- get(shardId: string, path: string): Promise<TombstoneRecord | null>;
16
- clear(shardId: string, path: string): Promise<void>;
17
- listByShard(shardId: string): Promise<TombstoneEntry[]>;
18
- listAll(): Promise<TombstoneEntry[]>;
19
- }
@@ -1,58 +0,0 @@
1
- /*
2
- * Tombstone store — records deletion metadata so sync connectors can
3
- * distinguish "never existed" from "was deleted." Backed by JSON docs
4
- * under the reserved sync shardId. GC is driven by the engine, not here.
5
- */
6
- import { readJson, writeJson, deletePath, listJsonPaths } from './serialization';
7
- const PREFIX = 'tombstones/';
8
- function key(shardId, path) {
9
- return `${PREFIX}${shardId}__${encodeURIComponent(path)}.json`;
10
- }
11
- export class TombstoneStore {
12
- constructor(backend, tenantId) {
13
- this.backend = backend;
14
- this.tenantId = tenantId;
15
- }
16
- async record(shardId, path, lastHash, deletedAt) {
17
- await writeJson(this.backend, this.tenantId, key(shardId, path), {
18
- deletedAt,
19
- lastHash,
20
- });
21
- }
22
- async get(shardId, path) {
23
- return readJson(this.backend, this.tenantId, key(shardId, path));
24
- }
25
- async clear(shardId, path) {
26
- await deletePath(this.backend, this.tenantId, key(shardId, path));
27
- }
28
- async listByShard(shardId) {
29
- const prefix = `${PREFIX}${shardId}__`;
30
- const paths = await listJsonPaths(this.backend, this.tenantId, prefix);
31
- const out = [];
32
- for (const p of paths) {
33
- const rec = await readJson(this.backend, this.tenantId, p);
34
- if (!rec)
35
- continue;
36
- const originalPath = decodeURIComponent(p.slice(prefix.length, -'.json'.length));
37
- out.push(Object.assign({ shardId, path: originalPath }, rec));
38
- }
39
- return out;
40
- }
41
- async listAll() {
42
- const paths = await listJsonPaths(this.backend, this.tenantId, PREFIX);
43
- const out = [];
44
- for (const p of paths) {
45
- const rec = await readJson(this.backend, this.tenantId, p);
46
- if (!rec)
47
- continue;
48
- const rest = p.slice(PREFIX.length, -'.json'.length);
49
- const sep = rest.indexOf('__');
50
- if (sep < 0)
51
- continue;
52
- const shardId = rest.slice(0, sep);
53
- const path = decodeURIComponent(rest.slice(sep + 2));
54
- out.push(Object.assign({ shardId, path }, rec));
55
- }
56
- return out;
57
- }
58
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,37 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { MemoryDocumentBackend } from '../backends';
3
- import { TombstoneStore } from './tombstones';
4
- describe('TombstoneStore', () => {
5
- let backend;
6
- let store;
7
- beforeEach(() => {
8
- backend = new MemoryDocumentBackend();
9
- store = new TombstoneStore(backend, 'tenant-a');
10
- });
11
- it('records and retrieves a tombstone', async () => {
12
- await store.record('shard-x', 'dir/file.md', 'hash123', 1000);
13
- const t = await store.get('shard-x', 'dir/file.md');
14
- expect(t).toEqual({ deletedAt: 1000, lastHash: 'hash123' });
15
- });
16
- it('returns null for non-tombstoned paths', async () => {
17
- expect(await store.get('shard-x', 'absent.md')).toBeNull();
18
- });
19
- it('clears a tombstone (on upsert of same path)', async () => {
20
- await store.record('shard-x', 'a.md', 'h', 1);
21
- await store.clear('shard-x', 'a.md');
22
- expect(await store.get('shard-x', 'a.md')).toBeNull();
23
- });
24
- it('lists tombstones within a shard', async () => {
25
- await store.record('shard-x', 'a.md', 'h1', 1);
26
- await store.record('shard-x', 'b.md', 'h2', 2);
27
- await store.record('shard-y', 'c.md', 'h3', 3);
28
- const list = await store.listByShard('shard-x');
29
- expect(list.map((t) => t.path).sort()).toEqual(['a.md', 'b.md']);
30
- });
31
- it('lists all tombstones across shards', async () => {
32
- await store.record('shard-x', 'a.md', 'h1', 1);
33
- await store.record('shard-y', 'c.md', 'h3', 3);
34
- const all = await store.listAll();
35
- expect(all.length).toBe(2);
36
- });
37
- });
@@ -1,116 +0,0 @@
1
- /** Permission string required in a shard manifest to obtain ctx.sync(). */
2
- export declare const PERMISSION_DOCUMENTS_SYNC = "documents:sync";
3
- /** Reserved shardId used to persist sync metadata (journal, tombstones, cursors, grants). */
4
- export declare const SYNC_RESERVED_SHARD_ID = "__sync__";
5
- export type SyncScope = {
6
- kind: 'shard';
7
- shardId: string;
8
- } | {
9
- kind: 'tenant';
10
- } | {
11
- kind: 'path';
12
- shardId: string;
13
- prefix: string;
14
- };
15
- export interface ManifestEntry {
16
- path: string;
17
- shardId: string;
18
- hash: string;
19
- size: number;
20
- lastModified: number;
21
- tombstone?: {
22
- deletedAt: number;
23
- };
24
- }
25
- export interface ApplyEntry {
26
- path: string;
27
- shardId: string;
28
- op: 'upsert' | 'delete';
29
- content?: string | ArrayBuffer;
30
- remoteHash: string;
31
- remoteMtime: number;
32
- }
33
- export interface ApplyOpts {
34
- onConflict?: ConflictPolicy;
35
- expectedLocalHash?: string;
36
- }
37
- export type ApplyOutcome = {
38
- status: 'applied';
39
- newHash: string;
40
- } | {
41
- status: 'skipped-identical';
42
- } | {
43
- status: 'conflict';
44
- resolution: ConflictResolution;
45
- };
46
- export interface ApplyBatchResult {
47
- applied: Array<{
48
- path: string;
49
- shardId: string;
50
- newHash: string;
51
- }>;
52
- skipped: Array<{
53
- path: string;
54
- shardId: string;
55
- reason: 'identical';
56
- }>;
57
- conflicts: ConflictResolution[];
58
- }
59
- export interface ConflictResolution {
60
- path: string;
61
- shardId: string;
62
- localHash: string;
63
- remoteHash: string;
64
- conflictArtifactPath: string;
65
- base?: {
66
- hash: string;
67
- };
68
- }
69
- export interface ConflictContext {
70
- path: string;
71
- shardId: string;
72
- localHash: string;
73
- remoteHash: string;
74
- baseHash?: string;
75
- }
76
- export type ConflictPolicy = 'default' | 'remote-wins' | 'local-wins' | 'keep-both' | ((ctx: ConflictContext) => Promise<'remote-wins' | 'local-wins' | 'keep-both'>);
77
- export interface JournalEntry {
78
- seq: number;
79
- ts: number;
80
- shardId: string;
81
- path: string;
82
- op: 'upsert' | 'delete';
83
- hash: string | null;
84
- }
85
- export interface ChangePage {
86
- entries: JournalEntry[];
87
- nextCursor: string;
88
- hasMore: boolean;
89
- truncated?: boolean;
90
- }
91
- export interface GrantRecord {
92
- connectorId: string;
93
- scope: SyncScope;
94
- grantedAt: number;
95
- }
96
- export interface SyncHandle {
97
- readonly connectorId: string;
98
- grantedScopes(): Promise<SyncScope[]>;
99
- getManifest(scope: SyncScope): Promise<ManifestEntry[]>;
100
- changesSince(scope: SyncScope, cursor?: string): Promise<ChangePage>;
101
- ack(scope: SyncScope, cursor: string): Promise<void>;
102
- apply(scope: SyncScope, entry: ApplyEntry, opts?: ApplyOpts): Promise<ApplyOutcome>;
103
- applyBatch(scope: SyncScope, manifest: ApplyEntry[], opts?: ApplyOpts): Promise<ApplyBatchResult>;
104
- forget(scope: SyncScope, path: string): Promise<void>;
105
- }
106
- export declare class ScopeNotGrantedError extends Error {
107
- readonly scope: SyncScope;
108
- constructor(scope: SyncScope);
109
- }
110
- export declare class ScopeRevokedError extends Error {
111
- readonly scope: SyncScope;
112
- constructor(scope: SyncScope);
113
- }
114
- export declare class TenantMismatchError extends Error {
115
- constructor();
116
- }
@@ -1,27 +0,0 @@
1
- /*
2
- * Document Sync API types. See docs/superpowers/specs/2026-04-14-document-sync-api-design.md.
3
- */
4
- /** Permission string required in a shard manifest to obtain ctx.sync(). */
5
- export const PERMISSION_DOCUMENTS_SYNC = 'documents:sync';
6
- /** Reserved shardId used to persist sync metadata (journal, tombstones, cursors, grants). */
7
- export const SYNC_RESERVED_SHARD_ID = '__sync__';
8
- export class ScopeNotGrantedError extends Error {
9
- constructor(scope) {
10
- super(`Scope not granted: ${JSON.stringify(scope)}`);
11
- this.scope = scope;
12
- this.name = 'ScopeNotGrantedError';
13
- }
14
- }
15
- export class ScopeRevokedError extends Error {
16
- constructor(scope) {
17
- super(`Scope revoked during operation: ${JSON.stringify(scope)}`);
18
- this.scope = scope;
19
- this.name = 'ScopeRevokedError';
20
- }
21
- }
22
- export class TenantMismatchError extends Error {
23
- constructor() {
24
- super('Sync handle tenantId does not match current session');
25
- this.name = 'TenantMismatchError';
26
- }
27
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,36 +0,0 @@
1
- // packages/sh3-core/src/documents/sync/write-hook.test.ts
2
- import { describe, it, expect, beforeEach } from 'vitest';
3
- import { MemoryDocumentBackend } from '../backends';
4
- import { __setDocumentBackend, __setTenantId } from '../config';
5
- import { createDocumentHandle } from '../handle';
6
- import { SyncEngine } from './engine';
7
- import { setJournalAppender, clearJournalAppender } from '../journal-hook';
8
- describe('regular document writes feed the sync journal', () => {
9
- let backend;
10
- let engine;
11
- beforeEach(async () => {
12
- backend = new MemoryDocumentBackend();
13
- __setDocumentBackend(backend);
14
- __setTenantId('tenant-a');
15
- engine = new SyncEngine(backend, 'tenant-a');
16
- await engine.init();
17
- setJournalAppender(async (e) => { await engine.journal.append(e); });
18
- });
19
- it('appends upsert to journal when shard writes via ctx.documents()', async () => {
20
- const h = createDocumentHandle('tenant-a', 'shard-x', backend, { format: 'text' });
21
- await h.write('a.md', 'hello');
22
- const page = await engine.journal.changesSince({ kind: 'tenant' });
23
- expect(page.entries.map((e) => `${e.shardId}:${e.path}:${e.op}`)).toEqual([
24
- 'shard-x:a.md:upsert',
25
- ]);
26
- clearJournalAppender();
27
- });
28
- it('appends delete to journal', async () => {
29
- const h = createDocumentHandle('tenant-a', 'shard-x', backend, { format: 'text' });
30
- await h.write('a.md', 'hello');
31
- await h.delete('a.md');
32
- const page = await engine.journal.changesSince({ kind: 'tenant' });
33
- expect(page.entries.map((e) => e.op)).toEqual(['upsert', 'delete']);
34
- clearJournalAppender();
35
- });
36
- });
@@ -1,6 +0,0 @@
1
- export { getSyncBundle } from './documents/sync/singleton';
2
- export { createSyncHandle } from './documents/sync/handle';
3
- export { createSyncRegistry } from './documents/sync/registry';
4
- export type { SyncHandle } from './documents/sync/types';
5
- export type { SyncRegistry } from './documents/sync/registry';
6
- export type { DocumentBackend, DocumentMeta } from './documents/types';