@mdbase-dev/sdk 0.1.0-beta.169 → 0.1.0-beta.173

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 (148) hide show
  1. package/README.md +91 -3
  2. package/dist/app-host/account-key-host.d.ts +128 -0
  3. package/dist/app-host/account-key-host.js +449 -0
  4. package/dist/app-host/account-key-host.js.map +1 -0
  5. package/dist/app-host/app-environment.d.ts +3 -4
  6. package/dist/app-host/app-environment.js +6 -6
  7. package/dist/app-host/app-environment.js.map +1 -1
  8. package/dist/app-host/app-origin.d.ts +5 -2
  9. package/dist/app-host/app-origin.js +10 -6
  10. package/dist/app-host/app-origin.js.map +1 -1
  11. package/dist/app-host/cloud-copy-bootstrap.js +2 -2
  12. package/dist/app-host/cloud-copy-bootstrap.js.map +1 -1
  13. package/dist/app-host/cloud-copy-host.d.ts +69 -5
  14. package/dist/app-host/cloud-copy-host.js +289 -20
  15. package/dist/app-host/cloud-copy-host.js.map +1 -1
  16. package/dist/app-host/cloud-copy-persistence-idb.d.ts +0 -2
  17. package/dist/app-host/cloud-copy-persistence-idb.js +2 -2
  18. package/dist/app-host/cloud-copy-persistence-idb.js.map +1 -1
  19. package/dist/app-host/cp-authority.js +2 -2
  20. package/dist/app-host/cp-authority.js.map +1 -1
  21. package/dist/app-host/cp-people.js +2 -2
  22. package/dist/app-host/cp-people.js.map +1 -1
  23. package/dist/app-host/device-key-idb.d.ts +0 -2
  24. package/dist/app-host/device-key-idb.js +1 -1
  25. package/dist/app-host/device-key-idb.js.map +1 -1
  26. package/dist/app-host/device-key-provider.d.ts +0 -3
  27. package/dist/app-host/device-key-provider.js +1 -1
  28. package/dist/app-host/device-key-provider.js.map +1 -1
  29. package/dist/app-host/device-registration.js +2 -2
  30. package/dist/app-host/device-registration.js.map +1 -1
  31. package/dist/app-host/http-log.js +2 -2
  32. package/dist/app-host/http-log.js.map +1 -1
  33. package/dist/app-host/index.d.ts +4 -2
  34. package/dist/app-host/index.js +2 -1
  35. package/dist/app-host/index.js.map +1 -1
  36. package/dist/app-host/installation-sign-in.d.ts +16 -1
  37. package/dist/app-host/installation-sign-in.js +134 -27
  38. package/dist/app-host/installation-sign-in.js.map +1 -1
  39. package/dist/app-host/installation-store.d.ts +0 -2
  40. package/dist/app-host/installation-store.js +4 -4
  41. package/dist/app-host/installation-store.js.map +1 -1
  42. package/dist/app-host/log-push.js +2 -2
  43. package/dist/app-host/log-push.js.map +1 -1
  44. package/dist/app-host/noise-idb.d.ts +0 -3
  45. package/dist/app-host/noise-idb.js +1 -1
  46. package/dist/app-host/noise-idb.js.map +1 -1
  47. package/dist/app-host/private-bootstrap.js +2 -2
  48. package/dist/app-host/private-bootstrap.js.map +1 -1
  49. package/dist/app-host/private-persistence-idb.d.ts +0 -2
  50. package/dist/app-host/private-persistence-idb.js +1 -1
  51. package/dist/app-host/private-persistence-idb.js.map +1 -1
  52. package/dist/app-host/private-persistence.js +2 -2
  53. package/dist/app-host/private-persistence.js.map +1 -1
  54. package/dist/app-host/scoped-sign-in-session.d.ts +4 -0
  55. package/dist/app-host/scoped-sign-in-session.js +9 -1
  56. package/dist/app-host/scoped-sign-in-session.js.map +1 -1
  57. package/dist/app-host/unsynced-collection.js +8 -3
  58. package/dist/app-host/unsynced-collection.js.map +1 -1
  59. package/dist/app-host/wasm-runtime.d.ts +14 -0
  60. package/dist/app-host/wasm-runtime.js +45 -0
  61. package/dist/app-host/wasm-runtime.js.map +1 -1
  62. package/dist/app-host/web-collection.d.ts +4 -1
  63. package/dist/app-host/web-collection.js +214 -125
  64. package/dist/app-host/web-collection.js.map +1 -1
  65. package/dist/app-host/web-sign-in-problem.js +6 -1
  66. package/dist/app-host/web-sign-in-problem.js.map +1 -1
  67. package/dist/app-host/web-sign-in-session.d.ts +46 -4
  68. package/dist/app-host/web-sign-in-session.js +88 -15
  69. package/dist/app-host/web-sign-in-session.js.map +1 -1
  70. package/dist/application/asset-loader.d.ts +0 -1
  71. package/dist/application/asset-loader.js +2 -2
  72. package/dist/application/asset-loader.js.map +1 -1
  73. package/dist/application/asset-pins.json +4 -4
  74. package/dist/application/browser-selection.js +1 -1
  75. package/dist/application/browser-selection.js.map +1 -1
  76. package/dist/application/collection-selection.js +11 -6
  77. package/dist/application/collection-selection.js.map +1 -1
  78. package/dist/application/declaration.js +2 -2
  79. package/dist/application/declaration.js.map +1 -1
  80. package/dist/application/entry.d.ts +19 -0
  81. package/dist/application/entry.js +81 -38
  82. package/dist/application/entry.js.map +1 -1
  83. package/dist/application/front-door.d.ts +6 -1
  84. package/dist/application/front-door.js +5 -0
  85. package/dist/application/front-door.js.map +1 -1
  86. package/dist/application/local-build.d.ts +4 -2
  87. package/dist/application/local-build.js +4 -2
  88. package/dist/application/local-build.js.map +1 -1
  89. package/dist/application/manifest.d.ts +1 -0
  90. package/dist/application/manifest.js +8 -3
  91. package/dist/application/manifest.js.map +1 -1
  92. package/dist/application/native-assets.js +3 -4
  93. package/dist/application/native-assets.js.map +1 -1
  94. package/dist/application/packaged-assets.d.ts +1 -1
  95. package/dist/application/packaged-assets.js +6 -3
  96. package/dist/application/packaged-assets.js.map +1 -1
  97. package/dist/application/phase.d.ts +18 -1
  98. package/dist/application/phase.js +9 -1
  99. package/dist/application/phase.js.map +1 -1
  100. package/dist/application/portal-reservation.js +2 -2
  101. package/dist/application/portal-reservation.js.map +1 -1
  102. package/dist/application/registration.d.ts +3 -0
  103. package/dist/application/registration.js +7 -4
  104. package/dist/application/registration.js.map +1 -1
  105. package/dist/application/return-url.d.ts +1 -1
  106. package/dist/application/return-url.js +6 -7
  107. package/dist/application/return-url.js.map +1 -1
  108. package/dist/application/runtime.d.ts +7 -1
  109. package/dist/application/runtime.js +53 -4
  110. package/dist/application/runtime.js.map +1 -1
  111. package/dist/application/startup-timing.d.ts +20 -0
  112. package/dist/application/startup-timing.js +58 -0
  113. package/dist/application/startup-timing.js.map +1 -0
  114. package/dist/application/ui-connections.js +17 -4
  115. package/dist/application/ui-connections.js.map +1 -1
  116. package/dist/application/worker-client.d.ts +3 -0
  117. package/dist/application/worker-client.js +22 -2
  118. package/dist/application/worker-client.js.map +1 -1
  119. package/dist/application/worker-host.js +51 -3
  120. package/dist/application/worker-host.js.map +1 -1
  121. package/dist/application/worker-protocol.d.ts +7 -2
  122. package/dist/application/worker-protocol.js +10 -1
  123. package/dist/application/worker-protocol.js.map +1 -1
  124. package/dist/application/worker-session.d.ts +1 -1
  125. package/dist/application/worker-session.js +5 -1
  126. package/dist/application/worker-session.js.map +1 -1
  127. package/dist/application/worker.js +8394 -2001
  128. package/dist/application/worker.js.map +1 -1
  129. package/dist/browser/mdbase-sdk.js +8064 -3633
  130. package/dist/browser/mdbase-sdk.min.js +78 -78
  131. package/dist/describe-error.d.ts +2 -2
  132. package/dist/describe-error.js +9 -2
  133. package/dist/describe-error.js.map +1 -1
  134. package/dist/index.d.ts +1 -1
  135. package/dist/local-build.d.ts +7 -4
  136. package/dist/local-build.js +7 -4
  137. package/dist/local-build.js.map +1 -1
  138. package/dist/native/provenance.json +5 -5
  139. package/dist/native/runtime.wasm +0 -0
  140. package/dist/private-account.js +13 -18
  141. package/dist/private-account.js.map +1 -1
  142. package/dist/testing/application.d.ts +3 -1
  143. package/dist/testing/application.js +8 -1
  144. package/dist/testing/application.js.map +1 -1
  145. package/dist/testing/test-app.d.ts +9 -0
  146. package/dist/testing/test-app.js +13 -0
  147. package/dist/testing/test-app.js.map +1 -1
  148. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-host/index.ts"],"names":[],"mappings":"AAAA;8EAC8E;AAC9E,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAG7O,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAE/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAEzE,OAAO,EAAE,mCAAmC,EAAE,MAAM,qBAAqB,CAAC;AAE1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAGjE,OAAO,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEhG,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAOrB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAOrD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAExK,gFAAgF;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAGvH,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEhG,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,IAAI,8BAA8B,EAAE,aAAa,IAAI,gBAAgB,EAAE,iBAAiB,IAAI,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE3N,OAAO,EAAE,kBAAkB,IAAI,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAEhG,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE9F,OAAO,EAAE,8BAA8B,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEvG,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-host/index.ts"],"names":[],"mappings":"AAAA;8EAC8E;AAC9E,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAG7O,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAE/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAEzE,OAAO,EAAE,mCAAmC,EAAE,MAAM,qBAAqB,CAAC;AAE1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,+BAA+B,EAAE,MAAM,gBAAgB,CAAC;AAGjE,OAAO,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEhG,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,4BAA4B,EAAE,kCAAkC,EAAE,MAAM,mBAAmB,CAAC;AAErH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAOrB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAOrD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAExK,gFAAgF;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAGvH,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEhG,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,IAAI,8BAA8B,EAAE,aAAa,IAAI,gBAAgB,EAAE,iBAAiB,IAAI,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE3N,OAAO,EAAE,kBAAkB,IAAI,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAEhG,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE9F,OAAO,EAAE,8BAA8B,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEvG,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE7J,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -57,6 +57,10 @@ export interface AppInstallationSignInOptions extends AppInstallationStoreOption
