@glance-apps/sync 1.6.0 → 1.8.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/types/index.d.ts CHANGED
@@ -27,7 +27,12 @@ export type SyncErrorCode =
27
27
  | 'VERIFIER_UNSUPPORTED'
28
28
  // DB transport: a row-scoped call (incl. the key verifier) was made before the
29
29
  // accountId was populated — retryable once the account id is available.
30
- | 'ACCOUNT_ID_REQUIRED';
30
+ | 'ACCOUNT_ID_REQUIRED'
31
+ // DB transport, per-account auth: the server rejected this device's
32
+ // credential (401 "invalid credential"). Surfaced with isHardStop === true;
33
+ // the engine persists a halt and stops retrying. Recovery (re-enrollment)
34
+ // is Phase 2.2 — nothing in this version clears the halt.
35
+ | 'CREDENTIAL_INVALID';
31
36
 
32
37
  export type SyncStatus = 'idle' | 'uploading' | 'downloading' | 'success' | 'error';
33
38
 
@@ -266,6 +271,14 @@ export interface CloudSyncProvider {
266
271
  export function webdavFetch(config: SyncEngineConfig): WebdavFetch;
267
272
  export function createProviders(config: SyncEngineConfig): Record<string, CloudSyncProvider>;
268
273
 
274
+ /**
275
+ * Normalizes a raw ETag header value for use in If-Match: strips a weak-
276
+ * validator prefix (`W/"abc"` -> `"abc"`) and the content-coding suffixes
277
+ * Apache mod_deflate/mod_brotli append inside the quotes (`"abc-gzip"` /
278
+ * `"abc-br"` -> `"abc"`). Quotes are preserved; null/undefined pass through.
279
+ */
280
+ export function normalizeEtag(raw: string | null | undefined): string | null | undefined;
281
+
269
282
  // ---------------------------------------------------------------------------
270
283
  // Sync engine
271
284
  // ---------------------------------------------------------------------------
@@ -413,10 +426,53 @@ export interface VaultClient {
413
426
 
414
427
  export function createVaultClient(config: {
415
428
  vaultUrl: string;
429
+ /** Shared device token, or the device's enrolled credential in per-account mode. */
416
430
  vaultToken: string;
417
431
  fetchImpl?: typeof fetch;
418
432
  }): VaultClient;
419
433
 
434
+ // Per-account auth (vault Phase 1.4b).
435
+
436
+ /** Which trust model a GLANCEvault server runs. Discoverable via fetchVaultHealth. */
437
+ export type VaultAuthMode = 'shared' | 'per-account';
438
+
439
+ export interface VaultHealth {
440
+ status: string;
441
+ version: string;
442
+ schemaVersion: number;
443
+ /** Normalized to 'shared' when the server predates the field. */
444
+ authMode: VaultAuthMode;
445
+ }
446
+
447
+ /** Unauthenticated GET /healthz. Safe to call before any token exists. */
448
+ export function fetchVaultHealth(config: {
449
+ vaultUrl: string;
450
+ fetchImpl?: typeof fetch;
451
+ }): Promise<VaultHealth>;
452
+
453
+ export interface VaultEnrollment {
454
+ credentialId: string;
455
+ /** Returned once, never retrievable again. Persist it, then discard the bootstrap secret. */
456
+ credential: string;
457
+ accountId: string;
458
+ deviceId: string;
459
+ createdAt: string;
460
+ }
461
+
462
+ /**
463
+ * POST /enroll — exchanges the bootstrap secret for this device's own credential.
464
+ * Rejects with err.code 'ENROLLMENT_REJECTED' (401, secret not accepted),
465
+ * 'ENROLLMENT_UNSUPPORTED' (404, server runs shared mode or predates enrollment),
466
+ * or 'VAULT_ERROR' with err.status for other failures.
467
+ */
468
+ export function enrollVaultDevice(config: {
469
+ vaultUrl: string;
470
+ enrollmentSecret: string;
471
+ accountId: string;
472
+ deviceId: string;
473
+ fetchImpl?: typeof fetch;
474
+ }): Promise<VaultEnrollment>;
475
+
420
476
  export interface DbSyncEngineConfig {
421
477
  // Identity / local state
422
478
  storageKeyPrefix: string;
@@ -502,7 +558,74 @@ export interface DbSyncEngine {
502
558
  isKeyVerified(): boolean;
503
559
  hasEncryptionReady(): boolean;
504
560
 
561
+ /** True while the persisted credential-rejected stop is set (CREDENTIAL_INVALID). */
562
+ isCredentialHalted(): boolean;
563
+ /** The persisted stop record, or null. Cleared only by recoverVaultSyncEngine after a verified successful re-enrollment. */
564
+ getCredentialHalt(): { message: string; at: string } | null;
565
+ /** True once this instance proved its bearer was superseded by recovery and went inert (in-memory only). */
566
+ isSuperseded(): boolean;
567
+ /** The resolved stable device identity this engine runs as. */
568
+ deviceId: string;
569
+
505
570
  vault: VaultClient;
506
571
  }
507
572
 
508
573
  export function createDbSyncEngine(config: DbSyncEngineConfig): DbSyncEngine;
574
+
575
+ /**
576
+ * Package-owned stable device identifier: generated once (crypto.randomUUID)
577
+ * and persisted under `{prefix}-device-id`. Used for both the device cursor
578
+ * and per-account enrollment. An explicit config.deviceId overrides it.
579
+ */
580
+ export function getOrCreateDeviceId(storageKeyPrefix: string): string;
581
+
582
+ /**
583
+ * The packaged vault connect flow: discovers the server's auth mode, resolves
584
+ * the Bearer value (enrolling with the bootstrap secret when the server is
585
+ * per-account and no credential is stored yet), persists the credential under
586
+ * `{prefix}-vault-credential`, and returns a ready engine. The bootstrap
587
+ * secret is never written to storage, never logged, and never retained past
588
+ * the call. No path re-enrolls automatically once a credential is stored.
589
+ *
590
+ * Typed failures: VAULT_TOKEN_REQUIRED, ENROLLMENT_SECRET_REQUIRED,
591
+ * CREDENTIAL_PERSIST_FAILED, VAULT_UNREACHABLE, plus enrollVaultDevice's
592
+ * ENROLLMENT_REJECTED / ENROLLMENT_UNSUPPORTED.
593
+ */
594
+ export function connectVaultSyncEngine(
595
+ config: Omit<DbSyncEngineConfig, 'vaultToken'> & {
596
+ vaultToken?: string;
597
+ enrollmentSecret?: string;
598
+ }
599
+ ): Promise<{
600
+ engine: DbSyncEngine;
601
+ /** null when discovery failed and the flow fell back to the last known auth state. */
602
+ authMode: VaultAuthMode | null;
603
+ /** True only when this call minted a credential. */
604
+ enrolled: boolean;
605
+ deviceId: string;
606
+ }>;
607
+
608
+ /**
609
+ * The exit from the credential halt (Phase 2.2): USER-INITIATED re-enrollment
610
+ * with the bootstrap secret. Refuses unless the device is halted (NOT_HALTED)
611
+ * and unless the server runs per-account auth (RECOVERY_UNSUPPORTED /
612
+ * VAULT_UNREACHABLE — recovery never falls back on discovery failure). The
613
+ * stale record's deviceId is used for enrollment so the server (2.1) revokes
614
+ * every still-active predecessor; an explicit config.deviceId differing from
615
+ * it is DEVICE_ID_CONFLICT. Order: canary -> enroll -> persist + verify
616
+ * (overwriting the stale record) -> clear the halt LAST -> fresh engine.
617
+ * Every failure leaves the device halted with its state intact. The secret is
618
+ * confined to the call, exactly as in connectVaultSyncEngine. Call only from
619
+ * a deliberate user action; no code path in this package invokes it.
620
+ */
621
+ export function recoverVaultSyncEngine(
622
+ config: Omit<DbSyncEngineConfig, 'vaultToken'> & {
623
+ vaultToken?: string;
624
+ enrollmentSecret: string;
625
+ }
626
+ ): Promise<{
627
+ engine: DbSyncEngine;
628
+ authMode: 'per-account';
629
+ enrolled: true;
630
+ deviceId: string;
631
+ }>;