@mdbase-dev/sdk 0.1.0-beta.169 → 0.1.0-beta.175
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/README.md +91 -3
- package/dist/app-host/account-key-host.d.ts +128 -0
- package/dist/app-host/account-key-host.js +449 -0
- package/dist/app-host/account-key-host.js.map +1 -0
- package/dist/app-host/app-environment.d.ts +3 -4
- package/dist/app-host/app-environment.js +6 -6
- package/dist/app-host/app-environment.js.map +1 -1
- package/dist/app-host/app-origin.d.ts +5 -2
- package/dist/app-host/app-origin.js +10 -6
- package/dist/app-host/app-origin.js.map +1 -1
- package/dist/app-host/cloud-copy-bootstrap.js +2 -2
- package/dist/app-host/cloud-copy-bootstrap.js.map +1 -1
- package/dist/app-host/cloud-copy-host.d.ts +69 -5
- package/dist/app-host/cloud-copy-host.js +289 -20
- package/dist/app-host/cloud-copy-host.js.map +1 -1
- package/dist/app-host/cloud-copy-persistence-idb.d.ts +0 -2
- package/dist/app-host/cloud-copy-persistence-idb.js +2 -2
- package/dist/app-host/cloud-copy-persistence-idb.js.map +1 -1
- package/dist/app-host/cp-authority.js +2 -2
- package/dist/app-host/cp-authority.js.map +1 -1
- package/dist/app-host/cp-people.js +2 -2
- package/dist/app-host/cp-people.js.map +1 -1
- package/dist/app-host/cp-problem.d.ts +1 -1
- package/dist/app-host/cp-problem.js +4 -0
- package/dist/app-host/cp-problem.js.map +1 -1
- package/dist/app-host/device-key-idb.d.ts +0 -2
- package/dist/app-host/device-key-idb.js +1 -1
- package/dist/app-host/device-key-idb.js.map +1 -1
- package/dist/app-host/device-key-provider.d.ts +0 -3
- package/dist/app-host/device-key-provider.js +1 -1
- package/dist/app-host/device-key-provider.js.map +1 -1
- package/dist/app-host/device-registration.js +2 -2
- package/dist/app-host/device-registration.js.map +1 -1
- package/dist/app-host/http-log.d.ts +2 -6
- package/dist/app-host/http-log.js +12 -45
- package/dist/app-host/http-log.js.map +1 -1
- package/dist/app-host/index.d.ts +4 -2
- package/dist/app-host/index.js +2 -1
- package/dist/app-host/index.js.map +1 -1
- package/dist/app-host/installation-sign-in.d.ts +19 -1
- package/dist/app-host/installation-sign-in.js +149 -30
- package/dist/app-host/installation-sign-in.js.map +1 -1
- package/dist/app-host/installation-store.d.ts +0 -2
- package/dist/app-host/installation-store.js +4 -4
- package/dist/app-host/installation-store.js.map +1 -1
- package/dist/app-host/log-push.js +2 -2
- package/dist/app-host/log-push.js.map +1 -1
- package/dist/app-host/noise-idb.d.ts +0 -3
- package/dist/app-host/noise-idb.js +1 -1
- package/dist/app-host/noise-idb.js.map +1 -1
- package/dist/app-host/private-bootstrap.js +2 -2
- package/dist/app-host/private-bootstrap.js.map +1 -1
- package/dist/app-host/private-persistence-idb.d.ts +0 -2
- package/dist/app-host/private-persistence-idb.js +1 -1
- package/dist/app-host/private-persistence-idb.js.map +1 -1
- package/dist/app-host/private-persistence.js +2 -2
- package/dist/app-host/private-persistence.js.map +1 -1
- package/dist/app-host/scoped-sign-in-session.d.ts +4 -0
- package/dist/app-host/scoped-sign-in-session.js +13 -1
- package/dist/app-host/scoped-sign-in-session.js.map +1 -1
- package/dist/app-host/spare-nonces.d.ts +27 -0
- package/dist/app-host/spare-nonces.js +58 -0
- package/dist/app-host/spare-nonces.js.map +1 -0
- package/dist/app-host/unsynced-collection.js +8 -3
- package/dist/app-host/unsynced-collection.js.map +1 -1
- package/dist/app-host/wasm-runtime.d.ts +14 -0
- package/dist/app-host/wasm-runtime.js +45 -0
- package/dist/app-host/wasm-runtime.js.map +1 -1
- package/dist/app-host/web-collection.d.ts +4 -1
- package/dist/app-host/web-collection.js +214 -125
- package/dist/app-host/web-collection.js.map +1 -1
- package/dist/app-host/web-sign-in-problem.js +6 -1
- package/dist/app-host/web-sign-in-problem.js.map +1 -1
- package/dist/app-host/web-sign-in-session.d.ts +46 -4
- package/dist/app-host/web-sign-in-session.js +88 -15
- package/dist/app-host/web-sign-in-session.js.map +1 -1
- package/dist/application/asset-loader.d.ts +0 -1
- package/dist/application/asset-loader.js +2 -2
- package/dist/application/asset-loader.js.map +1 -1
- package/dist/application/asset-pins.json +4 -4
- package/dist/application/browser-selection.js +1 -1
- package/dist/application/browser-selection.js.map +1 -1
- package/dist/application/collection-selection.d.ts +10 -0
- package/dist/application/collection-selection.js +58 -11
- package/dist/application/collection-selection.js.map +1 -1
- package/dist/application/declaration.js +2 -2
- package/dist/application/declaration.js.map +1 -1
- package/dist/application/entry.d.ts +19 -0
- package/dist/application/entry.js +81 -38
- package/dist/application/entry.js.map +1 -1
- package/dist/application/front-door.d.ts +6 -1
- package/dist/application/front-door.js +5 -0
- package/dist/application/front-door.js.map +1 -1
- package/dist/application/local-build.d.ts +4 -2
- package/dist/application/local-build.js +4 -2
- package/dist/application/local-build.js.map +1 -1
- package/dist/application/manifest.d.ts +1 -0
- package/dist/application/manifest.js +8 -3
- package/dist/application/manifest.js.map +1 -1
- package/dist/application/native-assets.js +3 -4
- package/dist/application/native-assets.js.map +1 -1
- package/dist/application/packaged-assets.d.ts +1 -1
- package/dist/application/packaged-assets.js +6 -3
- package/dist/application/packaged-assets.js.map +1 -1
- package/dist/application/phase.d.ts +21 -1
- package/dist/application/phase.js +12 -1
- package/dist/application/phase.js.map +1 -1
- package/dist/application/portal-reservation.js +2 -2
- package/dist/application/portal-reservation.js.map +1 -1
- package/dist/application/registration.d.ts +3 -0
- package/dist/application/registration.js +7 -4
- package/dist/application/registration.js.map +1 -1
- package/dist/application/return-url.d.ts +1 -1
- package/dist/application/return-url.js +6 -7
- package/dist/application/return-url.js.map +1 -1
- package/dist/application/runtime.d.ts +7 -1
- package/dist/application/runtime.js +53 -4
- package/dist/application/runtime.js.map +1 -1
- package/dist/application/startup-timing.d.ts +20 -0
- package/dist/application/startup-timing.js +58 -0
- package/dist/application/startup-timing.js.map +1 -0
- package/dist/application/ui-connections.js +17 -4
- package/dist/application/ui-connections.js.map +1 -1
- package/dist/application/worker-client.d.ts +3 -0
- package/dist/application/worker-client.js +27 -2
- package/dist/application/worker-client.js.map +1 -1
- package/dist/application/worker-host.js +51 -3
- package/dist/application/worker-host.js.map +1 -1
- package/dist/application/worker-protocol.d.ts +7 -2
- package/dist/application/worker-protocol.js +10 -1
- package/dist/application/worker-protocol.js.map +1 -1
- package/dist/application/worker-session.d.ts +1 -1
- package/dist/application/worker-session.js +5 -1
- package/dist/application/worker-session.js.map +1 -1
- package/dist/application/worker.js +8969 -2561
- package/dist/application/worker.js.map +1 -1
- package/dist/browser/mdbase-sdk.js +8174 -3680
- package/dist/browser/mdbase-sdk.min.js +78 -78
- package/dist/describe-error.d.ts +2 -2
- package/dist/describe-error.js +11 -2
- package/dist/describe-error.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/local-build.d.ts +7 -4
- package/dist/local-build.js +7 -4
- package/dist/local-build.js.map +1 -1
- package/dist/native/provenance.json +5 -5
- package/dist/native/runtime.wasm +0 -0
- package/dist/private-account.js +13 -18
- package/dist/private-account.js.map +1 -1
- package/dist/testing/application.d.ts +3 -1
- package/dist/testing/application.js +8 -1
- package/dist/testing/application.js.map +1 -1
- package/dist/testing/test-app.d.ts +9 -0
- package/dist/testing/test-app.js +13 -0
- package/dist/testing/test-app.js.map +1 -1
- package/dist/transport/control-route.d.ts +2 -1
- package/dist/transport/control-route.js +4 -1
- package/dist/transport/control-route.js.map +1 -1
- package/dist/transport/relay.d.ts +7 -0
- package/dist/transport/relay.js +3 -1
- package/dist/transport/relay.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,6 +187,8 @@ 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);
|
|
@@ -177,18 +199,16 @@ export class AppProtectedInstallationSignIn {
|
|
|
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,16 +244,28 @@ 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 || 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);
|
|
230
259
|
id(binding.collectionId);
|
|
231
260
|
}
|
|
232
261
|
if ((s.recoveryFor === undefined) !== (p.recoveryFor === undefined) || s.recoveryFor?.accountId !== p.recoveryFor?.accountId || s.recoveryFor?.collectionId !== p.recoveryFor?.collectionId || (s.recoveryFor && (s.requestedCreateCollections || s.createCollections ||
|
|
233
|
-
(s.selection && s.selection.accountId !== s.recoveryFor.accountId) || (s.token && (s.nativeSelection ? s.collectionIds.length !== 0 || s.nativeSelection.collectionId !== s.recoveryFor.collectionId
|
|
262
|
+
(s.selection && s.selection.accountId !== s.recoveryFor.accountId) || (s.token && (s.nativeSelection ? s.collectionIds.length !== 0 || s.nativeSelection.collectionId !== s.recoveryFor.collectionId
|
|
263
|
+
: !AppProtectedInstallationSignIn.recovered(s.recoveryFor.collectionId, s.collectionIds, s.privateDeviceCollectionIds ?? [], s.collectionSelection))))))
|
|
234
264
|
throw fail("recovery_required");
|
|
235
265
|
if (s.consentRequest !== null) {
|
|
236
|
-
const c = object(s.consentRequest, `requestId,secret,requestedCreateCollections,challenge,completed,addedCollectionIds,approvedCreateCollections${portalKeys(s.consentRequest, true)}`);
|
|
266
|
+
const c = object(s.consentRequest, `requestId,secret,requestedCreateCollections,challenge,completed,addedCollectionIds,approvedCreateCollections${Object.hasOwn(s.consentRequest, "requestedPrivateDevice") ? ",requestedPrivateDevice" : ""}${portalKeys(s.consentRequest, true)}`);
|
|
267
|
+
if (c.requestedPrivateDevice !== undefined && c.requestedPrivateDevice !== true)
|
|
268
|
+
throw fail("recovery_required");
|
|
237
269
|
portalMetadata(c, true);
|
|
238
270
|
id(c.requestId);
|
|
239
271
|
text(c.secret, /^pair_[A-Za-z0-9_-]{43}$/);
|
|
@@ -339,6 +371,10 @@ export class AppProtectedInstallationSignIn {
|
|
|
339
371
|
}
|
|
340
372
|
return s;
|
|
341
373
|
}
|
|
374
|
+
/** The row's mode agrees with the consent recorded for that collection. */
|
|
375
|
+
modeConsented(c) {
|
|
376
|
+
return (c.mode === "private") === (this.state.privateDeviceCollectionIds?.includes(c.collectionId) === true);
|
|
377
|
+
}
|
|
342
378
|
static collectionIds(value) {
|
|
343
379
|
if (!Array.isArray(value) || value.length > 1000)
|
|
344
380
|
throw fail("response");
|
|
@@ -357,7 +393,7 @@ export class AppProtectedInstallationSignIn {
|
|
|
357
393
|
view() {
|
|
358
394
|
this.check();
|
|
359
395
|
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 });
|
|
396
|
+
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
397
|
}
|
|
362
398
|
/** Persist before exposing changed selection/proof/credential or native ACK. */
|
|
363
399
|
async save(next) {
|
|
@@ -454,7 +490,10 @@ export class AppProtectedInstallationSignIn {
|
|
|
454
490
|
throw fail(error.code === "installation_pairing_expired" ? "expired" : "refused");
|
|
455
491
|
}
|
|
456
492
|
const server = appServerProblem(response.status, value, response.headers);
|
|
457
|
-
|
|
493
|
+
const refusal = new AppInstallationSignInError(appServerOutcome(server) ?? "refused", server);
|
|
494
|
+
if (path === "/v1/pairing-requests" && unknownPrivateDeviceField(response.status, error))
|
|
495
|
+
privateDeviceFieldRefused.add(refusal);
|
|
496
|
+
throw refusal;
|
|
458
497
|
}
|
|
459
498
|
return { status: response.status, value: value };
|
|
460
499
|
}
|
|
@@ -517,7 +556,20 @@ export class AppProtectedInstallationSignIn {
|
|
|
517
556
|
const s = this.state;
|
|
518
557
|
if (s.recoveryFor && hint !== null && hint !== s.recoveryFor.collectionId)
|
|
519
558
|
throw fail("binding");
|
|
520
|
-
|
|
559
|
+
let reply;
|
|
560
|
+
try {
|
|
561
|
+
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 } : {}) });
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
if (s.requestedPrivateDevice !== true || !(error instanceof AppInstallationSignInError) || !privateDeviceFieldRefused.has(error))
|
|
565
|
+
throw error;
|
|
566
|
+
// The stored terms change BEFORE the repeat, so a receipt that later
|
|
567
|
+
// names a Private device scope is refused: this request never asked.
|
|
568
|
+
const { requestedPrivateDevice: _dropped, ...withoutPrivateDevice } = s;
|
|
569
|
+
await this.save(withoutPrivateDevice);
|
|
570
|
+
return await this.startRequest(hint);
|
|
571
|
+
}
|
|
572
|
+
const { status, value: v } = reply;
|
|
521
573
|
if (status !== 200 && status !== 201)
|
|
522
574
|
throw fail("response");
|
|
523
575
|
object(v, `pairing_id,pairing_secret,verification_uri,expires_in,installation_device,app_id,app_origin,app_name${this.recoveryTargetKeys()}`);
|
|
@@ -573,9 +625,12 @@ export class AppProtectedInstallationSignIn {
|
|
|
573
625
|
if (this.state.token)
|
|
574
626
|
return this.view();
|
|
575
627
|
const { status, value: v } = await this.json(`/v1/pairing-requests/${this.state.requestId}/exchange`, true);
|
|
628
|
+
// A window renewed after a denial waits for the SAME account to be chosen
|
|
629
|
+
// again (Connect keeps its account, connector and challenge, and selected()
|
|
630
|
+
// still binds them), so it may read pending while this state holds the selection.
|
|
576
631
|
if (v.status === "pending") {
|
|
577
632
|
object(v, "status");
|
|
578
|
-
if (status !== 202 || this.state.selection)
|
|
633
|
+
if (status !== 202 || (this.state.selection && !this.state.renewal))
|
|
579
634
|
throw fail("response");
|
|
580
635
|
return this.view();
|
|
581
636
|
}
|
|
@@ -593,13 +648,20 @@ export class AppProtectedInstallationSignIn {
|
|
|
593
648
|
const scoped = Object.hasOwn(v, "collection_ids") || Object.hasOwn(v, "create_collections");
|
|
594
649
|
const picked = Object.hasOwn(v, "selected_collection_id") || Object.hasOwn(v, "created_collection_ids");
|
|
595
650
|
const native = Object.hasOwn(v, "native_selection");
|
|
596
|
-
|
|
651
|
+
const device = Object.hasOwn(v, "private_device_collection_ids");
|
|
652
|
+
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
653
|
if (status !== 200 || v.status !== "paired" || !this.state.confirmed || !this.state.proof)
|
|
598
654
|
throw fail("response");
|
|
599
655
|
const selection = this.selected(v), connector = object(v.connector, "id,name"), r = object(v.registration, "device_id,sign_pk,kem_pk,noise_pk");
|
|
600
656
|
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
657
|
throw fail("binding");
|
|
602
|
-
const
|
|
658
|
+
const cloudIds = scoped ? AppProtectedInstallationSignIn.collectionIds(v.collection_ids) : [];
|
|
659
|
+
// Private device scopes: an explicit list, disjoint from the cloud-copy
|
|
660
|
+
// one, and only for a request that asked. Absent means none.
|
|
661
|
+
const privateDeviceCollectionIds = device ? AppProtectedInstallationSignIn.collectionIds(v.private_device_collection_ids) : [];
|
|
662
|
+
if (privateDeviceCollectionIds.length && (!scoped || this.state.requestedPrivateDevice !== true || privateDeviceCollectionIds.some(c => cloudIds.includes(c))))
|
|
663
|
+
throw fail("response");
|
|
664
|
+
const collectionIds = AppProtectedInstallationSignIn.collectionIds([...cloudIds, ...privateDeviceCollectionIds]);
|
|
603
665
|
const createCollections = scoped ? v.create_collections : false;
|
|
604
666
|
if (typeof createCollections !== "boolean" || (createCollections && !this.state.requestedCreateCollections))
|
|
605
667
|
throw fail("response");
|
|
@@ -609,17 +671,28 @@ export class AppProtectedInstallationSignIn {
|
|
|
609
671
|
throw fail("response");
|
|
610
672
|
if (this.state.recoveryFor && nativeSelection && nativeSelection.collectionId !== this.state.recoveryFor.collectionId)
|
|
611
673
|
throw fail("binding");
|
|
612
|
-
if (this.state.recoveryFor && !nativeSelection && (createCollections ||
|
|
613
|
-
|
|
674
|
+
if (this.state.recoveryFor && !nativeSelection && (createCollections ||
|
|
675
|
+
!AppProtectedInstallationSignIn.recovered(this.state.recoveryFor.collectionId, collectionIds, privateDeviceCollectionIds, collectionSelection)))
|
|
614
676
|
throw fail("binding");
|
|
615
677
|
if (v.timer_consent === true && !collectionSelection)
|
|
616
678
|
throw fail("response");
|
|
679
|
+
// Device access approved without a pick: Connect never names a Private
|
|
680
|
+
// device collection as the selection, so this receipt carries none.
|
|
681
|
+
const deviceOnly = !collectionSelection && privateDeviceCollectionIds.length > 0;
|
|
617
682
|
const reminder = timerConsent(v, this.state.requestId, collectionIds, selection.portalConfirmed === true, collectionSelection?.selectedCollectionId);
|
|
618
683
|
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 } : {}) });
|
|
684
|
+
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
685
|
return this.view();
|
|
621
686
|
});
|
|
622
687
|
}
|
|
688
|
+
/** A recovery slot holds exactly its one collection: the selected cloud copy,
|
|
689
|
+
* or the one Private device scope (which Connect never names as a selection). */
|
|
690
|
+
static recovered(collection, ids, device, selection) {
|
|
691
|
+
if (ids.length !== 1 || ids[0] !== collection)
|
|
692
|
+
return false;
|
|
693
|
+
return device.length ? device.length === 1 && device[0] === collection && selection === undefined
|
|
694
|
+
: selection?.selectedCollectionId === collection && selection.createdCollectionIds.length === 0;
|
|
695
|
+
}
|
|
623
696
|
parseNativeSelection(value, requestId) {
|
|
624
697
|
const n = object(value, "authorization_request_id,collection_id"), a = this.state.nativeAuthorization;
|
|
625
698
|
if (!a || a.requestId !== requestId || id(n.authorization_request_id) !== a.authorizationId)
|
|
@@ -679,7 +752,7 @@ export class AppProtectedInstallationSignIn {
|
|
|
679
752
|
collectionConsentView() {
|
|
680
753
|
this.check();
|
|
681
754
|
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;
|
|
755
|
+
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
756
|
}
|
|
684
757
|
consentGuard(installation) {
|
|
685
758
|
const callback = installation.isCurrent;
|
|
@@ -707,7 +780,7 @@ export class AppProtectedInstallationSignIn {
|
|
|
707
780
|
/** Explicit additive consent, not renewal/registration/account selection.
|
|
708
781
|
* A new secret/request is protected BEFORE HTTP; unknown outcomes only permit
|
|
709
782
|
* an explicit repeat of that exact request, never new keys or a new actor. */
|
|
710
|
-
startCollectionConsent(installation, requestedCreateCollections = false, options = {}) {
|
|
783
|
+
startCollectionConsent(installation, requestedCreateCollections = false, options = {}, requestedPrivateDevice = false) {
|
|
711
784
|
const check = this.consentGuard(installation);
|
|
712
785
|
let hint;
|
|
713
786
|
try {
|
|
@@ -718,14 +791,15 @@ export class AppProtectedInstallationSignIn {
|
|
|
718
791
|
}
|
|
719
792
|
const attempt = async () => {
|
|
720
793
|
check();
|
|
721
|
-
if (this.state.recoveryFor || typeof requestedCreateCollections !== "boolean")
|
|
794
|
+
if (this.state.recoveryFor || typeof requestedCreateCollections !== "boolean" || typeof requestedPrivateDevice !== "boolean")
|
|
722
795
|
throw fail("binding");
|
|
723
796
|
let r = this.state.consentRequest;
|
|
724
797
|
if (!r || r.completed) {
|
|
725
798
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
726
799
|
try {
|
|
727
800
|
const secret = `pair_${btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
728
|
-
|
|
801
|
+
// A window still open keeps the terms it was stored with (below); only a new one asks for device access.
|
|
802
|
+
r = { requestId: crypto.randomUUID(), secret, requestedCreateCollections, challenge: null, completed: false, addedCollectionIds: [], approvedCreateCollections: false, ...(requestedPrivateDevice ? { requestedPrivateDevice: true } : {}) };
|
|
729
803
|
await this.save({ ...this.state, consentRequest: r });
|
|
730
804
|
check();
|
|
731
805
|
}
|
|
@@ -736,8 +810,23 @@ export class AppProtectedInstallationSignIn {
|
|
|
736
810
|
else if (r.requestedCreateCollections !== requestedCreateCollections)
|
|
737
811
|
throw fail("binding");
|
|
738
812
|
const s = this.state, token = s.token, req = r;
|
|
739
|
-
|
|
813
|
+
let reply;
|
|
814
|
+
try {
|
|
815
|
+
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 }));
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
if (req.requestedPrivateDevice !== true || !(error instanceof AppInstallationSignInError) || !privateDeviceFieldRefused.has(error))
|
|
819
|
+
throw error;
|
|
820
|
+
// Same single repeat as START: this window is re-stored as one that
|
|
821
|
+
// never asked for Private device access before it is sent again.
|
|
822
|
+
check();
|
|
823
|
+
const { requestedPrivateDevice: _dropped, ...withoutPrivateDevice } = req;
|
|
824
|
+
await this.save({ ...this.state, consentRequest: withoutPrivateDevice });
|
|
825
|
+
check();
|
|
826
|
+
return await attempt();
|
|
827
|
+
}
|
|
740
828
|
check();
|
|
829
|
+
const { status, value: v } = reply;
|
|
741
830
|
object(v, "pairing_id,pairing_secret,verification_uri,expires_in,installation_device,app_id,app_origin,app_name");
|
|
742
831
|
let uri = null;
|
|
743
832
|
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 +865,10 @@ export class AppProtectedInstallationSignIn {
|
|
|
776
865
|
const done = v.status === "scope_updated";
|
|
777
866
|
const picked = done && (Object.hasOwn(v, "selected_collection_id") || Object.hasOwn(v, "created_collection_ids"));
|
|
778
867
|
const native = done && Object.hasOwn(v, "native_selection");
|
|
779
|
-
|
|
868
|
+
// The contract names the receipt list `private_device_collection_ids`; an
|
|
869
|
+
// additive receipt may mirror `added_collection_ids`. Exactly one is read.
|
|
870
|
+
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;
|
|
871
|
+
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
872
|
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
873
|
throw fail("binding");
|
|
782
874
|
const confirmation = portalMetadata(v);
|
|
@@ -792,7 +884,15 @@ export class AppProtectedInstallationSignIn {
|
|
|
792
884
|
check();
|
|
793
885
|
return this.collectionConsentView();
|
|
794
886
|
}
|
|
795
|
-
const
|
|
887
|
+
const addedCloudIds = AppProtectedInstallationSignIn.collectionIds(v.added_collection_ids);
|
|
888
|
+
const addedPrivateIds = deviceKey ? AppProtectedInstallationSignIn.collectionIds(v[deviceKey]) : [];
|
|
889
|
+
// Disjoint lists; only a window that asked; a scope keeps the mode it was approved with.
|
|
890
|
+
if (addedPrivateIds.length && (r.requestedPrivateDevice !== true || addedPrivateIds.some(c => addedCloudIds.includes(c) || (s.collectionIds.includes(c) && !s.privateDeviceCollectionIds?.includes(c)))))
|
|
891
|
+
throw fail("response");
|
|
892
|
+
if (addedCloudIds.some(c => s.privateDeviceCollectionIds?.includes(c)))
|
|
893
|
+
throw fail("response");
|
|
894
|
+
const addedCollectionIds = AppProtectedInstallationSignIn.collectionIds([...addedCloudIds, ...addedPrivateIds]);
|
|
895
|
+
const privateDeviceCollectionIds = [...new Set([...(s.privateDeviceCollectionIds ?? []), ...addedPrivateIds])];
|
|
796
896
|
if (typeof v.approved_create_collections !== "boolean" || (v.approved_create_collections && !r.requestedCreateCollections))
|
|
797
897
|
throw fail("response");
|
|
798
898
|
const collectionIds = AppProtectedInstallationSignIn.collectionIds([...new Set([...s.collectionIds, ...addedCollectionIds])]);
|
|
@@ -803,7 +903,8 @@ export class AppProtectedInstallationSignIn {
|
|
|
803
903
|
if (v.timer_consent === true && !collectionSelection)
|
|
804
904
|
throw fail("response");
|
|
805
905
|
const reminder = timerConsent(v, r.requestId, addedCollectionIds, confirmation.portalConfirmed === true, collectionSelection?.selectedCollectionId);
|
|
806
|
-
|
|
906
|
+
// A device-only receipt has no pick: an earlier request's pick is not this one's.
|
|
907
|
+
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
908
|
check();
|
|
808
909
|
return this.collectionConsentView();
|
|
809
910
|
});
|
|
@@ -815,13 +916,29 @@ export class AppProtectedInstallationSignIn {
|
|
|
815
916
|
return this.exclusive(async () => {
|
|
816
917
|
check();
|
|
817
918
|
let reply;
|
|
919
|
+
// `mode` is asked for only by an app that declares Private device access
|
|
920
|
+
// AND holds such a scope: only then can a Private row be offered at all
|
|
921
|
+
// (modeConsented). Every other installation sends today's exact request;
|
|
922
|
+
// a server from before this feature matches `include=file_actions` exactly
|
|
923
|
+
// and would silently drop the file actions from any other spelling.
|
|
924
|
+
const mode = this.options.freshRequestedPrivateDevice === true && (this.state.privateDeviceCollectionIds?.length ?? 0) > 0;
|
|
925
|
+
const today = "/v1/next/collections?include=file_actions", ask = (path) => this.json(path, false, undefined, { bearer: this.state.token, get: true });
|
|
818
926
|
try {
|
|
819
|
-
reply = await
|
|
927
|
+
reply = await ask(mode ? `${today},mode` : today);
|
|
928
|
+
// A server from before this feature (one rolled back under an SDK that
|
|
929
|
+
// holds Private scopes) answers the spelling it does not know with bare
|
|
930
|
+
// rows: no `mode` and no file actions. A server that knows `mode` puts
|
|
931
|
+
// it on every row. So rows that all lack it are asked for once more,
|
|
932
|
+
// with today's exact request, and the file actions survive the rollback.
|
|
933
|
+
if (mode && reply.status === 200 && modeUnanswered(reply.value)) {
|
|
934
|
+
check();
|
|
935
|
+
reply = await ask(today);
|
|
936
|
+
}
|
|
820
937
|
}
|
|
821
938
|
catch (error) {
|
|
822
939
|
check();
|
|
823
940
|
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([]) })));
|
|
941
|
+
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
942
|
if (error instanceof AppInstallationSignInError && (error.server || nonOkResponses.has(error)) && this.state.collectionListing !== undefined) {
|
|
826
943
|
const state = { ...this.state };
|
|
827
944
|
delete state.collectionListing;
|
|
@@ -835,7 +952,9 @@ export class AppProtectedInstallationSignIn {
|
|
|
835
952
|
object(v, "collections");
|
|
836
953
|
if (status !== 200)
|
|
837
954
|
throw fail("response");
|
|
838
|
-
|
|
955
|
+
// A listed mode never adds consent: a Private row is offered only with
|
|
956
|
+
// this installation's Private device scope, a cloud-copy row never with one.
|
|
957
|
+
const collections = Object.freeze(collectionListing(v.collections).filter(c => this.modeConsented(c)));
|
|
839
958
|
// Receipt references narrow replay; metadata cannot add consent or infer a role.
|
|
840
959
|
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
960
|
const state = { ...this.state };
|