57
57
  * consent requests) when requestedCreateCollections is omitted. A restored
58
58
  * ledger keeps its stored request; this never rewrites original terms. */
59
59
  readonly freshRequestedCreateCollections?: boolean;
60
+ /** Ask to join existing Private (e2e) collections as a keyed device, for a
61
+ * FRESH installation and its later collection consent requests. A restored
62
+ * ledger keeps its stored request; this never rewrites original terms. */
63
+ readonly freshRequestedPrivateDevice?: boolean;
60
64
  /** Explicit next-app build selection; absent retains the frozen original
61
65
  * environment/origin policy, never selects a new origin implicitly. */
62
66
  readonly environmentSelection?: AppEnvironmentSelection;
@@ -76,6 +80,8 @@ export interface AppInstallationCollectionConsentView {
76
80
  readonly requestedCreateCollections: boolean;
77
81
  readonly addedCollectionIds: readonly string[];
78
82
  readonly approvedCreateCollections: boolean;
83
+ /** Present (true) only when this window asked for Private device access. */
84
+ readonly requestedPrivateDevice?: true;
79
85
  readonly accountEmail?: string | null;
80
86
  readonly accountProvider?: "github";
81
87
  readonly accountLogin?: string;
@@ -94,6 +100,10 @@ export interface AppInstallationCollection {
94
100
  /** SDK-10: `private` for a collection kept on the person's computers and
95
101
  * reached through the relay; absent or `cloud_copy` for a hosted one. */
96
102
  readonly kind?: "cloud_copy" | "private";
103
+ /** `private` for an end-to-end encrypted collection this installation joins
104
+ * as its own keyed device (it needs the encryption password once). Absent
105
+ * means a cloud copy. Distinct from `kind`, which marks a relay entry. */
106
+ readonly mode?: "private";
97
107
  }
98
108
  export type AppFileAction = "list" | "read" | "add" | "replace" | "move" | "delete";
99
109
  /** One at-the-moment-of-use request for optional file actions. Held only in
@@ -139,6 +149,9 @@ export interface AppInstallationSignInView {
139
149
  readonly collectionIds: readonly string[];
140
150
  readonly createCollections: boolean;
141
151
  readonly requestedCreateCollections: boolean;
152
+ /** The approved Private device scopes, a subset of collectionIds. Present
153
+ * only when the installation holds at least one. */
154
+ readonly privateDeviceCollectionIds?: readonly string[];
142
155
  }
