@de-otio/chaoskb-client 0.2.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.
- package/dist/cli/agent-registry/config-merger.d.ts +28 -0
- package/dist/cli/agent-registry/config-merger.d.ts.map +1 -0
- package/dist/cli/agent-registry/config-merger.js +90 -0
- package/dist/cli/agent-registry/config-merger.js.map +1 -0
- package/dist/cli/agent-registry/detector.d.ts +7 -0
- package/dist/cli/agent-registry/detector.d.ts.map +1 -0
- package/dist/cli/agent-registry/detector.js +100 -0
- package/dist/cli/agent-registry/detector.js.map +1 -0
- package/dist/cli/agent-registry/index.d.ts +26 -0
- package/dist/cli/agent-registry/index.d.ts.map +1 -0
- package/dist/cli/agent-registry/index.js +77 -0
- package/dist/cli/agent-registry/index.js.map +1 -0
- package/dist/cli/agent-registry/path-validator.d.ts +11 -0
- package/dist/cli/agent-registry/path-validator.d.ts.map +1 -0
- package/dist/cli/agent-registry/path-validator.js +69 -0
- package/dist/cli/agent-registry/path-validator.js.map +1 -0
- package/dist/cli/agent-registry/registry.json +108 -0
- package/dist/cli/agent-registry/types.d.ts +29 -0
- package/dist/cli/agent-registry/types.d.ts.map +1 -0
- package/dist/cli/agent-registry/types.js +2 -0
- package/dist/cli/agent-registry/types.js.map +1 -0
- package/dist/cli/bootstrap-lock.d.ts +7 -0
- package/dist/cli/bootstrap-lock.d.ts.map +1 -0
- package/dist/cli/bootstrap-lock.js +62 -0
- package/dist/cli/bootstrap-lock.js.map +1 -0
- package/dist/cli/bootstrap.d.ts +23 -0
- package/dist/cli/bootstrap.d.ts.map +1 -0
- package/dist/cli/bootstrap.js +438 -0
- package/dist/cli/bootstrap.js.map +1 -0
- package/dist/cli/commands/config.d.ts +13 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +244 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/devices.d.ts +21 -0
- package/dist/cli/commands/devices.d.ts.map +1 -0
- package/dist/cli/commands/devices.js +229 -0
- package/dist/cli/commands/devices.js.map +1 -0
- package/dist/cli/commands/export.d.ts +12 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +183 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/import.d.ts +26 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +311 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/kb.d.ts +39 -0
- package/dist/cli/commands/kb.d.ts.map +1 -0
- package/dist/cli/commands/kb.js +138 -0
- package/dist/cli/commands/kb.js.map +1 -0
- package/dist/cli/commands/project.d.ts +6 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +115 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/commands/projects.d.ts +33 -0
- package/dist/cli/commands/projects.d.ts.map +1 -0
- package/dist/cli/commands/projects.js +189 -0
- package/dist/cli/commands/projects.js.map +1 -0
- package/dist/cli/commands/register.d.ts +8 -0
- package/dist/cli/commands/register.d.ts.map +1 -0
- package/dist/cli/commands/register.js +146 -0
- package/dist/cli/commands/register.js.map +1 -0
- package/dist/cli/commands/rotate-key.d.ts +16 -0
- package/dist/cli/commands/rotate-key.d.ts.map +1 -0
- package/dist/cli/commands/rotate-key.js +197 -0
- package/dist/cli/commands/rotate-key.js.map +1 -0
- package/dist/cli/commands/setup-sync.d.ts +2 -0
- package/dist/cli/commands/setup-sync.d.ts.map +1 -0
- package/dist/cli/commands/setup-sync.js +165 -0
- package/dist/cli/commands/setup-sync.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +12 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +39 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/status.d.ts +5 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +96 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/uninstall.d.ts +4 -0
- package/dist/cli/commands/uninstall.d.ts.map +1 -0
- package/dist/cli/commands/uninstall.js +85 -0
- package/dist/cli/commands/uninstall.js.map +1 -0
- package/dist/cli/commands/unregister.d.ts +2 -0
- package/dist/cli/commands/unregister.d.ts.map +1 -0
- package/dist/cli/commands/unregister.js +46 -0
- package/dist/cli/commands/unregister.js.map +1 -0
- package/dist/cli/device-metadata.d.ts +15 -0
- package/dist/cli/device-metadata.d.ts.map +1 -0
- package/dist/cli/device-metadata.js +58 -0
- package/dist/cli/device-metadata.js.map +1 -0
- package/dist/cli/github.d.ts +38 -0
- package/dist/cli/github.d.ts.map +1 -0
- package/dist/cli/github.js +159 -0
- package/dist/cli/github.js.map +1 -0
- package/dist/cli/guide-hashes.json +13 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +226 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/mcp-server.d.ts +205 -0
- package/dist/cli/mcp-server.d.ts.map +1 -0
- package/dist/cli/mcp-server.js +366 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/tools/kb-delete.d.ts +10 -0
- package/dist/cli/tools/kb-delete.d.ts.map +1 -0
- package/dist/cli/tools/kb-delete.js +28 -0
- package/dist/cli/tools/kb-delete.js.map +1 -0
- package/dist/cli/tools/kb-ingest.d.ts +13 -0
- package/dist/cli/tools/kb-ingest.d.ts.map +1 -0
- package/dist/cli/tools/kb-ingest.js +72 -0
- package/dist/cli/tools/kb-ingest.js.map +1 -0
- package/dist/cli/tools/kb-list.d.ts +20 -0
- package/dist/cli/tools/kb-list.d.ts.map +1 -0
- package/dist/cli/tools/kb-list.js +24 -0
- package/dist/cli/tools/kb-list.js.map +1 -0
- package/dist/cli/tools/kb-query-shared.d.ts +27 -0
- package/dist/cli/tools/kb-query-shared.d.ts.map +1 -0
- package/dist/cli/tools/kb-query-shared.js +28 -0
- package/dist/cli/tools/kb-query-shared.js.map +1 -0
- package/dist/cli/tools/kb-query.d.ts +20 -0
- package/dist/cli/tools/kb-query.d.ts.map +1 -0
- package/dist/cli/tools/kb-query.js +109 -0
- package/dist/cli/tools/kb-query.js.map +1 -0
- package/dist/cli/tools/kb-summary.d.ts +29 -0
- package/dist/cli/tools/kb-summary.d.ts.map +1 -0
- package/dist/cli/tools/kb-summary.js +89 -0
- package/dist/cli/tools/kb-summary.js.map +1 -0
- package/dist/cli/tools/kb-sync-status.d.ts +7 -0
- package/dist/cli/tools/kb-sync-status.d.ts.map +1 -0
- package/dist/cli/tools/kb-sync-status.js +48 -0
- package/dist/cli/tools/kb-sync-status.js.map +1 -0
- package/dist/crypto/aad.d.ts +8 -0
- package/dist/crypto/aad.d.ts.map +1 -0
- package/dist/crypto/aad.js +11 -0
- package/dist/crypto/aad.js.map +1 -0
- package/dist/crypto/aead.d.ts +21 -0
- package/dist/crypto/aead.d.ts.map +1 -0
- package/dist/crypto/aead.js +43 -0
- package/dist/crypto/aead.js.map +1 -0
- package/dist/crypto/argon2.d.ts +11 -0
- package/dist/crypto/argon2.d.ts.map +1 -0
- package/dist/crypto/argon2.js +33 -0
- package/dist/crypto/argon2.js.map +1 -0
- package/dist/crypto/blob-id.d.ts +6 -0
- package/dist/crypto/blob-id.d.ts.map +1 -0
- package/dist/crypto/blob-id.js +33 -0
- package/dist/crypto/blob-id.js.map +1 -0
- package/dist/crypto/canonical-json.d.ts +6 -0
- package/dist/crypto/canonical-json.d.ts.map +1 -0
- package/dist/crypto/canonical-json.js +88 -0
- package/dist/crypto/canonical-json.js.map +1 -0
- package/dist/crypto/commitment.d.ts +12 -0
- package/dist/crypto/commitment.d.ts.map +1 -0
- package/dist/crypto/commitment.js +37 -0
- package/dist/crypto/commitment.js.map +1 -0
- package/dist/crypto/encryption-service.d.ts +19 -0
- package/dist/crypto/encryption-service.d.ts.map +1 -0
- package/dist/crypto/encryption-service.js +38 -0
- package/dist/crypto/encryption-service.js.map +1 -0
- package/dist/crypto/envelope-cbor.d.ts +37 -0
- package/dist/crypto/envelope-cbor.d.ts.map +1 -0
- package/dist/crypto/envelope-cbor.js +124 -0
- package/dist/crypto/envelope-cbor.js.map +1 -0
- package/dist/crypto/envelope.d.ts +34 -0
- package/dist/crypto/envelope.d.ts.map +1 -0
- package/dist/crypto/envelope.js +160 -0
- package/dist/crypto/envelope.js.map +1 -0
- package/dist/crypto/hkdf.d.ts +16 -0
- package/dist/crypto/hkdf.d.ts.map +1 -0
- package/dist/crypto/hkdf.js +33 -0
- package/dist/crypto/hkdf.js.map +1 -0
- package/dist/crypto/index.d.ts +15 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +15 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/invite.d.ts +31 -0
- package/dist/crypto/invite.d.ts.map +1 -0
- package/dist/crypto/invite.js +137 -0
- package/dist/crypto/invite.js.map +1 -0
- package/dist/crypto/keyring.d.ts +37 -0
- package/dist/crypto/keyring.d.ts.map +1 -0
- package/dist/crypto/keyring.js +219 -0
- package/dist/crypto/keyring.js.map +1 -0
- package/dist/crypto/known-keys.d.ts +34 -0
- package/dist/crypto/known-keys.d.ts.map +1 -0
- package/dist/crypto/known-keys.js +106 -0
- package/dist/crypto/known-keys.js.map +1 -0
- package/dist/crypto/project-keys.d.ts +26 -0
- package/dist/crypto/project-keys.d.ts.map +1 -0
- package/dist/crypto/project-keys.js +69 -0
- package/dist/crypto/project-keys.js.map +1 -0
- package/dist/crypto/secure-buffer.d.ts +31 -0
- package/dist/crypto/secure-buffer.d.ts.map +1 -0
- package/dist/crypto/secure-buffer.js +61 -0
- package/dist/crypto/secure-buffer.js.map +1 -0
- package/dist/crypto/ssh-agent.d.ts +16 -0
- package/dist/crypto/ssh-agent.d.ts.map +1 -0
- package/dist/crypto/ssh-agent.js +225 -0
- package/dist/crypto/ssh-agent.js.map +1 -0
- package/dist/crypto/ssh-keys.d.ts +19 -0
- package/dist/crypto/ssh-keys.d.ts.map +1 -0
- package/dist/crypto/ssh-keys.js +121 -0
- package/dist/crypto/ssh-keys.js.map +1 -0
- package/dist/crypto/tiers/enhanced.d.ts +25 -0
- package/dist/crypto/tiers/enhanced.d.ts.map +1 -0
- package/dist/crypto/tiers/enhanced.js +56 -0
- package/dist/crypto/tiers/enhanced.js.map +1 -0
- package/dist/crypto/tiers/maximum.d.ts +19 -0
- package/dist/crypto/tiers/maximum.d.ts.map +1 -0
- package/dist/crypto/tiers/maximum.js +25 -0
- package/dist/crypto/tiers/maximum.js.map +1 -0
- package/dist/crypto/tiers/standard.d.ts +27 -0
- package/dist/crypto/tiers/standard.d.ts.map +1 -0
- package/dist/crypto/tiers/standard.js +147 -0
- package/dist/crypto/tiers/standard.js.map +1 -0
- package/dist/crypto/types.d.ts +169 -0
- package/dist/crypto/types.d.ts.map +1 -0
- package/dist/crypto/types.js +11 -0
- package/dist/crypto/types.js.map +1 -0
- package/dist/pipeline/chunker.d.ts +27 -0
- package/dist/pipeline/chunker.d.ts.map +1 -0
- package/dist/pipeline/chunker.js +96 -0
- package/dist/pipeline/chunker.js.map +1 -0
- package/dist/pipeline/content-pipeline.d.ts +24 -0
- package/dist/pipeline/content-pipeline.d.ts.map +1 -0
- package/dist/pipeline/content-pipeline.js +49 -0
- package/dist/pipeline/content-pipeline.js.map +1 -0
- package/dist/pipeline/embedder.d.ts +49 -0
- package/dist/pipeline/embedder.d.ts.map +1 -0
- package/dist/pipeline/embedder.js +195 -0
- package/dist/pipeline/embedder.js.map +1 -0
- package/dist/pipeline/extract.d.ts +17 -0
- package/dist/pipeline/extract.d.ts.map +1 -0
- package/dist/pipeline/extract.js +70 -0
- package/dist/pipeline/extract.js.map +1 -0
- package/dist/pipeline/fetch.d.ts +26 -0
- package/dist/pipeline/fetch.d.ts.map +1 -0
- package/dist/pipeline/fetch.js +91 -0
- package/dist/pipeline/fetch.js.map +1 -0
- package/dist/pipeline/index.d.ts +10 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +10 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/model-manager.d.ts +57 -0
- package/dist/pipeline/model-manager.d.ts.map +1 -0
- package/dist/pipeline/model-manager.js +234 -0
- package/dist/pipeline/model-manager.js.map +1 -0
- package/dist/pipeline/search.d.ts +37 -0
- package/dist/pipeline/search.d.ts.map +1 -0
- package/dist/pipeline/search.js +65 -0
- package/dist/pipeline/search.js.map +1 -0
- package/dist/pipeline/tokenizer.d.ts +29 -0
- package/dist/pipeline/tokenizer.d.ts.map +1 -0
- package/dist/pipeline/tokenizer.js +54 -0
- package/dist/pipeline/tokenizer.js.map +1 -0
- package/dist/pipeline/types.d.ts +86 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +2 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/pipeline/wordpiece-tokenizer.d.ts +60 -0
- package/dist/pipeline/wordpiece-tokenizer.d.ts.map +1 -0
- package/dist/pipeline/wordpiece-tokenizer.js +251 -0
- package/dist/pipeline/wordpiece-tokenizer.js.map +1 -0
- package/dist/storage/chunk-repo.d.ts +29 -0
- package/dist/storage/chunk-repo.d.ts.map +1 -0
- package/dist/storage/chunk-repo.js +115 -0
- package/dist/storage/chunk-repo.js.map +1 -0
- package/dist/storage/database-manager.d.ts +17 -0
- package/dist/storage/database-manager.d.ts.map +1 -0
- package/dist/storage/database-manager.js +100 -0
- package/dist/storage/database-manager.js.map +1 -0
- package/dist/storage/database.d.ts +10 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +34 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/embedding-index.d.ts +22 -0
- package/dist/storage/embedding-index.d.ts.map +1 -0
- package/dist/storage/embedding-index.js +78 -0
- package/dist/storage/embedding-index.js.map +1 -0
- package/dist/storage/index.d.ts +10 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +10 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/kb-database.d.ts +11 -0
- package/dist/storage/kb-database.d.ts.map +1 -0
- package/dist/storage/kb-database.js +24 -0
- package/dist/storage/kb-database.js.map +1 -0
- package/dist/storage/schema.d.ts +6 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +122 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/source-repo.d.ts +20 -0
- package/dist/storage/source-repo.d.ts.map +1 -0
- package/dist/storage/source-repo.js +120 -0
- package/dist/storage/source-repo.js.map +1 -0
- package/dist/storage/sync-status-repo.d.ts +15 -0
- package/dist/storage/sync-status-repo.d.ts.map +1 -0
- package/dist/storage/sync-status-repo.js +40 -0
- package/dist/storage/sync-status-repo.js.map +1 -0
- package/dist/storage/types.d.ts +139 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +9 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/sync/canary.d.ts +14 -0
- package/dist/sync/canary.d.ts.map +1 -0
- package/dist/sync/canary.js +53 -0
- package/dist/sync/canary.js.map +1 -0
- package/dist/sync/full-sync.d.ts +16 -0
- package/dist/sync/full-sync.d.ts.map +1 -0
- package/dist/sync/full-sync.js +91 -0
- package/dist/sync/full-sync.js.map +1 -0
- package/dist/sync/http-client.d.ts +28 -0
- package/dist/sync/http-client.d.ts.map +1 -0
- package/dist/sync/http-client.js +90 -0
- package/dist/sync/http-client.js.map +1 -0
- package/dist/sync/incremental-sync.d.ts +17 -0
- package/dist/sync/incremental-sync.d.ts.map +1 -0
- package/dist/sync/incremental-sync.js +155 -0
- package/dist/sync/incremental-sync.js.map +1 -0
- package/dist/sync/index.d.ts +12 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +12 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/quota.d.ts +17 -0
- package/dist/sync/quota.d.ts.map +1 -0
- package/dist/sync/quota.js +48 -0
- package/dist/sync/quota.js.map +1 -0
- package/dist/sync/sequence.d.ts +21 -0
- package/dist/sync/sequence.d.ts.map +1 -0
- package/dist/sync/sequence.js +49 -0
- package/dist/sync/sequence.js.map +1 -0
- package/dist/sync/ssh-signer.d.ts +59 -0
- package/dist/sync/ssh-signer.d.ts.map +1 -0
- package/dist/sync/ssh-signer.js +241 -0
- package/dist/sync/ssh-signer.js.map +1 -0
- package/dist/sync/sync-service.d.ts +48 -0
- package/dist/sync/sync-service.d.ts.map +1 -0
- package/dist/sync/sync-service.js +116 -0
- package/dist/sync/sync-service.js.map +1 -0
- package/dist/sync/types.d.ts +106 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/upload-queue.d.ts +40 -0
- package/dist/sync/upload-queue.d.ts.map +1 -0
- package/dist/sync/upload-queue.js +148 -0
- package/dist/sync/upload-queue.js.map +1 -0
- package/dist/sync/verification.d.ts +17 -0
- package/dist/sync/verification.d.ts.map +1 -0
- package/dist/sync/verification.js +25 -0
- package/dist/sync/verification.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +16 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { SyncStatus } from '../storage/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Download changes from the server since the last sync timestamp.
|
|
4
|
+
*
|
|
5
|
+
* On first sync (no lastSyncTimestamp), downloads metadata for all blobs.
|
|
6
|
+
* For each new/updated blob, downloads the content and stores it locally.
|
|
7
|
+
* For each tombstone, soft-deletes the local record if it exists.
|
|
8
|
+
*
|
|
9
|
+
* Conflict resolution strategy:
|
|
10
|
+
* - New remote blobs (not present locally): accept as-is
|
|
11
|
+
* - Remote blob updated, local is synced: accept remote update
|
|
12
|
+
* - Remote blob updated, local has unsynchronized changes: last-write-wins
|
|
13
|
+
* - Remote tombstone, local has unsynchronized changes: keep local (local_wins)
|
|
14
|
+
*/
|
|
15
|
+
export async function incrementalSync(client, storage, lastSyncTimestamp) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const conflicts = [];
|
|
18
|
+
let newBlobs = 0;
|
|
19
|
+
let updatedBlobs = 0;
|
|
20
|
+
let deletedBlobs = 0;
|
|
21
|
+
// Fetch blob list from server
|
|
22
|
+
const listPath = lastSyncTimestamp
|
|
23
|
+
? `/v1/blobs?since=${encodeURIComponent(lastSyncTimestamp)}`
|
|
24
|
+
: '/v1/blobs';
|
|
25
|
+
const listResponse = await client.get(listPath);
|
|
26
|
+
if (!listResponse.ok) {
|
|
27
|
+
return {
|
|
28
|
+
newBlobs: 0,
|
|
29
|
+
updatedBlobs: 0,
|
|
30
|
+
deletedBlobs: 0,
|
|
31
|
+
conflicts: [],
|
|
32
|
+
errors: [
|
|
33
|
+
{
|
|
34
|
+
message: `Failed to list blobs: HTTP ${listResponse.status}`,
|
|
35
|
+
code: 'LIST_FAILED',
|
|
36
|
+
retryable: listResponse.status >= 500,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
success: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const data = (await listResponse.json());
|
|
43
|
+
// Download each new/updated blob
|
|
44
|
+
for (const blobMeta of data.blobs) {
|
|
45
|
+
try {
|
|
46
|
+
// Check for conflict: blob exists locally with unsynchronized changes
|
|
47
|
+
const existing = storage.syncStatus.get(blobMeta.id);
|
|
48
|
+
if (existing && existing.status === SyncStatus.LocalOnly) {
|
|
49
|
+
// Conflict: local has unsynchronized changes, remote also has changes.
|
|
50
|
+
// Resolve with last-write-wins based on timestamp.
|
|
51
|
+
const localTimestamp = existing.lastAttempt ?? '';
|
|
52
|
+
const remoteTimestamp = blobMeta.ts;
|
|
53
|
+
if (localTimestamp > remoteTimestamp) {
|
|
54
|
+
// Local is newer — keep local version, skip remote
|
|
55
|
+
conflicts.push({
|
|
56
|
+
blobId: blobMeta.id,
|
|
57
|
+
resolution: 'local_wins',
|
|
58
|
+
reason: 'Local changes are newer than remote',
|
|
59
|
+
localTimestamp,
|
|
60
|
+
remoteTimestamp,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Remote is newer — accept remote, overwrite local
|
|
66
|
+
conflicts.push({
|
|
67
|
+
blobId: blobMeta.id,
|
|
68
|
+
resolution: 'remote_wins',
|
|
69
|
+
reason: 'Remote changes are newer than local',
|
|
70
|
+
localTimestamp,
|
|
71
|
+
remoteTimestamp,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (existing && existing.status === SyncStatus.SyncFailed) {
|
|
76
|
+
// Previously failed sync — try again with remote version
|
|
77
|
+
conflicts.push({
|
|
78
|
+
blobId: blobMeta.id,
|
|
79
|
+
resolution: 'remote_wins',
|
|
80
|
+
reason: 'Local sync had failed, accepting remote version',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const blobResponse = await client.get(`/v1/blobs/${blobMeta.id}`);
|
|
84
|
+
if (!blobResponse.ok) {
|
|
85
|
+
errors.push({
|
|
86
|
+
blobId: blobMeta.id,
|
|
87
|
+
message: `Failed to download blob: HTTP ${blobResponse.status}`,
|
|
88
|
+
code: 'DOWNLOAD_FAILED',
|
|
89
|
+
retryable: blobResponse.status >= 500,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const blobData = new Uint8Array(await blobResponse.arrayBuffer());
|
|
94
|
+
if (existing) {
|
|
95
|
+
updatedBlobs++;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
newBlobs++;
|
|
99
|
+
}
|
|
100
|
+
// Store blob data — the caller is responsible for decryption.
|
|
101
|
+
// We store the raw encrypted envelope bytes as a source record.
|
|
102
|
+
// For now, update sync status to mark it as synced.
|
|
103
|
+
storage.syncStatus.set(blobMeta.id, SyncStatus.Synced);
|
|
104
|
+
// Store the blob data in the chunk repository as raw bytes.
|
|
105
|
+
// The actual deserialization into sources/chunks happens at a higher layer.
|
|
106
|
+
// Here we just track sync status.
|
|
107
|
+
void blobData; // Consumed by higher-layer processing
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
errors.push({
|
|
111
|
+
blobId: blobMeta.id,
|
|
112
|
+
message: error instanceof Error ? error.message : String(error),
|
|
113
|
+
code: 'DOWNLOAD_ERROR',
|
|
114
|
+
retryable: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Process tombstones
|
|
119
|
+
for (const tombstone of data.tombstones) {
|
|
120
|
+
try {
|
|
121
|
+
const existing = storage.syncStatus.get(tombstone.id);
|
|
122
|
+
if (existing) {
|
|
123
|
+
if (existing.status === SyncStatus.LocalOnly) {
|
|
124
|
+
// Conflict: remote deleted, but local has unsynchronized changes.
|
|
125
|
+
// Keep local version — user's local edits take priority over remote deletion.
|
|
126
|
+
conflicts.push({
|
|
127
|
+
blobId: tombstone.id,
|
|
128
|
+
resolution: 'local_wins',
|
|
129
|
+
reason: 'Remote deleted but local has unsynchronized changes',
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
storage.syncStatus.set(tombstone.id, SyncStatus.PendingDelete);
|
|
134
|
+
deletedBlobs++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
errors.push({
|
|
139
|
+
blobId: tombstone.id,
|
|
140
|
+
message: error instanceof Error ? error.message : String(error),
|
|
141
|
+
code: 'TOMBSTONE_ERROR',
|
|
142
|
+
retryable: false,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
newBlobs,
|
|
148
|
+
updatedBlobs,
|
|
149
|
+
deletedBlobs,
|
|
150
|
+
conflicts,
|
|
151
|
+
errors,
|
|
152
|
+
success: errors.length === 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=incremental-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incremental-sync.js","sourceRoot":"","sources":["../../sync/incremental-sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export { SSHSigner } from './ssh-signer.js';
|
|
3
|
+
export { SequenceCounter } from './sequence.js';
|
|
4
|
+
export { SyncHttpClient, RetryableError } from './http-client.js';
|
|
5
|
+
export { incrementalSync } from './incremental-sync.js';
|
|
6
|
+
export { fullSync } from './full-sync.js';
|
|
7
|
+
export { UploadQueue } from './upload-queue.js';
|
|
8
|
+
export { verifyCanary } from './canary.js';
|
|
9
|
+
export { verifyBlobCount } from './verification.js';
|
|
10
|
+
export { parseQuotaError, getQuotaWarning } from './quota.js';
|
|
11
|
+
export { SyncService } from './sync-service.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../sync/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export { SSHSigner } from './ssh-signer.js';
|
|
3
|
+
export { SequenceCounter } from './sequence.js';
|
|
4
|
+
export { SyncHttpClient, RetryableError } from './http-client.js';
|
|
5
|
+
export { incrementalSync } from './incremental-sync.js';
|
|
6
|
+
export { fullSync } from './full-sync.js';
|
|
7
|
+
export { UploadQueue } from './upload-queue.js';
|
|
8
|
+
export { verifyCanary } from './canary.js';
|
|
9
|
+
export { verifyBlobCount } from './verification.js';
|
|
10
|
+
export { parseQuotaError, getQuotaWarning } from './quota.js';
|
|
11
|
+
export { SyncService } from './sync-service.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../sync/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { QuotaInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a 413 (Payload Too Large / Quota Exceeded) response into QuotaInfo.
|
|
4
|
+
*
|
|
5
|
+
* Expects the response body to contain `{ used: number, limit: number }`
|
|
6
|
+
* where both values are in bytes.
|
|
7
|
+
*
|
|
8
|
+
* @returns QuotaInfo if the response could be parsed, null otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseQuotaError(response: Response): Promise<QuotaInfo | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Get a user-facing warning string based on quota usage.
|
|
13
|
+
*
|
|
14
|
+
* @returns A warning message, or null if usage is below the warning threshold.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getQuotaWarning(quota: QuotaInfo): string | null;
|
|
17
|
+
//# sourceMappingURL=quota.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../../sync/quota.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAmBnF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAc/D"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a 413 (Payload Too Large / Quota Exceeded) response into QuotaInfo.
|
|
3
|
+
*
|
|
4
|
+
* Expects the response body to contain `{ used: number, limit: number }`
|
|
5
|
+
* where both values are in bytes.
|
|
6
|
+
*
|
|
7
|
+
* @returns QuotaInfo if the response could be parsed, null otherwise.
|
|
8
|
+
*/
|
|
9
|
+
export async function parseQuotaError(response) {
|
|
10
|
+
if (response.status !== 413) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const data = (await response.json());
|
|
15
|
+
if (typeof data.used !== 'number' || typeof data.limit !== 'number') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const percentage = data.limit > 0 ? Math.round((data.used / data.limit) * 100) : 100;
|
|
19
|
+
return {
|
|
20
|
+
used: data.used,
|
|
21
|
+
limit: data.limit,
|
|
22
|
+
percentage,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get a user-facing warning string based on quota usage.
|
|
31
|
+
*
|
|
32
|
+
* @returns A warning message, or null if usage is below the warning threshold.
|
|
33
|
+
*/
|
|
34
|
+
export function getQuotaWarning(quota) {
|
|
35
|
+
const usedMB = Math.round(quota.used / (1024 * 1024));
|
|
36
|
+
const limitMB = Math.round(quota.limit / (1024 * 1024));
|
|
37
|
+
if (quota.percentage >= 100) {
|
|
38
|
+
return 'Storage limit reached. Articles are stored locally only and not synced.';
|
|
39
|
+
}
|
|
40
|
+
if (quota.percentage >= 95) {
|
|
41
|
+
return `Storage is nearly full (${usedMB}MB / ${limitMB}MB). New articles will be stored locally only.`;
|
|
42
|
+
}
|
|
43
|
+
if (quota.percentage >= 80) {
|
|
44
|
+
return `Storage is 80% full (${usedMB}MB / ${limitMB}MB)`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=quota.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota.js","sourceRoot":"","sources":["../../sync/quota.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAkB;IACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAsC,CAAC;QAC1E,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACpE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACrF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU;SACX,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAgB;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;IAExD,IAAI,KAAK,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;QAC5B,OAAO,yEAAyE,CAAC;IACnF,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QAC3B,OAAO,2BAA2B,MAAM,QAAQ,OAAO,gDAAgD,CAAC;IAC1G,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QAC3B,OAAO,wBAAwB,MAAM,QAAQ,OAAO,KAAK,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-device monotonic sequence counter for replay protection.
|
|
3
|
+
*
|
|
4
|
+
* Each signed request includes a sequence number that the server tracks.
|
|
5
|
+
* The server rejects any request with a sequence <= the highest it has seen,
|
|
6
|
+
* preventing replay attacks.
|
|
7
|
+
*
|
|
8
|
+
* The counter is persisted to disk so it survives process restarts.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SequenceCounter {
|
|
11
|
+
private readonly filePath;
|
|
12
|
+
private current;
|
|
13
|
+
constructor(filePath?: string);
|
|
14
|
+
/** Get the next sequence number (monotonically increasing). */
|
|
15
|
+
next(): number;
|
|
16
|
+
/** Get the current sequence number without incrementing. */
|
|
17
|
+
peek(): number;
|
|
18
|
+
private load;
|
|
19
|
+
private persist;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=sequence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../sync/sequence.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAS;gBAEZ,QAAQ,CAAC,EAAE,MAAM;IAK7B,+DAA+D;IAC/D,IAAI,IAAI,MAAM;IAMd,4DAA4D;IAC5D,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,OAAO;CAShB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Per-device monotonic sequence counter for replay protection.
|
|
6
|
+
*
|
|
7
|
+
* Each signed request includes a sequence number that the server tracks.
|
|
8
|
+
* The server rejects any request with a sequence <= the highest it has seen,
|
|
9
|
+
* preventing replay attacks.
|
|
10
|
+
*
|
|
11
|
+
* The counter is persisted to disk so it survives process restarts.
|
|
12
|
+
*/
|
|
13
|
+
export class SequenceCounter {
|
|
14
|
+
filePath;
|
|
15
|
+
current;
|
|
16
|
+
constructor(filePath) {
|
|
17
|
+
this.filePath = filePath ?? join(homedir(), '.chaoskb', 'sequence');
|
|
18
|
+
this.current = this.load();
|
|
19
|
+
}
|
|
20
|
+
/** Get the next sequence number (monotonically increasing). */
|
|
21
|
+
next() {
|
|
22
|
+
this.current += 1;
|
|
23
|
+
this.persist();
|
|
24
|
+
return this.current;
|
|
25
|
+
}
|
|
26
|
+
/** Get the current sequence number without incrementing. */
|
|
27
|
+
peek() {
|
|
28
|
+
return this.current;
|
|
29
|
+
}
|
|
30
|
+
load() {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(this.filePath, 'utf-8').trim();
|
|
33
|
+
const value = parseInt(content, 10);
|
|
34
|
+
return isNaN(value) ? 0 : value;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
persist() {
|
|
41
|
+
const dir = dirname(this.filePath);
|
|
42
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
43
|
+
// Atomic write: write to temp file, then rename
|
|
44
|
+
const tmpPath = this.filePath + '.tmp';
|
|
45
|
+
writeFileSync(tmpPath, String(this.current), { mode: 0o600 });
|
|
46
|
+
renameSync(tmpPath, this.filePath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=sequence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequence.js","sourceRoot":"","sources":["../../sync/sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAe;IACT,QAAQ,CAAS;IAC1B,OAAO,CAAS;IAExB,YAAY,QAAiB;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;QACpE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,+DAA+D;IAC/D,IAAI;QACF,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,4DAA4D;IAC5D,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAEO,IAAI;QACV,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5D,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACpC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAEO,OAAO;QACb,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAEjD,gDAAgD;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC;QACvC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;CACF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs HTTP requests with an SSH private key for ChaosKB-SSH authentication.
|
|
3
|
+
*
|
|
4
|
+
* Uses Ed25519 keys. Attempts ssh-agent first if SSH_AUTH_SOCK is available,
|
|
5
|
+
* then falls back to reading the private key file from disk.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SSHSigner {
|
|
8
|
+
private readonly keyPath;
|
|
9
|
+
constructor(sshKeyPath?: string);
|
|
10
|
+
/**
|
|
11
|
+
* Sign an HTTP request, returning headers for authentication.
|
|
12
|
+
*/
|
|
13
|
+
signRequest(method: string, path: string, sequence: number, body?: Uint8Array): Promise<{
|
|
14
|
+
authorization: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
sequence: number;
|
|
17
|
+
publicKey: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Compute SHA-256 hex digest of body bytes. Empty string if no body.
|
|
21
|
+
*/
|
|
22
|
+
computeBodyHash(body?: Uint8Array): string;
|
|
23
|
+
/**
|
|
24
|
+
* Build the canonical string to be signed.
|
|
25
|
+
*
|
|
26
|
+
* Format: chaoskb-auth\nMETHOD PATH\nTIMESTAMP\nSEQUENCE\nBODY_HASH
|
|
27
|
+
* The sequence number prevents replay attacks — the server rejects
|
|
28
|
+
* any sequence <= the highest it has seen for this device.
|
|
29
|
+
*/
|
|
30
|
+
buildCanonical(method: string, path: string, timestamp: string, sequence: number, bodyHash: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Read the SSH public key from the .pub file alongside the private key.
|
|
33
|
+
* Returns the raw public key content as a UTF-8 string.
|
|
34
|
+
*/
|
|
35
|
+
private readPublicKey;
|
|
36
|
+
/**
|
|
37
|
+
* Sign canonical data using the Ed25519 private key.
|
|
38
|
+
*
|
|
39
|
+
* Attempts ssh-agent first if SSH_AUTH_SOCK is set, falling back to
|
|
40
|
+
* reading the key file from disk.
|
|
41
|
+
*/
|
|
42
|
+
private signCanonical;
|
|
43
|
+
/**
|
|
44
|
+
* Sign using the SSH private key file on disk with Ed25519.
|
|
45
|
+
*/
|
|
46
|
+
private signWithKeyFile;
|
|
47
|
+
/**
|
|
48
|
+
* Sign using ssh-agent via SSH_AUTH_SOCK.
|
|
49
|
+
*
|
|
50
|
+
* Implements the SSH agent protocol (draft-miller-ssh-agent):
|
|
51
|
+
* 1. Connect to the Unix domain socket at SSH_AUTH_SOCK
|
|
52
|
+
* 2. Send SSH_AGENTC_REQUEST_IDENTITIES to list available keys
|
|
53
|
+
* 3. Find an Ed25519 key matching our public key
|
|
54
|
+
* 4. Send SSH_AGENTC_SIGN_REQUEST with the data to sign
|
|
55
|
+
* 5. Parse the SSH_AGENT_SIGN_RESPONSE to extract the signature
|
|
56
|
+
*/
|
|
57
|
+
private signWithAgent;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=ssh-signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-signer.d.ts","sourceRoot":"","sources":["../../sync/ssh-signer.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,UAAU,CAAC,EAAE,MAAM;IAI/B;;OAEG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAiBF;;OAEG;IACH,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM;IAO1C;;;;;;OAMG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM;IAIT;;;OAGG;YACW,aAAa;IAM3B;;;;;OAKG;YACW,aAAa;IAa3B;;OAEG;YACW,eAAe;IAS7B;;;;;;;;;OASG;YACW,aAAa;CAyB5B"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash, sign as cryptoSign } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { connect } from 'node:net';
|
|
6
|
+
/**
|
|
7
|
+
* Signs HTTP requests with an SSH private key for ChaosKB-SSH authentication.
|
|
8
|
+
*
|
|
9
|
+
* Uses Ed25519 keys. Attempts ssh-agent first if SSH_AUTH_SOCK is available,
|
|
10
|
+
* then falls back to reading the private key file from disk.
|
|
11
|
+
*/
|
|
12
|
+
export class SSHSigner {
|
|
13
|
+
keyPath;
|
|
14
|
+
constructor(sshKeyPath) {
|
|
15
|
+
this.keyPath = sshKeyPath ?? join(homedir(), '.ssh', 'id_ed25519');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sign an HTTP request, returning headers for authentication.
|
|
19
|
+
*/
|
|
20
|
+
async signRequest(method, path, sequence, body) {
|
|
21
|
+
const timestamp = new Date().toISOString();
|
|
22
|
+
const bodyHash = this.computeBodyHash(body);
|
|
23
|
+
const canonical = this.buildCanonical(method, path, timestamp, sequence, bodyHash);
|
|
24
|
+
const publicKeyRaw = await this.readPublicKey();
|
|
25
|
+
const signature = await this.signCanonical(canonical);
|
|
26
|
+
const base64Sig = signature.toString('base64');
|
|
27
|
+
// Extract the base64 key blob from the public key line (ssh-ed25519 AAAA... comment)
|
|
28
|
+
const parts = publicKeyRaw.split(/\s+/);
|
|
29
|
+
const publicKey = parts.length >= 2 ? parts[1] : publicKeyRaw;
|
|
30
|
+
const authorization = `SSH-Signature ${base64Sig}`;
|
|
31
|
+
return { authorization, timestamp, sequence, publicKey };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute SHA-256 hex digest of body bytes. Empty string if no body.
|
|
35
|
+
*/
|
|
36
|
+
computeBodyHash(body) {
|
|
37
|
+
if (!body || body.length === 0) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
return createHash('sha256').update(body).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the canonical string to be signed.
|
|
44
|
+
*
|
|
45
|
+
* Format: chaoskb-auth\nMETHOD PATH\nTIMESTAMP\nSEQUENCE\nBODY_HASH
|
|
46
|
+
* The sequence number prevents replay attacks — the server rejects
|
|
47
|
+
* any sequence <= the highest it has seen for this device.
|
|
48
|
+
*/
|
|
49
|
+
buildCanonical(method, path, timestamp, sequence, bodyHash) {
|
|
50
|
+
return `chaoskb-auth\n${method} ${path}\n${timestamp}\n${sequence}\n${bodyHash}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read the SSH public key from the .pub file alongside the private key.
|
|
54
|
+
* Returns the raw public key content as a UTF-8 string.
|
|
55
|
+
*/
|
|
56
|
+
async readPublicKey() {
|
|
57
|
+
const pubKeyPath = this.keyPath + '.pub';
|
|
58
|
+
const content = await readFile(pubKeyPath, 'utf-8');
|
|
59
|
+
return content.trim();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Sign canonical data using the Ed25519 private key.
|
|
63
|
+
*
|
|
64
|
+
* Attempts ssh-agent first if SSH_AUTH_SOCK is set, falling back to
|
|
65
|
+
* reading the key file from disk.
|
|
66
|
+
*/
|
|
67
|
+
async signCanonical(canonical) {
|
|
68
|
+
// Attempt ssh-agent if SSH_AUTH_SOCK is set
|
|
69
|
+
if (process.env.SSH_AUTH_SOCK) {
|
|
70
|
+
try {
|
|
71
|
+
return await this.signWithAgent(canonical);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Fall through to file-based signing
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return this.signWithKeyFile(canonical);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Sign using the SSH private key file on disk with Ed25519.
|
|
81
|
+
*/
|
|
82
|
+
async signWithKeyFile(canonical) {
|
|
83
|
+
const keyData = await readFile(this.keyPath, 'utf-8');
|
|
84
|
+
const data = Buffer.from(canonical, 'utf-8');
|
|
85
|
+
return cryptoSign(undefined, data, {
|
|
86
|
+
key: keyData,
|
|
87
|
+
format: 'pem',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sign using ssh-agent via SSH_AUTH_SOCK.
|
|
92
|
+
*
|
|
93
|
+
* Implements the SSH agent protocol (draft-miller-ssh-agent):
|
|
94
|
+
* 1. Connect to the Unix domain socket at SSH_AUTH_SOCK
|
|
95
|
+
* 2. Send SSH_AGENTC_REQUEST_IDENTITIES to list available keys
|
|
96
|
+
* 3. Find an Ed25519 key matching our public key
|
|
97
|
+
* 4. Send SSH_AGENTC_SIGN_REQUEST with the data to sign
|
|
98
|
+
* 5. Parse the SSH_AGENT_SIGN_RESPONSE to extract the signature
|
|
99
|
+
*/
|
|
100
|
+
async signWithAgent(canonical) {
|
|
101
|
+
const socketPath = process.env.SSH_AUTH_SOCK;
|
|
102
|
+
if (!socketPath) {
|
|
103
|
+
throw new Error('SSH_AUTH_SOCK not set');
|
|
104
|
+
}
|
|
105
|
+
const pubKeyContent = await this.readPublicKey();
|
|
106
|
+
const pubKeyBlob = parseSSHPublicKey(pubKeyContent);
|
|
107
|
+
const socket = await connectToAgent(socketPath);
|
|
108
|
+
try {
|
|
109
|
+
// Request the agent sign our data
|
|
110
|
+
const data = Buffer.from(canonical, 'utf-8');
|
|
111
|
+
const signatureBlob = await agentSign(socket, pubKeyBlob, data);
|
|
112
|
+
// The signature blob from the agent is in SSH wire format:
|
|
113
|
+
// string signature-format (e.g., "ssh-ed25519")
|
|
114
|
+
// string signature-blob
|
|
115
|
+
// We need the raw signature bytes for our authorization header.
|
|
116
|
+
const sigData = parseSSHSignature(signatureBlob);
|
|
117
|
+
return sigData;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
socket.destroy();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// --- SSH Agent Protocol Constants ---
|
|
125
|
+
/** SSH agent message types */
|
|
126
|
+
const SSH_AGENTC_SIGN_REQUEST = 13;
|
|
127
|
+
const SSH_AGENT_SIGN_RESPONSE = 14;
|
|
128
|
+
const SSH_AGENT_FAILURE = 5;
|
|
129
|
+
// --- SSH Agent Protocol Helpers ---
|
|
130
|
+
/**
|
|
131
|
+
* Connect to the ssh-agent Unix domain socket.
|
|
132
|
+
*/
|
|
133
|
+
function connectToAgent(socketPath) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const socket = connect(socketPath, () => resolve(socket));
|
|
136
|
+
socket.on('error', reject);
|
|
137
|
+
socket.setTimeout(5000);
|
|
138
|
+
socket.on('timeout', () => {
|
|
139
|
+
socket.destroy();
|
|
140
|
+
reject(new Error('ssh-agent connection timed out'));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Send an SSH_AGENTC_SIGN_REQUEST and read the response.
|
|
146
|
+
*
|
|
147
|
+
* Wire format:
|
|
148
|
+
* uint32 length
|
|
149
|
+
* byte SSH_AGENTC_SIGN_REQUEST (13)
|
|
150
|
+
* string key_blob
|
|
151
|
+
* string data
|
|
152
|
+
* uint32 flags (0 for default)
|
|
153
|
+
*/
|
|
154
|
+
function agentSign(socket, keyBlob, data) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
// Build the message body
|
|
157
|
+
const bodyParts = [
|
|
158
|
+
Buffer.from([SSH_AGENTC_SIGN_REQUEST]),
|
|
159
|
+
sshString(keyBlob),
|
|
160
|
+
sshString(data),
|
|
161
|
+
uint32(0), // flags
|
|
162
|
+
];
|
|
163
|
+
const body = Buffer.concat(bodyParts);
|
|
164
|
+
// Prepend length header
|
|
165
|
+
const message = Buffer.concat([uint32(body.length), body]);
|
|
166
|
+
socket.write(message);
|
|
167
|
+
// Read the response
|
|
168
|
+
const chunks = [];
|
|
169
|
+
socket.on('data', (chunk) => {
|
|
170
|
+
chunks.push(chunk);
|
|
171
|
+
// Check if we have enough data
|
|
172
|
+
const response = Buffer.concat(chunks);
|
|
173
|
+
if (response.length < 4)
|
|
174
|
+
return; // Need length header
|
|
175
|
+
const responseLen = response.readUInt32BE(0);
|
|
176
|
+
if (response.length < 4 + responseLen)
|
|
177
|
+
return; // Need full message
|
|
178
|
+
const msgType = response[4];
|
|
179
|
+
if (msgType === SSH_AGENT_FAILURE) {
|
|
180
|
+
reject(new Error('ssh-agent refused the signing request (key may not be loaded)'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (msgType !== SSH_AGENT_SIGN_RESPONSE) {
|
|
184
|
+
reject(new Error(`Unexpected ssh-agent response type: ${msgType}`));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Parse: byte SSH_AGENT_SIGN_RESPONSE, string signature
|
|
188
|
+
const sigOffset = 5; // 4 (length) + 1 (type)
|
|
189
|
+
const sigLen = response.readUInt32BE(sigOffset);
|
|
190
|
+
const signatureBlob = response.subarray(sigOffset + 4, sigOffset + 4 + sigLen);
|
|
191
|
+
resolve(Buffer.from(signatureBlob));
|
|
192
|
+
});
|
|
193
|
+
socket.on('error', reject);
|
|
194
|
+
socket.on('close', () => {
|
|
195
|
+
reject(new Error('ssh-agent connection closed unexpectedly'));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Parse an SSH public key line (e.g., "ssh-ed25519 AAAA... comment")
|
|
201
|
+
* into the raw key blob (base64-decoded middle field).
|
|
202
|
+
*/
|
|
203
|
+
function parseSSHPublicKey(pubKeyLine) {
|
|
204
|
+
const parts = pubKeyLine.trim().split(/\s+/);
|
|
205
|
+
if (parts.length < 2) {
|
|
206
|
+
throw new Error('Invalid SSH public key format');
|
|
207
|
+
}
|
|
208
|
+
return Buffer.from(parts[1], 'base64');
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Parse an SSH signature blob to extract the raw signature bytes.
|
|
212
|
+
*
|
|
213
|
+
* Wire format:
|
|
214
|
+
* string format (e.g., "ssh-ed25519")
|
|
215
|
+
* string signature
|
|
216
|
+
*/
|
|
217
|
+
function parseSSHSignature(blob) {
|
|
218
|
+
let offset = 0;
|
|
219
|
+
// Skip format string
|
|
220
|
+
const formatLen = blob.readUInt32BE(offset);
|
|
221
|
+
offset += 4 + formatLen;
|
|
222
|
+
// Read signature string
|
|
223
|
+
const sigLen = blob.readUInt32BE(offset);
|
|
224
|
+
offset += 4;
|
|
225
|
+
return Buffer.from(blob.subarray(offset, offset + sigLen));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Encode a buffer as an SSH string (uint32 length + bytes).
|
|
229
|
+
*/
|
|
230
|
+
function sshString(buf) {
|
|
231
|
+
return Buffer.concat([uint32(buf.length), buf]);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Encode a number as a big-endian uint32.
|
|
235
|
+
*/
|
|
236
|
+
function uint32(n) {
|
|
237
|
+
const buf = Buffer.alloc(4);
|
|
238
|
+
buf.writeUInt32BE(n);
|
|
239
|
+
return buf;
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=ssh-signer.js.map
|