@ibgib/web-gib 0.0.52 → 0.0.56
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/CHANGELOG.md +7 -0
- package/dist/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/api/commands/chat/hello-world.mjs.map +1 -1
- package/dist/api/commands/command-constants.d.mts +4 -4
- package/dist/api/commands/command-constants.d.mts.map +1 -1
- package/dist/api/commands/command-service-v1.d.mts.map +1 -1
- package/dist/api/commands/command-service-v1.mjs.map +1 -1
- package/dist/api/commands/ibgib/get-ibgibs.d.mts.map +1 -1
- package/dist/api/commands/ibgib/get-ibgibs.mjs.map +1 -1
- package/dist/api/ibgib-api-bridge.d.mts +81 -0
- package/dist/api/ibgib-api-bridge.d.mts.map +1 -0
- package/dist/api/ibgib-api-bridge.mjs +259 -0
- package/dist/api/ibgib-api-bridge.mjs.map +1 -0
- package/dist/app-bootstrap/init-orchestration.d.mts +1 -1
- package/dist/app-bootstrap/init-orchestration.d.mts.map +1 -1
- package/dist/app-bootstrap/init-orchestration.mjs +17 -3
- package/dist/app-bootstrap/init-orchestration.mjs.map +1 -1
- package/dist/assets/fullscreen-dialog.html +18 -0
- package/dist/assumptions.respec.mjs.map +1 -1
- package/dist/common/settings/settings-constants.d.mts.map +1 -1
- package/dist/helpers.d.mts.map +1 -1
- package/dist/helpers.mjs.map +1 -1
- package/dist/helpers.web.d.mts.map +1 -1
- package/dist/helpers.web.mjs.map +1 -1
- package/dist/helpers.web.respec.mjs.map +1 -1
- package/dist/identity/keystone-sync-service.d.mts +37 -0
- package/dist/identity/keystone-sync-service.d.mts.map +1 -0
- package/dist/identity/keystone-sync-service.mjs +211 -0
- package/dist/identity/keystone-sync-service.mjs.map +1 -0
- package/dist/identity/sso/sso-custodian-service.d.mts.map +1 -1
- package/dist/identity/sso/sso-custodian-service.mjs.map +1 -1
- package/dist/identity/sso/sso-custodian-service.respec.mjs.map +1 -1
- package/dist/index.d.mts +17 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +21 -26
- package/dist/index.mjs.map +1 -1
- package/dist/storage/storage-helpers.web.mjs.map +1 -1
- package/dist/ui/component/common-component-styles.css +270 -0
- package/dist/ui/component/ibgib-component-service.d.mts.map +1 -1
- package/dist/ui/component/ibgib-dynamic-component-bases.d.mts.map +1 -1
- package/dist/ui/component/ibgib-dynamic-component-bases.mjs.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.css +133 -0
- package/dist/ui/component/identity/add-device/add-device.d.mts +49 -0
- package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -0
- package/dist/ui/component/identity/add-device/add-device.html +52 -0
- package/dist/ui/component/identity/add-device/add-device.mjs +321 -0
- package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -0
- package/dist/ui/component/identity/identity-header/identity-header.css +317 -0
- package/dist/ui/component/identity/identity-header/identity-header.d.mts +85 -0
- package/dist/ui/component/identity/identity-header/identity-header.d.mts.map +1 -0
- package/dist/ui/component/identity/identity-header/identity-header.html +72 -0
- package/dist/ui/component/identity/identity-header/identity-header.mjs +546 -0
- package/dist/ui/component/identity/identity-header/identity-header.mjs.map +1 -0
- package/dist/ui/component/identity/identity-manager/identity-manager.css +178 -0
- package/dist/ui/component/identity/identity-manager/identity-manager.d.mts +76 -0
- package/dist/ui/component/identity/identity-manager/identity-manager.d.mts.map +1 -0
- package/dist/ui/component/identity/identity-manager/identity-manager.html +19 -0
- package/dist/ui/component/identity/identity-manager/identity-manager.mjs +591 -0
- package/dist/ui/component/identity/identity-manager/identity-manager.mjs.map +1 -0
- package/dist/ui/component/identity/index.d.mts +17 -0
- package/dist/ui/component/identity/index.d.mts.map +1 -0
- package/dist/ui/component/identity/index.mjs +34 -0
- package/dist/ui/component/identity/index.mjs.map +1 -0
- package/dist/ui/component/identity/keystone-creator/keystone-creator.css +97 -0
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +58 -0
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -0
- package/dist/ui/component/identity/keystone-creator/keystone-creator.html +51 -0
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +410 -0
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.css +621 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +98 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.html +154 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1377 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -0
- package/dist/ui/component/identity/keystone-scrubber/keystone-scrubber.css +46 -0
- package/dist/ui/component/identity/keystone-scrubber/keystone-scrubber.d.mts +64 -0
- package/dist/ui/component/identity/keystone-scrubber/keystone-scrubber.d.mts.map +1 -0
- package/dist/ui/component/identity/keystone-scrubber/keystone-scrubber.html +15 -0
- package/dist/ui/component/identity/keystone-scrubber/keystone-scrubber.mjs +300 -0
- package/dist/ui/component/identity/keystone-scrubber/keystone-scrubber.mjs.map +1 -0
- package/dist/ui/component/identity/remove-device/remove-device.css +118 -0
- package/dist/ui/component/identity/remove-device/remove-device.d.mts +50 -0
- package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -0
- package/dist/ui/component/identity/remove-device/remove-device.html +31 -0
- package/dist/ui/component/identity/remove-device/remove-device.mjs +188 -0
- package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -0
- package/dist/ui/ui-constants.d.mts +1 -0
- package/dist/ui/ui-constants.d.mts.map +1 -1
- package/dist/ui/ui-constants.mjs +1 -0
- package/dist/ui/ui-constants.mjs.map +1 -1
- package/dist/ui/ui-types.d.mts +7 -0
- package/dist/ui/ui-types.d.mts.map +1 -1
- package/dist/witness/agent/agent-constants.d.mts.map +1 -1
- package/dist/witness/agent/agent-helpers.mjs.map +1 -1
- package/dist/witness/agent/agent-one-file.d.mts.map +1 -1
- package/dist/witness/agent/agent-one-file.mjs.map +1 -1
- package/dist/witness/agent/agents-service-v1.d.mts.map +1 -1
- package/dist/witness/agent/agents-service-v1.mjs.map +1 -1
- package/dist/witness/agent/gemini/agent-witness-gemini-v1.d.mts.map +1 -1
- package/dist/witness/agent/gemini/gemini-agent-factory.d.mts.map +1 -1
- package/dist/witness/live-proxy-ibgib/live-proxy-ibgib-one-file.d.mts.map +1 -1
- package/dist/witness/live-proxy-ibgib/live-proxy-ibgib-one-file.mjs.map +1 -1
- package/dist/witness/space/metaspace/metaspace-webspace/metaspace-webspace-helper.mjs.map +1 -1
- package/dist/witness/space/metaspace/metaspace-webspace/metaspace-webspace.d.mts +4 -0
- package/dist/witness/space/metaspace/metaspace-webspace/metaspace-webspace.d.mts.map +1 -1
- package/dist/witness/space/metaspace/metaspace-webspace/metaspace-webspace.mjs +40 -1
- package/dist/witness/space/metaspace/metaspace-webspace/metaspace-webspace.mjs.map +1 -1
- package/dist/witness/space/web-filesystem-space/web-filesystem-space-types.d.mts.map +1 -1
- package/dist/witness/space/web-filesystem-space/web-filesystem-space-v1.d.mts.map +1 -1
- package/package.json +2 -2
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/api/ibgib-api-bridge.mts +292 -0
- package/src/app-bootstrap/init-orchestration.mts +17 -6
- package/src/custom.d.ts +11 -0
- package/src/identity/keystone-sync-service.mts +257 -0
- package/src/index.mts +24 -25
- package/src/ui/component/README.md +82 -5
- package/src/ui/component/common-component-styles.css +270 -0
- package/src/ui/component/identity/add-device/add-device.css +133 -0
- package/src/ui/component/identity/add-device/add-device.html +52 -0
- package/src/ui/component/identity/add-device/add-device.mts +397 -0
- package/src/ui/component/identity/identity-header/IMPLEMENTATION.md +45 -0
- package/src/ui/component/identity/identity-header/identity-header.css +317 -0
- package/src/ui/component/identity/identity-header/identity-header.html +72 -0
- package/src/ui/component/identity/identity-header/identity-header.mts +651 -0
- package/src/ui/component/identity/identity-manager/IMPLEMENTATION.md +100 -0
- package/src/ui/component/identity/identity-manager/identity-manager.css +178 -0
- package/src/ui/component/identity/identity-manager/identity-manager.html +19 -0
- package/src/ui/component/identity/identity-manager/identity-manager.mts +675 -0
- package/src/ui/component/identity/index.mts +36 -0
- package/src/ui/component/identity/keystone-creator/keystone-creator.css +97 -0
- package/src/ui/component/identity/keystone-creator/keystone-creator.html +51 -0
- package/src/ui/component/identity/keystone-creator/keystone-creator.mts +497 -0
- package/src/ui/component/identity/keystone-details/keystone-details.css +621 -0
- package/src/ui/component/identity/keystone-details/keystone-details.html +154 -0
- package/src/ui/component/identity/keystone-details/keystone-details.mts +1506 -0
- package/src/ui/component/identity/keystone-scrubber/SCRUBBER_IMPLEMENTATION.md +33 -0
- package/src/ui/component/identity/keystone-scrubber/keystone-scrubber.css +46 -0
- package/src/ui/component/identity/keystone-scrubber/keystone-scrubber.html +15 -0
- package/src/ui/component/identity/keystone-scrubber/keystone-scrubber.mts +355 -0
- package/src/ui/component/identity/remove-device/remove-device.css +118 -0
- package/src/ui/component/identity/remove-device/remove-device.html +31 -0
- package/src/ui/component/identity/remove-device/remove-device.mts +247 -0
- package/src/ui/ui-constants.mts +1 -0
- package/src/ui/ui-types.mts +8 -0
- package/src/witness/space/metaspace/metaspace-webspace/metaspace-webspace.mts +42 -1
- package/tools/auto-generated-agent-skills/skills/ibgib-dependency-graph/SKILL.md +209 -0
- package/tools/auto-generated-agent-skills/skills/ibgib-implementation-plan/SKILL.md +102 -0
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
import thisCss from "./keystone-details.css";
|
|
2
|
+
import thisHtml from "./keystone-details.html";
|
|
3
|
+
import commonComponentStylesCss from "../../common-component-styles.css";
|
|
4
|
+
import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
5
|
+
import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
|
|
6
|
+
import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
|
|
7
|
+
import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs";
|
|
8
|
+
import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase, } from "../../ibgib-dynamic-component-bases.mjs";
|
|
9
|
+
import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } from "../../../ui-constants.mjs";
|
|
10
|
+
import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
|
|
11
|
+
import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
|
|
12
|
+
import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
|
|
13
|
+
import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
14
|
+
import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
|
|
15
|
+
import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
|
|
16
|
+
import { promptForSecret } from "../../../../helpers.web.mjs";
|
|
17
|
+
import { isSsoProviderLinked } from "../../../../identity/sso/sso-helpers.mjs";
|
|
18
|
+
import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
|
|
19
|
+
const logalot = false;
|
|
20
|
+
export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details';
|
|
21
|
+
export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase {
|
|
22
|
+
lc = `[KeystoneDetailsComponentMeta]`;
|
|
23
|
+
routeRegExp = new RegExp(`^${KEYSTONE_DETAILS_COMPONENT_NAME}$`);
|
|
24
|
+
componentName = KEYSTONE_DETAILS_COMPONENT_NAME;
|
|
25
|
+
constructor() {
|
|
26
|
+
super(getComponentCtorArg());
|
|
27
|
+
if (!customElements.get(this.componentName)) {
|
|
28
|
+
customElements.define(this.componentName, KeystoneDetailsComponentInstance);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async createInstance({ path, ibGibAddr }) {
|
|
32
|
+
const lc = `${this.lc}[${this.createInstance.name}]`;
|
|
33
|
+
const component = document.createElement(this.componentName);
|
|
34
|
+
await component.initialize({
|
|
35
|
+
ibGibAddr,
|
|
36
|
+
meta: this,
|
|
37
|
+
html: thisHtml,
|
|
38
|
+
css: [commonComponentStylesCss, thisCss],
|
|
39
|
+
});
|
|
40
|
+
return component;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInstanceBase {
|
|
44
|
+
lc = `[KeystoneDetailsComponentInstance]`;
|
|
45
|
+
staticIbGib;
|
|
46
|
+
get ibGib() {
|
|
47
|
+
return this.staticIbGib;
|
|
48
|
+
}
|
|
49
|
+
globalActiveAddr = null;
|
|
50
|
+
isHistoricalFrame = false;
|
|
51
|
+
_onIdentityChanged = null;
|
|
52
|
+
keystoneCache = new Map();
|
|
53
|
+
constructor() {
|
|
54
|
+
super();
|
|
55
|
+
}
|
|
56
|
+
async initialize(opts) {
|
|
57
|
+
const lc = `${this.lc}[${this.initialize.name}]`;
|
|
58
|
+
try {
|
|
59
|
+
if (logalot) {
|
|
60
|
+
console.log(`${lc} starting... (I: 4c9328643a18e56b56e57292c87bd226)`);
|
|
61
|
+
}
|
|
62
|
+
await super.initialize(opts);
|
|
63
|
+
// await this.loadIbGib({ getLatest: false });
|
|
64
|
+
await this.loadIbGib({ getLatest: false });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (logalot) {
|
|
72
|
+
console.log(`${lc} complete.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async getKeystoneDetails(addr) {
|
|
77
|
+
const cached = this.keystoneCache.get(addr);
|
|
78
|
+
if (cached)
|
|
79
|
+
return cached;
|
|
80
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
81
|
+
if (!metaspace)
|
|
82
|
+
return { username: '', description: '', isPrimary: false, n: 0 };
|
|
83
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
84
|
+
if (!space)
|
|
85
|
+
return { username: '', description: '', isPrimary: false, n: 0 };
|
|
86
|
+
try {
|
|
87
|
+
const keystoneSvc = new KeystoneService_V1();
|
|
88
|
+
const info = await keystoneSvc.getKeystoneCommonInfo({
|
|
89
|
+
addr,
|
|
90
|
+
metaspace,
|
|
91
|
+
space
|
|
92
|
+
});
|
|
93
|
+
if (info) {
|
|
94
|
+
const details = {
|
|
95
|
+
username: info.name,
|
|
96
|
+
description: info.description,
|
|
97
|
+
isPrimary: info.isPrimary,
|
|
98
|
+
n: info.n
|
|
99
|
+
};
|
|
100
|
+
this.keystoneCache.set(addr, details);
|
|
101
|
+
return details;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.warn(`[getKeystoneDetails] Keystone common info not found in local space for addr: ${addr}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn(`Error getting details for keystone ${addr}: ${extractErrorMsg(err)}`);
|
|
109
|
+
}
|
|
110
|
+
return { username: '', description: '', isPrimary: false, n: 0 };
|
|
111
|
+
}
|
|
112
|
+
async created() {
|
|
113
|
+
const lc = `${this.lc}[${this.created.name}]`;
|
|
114
|
+
try {
|
|
115
|
+
if (logalot) {
|
|
116
|
+
console.log(`${lc} starting... (I: 0b2c6debcdac9db9b7928aa8695ed826)`);
|
|
117
|
+
}
|
|
118
|
+
this.elements = {};
|
|
119
|
+
const shadow = this.shadowRoot;
|
|
120
|
+
this.elements.identityDetailsViewEl = shadow.getElementById('identity-details-view');
|
|
121
|
+
this.elements.identityAddrEl = shadow.getElementById('identity-addr');
|
|
122
|
+
this.elements.identityGenEl = shadow.getElementById('identity-gen');
|
|
123
|
+
this.elements.identityTimestampEl = shadow.getElementById('identity-timestamp');
|
|
124
|
+
this.elements.identityUuidEl = shadow.getElementById('identity-uuid');
|
|
125
|
+
this.elements.identityNameEl = shadow.getElementById('identity-name');
|
|
126
|
+
this.elements.identityDescriptionEl = shadow.getElementById('identity-description');
|
|
127
|
+
this.elements.identityFrameDetailsEl = shadow.getElementById('identity-frame-details');
|
|
128
|
+
this.elements.identityAggrDetailsEl = shadow.getElementById('identity-aggr-details');
|
|
129
|
+
this.elements.poolsContainerEl = shadow.getElementById('pools-container');
|
|
130
|
+
this.elements.btnVerifyChainEl = shadow.getElementById('btn-verify-chain');
|
|
131
|
+
this.elements.btnSetActiveEl = shadow.getElementById('btn-set-active');
|
|
132
|
+
this.elements.btnCheckUpdatesEl = shadow.getElementById('btn-check-updates');
|
|
133
|
+
this.elements.btnUpdateIdentityEl = shadow.getElementById('btn-update-identity');
|
|
134
|
+
this.elements.syncStatusMsgEl = shadow.getElementById('sync-status-msg');
|
|
135
|
+
this.elements.verificationStatusEl = shadow.getElementById('verification-status');
|
|
136
|
+
this.elements.rawAccordionHeaderEl = shadow.getElementById('raw-accordion-header');
|
|
137
|
+
this.elements.rawAccordionContentEl = shadow.getElementById('raw-accordion-content');
|
|
138
|
+
this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json');
|
|
139
|
+
this.elements.delegatesCardEl = shadow.getElementById('delegates-card');
|
|
140
|
+
this.elements.delegatesListEl = shadow.getElementById('delegates-list');
|
|
141
|
+
this.elements.containerEl = shadow.getElementById('container');
|
|
142
|
+
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner');
|
|
143
|
+
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card');
|
|
144
|
+
this.elements.btnLinkGoogleEl = shadow.getElementById('btn-link-google');
|
|
145
|
+
this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github');
|
|
146
|
+
this.elements.ssoStatusMsgEl = shadow.getElementById('sso-status-msg');
|
|
147
|
+
this.elements.inputCurrentPassphraseEl = shadow.getElementById('input-current-passphrase');
|
|
148
|
+
this.elements.inputNewPassphraseEl = shadow.getElementById('input-new-passphrase');
|
|
149
|
+
this.elements.inputConfirmPassphraseEl = shadow.getElementById('input-confirm-passphrase');
|
|
150
|
+
this.elements.btnChangePassphraseEl = shadow.getElementById('btn-change-passphrase');
|
|
151
|
+
this.elements.changePassphraseStatusEl = shadow.getElementById('change-passphrase-status');
|
|
152
|
+
this.elements.changePassphraseCardEl = shadow.getElementById('change-passphrase-card');
|
|
153
|
+
// ElementBase requires contentEl
|
|
154
|
+
this.elements.contentEl = shadow.getElementById('identity-details-view');
|
|
155
|
+
this.initHandlers();
|
|
156
|
+
// Subscribe to global active identity changes
|
|
157
|
+
this._onIdentityChanged = async (e) => {
|
|
158
|
+
this.globalActiveAddr = e.detail?.activeIdentityAddr || null;
|
|
159
|
+
this.keystoneCache.clear();
|
|
160
|
+
await this.updateFrameDetailsView();
|
|
161
|
+
this.updateActiveButtonState();
|
|
162
|
+
};
|
|
163
|
+
window.addEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
164
|
+
await this.resolveActiveIdentity();
|
|
165
|
+
await this.loadIbGib({ getLatest: false });
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (logalot) {
|
|
173
|
+
console.log(`${lc} complete.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async disconnected() {
|
|
178
|
+
if (this._onIdentityChanged) {
|
|
179
|
+
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
initHandlers() {
|
|
183
|
+
// Setup raw JSON accordion toggle
|
|
184
|
+
this.elements.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
|
|
185
|
+
// Buttons
|
|
186
|
+
this.elements.btnVerifyChainEl.addEventListener('click', () => this.handleVerifyIdentityChain());
|
|
187
|
+
this.elements.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
188
|
+
this.elements.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
|
|
189
|
+
this.elements.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
|
|
190
|
+
this.elements.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
191
|
+
this.elements.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
192
|
+
this.elements.btnChangePassphraseEl.addEventListener('click', () => this.handleChangePassphrase());
|
|
193
|
+
}
|
|
194
|
+
toggleAccordion() {
|
|
195
|
+
const header = this.elements.rawAccordionHeaderEl;
|
|
196
|
+
const content = this.elements.rawAccordionContentEl;
|
|
197
|
+
header.classList.toggle('expanded');
|
|
198
|
+
content.classList.toggle('expanded');
|
|
199
|
+
}
|
|
200
|
+
setSyncStatus(msg, type) {
|
|
201
|
+
const statusEl = this.elements.syncStatusMsgEl;
|
|
202
|
+
statusEl.textContent = msg;
|
|
203
|
+
statusEl.className = `status-msg ${type}`;
|
|
204
|
+
statusEl.classList.remove('hidden');
|
|
205
|
+
}
|
|
206
|
+
async handleCheckForUpdates() {
|
|
207
|
+
const lc = `${this.lc}[${this.handleCheckForUpdates.name}]`;
|
|
208
|
+
if (!this.ibGibAddr) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.setSyncStatus("Checking server for keystone updates...", "info");
|
|
212
|
+
try {
|
|
213
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
214
|
+
const space = metaspace ? await metaspace.getLocalUserSpace({ lock: false }) : undefined;
|
|
215
|
+
const syncSvc = getKeystoneSyncService();
|
|
216
|
+
const res = await syncSvc.checkForKeystoneUpdate({
|
|
217
|
+
activePrimaryAddr: this.ibGibAddr,
|
|
218
|
+
metaspace,
|
|
219
|
+
space
|
|
220
|
+
});
|
|
221
|
+
if (res.hasUpdate) {
|
|
222
|
+
this.setSyncStatus(`Update Available (Server n=${res.serverN}, Local n=${res.localN})`, "info");
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
this.setSyncStatus(`Up to Date (n=${res.localN})`, "success");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
230
|
+
this.setSyncStatus(`Check failed: ${extractErrorMsg(error)}`, "error");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async handleUpdateIdentity() {
|
|
234
|
+
const lc = `${this.lc}[${this.handleUpdateIdentity.name}]`;
|
|
235
|
+
if (!this.ibGibAddr) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.setSyncStatus("Updating identity keystone from server...", "info");
|
|
239
|
+
try {
|
|
240
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
241
|
+
const space = metaspace ? await metaspace.getLocalUserSpace({ lock: false }) : undefined;
|
|
242
|
+
const syncSvc = getKeystoneSyncService();
|
|
243
|
+
const updatedFrame = await syncSvc.updateActiveKeystone({
|
|
244
|
+
activePrimaryAddr: this.ibGibAddr,
|
|
245
|
+
metaspace,
|
|
246
|
+
space
|
|
247
|
+
});
|
|
248
|
+
if (updatedFrame) {
|
|
249
|
+
const newAddr = getIbGibAddr({ ibGib: updatedFrame });
|
|
250
|
+
this.ibGibAddr = newAddr;
|
|
251
|
+
this.staticIbGib = updatedFrame;
|
|
252
|
+
this.setSyncStatus(`Identity updated successfully to n=${updatedFrame.data?.n ?? 0}`, "success");
|
|
253
|
+
await this.loadIbGib({ getLatest: true });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.setSyncStatus("Identity is already up to date.", "info");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
261
|
+
this.setSyncStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async handleVerifyIdentityChain() {
|
|
265
|
+
const lc = `${this.lc}[${this.handleVerifyIdentityChain.name}]`;
|
|
266
|
+
console.log(`${lc} Verifying identity keystone timeline...`);
|
|
267
|
+
this.setVerificationStatus("Verification request sent...", "info");
|
|
268
|
+
try {
|
|
269
|
+
this.setVerificationStatus("Verification complete. Root Keystone is verified (Genesis phase 1).", "success");
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
this.setVerificationStatus(`Verification failed: ${extractErrorMsg(error)}`, "error");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async handleSetActiveIdentity() {
|
|
276
|
+
if (!this.ibGibAddr) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, {
|
|
280
|
+
detail: { activeIdentityAddr: this.ibGibAddr },
|
|
281
|
+
bubbles: true,
|
|
282
|
+
composed: true
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
setVerificationStatus(msg, type) {
|
|
286
|
+
const statusEl = this.elements.verificationStatusEl;
|
|
287
|
+
statusEl.textContent = msg;
|
|
288
|
+
statusEl.className = `status-msg ${type}`;
|
|
289
|
+
statusEl.classList.remove('hidden');
|
|
290
|
+
}
|
|
291
|
+
async resolveActiveIdentity() {
|
|
292
|
+
const lc = `${this.lc}[resolveActiveIdentity]`;
|
|
293
|
+
try {
|
|
294
|
+
const domainIdentity = globalThis.ibgib?.identity?.domainIdentity;
|
|
295
|
+
this.globalActiveAddr = domainIdentity ? getIbGibAddr({ ibGib: domainIdentity }) : null;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
299
|
+
this.globalActiveAddr = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async loadIbGib(opts) {
|
|
303
|
+
const lc = `${this.lc}[${this.loadIbGib.name}]`;
|
|
304
|
+
try {
|
|
305
|
+
if (logalot) {
|
|
306
|
+
console.log(`${lc} starting... (I: f0f7e8d52cbdc37bec844b2875996e26)`);
|
|
307
|
+
}
|
|
308
|
+
if (!this.ibGibAddr) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
312
|
+
if (!metaspace) {
|
|
313
|
+
throw new Error(`(UNEXPECTED) metaspace falsy? (E: a9864dbbaefc1082ce7bd228ebd67c26)`);
|
|
314
|
+
}
|
|
315
|
+
let space = await metaspace.getLocalUserSpace({ lock: false });
|
|
316
|
+
if (!space) {
|
|
317
|
+
throw new Error(`(UNEXPECTED) space falsy and we couldn't get default local user space from metaspace? (E: fa298edcecbda889cfd7b228fbd57c26)`);
|
|
318
|
+
}
|
|
319
|
+
let loadAddr = this.ibGibAddr;
|
|
320
|
+
if (opts?.getLatest) {
|
|
321
|
+
const latest = await metaspace.getLatestAddr({ addr: this.ibGibAddr, space });
|
|
322
|
+
if (latest) {
|
|
323
|
+
loadAddr = latest;
|
|
324
|
+
this.ibGibAddr = latest;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const resGet = await metaspace.get({ addrs: [loadAddr], space });
|
|
328
|
+
if (resGet.success && resGet.ibGibs && resGet.ibGibs.length === 1) {
|
|
329
|
+
this.staticIbGib = resGet.ibGibs[0];
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const resIbGib = resGet.rawResultIbGib;
|
|
333
|
+
const addrsNotFound = resIbGib?.data?.addrsNotFound ?? 'unknown';
|
|
334
|
+
throw new Error(`couldn't find address ${loadAddr}. addrsNotFound: ${addrsNotFound}? resGet.errorMsg: ${resGet.errorMsg} (E: e0e5c9b7de2837bc902fa88390cd9f26)`);
|
|
335
|
+
}
|
|
336
|
+
await this.updateFrameDetailsView();
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
if (logalot) {
|
|
344
|
+
console.log(`${lc} complete.`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async renderUI() {
|
|
349
|
+
const lc = `${this.lc}[${this.renderUI.name}]`;
|
|
350
|
+
try {
|
|
351
|
+
if (logalot) {
|
|
352
|
+
console.log(`${lc} starting... (I: c8aa086352d8079708e00f78b6ac0826)`);
|
|
353
|
+
}
|
|
354
|
+
await super.renderUI();
|
|
355
|
+
// await this.updateFrameDetailsView();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
if (logalot) {
|
|
363
|
+
console.log(`${lc} complete.`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
updateActiveButtonState() {
|
|
368
|
+
if (!this.elements) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const tjpGib = getGibInfo({ ibGibAddr: this.ibGibAddr }).tjpGib ?? getIbAndGib({ ibGibAddr: this.ibGibAddr }).gib;
|
|
372
|
+
const activeTjpGib = this.globalActiveAddr
|
|
373
|
+
? (getGibInfo({ ibGibAddr: this.globalActiveAddr }).tjpGib ?? getIbAndGib({ ibGibAddr: this.globalActiveAddr }).gib)
|
|
374
|
+
: null;
|
|
375
|
+
const isTimelineActive = !!(this.globalActiveAddr && tjpGib === activeTjpGib);
|
|
376
|
+
if (isTimelineActive) {
|
|
377
|
+
this.elements.btnSetActiveEl.textContent = "Active Identity";
|
|
378
|
+
this.elements.btnSetActiveEl.classList.add('active-primary');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
this.elements.btnSetActiveEl.textContent = "Set as Active Identity";
|
|
382
|
+
this.elements.btnSetActiveEl.classList.remove('active-primary');
|
|
383
|
+
}
|
|
384
|
+
// Disable buttons if historical
|
|
385
|
+
if (this.isHistoricalFrame) {
|
|
386
|
+
this.elements.btnSetActiveEl.disabled = true;
|
|
387
|
+
this.elements.btnVerifyChainEl.disabled = true;
|
|
388
|
+
this.elements.btnLinkGoogleEl.classList.add('disabled');
|
|
389
|
+
this.elements.btnLinkGithubEl.classList.add('disabled');
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
this.elements.btnVerifyChainEl.disabled = false;
|
|
393
|
+
this.elements.btnSetActiveEl.disabled = isTimelineActive;
|
|
394
|
+
this.elements.btnLinkGoogleEl.classList.remove('disabled');
|
|
395
|
+
this.elements.btnLinkGithubEl.classList.remove('disabled');
|
|
396
|
+
// Check if already linked
|
|
397
|
+
const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'google' });
|
|
398
|
+
const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'github' });
|
|
399
|
+
// Google Card States
|
|
400
|
+
const googleCard = this.elements.btnLinkGoogleEl;
|
|
401
|
+
const googleLabel = googleCard.querySelector('.toggle-state-text');
|
|
402
|
+
if (isGoogleLinked) {
|
|
403
|
+
googleCard.classList.remove('unlinked');
|
|
404
|
+
googleCard.classList.add('linked');
|
|
405
|
+
googleLabel.textContent = "Google Linked ✅";
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
googleCard.classList.remove('linked');
|
|
409
|
+
googleCard.classList.add('unlinked');
|
|
410
|
+
googleLabel.textContent = "Unlinked";
|
|
411
|
+
}
|
|
412
|
+
// GitHub Card States
|
|
413
|
+
const githubCard = this.elements.btnLinkGithubEl;
|
|
414
|
+
const githubLabel = githubCard.querySelector('.toggle-state-text');
|
|
415
|
+
if (isGithubLinked) {
|
|
416
|
+
githubCard.classList.remove('unlinked');
|
|
417
|
+
githubCard.classList.add('linked');
|
|
418
|
+
githubLabel.textContent = "GitHub Linked ✅";
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
githubCard.classList.remove('linked');
|
|
422
|
+
githubCard.classList.add('unlinked');
|
|
423
|
+
githubLabel.textContent = "Unlinked";
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async updateFrameDetailsView() {
|
|
428
|
+
const lc = `${this.lc}[${this.updateFrameDetailsView.name}]`;
|
|
429
|
+
try {
|
|
430
|
+
if (logalot) {
|
|
431
|
+
console.log(`${lc} starting... (I: 1fa9e66d2e78fb100874573e2db71d26)`);
|
|
432
|
+
}
|
|
433
|
+
if (!this.elements) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const keystone = this.ibGib;
|
|
437
|
+
if (!keystone) {
|
|
438
|
+
this.clearActiveDetails();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const keystoneAddr = this.ibGibAddr;
|
|
442
|
+
// Update Verifiable Address
|
|
443
|
+
this.elements.identityAddrEl.textContent = keystoneAddr;
|
|
444
|
+
// Update Generation
|
|
445
|
+
this.elements.identityGenEl.textContent = keystone.data?.n !== undefined ? String(keystone.data.n) : '0 (Genesis)';
|
|
446
|
+
// Update Timestamp
|
|
447
|
+
const timestampStr = keystone.data?.timestamp;
|
|
448
|
+
let formattedTimestamp = 'N/A';
|
|
449
|
+
if (timestampStr) {
|
|
450
|
+
const parsed = parseInt(timestampStr);
|
|
451
|
+
if (!isNaN(parsed)) {
|
|
452
|
+
formattedTimestamp = new Date(parsed).toLocaleString();
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
formattedTimestamp = new Date(timestampStr).toLocaleString();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
this.elements.identityTimestampEl.textContent = formattedTimestamp;
|
|
459
|
+
// Update UUID
|
|
460
|
+
this.elements.identityUuidEl.textContent = keystone.data?.uuid || 'N/A';
|
|
461
|
+
// Show Frame Details
|
|
462
|
+
const frameDetails = keystone.data?.frameDetails;
|
|
463
|
+
this.elements.identityFrameDetailsEl.textContent = frameDetails ? pretty(frameDetails) : '{}';
|
|
464
|
+
// Get and Show Aggregated details up to the scrolled-to frame
|
|
465
|
+
let isHistorical = false;
|
|
466
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
467
|
+
if (metaspace) {
|
|
468
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
469
|
+
if (space) {
|
|
470
|
+
try {
|
|
471
|
+
const latestAddr = await metaspace.getLatestAddr({ addr: this.ibGibAddr, space });
|
|
472
|
+
if (latestAddr && latestAddr !== this.ibGibAddr) {
|
|
473
|
+
isHistorical = true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
console.warn(`${lc} Error checking latest address: ${extractErrorMsg(err)}`);
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const keystoneSvc = new KeystoneService_V1();
|
|
481
|
+
const aggregated = await keystoneSvc.getAggregateDetails({
|
|
482
|
+
latestKeystone: keystone,
|
|
483
|
+
metaspace,
|
|
484
|
+
space
|
|
485
|
+
});
|
|
486
|
+
this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
|
|
487
|
+
// Update Username and Description fields
|
|
488
|
+
this.elements.identityNameEl.textContent = aggregated?.username || aggregated?.profile?.name || aggregated?.name || '-';
|
|
489
|
+
this.elements.identityDescriptionEl.textContent = aggregated?.description || aggregated?.profile?.description || '-';
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
|
|
493
|
+
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
494
|
+
this.elements.identityNameEl.textContent = '-';
|
|
495
|
+
this.elements.identityDescriptionEl.textContent = '-';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
500
|
+
this.elements.identityNameEl.textContent = '-';
|
|
501
|
+
this.elements.identityDescriptionEl.textContent = '-';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
506
|
+
this.elements.identityNameEl.textContent = '-';
|
|
507
|
+
this.elements.identityDescriptionEl.textContent = '-';
|
|
508
|
+
}
|
|
509
|
+
// Update historical warning state
|
|
510
|
+
this.isHistoricalFrame = isHistorical;
|
|
511
|
+
const banner = this.elements.historicalWarningBannerEl;
|
|
512
|
+
const container = this.elements.containerEl;
|
|
513
|
+
if (banner && container) {
|
|
514
|
+
if (isHistorical) {
|
|
515
|
+
banner.classList.remove('hidden');
|
|
516
|
+
container.classList.add('is-historical');
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
banner.classList.add('hidden');
|
|
520
|
+
container.classList.remove('is-historical');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Render Raw Timeline JSON
|
|
524
|
+
this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
|
|
525
|
+
// Render Challenge Pools
|
|
526
|
+
this.renderChallengePools(keystone.data?.challengePools || []);
|
|
527
|
+
// Render Delegates, SSO link, and Passphrase Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
528
|
+
const details = await this.getKeystoneDetails(this.ibGibAddr);
|
|
529
|
+
if (details.isPrimary) {
|
|
530
|
+
this.elements.delegatesCardEl.classList.remove('hidden');
|
|
531
|
+
await this.renderDelegates(keystone.data?.delegates);
|
|
532
|
+
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
533
|
+
this.elements.changePassphraseCardEl.classList.remove('hidden');
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
this.elements.delegatesCardEl.classList.add('hidden');
|
|
537
|
+
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
538
|
+
this.elements.changePassphraseCardEl.classList.add('hidden');
|
|
539
|
+
}
|
|
540
|
+
this.updateActiveButtonState();
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
if (logalot) {
|
|
548
|
+
console.log(`${lc} complete.`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
clearActiveDetails() {
|
|
553
|
+
if (!this.elements) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
this.isHistoricalFrame = false;
|
|
557
|
+
if (this.elements.historicalWarningBannerEl) {
|
|
558
|
+
this.elements.historicalWarningBannerEl.classList.add('hidden');
|
|
559
|
+
}
|
|
560
|
+
if (this.elements.containerEl) {
|
|
561
|
+
this.elements.containerEl.classList.remove('is-historical');
|
|
562
|
+
}
|
|
563
|
+
this.elements.identityGenEl.textContent = '-';
|
|
564
|
+
this.elements.identityTimestampEl.textContent = '-';
|
|
565
|
+
this.elements.identityUuidEl.textContent = '-';
|
|
566
|
+
this.elements.identityNameEl.textContent = '-';
|
|
567
|
+
this.elements.identityDescriptionEl.textContent = '-';
|
|
568
|
+
this.elements.identityFrameDetailsEl.textContent = '{}';
|
|
569
|
+
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
570
|
+
this.elements.poolsContainerEl.innerHTML = '';
|
|
571
|
+
if (this.elements.delegatesListEl) {
|
|
572
|
+
this.elements.delegatesListEl.innerHTML = '';
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
renderChallengePools(pools) {
|
|
576
|
+
if (!this.elements) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const container = this.elements.poolsContainerEl;
|
|
580
|
+
container.innerHTML = '';
|
|
581
|
+
if (!pools || pools.length === 0) {
|
|
582
|
+
container.innerHTML = `<p class="description">No challenge pools found in this keystone.</p>`;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
for (const pool of pools) {
|
|
586
|
+
const card = document.createElement('div');
|
|
587
|
+
card.className = 'pool-card';
|
|
588
|
+
const config = pool.config || {};
|
|
589
|
+
const behavior = config.behavior || {};
|
|
590
|
+
const allowedVerbs = config.allowedVerbs || [];
|
|
591
|
+
const activeCount = pool.challenges ? Object.keys(pool.challenges).length : 0;
|
|
592
|
+
const isForeign = !!pool.isForeign;
|
|
593
|
+
// Generate HTML for the challenges list
|
|
594
|
+
const challenges = pool.challenges || {};
|
|
595
|
+
const challengeListHtml = Object.entries(challenges).map(([id, challenge]) => {
|
|
596
|
+
const truncatedHash = challenge.hash && challenge.hash.length > 20
|
|
597
|
+
? challenge.hash.substring(0, 10) + '...' + challenge.hash.substring(challenge.hash.length - 8)
|
|
598
|
+
: challenge.hash || 'N/A';
|
|
599
|
+
return `
|
|
600
|
+
<div class="pool-challenge-item">
|
|
601
|
+
<span class="challenge-id">${id}</span>
|
|
602
|
+
<span class="challenge-hash" title="${challenge.hash || ''}">${truncatedHash}</span>
|
|
603
|
+
</div>
|
|
604
|
+
`;
|
|
605
|
+
}).join('');
|
|
606
|
+
card.innerHTML = `
|
|
607
|
+
<div class="pool-header">
|
|
608
|
+
<span class="pool-title">${pool.id || 'Unnamed Pool'}</span>
|
|
609
|
+
<span class="pool-tag ${isForeign ? 'foreign' : 'native'}">${isForeign ? 'Foreign' : 'Native'}</span>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="pool-grid">
|
|
612
|
+
<div class="pool-field">
|
|
613
|
+
<label>Verbs Allowed</label>
|
|
614
|
+
<span>${allowedVerbs.length > 0 ? allowedVerbs.join(', ') : 'Any'}</span>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="pool-field">
|
|
617
|
+
<label>FIFO / Random cost</label>
|
|
618
|
+
<span>FIFO: ${behavior.selectSequentially ?? 0}, Rand: ${behavior.selectRandomly ?? 0}</span>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<!-- Collapsible Challenges List -->
|
|
623
|
+
<details class="pool-details">
|
|
624
|
+
<summary class="pool-details-summary">Challenges (${activeCount})</summary>
|
|
625
|
+
<div class="pool-challenges-list">
|
|
626
|
+
${challengeListHtml || '<p class="placeholder-text" style="padding: 0.5rem; text-align: center;">No challenges active.</p>'}
|
|
627
|
+
</div>
|
|
628
|
+
</details>
|
|
629
|
+
`;
|
|
630
|
+
container.appendChild(card);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
showRemoteDelegateModal(info) {
|
|
634
|
+
const shadow = this.shadowRoot;
|
|
635
|
+
let modalOverlay = shadow.getElementById('remote-delegate-modal-overlay');
|
|
636
|
+
if (modalOverlay) {
|
|
637
|
+
modalOverlay.remove();
|
|
638
|
+
}
|
|
639
|
+
modalOverlay = document.createElement('div');
|
|
640
|
+
modalOverlay.id = 'remote-delegate-modal-overlay';
|
|
641
|
+
modalOverlay.className = 'modal-overlay';
|
|
642
|
+
modalOverlay.style.cssText = `
|
|
643
|
+
position: fixed;
|
|
644
|
+
top: 0;
|
|
645
|
+
left: 0;
|
|
646
|
+
width: 100%;
|
|
647
|
+
height: 100%;
|
|
648
|
+
background: rgba(0, 0, 0, 0.7);
|
|
649
|
+
display: flex;
|
|
650
|
+
align-items: center;
|
|
651
|
+
justify-content: center;
|
|
652
|
+
z-index: 9999;
|
|
653
|
+
`;
|
|
654
|
+
const modalContent = document.createElement('div');
|
|
655
|
+
modalContent.className = 'modal-card card';
|
|
656
|
+
modalContent.style.cssText = `
|
|
657
|
+
background: var(--clr-bg-card, #1e1e1e);
|
|
658
|
+
color: var(--clr-text-primary, #ffffff);
|
|
659
|
+
padding: 1.5rem;
|
|
660
|
+
border-radius: 8px;
|
|
661
|
+
max-width: 550px;
|
|
662
|
+
width: 90%;
|
|
663
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
664
|
+
font-family: inherit;
|
|
665
|
+
`;
|
|
666
|
+
const deviceNameText = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
|
|
667
|
+
const nameText = info.username || info.name || info.details?.name || info.details?.username || 'Remote Delegate';
|
|
668
|
+
const descText = info.description || info.details?.description || 'N/A';
|
|
669
|
+
const spaceIdText = info.spaceId || info.details?.spaceId || 'N/A';
|
|
670
|
+
const allowedVerbsText = info.allowedVerbs ? info.allowedVerbs.join(', ') : 'None';
|
|
671
|
+
const tjpAddrText = info.delegateTjpAddr || 'N/A';
|
|
672
|
+
modalContent.innerHTML = `
|
|
673
|
+
<h3 style="margin-top: 0; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;">
|
|
674
|
+
<span>Remote Delegate Metadata</span>
|
|
675
|
+
<span class="badge badge-remote-device" style="background-color: #0d47a1; color: #90caf9; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: bold;">Remote Device</span>
|
|
676
|
+
</h3>
|
|
677
|
+
<p style="color: var(--clr-text-secondary, #b3b3b3); font-size: 0.875rem; margin-bottom: 1rem;">
|
|
678
|
+
This delegate origin is a remote device. Full timeline history stays isolated on the origin device unless explicitly synced.
|
|
679
|
+
</p>
|
|
680
|
+
<div class="kv-grid" style="display: grid; grid-template-columns: 120px 1fr; gap: 0.5rem 1rem; margin-bottom: 1.5rem; font-size: 0.9rem;">
|
|
681
|
+
<div style="font-weight: bold; color: var(--clr-text-secondary, #b3b3b3);">Device Name</div>
|
|
682
|
+
<div>💻 ${deviceNameText}</div>
|
|
683
|
+
|
|
684
|
+
<div style="font-weight: bold; color: var(--clr-text-secondary, #b3b3b3);">Name</div>
|
|
685
|
+
<div>${nameText}</div>
|
|
686
|
+
|
|
687
|
+
<div style="font-weight: bold; color: var(--clr-text-secondary, #b3b3b3);">Description</div>
|
|
688
|
+
<div>${descText}</div>
|
|
689
|
+
|
|
690
|
+
<div style="font-weight: bold; color: var(--clr-text-secondary, #b3b3b3);">Space ID</div>
|
|
691
|
+
<code style="word-break: break-all; background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px;">${spaceIdText}</code>
|
|
692
|
+
|
|
693
|
+
<div style="font-weight: bold; color: var(--clr-text-secondary, #b3b3b3);">Allowed Verbs</div>
|
|
694
|
+
<div>${allowedVerbsText}</div>
|
|
695
|
+
|
|
696
|
+
<div style="font-weight: bold; color: var(--clr-text-secondary, #b3b3b3);">TJP Address</div>
|
|
697
|
+
<code style="word-break: break-all; background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px;">${tjpAddrText}</code>
|
|
698
|
+
</div>
|
|
699
|
+
<div style="display: flex; justify-content: flex-end;">
|
|
700
|
+
<button id="close-remote-modal-btn" class="action-btn secondary">Close</button>
|
|
701
|
+
</div>
|
|
702
|
+
`;
|
|
703
|
+
modalOverlay.appendChild(modalContent);
|
|
704
|
+
shadow.appendChild(modalOverlay);
|
|
705
|
+
const closeBtn = modalContent.querySelector('#close-remote-modal-btn');
|
|
706
|
+
closeBtn?.addEventListener('click', () => {
|
|
707
|
+
modalOverlay?.remove();
|
|
708
|
+
});
|
|
709
|
+
modalOverlay.addEventListener('click', (e) => {
|
|
710
|
+
if (e.target === modalOverlay) {
|
|
711
|
+
modalOverlay?.remove();
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async renderDelegates(delegates) {
|
|
716
|
+
if (!this.elements) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const listEl = this.elements.delegatesListEl;
|
|
720
|
+
listEl.innerHTML = '';
|
|
721
|
+
if (!delegates || Object.keys(delegates).length === 0) {
|
|
722
|
+
listEl.innerHTML = `
|
|
723
|
+
<tr>
|
|
724
|
+
<td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3);">
|
|
725
|
+
No registered delegates.
|
|
726
|
+
</td>
|
|
727
|
+
</tr>
|
|
728
|
+
`;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
732
|
+
let localSpaceId;
|
|
733
|
+
if (metaspace) {
|
|
734
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
735
|
+
if (space?.ib) {
|
|
736
|
+
localSpaceId = parseSpaceIb({ spaceIb: space.ib }).spaceId;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
for (const [delegateTjpAddr, info] of Object.entries(delegates)) {
|
|
740
|
+
const tr = document.createElement('tr');
|
|
741
|
+
const deviceName = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
|
|
742
|
+
const tdName = document.createElement('td');
|
|
743
|
+
const delegateSpaceId = info.spaceId || info.details?.spaceId;
|
|
744
|
+
let badgeEl = null;
|
|
745
|
+
if (delegateSpaceId) {
|
|
746
|
+
badgeEl = document.createElement('span');
|
|
747
|
+
badgeEl.style.marginLeft = '0.5rem';
|
|
748
|
+
badgeEl.style.padding = '0.15rem 0.4rem';
|
|
749
|
+
badgeEl.style.borderRadius = '4px';
|
|
750
|
+
badgeEl.style.fontSize = '0.75rem';
|
|
751
|
+
badgeEl.style.fontWeight = 'bold';
|
|
752
|
+
if (localSpaceId && delegateSpaceId === localSpaceId) {
|
|
753
|
+
badgeEl.textContent = 'This Device';
|
|
754
|
+
badgeEl.className = 'badge badge-this-device';
|
|
755
|
+
badgeEl.style.backgroundColor = '#1b5e20';
|
|
756
|
+
badgeEl.style.color = '#a5d6a7';
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
badgeEl.textContent = 'Remote Device';
|
|
760
|
+
badgeEl.className = 'badge badge-remote-device';
|
|
761
|
+
badgeEl.style.backgroundColor = '#0d47a1';
|
|
762
|
+
badgeEl.style.color = '#90caf9';
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const applyNameAndBadge = (displayName, hoverText) => {
|
|
766
|
+
tdName.innerHTML = '';
|
|
767
|
+
const textSpan = document.createElement('span');
|
|
768
|
+
textSpan.textContent = displayName;
|
|
769
|
+
tdName.appendChild(textSpan);
|
|
770
|
+
if (badgeEl) {
|
|
771
|
+
tdName.appendChild(badgeEl);
|
|
772
|
+
}
|
|
773
|
+
tdName.title = hoverText || displayName;
|
|
774
|
+
};
|
|
775
|
+
const displayName = `💻 ${deviceName}`;
|
|
776
|
+
applyNameAndBadge(displayName, `TJP: ${delegateTjpAddr}\nDevice: ${deviceName}`);
|
|
777
|
+
const tdTjp = document.createElement('td');
|
|
778
|
+
tdTjp.textContent = delegateTjpAddr;
|
|
779
|
+
tdTjp.title = delegateTjpAddr;
|
|
780
|
+
const tdVerbs = document.createElement('td');
|
|
781
|
+
tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-';
|
|
782
|
+
tdVerbs.title = tdVerbs.textContent;
|
|
783
|
+
const isRemote = !!(delegateSpaceId && localSpaceId && delegateSpaceId !== localSpaceId);
|
|
784
|
+
const tdActions = document.createElement('td');
|
|
785
|
+
const btnView = document.createElement('button');
|
|
786
|
+
btnView.className = 'action-btn secondary small';
|
|
787
|
+
btnView.textContent = 'View Details';
|
|
788
|
+
btnView.addEventListener('click', async () => {
|
|
789
|
+
const lc = `${this.lc}[renderDelegates.click]`;
|
|
790
|
+
if (isRemote) {
|
|
791
|
+
this.showRemoteDelegateModal(info);
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
if (info.delegateAddr) {
|
|
795
|
+
let addrToLoad = info.delegateAddr;
|
|
796
|
+
if (metaspace) {
|
|
797
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
798
|
+
if (space) {
|
|
799
|
+
try {
|
|
800
|
+
const latest = await metaspace.getLatestAddr({ addr: delegateTjpAddr, space });
|
|
801
|
+
if (latest) {
|
|
802
|
+
addrToLoad = latest;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch (err) {
|
|
806
|
+
console.warn(`${lc} Error resolving latest delegate address: ${extractErrorMsg(err)}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
|
|
811
|
+
detail: { addr: addrToLoad },
|
|
812
|
+
bubbles: true,
|
|
813
|
+
composed: true
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
this.showRemoteDelegateModal(info);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
tdActions.appendChild(btnView);
|
|
822
|
+
const btnRevoke = document.createElement('button');
|
|
823
|
+
btnRevoke.className = 'action-btn danger small';
|
|
824
|
+
btnRevoke.style.marginLeft = '0.4rem';
|
|
825
|
+
btnRevoke.textContent = 'Revoke';
|
|
826
|
+
btnRevoke.addEventListener('click', async () => {
|
|
827
|
+
await this.handleRevokeDelegate(delegateTjpAddr, deviceName);
|
|
828
|
+
});
|
|
829
|
+
tdActions.appendChild(btnRevoke);
|
|
830
|
+
tr.appendChild(tdName);
|
|
831
|
+
tr.appendChild(tdTjp);
|
|
832
|
+
tr.appendChild(tdVerbs);
|
|
833
|
+
tr.appendChild(tdActions);
|
|
834
|
+
listEl.appendChild(tr);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async handleRevokeDelegate(delegateTjpAddr, deviceName) {
|
|
838
|
+
const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`;
|
|
839
|
+
try {
|
|
840
|
+
let masterSecret;
|
|
841
|
+
try {
|
|
842
|
+
masterSecret = await promptForSecret({
|
|
843
|
+
msg: `Enter Master Passphrase to revoke delegate '${deviceName}':`,
|
|
844
|
+
confirm: false
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
catch (err) {
|
|
848
|
+
// User cancelled prompt
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (!masterSecret) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
this.setSyncStatus("Revoking delegate device...", "info");
|
|
855
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
856
|
+
if (!metaspace)
|
|
857
|
+
throw new Error("Metaspace not found");
|
|
858
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
859
|
+
if (!space) {
|
|
860
|
+
throw new Error("Local space not found");
|
|
861
|
+
}
|
|
862
|
+
const keystoneSvc = new KeystoneService_V1();
|
|
863
|
+
// Unregister delegate from parent keystone
|
|
864
|
+
const evolvedParent = await keystoneSvc.unregisterDelegate({
|
|
865
|
+
parentKeystone: this.ibGib,
|
|
866
|
+
delegateTjpAddr,
|
|
867
|
+
masterSecret,
|
|
868
|
+
metaspace,
|
|
869
|
+
space
|
|
870
|
+
});
|
|
871
|
+
// Put evolved parent keystone to server via putEvolveKeystone
|
|
872
|
+
const parentTjp = getTjpAddr({ ibGib: this.ibGib }) || getIbGibAddr({ ibGib: this.ibGib });
|
|
873
|
+
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
874
|
+
domainAddr: parentTjp,
|
|
875
|
+
keystoneIbGib: evolvedParent,
|
|
876
|
+
relatedKeystoneIbGibs: []
|
|
877
|
+
});
|
|
878
|
+
if (!syncRes.success) {
|
|
879
|
+
throw new Error(`Server rejected delegate revocation: ${syncRes.message}`);
|
|
880
|
+
}
|
|
881
|
+
// Save evolved parent to space and update local index
|
|
882
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
883
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
884
|
+
await updateSpecialIndex({
|
|
885
|
+
type: "keystones",
|
|
886
|
+
rel8nInfos: [
|
|
887
|
+
{
|
|
888
|
+
rel8nName: "keystone",
|
|
889
|
+
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
890
|
+
}
|
|
891
|
+
],
|
|
892
|
+
metaspace,
|
|
893
|
+
space
|
|
894
|
+
});
|
|
895
|
+
const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
896
|
+
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
897
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
898
|
+
detail: { activeIdentityAddr: evolvedAddr },
|
|
899
|
+
bubbles: true,
|
|
900
|
+
composed: true
|
|
901
|
+
}));
|
|
902
|
+
this.setSyncStatus(`Successfully revoked device '${deviceName}'!`, "success");
|
|
903
|
+
// Refresh the view
|
|
904
|
+
this.ibGibAddr = evolvedAddr;
|
|
905
|
+
this.staticIbGib = evolvedParent;
|
|
906
|
+
await this.loadIbGib({ getLatest: true });
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
console.error(`${lc} Delegate revocation failed: ${extractErrorMsg(error)}`);
|
|
910
|
+
this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async handleContextUpdated() {
|
|
914
|
+
const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
|
|
915
|
+
try {
|
|
916
|
+
if (logalot) {
|
|
917
|
+
console.log(`${lc} starting... (I: 243a88c1d4689c779bbbf2a4abd5da26)`);
|
|
918
|
+
}
|
|
919
|
+
// don't do anything when the context is updated?
|
|
920
|
+
// is this hit? no. leave this as a no-op
|
|
921
|
+
console.warn(`${lc} this component currently designed expecting that this does not hit (W: 95dc018b87027287e860de0826146826)`);
|
|
922
|
+
await super.handleContextUpdated();
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
if (logalot) {
|
|
930
|
+
console.log(`${lc} complete.`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async handleNewContextChild({ childIbGib }) {
|
|
935
|
+
const lc = `${this.lc}[${this.handleNewContextChild.name}]`;
|
|
936
|
+
try {
|
|
937
|
+
if (logalot) {
|
|
938
|
+
console.log(`${lc} starting... (I: b04dc63842f871658840233485659726)`);
|
|
939
|
+
}
|
|
940
|
+
// is this hit? no
|
|
941
|
+
await super.handleNewContextChild({ childIbGib });
|
|
942
|
+
console.warn(`${lc} this component currently designed expecting that this does not hit (W: 95dc018b87027287e860de0826146826)`);
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
finally {
|
|
949
|
+
if (logalot) {
|
|
950
|
+
console.log(`${lc} complete.`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async handleSSOLink(providerId) {
|
|
955
|
+
const lc = `${this.lc}[${this.handleSSOLink.name}]`;
|
|
956
|
+
this.setSSOStatus(`Linking ${providerId}...`, "info");
|
|
957
|
+
try {
|
|
958
|
+
// 1. Fetch public SSO configs from server
|
|
959
|
+
const res = await fetch('/api/identity/sso/config');
|
|
960
|
+
if (!res.ok) {
|
|
961
|
+
throw new Error(`Failed to fetch SSO config from server: ${res.statusText}`);
|
|
962
|
+
}
|
|
963
|
+
const data = await res.json();
|
|
964
|
+
if (!data.success || !data.providers) {
|
|
965
|
+
throw new Error(`SSO configuration fetch failed: ${data.message || 'unknown error'}`);
|
|
966
|
+
}
|
|
967
|
+
const providerConfig = data.providers.find((p) => p.providerId === providerId);
|
|
968
|
+
if (!providerConfig) {
|
|
969
|
+
throw new Error(`SSO provider '${providerId}' is not configured on the server.`);
|
|
970
|
+
}
|
|
971
|
+
// 2. Generate a secure, high-entropy user nonce
|
|
972
|
+
const userNonce = (Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).substring(0, 32);
|
|
973
|
+
// 3. Construct state payload
|
|
974
|
+
const stateObj = {
|
|
975
|
+
providerId,
|
|
976
|
+
parentTjpAddr: this.ibGibAddr,
|
|
977
|
+
userNonce
|
|
978
|
+
};
|
|
979
|
+
const state = btoa(JSON.stringify(stateObj));
|
|
980
|
+
// 4. Construct Auth URL
|
|
981
|
+
const authUrl = `${providerConfig.authUrl}?client_id=${providerConfig.clientId}&redirect_uri=${encodeURIComponent(providerConfig.redirectUri)}&response_type=code&scope=${encodeURIComponent(providerConfig.scope)}&state=${encodeURIComponent(state)}`;
|
|
982
|
+
// 5. Open popup window
|
|
983
|
+
const popup = window.open(authUrl, 'sso-oauth-popup', 'width=600,height=700,status=yes,toolbar=no,menubar=no,location=yes');
|
|
984
|
+
if (!popup) {
|
|
985
|
+
throw new Error("Popup blocked by browser. Please enable popups for this site.");
|
|
986
|
+
}
|
|
987
|
+
// 6. Listen for callback from pop-up
|
|
988
|
+
let checkClosedInterval;
|
|
989
|
+
const messageListener = async (event) => {
|
|
990
|
+
if (event.origin !== window.location.origin)
|
|
991
|
+
return;
|
|
992
|
+
const callbackData = event.data;
|
|
993
|
+
if (callbackData && callbackData.type === 'sso-oauth-callback') {
|
|
994
|
+
window.removeEventListener('message', messageListener);
|
|
995
|
+
clearInterval(checkClosedInterval);
|
|
996
|
+
await this.handleSSOLinkCallback(callbackData.code, stateObj, providerConfig.redirectUri);
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
window.addEventListener('message', messageListener);
|
|
1000
|
+
// 7. Poll for manual closure of the popup window
|
|
1001
|
+
checkClosedInterval = setInterval(() => {
|
|
1002
|
+
if (popup.closed) {
|
|
1003
|
+
clearInterval(checkClosedInterval);
|
|
1004
|
+
window.removeEventListener('message', messageListener);
|
|
1005
|
+
const statusEl = this.elements.ssoStatusMsgEl;
|
|
1006
|
+
if (statusEl.textContent && statusEl.textContent.startsWith('Linking')) {
|
|
1007
|
+
this.setSSOStatus('Linking cancelled (window closed).', 'error');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}, 500);
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
1014
|
+
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
setSSOStatus(msg, type) {
|
|
1018
|
+
const statusEl = this.elements.ssoStatusMsgEl;
|
|
1019
|
+
statusEl.textContent = msg;
|
|
1020
|
+
statusEl.className = `status-msg ${type}`;
|
|
1021
|
+
statusEl.classList.remove('hidden');
|
|
1022
|
+
}
|
|
1023
|
+
async handleSSOLinkCallback(code, stateObj, redirectUri) {
|
|
1024
|
+
const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
|
|
1025
|
+
this.setSSOStatus("Retrieving challenge pool configuration from server...", "info");
|
|
1026
|
+
try {
|
|
1027
|
+
// 1. Exchange OAuth code for custodian public pool config
|
|
1028
|
+
const res = await fetch('/api/identity/sso/link', {
|
|
1029
|
+
method: 'POST',
|
|
1030
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1031
|
+
body: JSON.stringify({
|
|
1032
|
+
code,
|
|
1033
|
+
providerId: stateObj.providerId,
|
|
1034
|
+
parentTjpAddr: stateObj.parentTjpAddr,
|
|
1035
|
+
userNonce: stateObj.userNonce,
|
|
1036
|
+
redirectUri
|
|
1037
|
+
})
|
|
1038
|
+
});
|
|
1039
|
+
if (!res.ok) {
|
|
1040
|
+
const errBody = await res.json().catch(() => ({}));
|
|
1041
|
+
throw new Error(`Server link exchange failed: ${errBody.message || res.statusText}`);
|
|
1042
|
+
}
|
|
1043
|
+
const linkData = await res.json();
|
|
1044
|
+
if (!linkData.success || !linkData.custodianPool) {
|
|
1045
|
+
throw new Error(`Failed to retrieve custodian challenge pool: ${linkData.message || 'unknown error'}`);
|
|
1046
|
+
}
|
|
1047
|
+
const custodianPool = linkData.custodianPool;
|
|
1048
|
+
// 2. Prompt user for master secret and verify it
|
|
1049
|
+
const keystoneService = new KeystoneService_V1();
|
|
1050
|
+
let masterSecret;
|
|
1051
|
+
let isCorrect = false;
|
|
1052
|
+
let tries = 0;
|
|
1053
|
+
const maxTries = 8;
|
|
1054
|
+
while (tries < maxTries && !isCorrect) {
|
|
1055
|
+
try {
|
|
1056
|
+
const promptMsg = tries === 0
|
|
1057
|
+
? "Link successful! Enter your local Master Secret (Passphrase) to sign and register the custodian pool"
|
|
1058
|
+
: `Incorrect passphrase. Please try again (${maxTries - tries} tries remaining):`;
|
|
1059
|
+
masterSecret = await promptForSecret({
|
|
1060
|
+
msg: promptMsg,
|
|
1061
|
+
confirm: false
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
// cancelled
|
|
1066
|
+
if (logalot) {
|
|
1067
|
+
console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`);
|
|
1068
|
+
}
|
|
1069
|
+
masterSecret = undefined;
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
if (!masterSecret) {
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
1076
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
1077
|
+
keystoneIbGib: this.ibGib,
|
|
1078
|
+
signingSecret: masterSecret,
|
|
1079
|
+
poolId: 'manage'
|
|
1080
|
+
});
|
|
1081
|
+
if (!isCorrect) {
|
|
1082
|
+
tries++;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (!isCorrect) {
|
|
1086
|
+
this.setSSOStatus("Link cancelled or failed (incorrect passphrase).", "error");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
|
|
1090
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1091
|
+
if (!metaspace)
|
|
1092
|
+
throw new Error("Metaspace not found");
|
|
1093
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1094
|
+
if (!space) {
|
|
1095
|
+
throw new Error("Local space not found");
|
|
1096
|
+
}
|
|
1097
|
+
// 3. Evolve parent keystone to include the custodian pool
|
|
1098
|
+
const evolvedParent = await keystoneService.addPools({
|
|
1099
|
+
latestKeystone: this.ibGib,
|
|
1100
|
+
masterSecret: masterSecret,
|
|
1101
|
+
newPools: [custodianPool],
|
|
1102
|
+
metaspace,
|
|
1103
|
+
space
|
|
1104
|
+
});
|
|
1105
|
+
// 4. Sync evolved parent keystone back to server
|
|
1106
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1107
|
+
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1108
|
+
domainAddr: stateObj.parentTjpAddr,
|
|
1109
|
+
keystoneIbGib: evolvedParent,
|
|
1110
|
+
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1111
|
+
});
|
|
1112
|
+
if (!syncRes.success) {
|
|
1113
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1114
|
+
}
|
|
1115
|
+
// 5. Update index locally
|
|
1116
|
+
await updateSpecialIndex({
|
|
1117
|
+
type: "keystones",
|
|
1118
|
+
rel8nInfos: [
|
|
1119
|
+
{
|
|
1120
|
+
rel8nName: "keystone",
|
|
1121
|
+
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
1122
|
+
}
|
|
1123
|
+
],
|
|
1124
|
+
metaspace,
|
|
1125
|
+
space
|
|
1126
|
+
});
|
|
1127
|
+
this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
|
|
1128
|
+
await this.loadIbGib({ getLatest: false });
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
|
|
1132
|
+
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async handleSSOClick(providerId) {
|
|
1136
|
+
const isLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId });
|
|
1137
|
+
if (isLinked) {
|
|
1138
|
+
await this.handleSSOUnlink(providerId);
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
await this.handleSSOLink(providerId);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async handleSSOUnlink(providerId) {
|
|
1145
|
+
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
1146
|
+
this.setSSOStatus(`Unlinking ${providerId}...`, "info");
|
|
1147
|
+
try {
|
|
1148
|
+
// 1. Prompt user for passphrase
|
|
1149
|
+
const keystoneService = new KeystoneService_V1();
|
|
1150
|
+
let masterSecret;
|
|
1151
|
+
let isCorrect = false;
|
|
1152
|
+
let tries = 0;
|
|
1153
|
+
const maxTries = 8;
|
|
1154
|
+
while (tries < maxTries && !isCorrect) {
|
|
1155
|
+
try {
|
|
1156
|
+
const promptMsg = tries === 0
|
|
1157
|
+
? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Passphrase) to authorize. Otherwise, click Cancel.`
|
|
1158
|
+
: `Incorrect passphrase. Please try again (${maxTries - tries} tries remaining):`;
|
|
1159
|
+
masterSecret = await promptForSecret({
|
|
1160
|
+
msg: promptMsg,
|
|
1161
|
+
confirm: false
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
masterSecret = undefined;
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
if (!masterSecret) {
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
1172
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
1173
|
+
keystoneIbGib: this.ibGib,
|
|
1174
|
+
signingSecret: masterSecret,
|
|
1175
|
+
poolId: 'manage'
|
|
1176
|
+
});
|
|
1177
|
+
if (!isCorrect) {
|
|
1178
|
+
tries++;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (!isCorrect) {
|
|
1182
|
+
this.setSSOStatus("Unlink cancelled or failed (incorrect passphrase).", "error");
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
1186
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1187
|
+
if (!metaspace)
|
|
1188
|
+
throw new Error("Metaspace not found");
|
|
1189
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1190
|
+
if (!space) {
|
|
1191
|
+
throw new Error("Local space not found");
|
|
1192
|
+
}
|
|
1193
|
+
// 2. Evolve parent keystone to remove the custodian pool
|
|
1194
|
+
const targetPoolId = `custodian-manage-${providerId}`;
|
|
1195
|
+
const evolvedParent = await keystoneService.removePools({
|
|
1196
|
+
latestKeystone: this.ibGib,
|
|
1197
|
+
masterSecret: masterSecret,
|
|
1198
|
+
poolIds: [targetPoolId],
|
|
1199
|
+
metaspace,
|
|
1200
|
+
space
|
|
1201
|
+
});
|
|
1202
|
+
// 3. Sync evolved parent keystone back to server
|
|
1203
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1204
|
+
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1205
|
+
domainAddr: getIbGibAddr({ ibGib: this.ibGib }),
|
|
1206
|
+
keystoneIbGib: evolvedParent,
|
|
1207
|
+
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1208
|
+
});
|
|
1209
|
+
if (!syncRes.success) {
|
|
1210
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1211
|
+
}
|
|
1212
|
+
// 4. Update index locally
|
|
1213
|
+
await updateSpecialIndex({
|
|
1214
|
+
type: "keystones",
|
|
1215
|
+
rel8nInfos: [
|
|
1216
|
+
{
|
|
1217
|
+
rel8nName: "keystone",
|
|
1218
|
+
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
1219
|
+
}
|
|
1220
|
+
],
|
|
1221
|
+
metaspace,
|
|
1222
|
+
space
|
|
1223
|
+
});
|
|
1224
|
+
this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
|
|
1225
|
+
await this.loadIbGib({ getLatest: true });
|
|
1226
|
+
}
|
|
1227
|
+
catch (error) {
|
|
1228
|
+
console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
|
|
1229
|
+
this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
setChangePassphraseStatus(msg, type) {
|
|
1233
|
+
const el = this.elements.changePassphraseStatusEl;
|
|
1234
|
+
el.textContent = msg;
|
|
1235
|
+
el.className = `status-msg ${type}`;
|
|
1236
|
+
el.classList.remove('hidden');
|
|
1237
|
+
}
|
|
1238
|
+
async handleChangePassphrase() {
|
|
1239
|
+
const lc = `${this.lc}[${this.handleChangePassphrase.name}]`;
|
|
1240
|
+
this.setChangePassphraseStatus("Validating entries...", "info");
|
|
1241
|
+
try {
|
|
1242
|
+
const currentPassphrase = this.elements.inputCurrentPassphraseEl.value;
|
|
1243
|
+
const newPassphrase = this.elements.inputNewPassphraseEl.value;
|
|
1244
|
+
const confirmPassphrase = this.elements.inputConfirmPassphraseEl.value;
|
|
1245
|
+
if (!currentPassphrase) {
|
|
1246
|
+
this.setChangePassphraseStatus("Current passphrase is required.", "error");
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (!newPassphrase) {
|
|
1250
|
+
this.setChangePassphraseStatus("New passphrase is required.", "error");
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (newPassphrase !== confirmPassphrase) {
|
|
1254
|
+
this.setChangePassphraseStatus("New passphrase and confirmation do not match.", "error");
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const keystoneService = new KeystoneService_V1();
|
|
1258
|
+
// 1. Verify current passphrase against parent manage pool
|
|
1259
|
+
const isMatch = await keystoneService.verifySigningSecret({
|
|
1260
|
+
keystoneIbGib: this.ibGib,
|
|
1261
|
+
signingSecret: currentPassphrase,
|
|
1262
|
+
poolId: 'manage'
|
|
1263
|
+
});
|
|
1264
|
+
if (!isMatch) {
|
|
1265
|
+
this.setChangePassphraseStatus("Incorrect current passphrase.", "error");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.setChangePassphraseStatus("Evolving primary identity keystone...", "info");
|
|
1269
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1270
|
+
if (!metaspace)
|
|
1271
|
+
throw new Error("Metaspace not found");
|
|
1272
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1273
|
+
if (!space) {
|
|
1274
|
+
throw new Error("Local space not found");
|
|
1275
|
+
}
|
|
1276
|
+
// 2. Rotate parent identity
|
|
1277
|
+
debugger; // walk through this...looking for delegate failure
|
|
1278
|
+
const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
|
|
1279
|
+
keystoneIbGib: this.ibGib,
|
|
1280
|
+
newMasterSecret: newPassphrase,
|
|
1281
|
+
signingSecret: currentPassphrase,
|
|
1282
|
+
signingPoolId: 'manage',
|
|
1283
|
+
metaspace,
|
|
1284
|
+
space
|
|
1285
|
+
});
|
|
1286
|
+
debugger; // look at evolvedDelegates
|
|
1287
|
+
this.setChangePassphraseStatus("Resolving and rotating delegate keystones...", "info");
|
|
1288
|
+
// // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
|
|
1289
|
+
// const delegates = this.ibGib!.data?.delegates || {};
|
|
1290
|
+
// const evolvedDelegates: KeystoneIbGib_V1[] = [];
|
|
1291
|
+
// for (const delegateInfo of Object.values(delegates)) {
|
|
1292
|
+
// const delegateAddr = delegateInfo.delegateAddr;
|
|
1293
|
+
// const delegateKeystone = await keystoneService.getLatestKeystone({
|
|
1294
|
+
// addr: delegateAddr,
|
|
1295
|
+
// metaspace,
|
|
1296
|
+
// space,
|
|
1297
|
+
// });
|
|
1298
|
+
// // Identify user-owned challenge pools in the delegate that match the old passphrase
|
|
1299
|
+
// const poolsToRotate: string[] = [];
|
|
1300
|
+
// for (const pool of delegateKeystone.data?.challengePools ?? []) {
|
|
1301
|
+
// if (!pool.isForeign) {
|
|
1302
|
+
// /**
|
|
1303
|
+
// * WARNING: There is an extreme edge case that if two pools
|
|
1304
|
+
// * happen to be owned by different parties but share the
|
|
1305
|
+
// * same secret, this will change the wrong pool's secret
|
|
1306
|
+
// * also. For V1, this shouldn't matter. But we need a
|
|
1307
|
+
// * better way of tracking pool ownership for V2 (or even
|
|
1308
|
+
// * V1 if we have multi-party support). (DO NOT REMOVE
|
|
1309
|
+
// * THIS COMMENT UNTIL THIS IS ADDRESSED)
|
|
1310
|
+
// */
|
|
1311
|
+
// const isPoolMatch = await keystoneService.verifySigningSecret({
|
|
1312
|
+
// keystoneIbGib: delegateKeystone,
|
|
1313
|
+
// signingSecret: currentPassphrase,
|
|
1314
|
+
// poolId: pool.id
|
|
1315
|
+
// });
|
|
1316
|
+
// if (isPoolMatch) { poolsToRotate.push(pool.id); }
|
|
1317
|
+
// }
|
|
1318
|
+
// }
|
|
1319
|
+
// if (poolsToRotate.length > 0) {
|
|
1320
|
+
// console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
|
|
1321
|
+
// const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
|
|
1322
|
+
// keystoneIbGib: delegateKeystone,
|
|
1323
|
+
// newMasterSecret: newPassphrase,
|
|
1324
|
+
// signingSecret: currentPassphrase,
|
|
1325
|
+
// signingPoolId: 'manage',
|
|
1326
|
+
// poolIds: poolsToRotate,
|
|
1327
|
+
// metaspace,
|
|
1328
|
+
// space
|
|
1329
|
+
// });
|
|
1330
|
+
// evolvedDelegates.push(evolvedDelegate);
|
|
1331
|
+
// }
|
|
1332
|
+
// }
|
|
1333
|
+
// 4. Sync evolved parent and all evolved delegate keystones atomically
|
|
1334
|
+
this.setChangePassphraseStatus("Syncing evolved keystones to server...", "info");
|
|
1335
|
+
const parentTjp = getIbGibAddr({ ibGib: this.ibGib }); // is this.ibGib the tjp always? or is it the latest in the keystone line? Does it matter?
|
|
1336
|
+
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1337
|
+
domainAddr: parentTjp,
|
|
1338
|
+
keystoneIbGib: evolvedParent,
|
|
1339
|
+
relatedKeystoneIbGibs: evolvedDelegates,
|
|
1340
|
+
});
|
|
1341
|
+
if (!syncRes.success) {
|
|
1342
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
// 5. Update index locally
|
|
1345
|
+
this.setChangePassphraseStatus("Updating local indexes...", "info");
|
|
1346
|
+
const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
|
|
1347
|
+
if (evolvedDelegates) {
|
|
1348
|
+
evolvedDelegates.forEach(d => {
|
|
1349
|
+
newKeystoneIbGibs.push(toDto({ ibGib: d }));
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
const rel8nInfos = [
|
|
1353
|
+
{
|
|
1354
|
+
rel8nName: "keystone",
|
|
1355
|
+
ibGibs: newKeystoneIbGibs,
|
|
1356
|
+
}
|
|
1357
|
+
];
|
|
1358
|
+
await updateSpecialIndex({
|
|
1359
|
+
type: "keystones",
|
|
1360
|
+
rel8nInfos,
|
|
1361
|
+
metaspace,
|
|
1362
|
+
space
|
|
1363
|
+
});
|
|
1364
|
+
// Clear inputs
|
|
1365
|
+
this.elements.inputCurrentPassphraseEl.value = "";
|
|
1366
|
+
this.elements.inputNewPassphraseEl.value = "";
|
|
1367
|
+
this.elements.inputConfirmPassphraseEl.value = "";
|
|
1368
|
+
this.setChangePassphraseStatus("Passphrase updated successfully!", "success");
|
|
1369
|
+
await this.loadIbGib({ getLatest: true });
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
console.error(`${lc} Passphrase rotation failed: ${extractErrorMsg(error)}`);
|
|
1373
|
+
this.setChangePassphraseStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
//# sourceMappingURL=keystone-details.mjs.map
|