@glyphteck/veyl 0.63.7 → 0.65.0
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/account.js +3677 -308
- package/dist/accountprofiles.js +1362 -0
- package/dist/auth.js +26 -10
- package/dist/cli.js +4740 -773
- package/dist/index.js +4729 -768
- package/docs/api.md +12 -7
- package/docs/cli.md +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,1362 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
9
|
+
var __toESMCache_node;
|
|
10
|
+
var __toESMCache_esm;
|
|
11
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
12
|
+
var canCache = mod != null && typeof mod === "object";
|
|
13
|
+
if (canCache) {
|
|
14
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
15
|
+
var cached = cache.get(mod);
|
|
16
|
+
if (cached)
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
20
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
21
|
+
if (mod && typeof mod === "object" || typeof mod === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
31
|
+
return to;
|
|
32
|
+
};
|
|
33
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
+
|
|
35
|
+
// ../../core/config.js
|
|
36
|
+
var MS_PER_SECOND = 1000;
|
|
37
|
+
var MINUTE_MS = 60 * MS_PER_SECOND;
|
|
38
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
39
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
40
|
+
var KIB_BYTES = 1024;
|
|
41
|
+
var MIB_BYTES = 1024 * 1024;
|
|
42
|
+
var SATS_PER_BITCOIN = 100000000n;
|
|
43
|
+
var USERNAME_MAX_CHARS = 12;
|
|
44
|
+
var PASSWORD_MIN_CHARS = 15;
|
|
45
|
+
var PASSWORD_MAX_CHARS = 64;
|
|
46
|
+
var PASSWORD_MAX_BYTES = 4 * 1024;
|
|
47
|
+
var LOCAL_MEDIA_CACHE_MAX_BYTES = 512 * MIB_BYTES;
|
|
48
|
+
var LOCAL_PROFILE_CACHE_MAX_ITEMS = 500;
|
|
49
|
+
var LOCAL_PROFILE_CACHE_MAX_AGE_MS = 30 * DAY_MS;
|
|
50
|
+
var LOCAL_CHAT_CACHE_MAX_ITEMS = 1000;
|
|
51
|
+
var LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000;
|
|
52
|
+
var LOCAL_AVATAR_CACHE_MAX_BYTES = 128 * MIB_BYTES;
|
|
53
|
+
var LOCAL_AVATAR_CACHE_MAX_AGE_MS = 30 * DAY_MS;
|
|
54
|
+
var AVATAR_IMAGE_MAX_BYTES = 256 * KIB_BYTES;
|
|
55
|
+
var AVATAR_IMAGE_QUALITY_ATTEMPTS = Object.freeze([0.92, 0.84, 0.76, 0.68]);
|
|
56
|
+
var CHAT_AVATAR_IMAGE_MAX_BYTES = AVATAR_IMAGE_MAX_BYTES;
|
|
57
|
+
var CHAT_AVATAR_REF_MAX_CHARS = 256;
|
|
58
|
+
var CHAT_MESSAGE_FILE_CACHE_MAX_BYTES = 64 * MIB_BYTES;
|
|
59
|
+
var IDLE_CALLBACK_MIN_TIMEOUT_MS = 50;
|
|
60
|
+
var ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500;
|
|
61
|
+
var ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250;
|
|
62
|
+
var CHAT_UNSAVED_TTL_DAYS = 21;
|
|
63
|
+
var CHAT_UNSAVED_TTL_MS = CHAT_UNSAVED_TTL_DAYS * DAY_MS;
|
|
64
|
+
var CHAT_AFTER_SEEN_MS = DAY_MS;
|
|
65
|
+
var CHAT_MEDIA_TTL_DAYS = CHAT_UNSAVED_TTL_DAYS;
|
|
66
|
+
var CHAT_MEDIA_TTL_MS = CHAT_MEDIA_TTL_DAYS * DAY_MS;
|
|
67
|
+
var CHAT_UPLOAD_MAX_BYTES = 64 * MIB_BYTES;
|
|
68
|
+
var CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000;
|
|
69
|
+
var CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000;
|
|
70
|
+
var CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS = CHAT_AUDIO_TRANSCODE_BITRATE_BPS;
|
|
71
|
+
var CHAT_MESSAGE_BATCH_SIZE = 40;
|
|
72
|
+
var CHAT_MESSAGE_QUERY_MAX_DOCS = 120;
|
|
73
|
+
var CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256;
|
|
74
|
+
var CHAT_TTL_CLIENT_DELETE_GRACE_MS = MINUTE_MS;
|
|
75
|
+
var CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500;
|
|
76
|
+
var CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250;
|
|
77
|
+
var CHAT_LIST_PAGE_SIZE = 20;
|
|
78
|
+
var CHAT_LIST_LIVE_COUNT = CHAT_LIST_PAGE_SIZE;
|
|
79
|
+
var CHAT_INBOX_CURSOR_OVERLAP_MS = 5 * MS_PER_SECOND;
|
|
80
|
+
var CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500;
|
|
81
|
+
var CHAT_LIST_SNAPSHOT_COALESCE_MS = 80;
|
|
82
|
+
var CHAT_LIST_LISTENER_RETRY_MS = 1000;
|
|
83
|
+
var CHAT_TOP_WARM_COUNT = 0;
|
|
84
|
+
var CHAT_EAGER_WARM_COUNT = 0;
|
|
85
|
+
var CHAT_WARM_DELAY_MS = 3000;
|
|
86
|
+
var CHAT_WARM_BATCH_SIZE = CHAT_MESSAGE_BATCH_SIZE;
|
|
87
|
+
var CHAT_MESSAGE_VIEW_CACHE_SIZE = 30;
|
|
88
|
+
var CHAT_MEDIA_WARM_MESSAGES_PER_CHAT = CHAT_MESSAGE_BATCH_SIZE;
|
|
89
|
+
var CHAT_MEDIA_WARM_START_DELAY_MS = 600;
|
|
90
|
+
var CHAT_MEDIA_WARM_STEP_DELAY_MS = 120;
|
|
91
|
+
var CHAT_MEDIA_WARM_TYPES = Object.freeze(["img", "gif", "mp4"]);
|
|
92
|
+
var CHAT_MEDIA_WARM_MAX_BYTES = 0;
|
|
93
|
+
var CHAT_READ_WRITE_INTERVAL_MS = 2 * MS_PER_SECOND;
|
|
94
|
+
var CHAT_READ_WRITE_MAX_WAIT_MS = 8 * MS_PER_SECOND;
|
|
95
|
+
var CHAT_LIVE_PING_INTERVAL_MS = 5 * MS_PER_SECOND;
|
|
96
|
+
var CHAT_LIVE_SWEEP_INTERVAL_MS = 15 * MS_PER_SECOND;
|
|
97
|
+
var CHAT_LIVE_STALE_AFTER_MS = 15 * MS_PER_SECOND;
|
|
98
|
+
var CHAT_LIVE_RECONNECT_MAX_MS = 5 * MS_PER_SECOND;
|
|
99
|
+
var CHAT_LIVE_TYPING_IDLE_MS = 8 * MS_PER_SECOND;
|
|
100
|
+
var CHAT_LIVE_TYPING_RENEW_MS = 5 * MS_PER_SECOND;
|
|
101
|
+
var CHAT_LIVE_COMPOSITION_HANDOFF_MS = 15 * MS_PER_SECOND;
|
|
102
|
+
var CHAT_LIVE_READ_SEND_INTERVAL_MS = 120;
|
|
103
|
+
var CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500;
|
|
104
|
+
var CHAT_LIVE_READ_WRITE_INTERVAL_MS = 12 * MS_PER_SECOND;
|
|
105
|
+
var CHAT_LIVE_READ_WRITE_MAX_WAIT_MS = 30 * MS_PER_SECOND;
|
|
106
|
+
var CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12;
|
|
107
|
+
var CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS = 10 * MS_PER_SECOND;
|
|
108
|
+
var CHAT_SETTINGS_BODY_MAX_BYTES = 32 * KIB_BYTES;
|
|
109
|
+
var CHAT_MANIFEST_MAX_BYTES = 256 * KIB_BYTES;
|
|
110
|
+
var CHAT_RECEIPT_MAX_MEMBERS = 32;
|
|
111
|
+
var CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128;
|
|
112
|
+
var CHAT_MAX_MEMBERS = 256;
|
|
113
|
+
var CHAT_MAX_TEXT_CHARS = 2048;
|
|
114
|
+
var CHAT_TITLE_MAX_CHARS = 16;
|
|
115
|
+
var CHAT_MAX_REACTIONS = CHAT_MAX_MEMBERS;
|
|
116
|
+
var SEARCH_DEBOUNCE_MS = 300;
|
|
117
|
+
var RECENT_PEER_REFRESH_LIMIT = 50;
|
|
118
|
+
var RECENT_PEER_REFRESH_DELAY_MS = 250;
|
|
119
|
+
var RECENT_PEER_REFRESH_INTERVAL_MS = 5 * MINUTE_MS;
|
|
120
|
+
var RECENT_PEER_REFRESH_THROTTLE_MS = 120;
|
|
121
|
+
var BAN_REFRESH_GRACE_MS = 50;
|
|
122
|
+
var REQUEST_MONEY_MAX_SATS = SATS_PER_BITCOIN * 100000n;
|
|
123
|
+
var WALLET_TRANSFER_POLL_MS = 3 * MS_PER_SECOND;
|
|
124
|
+
var WALLET_ACTIVE_TRANSFER_REFRESH_MS = MS_PER_SECOND;
|
|
125
|
+
var WALLET_IDLE_ACTIVE_TRANSFER_REFRESH_MS = 5 * MS_PER_SECOND;
|
|
126
|
+
var WALLET_VISIBLE_TRANSFER_DISCOVERY_WINDOW_MS = 12 * MS_PER_SECOND;
|
|
127
|
+
var WALLET_ACTIVE_TRANSFER_EMPTY_SCAN_LIMIT = 8;
|
|
128
|
+
var WALLET_CACHE_HYDRATE_DELAY_MS = 0;
|
|
129
|
+
var WALLET_BOOT_REFRESH_DELAY_MS = 500;
|
|
130
|
+
var WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500;
|
|
131
|
+
var WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS = 20 * MS_PER_SECOND;
|
|
132
|
+
var WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS = MINUTE_MS;
|
|
133
|
+
var WALLET_BALANCE_EVENT_COALESCE_MS = 2 * MS_PER_SECOND;
|
|
134
|
+
var WALLET_INCOMING_UPDATE_COALESCE_MS = 250;
|
|
135
|
+
var WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000;
|
|
136
|
+
var WALLET_CLAIM_PAGE_SIZE = 100;
|
|
137
|
+
var WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50;
|
|
138
|
+
var WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2;
|
|
139
|
+
var WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50;
|
|
140
|
+
var WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS = 15 * MS_PER_SECOND;
|
|
141
|
+
var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3;
|
|
142
|
+
var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10;
|
|
143
|
+
var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2;
|
|
144
|
+
var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3;
|
|
145
|
+
var WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS = 4 * MS_PER_SECOND;
|
|
146
|
+
var WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS = 15 * MS_PER_SECOND;
|
|
147
|
+
var WALLET_SDK_BACKGROUND_QUIET_MS = MS_PER_SECOND;
|
|
148
|
+
var WALLET_RECENT_TRANSFER_LIMIT = 100;
|
|
149
|
+
var WALLET_TRANSFER_PAGE_LIMIT = 100;
|
|
150
|
+
var WALLET_TRANSFER_FETCH_THROTTLE_MS = 150;
|
|
151
|
+
var WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS = 3 * MS_PER_SECOND;
|
|
152
|
+
var WALLET_SENT_TRANSFER_REFRESH_DELAY_MS = 3 * MS_PER_SECOND;
|
|
153
|
+
var WALLET_PENDING_TRANSFER_BOOT_GRACE_MS = 15 * MS_PER_SECOND;
|
|
154
|
+
var WALLET_PENDING_TRANSFER_HOT_AGE_MS = MINUTE_MS;
|
|
155
|
+
var WALLET_PENDING_TRANSFER_WARM_AGE_MS = 10 * MINUTE_MS;
|
|
156
|
+
var WALLET_PENDING_TRANSFER_WARM_RETRY_MS = 15 * MS_PER_SECOND;
|
|
157
|
+
var WALLET_PENDING_TRANSFER_STALE_RETRY_MS = 2 * MINUTE_MS;
|
|
158
|
+
var WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
|
|
159
|
+
var WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
|
|
160
|
+
var WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
|
|
161
|
+
|
|
162
|
+
// ../../core/utils/text.js
|
|
163
|
+
function cleanText(value) {
|
|
164
|
+
return typeof value === "string" ? value.trim() : "";
|
|
165
|
+
}
|
|
166
|
+
function lowerText(value) {
|
|
167
|
+
return cleanText(value).toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
function sameText(left, right) {
|
|
170
|
+
return lowerText(left) === lowerText(right);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ../../core/avatar.js
|
|
174
|
+
var AVATAR_VERSION_RE = /^[0-9a-f]{64}$/;
|
|
175
|
+
function readAvatarVersion(value) {
|
|
176
|
+
const version = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
177
|
+
return AVATAR_VERSION_RE.test(version) ? version : null;
|
|
178
|
+
}
|
|
179
|
+
function cleanAvatarUsername(value) {
|
|
180
|
+
return cleanText(value).replace(/^@+/, "").slice(0, 40);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ../../core/crypto/core.js
|
|
184
|
+
"use client";
|
|
185
|
+
var encoder = new TextEncoder;
|
|
186
|
+
var decoder = new TextDecoder;
|
|
187
|
+
var MEM_KIB = 32 * 1024;
|
|
188
|
+
function randomBytes(length) {
|
|
189
|
+
const out = new Uint8Array(length);
|
|
190
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
191
|
+
globalThis.crypto.getRandomValues(out);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
throw new Error("crypto.getRandomValues is not available in this environment");
|
|
195
|
+
}
|
|
196
|
+
function toBytes(value, label = "bytes") {
|
|
197
|
+
if (value instanceof Uint8Array) {
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
if (typeof value === "string") {
|
|
201
|
+
return encoder.encode(value);
|
|
202
|
+
}
|
|
203
|
+
if (value instanceof ArrayBuffer) {
|
|
204
|
+
return new Uint8Array(value);
|
|
205
|
+
}
|
|
206
|
+
if (ArrayBuffer.isView(value)) {
|
|
207
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Invalid ${label}`);
|
|
210
|
+
}
|
|
211
|
+
function toBytes32(k, label = "key") {
|
|
212
|
+
if (k instanceof Uint8Array) {
|
|
213
|
+
if (k.length !== 32)
|
|
214
|
+
throw new Error(`Invalid ${label} key length`);
|
|
215
|
+
return k;
|
|
216
|
+
}
|
|
217
|
+
if (typeof k === "string") {
|
|
218
|
+
return fromHex(k, label);
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`${label} key must be Uint8Array or 64-char hex`);
|
|
221
|
+
}
|
|
222
|
+
function toHex(value) {
|
|
223
|
+
return Array.from(toBytes(value)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
224
|
+
}
|
|
225
|
+
function fromHexBytes(hex, label = "hex") {
|
|
226
|
+
if (typeof hex !== "string" || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) {
|
|
227
|
+
throw new Error(`Invalid ${label} hex`);
|
|
228
|
+
}
|
|
229
|
+
const out = new Uint8Array(hex.length / 2);
|
|
230
|
+
for (let i = 0;i < out.length; i += 1) {
|
|
231
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
function fromHex(hex, label = "hex") {
|
|
236
|
+
const bytes = fromHexBytes(hex, label);
|
|
237
|
+
if (bytes.length !== 32) {
|
|
238
|
+
throw new Error(`Invalid ${label} hex`);
|
|
239
|
+
}
|
|
240
|
+
return bytes;
|
|
241
|
+
}
|
|
242
|
+
function cleanBytes(...values) {
|
|
243
|
+
for (const value of values) {
|
|
244
|
+
try {
|
|
245
|
+
value?.fill?.(0);
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ../../core/account/profiles.js
|
|
251
|
+
var ACCOUNT_PROFILES_VERSION = 1;
|
|
252
|
+
var PROFILE_ID_RE = /^[0-9a-f]{32}$/;
|
|
253
|
+
var REGISTRY_FIELDS = new Set(["version", "environment", "selectedProfileId", "profiles"]);
|
|
254
|
+
var PROFILE_FIELDS = new Set([
|
|
255
|
+
"profileId",
|
|
256
|
+
"environment",
|
|
257
|
+
"uid",
|
|
258
|
+
"username",
|
|
259
|
+
"avatarVersion",
|
|
260
|
+
"createdAt",
|
|
261
|
+
"lastUsedAt"
|
|
262
|
+
]);
|
|
263
|
+
var INPUT_FIELDS = new Set(["environment", "uid", "username", "avatarVersion"]);
|
|
264
|
+
var PRESENTATION_FIELDS = new Set(["username", "avatarVersion"]);
|
|
265
|
+
function requireRecord(value, label) {
|
|
266
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
267
|
+
throw new Error(`${label} required`);
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
function requireFields(value, allowed, label) {
|
|
272
|
+
requireRecord(value, label);
|
|
273
|
+
for (const key of Object.keys(value)) {
|
|
274
|
+
if (!allowed.has(key))
|
|
275
|
+
throw new Error(`${label} field not allowed: ${key}`);
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
function requireEnvironment(value) {
|
|
280
|
+
const environment = lowerText(value);
|
|
281
|
+
if (!environment)
|
|
282
|
+
throw new Error("account profile environment required");
|
|
283
|
+
return environment;
|
|
284
|
+
}
|
|
285
|
+
function requireUid(value) {
|
|
286
|
+
const uid = cleanText(value);
|
|
287
|
+
if (!uid)
|
|
288
|
+
throw new Error("account profile uid required");
|
|
289
|
+
return uid;
|
|
290
|
+
}
|
|
291
|
+
function readAccountProfileId(value) {
|
|
292
|
+
const profileId = lowerText(value);
|
|
293
|
+
if (!PROFILE_ID_RE.test(profileId))
|
|
294
|
+
throw new Error("valid account profile id required");
|
|
295
|
+
return profileId;
|
|
296
|
+
}
|
|
297
|
+
function requireTime(value, label) {
|
|
298
|
+
const time = Number(value);
|
|
299
|
+
if (!Number.isSafeInteger(time) || time <= 0)
|
|
300
|
+
throw new Error(`${label} required`);
|
|
301
|
+
return time;
|
|
302
|
+
}
|
|
303
|
+
function currentTime(now = Date.now) {
|
|
304
|
+
return requireTime(typeof now === "function" ? now() : now, "account profile time");
|
|
305
|
+
}
|
|
306
|
+
function readUsername(value, strict = false) {
|
|
307
|
+
if (value == null || value === "")
|
|
308
|
+
return null;
|
|
309
|
+
const username = cleanAvatarUsername(value) || null;
|
|
310
|
+
if (strict && username !== value)
|
|
311
|
+
throw new Error("canonical account profile username required");
|
|
312
|
+
return username;
|
|
313
|
+
}
|
|
314
|
+
function readProfileAvatarVersion(value, strict = false) {
|
|
315
|
+
if (value == null || value === "")
|
|
316
|
+
return null;
|
|
317
|
+
const avatarVersion = readAvatarVersion(value);
|
|
318
|
+
if (!avatarVersion || strict && avatarVersion !== value) {
|
|
319
|
+
throw new Error("valid account profile avatar version required");
|
|
320
|
+
}
|
|
321
|
+
return avatarVersion;
|
|
322
|
+
}
|
|
323
|
+
function freezeProfile(profile) {
|
|
324
|
+
return Object.freeze(profile);
|
|
325
|
+
}
|
|
326
|
+
function freezeRegistry(registry) {
|
|
327
|
+
return Object.freeze({
|
|
328
|
+
...registry,
|
|
329
|
+
profiles: Object.freeze(registry.profiles)
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function readStoredProfile(value, environment) {
|
|
333
|
+
requireFields(value, PROFILE_FIELDS, "account profile");
|
|
334
|
+
if (Object.keys(value).length !== PROFILE_FIELDS.size) {
|
|
335
|
+
throw new Error("account profile fields incomplete");
|
|
336
|
+
}
|
|
337
|
+
const storedEnvironment = requireEnvironment(value.environment);
|
|
338
|
+
if (storedEnvironment !== value.environment || storedEnvironment !== environment) {
|
|
339
|
+
throw new Error("account profile environment mismatch");
|
|
340
|
+
}
|
|
341
|
+
const uid = requireUid(value.uid);
|
|
342
|
+
if (uid !== value.uid)
|
|
343
|
+
throw new Error("canonical account profile uid required");
|
|
344
|
+
return freezeProfile({
|
|
345
|
+
profileId: readAccountProfileId(value.profileId),
|
|
346
|
+
environment,
|
|
347
|
+
uid,
|
|
348
|
+
username: readUsername(value.username, true),
|
|
349
|
+
avatarVersion: readProfileAvatarVersion(value.avatarVersion, true),
|
|
350
|
+
createdAt: requireTime(value.createdAt, "account profile created time"),
|
|
351
|
+
lastUsedAt: requireTime(value.lastUsedAt, "account profile last-used time")
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function profileInput(value, environment) {
|
|
355
|
+
requireFields(value, INPUT_FIELDS, "account profile input");
|
|
356
|
+
const inputEnvironment = Object.hasOwn(value, "environment") ? requireEnvironment(value.environment) : environment;
|
|
357
|
+
if (inputEnvironment !== environment)
|
|
358
|
+
throw new Error("account profile environment mismatch");
|
|
359
|
+
return {
|
|
360
|
+
environment,
|
|
361
|
+
uid: requireUid(value.uid),
|
|
362
|
+
username: Object.hasOwn(value, "username") ? readUsername(value.username) : undefined,
|
|
363
|
+
avatarVersion: Object.hasOwn(value, "avatarVersion") ? readProfileAvatarVersion(value.avatarVersion) : undefined
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function normalizedRegistry(value) {
|
|
367
|
+
return readAccountProfiles(value, value?.environment);
|
|
368
|
+
}
|
|
369
|
+
function replaceProfile(registry, profile) {
|
|
370
|
+
return freezeRegistry({
|
|
371
|
+
...registry,
|
|
372
|
+
profiles: registry.profiles.map((item) => item.profileId === profile.profileId ? profile : item)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function makeAccountProfileId() {
|
|
376
|
+
return toHex(randomBytes(16));
|
|
377
|
+
}
|
|
378
|
+
function emptyAccountProfiles(environment) {
|
|
379
|
+
return freezeRegistry({
|
|
380
|
+
version: ACCOUNT_PROFILES_VERSION,
|
|
381
|
+
environment: requireEnvironment(environment),
|
|
382
|
+
selectedProfileId: null,
|
|
383
|
+
profiles: []
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
function readAccountProfiles(value, environment) {
|
|
387
|
+
const expectedEnvironment = requireEnvironment(environment);
|
|
388
|
+
if (value == null)
|
|
389
|
+
return emptyAccountProfiles(expectedEnvironment);
|
|
390
|
+
requireFields(value, REGISTRY_FIELDS, "account profile registry");
|
|
391
|
+
if (Object.keys(value).length !== REGISTRY_FIELDS.size) {
|
|
392
|
+
throw new Error("account profile registry fields incomplete");
|
|
393
|
+
}
|
|
394
|
+
if (value.version !== ACCOUNT_PROFILES_VERSION) {
|
|
395
|
+
throw new Error("unsupported account profile registry version");
|
|
396
|
+
}
|
|
397
|
+
const storedEnvironment = requireEnvironment(value.environment);
|
|
398
|
+
if (storedEnvironment !== value.environment || storedEnvironment !== expectedEnvironment) {
|
|
399
|
+
throw new Error("account profile registry environment mismatch");
|
|
400
|
+
}
|
|
401
|
+
if (!Array.isArray(value.profiles))
|
|
402
|
+
throw new Error("account profiles required");
|
|
403
|
+
const profiles = value.profiles.map((profile) => readStoredProfile(profile, expectedEnvironment));
|
|
404
|
+
const profileIds = new Set;
|
|
405
|
+
const accountUids = new Set;
|
|
406
|
+
for (const profile of profiles) {
|
|
407
|
+
if (profileIds.has(profile.profileId))
|
|
408
|
+
throw new Error("duplicate account profile id");
|
|
409
|
+
if (accountUids.has(profile.uid))
|
|
410
|
+
throw new Error("duplicate account profile uid");
|
|
411
|
+
profileIds.add(profile.profileId);
|
|
412
|
+
accountUids.add(profile.uid);
|
|
413
|
+
}
|
|
414
|
+
const selectedProfileId = value.selectedProfileId == null ? null : readAccountProfileId(value.selectedProfileId);
|
|
415
|
+
if (selectedProfileId && !profileIds.has(selectedProfileId)) {
|
|
416
|
+
throw new Error("selected account profile missing");
|
|
417
|
+
}
|
|
418
|
+
return freezeRegistry({
|
|
419
|
+
version: ACCOUNT_PROFILES_VERSION,
|
|
420
|
+
environment: expectedEnvironment,
|
|
421
|
+
selectedProfileId,
|
|
422
|
+
profiles
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function accountProfileById(value, profileId) {
|
|
426
|
+
const registry = normalizedRegistry(value);
|
|
427
|
+
const id = readAccountProfileId(profileId);
|
|
428
|
+
return registry.profiles.find((profile) => profile.profileId === id) || null;
|
|
429
|
+
}
|
|
430
|
+
function accountProfilesByRecency(value) {
|
|
431
|
+
const registry = normalizedRegistry(value);
|
|
432
|
+
return Object.freeze([...registry.profiles].sort((left, right) => right.lastUsedAt - left.lastUsedAt || left.profileId.localeCompare(right.profileId)));
|
|
433
|
+
}
|
|
434
|
+
function putAccountProfile(value, fields, options = {}) {
|
|
435
|
+
const registry = normalizedRegistry(value);
|
|
436
|
+
const input = profileInput(fields, registry.environment);
|
|
437
|
+
const existing = registry.profiles.find((profile2) => profile2.uid === input.uid) || null;
|
|
438
|
+
if (existing) {
|
|
439
|
+
const profile2 = freezeProfile({
|
|
440
|
+
...existing,
|
|
441
|
+
username: input.username === undefined ? existing.username : input.username,
|
|
442
|
+
avatarVersion: input.avatarVersion === undefined ? existing.avatarVersion : input.avatarVersion
|
|
443
|
+
});
|
|
444
|
+
return Object.freeze({ registry: replaceProfile(registry, profile2), profile: profile2, created: false });
|
|
445
|
+
}
|
|
446
|
+
const createProfileId = options.createProfileId || makeAccountProfileId;
|
|
447
|
+
if (typeof createProfileId !== "function")
|
|
448
|
+
throw new Error("account profile id factory required");
|
|
449
|
+
const profileId = readAccountProfileId(createProfileId());
|
|
450
|
+
if (registry.profiles.some((profile2) => profile2.profileId === profileId)) {
|
|
451
|
+
throw new Error("account profile id collision");
|
|
452
|
+
}
|
|
453
|
+
const time = currentTime(options.now);
|
|
454
|
+
const profile = freezeProfile({
|
|
455
|
+
profileId,
|
|
456
|
+
environment: registry.environment,
|
|
457
|
+
uid: input.uid,
|
|
458
|
+
username: input.username ?? null,
|
|
459
|
+
avatarVersion: input.avatarVersion ?? null,
|
|
460
|
+
createdAt: time,
|
|
461
|
+
lastUsedAt: time
|
|
462
|
+
});
|
|
463
|
+
return Object.freeze({
|
|
464
|
+
registry: freezeRegistry({ ...registry, profiles: [...registry.profiles, profile] }),
|
|
465
|
+
profile,
|
|
466
|
+
created: true
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function updateAccountProfile(value, profileId, fields) {
|
|
470
|
+
const registry = normalizedRegistry(value);
|
|
471
|
+
requireFields(fields, PRESENTATION_FIELDS, "account profile presentation");
|
|
472
|
+
const existing = accountProfileById(registry, profileId);
|
|
473
|
+
if (!existing)
|
|
474
|
+
throw new Error("account profile not found");
|
|
475
|
+
const profile = freezeProfile({
|
|
476
|
+
...existing,
|
|
477
|
+
username: Object.hasOwn(fields, "username") ? readUsername(fields.username) : existing.username,
|
|
478
|
+
avatarVersion: Object.hasOwn(fields, "avatarVersion") ? readProfileAvatarVersion(fields.avatarVersion) : existing.avatarVersion
|
|
479
|
+
});
|
|
480
|
+
return Object.freeze({ registry: replaceProfile(registry, profile), profile });
|
|
481
|
+
}
|
|
482
|
+
function selectAccountProfile(value, profileId, options = {}) {
|
|
483
|
+
const registry = normalizedRegistry(value);
|
|
484
|
+
const existing = accountProfileById(registry, profileId);
|
|
485
|
+
if (!existing)
|
|
486
|
+
throw new Error("account profile not found");
|
|
487
|
+
const selected = freezeProfile({ ...existing, lastUsedAt: currentTime(options.now) });
|
|
488
|
+
return freezeRegistry({
|
|
489
|
+
...registry,
|
|
490
|
+
selectedProfileId: selected.profileId,
|
|
491
|
+
profiles: registry.profiles.map((profile) => profile.profileId === selected.profileId ? selected : profile)
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function removeAccountProfile(value, profileId) {
|
|
495
|
+
const registry = normalizedRegistry(value);
|
|
496
|
+
const existing = accountProfileById(registry, profileId);
|
|
497
|
+
if (!existing)
|
|
498
|
+
throw new Error("account profile not found");
|
|
499
|
+
return freezeRegistry({
|
|
500
|
+
...registry,
|
|
501
|
+
selectedProfileId: registry.selectedProfileId === existing.profileId ? null : registry.selectedProfileId,
|
|
502
|
+
profiles: registry.profiles.filter((profile) => profile.profileId !== existing.profileId)
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
function deselectAccountProfile(value, profileId) {
|
|
506
|
+
const registry = normalizedRegistry(value);
|
|
507
|
+
const id = readAccountProfileId(profileId);
|
|
508
|
+
if (registry.selectedProfileId !== id)
|
|
509
|
+
return registry;
|
|
510
|
+
return freezeRegistry({ ...registry, selectedProfileId: null });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/accountprofiles.js
|
|
514
|
+
var ACCOUNT_PROFILE_STALE_CODE = "account-profiles/stale";
|
|
515
|
+
var ACCOUNT_PROFILE_AUTH_REQUIRED_CODE = "account-profiles/auth-required";
|
|
516
|
+
var NOOP = () => {};
|
|
517
|
+
var NOOP_RELEASE = () => false;
|
|
518
|
+
function beginAccountProfileTransition(runtime) {
|
|
519
|
+
if (!runtime)
|
|
520
|
+
return NOOP_RELEASE;
|
|
521
|
+
if (typeof runtime.beginTransition !== "function") {
|
|
522
|
+
throw new Error("account profile transition barrier required");
|
|
523
|
+
}
|
|
524
|
+
return runtime.beginTransition();
|
|
525
|
+
}
|
|
526
|
+
function createAccountProfileRuntimeLifecycle(account) {
|
|
527
|
+
if (!account || typeof account.close !== "function" || typeof account.closeBarrier?.acquire !== "function" || typeof account.closeBarrier?.release !== "function" || typeof account.closeBarrier?.prepare !== "function") {
|
|
528
|
+
throw new Error("account profile runtime lifecycle ports required");
|
|
529
|
+
}
|
|
530
|
+
let closePromise = null;
|
|
531
|
+
let closeLease = null;
|
|
532
|
+
let closeStarted = false;
|
|
533
|
+
return Object.freeze({
|
|
534
|
+
beginTransition() {
|
|
535
|
+
if (closeStarted || closeLease) {
|
|
536
|
+
throw new Error("account profile transition already started");
|
|
537
|
+
}
|
|
538
|
+
const lease = account.closeBarrier.acquire();
|
|
539
|
+
closeLease = lease;
|
|
540
|
+
let released = false;
|
|
541
|
+
return () => {
|
|
542
|
+
if (released || closeStarted)
|
|
543
|
+
return false;
|
|
544
|
+
released = true;
|
|
545
|
+
const didRelease = account.closeBarrier.release(lease);
|
|
546
|
+
if (didRelease && closeLease === lease)
|
|
547
|
+
closeLease = null;
|
|
548
|
+
return didRelease;
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
async lock() {
|
|
552
|
+
if (closePromise)
|
|
553
|
+
return closePromise;
|
|
554
|
+
closeStarted = true;
|
|
555
|
+
await account.closeBarrier.prepare(closeLease);
|
|
556
|
+
},
|
|
557
|
+
close() {
|
|
558
|
+
if (!closePromise) {
|
|
559
|
+
closeStarted = true;
|
|
560
|
+
closePromise = Promise.resolve().then(() => account.close());
|
|
561
|
+
}
|
|
562
|
+
return closePromise;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function requirePort(value, message) {
|
|
567
|
+
if (typeof value !== "function")
|
|
568
|
+
throw new Error(message);
|
|
569
|
+
return value;
|
|
570
|
+
}
|
|
571
|
+
function staleTransitionError(cause = null) {
|
|
572
|
+
const error = new Error("account profile transition superseded");
|
|
573
|
+
error.code = ACCOUNT_PROFILE_STALE_CODE;
|
|
574
|
+
if (cause)
|
|
575
|
+
error.cause = cause;
|
|
576
|
+
return error;
|
|
577
|
+
}
|
|
578
|
+
function authenticationRequiredError() {
|
|
579
|
+
const error = new Error("account profile authentication required");
|
|
580
|
+
error.code = ACCOUNT_PROFILE_AUTH_REQUIRED_CODE;
|
|
581
|
+
return error;
|
|
582
|
+
}
|
|
583
|
+
function isStaleAccountProfileTransition(error) {
|
|
584
|
+
return error?.code === ACCOUNT_PROFILE_STALE_CODE;
|
|
585
|
+
}
|
|
586
|
+
function requirePorts(options) {
|
|
587
|
+
const environment = emptyAccountProfiles(options?.environment).environment;
|
|
588
|
+
const storage = options?.storage;
|
|
589
|
+
requirePort(storage?.read, "account profile storage read port required");
|
|
590
|
+
requirePort(storage?.update, "account profile storage update port required");
|
|
591
|
+
requirePort(storage?.subscribe, "account profile storage subscribe port required");
|
|
592
|
+
return {
|
|
593
|
+
environment,
|
|
594
|
+
storage,
|
|
595
|
+
resolveAuthenticatedProfile: requirePort(options?.resolveAuthenticatedProfile, "account profile authentication resolver required"),
|
|
596
|
+
openProfile: requirePort(options?.openProfile, "account profile open port required"),
|
|
597
|
+
clearNotification: requirePort(options?.clearNotification, "account profile notification-clear port required"),
|
|
598
|
+
dropPush: requirePort(options?.dropPush, "account profile push-drop port required"),
|
|
599
|
+
removeProfile: requirePort(options?.removeProfile, "account profile removal port required"),
|
|
600
|
+
discardProfile: requirePort(options?.discardProfile, "account profile discard port required"),
|
|
601
|
+
signOutProfile: requirePort(options?.signOutProfile, "account profile sign-out port required"),
|
|
602
|
+
createProfileId: options.createProfileId,
|
|
603
|
+
now: options.now,
|
|
604
|
+
onError: typeof options.onError === "function" ? options.onError : null
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
async function resolveAuthenticatedProfile(owner, generation, reason) {
|
|
608
|
+
ensureGeneration(owner, generation);
|
|
609
|
+
const registry = owner.registry;
|
|
610
|
+
const context = Object.freeze({
|
|
611
|
+
environment: registry.environment,
|
|
612
|
+
generation,
|
|
613
|
+
reason,
|
|
614
|
+
requestedProfileId: owner.requestedProfileId,
|
|
615
|
+
isCurrent: () => !owner.closing && !owner.closed && owner.generation === generation
|
|
616
|
+
});
|
|
617
|
+
const profileId = await owner.ports.resolveAuthenticatedProfile(registry, context);
|
|
618
|
+
ensureGeneration(owner, generation);
|
|
619
|
+
if (profileId == null)
|
|
620
|
+
return null;
|
|
621
|
+
const profile = accountProfileById(registry, profileId);
|
|
622
|
+
if (!profile)
|
|
623
|
+
throw new Error("authenticated account profile not found");
|
|
624
|
+
return profile;
|
|
625
|
+
}
|
|
626
|
+
function publicSnapshot(owner) {
|
|
627
|
+
return Object.freeze({
|
|
628
|
+
ready: owner.ready,
|
|
629
|
+
environment: owner.registry.environment,
|
|
630
|
+
profiles: accountProfilesByRecency(owner.registry),
|
|
631
|
+
selectedProfileId: owner.registry.selectedProfileId,
|
|
632
|
+
activeProfileId: owner.active?.profile.profileId || null,
|
|
633
|
+
status: owner.status,
|
|
634
|
+
generation: owner.generation,
|
|
635
|
+
error: owner.error
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function publish(owner, status = owner.status, error = owner.error) {
|
|
639
|
+
owner.status = status;
|
|
640
|
+
owner.error = error;
|
|
641
|
+
owner.snapshot = publicSnapshot(owner);
|
|
642
|
+
for (const listener of owner.listeners)
|
|
643
|
+
listener();
|
|
644
|
+
return owner.snapshot;
|
|
645
|
+
}
|
|
646
|
+
function enqueue(owner, work) {
|
|
647
|
+
const operation = owner.tail.then(work, work);
|
|
648
|
+
owner.tail = operation.catch(NOOP);
|
|
649
|
+
return operation;
|
|
650
|
+
}
|
|
651
|
+
function ensureAvailable(owner) {
|
|
652
|
+
if (owner.closing || owner.closed)
|
|
653
|
+
throw new Error("account profiles closed");
|
|
654
|
+
}
|
|
655
|
+
function ensureReady(owner) {
|
|
656
|
+
ensureAvailable(owner);
|
|
657
|
+
if (!owner.ready)
|
|
658
|
+
throw new Error("account profiles not ready");
|
|
659
|
+
}
|
|
660
|
+
function requestSelection(owner, profileId, invalidateActive = true) {
|
|
661
|
+
ensureAvailable(owner);
|
|
662
|
+
owner.generation += 1;
|
|
663
|
+
owner.requestedProfileId = profileId;
|
|
664
|
+
if (owner.openingLease)
|
|
665
|
+
owner.openingLease.valid = false;
|
|
666
|
+
if (invalidateActive && profileId && owner.active?.profile.profileId !== profileId) {
|
|
667
|
+
if (owner.active)
|
|
668
|
+
owner.active.lease.valid = false;
|
|
669
|
+
}
|
|
670
|
+
publish(owner);
|
|
671
|
+
return owner.generation;
|
|
672
|
+
}
|
|
673
|
+
function bindSelection(owner, generation, profileId) {
|
|
674
|
+
ensureGeneration(owner, generation);
|
|
675
|
+
owner.requestedProfileId = profileId;
|
|
676
|
+
if (owner.active?.profile.profileId !== profileId && owner.active) {
|
|
677
|
+
owner.active.lease.valid = false;
|
|
678
|
+
}
|
|
679
|
+
publish(owner);
|
|
680
|
+
}
|
|
681
|
+
function ensureGeneration(owner, generation) {
|
|
682
|
+
if (owner.closing || owner.closed || owner.generation !== generation) {
|
|
683
|
+
throw staleTransitionError();
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function isRequested(owner, generation, profileId) {
|
|
687
|
+
return !owner.closing && !owner.closed && owner.generation === generation && owner.requestedProfileId === profileId;
|
|
688
|
+
}
|
|
689
|
+
function ensureRequested(owner, generation, profileId) {
|
|
690
|
+
if (!isRequested(owner, generation, profileId))
|
|
691
|
+
throw staleTransitionError();
|
|
692
|
+
}
|
|
693
|
+
function transitionContext(owner, lease, reason, details = {}) {
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
generation: lease.generation,
|
|
696
|
+
profileId: lease.profileId,
|
|
697
|
+
reason,
|
|
698
|
+
...details,
|
|
699
|
+
isCurrent: () => !owner.closing && !owner.closed && lease.valid
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function report(owner, operation, error, profile = null) {
|
|
703
|
+
try {
|
|
704
|
+
owner.ports.onError?.(error, { operation, profile });
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
707
|
+
async function cleanupBinding(owner, binding, context) {
|
|
708
|
+
const profile = binding?.profile || null;
|
|
709
|
+
let failure = null;
|
|
710
|
+
const attempt = async (operation, work) => {
|
|
711
|
+
try {
|
|
712
|
+
await work();
|
|
713
|
+
} catch (error) {
|
|
714
|
+
failure ||= error;
|
|
715
|
+
report(owner, operation, error, profile);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
if (binding) {
|
|
719
|
+
binding.lease.valid = false;
|
|
720
|
+
await attempt("lock", () => binding.runtime.lock());
|
|
721
|
+
}
|
|
722
|
+
await attempt("clearNotification", () => owner.ports.clearNotification(profile, context));
|
|
723
|
+
if (binding) {
|
|
724
|
+
try {
|
|
725
|
+
await owner.ports.dropPush(profile, binding.runtime, context);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
report(owner, "dropPush", error, profile);
|
|
728
|
+
}
|
|
729
|
+
await attempt("close", () => binding.runtime.close());
|
|
730
|
+
}
|
|
731
|
+
if (failure)
|
|
732
|
+
throw failure;
|
|
733
|
+
}
|
|
734
|
+
async function deactivate(owner, context) {
|
|
735
|
+
const binding = owner.active;
|
|
736
|
+
owner.active = null;
|
|
737
|
+
if (binding)
|
|
738
|
+
binding.lease.valid = false;
|
|
739
|
+
publish(owner);
|
|
740
|
+
await cleanupBinding(owner, binding, context);
|
|
741
|
+
}
|
|
742
|
+
function requireRuntime(runtime) {
|
|
743
|
+
if (!runtime || typeof runtime.lock !== "function" || typeof runtime.close !== "function") {
|
|
744
|
+
throw new Error("opened account profile requires lock and close");
|
|
745
|
+
}
|
|
746
|
+
return runtime;
|
|
747
|
+
}
|
|
748
|
+
async function discardCandidate(owner, profile, runtime, lease, reason, cause = null) {
|
|
749
|
+
if (runtime && typeof runtime.lock === "function" && typeof runtime.close === "function") {
|
|
750
|
+
const context = transitionContext(owner, lease, reason);
|
|
751
|
+
try {
|
|
752
|
+
await cleanupBinding(owner, { profile, runtime, lease }, context);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
throw staleTransitionError(error);
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
try {
|
|
758
|
+
await runtime?.close?.();
|
|
759
|
+
} catch (error) {
|
|
760
|
+
cause ||= error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
throw staleTransitionError(cause);
|
|
764
|
+
}
|
|
765
|
+
async function finalizeOpen(owner, finalize, context, error = null) {
|
|
766
|
+
if (typeof finalize !== "function")
|
|
767
|
+
return error;
|
|
768
|
+
try {
|
|
769
|
+
await finalize(context, error);
|
|
770
|
+
return error;
|
|
771
|
+
} catch (cleanupError) {
|
|
772
|
+
report(owner, "discardProfile", cleanupError, null);
|
|
773
|
+
if (error && !cleanupError.cause)
|
|
774
|
+
cleanupError.cause = error;
|
|
775
|
+
return cleanupError;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function openTarget(owner, profile, generation, reason, options = {}) {
|
|
779
|
+
ensureRequested(owner, generation, profile.profileId);
|
|
780
|
+
const lease = { generation, profileId: profile.profileId, valid: true };
|
|
781
|
+
const context = transitionContext(owner, lease, reason, options.details);
|
|
782
|
+
owner.openingLease = lease;
|
|
783
|
+
let runtime;
|
|
784
|
+
try {
|
|
785
|
+
runtime = await owner.ports.openProfile(profile, context);
|
|
786
|
+
} catch (error) {
|
|
787
|
+
if (owner.openingLease === lease)
|
|
788
|
+
owner.openingLease = null;
|
|
789
|
+
const failure = await finalizeOpen(owner, options.finalize, context, error);
|
|
790
|
+
if (!context.isCurrent() || !isRequested(owner, generation, profile.profileId)) {
|
|
791
|
+
throw staleTransitionError(failure);
|
|
792
|
+
}
|
|
793
|
+
throw failure;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
requireRuntime(runtime);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
lease.valid = false;
|
|
799
|
+
if (owner.openingLease === lease)
|
|
800
|
+
owner.openingLease = null;
|
|
801
|
+
const failure = await finalizeOpen(owner, options.finalize, context, error);
|
|
802
|
+
await runtime?.close?.();
|
|
803
|
+
throw failure;
|
|
804
|
+
}
|
|
805
|
+
const finalizationError = await finalizeOpen(owner, options.finalize, context);
|
|
806
|
+
if (finalizationError) {
|
|
807
|
+
if (owner.openingLease === lease)
|
|
808
|
+
owner.openingLease = null;
|
|
809
|
+
try {
|
|
810
|
+
await cleanupBinding(owner, { profile, runtime, lease }, context);
|
|
811
|
+
} catch (cleanupError) {
|
|
812
|
+
finalizationError.cause ||= cleanupError;
|
|
813
|
+
}
|
|
814
|
+
throw finalizationError;
|
|
815
|
+
}
|
|
816
|
+
if (!context.isCurrent() || !isRequested(owner, generation, profile.profileId)) {
|
|
817
|
+
if (owner.openingLease === lease)
|
|
818
|
+
owner.openingLease = null;
|
|
819
|
+
return discardCandidate(owner, profile, runtime, lease, "stale", null);
|
|
820
|
+
}
|
|
821
|
+
owner.openingLease = null;
|
|
822
|
+
owner.active = { profile, runtime, lease };
|
|
823
|
+
publish(owner, "idle", null);
|
|
824
|
+
return runtime;
|
|
825
|
+
}
|
|
826
|
+
async function mutateRegistry(owner, update) {
|
|
827
|
+
owner.storageGeneration += 1;
|
|
828
|
+
owner.storageUpdating = true;
|
|
829
|
+
try {
|
|
830
|
+
const committed = await owner.ports.storage.update(owner.ports.environment, (stored) => {
|
|
831
|
+
const current = readAccountProfiles(stored, owner.ports.environment);
|
|
832
|
+
return update(current);
|
|
833
|
+
});
|
|
834
|
+
owner.registry = readAccountProfiles(committed, owner.ports.environment);
|
|
835
|
+
publish(owner);
|
|
836
|
+
return owner.registry;
|
|
837
|
+
} finally {
|
|
838
|
+
owner.storageUpdating = false;
|
|
839
|
+
owner.storageGeneration += 1;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function canonicalSelection(registry, profileId) {
|
|
843
|
+
if (!profileId || registry.selectedProfileId !== profileId)
|
|
844
|
+
return null;
|
|
845
|
+
return accountProfileById(registry, profileId);
|
|
846
|
+
}
|
|
847
|
+
function observationIsCurrent(owner, observation) {
|
|
848
|
+
return !owner.closing && !owner.closed && !owner.storageUpdating && owner.storageGeneration === observation.storageGeneration && owner.generation === observation.generation;
|
|
849
|
+
}
|
|
850
|
+
function preemptNoncanonicalBindings(owner, registry, observation) {
|
|
851
|
+
if (!observationIsCurrent(owner, observation))
|
|
852
|
+
return;
|
|
853
|
+
const active = owner.active;
|
|
854
|
+
if (active && !canonicalSelection(registry, active.profile.profileId))
|
|
855
|
+
active.lease.valid = false;
|
|
856
|
+
const opening = owner.openingLease;
|
|
857
|
+
if (!opening || canonicalSelection(registry, opening.profileId))
|
|
858
|
+
return;
|
|
859
|
+
opening.valid = false;
|
|
860
|
+
if (owner.generation === opening.generation && owner.requestedProfileId === opening.profileId) {
|
|
861
|
+
owner.generation += 1;
|
|
862
|
+
owner.requestedProfileId = null;
|
|
863
|
+
publish(owner);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function reconcileRequestedProfile(owner, registry, retiredProfileId) {
|
|
867
|
+
const profileId = owner.requestedProfileId;
|
|
868
|
+
if (!profileId)
|
|
869
|
+
return;
|
|
870
|
+
const pending = owner.reservations.has(profileId) || owner.openingLease?.profileId === profileId;
|
|
871
|
+
const stillStored = accountProfileById(registry, profileId);
|
|
872
|
+
if (!pending && (!stillStored || profileId === retiredProfileId && registry.selectedProfileId !== profileId)) {
|
|
873
|
+
owner.requestedProfileId = null;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function reconcileProfileRegistry(owner, registry, observation) {
|
|
877
|
+
ensureAvailable(owner);
|
|
878
|
+
if (!observationIsCurrent(owner, observation))
|
|
879
|
+
return false;
|
|
880
|
+
const active = owner.active;
|
|
881
|
+
const activeProfileId = active?.profile.profileId || null;
|
|
882
|
+
const currentProfile = canonicalSelection(registry, activeProfileId);
|
|
883
|
+
owner.registry = registry;
|
|
884
|
+
reconcileRequestedProfile(owner, registry, currentProfile ? null : activeProfileId);
|
|
885
|
+
if (currentProfile) {
|
|
886
|
+
active.profile = currentProfile;
|
|
887
|
+
publish(owner, "idle", null);
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
if (!active) {
|
|
891
|
+
publish(owner, "idle", null);
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
active.lease.valid = false;
|
|
895
|
+
publish(owner, "refreshing", null);
|
|
896
|
+
try {
|
|
897
|
+
await deactivate(owner, transitionContext(owner, active.lease, "refreshing"));
|
|
898
|
+
} catch (error) {
|
|
899
|
+
publish(owner, "error", error);
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
if (owner.generation === observation.generation)
|
|
903
|
+
publish(owner, "idle", null);
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
async function refreshProfileRegistry(owner, requestId) {
|
|
907
|
+
while (owner.refreshRequested === requestId) {
|
|
908
|
+
ensureAvailable(owner);
|
|
909
|
+
const observation = {
|
|
910
|
+
generation: owner.generation,
|
|
911
|
+
storageGeneration: owner.storageGeneration
|
|
912
|
+
};
|
|
913
|
+
const stored = await owner.ports.storage.read(owner.ports.environment);
|
|
914
|
+
const registry = readAccountProfiles(stored, owner.ports.environment);
|
|
915
|
+
if (owner.refreshRequested !== requestId)
|
|
916
|
+
return owner.snapshot;
|
|
917
|
+
preemptNoncanonicalBindings(owner, registry, observation);
|
|
918
|
+
const applied = await enqueue(owner, () => owner.refreshRequested === requestId ? reconcileProfileRegistry(owner, registry, observation) : false);
|
|
919
|
+
if (applied)
|
|
920
|
+
return owner.snapshot;
|
|
921
|
+
}
|
|
922
|
+
return owner.snapshot;
|
|
923
|
+
}
|
|
924
|
+
function requestProfileRefresh(owner) {
|
|
925
|
+
ensureAvailable(owner);
|
|
926
|
+
const requestId = owner.refreshRequested + 1;
|
|
927
|
+
owner.refreshRequested = requestId;
|
|
928
|
+
const operation = refreshProfileRegistry(owner, requestId).then((snapshot) => owner.refreshRequested === requestId ? snapshot : owner.refreshPromise || owner.snapshot);
|
|
929
|
+
const tracked = operation.finally(() => {
|
|
930
|
+
if (owner.refreshPromise === tracked)
|
|
931
|
+
owner.refreshPromise = null;
|
|
932
|
+
});
|
|
933
|
+
owner.refreshPromise = tracked;
|
|
934
|
+
return tracked;
|
|
935
|
+
}
|
|
936
|
+
async function transitionTo(owner, profile, generation, reason, options = {}) {
|
|
937
|
+
ensureRequested(owner, generation, profile.profileId);
|
|
938
|
+
const active = owner.active;
|
|
939
|
+
if (!options.force && active?.profile.profileId === profile.profileId && active.lease.valid) {
|
|
940
|
+
const selected2 = await mutateRegistry(owner, (current) => selectAccountProfile(current, profile.profileId, { now: owner.ports.now }));
|
|
941
|
+
active.profile = accountProfileById(selected2, profile.profileId);
|
|
942
|
+
publish(owner, "idle", null);
|
|
943
|
+
return active.runtime;
|
|
944
|
+
}
|
|
945
|
+
publish(owner, reason, null);
|
|
946
|
+
const retiringLease = active?.lease || { generation, profileId: null, valid: false };
|
|
947
|
+
if (active || options.clearEmpty !== false) {
|
|
948
|
+
await deactivate(owner, transitionContext(owner, retiringLease, reason));
|
|
949
|
+
}
|
|
950
|
+
ensureRequested(owner, generation, profile.profileId);
|
|
951
|
+
const selected = await mutateRegistry(owner, (current) => selectAccountProfile(current, profile.profileId, { now: owner.ports.now }));
|
|
952
|
+
ensureRequested(owner, generation, profile.profileId);
|
|
953
|
+
return openTarget(owner, accountProfileById(selected, profile.profileId), generation, reason, { details: options.openDetails, finalize: options.finalizeOpen });
|
|
954
|
+
}
|
|
955
|
+
async function applyAuthenticatedProfile(owner, profile, generation, reason, options = {}) {
|
|
956
|
+
ensureGeneration(owner, generation);
|
|
957
|
+
if (profile) {
|
|
958
|
+
bindSelection(owner, generation, profile.profileId);
|
|
959
|
+
await transitionTo(owner, profile, generation, reason, {
|
|
960
|
+
clearEmpty: options.clearEmpty
|
|
961
|
+
});
|
|
962
|
+
return accountProfileById(owner.registry, profile.profileId);
|
|
963
|
+
}
|
|
964
|
+
owner.requestedProfileId = null;
|
|
965
|
+
const lease = owner.active?.lease || { generation, profileId: null, valid: false };
|
|
966
|
+
if (owner.active)
|
|
967
|
+
owner.active.lease.valid = false;
|
|
968
|
+
if (owner.active || options.clearEmpty !== false) {
|
|
969
|
+
await deactivate(owner, transitionContext(owner, lease, reason));
|
|
970
|
+
}
|
|
971
|
+
ensureGeneration(owner, generation);
|
|
972
|
+
if (owner.registry.selectedProfileId) {
|
|
973
|
+
const selectedProfileId = owner.registry.selectedProfileId;
|
|
974
|
+
await mutateRegistry(owner, (current) => deselectAccountProfile(current, selectedProfileId));
|
|
975
|
+
ensureGeneration(owner, generation);
|
|
976
|
+
}
|
|
977
|
+
publish(owner, "idle", null);
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
async function resumeAuthenticatedProfile(owner, generation, reason, options = {}) {
|
|
981
|
+
publish(owner, reason, null);
|
|
982
|
+
const profile = await resolveAuthenticatedProfile(owner, generation, reason);
|
|
983
|
+
return applyAuthenticatedProfile(owner, profile, generation, reason, options);
|
|
984
|
+
}
|
|
985
|
+
function transitionFailure(owner, error, generation, profileId) {
|
|
986
|
+
if (isRequested(owner, generation, profileId))
|
|
987
|
+
publish(owner, "error", error);
|
|
988
|
+
return error;
|
|
989
|
+
}
|
|
990
|
+
async function initialize(owner) {
|
|
991
|
+
try {
|
|
992
|
+
const stored = await owner.ports.storage.read(owner.ports.environment);
|
|
993
|
+
owner.registry = readAccountProfiles(stored, owner.ports.environment);
|
|
994
|
+
const profile = await resolveAuthenticatedProfile(owner, 0, "restoring");
|
|
995
|
+
owner.ready = true;
|
|
996
|
+
publish(owner, "restoring", null);
|
|
997
|
+
if (owner.closing || owner.closed || owner.generation !== 0)
|
|
998
|
+
return owner.snapshot;
|
|
999
|
+
await applyAuthenticatedProfile(owner, profile, 0, "restoring");
|
|
1000
|
+
return owner.snapshot;
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
owner.ready = true;
|
|
1003
|
+
if (isStaleAccountProfileTransition(error))
|
|
1004
|
+
return owner.snapshot;
|
|
1005
|
+
if (!owner.closing && !owner.closed)
|
|
1006
|
+
publish(owner, "error", error);
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function requestAuthenticatedProfileResume(owner) {
|
|
1011
|
+
ensureReady(owner);
|
|
1012
|
+
const generation = requestSelection(owner, null, false);
|
|
1013
|
+
return enqueue(owner, async () => {
|
|
1014
|
+
try {
|
|
1015
|
+
return await resumeAuthenticatedProfile(owner, generation, "restoring");
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (!isStaleAccountProfileTransition(error))
|
|
1018
|
+
publish(owner, "error", error);
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
function reservedProfileId(owner) {
|
|
1024
|
+
const createProfileId = owner.ports.createProfileId || makeAccountProfileId;
|
|
1025
|
+
if (typeof createProfileId !== "function")
|
|
1026
|
+
throw new Error("account profile id factory required");
|
|
1027
|
+
const profileId = readAccountProfileId(createProfileId());
|
|
1028
|
+
if (accountProfileById(owner.registry, profileId))
|
|
1029
|
+
throw new Error("account profile id collision");
|
|
1030
|
+
return profileId;
|
|
1031
|
+
}
|
|
1032
|
+
async function discardReservation(owner, reservation, activation, reason) {
|
|
1033
|
+
const lease = {
|
|
1034
|
+
generation: reservation.generation,
|
|
1035
|
+
profileId: reservation.profileId,
|
|
1036
|
+
valid: isRequested(owner, reservation.generation, reservation.profileId)
|
|
1037
|
+
};
|
|
1038
|
+
await owner.ports.discardProfile(reservation.profileId, activation, transitionContext(owner, lease, reason));
|
|
1039
|
+
}
|
|
1040
|
+
async function activateReservation(owner, reservation, fields, activation) {
|
|
1041
|
+
const { generation, profileId: provisionalProfileId } = reservation;
|
|
1042
|
+
let durable = false;
|
|
1043
|
+
let finalProfileId = null;
|
|
1044
|
+
let discarded = false;
|
|
1045
|
+
const discard = async (reason) => {
|
|
1046
|
+
if (discarded)
|
|
1047
|
+
return;
|
|
1048
|
+
await discardReservation(owner, reservation, activation, reason);
|
|
1049
|
+
discarded = true;
|
|
1050
|
+
};
|
|
1051
|
+
try {
|
|
1052
|
+
ensureGeneration(owner, generation);
|
|
1053
|
+
publish(owner, "switching", null);
|
|
1054
|
+
let authenticatedUid = null;
|
|
1055
|
+
const committed = await mutateRegistry(owner, (current) => {
|
|
1056
|
+
const result = putAccountProfile(current, fields, {
|
|
1057
|
+
createProfileId: () => provisionalProfileId,
|
|
1058
|
+
now: owner.ports.now
|
|
1059
|
+
});
|
|
1060
|
+
authenticatedUid = result.profile.uid;
|
|
1061
|
+
return result.registry;
|
|
1062
|
+
});
|
|
1063
|
+
const profile = committed.profiles.find((item) => item.uid === authenticatedUid) || null;
|
|
1064
|
+
if (!profile)
|
|
1065
|
+
throw new Error("committed account profile missing");
|
|
1066
|
+
finalProfileId = profile.profileId;
|
|
1067
|
+
durable = finalProfileId === provisionalProfileId;
|
|
1068
|
+
ensureGeneration(owner, generation);
|
|
1069
|
+
bindSelection(owner, generation, finalProfileId);
|
|
1070
|
+
const openDetails = { provisionalProfileId };
|
|
1071
|
+
if (activation !== undefined)
|
|
1072
|
+
openDetails.activation = activation;
|
|
1073
|
+
await transitionTo(owner, profile, generation, "switching", {
|
|
1074
|
+
force: true,
|
|
1075
|
+
openDetails,
|
|
1076
|
+
finalizeOpen: finalProfileId === provisionalProfileId ? null : () => discard("deduplicated")
|
|
1077
|
+
});
|
|
1078
|
+
return accountProfileById(owner.registry, finalProfileId);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
let failure = error;
|
|
1081
|
+
if (!durable || finalProfileId !== provisionalProfileId) {
|
|
1082
|
+
try {
|
|
1083
|
+
await discard("failed");
|
|
1084
|
+
} catch (cleanupError) {
|
|
1085
|
+
cleanupError.cause ||= error;
|
|
1086
|
+
failure = cleanupError;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
throw transitionFailure(owner, failure, generation, owner.requestedProfileId);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function beginAddProfile(owner) {
|
|
1093
|
+
ensureReady(owner);
|
|
1094
|
+
const profileId = reservedProfileId(owner);
|
|
1095
|
+
owner.reservations.add(profileId);
|
|
1096
|
+
const generation = requestSelection(owner, profileId, false);
|
|
1097
|
+
let settled = false;
|
|
1098
|
+
const reservation = Object.freeze({
|
|
1099
|
+
profileId,
|
|
1100
|
+
generation,
|
|
1101
|
+
isCurrent: () => !settled && isRequested(owner, generation, profileId),
|
|
1102
|
+
activate(fields, activation) {
|
|
1103
|
+
if (settled)
|
|
1104
|
+
return Promise.reject(new Error("account profile add reservation settled"));
|
|
1105
|
+
settled = true;
|
|
1106
|
+
return enqueue(owner, () => activateReservation(owner, { generation, profileId }, fields, activation)).finally(() => owner.reservations.delete(profileId));
|
|
1107
|
+
},
|
|
1108
|
+
cancel(activation) {
|
|
1109
|
+
if (settled)
|
|
1110
|
+
return Promise.resolve(false);
|
|
1111
|
+
settled = true;
|
|
1112
|
+
const current = isRequested(owner, generation, profileId);
|
|
1113
|
+
if (current) {
|
|
1114
|
+
owner.generation += 1;
|
|
1115
|
+
owner.requestedProfileId = owner.active?.profile.profileId || null;
|
|
1116
|
+
if (owner.openingLease?.profileId === profileId)
|
|
1117
|
+
owner.openingLease.valid = false;
|
|
1118
|
+
publish(owner);
|
|
1119
|
+
}
|
|
1120
|
+
return enqueue(owner, async () => {
|
|
1121
|
+
await discardReservation(owner, { generation, profileId }, activation, "cancelled");
|
|
1122
|
+
return current;
|
|
1123
|
+
}).finally(() => owner.reservations.delete(profileId));
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
return reservation;
|
|
1127
|
+
}
|
|
1128
|
+
async function switchExisting(owner, profileId, generation) {
|
|
1129
|
+
try {
|
|
1130
|
+
ensureRequested(owner, generation, profileId);
|
|
1131
|
+
const profile = accountProfileById(owner.registry, profileId);
|
|
1132
|
+
if (!profile)
|
|
1133
|
+
throw new Error("account profile not found");
|
|
1134
|
+
const authenticated = await resolveAuthenticatedProfile(owner, generation, "switching");
|
|
1135
|
+
ensureRequested(owner, generation, profileId);
|
|
1136
|
+
if (authenticated?.profileId !== profileId) {
|
|
1137
|
+
owner.requestedProfileId = owner.active?.profile.profileId || null;
|
|
1138
|
+
const error = authenticationRequiredError();
|
|
1139
|
+
publish(owner, "error", error);
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
if (owner.active && owner.active.profile.profileId !== profileId)
|
|
1143
|
+
owner.active.lease.valid = false;
|
|
1144
|
+
await transitionTo(owner, profile, generation, "switching");
|
|
1145
|
+
return accountProfileById(owner.registry, profileId);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
throw transitionFailure(owner, error, generation, profileId);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function updateStoredProfile(owner, profileId, fields) {
|
|
1151
|
+
ensureAvailable(owner);
|
|
1152
|
+
const registry = await mutateRegistry(owner, (current) => updateAccountProfile(current, profileId, fields).registry);
|
|
1153
|
+
const profile = accountProfileById(registry, profileId);
|
|
1154
|
+
if (owner.active?.profile.profileId === profileId)
|
|
1155
|
+
owner.active.profile = profile;
|
|
1156
|
+
publish(owner, owner.status, null);
|
|
1157
|
+
return profile;
|
|
1158
|
+
}
|
|
1159
|
+
async function signOutStoredProfile(owner, profile) {
|
|
1160
|
+
try {
|
|
1161
|
+
publish(owner, "signing-out", null);
|
|
1162
|
+
if (!owner.active || owner.active.profile.profileId === profile.profileId) {
|
|
1163
|
+
const lease = owner.active?.lease || {
|
|
1164
|
+
generation: owner.generation,
|
|
1165
|
+
profileId: profile.profileId,
|
|
1166
|
+
valid: false
|
|
1167
|
+
};
|
|
1168
|
+
await deactivate(owner, transitionContext(owner, lease, "signing-out"));
|
|
1169
|
+
}
|
|
1170
|
+
const signOutLease = {
|
|
1171
|
+
generation: owner.generation,
|
|
1172
|
+
profileId: profile.profileId,
|
|
1173
|
+
valid: true
|
|
1174
|
+
};
|
|
1175
|
+
let signOutFailure = null;
|
|
1176
|
+
try {
|
|
1177
|
+
await owner.ports.signOutProfile(profile, transitionContext(owner, signOutLease, "signing-out"));
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
signOutFailure = error;
|
|
1180
|
+
report(owner, "signOutProfile", error, profile);
|
|
1181
|
+
}
|
|
1182
|
+
await mutateRegistry(owner, (current) => current.selectedProfileId === profile.profileId ? deselectAccountProfile(current, profile.profileId) : current);
|
|
1183
|
+
if (owner.requestedProfileId === profile.profileId)
|
|
1184
|
+
owner.requestedProfileId = null;
|
|
1185
|
+
await resumeAuthenticatedProfile(owner, owner.generation, "restoring", { clearEmpty: false });
|
|
1186
|
+
if (signOutFailure)
|
|
1187
|
+
throw signOutFailure;
|
|
1188
|
+
return profile;
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
publish(owner, "error", error);
|
|
1191
|
+
throw error;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function requestProfileSignOut(owner) {
|
|
1195
|
+
ensureReady(owner);
|
|
1196
|
+
const profileId = owner.active?.profile.profileId || owner.registry.selectedProfileId;
|
|
1197
|
+
const profile = profileId ? accountProfileById(owner.registry, profileId) : null;
|
|
1198
|
+
if (!profile)
|
|
1199
|
+
return Promise.reject(new Error("active account profile required"));
|
|
1200
|
+
owner.generation += 1;
|
|
1201
|
+
owner.requestedProfileId = null;
|
|
1202
|
+
if (owner.openingLease)
|
|
1203
|
+
owner.openingLease.valid = false;
|
|
1204
|
+
if (owner.active)
|
|
1205
|
+
owner.active.lease.valid = false;
|
|
1206
|
+
publish(owner);
|
|
1207
|
+
return enqueue(owner, () => signOutStoredProfile(owner, profile));
|
|
1208
|
+
}
|
|
1209
|
+
async function removeStoredProfile(owner, profileId) {
|
|
1210
|
+
ensureAvailable(owner);
|
|
1211
|
+
const profile = accountProfileById(owner.registry, profileId);
|
|
1212
|
+
if (!profile)
|
|
1213
|
+
throw new Error("account profile not found");
|
|
1214
|
+
const wasActive = owner.active?.profile.profileId === profileId;
|
|
1215
|
+
try {
|
|
1216
|
+
publish(owner, "removing", null);
|
|
1217
|
+
if (wasActive) {
|
|
1218
|
+
owner.active.lease.valid = false;
|
|
1219
|
+
const lease = owner.active.lease;
|
|
1220
|
+
await deactivate(owner, transitionContext(owner, lease, "removing"));
|
|
1221
|
+
}
|
|
1222
|
+
const removalLease = { generation: owner.generation, profileId, valid: true };
|
|
1223
|
+
await owner.ports.removeProfile(profile, transitionContext(owner, removalLease, "removing"));
|
|
1224
|
+
const registry = await mutateRegistry(owner, (current) => accountProfileById(current, profileId) ? removeAccountProfile(current, profileId) : current);
|
|
1225
|
+
if (owner.requestedProfileId === profileId) {
|
|
1226
|
+
owner.requestedProfileId = owner.active?.profile.profileId || registry.selectedProfileId;
|
|
1227
|
+
}
|
|
1228
|
+
if (wasActive) {
|
|
1229
|
+
await resumeAuthenticatedProfile(owner, owner.generation, "restoring", { clearEmpty: false });
|
|
1230
|
+
} else
|
|
1231
|
+
publish(owner, "idle", null);
|
|
1232
|
+
return profile;
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
let failure = error;
|
|
1235
|
+
if (wasActive && !owner.active) {
|
|
1236
|
+
try {
|
|
1237
|
+
await resumeAuthenticatedProfile(owner, owner.generation, "restoring", { clearEmpty: false });
|
|
1238
|
+
} catch (recoveryError) {
|
|
1239
|
+
recoveryError.cause ||= error;
|
|
1240
|
+
failure = recoveryError;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
publish(owner, "error", failure);
|
|
1244
|
+
throw failure;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function requestProfileRemoval(owner, profileId) {
|
|
1248
|
+
ensureAvailable(owner);
|
|
1249
|
+
if (owner.ready && !accountProfileById(owner.registry, profileId)) {
|
|
1250
|
+
return Promise.reject(new Error("account profile not found"));
|
|
1251
|
+
}
|
|
1252
|
+
if (owner.openingLease?.profileId === profileId || owner.requestedProfileId === profileId) {
|
|
1253
|
+
owner.generation += 1;
|
|
1254
|
+
owner.requestedProfileId = owner.active?.profile.profileId || null;
|
|
1255
|
+
if (owner.openingLease?.profileId === profileId)
|
|
1256
|
+
owner.openingLease.valid = false;
|
|
1257
|
+
publish(owner);
|
|
1258
|
+
}
|
|
1259
|
+
if (owner.active?.profile.profileId === profileId)
|
|
1260
|
+
owner.active.lease.valid = false;
|
|
1261
|
+
return enqueue(owner, () => removeStoredProfile(owner, profileId));
|
|
1262
|
+
}
|
|
1263
|
+
function closeProfiles(owner) {
|
|
1264
|
+
if (owner.closePromise)
|
|
1265
|
+
return owner.closePromise;
|
|
1266
|
+
owner.closing = true;
|
|
1267
|
+
owner.stopStorage();
|
|
1268
|
+
owner.stopStorage = NOOP;
|
|
1269
|
+
owner.generation += 1;
|
|
1270
|
+
owner.requestedProfileId = null;
|
|
1271
|
+
if (owner.openingLease)
|
|
1272
|
+
owner.openingLease.valid = false;
|
|
1273
|
+
if (owner.active)
|
|
1274
|
+
owner.active.lease.valid = false;
|
|
1275
|
+
owner.closePromise = enqueue(owner, async () => {
|
|
1276
|
+
let failure = null;
|
|
1277
|
+
try {
|
|
1278
|
+
const lease = owner.active?.lease || { generation: owner.generation, profileId: null, valid: false };
|
|
1279
|
+
await deactivate(owner, transitionContext(owner, lease, "closed"));
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
failure = error;
|
|
1282
|
+
}
|
|
1283
|
+
owner.closed = true;
|
|
1284
|
+
owner.closing = false;
|
|
1285
|
+
publish(owner, "closed", failure);
|
|
1286
|
+
owner.listeners.clear();
|
|
1287
|
+
if (failure)
|
|
1288
|
+
throw failure;
|
|
1289
|
+
});
|
|
1290
|
+
return owner.closePromise;
|
|
1291
|
+
}
|
|
1292
|
+
function createAccountProfiles(options = {}) {
|
|
1293
|
+
const ports = requirePorts(options);
|
|
1294
|
+
const owner = {
|
|
1295
|
+
ports,
|
|
1296
|
+
registry: emptyAccountProfiles(ports.environment),
|
|
1297
|
+
active: null,
|
|
1298
|
+
openingLease: null,
|
|
1299
|
+
requestedProfileId: null,
|
|
1300
|
+
generation: 0,
|
|
1301
|
+
ready: false,
|
|
1302
|
+
closing: false,
|
|
1303
|
+
closed: false,
|
|
1304
|
+
closePromise: null,
|
|
1305
|
+
status: "loading",
|
|
1306
|
+
error: null,
|
|
1307
|
+
snapshot: null,
|
|
1308
|
+
listeners: new Set,
|
|
1309
|
+
reservations: new Set,
|
|
1310
|
+
storageGeneration: 0,
|
|
1311
|
+
storageUpdating: false,
|
|
1312
|
+
refreshRequested: 0,
|
|
1313
|
+
refreshPromise: null,
|
|
1314
|
+
stopStorage: NOOP,
|
|
1315
|
+
tail: Promise.resolve()
|
|
1316
|
+
};
|
|
1317
|
+
owner.snapshot = publicSnapshot(owner);
|
|
1318
|
+
const ready = enqueue(owner, () => initialize(owner));
|
|
1319
|
+
owner.stopStorage = ports.storage.subscribe(ports.environment, () => {
|
|
1320
|
+
if (owner.closing || owner.closed)
|
|
1321
|
+
return;
|
|
1322
|
+
requestProfileRefresh(owner).catch((error) => report(owner, "refresh", error));
|
|
1323
|
+
});
|
|
1324
|
+
if (typeof owner.stopStorage !== "function") {
|
|
1325
|
+
throw new Error("account profile storage subscribe must return unsubscribe");
|
|
1326
|
+
}
|
|
1327
|
+
return Object.freeze({
|
|
1328
|
+
ready,
|
|
1329
|
+
getSnapshot: () => owner.snapshot,
|
|
1330
|
+
subscribe(listener) {
|
|
1331
|
+
if (typeof listener !== "function")
|
|
1332
|
+
return NOOP;
|
|
1333
|
+
owner.listeners.add(listener);
|
|
1334
|
+
return () => owner.listeners.delete(listener);
|
|
1335
|
+
},
|
|
1336
|
+
beginAdd: () => beginAddProfile(owner),
|
|
1337
|
+
update(profileId, fields) {
|
|
1338
|
+
return enqueue(owner, () => updateStoredProfile(owner, profileId, fields));
|
|
1339
|
+
},
|
|
1340
|
+
switchTo(profileId) {
|
|
1341
|
+
if (owner.ready && !accountProfileById(owner.registry, profileId)) {
|
|
1342
|
+
return Promise.reject(new Error("account profile not found"));
|
|
1343
|
+
}
|
|
1344
|
+
const generation = requestSelection(owner, profileId, false);
|
|
1345
|
+
return enqueue(owner, () => switchExisting(owner, profileId, generation));
|
|
1346
|
+
},
|
|
1347
|
+
resumeAuthenticated: () => requestAuthenticatedProfileResume(owner),
|
|
1348
|
+
refresh: () => requestProfileRefresh(owner),
|
|
1349
|
+
signOut: () => requestProfileSignOut(owner),
|
|
1350
|
+
remove: (profileId) => requestProfileRemoval(owner, profileId),
|
|
1351
|
+
getActiveRuntime: () => owner.active?.runtime || null,
|
|
1352
|
+
close: () => closeProfiles(owner)
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
export {
|
|
1356
|
+
ACCOUNT_PROFILE_AUTH_REQUIRED_CODE,
|
|
1357
|
+
ACCOUNT_PROFILE_STALE_CODE,
|
|
1358
|
+
beginAccountProfileTransition,
|
|
1359
|
+
createAccountProfileRuntimeLifecycle,
|
|
1360
|
+
createAccountProfiles,
|
|
1361
|
+
isStaleAccountProfileTransition
|
|
1362
|
+
};
|