@oxy.so/core 7.9.2 → 7.10.1
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/dist/cjs/api/storeInstaller.js +46 -0
- package/dist/cjs/index.js +10 -4
- package/dist/cjs/session/SessionClient.js +126 -12
- package/dist/esm/api/storeInstaller.js +40 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/session/SessionClient.js +126 -12
- package/dist/types/api/storeInstaller.d.ts +155 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/session/SessionClient.d.ts +8 -0
- package/package.json +1 -1
- package/src/api/storeInstaller.ts +158 -0
- package/src/index.ts +15 -0
- package/src/session/SessionClient.ts +149 -13
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The store's installer: how a store installs, opens and updates an application on a machine
|
|
4
|
+
* that installs applications, beside `oxy.store`, which reads the catalogue.
|
|
5
|
+
*
|
|
6
|
+
* `oxy.store` says what the store lists; an installer says what the machine can do with a
|
|
7
|
+
* listing. A runtime that installs applications registers one (OxyOS's Bloom host registers one
|
|
8
|
+
* backed by the system's install service before the application's code runs, OxyOS ADR 0030
|
|
9
|
+
* decision 9; ADR 0053 here). Where none is registered — the web, iOS, Android — there is
|
|
10
|
+
* nothing to install, and a store shows what it shows without one. An application never asks
|
|
11
|
+
* which platform it runs on: it asks whether there is an installer.
|
|
12
|
+
*
|
|
13
|
+
* The installer is the store's hands, never its judge. It is given a listing's slug and nothing
|
|
14
|
+
* else: the machine resolves slug → package → release from the store's signed metadata, verifies
|
|
15
|
+
* it, and asks the person in a sheet of its own that no application can draw or press. The store
|
|
16
|
+
* learns the outcome and nothing of the sheet. Its errors say whether the machine already told the
|
|
17
|
+
* person (`told`), so a store never repeats what the system said.
|
|
18
|
+
*
|
|
19
|
+
* React-free and dependency-free, as everything in `@oxy.so/core`.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.StoreInstallerError = void 0;
|
|
23
|
+
exports.setStoreInstaller = setStoreInstaller;
|
|
24
|
+
exports.getStoreInstaller = getStoreInstaller;
|
|
25
|
+
/** A failure of an installer's request. */
|
|
26
|
+
class StoreInstallerError extends Error {
|
|
27
|
+
constructor(reason, options = {}) {
|
|
28
|
+
super(options.message ?? reason);
|
|
29
|
+
this.name = 'StoreInstallerError';
|
|
30
|
+
this.reason = reason;
|
|
31
|
+
this.told = options.told ?? false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.StoreInstallerError = StoreInstallerError;
|
|
35
|
+
let installer = null;
|
|
36
|
+
/**
|
|
37
|
+
* Registers the machine's installer, or removes it with null. A runtime calls it once, before
|
|
38
|
+
* the application's code runs; nothing else should.
|
|
39
|
+
*/
|
|
40
|
+
function setStoreInstaller(next) {
|
|
41
|
+
installer = next;
|
|
42
|
+
}
|
|
43
|
+
/** The machine's installer, or null where nothing installs applications. */
|
|
44
|
+
function getStoreInstaller() {
|
|
45
|
+
return installer;
|
|
46
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
* If a symbol does not appear here, it is NOT part of the public API.
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.
|
|
25
|
-
exports.
|
|
26
|
-
exports.
|
|
27
|
-
exports.packageInfo = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.CENTRAL_IDP_APEX = exports.AUTH_WEB_ORIGIN = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = void 0;
|
|
24
|
+
exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.normalizeProfileLinks = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.commonsDeliveryPlatform = exports.pushTargetsFromDelivery = exports.selectCommonsDelivery = exports.parseCommonsApprovalExpiresAt = exports.getCommonsApprovalBlockingReason = exports.OxyAppDataIdentifierError = exports.AccountNotOnDeviceError = exports.REPUTATION_CACHE_PREFIX = exports.StoreInstallerError = exports.setStoreInstaller = exports.getStoreInstaller = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_CATEGORY_IDS = exports.buildUserDid = exports.DirectUploadInterruptedError = exports.MAX_UPLOAD_BYTES = exports.assetUrlCacheTTL = exports.ASSET_MULTIPART_UPLOAD_MAX_BYTES = exports.ServiceAssetMetadataError = exports.AssetUrlResolutionError = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyApiError = exports.OXY_CLOUD_URL = exports.OXY_API_URL = exports.OxyServices = void 0;
|
|
25
|
+
exports.isLocaleLoaded = exports.loadLocale = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.coerceToSupportedLocale = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = void 0;
|
|
26
|
+
exports.normalizeMultilineText = exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.accountNameInvalidKey = exports.accountNameInvalidMessage = exports.accountNameMaxLength = exports.isValidAccountName = exports.isValidDisplayName = exports.isValidPassword = exports.isValidEmail = exports.ACCOUNT_TITLE_INVALID_MESSAGE = exports.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = exports.PASSWORD_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.parseHttpErrorBody = exports.isHttpRequestError = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.trustTierLabel = exports.reputationCategoryLabel = exports.accountRoleLabel = exports.accountCategoryLabel = exports.getLocalesVersion = exports.subscribeLocales = void 0;
|
|
27
|
+
exports.packageInfo = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.CENTRAL_IDP_APEX = exports.AUTH_WEB_ORIGIN = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = void 0;
|
|
28
28
|
// The crypto shim MUST stay the first import. The root itself ships no crypto
|
|
29
29
|
// library (`__tests__/rootEntryGraph.test.ts`), but `@oxy.so/protocol`'s root
|
|
30
30
|
// entry (reached by the OAuth/PKCE and device helpers) does, and Metro does not
|
|
@@ -58,6 +58,12 @@ Object.defineProperty(exports, "MAX_ACCOUNT_CATEGORIES", { enumerable: true, get
|
|
|
58
58
|
Object.defineProperty(exports, "SELECTABLE_ACCOUNT_CATEGORY_IDS", { enumerable: true, get: function () { return accounts_1.SELECTABLE_ACCOUNT_CATEGORY_IDS; } });
|
|
59
59
|
Object.defineProperty(exports, "isSelectableAccountCategoryId", { enumerable: true, get: function () { return accounts_1.isSelectableAccountCategoryId; } });
|
|
60
60
|
Object.defineProperty(exports, "kindAcceptsAccountCategories", { enumerable: true, get: function () { return accounts_1.kindAcceptsAccountCategories; } });
|
|
61
|
+
// The store's installer (ADR 0053): what a runtime that installs applications registers beside
|
|
62
|
+
// `oxy.store`, and what a store asks it.
|
|
63
|
+
var storeInstaller_1 = require("./api/storeInstaller");
|
|
64
|
+
Object.defineProperty(exports, "getStoreInstaller", { enumerable: true, get: function () { return storeInstaller_1.getStoreInstaller; } });
|
|
65
|
+
Object.defineProperty(exports, "setStoreInstaller", { enumerable: true, get: function () { return storeInstaller_1.setStoreInstaller; } });
|
|
66
|
+
Object.defineProperty(exports, "StoreInstallerError", { enumerable: true, get: function () { return storeInstaller_1.StoreInstallerError; } });
|
|
61
67
|
var reputation_1 = require("./api/reputation");
|
|
62
68
|
Object.defineProperty(exports, "REPUTATION_CACHE_PREFIX", { enumerable: true, get: function () { return reputation_1.REPUTATION_CACHE_PREFIX; } });
|
|
63
69
|
var devices_1 = require("./api/devices");
|
|
@@ -39,6 +39,8 @@ class SessionClient {
|
|
|
39
39
|
this.tokenUnsub = null;
|
|
40
40
|
this.started = false;
|
|
41
41
|
this.lifecycleGeneration = 0;
|
|
42
|
+
/** Changes whenever the locally projected device changes, including ABA. */
|
|
43
|
+
this.deviceGeneration = 0;
|
|
42
44
|
this.fullSignOutPersistence = null;
|
|
43
45
|
/** Same-origin cross-tab state-propagation channel; null on platforms without BroadcastChannel. */
|
|
44
46
|
this.channel = null;
|
|
@@ -68,6 +70,25 @@ class SessionClient {
|
|
|
68
70
|
throw new Error('Device session request was superseded');
|
|
69
71
|
return response;
|
|
70
72
|
}
|
|
73
|
+
/** Capture the local device before a request, including device ABA. */
|
|
74
|
+
captureDeviceRequest() {
|
|
75
|
+
return { generation: this.deviceGeneration, initialDevice: this.state?.deviceId ?? null };
|
|
76
|
+
}
|
|
77
|
+
isCurrentDeviceRequest(request, response) {
|
|
78
|
+
if (request.generation === this.deviceGeneration)
|
|
79
|
+
return true;
|
|
80
|
+
if (request.initialDevice !== null || this.deviceGeneration !== request.generation + 1)
|
|
81
|
+
return false;
|
|
82
|
+
if (response === null || typeof response !== 'object')
|
|
83
|
+
return false;
|
|
84
|
+
const record = response;
|
|
85
|
+
const deviceId = record.state?.deviceId ?? record.directory?.deviceId ?? record.deviceId;
|
|
86
|
+
return typeof deviceId === 'string' && deviceId === this.state?.deviceId;
|
|
87
|
+
}
|
|
88
|
+
assertCurrentDeviceRequest(request, response) {
|
|
89
|
+
if (!this.isCurrentDeviceRequest(request, response))
|
|
90
|
+
throw new Error('Device session request was superseded');
|
|
91
|
+
}
|
|
71
92
|
/** Stop and discard only this client's local device projection; no server revocation. */
|
|
72
93
|
resetLocalState() {
|
|
73
94
|
this.lifecycleGeneration += 1;
|
|
@@ -216,6 +237,8 @@ class SessionClient {
|
|
|
216
237
|
return false;
|
|
217
238
|
}
|
|
218
239
|
const previousState = this.state;
|
|
240
|
+
if (previousState?.deviceId !== next.deviceId)
|
|
241
|
+
this.deviceGeneration += 1;
|
|
219
242
|
this.state = next;
|
|
220
243
|
if (next.accounts.length === 0) {
|
|
221
244
|
// A device with nobody signed in has no principals either. The directory
|
|
@@ -324,11 +347,13 @@ class SessionClient {
|
|
|
324
347
|
.ensureActiveToken(next)
|
|
325
348
|
.then(finishApply)
|
|
326
349
|
.catch((error) => {
|
|
327
|
-
if (generation !== this.lifecycleGeneration)
|
|
350
|
+
if (generation !== this.lifecycleGeneration || this.state !== next)
|
|
328
351
|
return;
|
|
329
352
|
logger_1.logger.warn('[SessionClient] ensureActiveToken failed — reverting session state', { component: 'SessionClient' }, error);
|
|
330
353
|
// Do NOT notify under a mismatched bearer. Revert to the last applied
|
|
331
354
|
// state so subscribers keep observing the account whose token is planted.
|
|
355
|
+
if (this.state?.deviceId !== previousState?.deviceId)
|
|
356
|
+
this.deviceGeneration += 1;
|
|
332
357
|
this.state = previousState ?? null;
|
|
333
358
|
});
|
|
334
359
|
}
|
|
@@ -378,6 +403,21 @@ class SessionClient {
|
|
|
378
403
|
if (this.fullSignOutPersistence?.state === this.state)
|
|
379
404
|
await this.fullSignOutPersistence.pending;
|
|
380
405
|
}
|
|
406
|
+
/** Keep mutation callers from reporting success after a device change during persistence. */
|
|
407
|
+
async applySyncForMutation(raw) {
|
|
408
|
+
const before = this.captureDeviceRequest();
|
|
409
|
+
const lifecycle = this.lifecycleGeneration;
|
|
410
|
+
const checked = (0, client_1.deviceSessionSyncCheck)(raw, []);
|
|
411
|
+
const responseDevice = checked.ok ? checked.value.state.deviceId : before.initialDevice;
|
|
412
|
+
const expectedGeneration = before.generation + (responseDevice === before.initialDevice ? 0 : 1);
|
|
413
|
+
const pending = this.applySync(raw);
|
|
414
|
+
const generation = this.deviceGeneration;
|
|
415
|
+
await pending;
|
|
416
|
+
if (generation !== expectedGeneration ||
|
|
417
|
+
generation !== this.deviceGeneration ||
|
|
418
|
+
lifecycle !== this.lifecycleGeneration)
|
|
419
|
+
throw new Error('Device session request was superseded');
|
|
420
|
+
}
|
|
381
421
|
/**
|
|
382
422
|
* The apply + token-plant half of {@link applySync}, on an ALREADY-VALIDATED
|
|
383
423
|
* sync. Split out so the context-aware removal lane — whose response is a
|
|
@@ -397,14 +437,15 @@ class SessionClient {
|
|
|
397
437
|
const applied = this.applyState(sync.state, 'request', sync.activeToken?.accessToken);
|
|
398
438
|
// Equal-revision restate (this revision was already applied by a preceding
|
|
399
439
|
// socket push): `applyState` no-ops without planting, but the token still
|
|
400
|
-
// needs planting.
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
// being the PINNED one (same rule as the applied path in `applyState`).
|
|
440
|
+
// needs planting. Only the exact same device revision is an equal-revision
|
|
441
|
+
// restate: an older response may identify a different operator of the same
|
|
442
|
+
// account. Also require the current account and the identity pin to match.
|
|
404
443
|
const pinnedAccountId = this.pinnedAccountId();
|
|
405
444
|
if (!applied &&
|
|
406
445
|
sync.activeToken &&
|
|
407
446
|
this.state &&
|
|
447
|
+
sync.state.deviceId === this.state.deviceId &&
|
|
448
|
+
sync.state.revision === this.state.revision &&
|
|
408
449
|
sync.state.activeAccountId !== null &&
|
|
409
450
|
sync.state.activeAccountId === this.state.activeAccountId &&
|
|
410
451
|
(pinnedAccountId === null || sync.state.activeAccountId === pinnedAccountId) &&
|
|
@@ -454,9 +495,12 @@ class SessionClient {
|
|
|
454
495
|
}
|
|
455
496
|
/** `GET /session/device/directory` → {@link applyDirectory}. No notify. */
|
|
456
497
|
async fetchDirectory() {
|
|
498
|
+
const request = this.captureDeviceRequest();
|
|
457
499
|
const res = await this.requestDevice('GET', '/session/device/directory', undefined, {
|
|
458
500
|
cache: false,
|
|
459
501
|
});
|
|
502
|
+
if (!this.isCurrentDeviceRequest(request, res))
|
|
503
|
+
return false;
|
|
460
504
|
return this.applyDirectory(res);
|
|
461
505
|
}
|
|
462
506
|
/**
|
|
@@ -518,14 +562,13 @@ class SessionClient {
|
|
|
518
562
|
if (!accessToken) {
|
|
519
563
|
return;
|
|
520
564
|
}
|
|
565
|
+
const pinnedAccountId = this.pinnedAccountId();
|
|
521
566
|
const subjectAccountId = (0, deviceDirectory_1.resolveActiveContext)(directory)?.subject.accountId ?? null;
|
|
522
567
|
if (subjectAccountId === null) {
|
|
523
568
|
return;
|
|
524
569
|
}
|
|
525
|
-
|
|
526
|
-
if (pinnedAccountId !== null && subjectAccountId !== pinnedAccountId) {
|
|
570
|
+
if (pinnedAccountId !== null && subjectAccountId !== pinnedAccountId)
|
|
527
571
|
return;
|
|
528
|
-
}
|
|
529
572
|
if (accessToken === this.host.getAccessToken()) {
|
|
530
573
|
return;
|
|
531
574
|
}
|
|
@@ -565,9 +608,12 @@ class SessionClient {
|
|
|
565
608
|
}
|
|
566
609
|
}
|
|
567
610
|
async bootstrap() {
|
|
611
|
+
const request = this.captureDeviceRequest();
|
|
568
612
|
const res = await this.requestDevice('GET', '/session/device/state', undefined, {
|
|
569
613
|
cache: false,
|
|
570
614
|
});
|
|
615
|
+
if (!this.isCurrentDeviceRequest(request, res))
|
|
616
|
+
return;
|
|
571
617
|
await this.applySync(res);
|
|
572
618
|
}
|
|
573
619
|
/**
|
|
@@ -611,13 +657,41 @@ class SessionClient {
|
|
|
611
657
|
* compatibility path for callers still keyed on account ids.
|
|
612
658
|
*/
|
|
613
659
|
async activateContext(contextId) {
|
|
660
|
+
const request = this.captureDeviceRequest();
|
|
614
661
|
const res = await this.requestDevice('POST', '/session/device/activate', { contextId }, { cache: false });
|
|
662
|
+
this.assertCurrentDeviceRequest(request, res);
|
|
615
663
|
const activation = (0, client_1.checkContract)(client_1.deviceActivateResponseCheck, res);
|
|
616
664
|
if (!activation) {
|
|
617
665
|
logger_1.logger.warn('[SessionClient] discarded invalid activation response');
|
|
618
666
|
return;
|
|
619
667
|
}
|
|
668
|
+
if (this.pinnedAccountId() === null) {
|
|
669
|
+
const active = (0, deviceDirectory_1.resolveActiveContext)(activation.directory);
|
|
670
|
+
if (!active || activation.directory.activeContextId !== contextId)
|
|
671
|
+
throw new Error('Device session request was superseded');
|
|
672
|
+
const claims = activation.activeToken?.accessToken
|
|
673
|
+
? (0, tokenClaims_1.decodeTokenClaims)(activation.activeToken.accessToken)
|
|
674
|
+
: null;
|
|
675
|
+
const contextClaim = claims?.device_context_id;
|
|
676
|
+
const actor = claims?.act;
|
|
677
|
+
const actorClaim = actor !== null && typeof actor === 'object' ? actor.sub : undefined;
|
|
678
|
+
const subjectClaim = claims?.userId ?? claims?.id ?? claims?.sub;
|
|
679
|
+
// Decoded claims are only a consistency check, never authorization.
|
|
680
|
+
// Legacy and opaque bearers may omit these claims entirely.
|
|
681
|
+
if ((typeof contextClaim === 'string' && contextClaim !== active.contextId) ||
|
|
682
|
+
(typeof actorClaim === 'string' && actorClaim !== active.actor.userId) ||
|
|
683
|
+
(typeof subjectClaim === 'string' && subjectClaim !== active.subject.accountId))
|
|
684
|
+
throw new Error('Device session request was superseded');
|
|
685
|
+
}
|
|
620
686
|
const known = this.knownRevisionFor(activation.directory.deviceId);
|
|
687
|
+
if (known !== null && activation.directory.revision < known)
|
|
688
|
+
throw new Error('Device session request was superseded');
|
|
689
|
+
const held = this.directory;
|
|
690
|
+
if (this.pinnedAccountId() === null &&
|
|
691
|
+
held?.deviceId === activation.directory.deviceId &&
|
|
692
|
+
held.revision === activation.directory.revision &&
|
|
693
|
+
held.activeContextId !== activation.directory.activeContextId)
|
|
694
|
+
throw new Error('Device session request was superseded');
|
|
621
695
|
const moved = known === null || activation.directory.revision > known;
|
|
622
696
|
this.plantActiveContextToken(activation.directory, activation.activeToken?.accessToken);
|
|
623
697
|
// Applied BEFORE the reconcile, and only then: the reconcile's own
|
|
@@ -627,8 +701,21 @@ class SessionClient {
|
|
|
627
701
|
// already holding in hand.
|
|
628
702
|
const applied = this.applyDirectory(activation.directory);
|
|
629
703
|
if (moved) {
|
|
704
|
+
const beforeReconcile = this.captureDeviceRequest();
|
|
705
|
+
const lifecycle = this.lifecycleGeneration;
|
|
630
706
|
await this.reconcileFlatState(activation.directory);
|
|
707
|
+
const landedOnTarget = beforeReconcile.initialDevice !== activation.directory.deviceId &&
|
|
708
|
+
this.deviceGeneration === beforeReconcile.generation + 1 &&
|
|
709
|
+
this.state?.deviceId === activation.directory.deviceId;
|
|
710
|
+
if (lifecycle !== this.lifecycleGeneration ||
|
|
711
|
+
(this.deviceGeneration !== beforeReconcile.generation && !landedOnTarget))
|
|
712
|
+
throw new Error('Device session request was superseded');
|
|
631
713
|
}
|
|
714
|
+
if (this.pinnedAccountId() === null &&
|
|
715
|
+
(this.knownRevisionFor(activation.directory.deviceId) !== activation.directory.revision ||
|
|
716
|
+
this.directory?.deviceId !== activation.directory.deviceId ||
|
|
717
|
+
this.directory.activeContextId !== contextId))
|
|
718
|
+
throw new Error('Device session request was superseded');
|
|
632
719
|
if (applied) {
|
|
633
720
|
this.notify();
|
|
634
721
|
}
|
|
@@ -637,8 +724,18 @@ class SessionClient {
|
|
|
637
724
|
}
|
|
638
725
|
}
|
|
639
726
|
async switchAccount(accountId) {
|
|
727
|
+
const request = this.captureDeviceRequest();
|
|
640
728
|
const res = await this.requestDevice('POST', '/session/device/switch', { accountId }, { cache: false });
|
|
641
|
-
|
|
729
|
+
this.assertCurrentDeviceRequest(request, res);
|
|
730
|
+
const checked = (0, client_1.deviceSessionSyncCheck)(res, []);
|
|
731
|
+
const isStale = () => checked.ok &&
|
|
732
|
+
this.state?.deviceId === checked.value.state.deviceId &&
|
|
733
|
+
this.state.revision > checked.value.state.revision;
|
|
734
|
+
if (isStale())
|
|
735
|
+
throw new Error('Device session request was superseded');
|
|
736
|
+
await this.applySyncForMutation(res);
|
|
737
|
+
if (isStale())
|
|
738
|
+
throw new Error('Device session request was superseded');
|
|
642
739
|
this.postCommitPing();
|
|
643
740
|
}
|
|
644
741
|
/**
|
|
@@ -653,10 +750,12 @@ class SessionClient {
|
|
|
653
750
|
* {@link signOutPrincipal} for the two that can tell those apart.
|
|
654
751
|
*/
|
|
655
752
|
async signOut(target) {
|
|
753
|
+
const request = this.captureDeviceRequest();
|
|
656
754
|
const res = await this.requestDevice('POST', '/session/device/signout', target, {
|
|
657
755
|
cache: false,
|
|
658
756
|
});
|
|
659
|
-
|
|
757
|
+
this.assertCurrentDeviceRequest(request, res);
|
|
758
|
+
await this.applySyncForMutation(res);
|
|
660
759
|
this.postCommitPing();
|
|
661
760
|
if (this.state?.accounts.length === 0)
|
|
662
761
|
await this.persistFullSignOut();
|
|
@@ -698,21 +797,34 @@ class SessionClient {
|
|
|
698
797
|
* applied this revision.
|
|
699
798
|
*/
|
|
700
799
|
async removeFromDevice(target) {
|
|
800
|
+
const request = this.captureDeviceRequest();
|
|
701
801
|
const res = await this.requestDevice('POST', '/session/device/signout', target, {
|
|
702
802
|
cache: false,
|
|
703
803
|
});
|
|
804
|
+
this.assertCurrentDeviceRequest(request, res);
|
|
704
805
|
const removal = (0, client_1.checkContract)(client_1.deviceDirectorySyncCheck, res);
|
|
705
806
|
if (!removal) {
|
|
706
807
|
logger_1.logger.warn('[SessionClient] discarded invalid device removal response');
|
|
707
808
|
return;
|
|
708
809
|
}
|
|
709
810
|
const directoryApplied = this.applyDirectory(removal.directory);
|
|
811
|
+
const beforeCommit = this.captureDeviceRequest();
|
|
812
|
+
const commitLifecycle = this.lifecycleGeneration;
|
|
813
|
+
const expectedGeneration = beforeCommit.generation + (beforeCommit.initialDevice === removal.state.deviceId ? 0 : 1);
|
|
710
814
|
const stateApplied = this.commitSync({
|
|
711
815
|
state: removal.state,
|
|
712
816
|
activeToken: removal.activeToken,
|
|
713
817
|
});
|
|
714
|
-
if (
|
|
818
|
+
if (expectedGeneration !== this.deviceGeneration ||
|
|
819
|
+
commitLifecycle !== this.lifecycleGeneration)
|
|
820
|
+
throw new Error('Device session request was superseded');
|
|
821
|
+
if (this.fullSignOutPersistence?.state === this.state) {
|
|
822
|
+
const generation = this.deviceGeneration;
|
|
823
|
+
const lifecycle = this.lifecycleGeneration;
|
|
715
824
|
await this.fullSignOutPersistence.pending;
|
|
825
|
+
if (generation !== this.deviceGeneration || lifecycle !== this.lifecycleGeneration)
|
|
826
|
+
throw new Error('Device session request was superseded');
|
|
827
|
+
}
|
|
716
828
|
// `commitSync` publishes whenever the flat state moved. When only the
|
|
717
829
|
// directory did — a socket push already applied this revision — the
|
|
718
830
|
// directory half would otherwise never reach a subscriber.
|
|
@@ -724,10 +836,12 @@ class SessionClient {
|
|
|
724
836
|
await this.persistFullSignOut();
|
|
725
837
|
}
|
|
726
838
|
async addCurrentAccount() {
|
|
839
|
+
const request = this.captureDeviceRequest();
|
|
727
840
|
const res = await this.requestDevice('POST', '/session/device/add', undefined, {
|
|
728
841
|
cache: false,
|
|
729
842
|
});
|
|
730
|
-
|
|
843
|
+
this.assertCurrentDeviceRequest(request, res);
|
|
844
|
+
await this.applySyncForMutation(res);
|
|
731
845
|
this.postCommitPing();
|
|
732
846
|
}
|
|
733
847
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store's installer: how a store installs, opens and updates an application on a machine
|
|
3
|
+
* that installs applications, beside `oxy.store`, which reads the catalogue.
|
|
4
|
+
*
|
|
5
|
+
* `oxy.store` says what the store lists; an installer says what the machine can do with a
|
|
6
|
+
* listing. A runtime that installs applications registers one (OxyOS's Bloom host registers one
|
|
7
|
+
* backed by the system's install service before the application's code runs, OxyOS ADR 0030
|
|
8
|
+
* decision 9; ADR 0053 here). Where none is registered — the web, iOS, Android — there is
|
|
9
|
+
* nothing to install, and a store shows what it shows without one. An application never asks
|
|
10
|
+
* which platform it runs on: it asks whether there is an installer.
|
|
11
|
+
*
|
|
12
|
+
* The installer is the store's hands, never its judge. It is given a listing's slug and nothing
|
|
13
|
+
* else: the machine resolves slug → package → release from the store's signed metadata, verifies
|
|
14
|
+
* it, and asks the person in a sheet of its own that no application can draw or press. The store
|
|
15
|
+
* learns the outcome and nothing of the sheet. Its errors say whether the machine already told the
|
|
16
|
+
* person (`told`), so a store never repeats what the system said.
|
|
17
|
+
*
|
|
18
|
+
* React-free and dependency-free, as everything in `@oxy.so/core`.
|
|
19
|
+
*/
|
|
20
|
+
/** A failure of an installer's request. */
|
|
21
|
+
export class StoreInstallerError extends Error {
|
|
22
|
+
constructor(reason, options = {}) {
|
|
23
|
+
super(options.message ?? reason);
|
|
24
|
+
this.name = 'StoreInstallerError';
|
|
25
|
+
this.reason = reason;
|
|
26
|
+
this.told = options.told ?? false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
let installer = null;
|
|
30
|
+
/**
|
|
31
|
+
* Registers the machine's installer, or removes it with null. A runtime calls it once, before
|
|
32
|
+
* the application's code runs; nothing else should.
|
|
33
|
+
*/
|
|
34
|
+
export function setStoreInstaller(next) {
|
|
35
|
+
installer = next;
|
|
36
|
+
}
|
|
37
|
+
/** The machine's installer, or null where nothing installs applications. */
|
|
38
|
+
export function getStoreInstaller() {
|
|
39
|
+
return installer;
|
|
40
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -34,6 +34,9 @@ export { ASSET_MULTIPART_UPLOAD_MAX_BYTES, assetUrlCacheTTL, MAX_UPLOAD_BYTES }
|
|
|
34
34
|
export { DirectUploadInterruptedError } from './api/directUpload.js';
|
|
35
35
|
export { buildUserDid } from './api/identity.js';
|
|
36
36
|
export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './api/accounts.js';
|
|
37
|
+
// The store's installer (ADR 0053): what a runtime that installs applications registers beside
|
|
38
|
+
// `oxy.store`, and what a store asks it.
|
|
39
|
+
export { getStoreInstaller, setStoreInstaller, StoreInstallerError, } from './api/storeInstaller.js';
|
|
37
40
|
export { REPUTATION_CACHE_PREFIX } from './api/reputation.js';
|
|
38
41
|
export { AccountNotOnDeviceError } from './api/devices.js';
|
|
39
42
|
export { OxyAppDataIdentifierError } from './api/appData.js';
|