@mdbase-dev/sdk 0.1.0-beta.168 → 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.
- 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/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.js +2 -2
- 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 +16 -1
- package/dist/app-host/installation-sign-in.js +134 -27
- 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 +9 -1
- package/dist/app-host/scoped-sign-in-session.js.map +1 -1
- 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.js +11 -6
- 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 +18 -1
- package/dist/application/phase.js +9 -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 +22 -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 +8394 -2001
- package/dist/application/worker.js.map +1 -1
- package/dist/browser/mdbase-sdk.js +8064 -3633
- package/dist/browser/mdbase-sdk.min.js +78 -78
- package/dist/describe-error.d.ts +2 -2
- package/dist/describe-error.js +9 -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/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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|