tr-plugin-auth 0.0.0-stage → 1.0.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/README.md +45 -2
- package/cli.mjs +384 -0
- package/package.json +40 -4
- package/plugin.json +266 -0
- package/src/access-keys.mjs +225 -0
- package/src/agent-dir.mjs +3 -0
- package/src/browser-session.mjs +240 -0
- package/src/cli-secret.mjs +1687 -0
- package/src/config.mjs +4 -0
- package/src/configuration.mjs +19 -0
- package/src/credential-preservation.mjs +28 -0
- package/src/crypto.mjs +195 -0
- package/src/env-import.mjs +502 -0
- package/src/fact-seeker.mjs +3 -0
- package/src/frontmatter.mjs +3 -0
- package/src/google-oauth.mjs +139 -0
- package/src/host-runtime.mjs +7 -0
- package/src/leader-election.mjs +9 -0
- package/src/logger.mjs +3 -0
- package/src/mesh-auth.mjs +7 -0
- package/src/mesh-identity.mjs +16 -0
- package/src/mesh-secrets.mjs +146 -0
- package/src/mesh.mjs +23 -0
- package/src/metadata-state.mjs +17 -0
- package/src/project-id.mjs +3 -0
- package/src/provider-account-sync.mjs +1145 -0
- package/src/provider-catalog.mjs +515 -0
- package/src/provider-rotation-recipes.mjs +299 -0
- package/src/research-gate.mjs +3 -0
- package/src/research-queue.mjs +3 -0
- package/src/rotation-capability.mjs +189 -0
- package/src/routes/auth.mjs +207 -0
- package/src/routes/keys.mjs +61 -0
- package/src/routes/secrets-runtime.mjs +5 -0
- package/src/routes/secrets.mjs +747 -0
- package/src/routes/webauthn.mjs +163 -0
- package/src/runtime-credentials.mjs +19 -0
- package/src/runtime.mjs +3 -0
- package/src/secret-integration-research.mjs +340 -0
- package/src/secrets-env-diff.mjs +132 -0
- package/src/secrets-env-export.mjs +322 -0
- package/src/secrets-gateway.mjs +253 -0
- package/src/secrets-keychain.mjs +83 -0
- package/src/secrets-rekey.mjs +366 -0
- package/src/secrets-remote-deploy.mjs +250 -0
- package/src/secrets-rotate.mjs +437 -0
- package/src/secrets-store.mjs +756 -0
- package/src/secrets-sync.mjs +81 -0
- package/src/secure-file.mjs +71 -0
- package/src/server-auth.mjs +500 -0
- package/src/session-credentials.mjs +71 -0
- package/src/ssss-store.mjs +13 -0
- package/src/stdin.mjs +31 -0
- package/src/throttled-fetch.mjs +3 -0
- package/src/vault.mjs +3 -0
- package/src/vfs-documents.mjs +3 -0
- package/src/webauthn-store.mjs +265 -0
- package/ui/DESIGN.md +19 -0
- package/ui/card.js +274 -0
- package/ui/panel.js +338 -0
package/ui/panel.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Auth Management Panel Web Component for Total Recall
|
|
2
|
+
// Exposes Passkey registration, Google OAuth2 configuration, and status verification
|
|
3
|
+
|
|
4
|
+
const esc = (value) =>
|
|
5
|
+
String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
6
|
+
|
|
7
|
+
class AuthPanel extends HTMLElement {
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
this.attachShadow({ mode: 'open' });
|
|
10
|
+
this.status = null;
|
|
11
|
+
this.error = '';
|
|
12
|
+
this.info = '';
|
|
13
|
+
this.probeResult = '';
|
|
14
|
+
this.load();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async load() {
|
|
18
|
+
this.shadowRoot.innerHTML = '<p role="status">Loading Auth Engine status…</p>';
|
|
19
|
+
try {
|
|
20
|
+
if (this.host?.run) {
|
|
21
|
+
const result = await this.host.run('status', ['--json']);
|
|
22
|
+
if (result?.code && result.code !== 0) throw new Error('Auth status request failed');
|
|
23
|
+
this.status = typeof result?.output === 'string' ? JSON.parse(result.output) : result;
|
|
24
|
+
} else {
|
|
25
|
+
const res = await fetch('/auth/providers');
|
|
26
|
+
if (!res.ok) throw new Error(`Auth status returned HTTP ${res.status}`);
|
|
27
|
+
this.status = await res.json();
|
|
28
|
+
}
|
|
29
|
+
this.render();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
this.error = `Unable to query auth status: ${err.message}`;
|
|
32
|
+
this.render();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async registerPasskey() {
|
|
37
|
+
this.info = 'Beginning WebAuthn registration…';
|
|
38
|
+
this.render();
|
|
39
|
+
try {
|
|
40
|
+
const optRes = await fetch('/api/webauthn/register/options', { method: 'POST' });
|
|
41
|
+
if (!optRes.ok) {
|
|
42
|
+
throw new Error('Server rejected registration options request');
|
|
43
|
+
}
|
|
44
|
+
if (!globalThis.PublicKeyCredential?.parseCreationOptionsFromJSON || !navigator.credentials?.create) throw new Error('Passkey registration requires a supported secure browser');
|
|
45
|
+
const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(await optRes.json());
|
|
46
|
+
const credential = await navigator.credentials.create({ publicKey });
|
|
47
|
+
if (!credential?.toJSON) throw new Error('Passkey registration was not completed');
|
|
48
|
+
const verified = await fetch('/api/webauthn/register/verify', {
|
|
49
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ response: credential.toJSON() }),
|
|
51
|
+
});
|
|
52
|
+
const result = await verified.json();
|
|
53
|
+
if (!verified.ok || result.verified !== true) throw new Error('Passkey verification failed');
|
|
54
|
+
this.info = 'Passkey registered and verified.';
|
|
55
|
+
await this.load();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
this.error = `Passkey registration error: ${err.message}`;
|
|
58
|
+
this.render();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async runProbe() {
|
|
63
|
+
this.probeResult = 'Probing /auth/status…';
|
|
64
|
+
this.render();
|
|
65
|
+
const start = performance.now();
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch('/auth/status');
|
|
68
|
+
const latency = Math.round(performance.now() - start);
|
|
69
|
+
this.probeResult = `HTTP ${res.status} ${res.ok ? 'reachable' : 'request failed'} (${latency}ms)`;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.probeResult = `Probe error: ${err.message}`;
|
|
72
|
+
}
|
|
73
|
+
this.render();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async googleAction(action) {
|
|
77
|
+
this.error = ''; this.info = '';
|
|
78
|
+
const id = this.shadowRoot.getElementById('google-account-id')?.value.trim();
|
|
79
|
+
try {
|
|
80
|
+
if (action !== 'accounts' && !id) throw new Error('Choose an account identifier');
|
|
81
|
+
const args = action === 'accounts' ? ['accounts', '--json'] : [action, id, '--json'];
|
|
82
|
+
let result;
|
|
83
|
+
if (this.host?.run) result = await this.host.run('google', args);
|
|
84
|
+
else {
|
|
85
|
+
const url = action === 'accounts' ? '/auth/google/accounts' : `/auth/google/accounts/${encodeURIComponent(id)}/${action}`;
|
|
86
|
+
const response = await fetch(url, { method: action === 'accounts' ? 'GET' : 'POST' });
|
|
87
|
+
if (!response.ok) throw new Error('Connection command failed');
|
|
88
|
+
result = await response.json();
|
|
89
|
+
}
|
|
90
|
+
if (result?.code && result.code !== 0) throw new Error('Connection command failed');
|
|
91
|
+
this.info = typeof result?.output === 'string' ? result.output : JSON.stringify(result, null, 2);
|
|
92
|
+
} catch (error) { this.error = error.message; }
|
|
93
|
+
this.render();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async keyAction(action) {
|
|
97
|
+
this.error = ''; this.info = '';
|
|
98
|
+
const value = id => this.shadowRoot.getElementById(id)?.value.trim();
|
|
99
|
+
try {
|
|
100
|
+
if (!this.host?.run) throw new Error('Use the connected CLI command surface for key management');
|
|
101
|
+
let args = [action];
|
|
102
|
+
if (action === 'issue') {
|
|
103
|
+
const scopes = [...this.shadowRoot.querySelectorAll('input[name="key-scope"]:checked')].map(input => input.value).join(',');
|
|
104
|
+
if (!value('key-name') || !scopes) throw new Error('Choose a name and explicit permissions');
|
|
105
|
+
args = ['issue', value('key-name'), '--scopes', scopes, '--kind', value('key-kind')];
|
|
106
|
+
if (value('key-role')) args.push('--role', value('key-role'));
|
|
107
|
+
if (value('key-expiry')) args.push('--expires', value('key-expiry'));
|
|
108
|
+
} else if (action === 'role-set') {
|
|
109
|
+
const scopes = [...this.shadowRoot.querySelectorAll('input[name="key-scope"]:checked')].map(input => input.value).join(',');
|
|
110
|
+
if (!value('key-role')) throw new Error('Choose a role name');
|
|
111
|
+
args = ['role-set', value('key-role'), '--scopes', scopes];
|
|
112
|
+
} else if (['revoke', 'usage'].includes(action)) {
|
|
113
|
+
if (!value('key-id')) throw new Error('Choose a key identifier'); args.push(value('key-id'));
|
|
114
|
+
}
|
|
115
|
+
const result = await this.host.run('key', [...args, '--json']);
|
|
116
|
+
if (result?.code && result.code !== 0) throw new Error('Key command failed');
|
|
117
|
+
this.info = typeof result?.output === 'string' ? result.output : JSON.stringify(result, null, 2);
|
|
118
|
+
} catch (error) { this.error = error.message; }
|
|
119
|
+
this.render();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
render() {
|
|
123
|
+
const passkeyCount = this.status?.passkey?.count ?? this.status?.passkey?.registered_count ?? null;
|
|
124
|
+
const googleReady = Boolean(this.status?.google?.configured || this.status?.google_oauth2?.configured);
|
|
125
|
+
|
|
126
|
+
this.shadowRoot.innerHTML = `
|
|
127
|
+
<style>
|
|
128
|
+
:host {
|
|
129
|
+
display: block;
|
|
130
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
color: #e5e7eb;
|
|
133
|
+
background: #0f1219;
|
|
134
|
+
border-radius: 12px;
|
|
135
|
+
padding: 16px;
|
|
136
|
+
}
|
|
137
|
+
* { box-sizing: border-box; }
|
|
138
|
+
header {
|
|
139
|
+
display: flex;
|
|
140
|
+
justify-content: space-between;
|
|
141
|
+
align-items: center;
|
|
142
|
+
margin-bottom: 16px;
|
|
143
|
+
border-bottom: 1px solid #1f293d;
|
|
144
|
+
padding-bottom: 12px;
|
|
145
|
+
}
|
|
146
|
+
h2 { margin: 0; font-size: 18px; color: #f3f4f6; }
|
|
147
|
+
section {
|
|
148
|
+
background: #151a24;
|
|
149
|
+
border: 1px solid #232d40;
|
|
150
|
+
border-radius: 10px;
|
|
151
|
+
padding: 14px;
|
|
152
|
+
margin-bottom: 14px;
|
|
153
|
+
}
|
|
154
|
+
h3 { margin-top: 0; font-size: 14px; color: #c7d2fe; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
155
|
+
.row { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; }
|
|
156
|
+
.badge {
|
|
157
|
+
display: inline-block;
|
|
158
|
+
padding: 3px 8px;
|
|
159
|
+
border-radius: 999px;
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
}
|
|
163
|
+
.badge-ok { background: rgba(16, 185, 129, 0.2); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.4); }
|
|
164
|
+
.badge-muted { background: rgba(156, 163, 175, 0.2); color: #9ca3af; border: 1px solid rgba(156, 163, 175, 0.3); }
|
|
165
|
+
button {
|
|
166
|
+
min-height: 40px;
|
|
167
|
+
padding: 8px 16px;
|
|
168
|
+
border-radius: 8px;
|
|
169
|
+
border: 1px solid #374151;
|
|
170
|
+
background: #252e42;
|
|
171
|
+
color: #f9fafb;
|
|
172
|
+
font-weight: 600;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
}
|
|
175
|
+
button.primary {
|
|
176
|
+
background: #4f46e5;
|
|
177
|
+
border-color: #6366f1;
|
|
178
|
+
}
|
|
179
|
+
button:hover { background: #374151; }
|
|
180
|
+
button.primary:hover { background: #4338ca; }
|
|
181
|
+
input {
|
|
182
|
+
width: 100%;
|
|
183
|
+
padding: 10px;
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
border: 1px solid #374151;
|
|
186
|
+
background: #0b0e14;
|
|
187
|
+
color: #fff;
|
|
188
|
+
font-family: monospace;
|
|
189
|
+
margin-top: 4px;
|
|
190
|
+
}
|
|
191
|
+
label { display: block; margin: 8px 0; color: #9ca3af; font-size: 12px; }
|
|
192
|
+
.alert-error { color: #f87171; background: rgba(248, 113, 113, 0.1); padding: 8px; border-radius: 6px; }
|
|
193
|
+
.alert-info { color: #60a5fa; background: rgba(96, 165, 250, 0.1); padding: 8px; border-radius: 6px; }
|
|
194
|
+
pre { background: #06080d; padding: 10px; border-radius: 6px; font-size: 12px; overflow-x: auto; color: #818cf8; }
|
|
195
|
+
</style>
|
|
196
|
+
|
|
197
|
+
<header>
|
|
198
|
+
<h2> Auth & Credentials</h2>
|
|
199
|
+
<button id="btn-refresh">Refresh Status</button>
|
|
200
|
+
</header>
|
|
201
|
+
|
|
202
|
+
${this.error ? `<div class="alert-error">${esc(this.error)}</div>` : ''}
|
|
203
|
+
${this.info ? `<div class="alert-info">${esc(this.info)}</div>` : ''}
|
|
204
|
+
|
|
205
|
+
<section>
|
|
206
|
+
<h3>Passkey WebAuthn Authentication</h3>
|
|
207
|
+
<div class="row">
|
|
208
|
+
<span>Active Credentials</span>
|
|
209
|
+
<span class="badge ${passkeyCount > 0 ? 'badge-ok' : 'badge-muted'}">${passkeyCount === null ? 'Not checked' : `${passkeyCount} Registered`}</span>
|
|
210
|
+
</div>
|
|
211
|
+
<p style="color:#9ca3af;font-size:12px;margin:8px 0;">
|
|
212
|
+
Authenticate securely using platform biometrics such as Touch ID, Face ID, or Windows Hello.
|
|
213
|
+
</p>
|
|
214
|
+
<button class="primary" id="btn-register-passkey">Register New Passkey</button>
|
|
215
|
+
</section>
|
|
216
|
+
|
|
217
|
+
<section>
|
|
218
|
+
<h3>Google OAuth2 Authentication</h3>
|
|
219
|
+
<div class="row">
|
|
220
|
+
<span>Integration Status</span>
|
|
221
|
+
<span class="badge ${googleReady ? 'badge-ok' : 'badge-muted'}">${this.status ? (googleReady ? 'Configured · not verified' : 'Unconfigured') : 'Not checked'}</span>
|
|
222
|
+
</div>
|
|
223
|
+
<form id="google-form">
|
|
224
|
+
<label>
|
|
225
|
+
Google Client ID
|
|
226
|
+
<input readonly name="client_id" placeholder="apps.googleusercontent.com identifier" value="${esc(
|
|
227
|
+
this.status?.google?.client_id || '',
|
|
228
|
+
)}">
|
|
229
|
+
</label>
|
|
230
|
+
<label>
|
|
231
|
+
Redirect URI
|
|
232
|
+
<input readonly value="${esc(new URL('/auth/google/callback', location.origin).href)}">
|
|
233
|
+
</label>
|
|
234
|
+
</form>
|
|
235
|
+
<a href="/auth/google" target="_blank" rel="noopener">Connect account</a>
|
|
236
|
+
<label>Account identifier<input id="google-account-id" placeholder="Identifier from connections"></label>
|
|
237
|
+
<button id="google-accounts">List connections</button>
|
|
238
|
+
<button id="google-refresh">Refresh access</button>
|
|
239
|
+
<button id="google-disconnect">Disconnect account</button>
|
|
240
|
+
</section>
|
|
241
|
+
|
|
242
|
+
<section>
|
|
243
|
+
<h3>Encrypted credentials</h3>
|
|
244
|
+
<p style="color:#9ca3af;font-size:12px;margin:8px 0;">
|
|
245
|
+
Save credentials through auth. Provider verification results appear in the response.
|
|
246
|
+
</p>
|
|
247
|
+
<form id="secrets-form">
|
|
248
|
+
<label>
|
|
249
|
+
Secret Key Name
|
|
250
|
+
<select id="panel-secret-key" style="width:100%;padding:10px;border-radius:6px;border:1px solid #374151;background:#0b0e14;color:#fff;">
|
|
251
|
+
<option value="GOOGLE_CLIENT_ID">GOOGLE_CLIENT_ID</option>
|
|
252
|
+
<option value="GOOGLE_CLIENT_SECRET">GOOGLE_CLIENT_SECRET</option>
|
|
253
|
+
<option value="GOOGLE_REDIRECT_URI">GOOGLE_REDIRECT_URI</option>
|
|
254
|
+
<option value="GOOGLE_ALLOWED_EMAILS">GOOGLE_ALLOWED_EMAILS</option>
|
|
255
|
+
<option value="SESSION_SECRET">SESSION_SECRET</option>
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
<label>
|
|
259
|
+
Secret Value
|
|
260
|
+
<input id="panel-secret-val" type="password" placeholder="Enter key value" required>
|
|
261
|
+
</label>
|
|
262
|
+
<div style="margin-top:10px;">
|
|
263
|
+
<button class="primary" type="submit" id="btn-save-secrets">Save to Total Recall Secrets</button>
|
|
264
|
+
</div>
|
|
265
|
+
</form>
|
|
266
|
+
</section>
|
|
267
|
+
|
|
268
|
+
<section>
|
|
269
|
+
<h3>Access keys and permissions</h3>
|
|
270
|
+
<label>Name<input id="key-name"></label>
|
|
271
|
+
<label>Key type<select id="key-kind"><option value="pat">Personal access token</option><option value="api_key">Application API key</option></select></label>
|
|
272
|
+
<label>Role ceiling<input id="key-role" placeholder="reader, operator, admin or configured role"></label>
|
|
273
|
+
<label>Expiry<input id="key-expiry" type="datetime-local"></label>
|
|
274
|
+
<fieldset><legend>Permissions</legend>
|
|
275
|
+
${['*', 'chat:read', 'chat:write', 'models:read', 'instructions:read', 'ssss:read', 'ssss:write', 'memory:read', 'memory:write', 'memory:recompile', 'sandbox:run', 'tts:use', 'tasks:read', 'tasks:write', 'files:read', 'config:read', 'config:write', 'keys:read', 'keys:write', 'health:read'].map(scope => `<label><input type="checkbox" name="key-scope" value="${scope}">${scope}</label>`).join('')}
|
|
276
|
+
</fieldset>
|
|
277
|
+
<p>Issued tokens are shown once. Save the result securely.</p>
|
|
278
|
+
<label>Existing key identifier<input id="key-id"></label>
|
|
279
|
+
<button id="key-issue">Issue key</button><button id="key-list">List keys</button>
|
|
280
|
+
<button id="key-revoke">Revoke key</button><button id="key-usage">Usage history</button><button id="key-roles">Role policies</button><button id="key-role-set">Save role permissions</button>
|
|
281
|
+
</section>
|
|
282
|
+
<section>
|
|
283
|
+
<h3>Auth endpoint check</h3>
|
|
284
|
+
<p style="color:#9ca3af;font-size:12px;margin:8px 0;">
|
|
285
|
+
Execute an HTTP check against the auth engine endpoints to confirm live server health.
|
|
286
|
+
</p>
|
|
287
|
+
<div class="row">
|
|
288
|
+
<button id="btn-probe">Check endpoint</button>
|
|
289
|
+
<span>${esc(this.probeResult || 'Awaiting probe execution')}</span>
|
|
290
|
+
</div>
|
|
291
|
+
</section>
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
this.shadowRoot.getElementById('btn-refresh')?.addEventListener('click', () => {
|
|
295
|
+
this.error = '';
|
|
296
|
+
this.info = '';
|
|
297
|
+
this.load();
|
|
298
|
+
});
|
|
299
|
+
this.shadowRoot.getElementById('btn-register-passkey')?.addEventListener('click', () => this.registerPasskey());
|
|
300
|
+
this.shadowRoot.getElementById('btn-probe')?.addEventListener('click', () => this.runProbe());
|
|
301
|
+
for (const action of ['accounts', 'refresh', 'disconnect']) this.shadowRoot.getElementById(`google-${action}`)?.addEventListener('click', () => this.googleAction(action));
|
|
302
|
+
for (const action of ['issue', 'list', 'revoke', 'usage', 'roles', 'role-set']) this.shadowRoot.getElementById(`key-${action}`)?.addEventListener('click', () => this.keyAction(action));
|
|
303
|
+
this.shadowRoot.getElementById('secrets-form')?.addEventListener('submit', async (event) => {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
const key = this.shadowRoot.getElementById('panel-secret-key')?.value;
|
|
306
|
+
const val = this.shadowRoot.getElementById('panel-secret-val')?.value?.trim();
|
|
307
|
+
|
|
308
|
+
if (!val) return;
|
|
309
|
+
this.info = `Saving ${key} to Total Recall secrets store…`;
|
|
310
|
+
this.render();
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch('/api/secrets', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
key,
|
|
318
|
+
value: val,
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
if (res.ok) {
|
|
322
|
+
this.info = `Credential ${key} saved through auth.`;
|
|
323
|
+
} else {
|
|
324
|
+
const data = await res.json().catch(() => ({}));
|
|
325
|
+
this.error = `Failed to save credential: ${data.error || 'Request rejected'}`;
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
this.error = `Error saving credential: ${err.message}`;
|
|
329
|
+
}
|
|
330
|
+
this.render();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!customElements.get('tr-auth-panel')) {
|
|
336
|
+
customElements.define('tr-auth-panel', AuthPanel);
|
|
337
|
+
}
|
|
338
|
+
export { AuthPanel };
|