@lamalibre/create-lamaste 4.3.0 → 4.4.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/package.json +2 -2
- package/scripts/bundle-vendor.js +13 -8
- package/src/index.js +4 -0
- package/src/lib/panel-pin.js +41 -0
- package/src/lib/service-config.js +6 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +50 -2
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +6 -6
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
- package/vendor/docs/02-guides/desktop-app-setup.md +12 -11
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +7 -7
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +393 -33
- package/vendor/docs/04-api-reference/app-host.md +87 -38
- package/vendor/docs/04-api-reference/audit-and-notifications.md +10 -10
- package/vendor/docs/04-api-reference/certificates.md +22 -19
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +33 -11
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -135
- package/vendor/docs/04-api-reference/registry-and-oidc.md +1 -1
- package/vendor/docs/04-api-reference/tunnels.md +14 -10
- package/vendor/docs/05-operations/backup-and-restore.md +4 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +6 -6
- package/vendor/docs/06-reference/config-files.md +94 -84
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +2 -2
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +11 -1
- package/vendor/server/package.json +4 -3
- package/vendor/server/src/commands/panel-pin.js +40 -0
- package/vendor/server/src/commands/plugin-settings.js +541 -0
- package/vendor/server/src/commands/plugins.js +94 -27
- package/vendor/server/src/panel-api.js +281 -34
- package/vendor/server/src/panel-identity.js +165 -0
- package/vendor/server/src/plugin-approval.js +78 -0
- package/vendor/server/src/plugin-settings.js +699 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +1 -0
- package/vendor/server-ui/dist/assets/index-CESyyK8V.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +9 -0
- package/vendor/serverd/src/lib/agent-presence.js +106 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +20 -4
- package/vendor/serverd/src/lib/migrations/state/0012_plugin_settings_and_presence.sql +62 -0
- package/vendor/serverd/src/lib/mtls.js +44 -4
- package/vendor/serverd/src/lib/plugin-hooks.js +120 -20
- package/vendor/serverd/src/lib/plugin-hosting.js +35 -14
- package/vendor/serverd/src/lib/plugin-lifecycle.js +267 -83
- package/vendor/serverd/src/lib/plugin-settings.js +1101 -0
- package/vendor/serverd/src/lib/plugins.js +73 -22
- package/vendor/serverd/src/lib/tunnel-hooks.js +11 -0
- package/vendor/serverd/src/routes/management/plugins.js +166 -19
- package/vendor/serverd/src/routes/management/tunnels.js +21 -1
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +0 -1
- package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +0 -84
|
@@ -69,10 +69,19 @@ async function getStmts() {
|
|
|
69
69
|
INSERT INTO plugins
|
|
70
70
|
(name, route, display_name, package_name, version, description, capabilities,
|
|
71
71
|
packages, panel, config, modes, hosting, agent, hooks, hooks_entry_sha256,
|
|
72
|
-
agent_half_pin, status, installed_at, enabled_at)
|
|
73
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
agent_half_pin, settings, status, installed_at, enabled_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
74
74
|
`),
|
|
75
75
|
deleteByName: db.prepare('DELETE FROM plugins WHERE name = ?'),
|
|
76
|
+
// The relay's cache of the plugin's settings (lib/plugin-settings.js)
|
|
77
|
+
// goes with its row, and follows its settings schema.
|
|
78
|
+
deleteSettingsByName: db.prepare('DELETE FROM plugin_settings WHERE plugin = ?'),
|
|
79
|
+
selectSettingsByName: db.prepare('SELECT * FROM plugin_settings WHERE plugin = ?'),
|
|
80
|
+
updatePrunedSettings: db.prepare(`
|
|
81
|
+
UPDATE plugin_settings
|
|
82
|
+
SET values_json = ?, secrets_set_json = ?, configured = ?, missing_json = ?
|
|
83
|
+
WHERE plugin = ?
|
|
84
|
+
`),
|
|
76
85
|
deleteAll: db.prepare('DELETE FROM plugins'),
|
|
77
86
|
updateStatusEnable: db.prepare(`
|
|
78
87
|
UPDATE plugins SET status = 'enabled', enabled_at = ? WHERE name = ?
|
|
@@ -125,6 +134,7 @@ function rowToPlugin(row) {
|
|
|
125
134
|
if (row.hooks != null) entry.hooks = JSON.parse(row.hooks);
|
|
126
135
|
if (row.hooks_entry_sha256 != null) entry.hooksEntrySha256 = row.hooks_entry_sha256;
|
|
127
136
|
if (row.agent_half_pin != null) entry.agentHalfPin = JSON.parse(row.agent_half_pin);
|
|
137
|
+
if (row.settings != null) entry.settings = JSON.parse(row.settings);
|
|
128
138
|
if (row.enabled_at != null) entry.enabledAt = row.enabled_at;
|
|
129
139
|
return entry;
|
|
130
140
|
}
|
|
@@ -183,6 +193,7 @@ export async function writePlugins(data) {
|
|
|
183
193
|
nullableJson(p.hooks),
|
|
184
194
|
nullableString(p.hooksEntrySha256),
|
|
185
195
|
nullableJson(p.agentHalfPin),
|
|
196
|
+
nullableJson(p.settings),
|
|
186
197
|
p.status,
|
|
187
198
|
p.installedAt,
|
|
188
199
|
nullableString(p.enabledAt),
|
|
@@ -448,12 +459,16 @@ export function installPlugin(packageName, logger) {
|
|
|
448
459
|
hooks: manifest.hooks,
|
|
449
460
|
hooksEntrySha256,
|
|
450
461
|
agentHalfPin,
|
|
462
|
+
settings: manifest.settings,
|
|
451
463
|
status: 'disabled',
|
|
452
464
|
installedAt: new Date().toISOString(),
|
|
453
465
|
};
|
|
454
466
|
|
|
455
467
|
s.begin.run();
|
|
456
468
|
try {
|
|
469
|
+
// No settings are cached for a plugin that is not installed (its
|
|
470
|
+
// uninstall dropped them); none from before may meet this schema.
|
|
471
|
+
s.deleteSettingsByName.run(entry.name);
|
|
457
472
|
s.insert.run(
|
|
458
473
|
entry.name,
|
|
459
474
|
entry.route,
|
|
@@ -471,6 +486,7 @@ export function installPlugin(packageName, logger) {
|
|
|
471
486
|
nullableJson(entry.hooks),
|
|
472
487
|
nullableString(entry.hooksEntrySha256),
|
|
473
488
|
nullableJson(entry.agentHalfPin),
|
|
489
|
+
nullableJson(entry.settings),
|
|
474
490
|
entry.status,
|
|
475
491
|
entry.installedAt,
|
|
476
492
|
null,
|
|
@@ -543,10 +559,13 @@ export function uninstallPlugin(name, logger) {
|
|
|
543
559
|
logger.warn({ err: err.message }, 'Failed to remove plugin state directory');
|
|
544
560
|
});
|
|
545
561
|
|
|
546
|
-
// Remove from registry
|
|
562
|
+
// Remove from registry, with the cache of its settings (the agent half
|
|
563
|
+
// holds the settings themselves; a purge removed them, or they stay with
|
|
564
|
+
// the data an agent keeps of it)
|
|
547
565
|
s.begin.run();
|
|
548
566
|
try {
|
|
549
567
|
s.deleteByName.run(name);
|
|
568
|
+
s.deleteSettingsByName.run(name);
|
|
550
569
|
s.commit.run();
|
|
551
570
|
} catch (err) {
|
|
552
571
|
s.rollback.run();
|
|
@@ -593,34 +612,66 @@ const updateRowSql = `
|
|
|
593
612
|
UPDATE plugins SET
|
|
594
613
|
display_name = ?, version = ?, description = ?, capabilities = ?, packages = ?, panel = ?,
|
|
595
614
|
config = ?, modes = ?, hosting = ?, agent = ?, hooks = ?, hooks_entry_sha256 = ?,
|
|
596
|
-
agent_half_pin = ?
|
|
615
|
+
agent_half_pin = ?, settings = ?
|
|
597
616
|
WHERE name = ?`;
|
|
598
617
|
|
|
599
618
|
/**
|
|
600
619
|
* Rewrite an installed plugin's row from a manifest (an update, or its
|
|
601
620
|
* reversal) with the agent half pinned for that version (null when Lamaste
|
|
602
|
-
* installs none). Status and install time stay.
|
|
621
|
+
* installs none). Status and install time stay. When the settings schema
|
|
622
|
+
* changes, the relay's cache of the settings is pruned in the same
|
|
623
|
+
* transaction — values and secrets whose field changed type or went
|
|
624
|
+
* (`prunedSettingsCache`) — so no read pairs the new schema with values
|
|
625
|
+
* stored for the old one: a value whose field became a secret never shows
|
|
626
|
+
* beside it, and a secret whose field became a plain value is no longer
|
|
627
|
+
* reported set.
|
|
603
628
|
*/
|
|
604
629
|
export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalfPin) {
|
|
605
630
|
const s = await getStmts();
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
631
|
+
const { prunedSettingsCache } = await import('./plugin-settings.js');
|
|
632
|
+
s.begin.run();
|
|
633
|
+
try {
|
|
634
|
+
const before = s.selectByName.get(name);
|
|
635
|
+
s.db
|
|
636
|
+
.prepare(updateRowSql)
|
|
637
|
+
.run(
|
|
638
|
+
nullableString(manifest.displayName),
|
|
639
|
+
manifest.version,
|
|
640
|
+
nullableString(manifest.description),
|
|
641
|
+
JSON.stringify(manifest.capabilities ?? []),
|
|
642
|
+
nullableJson(manifest.packages),
|
|
643
|
+
nullableJson(manifest.panel),
|
|
644
|
+
nullableJson(manifest.config),
|
|
645
|
+
nullableJson(manifest.modes),
|
|
646
|
+
manifest.hosting ?? DEFAULT_PLUGIN_HOSTING,
|
|
647
|
+
nullableJson(manifest.agent),
|
|
648
|
+
nullableJson(manifest.hooks),
|
|
649
|
+
nullableString(hooksEntrySha256),
|
|
650
|
+
nullableJson(agentHalfPin),
|
|
651
|
+
nullableJson(manifest.settings),
|
|
652
|
+
name,
|
|
653
|
+
);
|
|
654
|
+
const pruned = prunedSettingsCache(
|
|
655
|
+
s.selectSettingsByName.get(name),
|
|
656
|
+
before?.settings != null ? JSON.parse(before.settings) : null,
|
|
657
|
+
manifest.settings ?? null,
|
|
623
658
|
);
|
|
659
|
+
if (pruned?.delete) {
|
|
660
|
+
s.deleteSettingsByName.run(name);
|
|
661
|
+
} else if (pruned) {
|
|
662
|
+
s.updatePrunedSettings.run(
|
|
663
|
+
JSON.stringify(pruned.values),
|
|
664
|
+
JSON.stringify(pruned.secretsSet),
|
|
665
|
+
pruned.configured ? 1 : 0,
|
|
666
|
+
JSON.stringify(pruned.missing),
|
|
667
|
+
name,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
s.commit.run();
|
|
671
|
+
} catch (err) {
|
|
672
|
+
s.rollback.run();
|
|
673
|
+
throw err;
|
|
674
|
+
}
|
|
624
675
|
}
|
|
625
676
|
|
|
626
677
|
/**
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* up, or after a restart of the panel. The agent side of the pipeline runs
|
|
10
10
|
* on the agent (its sync follows the relay's view of the tunnel).
|
|
11
11
|
*
|
|
12
|
+
* Whenever the plugin comes up — after a `down`, or first seen up — the
|
|
13
|
+
* relay also re-reads its settings from the agent half: they may have
|
|
14
|
+
* changed while the relay could not see it (a restore, a retyped field
|
|
15
|
+
* dropped by a new version).
|
|
16
|
+
*
|
|
12
17
|
* Calls for one plugin are serialised and run beside the monitor, which
|
|
13
18
|
* does not wait for them.
|
|
14
19
|
*/
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
} from './plugin-hooks.js';
|
|
22
27
|
import { runRelayHook } from './relay-hooks.js';
|
|
23
28
|
import { rememberTunnelFallback } from './request-hooks.js';
|
|
29
|
+
import { refreshPluginSettingsBestEffort } from './plugin-settings.js';
|
|
24
30
|
|
|
25
31
|
const chains = new Map();
|
|
26
32
|
|
|
@@ -34,6 +40,11 @@ export function onHealthTransition(plugin, before, after, logger) {
|
|
|
34
40
|
.then(() => runTunnelHook(plugin, before, after, logger))
|
|
35
41
|
.catch((err) => {
|
|
36
42
|
logger?.error({ plugin, err: err?.message }, 'Tunnel-state hook pipeline failed');
|
|
43
|
+
})
|
|
44
|
+
.then(async () => {
|
|
45
|
+
if (after.state === 'up' && before?.state !== 'up') {
|
|
46
|
+
await refreshPluginSettingsBestEffort(plugin, logger);
|
|
47
|
+
}
|
|
37
48
|
});
|
|
38
49
|
chains.set(plugin, next);
|
|
39
50
|
next.finally(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { AGENT_LABEL_REGEX } from '@lamalibre/lamaste';
|
|
2
|
+
import { AGENT_HALF_SETTINGS_MAX_BYTES, AGENT_LABEL_REGEX } from '@lamalibre/lamaste';
|
|
3
3
|
import {
|
|
4
4
|
readPlugins,
|
|
5
5
|
installPlugin,
|
|
@@ -35,6 +35,11 @@ import {
|
|
|
35
35
|
listPluginOperations,
|
|
36
36
|
takeOperationReveal,
|
|
37
37
|
} from '../../lib/plugin-operations.js';
|
|
38
|
+
import {
|
|
39
|
+
attachSettingsSummaries,
|
|
40
|
+
getPluginSettingsView,
|
|
41
|
+
updatePluginSettings,
|
|
42
|
+
} from '../../lib/plugin-settings.js';
|
|
38
43
|
import { actorFromRequest, certSerialFromRequest } from '../../lib/actor.js';
|
|
39
44
|
import { recordAudit } from '../../lib/audit.js';
|
|
40
45
|
import {
|
|
@@ -112,13 +117,57 @@ function operationErrorBody(err, fallback) {
|
|
|
112
117
|
};
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The error body of a refused settings read or change: the message and what
|
|
122
|
+
* a client acts on — `code` (`no_settings`, `operation_running` with the
|
|
123
|
+
* operation's id, `hosting_offline` with the `hosting`, `revision_mismatch`
|
|
124
|
+
* with the current `revision`, `schema_mismatch`, `settings_unsupported`,
|
|
125
|
+
* `refused`, `timeout`, …) and `errors` per field (400: the panel's check,
|
|
126
|
+
* 422: the agent half's). Built from what the settings library put on the
|
|
127
|
+
* error — never from the request body.
|
|
128
|
+
*/
|
|
129
|
+
function settingsErrorBody(err, fallback) {
|
|
130
|
+
// Only the settings library's own refusals carry a status; anything else
|
|
131
|
+
// is answered without its message.
|
|
132
|
+
if (!err.statusCode) return { error: fallback };
|
|
133
|
+
return {
|
|
134
|
+
error: err.message,
|
|
135
|
+
...(err.code ? { code: err.code } : {}),
|
|
136
|
+
...(err.errors ? { errors: err.errors } : {}),
|
|
137
|
+
...(Number.isInteger(err.revision) ? { revision: err.revision } : {}),
|
|
138
|
+
...(err.operationId ? { operationId: err.operationId } : {}),
|
|
139
|
+
...(err.hosting !== undefined ? { hosting: err.hosting } : {}),
|
|
140
|
+
// The agent half's own status, when it refused with an unexpected one.
|
|
141
|
+
...(Number.isInteger(err.status) ? { status: err.status } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
const OperationParamSchema = z.object({
|
|
116
146
|
id: z.string().uuid(),
|
|
117
147
|
});
|
|
118
148
|
|
|
149
|
+
/**
|
|
150
|
+
* `digest`: the approval digest that was shown — the installed version's or
|
|
151
|
+
* a pending update's. `version`: a pending update's version, which approves
|
|
152
|
+
* that update (with its digest, null when it brings neither relay hooks nor
|
|
153
|
+
* an agent half).
|
|
154
|
+
*/
|
|
119
155
|
const ApproveHooksBodySchema = z
|
|
120
|
-
.object({
|
|
121
|
-
|
|
156
|
+
.object({
|
|
157
|
+
digest: z
|
|
158
|
+
.string()
|
|
159
|
+
.regex(/^[a-f0-9]{64}$/, 'digest is a SHA-256 hex string')
|
|
160
|
+
.nullable()
|
|
161
|
+
.optional(),
|
|
162
|
+
version: z
|
|
163
|
+
.string()
|
|
164
|
+
.regex(/^[0-9A-Za-z.+-]{1,64}$/, 'version is a plugin version')
|
|
165
|
+
.optional(),
|
|
166
|
+
})
|
|
167
|
+
.strict()
|
|
168
|
+
.refine((body) => (body.digest ?? null) !== null || body.version !== undefined, {
|
|
169
|
+
message: 'digest or version is required',
|
|
170
|
+
});
|
|
122
171
|
|
|
123
172
|
const AutoApproveBodySchema = z.object({ enabled: z.boolean() }).strict();
|
|
124
173
|
|
|
@@ -281,7 +330,10 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
281
330
|
// `retainedData`: what agents keep of its agent half after it was
|
|
282
331
|
// removed without purge. `hooks`: its lifecycle hooks and their
|
|
283
332
|
// approval (see GET /plugins/:name/hooks).
|
|
284
|
-
|
|
333
|
+
// `settings`: for a plugin that declares settings, whether it still
|
|
334
|
+
// needs configuration — { configured, missing, syncedAt }, no values
|
|
335
|
+
// (see GET /plugins/:name/settings).
|
|
336
|
+
const plugins = await attachSettingsSummaries(await attachHostings(registry.plugins));
|
|
285
337
|
return {
|
|
286
338
|
plugins: await Promise.all(plugins.map(withHooks)),
|
|
287
339
|
// Every agent's retained data — of uninstalled plugins too, which
|
|
@@ -308,7 +360,7 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
308
360
|
return reply.code(404).send({ error: `Plugin "${params.name}" not found` });
|
|
309
361
|
}
|
|
310
362
|
|
|
311
|
-
const [withHosting] = await attachHostings([plugin]);
|
|
363
|
+
const [withHosting] = await attachSettingsSummaries(await attachHostings([plugin]));
|
|
312
364
|
return { plugin: await withHooks(withHosting) };
|
|
313
365
|
},
|
|
314
366
|
);
|
|
@@ -538,6 +590,89 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
538
590
|
},
|
|
539
591
|
);
|
|
540
592
|
|
|
593
|
+
// ===========================================================================
|
|
594
|
+
// Settings of an agent-hosted plugin (kept by its agent half)
|
|
595
|
+
// ===========================================================================
|
|
596
|
+
|
|
597
|
+
// ------------------------------------------------------------------
|
|
598
|
+
// GET /plugins/:name/settings — { schemaVersion, fields, values, configured,
|
|
599
|
+
// missing, revision, source, syncedAt, hosting }: each non-secret field's
|
|
600
|
+
// effective value, each secret as { set: true, setAt } or { set: false }
|
|
601
|
+
// (never a value). Read from the agent half when its hosting is active
|
|
602
|
+
// and not down (`source: live`), else from the relay's cache (`cache`;
|
|
603
|
+
// `none` when never read). 404 `no_settings` for a plugin without them.
|
|
604
|
+
// ------------------------------------------------------------------
|
|
605
|
+
fastify.get(
|
|
606
|
+
'/plugins/:name/settings',
|
|
607
|
+
{
|
|
608
|
+
preHandler: fastify.requireRole(['admin']),
|
|
609
|
+
},
|
|
610
|
+
async (request, reply) => {
|
|
611
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
612
|
+
try {
|
|
613
|
+
return await getPluginSettingsView(params.name, { logger: request.log });
|
|
614
|
+
} catch (err) {
|
|
615
|
+
if (!err.statusCode) {
|
|
616
|
+
request.log.error(
|
|
617
|
+
{ plugin: params.name, err: err.message },
|
|
618
|
+
'Failed to read the plugin settings',
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return reply
|
|
622
|
+
.code(err.statusCode || 500)
|
|
623
|
+
.send(settingsErrorBody(err, 'Failed to read the plugin settings'));
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// ------------------------------------------------------------------
|
|
629
|
+
// PUT /plugins/:name/settings { revision, values?, secrets? } — a partial
|
|
630
|
+
// change (`null` clears an optional field), sent through the tunnel to
|
|
631
|
+
// the agent half, which stores it: 200 with the settings as above. 400
|
|
632
|
+
// `errors` (checked here), 409 (`operation_running`, the agent half's
|
|
633
|
+
// `revision_mismatch` with `revision`, `schema_mismatch`), 422 `errors`
|
|
634
|
+
// (the agent half's check, normalized), 503 `hosting_offline` with
|
|
635
|
+
// `hosting`, 502 `settings_unsupported` / `refused` / `unauthenticated`
|
|
636
|
+
// (an answer without the agent half's responder signature), 504 `timeout`.
|
|
637
|
+
//
|
|
638
|
+
// The body may carry secrets. It is handed to the settings library once
|
|
639
|
+
// and dropped from the request; nothing here logs or repeats it. The
|
|
640
|
+
// library seals each secret to the one call that carries it through the
|
|
641
|
+
// tunnel.
|
|
642
|
+
// ------------------------------------------------------------------
|
|
643
|
+
fastify.put(
|
|
644
|
+
'/plugins/:name/settings',
|
|
645
|
+
{
|
|
646
|
+
preHandler: fastify.requireRole(['admin']),
|
|
647
|
+
bodyLimit: AGENT_HALF_SETTINGS_MAX_BYTES,
|
|
648
|
+
config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
|
|
649
|
+
},
|
|
650
|
+
async (request, reply) => {
|
|
651
|
+
const body = request.body;
|
|
652
|
+
request.body = undefined;
|
|
653
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
654
|
+
try {
|
|
655
|
+
return await updatePluginSettings(params.name, body, {
|
|
656
|
+
actor: actorFromRequest(request),
|
|
657
|
+
certSerial: certSerialFromRequest(request),
|
|
658
|
+
logger: request.log,
|
|
659
|
+
});
|
|
660
|
+
} catch (err) {
|
|
661
|
+
if (!err.statusCode) {
|
|
662
|
+
// A failure of Lamaste's own (state.db, a malformed hosting record):
|
|
663
|
+
// its message names no value of the change.
|
|
664
|
+
request.log.error(
|
|
665
|
+
{ plugin: params.name, err: err.message },
|
|
666
|
+
'Failed to change the plugin settings',
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
return reply
|
|
670
|
+
.code(err.statusCode || 500)
|
|
671
|
+
.send(settingsErrorBody(err, 'Failed to change the plugin settings'));
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
|
|
541
676
|
// ===========================================================================
|
|
542
677
|
// Lifecycle hooks: review, approval, auto-approve
|
|
543
678
|
// ===========================================================================
|
|
@@ -547,7 +682,8 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
547
682
|
// fail-open flags, effects, digest), the agent hooks, the agent half
|
|
548
683
|
// Lamaste installs (package, version, integrity, service, system
|
|
549
684
|
// packages), the approval (`approval.digest` is what to approve) and a
|
|
550
|
-
// pending update
|
|
685
|
+
// pending update — its relay hook set and agent half, its settings
|
|
686
|
+
// schema diff, why it waits (`approvalRequired`)
|
|
551
687
|
// ------------------------------------------------------------------
|
|
552
688
|
fastify.get(
|
|
553
689
|
'/plugins/:name/hooks',
|
|
@@ -563,10 +699,12 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
563
699
|
);
|
|
564
700
|
|
|
565
701
|
// ------------------------------------------------------------------
|
|
566
|
-
// POST /plugins/:name/hooks/approve — approve what
|
|
567
|
-
// (relay hooks, an agent half Lamaste installs) with
|
|
568
|
-
// shown. Approving a pending update
|
|
569
|
-
//
|
|
702
|
+
// POST /plugins/:name/hooks/approve { digest?, version? } — approve what
|
|
703
|
+
// the plugin brings (relay hooks, an agent half Lamaste installs) with
|
|
704
|
+
// the digest that was shown. Approving a pending update — by its digest,
|
|
705
|
+
// or by its version (and digest, when it has one) — approves everything
|
|
706
|
+
// it waits for (`approvalRequired`) and applies it (202 with its
|
|
707
|
+
// operation).
|
|
570
708
|
// ------------------------------------------------------------------
|
|
571
709
|
fastify.post(
|
|
572
710
|
'/plugins/:name/hooks/approve',
|
|
@@ -582,18 +720,23 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
582
720
|
const actor = actorFromRequest(request);
|
|
583
721
|
const certSerial = certSerialFromRequest(request);
|
|
584
722
|
try {
|
|
585
|
-
const approved = await approveHookSet(plugin, body.digest, {
|
|
723
|
+
const approved = await approveHookSet(plugin, body.digest ?? null, {
|
|
586
724
|
actor,
|
|
587
725
|
certSerial,
|
|
588
726
|
logger: request.log,
|
|
727
|
+
version: body.version ?? null,
|
|
589
728
|
});
|
|
590
729
|
if (approved.target === 'update') {
|
|
591
|
-
|
|
592
|
-
const result = await updatePlugin(plugin.name,
|
|
730
|
+
// The update as it was approved: fetched again, it must still be that.
|
|
731
|
+
const result = await updatePlugin(plugin.name, approved.version, {
|
|
593
732
|
actor,
|
|
594
733
|
certSerial,
|
|
595
734
|
logger: request.log,
|
|
596
|
-
|
|
735
|
+
approvedUpdate: {
|
|
736
|
+
version: approved.version,
|
|
737
|
+
digest: approved.digest,
|
|
738
|
+
secretToPlain: approved.secretToPlain,
|
|
739
|
+
},
|
|
597
740
|
sourceIp: request.ip,
|
|
598
741
|
});
|
|
599
742
|
return reply.code(202).send({ ok: true, ...result });
|
|
@@ -603,7 +746,8 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
603
746
|
const statusCode = err.statusCode || 500;
|
|
604
747
|
return reply.code(statusCode).send({
|
|
605
748
|
...operationErrorBody(err, 'Failed to approve the hook set'),
|
|
606
|
-
|
|
749
|
+
// The digest to review now: null when what waits brings nothing to approve by one.
|
|
750
|
+
...(err.currentDigest !== undefined ? { currentDigest: err.currentDigest } : {}),
|
|
607
751
|
});
|
|
608
752
|
}
|
|
609
753
|
},
|
|
@@ -638,9 +782,11 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
638
782
|
// ------------------------------------------------------------------
|
|
639
783
|
// POST /plugins/:name/update { version? } — fetch the version (default
|
|
640
784
|
// latest) and read it without installing it; pin its agent half. A
|
|
641
|
-
// changed relay hook set or agent half
|
|
642
|
-
//
|
|
643
|
-
// (202 {
|
|
785
|
+
// changed relay hook set or agent half (unless auto-approved), and a
|
|
786
|
+
// secret setting turned plain (always), wait for approval
|
|
787
|
+
// (202 { pendingUpdate }, its `approvalRequired` says why); otherwise the
|
|
788
|
+
// update runs as an operation (202 { operation }). 200 { upToDate } when
|
|
789
|
+
// there is nothing to do.
|
|
644
790
|
// ------------------------------------------------------------------
|
|
645
791
|
fastify.post(
|
|
646
792
|
'/plugins/:name/update',
|
|
@@ -716,7 +862,8 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
716
862
|
details: { packageName: plugin.packageName, version: plugin.version },
|
|
717
863
|
});
|
|
718
864
|
// `hooks`: the relay hook set to review (and approve) before hosting.
|
|
719
|
-
|
|
865
|
+
const [listed] = await attachSettingsSummaries([plugin]);
|
|
866
|
+
return { ok: true, plugin: listed, oidcClients, hooks: await hookStatus(plugin) };
|
|
720
867
|
} catch (err) {
|
|
721
868
|
const statusCode = err.statusCode || 500;
|
|
722
869
|
// `route_taken`: another installed plugin holds the route (`route`, `owner`).
|
|
@@ -24,11 +24,12 @@ import {
|
|
|
24
24
|
} from '@lamalibre/lamaste';
|
|
25
25
|
import { getConfig } from '../../lib/config.js';
|
|
26
26
|
import { readTunnels, readSites } from '../../lib/state.js';
|
|
27
|
-
import { loadAgentRegistry } from '../../lib/mtls.js';
|
|
27
|
+
import { getAgentHostFactsHash, loadAgentRegistry } from '../../lib/mtls.js';
|
|
28
28
|
import { getChiselCredentialIssuedAt } from '../../lib/chisel-users.js';
|
|
29
29
|
import { buildChiselArgs } from '../../lib/chisel-args.js';
|
|
30
30
|
import { listAgentPluginAssignments } from '../../lib/plugin-hosting.js';
|
|
31
31
|
import { pendingAgentJobs } from '../../lib/agent-jobs.js';
|
|
32
|
+
import { recordAgentContact } from '../../lib/agent-presence.js';
|
|
32
33
|
import {
|
|
33
34
|
tunnelNginxDeps,
|
|
34
35
|
tunnelCertbotDeps,
|
|
@@ -236,6 +237,17 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
236
237
|
// Parsed outside the try so a malformed ?agent= is a 400 from the
|
|
237
238
|
// shared Zod error handler, not a 500 from the catch below.
|
|
238
239
|
const { agent } = AgentConfigQuerySchema.parse(request.query);
|
|
240
|
+
// An agent's own call (its sync, every 30 s) is its contact with the
|
|
241
|
+
// relay — what the panel shows as when a hosting agent last synced.
|
|
242
|
+
// An admin naming an agent with ?agent= is not that agent's contact.
|
|
243
|
+
const caller = confinedLabel(request);
|
|
244
|
+
if (caller !== null && AGENT_LABEL_REGEX.test(caller)) {
|
|
245
|
+
try {
|
|
246
|
+
await recordAgentContact(caller);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
request.log.warn({ err: err.message, agent: caller }, 'Failed to record agent contact');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
239
251
|
try {
|
|
240
252
|
const config = getConfig();
|
|
241
253
|
const tunnels = await readTunnels();
|
|
@@ -285,6 +297,14 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
285
297
|
// through its app host helper (`app-install`, `app-activate`,
|
|
286
298
|
// `app-remove`) — see lib/agent-jobs.js for each job's fields.
|
|
287
299
|
agentJobs: AGENT_LABEL_REGEX.test(label) ? await pendingAgentJobs(label) : [],
|
|
300
|
+
// For the agent's own sync: the hash of the host facts stored on
|
|
301
|
+
// its record (null: none), which it compares with its own and
|
|
302
|
+
// reports them again when they differ — a record replaced since its
|
|
303
|
+
// last report (enrolled again on the label, a restored backup)
|
|
304
|
+
// holds none. Not for an admin's ?agent= call.
|
|
305
|
+
...(caller !== null && AGENT_LABEL_REGEX.test(caller)
|
|
306
|
+
? { hostFactsHash: await getAgentHostFactsHash(caller) }
|
|
307
|
+
: {}),
|
|
288
308
|
};
|
|
289
309
|
} catch (err) {
|
|
290
310
|
request.log.error(err, 'Failed to generate agent config');
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:JetBrains Mono,Fira Code,ui-monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:#1118271a;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-zinc{--tw-prose-body:#3f3f46;--tw-prose-headings:#18181b;--tw-prose-lead:#52525b;--tw-prose-links:#18181b;--tw-prose-bold:#18181b;--tw-prose-counters:#71717a;--tw-prose-bullets:#d4d4d8;--tw-prose-hr:#e4e4e7;--tw-prose-quotes:#18181b;--tw-prose-quote-borders:#e4e4e7;--tw-prose-captions:#71717a;--tw-prose-kbd:#18181b;--tw-prose-kbd-shadows:#18181b1a;--tw-prose-code:#18181b;--tw-prose-pre-code:#e4e4e7;--tw-prose-pre-bg:#27272a;--tw-prose-th-borders:#d4d4d8;--tw-prose-td-borders:#e4e4e7;--tw-prose-invert-body:#d4d4d8;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#a1a1aa;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#a1a1aa;--tw-prose-invert-bullets:#52525b;--tw-prose-invert-hr:#3f3f46;--tw-prose-invert-quotes:#f4f4f5;--tw-prose-invert-quote-borders:#3f3f46;--tw-prose-invert-captions:#a1a1aa;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d4d4d8;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#52525b;--tw-prose-invert-td-borders:#3f3f46}.prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-4{bottom:1rem}.left-4{left:1rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.top-6{top:1.5rem}.z-40{z-index:40}.z-50{z-index:50}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.-ml-px{margin-left:-1px}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-36{margin-top:9rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-32{height:8rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-96{height:24rem}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[90vh\]{max-height:90vh}.max-h-full{max-height:100%}.min-h-64{min-height:16rem}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0}.min-w-\[16rem\]{min-width:16rem}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.basis-full{flex-basis:100%}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in{animation:.5s ease-out fade-in}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:1s linear infinite spin}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-all{-webkit-user-select:all;user-select:all}.scroll-mt-6{scroll-margin-top:1.5rem}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-1\.5{-moz-column-gap:.375rem;column-gap:.375rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-0{border-left-width:0}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0}.border-amber-500\/20{border-color:#f59e0b33}.border-amber-500\/30{border-color:#f59e0b4d}.border-amber-700{--tw-border-opacity:1;border-color:rgb(180 83 9/var(--tw-border-opacity,1))}.border-cyan-400{--tw-border-opacity:1;border-color:rgb(34 211 238/var(--tw-border-opacity,1))}.border-cyan-500\/20{border-color:#06b6d433}.border-cyan-500\/30{border-color:#06b6d44d}.border-cyan-500\/60{border-color:#06b6d499}.border-emerald-500\/20{border-color:#10b98133}.border-green-500\/20{border-color:#22c55e33}.border-purple-500\/20{border-color:#a855f733}.border-red-500\/20{border-color:#ef444433}.border-red-500\/30{border-color:#ef44444d}.border-red-500\/70{border-color:#ef4444b3}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.border-transparent{border-color:#0000}.border-violet-500\/20{border-color:#8b5cf633}.border-yellow-500\/20{border-color:#eab30833}.border-yellow-500\/30{border-color:#eab3084d}.border-zinc-600{--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.border-zinc-700{--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.border-zinc-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.border-zinc-800\/50{border-color:#27272a80}.border-t-cyan-400{--tw-border-opacity:1;border-top-color:rgb(34 211 238/var(--tw-border-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/20{background-color:#f59e0b33}.bg-amber-500\/5{background-color:#f59e0b0d}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity,1))}.bg-amber-900\/30{background-color:#78350f4d}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/20{background-color:#3b82f633}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity,1))}.bg-cyan-500{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity,1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-cyan-500\/20{background-color:#06b6d433}.bg-cyan-500\/5{background-color:#06b6d40d}.bg-cyan-600{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-500\/15{background-color:#22c55e26}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:#f973161a}.bg-orange-500\/20{background-color:#f9731633}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-500\/20{background-color:#a855f733}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/20{background-color:#ef444433}.bg-red-500\/5{background-color:#ef44440d}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/20{background-color:#dc262633}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-transparent{background-color:#0000}.bg-violet-500\/10{background-color:#8b5cf61a}.bg-yellow-500\/10{background-color:#eab3081a}.bg-yellow-500\/15{background-color:#eab30826}.bg-yellow-500\/20{background-color:#eab30833}.bg-yellow-500\/5{background-color:#eab3080d}.bg-zinc-500{--tw-bg-opacity:1;background-color:rgb(113 113 122/var(--tw-bg-opacity,1))}.bg-zinc-500\/20{background-color:#71717a33}.bg-zinc-600{--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity,1))}.bg-zinc-700{--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity,1))}.bg-zinc-700\/60{background-color:#3f3f4699}.bg-zinc-800{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.bg-zinc-800\/20{background-color:#27272a33}.bg-zinc-800\/50{background-color:#27272a80}.bg-zinc-800\/60{background-color:#27272a99}.bg-zinc-900{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.bg-zinc-900\/50{background-color:#18181b80}.bg-zinc-950{--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1))}.bg-zinc-950\/40{background-color:#09090b66}.bg-zinc-950\/60{background-color:#09090b99}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pl-1{padding-left:.25rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:JetBrains Mono,Fira Code,ui-monospace,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wider{letter-spacing:.05em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/70{color:#34d399b3}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-violet-400{--tw-text-opacity:1;color:rgb(167 139 250/var(--tw-text-opacity,1))}.text-violet-400\/70{color:#a78bfab3}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-400\/90{color:#facc15e6}.text-yellow-500\/80{color:#eab308cc}.text-zinc-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.text-zinc-200{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.placeholder-zinc-500::placeholder{--tw-placeholder-opacity:1;color:rgb(113 113 122/var(--tw-placeholder-opacity,1))}.placeholder-zinc-600::placeholder{--tw-placeholder-opacity:1;color:rgb(82 82 91/var(--tw-placeholder-opacity,1))}.accent-cyan-500{accent-color:#06b6d4}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.placeholder\:text-zinc-600::placeholder{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.last\:border-b-0:last-child{border-bottom-width:0}.hover\:border-cyan-500:hover{--tw-border-opacity:1;border-color:rgb(6 182 212/var(--tw-border-opacity,1))}.hover\:border-cyan-600:hover{--tw-border-opacity:1;border-color:rgb(8 145 178/var(--tw-border-opacity,1))}.hover\:border-zinc-600:hover{--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.hover\:border-zinc-700:hover{--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.hover\:bg-amber-500:hover{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.hover\:bg-cyan-300:hover{--tw-bg-opacity:1;background-color:rgb(103 232 249/var(--tw-bg-opacity,1))}.hover\:bg-cyan-500:hover{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity,1))}.hover\:bg-cyan-600:hover{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.hover\:bg-cyan-600\/20:hover{background-color:#0891b233}.hover\:bg-emerald-500:hover{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.hover\:bg-green-500:hover{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity,1))}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/20:hover{background-color:#ef444433}.hover\:bg-red-600\/20:hover{background-color:#dc262633}.hover\:bg-red-600\/30:hover{background-color:#dc26264d}.hover\:bg-yellow-600\/20:hover{background-color:#ca8a0433}.hover\:bg-zinc-500:hover{--tw-bg-opacity:1;background-color:rgb(113 113 122/var(--tw-bg-opacity,1))}.hover\:bg-zinc-600:hover{--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity,1))}.hover\:bg-zinc-700:hover{--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity,1))}.hover\:bg-zinc-800:hover{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.hover\:bg-zinc-800\/30:hover{background-color:#27272a4d}.hover\:bg-zinc-800\/50:hover{background-color:#27272a80}.hover\:text-cyan-300:hover{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.hover\:text-cyan-400:hover{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:text-yellow-300:hover{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.hover\:text-zinc-100:hover{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.hover\:text-zinc-200:hover{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.hover\:text-zinc-300:hover{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.hover\:text-zinc-400:hover{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-cyan-400:focus{--tw-border-opacity:1;border-color:rgb(34 211 238/var(--tw-border-opacity,1))}.focus\:border-cyan-500:focus{--tw-border-opacity:1;border-color:rgb(6 182 212/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow,0 0 #0000)}.focus\:ring-cyan-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(34 211 238/var(--tw-ring-opacity,1))}.focus\:ring-cyan-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(6 182 212/var(--tw-ring-opacity,1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:text-zinc-500:disabled{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.prose-headings\:scroll-mt-20 :where(h1,h2,h3,h4,h5,h6,th):not(:where([class~=not-prose],[class~=not-prose] *)){scroll-margin-top:5rem}.prose-headings\:font-mono :where(h1,h2,h3,h4,h5,h6,th):not(:where([class~=not-prose],[class~=not-prose] *)){font-family:JetBrains Mono,Fira Code,ui-monospace,monospace}.prose-a\:text-cyan-400 :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.prose-a\:no-underline :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.hover\:prose-a\:underline :where(a):not(:where([class~=not-prose],[class~=not-prose] *)):hover{text-decoration-line:underline}.prose-blockquote\:border-cyan-400\/30 :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:#22d3ee4d}.prose-blockquote\:text-zinc-400 :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.prose-strong\:text-zinc-200 :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.prose-code\:rounded :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.25rem}.prose-code\:bg-zinc-800 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.prose-code\:px-1\.5 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375rem;padding-right:.375rem}.prose-code\:py-0\.5 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.125rem;padding-bottom:.125rem}.prose-code\:text-cyan-300 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.prose-code\:before\:content-none :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose-code\:after\:content-none :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{--tw-content:none;content:var(--tw-content)}.prose-pre\:border :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){border-width:1px}.prose-pre\:border-zinc-800 :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.prose-pre\:bg-zinc-900 :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.prose-th\:text-zinc-300 :where(th):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.prose-td\:text-zinc-400 :where(td):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.prose-img\:rounded-lg :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.5rem}.prose-hr\:border-zinc-800 :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}@media (width>=640px){.sm\:table-cell{display:table-cell}}@media (width>=768px){.md\:block{display:block}.md\:table-cell{display:table-cell}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (width>=1024px){.lg\:flex{display:flex}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:p-8{padding:2rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (width>=1280px){.xl\:block{display:block}}
|