selfstore 1.8.5 → 1.8.7

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.
@@ -47,7 +47,44 @@ interface FlowHost {
47
47
  type StoreLike = FlowHost | {
48
48
  flowHost: FlowHost;
49
49
  };
50
+ /**
51
+ * A destination this device has already used, offered as one gesture back to
52
+ * it rather than as the question it answered the first time.
53
+ *
54
+ * The screen that asks "where should we save your data?" is right exactly
55
+ * once. Every time after that - a device whose destination was detached, a
56
+ * browser whose storage was cleared - the app usually still knows the answer,
57
+ * and asking again makes the returning user re-derive a decision they already
58
+ * made. This offer carries that memory: it goes first, above a separator, and
59
+ * says WHICH backup it will reopen.
60
+ *
61
+ * The host supplies the connector, because only the host knows how to reach
62
+ * the remembered destination (adopting a known file id, not searching by
63
+ * name). The flow supplies what follows: no conflict step, and no chooser.
64
+ */
65
+ interface ResumeOffer {
66
+ /** Which destination it is. Picks the icon and the wording, nothing else. */
67
+ kind: ConnectKind;
68
+ /**
69
+ * What exactly is being reopened - the account, the file name. Rendered as
70
+ * the card's second line: "resume my backup" only answers the user's
71
+ * question when it can name the backup.
72
+ */
73
+ detail?: string;
74
+ /** Reopen it. Same contract as any connector: null when the user cancels. */
75
+ connect: Connector;
76
+ }
50
77
  interface ConnectFlowOptions {
78
+ /**
79
+ * The destination this device already used, offered ahead of the others.
80
+ * Absent (the default) means every visit is a first visit.
81
+ *
82
+ * Taking this offer never raises the conflict step, whatever
83
+ * `hasLocalData` says: reopening one's own backup is not a merge question,
84
+ * it IS the answer to it. The question still stands for a destination the
85
+ * user newly points at.
86
+ */
87
+ resume?: ResumeOffer;
51
88
  /** Declare when this device holds data worth a question: only then does an
52
89
  * existing backup raise the 'conflict' step with the destructive choices.
53
90
  * Default: never - the flow applies defaultResolution. */
@@ -126,6 +163,10 @@ interface ConnectFlow extends FlowStore<ConnectSnapshot> {
126
163
  * with the object form, `variant` picks the gesture: 'create' (default,
127
164
  * save picker) or 'open' (adopt an existing backup file). */
128
165
  choose(kind: ConnectKind, variant?: 'create' | 'open'): void;
166
+ /** Reopen the remembered destination (`options.resume`). Same gesture rule
167
+ * as choose - call it inside the click. No-op when nothing is remembered.
168
+ * Never raises the conflict step: this backup is already this device's. */
169
+ resume(): void;
129
170
  /** 'form' step (webdav: true): submit the server config. */
130
171
  submitWebdav(config: WebdavConfig): void;
131
172
  /** 'form' step (s3: true): submit the bucket config. */
@@ -323,4 +364,4 @@ declare function joinFlow(link: string, engine: JoinEngine, options?: {
323
364
  deadlineMs?: number;
324
365
  }): JoinFlow;
325
366
 
326
- export { replicaFlow as A, shareFlow as B, type ConnectTargets as C, type FlowHost as F, type JoinEngine as J, type ReplicaFlow as R, type StoreLike as S, type ConnectFlowOptions as a, type ConnectKind as b, type ConnectFlow as c, type ShareEngine as d, type ShareLevel as e, type ShareFlow as f, type JoinFlow as g, type ConnectFlowOutcome as h, type ConnectResolution as i, type ConnectSnapshot as j, type ConnectStep as k, type Connector as l, type JoinOutcome as m, type JoinPreview as n, type JoinSnapshot as o, type JoinStep as p, REPLICA_ID as q, type ReplicaFlowOptions as r, type ReplicaSnapshot as s, type ReplicaStep as t, type ShareBusy as u, type ShareLinkInfo as v, type ShareMemberInfo as w, type ShareSnapshot as x, connectFlow as y, joinFlow as z };
367
+ export { joinFlow as A, replicaFlow as B, type ConnectTargets as C, shareFlow as D, type FlowHost as F, type JoinEngine as J, type ReplicaFlow as R, type StoreLike as S, type ConnectFlowOptions as a, type ConnectKind as b, type ConnectFlow as c, type ShareEngine as d, type ShareLevel as e, type ShareFlow as f, type JoinFlow as g, type ConnectFlowOutcome as h, type ConnectResolution as i, type ConnectSnapshot as j, type ConnectStep as k, type Connector as l, type JoinOutcome as m, type JoinPreview as n, type JoinSnapshot as o, type JoinStep as p, REPLICA_ID as q, type ReplicaFlowOptions as r, type ReplicaSnapshot as s, type ReplicaStep as t, type ResumeOffer as u, type ShareBusy as v, type ShareLinkInfo as w, type ShareMemberInfo as x, type ShareSnapshot as y, connectFlow as z };
package/dist/flows.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { c as ConnectFlow, a as ConnectFlowOptions, h as ConnectFlowOutcome, b as ConnectKind, i as ConnectResolution, j as ConnectSnapshot, k as ConnectStep, C as ConnectTargets, l as Connector, F as FlowHost, J as JoinEngine, g as JoinFlow, m as JoinOutcome, n as JoinPreview, o as JoinSnapshot, p as JoinStep, q as REPLICA_ID, R as ReplicaFlow, r as ReplicaFlowOptions, s as ReplicaSnapshot, t as ReplicaStep, u as ShareBusy, d as ShareEngine, f as ShareFlow, e as ShareLevel, v as ShareLinkInfo, w as ShareMemberInfo, x as ShareSnapshot, y as connectFlow, z as joinFlow, A as replicaFlow, B as shareFlow } from './flows-1EntUZ91.js';
1
+ export { c as ConnectFlow, a as ConnectFlowOptions, h as ConnectFlowOutcome, b as ConnectKind, i as ConnectResolution, j as ConnectSnapshot, k as ConnectStep, C as ConnectTargets, l as Connector, F as FlowHost, J as JoinEngine, g as JoinFlow, m as JoinOutcome, n as JoinPreview, o as JoinSnapshot, p as JoinStep, q as REPLICA_ID, R as ReplicaFlow, r as ReplicaFlowOptions, s as ReplicaSnapshot, t as ReplicaStep, u as ResumeOffer, v as ShareBusy, d as ShareEngine, f as ShareFlow, e as ShareLevel, w as ShareLinkInfo, x as ShareMemberInfo, y as ShareSnapshot, z as connectFlow, A as joinFlow, B as replicaFlow, D as shareFlow } from './flows-vX9zy9bI.js';
2
2
  export { F as FlowStore, w as withDeadline } from './machine-ALMkT3mk.js';
3
3
  import './store-BiQ-BuVl.js';
4
4
  import './cache-ilsK3Tki.js';
package/dist/flows.js CHANGED
@@ -1,4 +1,4 @@
1
- export { connectFlow, joinFlow, shareFlow } from './chunk-PNX7AJYO.js';
1
+ export { connectFlow, joinFlow, shareFlow } from './chunk-LKIRWVXM.js';
2
2
  import { connect as connect$1, connect3, connect2, fromSession as fromSession$1, fromSession2, fromSession3 } from './chunk-TKWU52LK.js';
3
3
  import { makeMachine, toStoreError } from './chunk-W22V5CRM.js';
4
4
  export { withDeadline } from './chunk-W22V5CRM.js';
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { L as LocalCache, K as KV } from './cache-ilsK3Tki.js';
4
4
  import { SyncConfig } from './sync.js';
5
5
  import { i as DriveAuth } from './drive-CTYvGg_7.js';
6
6
  import { W as WebdavConfig, S as S3Config } from './s3-DvsnA-FK.js';
7
- export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-C52j3DeH.js';
7
+ export { f as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-VLHtF3vT.js';
8
8
 
9
9
  declare const BACKUP_EXTENSION = ".zip";
10
10
  declare const BACKUP_MIME = "application/zip";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-ELDSPUTS.js';
2
- export { gisDriveAuth } from './chunk-ELDSPUTS.js';
1
+ import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-TXAMWBG5.js';
2
+ export { gisDriveAuth } from './chunk-TXAMWBG5.js';
3
3
  import './chunk-SKWHKM25.js';
4
4
  import { saveToDisk } from './chunk-DCEXWSM6.js';
5
5
  export { pickFromDisk, saveToDisk } from './chunk-DCEXWSM6.js';
@@ -21,19 +21,54 @@ declare function changePassword(input: Blob | Uint8Array, opts: {
21
21
 
22
22
  /**
23
23
  * A client-only DriveAuth: the Google Identity Services token flow, no backend.
24
- * The access token is cached in sessionStorage so a reload reuses it, and a
25
- * silent re-acquisition is attempted when it lapses; if that fails (the Google
26
- * session expired, or third-party cookies are blocked) the Drive target reports
27
- * needs-attention and the user reconnects in one click. For a connection that
28
- * never re-prompts, back the Drive target with a server that holds a refresh
29
- * token instead (a broker implementing the same DriveAuth interface).
24
+ *
25
+ * What this flow can and cannot do, because it decides how often a user is
26
+ * interrupted. Google's browser token flow issues an access token that lives
27
+ * about an hour and NO refresh token - a refresh token requires a client secret,
28
+ * which requires a server. And `requestAccessToken` always opens a popup, even
29
+ * with `prompt: ''`: the popup closes by itself when the grant is still on file,
30
+ * but opening it at all needs a user gesture. So a purely client-side app cannot
31
+ * silently re-acquire a token on page load. What it CAN do:
32
+ *
33
+ * - reuse a token it already has (`persist: 'session'`), so a reload inside the
34
+ * hour asks for nothing at all;
35
+ * - name the account (`hint`), so the popup does not ask which one;
36
+ * - re-grant without the consent screen (`prompt: ''` before `'consent'`) -
37
+ * which is also what stops Google mailing "you granted access" every time.
38
+ *
39
+ * For a connection that never re-prompts, back the Drive target with a server
40
+ * that holds a refresh token instead (a broker implementing this same DriveAuth).
30
41
  */
31
42
 
32
- /** Build a client-only DriveAuth for the given OAuth client id (no backend). */
33
- declare function gisDriveAuth(opts: {
43
+ /** Where the access token survives between requests. */
44
+ type GisTokenPersistence = 'memory' | 'session';
45
+ interface GisDriveAuthOptions {
34
46
  clientId: string;
35
47
  scope?: string;
36
- }): DriveAuth;
48
+ /**
49
+ * The account to re-grant for, so the popup does not ask which one. Pass the
50
+ * address the app remembers (`driveTarget.account()` answers it once
51
+ * connected); a function is read at every request, for a host that only
52
+ * learns it after the first connect.
53
+ */
54
+ hint?: string | (() => string | null | undefined);
55
+ /**
56
+ * Where the access token lives between requests.
57
+ *
58
+ * `'memory'` (the default) keeps it in a closure and nowhere else: a reload
59
+ * has to re-acquire it, which needs a click, but no Drive-scoped credential is
60
+ * ever written to web storage.
61
+ *
62
+ * `'session'` also keeps it in `sessionStorage`, so a reload reuses the token
63
+ * it already has and asks for nothing. That token is a bearer credential for
64
+ * the `drive.file` scope, readable by any script on the origin; it dies with
65
+ * the tab and within the hour. Opt in for an app whose users reload often;
66
+ * leave it alone where an XSS on the origin must find nothing to lift.
67
+ */
68
+ persist?: GisTokenPersistence;
69
+ }
70
+ /** Build a client-only DriveAuth for the given OAuth client id (no backend). */
71
+ declare function gisDriveAuth(opts: GisDriveAuthOptions): DriveAuth;
37
72
 
38
73
  /**
39
74
  * Save a backup blob to disk via the File System Access API, else a download.
@@ -62,4 +97,4 @@ declare function datedName(filename: string, when?: Date): string;
62
97
  /** Prompt the user to pick a backup file from disk. Resolves null if cancelled. */
63
98
  declare function pickFromDisk(): Promise<File | null>;
64
99
 
65
- export { inspect as a, isEncrypted as b, changePassword as c, datedName as d, exportSnapshot as e, gisDriveAuth as g, importSnapshot as i, pickFromDisk as p, saveToDisk as s };
100
+ export { type GisDriveAuthOptions as G, type GisTokenPersistence as a, inspect as b, isEncrypted as c, datedName as d, exportSnapshot as e, changePassword as f, gisDriveAuth as g, importSnapshot as i, pickFromDisk as p, saveToDisk as s };
package/dist/widgets.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as FlowHost, S as StoreLike, C as ConnectTargets, a as ConnectFlowOptions, b as ConnectKind, c as ConnectFlow, d as ShareEngine, e as ShareLevel, f as ShareFlow, J as JoinEngine, g as JoinFlow, R as ReplicaFlow } from './flows-1EntUZ91.js';
1
+ import { F as FlowHost, S as StoreLike, C as ConnectTargets, a as ConnectFlowOptions, b as ConnectKind, c as ConnectFlow, d as ShareEngine, e as ShareLevel, f as ShareFlow, J as JoinEngine, g as JoinFlow, R as ReplicaFlow } from './flows-vX9zy9bI.js';
2
2
  import { f as BackupsManager } from './manager-DZ1c_aSS.js';
3
3
  import './store-BiQ-BuVl.js';
4
4
  import './cache-ilsK3Tki.js';
@@ -127,6 +127,15 @@ declare class SelfstoreConnectElement extends FlowWidget {
127
127
  attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
128
128
  connectedCallback(): void;
129
129
  private wire;
130
+ /**
131
+ * The destination choice: the way back first, then a rule, then the ways
132
+ * forward.
133
+ *
134
+ * Its own method because `view` covers every step of the journey and was at
135
+ * the complexity cap; this is the step that keeps growing, since it is the
136
+ * one the host shapes.
137
+ */
138
+ private viewChoose;
130
139
  protected view(into: HTMLElement): void;
131
140
  /**
132
141
  * A form field that keeps its name once you start typing.
@@ -137,6 +146,15 @@ declare class SelfstoreConnectElement extends FlowWidget {
137
146
  * never a label.
138
147
  */
139
148
  private labelled;
149
+ /**
150
+ * The destination this device already used, first and on its own.
151
+ *
152
+ * It carries the same shape as an ordinary card so nothing new has to be
153
+ * styled, and the host's icon for that kind, so it reads as the same place
154
+ * rather than a fourth option. Its second line names WHICH backup - without
155
+ * that, "resume my backup" is one more thing to wonder about.
156
+ */
157
+ private resumeCard;
140
158
  private destCard;
141
159
  /** The grouped "your own server" card shown when both WebDAV and S3 are
142
160
  * offered: one entry into the tabbed form, opening on the first server kind. */
package/dist/widgets.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { saveToDisk, datedName } from './chunk-DCEXWSM6.js';
2
- import { connectFlow, shareFlow, joinFlow } from './chunk-PNX7AJYO.js';
2
+ import { connectFlow, shareFlow, joinFlow } from './chunk-LKIRWVXM.js';
3
3
  import { isOpenSupported } from './chunk-TKWU52LK.js';
4
4
  import './chunk-W22V5CRM.js';
5
5
  import './chunk-J5MFXJU6.js';
@@ -81,6 +81,24 @@ button[part~='dot-button'] {
81
81
  }
82
82
  button[part~='card'] { cursor: pointer; font: inherit; color: inherit; }
83
83
  button[part~='card']:hover { border-color: var(--_accent); }
84
+ /* The rule between "go back to what you had" and "start something new". A word
85
+ sits in the gap rather than beside it, so the two groups read as two even
86
+ before either is read. */
87
+ [part~='separator'] {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.7rem;
91
+ margin: 0.15rem 0;
92
+ color: var(--_ink-dim);
93
+ font-size: 0.85em;
94
+ }
95
+ [part~='separator']::before,
96
+ [part~='separator']::after {
97
+ content: '';
98
+ flex: 1;
99
+ height: 1px;
100
+ background: var(--_border);
101
+ }
84
102
  img[part~='icon'] {
85
103
  width: var(--selfstore-icon-size, 2.1em);
86
104
  height: var(--selfstore-icon-size, 2.1em);
@@ -487,6 +505,10 @@ var FlowWidget = class extends HTMLElement {
487
505
  var EN = {
488
506
  "connect.title": "Where should we save your data?",
489
507
  "connect.recommended": "Recommended",
508
+ // The way back, and the rule under it. A host that can name the destination
509
+ // overrides this with something less abstract ("Resume my Google Drive").
510
+ "connect.resume": "Resume my backup",
511
+ "connect.or": "or",
490
512
  "connect.drive": "Google Drive",
491
513
  "connect.drive.sub": "Available on all your devices",
492
514
  "connect.file": "A file on this device",
@@ -548,6 +570,8 @@ var EN = {
548
570
  var FR = {
549
571
  "connect.title": "O\xF9 enregistrer vos donn\xE9es ?",
550
572
  "connect.recommended": "Recommand\xE9",
573
+ "connect.resume": "Reprendre ma sauvegarde",
574
+ "connect.or": "ou",
551
575
  "connect.drive": "Google Drive",
552
576
  "connect.drive.sub": "Disponible sur tous vos appareils",
553
577
  "connect.file": "Un fichier sur cet appareil",
@@ -755,39 +779,54 @@ var SelfstoreConnectElement = class extends FlowWidget {
755
779
  this.rerender();
756
780
  });
757
781
  }
782
+ /**
783
+ * The destination choice: the way back first, then a rule, then the ways
784
+ * forward.
785
+ *
786
+ * Its own method because `view` covers every step of the journey and was at
787
+ * the complexity cap; this is the step that keeps growing, since it is the
788
+ * one the host shapes.
789
+ */
790
+ viewChoose(into, s) {
791
+ put(into, this.heading("title", "connect.title"));
792
+ const offer = this.#options.resume;
793
+ if (offer) {
794
+ into.append(this.resumeCard(offer));
795
+ into.append(h("div", { part: "separator" }, h("span", {}, this.t("connect.or"))));
796
+ }
797
+ const serverKinds = ["webdav", "s3"].filter(
798
+ (k) => s.kinds.includes(k) && !this.#advanced.includes(k)
799
+ );
800
+ const groupServer = serverKinds.length >= 2;
801
+ for (const kind of s.kinds) {
802
+ if (this.#advanced.includes(kind)) continue;
803
+ if (groupServer && (kind === "webdav" || kind === "s3")) {
804
+ if (kind === serverKinds[0]) into.append(this.serverCard(serverKinds[0]));
805
+ continue;
806
+ }
807
+ into.append(this.destCard(kind));
808
+ }
809
+ for (const kind of s.kinds) {
810
+ if (!this.#advanced.includes(kind)) continue;
811
+ into.append(
812
+ h(
813
+ "button",
814
+ {
815
+ part: "link advanced-link",
816
+ "data-kind": kind,
817
+ onclick: () => this.#flow?.choose(kind)
818
+ },
819
+ this.t(`connect.${kind}`)
820
+ )
821
+ );
822
+ }
823
+ }
758
824
  view(into) {
759
825
  const flow = this.#flow;
760
826
  if (!flow) return;
761
827
  const s = flow.snapshot;
762
828
  if (s.step === "choose") {
763
- put(into, this.heading("title", "connect.title"));
764
- const serverKinds = ["webdav", "s3"].filter(
765
- (k) => s.kinds.includes(k) && !this.#advanced.includes(k)
766
- );
767
- const groupServer = serverKinds.length >= 2;
768
- for (const kind of s.kinds) {
769
- if (this.#advanced.includes(kind)) continue;
770
- if (groupServer && (kind === "webdav" || kind === "s3")) {
771
- if (kind === serverKinds[0]) into.append(this.serverCard(serverKinds[0]));
772
- continue;
773
- }
774
- into.append(this.destCard(kind));
775
- }
776
- for (const kind of s.kinds) {
777
- if (this.#advanced.includes(kind)) {
778
- into.append(
779
- h(
780
- "button",
781
- {
782
- part: "link advanced-link",
783
- "data-kind": kind,
784
- onclick: () => this.#flow?.choose(kind)
785
- },
786
- this.t(`connect.${kind}`)
787
- )
788
- );
789
- }
790
- }
829
+ this.viewChoose(into, s);
791
830
  return;
792
831
  }
793
832
  if (s.step === "form") {
@@ -962,6 +1001,30 @@ var SelfstoreConnectElement = class extends FlowWidget {
962
1001
  labelled(labelKey, input) {
963
1002
  return h("label", { part: "labelled" }, h("span", { part: "label" }, this.t(labelKey)), input);
964
1003
  }
1004
+ /**
1005
+ * The destination this device already used, first and on its own.
1006
+ *
1007
+ * It carries the same shape as an ordinary card so nothing new has to be
1008
+ * styled, and the host's icon for that kind, so it reads as the same place
1009
+ * rather than a fourth option. Its second line names WHICH backup - without
1010
+ * that, "resume my backup" is one more thing to wonder about.
1011
+ */
1012
+ resumeCard(offer) {
1013
+ const icon = this.#icons[offer.kind] ?? defaultIcons[offer.kind];
1014
+ const head = icon ? h("img", { part: "icon", src: icon, alt: "" }) : null;
1015
+ const title = h("div", { part: "title" }, this.t("connect.resume"));
1016
+ const sub = offer.detail ? h("div", { part: "sub" }, offer.detail) : null;
1017
+ return h(
1018
+ "button",
1019
+ {
1020
+ part: "card resume-card",
1021
+ "data-kind": offer.kind,
1022
+ onclick: () => this.#flow?.resume()
1023
+ },
1024
+ head,
1025
+ h("div", {}, title, sub)
1026
+ );
1027
+ }
965
1028
  destCard(kind) {
966
1029
  const icon = this.#icons[kind] ?? defaultIcons[kind];
967
1030
  const head = icon ? h("img", { part: "icon", src: icon, alt: "" }) : null;