143
156
  /** Owns pre-account origin slot and original secret capability, not the account
144
157
  * installation/native lease. User account confirmation + actual installation
@@ -165,6 +178,8 @@ export declare class AppProtectedInstallationSignIn {
165
178
  private constructor();
166
179
  static open(options: AppInstallationSignInOptions): Promise<AppProtectedInstallationSignIn>;
167
180
  private static decode;
181
+ /** The row's mode agrees with the consent recorded for that collection. */
182
+ private modeConsented;
168
183
  private static collectionIds;
169
184
  private static proof;
170
185
  isCurrent(): boolean;
@@ -214,7 +229,7 @@ export declare class AppProtectedInstallationSignIn {
214
229
  /** Explicit additive consent, not renewal/registration/account selection.
215
230
  * A new secret/request is protected BEFORE HTTP; unknown outcomes only permit
216
231
  * an explicit repeat of that exact request, never new keys or a new actor. */
217
- startCollectionConsent(installation: AppInstallationCustodyAuthority, requestedCreateCollections?: boolean, options?: AppInstallationStartOptions): Promise<AppInstallationCollectionConsentView>;
232
+ startCollectionConsent(installation: AppInstallationCustodyAuthority, requestedCreateCollections?: boolean, options?: AppInstallationStartOptions, requestedPrivateDevice?: boolean): Promise<AppInstallationCollectionConsentView>;
218
233
  /** The ORIGINAL consent-request secret consumes approval. Strict response
219
234
  * keys forbid replacement tokens/registrations; original actor/proof remains. */
220
235
  exchangeCollectionConsent(installation: AppInstallationCustodyAuthority): Promise<AppInstallationCollectionConsentView>;
@@ -23,6 +23,19 @@ export class AppInstallationSignInError extends Error {
23
23
  const fail = (reason) => new AppInstallationSignInError(reason);
24
24
  // Preserve definite HTTP status provenance even when its body cannot be parsed.
25
25
  const nonOkResponses = new WeakSet();
26
+ // A Connect server from before Private device access parses START strictly and
27
+ // answers the one optional field it does not know with exactly this refusal
28
+ // (400 invalid_request naming the key). Nothing was stored for that request, so
29
+ // START may be repeated ONCE without the field: the app then asks only for cloud
30
+ // copies, as an older SDK would. Any other refusal stays a refusal.
31
+ const privateDeviceFieldRefused = new WeakSet();
32
+ function unknownPrivateDeviceField(status, error) {
33
+ if (status !== 400 || !error || typeof error !== "object" || Array.isArray(error))
34
+ return false;
35
+ const { code, message } = error;
36
+ return code === "invalid_request" && typeof message === "string" && message.length <= 512 &&
37
+ /unrecognized key/i.test(message) && message.includes("requested_private_device");
38
+ }
26
39
  const hex = (b) => Array.from(b, v => v.toString(16).padStart(2, "0")).join("");
27
40
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
28
41
  function id(v) { if (typeof v !== "string" || !UUID.test(v) || uuidToBytes(v).every(b => b === 0))
@@ -95,6 +108,11 @@ function timerChannel(value) {
95
108
  return Object.freeze({ grantId: id(row.grantId), kind: row.kind, targetDigest: text(row.targetDigest, /^[0-9a-f]{64}$/),
96
109
  channelId: row.channelId === null ? null : id(row.channelId), pending: row.pending });
97
110
  }
111
+ /** True for a non-empty listing in which no row carries `mode`, although it was asked for. */
112
+ function modeUnanswered(value) {
113
+ const rows = value && typeof value === "object" && !Array.isArray(value) ? value.collections : undefined;
114
+ return Array.isArray(rows) && rows.length > 0 && rows.every(row => !row || typeof row !== "object" || !("mode" in row));
115
+ }
98
116
  /** Strictly parsed CP display/scope metadata, never native readiness. */
99
117
  function collectionListing(value) {
100
118
  if (!Array.isArray(value) || value.length > 1000)
@@ -102,13 +120,15 @@ function collectionListing(value) {
102
120
  const seen = new Set();
103
121
  return Object.freeze(value.map(value => {
104
122
  const has = (key) => value && typeof value === "object" && key in value ? `,${key}` : "";
105
- const r = object(value, `collection_id,display_name,role${has("availability")}${has("file_actions")}`), collectionId = id(r.collection_id);
123
+ const r = object(value, `collection_id,display_name,role${has("availability")}${has("file_actions")}${has("mode")}`), collectionId = id(r.collection_id);
124
+ if ("mode" in r && r.mode !== "cloud_copy" && r.mode !== "private")
125
+ throw fail("response");
106
126
  if (seen.has(collectionId) || typeof r.display_name !== "string" || !r.display_name.length || r.display_name.length > 1024 || (r.role !== "owner" && r.role !== "editor" && r.role !== "viewer"))
107
127
  throw fail("response");
108
128
  const availability = parseCollectionAvailability(r.availability);
109
129
  const allowed = "file_actions" in r ? fileActions(r.file_actions) : null;
110
130
  seen.add(collectionId);
111
- return Object.freeze({ collectionId, displayName: r.display_name, role: r.role, ...(availability ? { availability } : {}), ...(allowed ? { fileActions: allowed } : {}) });
131
+ return Object.freeze({ collectionId, displayName: r.display_name, role: r.role, ...(availability ? { availability } : {}), ...(allowed ? { fileActions: allowed } : {}), ...(r.mode === "private" ? { mode: "private" } : {}) });
112
132
  }));
113
133
  }
114
134
  const FILE_ACTIONS = ["list", "read", "add", "replace", "move", "delete"];
@@ -167,28 +187,28 @@ export class AppProtectedInstallationSignIn {
167
187
  throw fail("binding");
168
188
  if (p.freshRequestedCreateCollections !== undefined && typeof p.freshRequestedCreateCollections !== "boolean")
169
189
  throw fail("binding");
190
+ if (p.freshRequestedPrivateDevice !== undefined && typeof p.freshRequestedPrivateDevice !== "boolean")
191
+ throw fail("binding");
170
192
  if (p.recoveryFor !== undefined) {
171
193
  const binding = object(p.recoveryFor, "accountId,collectionId");
172
194
  id(binding.accountId);
173
195
  id(binding.collectionId);
174
- if (!p.owner || p.requestedCreateCollections === true || p.freshRequestedCreateCollections === true)
196
+ if (!p.owner || p.requestedCreateCollections === true || p.freshRequestedCreateCollections === true || p.freshRequestedPrivateDevice === true)
175
197
  throw fail("binding");
176
198
  }
177
199
  if (selection !== undefined) {
178
200
  if (!isAppEnvironmentSelection(selection) || selection.environment !== p.environment ||
179
201
  selection.appOrigin !== p.origin || selection.cpOrigin !== p.cpOrigin ||
180
- (p.allowLoopbackHttp === true && !selection.allowLoopbackHttp) ||
181
- (p.allowNativeLoopbackHttp === true && !selection.allowNativeLoopbackHttp))
202
+ (p.allowLoopbackHttp === true && !selection.allowLoopbackHttp))
182
203
  throw fail("binding");
183
204
  }
184
205
  // The application supplies its exact origin; the origin-bound store and
185
206
  // CP registry authorize it. No brand-specific client allowlist.
186
- opened = await AppInstallationStore.open({ ...p, allowLoopbackHttp: selection?.allowLoopbackHttp ?? p.allowLoopbackHttp,
187
- allowNativeLoopbackHttp: selection?.allowNativeLoopbackHttp ?? p.allowNativeLoopbackHttp }, () => {
207
+ opened = await AppInstallationStore.open({ ...p, allowLoopbackHttp: selection?.allowLoopbackHttp ?? p.allowLoopbackHttp }, () => {
188
208
  const bytes = crypto.getRandomValues(new Uint8Array(32));
189
209
  try {
190
210
  const secret = `pair_${btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
191
- return new TextEncoder().encode(JSON.stringify({ version: 3, renewal: null, requestedCreateCollections: p.requestedCreateCollections ?? p.freshRequestedCreateCollections ?? false, collectionIds: [], createCollections: false, consentRequest: null, requestId: crypto.randomUUID(), installationId: crypto.randomUUID(), deviceId: crypto.randomUUID(), secret, appId: p.appId, ...(p.owner === undefined ? {} : { owner: p.owner }), kind: p.installationKind ?? "app-runtime", origin: p.origin, cpOrigin: p.cpOrigin, environment: p.environment, selection: null, confirmed: false, nativeStarted: false, proof: null, token: null, ...(p.recoveryFor ? { recoveryFor: p.recoveryFor } : {}) }));
211
+ return new TextEncoder().encode(JSON.stringify({ version: 3, renewal: null, requestedCreateCollections: p.requestedCreateCollections ?? p.freshRequestedCreateCollections ?? false, collectionIds: [], createCollections: false, consentRequest: null, ...(p.freshRequestedPrivateDevice === true ? { requestedPrivateDevice: true } : {}), requestId: crypto.randomUUID(), installationId: crypto.randomUUID(), deviceId: crypto.randomUUID(), secret, appId: p.appId, ...(p.owner === undefined ? {} : { owner: p.owner }), kind: p.installationKind ?? "app-runtime", origin: p.origin, cpOrigin: p.cpOrigin, environment: p.environment, selection: null, confirmed: false, nativeStarted: false, proof: null, token: null, ...(p.recoveryFor ? { recoveryFor: p.recoveryFor } : {}) }));
192
212
  }
193
213
  finally {
194
214
  bytes.fill(0);
@@ -216,7 +236,7 @@ export class AppProtectedInstallationSignIn {
216
236
  const fields = `${base},requestedCreateCollections,collectionIds,createCollections,consentRequest`;
217
237
  const r = legacy ? { ...object(parsed, base), version: 3, renewal: null, requestedCreateCollections: false, collectionIds: [], createCollections: false, consentRequest: null }
218
238
  : parsed?.version === 2 ? { ...object(parsed, fields), version: 3, renewal: null }
219
- : object(parsed, `${fields},renewal${Object.hasOwn(parsed, "collectionSelection") ? ",collectionSelection" : ""}${Object.hasOwn(parsed, "timerConsent") ? ",timerConsent" : ""}${Object.hasOwn(parsed, "timerChannel") ? ",timerChannel" : ""}${Object.hasOwn(parsed, "owner") ? ",owner" : ""}${Object.hasOwn(parsed, "collectionListing") ? ",collectionListing" : ""}${Object.hasOwn(parsed, "collectionRecoveries") ? ",collectionRecoveries" : ""}${Object.hasOwn(parsed, "recoveryFor") ? ",recoveryFor" : ""}${Object.hasOwn(parsed, "nativeAuthorization") ? ",nativeAuthorization" : ""}${Object.hasOwn(parsed, "nativeSelection") ? ",nativeSelection" : ""}${Object.hasOwn(parsed, "nativeGrants") ? ",nativeGrants" : ""}`);
239
+ : object(parsed, `${fields},renewal${Object.hasOwn(parsed, "collectionSelection") ? ",collectionSelection" : ""}${Object.hasOwn(parsed, "timerConsent") ? ",timerConsent" : ""}${Object.hasOwn(parsed, "timerChannel") ? ",timerChannel" : ""}${Object.hasOwn(parsed, "owner") ? ",owner" : ""}${Object.hasOwn(parsed, "collectionListing") ? ",collectionListing" : ""}${Object.hasOwn(parsed, "collectionRecoveries") ? ",collectionRecoveries" : ""}${Object.hasOwn(parsed, "recoveryFor") ? ",recoveryFor" : ""}${Object.hasOwn(parsed, "nativeAuthorization") ? ",nativeAuthorization" : ""}${Object.hasOwn(parsed, "nativeSelection") ? ",nativeSelection" : ""}${Object.hasOwn(parsed, "nativeGrants") ? ",nativeGrants" : ""}${Object.hasOwn(parsed, "requestedPrivateDevice") ? ",requestedPrivateDevice" : ""}${Object.hasOwn(parsed, "privateDeviceCollectionIds") ? ",privateDeviceCollectionIds" : ""}`);
220
240
  if (r.version !== 3 || r.appId !== p.appId || r.owner !== p.owner || r.kind !== (p.installationKind ?? "app-runtime") || r.origin !== p.origin || r.cpOrigin !== p.cpOrigin || r.environment !== p.environment || typeof r.confirmed !== "boolean" || typeof r.nativeStarted !== "boolean")
221
241
  throw fail("recovery_required");
222
242
  const s = r;
@@ -224,6 +244,15 @@ export class AppProtectedInstallationSignIn {
224
244
  (p.requestedCreateCollections !== undefined && p.requestedCreateCollections !== s.requestedCreateCollections))
225
245
  throw fail("binding");
226
246
  s.collectionIds = this.collectionIds(s.collectionIds);
247
+ // Private device consent: stored only as an explicit true / non-empty subset.
248
+ if (s.requestedPrivateDevice !== undefined && s.requestedPrivateDevice !== true)
249
+ throw fail("recovery_required");
250
+ if (s.privateDeviceCollectionIds !== undefined) {
251
+ const scopes = this.collectionIds(s.privateDeviceCollectionIds);
252
+ if (!scopes.length || !s.token || s.recoveryFor !== undefined || scopes.some(c => !s.collectionIds.includes(c)))
253
+ throw fail("recovery_required");
254
+ s.privateDeviceCollectionIds = scopes;
255
+ }
227
256
  if (s.recoveryFor !== undefined) {
228
257
  const binding = object(s.recoveryFor, "accountId,collectionId");
229
258
  id(binding.accountId);
@@ -233,7 +262,9 @@ export class AppProtectedInstallationSignIn {
233
262
  (s.selection && s.selection.accountId !== s.recoveryFor.accountId) || (s.token && (s.nativeSelection ? s.collectionIds.length !== 0 || s.nativeSelection.collectionId !== s.recoveryFor.collectionId : s.collectionIds.length !== 1 || s.collectionIds[0] !== s.recoveryFor.collectionId || s.collectionSelection?.selectedCollectionId !== s.recoveryFor.collectionId)))))
234
263
  throw fail("recovery_required");
235
264
  if (s.consentRequest !== null) {
236
- const c = object(s.consentRequest, `requestId,secret,requestedCreateCollections,challenge,completed,addedCollectionIds,approvedCreateCollections${portalKeys(s.consentRequest, true)}`);
265
+ const c = object(s.consentRequest, `requestId,secret,requestedCreateCollections,challenge,completed,addedCollectionIds,approvedCreateCollections${Object.hasOwn(s.consentRequest, "requestedPrivateDevice") ? ",requestedPrivateDevice" : ""}${portalKeys(s.consentRequest, true)}`);
266
+ if (c.requestedPrivateDevice !== undefined && c.requestedPrivateDevice !== true)
267
+ throw fail("recovery_required");
237
268
  portalMetadata(c, true);
238
269
  id(c.requestId);
239
270
  text(c.secret, /^pair_[A-Za-z0-9_-]{43}$/);
@@ -339,6 +370,10 @@ export class AppProtectedInstallationSignIn {
339
370
  }
340
371
  return s;
341
372
  }
373
+ /** The row's mode agrees with the consent recorded for that collection. */
374
+ modeConsented(c) {
375
+ return (c.mode === "private") === (this.state.privateDeviceCollectionIds?.includes(c.collectionId) === true);
376
+ }
342
377
  static collectionIds(value) {
343
378
  if (!Array.isArray(value) || value.length > 1000)
344
379
  throw fail("response");
@@ -357,7 +392,7 @@ export class AppProtectedInstallationSignIn {
357
392
  view() {
358
393
  this.check();
359
394
  const s = this.state, identity = s.consentRequest?.portalConfirmed ? s.consentRequest : s.selection;
360
- return Object.freeze({ requestId: s.requestId, installationId: s.installationId, deviceId: s.deviceId, appId: s.appId, kind: s.kind, verificationUri: `${s.cpOrigin}/pair/${s.requestId}`, state: s.token ? "paired" : s.proof ? "awaiting_approval" : s.confirmed ? "account_confirmed" : s.selection ? "account_selected" : "pending", accountId: s.selection?.accountId ?? null, accountEmail: identity?.accountEmail ?? null, ...(identity?.accountProvider ? { accountProvider: identity.accountProvider, accountLogin: identity.accountLogin } : {}), collectionSelection: s.collectionSelection ? Object.freeze({ ...s.collectionSelection, createdCollectionIds: Object.freeze([...s.collectionSelection.createdCollectionIds]) }) : null, nativeSelection: s.nativeSelection && s.nativeGrants?.some(c => c.authorization_request_id === s.nativeSelection.authorizationRequestId && c.collection_id === s.nativeSelection.collectionId) ? Object.freeze({ ...s.nativeSelection }) : null, nativeGrants: Object.freeze([...(s.nativeGrants ?? [])]), ...(s.timerConsent ? { timerConsent: s.timerConsent } : {}), collectionIds: Object.freeze([...s.collectionIds]), createCollections: s.createCollections, requestedCreateCollections: s.requestedCreateCollections });
395
+ return Object.freeze({ requestId: s.requestId, installationId: s.installationId, deviceId: s.deviceId, appId: s.appId, kind: s.kind, verificationUri: `${s.cpOrigin}/pair/${s.requestId}`, state: s.token ? "paired" : s.proof ? "awaiting_approval" : s.confirmed ? "account_confirmed" : s.selection ? "account_selected" : "pending", accountId: s.selection?.accountId ?? null, accountEmail: identity?.accountEmail ?? null, ...(identity?.accountProvider ? { accountProvider: identity.accountProvider, accountLogin: identity.accountLogin } : {}), collectionSelection: s.collectionSelection ? Object.freeze({ ...s.collectionSelection, createdCollectionIds: Object.freeze([...s.collectionSelection.createdCollectionIds]) }) : null, nativeSelection: s.nativeSelection && s.nativeGrants?.some(c => c.authorization_request_id === s.nativeSelection.authorizationRequestId && c.collection_id === s.nativeSelection.collectionId) ? Object.freeze({ ...s.nativeSelection }) : null, nativeGrants: Object.freeze([...(s.nativeGrants ?? [])]), ...(s.timerConsent ? { timerConsent: s.timerConsent } : {}), collectionIds: Object.freeze([...s.collectionIds]), createCollections: s.createCollections, requestedCreateCollections: s.requestedCreateCollections, ...(s.privateDeviceCollectionIds?.length ? { privateDeviceCollectionIds: Object.freeze([...s.privateDeviceCollectionIds]) } : {}) });
361
396
  }
362
397
  /** Persist before exposing changed selection/proof/credential or native ACK. */
363
398
  async save(next) {
@@ -454,7 +489,10 @@ export class AppProtectedInstallationSignIn {
454
489
  throw fail(error.code === "installation_pairing_expired" ? "expired" : "refused");
455
490
  }
456
491
  const server = appServerProblem(response.status, value, response.headers);
457
- throw new AppInstallationSignInError(appServerOutcome(server) ?? "refused", server);
492
+ const refusal = new AppInstallationSignInError(appServerOutcome(server) ?? "refused", server);
493
+ if (path === "/v1/pairing-requests" && unknownPrivateDeviceField(response.status, error))
494
+ privateDeviceFieldRefused.add(refusal);
495
+ throw refusal;
458
496
  }
459
497
  return { status: response.status, value: value };
460
498
  }
@@ -517,7 +555,20 @@ export class AppProtectedInstallationSignIn {
517
555
  const s = this.state;
518
556
  if (s.recoveryFor && hint !== null && hint !== s.recoveryFor.collectionId)
519
557
  throw fail("binding");
520
- const { status, value: v } = await this.json("/v1/pairing-requests", false, { installation: { app_id: s.appId, request_id: s.requestId, pairing_secret: s.secret, installation_id: s.installationId, device_id: s.deviceId, kind: s.kind, requested_create_collections: s.requestedCreateCollections, ...(s.recoveryFor ? { recovery_collection_id: s.recoveryFor.collectionId } : {}), ...(s.renewal ? { renewal: { request_id: s.renewal.requestId, pairing_secret: s.renewal.secret } } : {}) }, ...(hint ? { collection_hint: hint } : {}) });
558
+ let reply;
559
+ try {
560
+ reply = await this.json("/v1/pairing-requests", false, { installation: { app_id: s.appId, request_id: s.requestId, pairing_secret: s.secret, installation_id: s.installationId, device_id: s.deviceId, kind: s.kind, requested_create_collections: s.requestedCreateCollections, ...(s.requestedPrivateDevice === true ? { requested_private_device: true } : {}), ...(s.recoveryFor ? { recovery_collection_id: s.recoveryFor.collectionId } : {}), ...(s.renewal ? { renewal: { request_id: s.renewal.requestId, pairing_secret: s.renewal.secret } } : {}) }, ...(hint ? { collection_hint: hint } : {}) });
561
+ }
562
+ catch (error) {
563
+ if (s.requestedPrivateDevice !== true || !(error instanceof AppInstallationSignInError) || !privateDeviceFieldRefused.has(error))
564
+ throw error;
565
+ // The stored terms change BEFORE the repeat, so a receipt that later
566
+ // names a Private device scope is refused: this request never asked.
567
+ const { requestedPrivateDevice: _dropped, ...withoutPrivateDevice } = s;
568
+ await this.save(withoutPrivateDevice);
569
+ return await this.startRequest(hint);
570
+ }
571
+ const { status, value: v } = reply;
521
572
  if (status !== 200 && status !== 201)
522
573
  throw fail("response");
523
574
  object(v, `pairing_id,pairing_secret,verification_uri,expires_in,installation_device,app_id,app_origin,app_name${this.recoveryTargetKeys()}`);
@@ -593,13 +644,20 @@ export class AppProtectedInstallationSignIn {
593
644
  const scoped = Object.hasOwn(v, "collection_ids") || Object.hasOwn(v, "create_collections");
594
645
  const picked = Object.hasOwn(v, "selected_collection_id") || Object.hasOwn(v, "created_collection_ids");
595
646
  const native = Object.hasOwn(v, "native_selection");
596
- object(v, `${keys},connector,token,registration${scoped ? ",collection_ids,create_collections" : ""}${picked ? ",selected_collection_id,created_collection_ids" : ""}${native ? ",native_selection" : ""}${timerKeys(v)}`);
647
+ const device = Object.hasOwn(v, "private_device_collection_ids");
648
+ object(v, `${keys},connector,token,registration${scoped ? ",collection_ids,create_collections" : ""}${device ? ",private_device_collection_ids" : ""}${picked ? ",selected_collection_id,created_collection_ids" : ""}${native ? ",native_selection" : ""}${timerKeys(v)}`);
597
649
  if (status !== 200 || v.status !== "paired" || !this.state.confirmed || !this.state.proof)
598
650
  throw fail("response");
599
651
  const selection = this.selected(v), connector = object(v.connector, "id,name"), r = object(v.registration, "device_id,sign_pk,kem_pk,noise_pk");
600
652
  if (connector.id !== selection.connectorId || connector.name !== this.options.appName || r.device_id !== this.state.deviceId || r.sign_pk !== this.state.proof.sign_pk || r.kem_pk !== this.state.proof.kem_pk || r.noise_pk !== this.state.proof.noise_pk)
601
653
  throw fail("binding");
602
- const collectionIds = scoped ? AppProtectedInstallationSignIn.collectionIds(v.collection_ids) : [];
654
+ const cloudIds = scoped ? AppProtectedInstallationSignIn.collectionIds(v.collection_ids) : [];
655
+ // Private device scopes: an explicit list, disjoint from the cloud-copy
656
+ // one, and only for a request that asked. Absent means none.
657
+ const privateDeviceCollectionIds = device ? AppProtectedInstallationSignIn.collectionIds(v.private_device_collection_ids) : [];
658
+ if (privateDeviceCollectionIds.length && (!scoped || this.state.requestedPrivateDevice !== true || privateDeviceCollectionIds.some(c => cloudIds.includes(c))))
659
+ throw fail("response");
660
+ const collectionIds = AppProtectedInstallationSignIn.collectionIds([...cloudIds, ...privateDeviceCollectionIds]);
603
661
  const createCollections = scoped ? v.create_collections : false;
604
662
  if (typeof createCollections !== "boolean" || (createCollections && !this.state.requestedCreateCollections))
605
663
  throw fail("response");
@@ -614,9 +672,12 @@ export class AppProtectedInstallationSignIn {
614
672
  throw fail("binding");
615
673
  if (v.timer_consent === true && !collectionSelection)
616
674
  throw fail("response");
675
+ // Device access approved without a pick: Connect never names a Private
676
+ // device collection as the selection, so this receipt carries none.
677
+ const deviceOnly = !collectionSelection && privateDeviceCollectionIds.length > 0;
617
678
  const reminder = timerConsent(v, this.state.requestId, collectionIds, selection.portalConfirmed === true, collectionSelection?.selectedCollectionId);
618
679
  const token = text(v.token, /^idev_[A-Za-z0-9_-]{43}$/);
619
- await this.save({ ...this.state, selection, token, collectionIds, createCollections, nativeSelection, ...(nativeSelection ? { collectionSelection: undefined } : collectionSelection ? { collectionSelection } : {}), ...(reminder ? { timerConsent: reminder } : {}) });
680
+ await this.save({ ...this.state, selection, token, collectionIds, createCollections, privateDeviceCollectionIds: privateDeviceCollectionIds.length ? privateDeviceCollectionIds : undefined, nativeSelection, ...(nativeSelection || deviceOnly ? { collectionSelection: undefined } : collectionSelection ? { collectionSelection } : {}), ...(reminder ? { timerConsent: reminder } : {}) });
620
681
  return this.view();
621
682
  });
622
683
  }
@@ -679,7 +740,7 @@ export class AppProtectedInstallationSignIn {
679
740
  collectionConsentView() {
680
741
  this.check();
681
742
  const r = this.state.consentRequest;
682
- return r ? Object.freeze({ requestId: r.requestId, verificationUri: `${this.state.cpOrigin}/pair/${r.requestId}`, state: r.completed ? "scope_updated" : "pending", requestedCreateCollections: r.requestedCreateCollections, addedCollectionIds: Object.freeze([...r.addedCollectionIds]), approvedCreateCollections: r.approvedCreateCollections, accountEmail: r.accountEmail ?? null, ...(r.accountProvider ? { accountProvider: r.accountProvider, accountLogin: r.accountLogin } : {}), accountSelectionConfirmed: r.portalConfirmed === true }) : null;
743
+ return r ? Object.freeze({ requestId: r.requestId, verificationUri: `${this.state.cpOrigin}/pair/${r.requestId}`, state: r.completed ? "scope_updated" : "pending", requestedCreateCollections: r.requestedCreateCollections, addedCollectionIds: Object.freeze([...r.addedCollectionIds]), approvedCreateCollections: r.approvedCreateCollections, ...(r.requestedPrivateDevice === true ? { requestedPrivateDevice: true } : {}), accountEmail: r.accountEmail ?? null, ...(r.accountProvider ? { accountProvider: r.accountProvider, accountLogin: r.accountLogin } : {}), accountSelectionConfirmed: r.portalConfirmed === true }) : null;
683
744
  }
684
745
  consentGuard(installation) {
685
746
  const callback = installation.isCurrent;
@@ -707,7 +768,7 @@ export class AppProtectedInstallationSignIn {
707
768
  /** Explicit additive consent, not renewal/registration/account selection.
708
769
  * A new secret/request is protected BEFORE HTTP; unknown outcomes only permit
709
770
  * an explicit repeat of that exact request, never new keys or a new actor. */
710
- startCollectionConsent(installation, requestedCreateCollections = false, options = {}) {
771
+ startCollectionConsent(installation, requestedCreateCollections = false, options = {}, requestedPrivateDevice = false) {
711
772
  const check = this.consentGuard(installation);
712
773
  let hint;
713
774
  try {
@@ -718,14 +779,15 @@ export class AppProtectedInstallationSignIn {
718
779
  }
719
780
  const attempt = async () => {
720
781
  check();
721
- if (this.state.recoveryFor || typeof requestedCreateCollections !== "boolean")
782
+ if (this.state.recoveryFor || typeof requestedCreateCollections !== "boolean" || typeof requestedPrivateDevice !== "boolean")
722
783
  throw fail("binding");
723
784
  let r = this.state.consentRequest;
724
785
  if (!r || r.completed) {
725
786
  const bytes = crypto.getRandomValues(new Uint8Array(32));
726
787
  try {
727
788
  const secret = `pair_${btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
728
- r = { requestId: crypto.randomUUID(), secret, requestedCreateCollections, challenge: null, completed: false, addedCollectionIds: [], approvedCreateCollections: false };
789
+ // A window still open keeps the terms it was stored with (below); only a new one asks for device access.
790
+ r = { requestId: crypto.randomUUID(), secret, requestedCreateCollections, challenge: null, completed: false, addedCollectionIds: [], approvedCreateCollections: false, ...(requestedPrivateDevice ? { requestedPrivateDevice: true } : {}) };
729
791
  await this.save({ ...this.state, consentRequest: r });
730
792
  check();
731
793
  }
@@ -736,8 +798,23 @@ export class AppProtectedInstallationSignIn {
736
798
  else if (r.requestedCreateCollections !== requestedCreateCollections)
737
799
  throw fail("binding");
738
800
  const s = this.state, token = s.token, req = r;
739
- const { status, value: v } = await this.consentWindow(req.requestId, () => this.json("/v1/pairing-requests", false, { installation: { app_id: s.appId, request_id: req.requestId, pairing_secret: req.secret, installation_id: s.installationId, device_id: s.deviceId, kind: s.kind, requested_create_collections: req.requestedCreateCollections, reconsent: true }, ...(hint ? { collection_hint: hint } : {}) }, { bearer: token }));
801
+ let reply;
802
+ try {
803
+ reply = await this.consentWindow(req.requestId, () => this.json("/v1/pairing-requests", false, { installation: { app_id: s.appId, request_id: req.requestId, pairing_secret: req.secret, installation_id: s.installationId, device_id: s.deviceId, kind: s.kind, requested_create_collections: req.requestedCreateCollections, ...(req.requestedPrivateDevice === true ? { requested_private_device: true } : {}), reconsent: true }, ...(hint ? { collection_hint: hint } : {}) }, { bearer: token }));
804
+ }
805
+ catch (error) {
806
+ if (req.requestedPrivateDevice !== true || !(error instanceof AppInstallationSignInError) || !privateDeviceFieldRefused.has(error))
807
+ throw error;
808
+ // Same single repeat as START: this window is re-stored as one that
809
+ // never asked for Private device access before it is sent again.
810
+ check();
811
+ const { requestedPrivateDevice: _dropped, ...withoutPrivateDevice } = req;
812
+ await this.save({ ...this.state, consentRequest: withoutPrivateDevice });
813
+ check();
814
+ return await attempt();
815
+ }
740
816
  check();
817
+ const { status, value: v } = reply;
741
818
  object(v, "pairing_id,pairing_secret,verification_uri,expires_in,installation_device,app_id,app_origin,app_name");
742
819
  let uri = null;
743
820
  if ((status !== 200 && status !== 201) || v.pairing_id !== r.requestId || v.pairing_secret !== r.secret || (uri = verificationUri(v.verification_uri, `${s.cpOrigin}/pair/${r.requestId}`, hint)) === null || v.installation_device !== true || v.app_id !== s.appId || v.app_origin !== s.origin || v.app_name !== this.options.appName || !Number.isSafeInteger(v.expires_in) || v.expires_in < 0 || v.expires_in > 600 || token !== this.state.token)
@@ -776,7 +853,10 @@ export class AppProtectedInstallationSignIn {
776
853
  const done = v.status === "scope_updated";
777
854
  const picked = done && (Object.hasOwn(v, "selected_collection_id") || Object.hasOwn(v, "created_collection_ids"));
778
855
  const native = done && Object.hasOwn(v, "native_selection");
779
- object(v, `${keys}${done ? ",added_collection_ids,approved_create_collections" : ""}${picked ? ",selected_collection_id,created_collection_ids" : ""}${native ? ",native_selection" : ""}${done ? timerKeys(v) : ""}`);
856
+ // The contract names the receipt list `private_device_collection_ids`; an
857
+ // additive receipt may mirror `added_collection_ids`. Exactly one is read.
858
+ const deviceKey = !done ? null : Object.hasOwn(v, "added_private_device_collection_ids") ? "added_private_device_collection_ids" : Object.hasOwn(v, "private_device_collection_ids") ? "private_device_collection_ids" : null;
859
+ object(v, `${keys}${done ? ",added_collection_ids,approved_create_collections" : ""}${deviceKey ? `,${deviceKey}` : ""}${picked ? ",selected_collection_id,created_collection_ids" : ""}${native ? ",native_selection" : ""}${done ? timerKeys(v) : ""}`);
780
860
  if ((!done && (status !== 202 || v.status !== "awaiting_approval")) || (done && status !== 200) || v.request_id !== r.requestId || v.account_id !== s.selection.accountId || v.connector_id !== s.selection.connectorId || v.device_id !== s.deviceId || v.installation_id !== s.installationId || v.kind !== s.kind || v.app_id !== s.appId || v.app_origin !== s.origin || v.approval_mode !== s.selection.approvalMode || !Number.isSafeInteger(v.expires_at) || v.expires_at < 0)
781
861
  throw fail("binding");
782
862
  const confirmation = portalMetadata(v);
@@ -792,7 +872,15 @@ export class AppProtectedInstallationSignIn {
792
872
  check();
793
873
  return this.collectionConsentView();
794
874
  }
795
- const addedCollectionIds = AppProtectedInstallationSignIn.collectionIds(v.added_collection_ids);
875
+ const addedCloudIds = AppProtectedInstallationSignIn.collectionIds(v.added_collection_ids);
876
+ const addedPrivateIds = deviceKey ? AppProtectedInstallationSignIn.collectionIds(v[deviceKey]) : [];
877
+ // Disjoint lists; only a window that asked; a scope keeps the mode it was approved with.
878
+ if (addedPrivateIds.length && (r.requestedPrivateDevice !== true || addedPrivateIds.some(c => addedCloudIds.includes(c) || (s.collectionIds.includes(c) && !s.privateDeviceCollectionIds?.includes(c)))))
879
+ throw fail("response");
880
+ if (addedCloudIds.some(c => s.privateDeviceCollectionIds?.includes(c)))
881
+ throw fail("response");
882
+ const addedCollectionIds = AppProtectedInstallationSignIn.collectionIds([...addedCloudIds, ...addedPrivateIds]);
883
+ const privateDeviceCollectionIds = [...new Set([...(s.privateDeviceCollectionIds ?? []), ...addedPrivateIds])];
796
884
  if (typeof v.approved_create_collections !== "boolean" || (v.approved_create_collections && !r.requestedCreateCollections))
797
885
  throw fail("response");
798
886
  const collectionIds = AppProtectedInstallationSignIn.collectionIds([...new Set([...s.collectionIds, ...addedCollectionIds])]);
@@ -803,7 +891,8 @@ export class AppProtectedInstallationSignIn {
803
891
  if (v.timer_consent === true && !collectionSelection)
804
892
  throw fail("response");
805
893
  const reminder = timerConsent(v, r.requestId, addedCollectionIds, confirmation.portalConfirmed === true, collectionSelection?.selectedCollectionId);
806
- await this.save({ ...s, collectionIds, nativeSelection, ...(nativeSelection ? { collectionSelection: undefined } : collectionSelection ? { collectionSelection } : {}), ...(reminder ? { timerConsent: reminder } : {}), createCollections: s.createCollections || v.approved_create_collections, consentRequest: { ...r, ...confirmation, challenge, completed: true, addedCollectionIds, approvedCreateCollections: v.approved_create_collections } });
894
+ // A device-only receipt has no pick: an earlier request's pick is not this one's.
895
+ await this.save({ ...s, collectionIds, privateDeviceCollectionIds: privateDeviceCollectionIds.length ? privateDeviceCollectionIds : undefined, nativeSelection, ...(nativeSelection || (!collectionSelection && addedPrivateIds.length > 0) ? { collectionSelection: undefined } : collectionSelection ? { collectionSelection } : {}), ...(reminder ? { timerConsent: reminder } : {}), createCollections: s.createCollections || v.approved_create_collections, consentRequest: { ...r, ...confirmation, challenge, completed: true, addedCollectionIds, approvedCreateCollections: v.approved_create_collections } });
807
896
  check();
808
897
  return this.collectionConsentView();
809
898
  });
@@ -815,13 +904,29 @@ export class AppProtectedInstallationSignIn {
815
904
  return this.exclusive(async () => {
816
905
  check();
817
906
  let reply;
907
+ // `mode` is asked for only by an app that declares Private device access
908
+ // AND holds such a scope: only then can a Private row be offered at all
909
+ // (modeConsented). Every other installation sends today's exact request;
910
+ // a server from before this feature matches `include=file_actions` exactly
911
+ // and would silently drop the file actions from any other spelling.
912
+ const mode = this.options.freshRequestedPrivateDevice === true && (this.state.privateDeviceCollectionIds?.length ?? 0) > 0;
913
+ const today = "/v1/next/collections?include=file_actions", ask = (path) => this.json(path, false, undefined, { bearer: this.state.token, get: true });
818
914
  try {
819
- reply = await this.json("/v1/next/collections?include=file_actions", false, undefined, { bearer: this.state.token, get: true });
915
+ reply = await ask(mode ? `${today},mode` : today);
916
+ // A server from before this feature (one rolled back under an SDK that
917
+ // holds Private scopes) answers the spelling it does not know with bare
918
+ // rows: no `mode` and no file actions. A server that knows `mode` puts
919
+ // it on every row. So rows that all lack it are asked for once more,
920
+ // with today's exact request, and the file actions survive the rollback.
921
+ if (mode && reply.status === 200 && modeUnanswered(reply.value)) {
922
+ check();
923
+ reply = await ask(today);
924
+ }
820
925
  }
821
926
  catch (error) {
822
927
  check();
823
928
  if (error instanceof AppInstallationSignInError && error.reason === "unavailable" && !error.server && !nonOkResponses.has(error) && this.state.collectionListing !== undefined)
824
- return Object.freeze(collectionListing(this.state.collectionListing).filter(c => this.state.collectionIds.includes(c.collectionId)).map(c => Object.freeze({ ...c, fileActions: Object.freeze([]) })));
929
+ return Object.freeze(collectionListing(this.state.collectionListing).filter(c => this.state.collectionIds.includes(c.collectionId) && this.modeConsented(c)).map(c => Object.freeze({ ...c, fileActions: Object.freeze([]) })));
825
930
  if (error instanceof AppInstallationSignInError && (error.server || nonOkResponses.has(error)) && this.state.collectionListing !== undefined) {
826
931
  const state = { ...this.state };
827
932
  delete state.collectionListing;
@@ -835,7 +940,9 @@ export class AppProtectedInstallationSignIn {
835
940
  object(v, "collections");
836
941
  if (status !== 200)
837
942
  throw fail("response");
838
- const collections = collectionListing(v.collections);
943
+ // A listed mode never adds consent: a Private row is offered only with
944
+ // this installation's Private device scope, a cloud-copy row never with one.
945
+ const collections = Object.freeze(collectionListing(v.collections).filter(c => this.modeConsented(c)));
839
946
  // Receipt references narrow replay; metadata cannot add consent or infer a role.
840
947
  const approved = v.collections.filter(c => this.state.collectionIds.includes(c.collection_id)).map(c => { const metadata = { ...c }; delete metadata.file_actions; return metadata; });
841
948
  const state = { ...this.state };