unotoken 0.1.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.
Files changed (122) hide show
  1. package/README.md +360 -0
  2. package/dist/cli.d.ts +17 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +1207 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/client.d.ts +15 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +15 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/db.d.ts +52 -0
  11. package/dist/db.d.ts.map +1 -0
  12. package/dist/db.js +97 -0
  13. package/dist/db.js.map +1 -0
  14. package/dist/dotenv.d.ts +69 -0
  15. package/dist/dotenv.d.ts.map +1 -0
  16. package/dist/dotenv.js +115 -0
  17. package/dist/dotenv.js.map +1 -0
  18. package/dist/env-mapper.d.ts +55 -0
  19. package/dist/env-mapper.d.ts.map +1 -0
  20. package/dist/env-mapper.js +97 -0
  21. package/dist/env-mapper.js.map +1 -0
  22. package/dist/exec.d.ts +80 -0
  23. package/dist/exec.d.ts.map +1 -0
  24. package/dist/exec.js +214 -0
  25. package/dist/exec.js.map +1 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +43 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth/commands.d.ts +151 -0
  31. package/dist/oauth/commands.d.ts.map +1 -0
  32. package/dist/oauth/commands.js +322 -0
  33. package/dist/oauth/commands.js.map +1 -0
  34. package/dist/oauth/config.d.ts +84 -0
  35. package/dist/oauth/config.d.ts.map +1 -0
  36. package/dist/oauth/config.js +156 -0
  37. package/dist/oauth/config.js.map +1 -0
  38. package/dist/oauth/crypto-helpers.d.ts +44 -0
  39. package/dist/oauth/crypto-helpers.d.ts.map +1 -0
  40. package/dist/oauth/crypto-helpers.js +94 -0
  41. package/dist/oauth/crypto-helpers.js.map +1 -0
  42. package/dist/oauth/device-secret.d.ts +57 -0
  43. package/dist/oauth/device-secret.d.ts.map +1 -0
  44. package/dist/oauth/device-secret.js +106 -0
  45. package/dist/oauth/device-secret.js.map +1 -0
  46. package/dist/oauth/flow.d.ts +112 -0
  47. package/dist/oauth/flow.d.ts.map +1 -0
  48. package/dist/oauth/flow.js +255 -0
  49. package/dist/oauth/flow.js.map +1 -0
  50. package/dist/oauth/index.d.ts +18 -0
  51. package/dist/oauth/index.d.ts.map +1 -0
  52. package/dist/oauth/index.js +24 -0
  53. package/dist/oauth/index.js.map +1 -0
  54. package/dist/oauth/key-wrap.d.ts +146 -0
  55. package/dist/oauth/key-wrap.d.ts.map +1 -0
  56. package/dist/oauth/key-wrap.js +275 -0
  57. package/dist/oauth/key-wrap.js.map +1 -0
  58. package/dist/oauth/pkce.d.ts +29 -0
  59. package/dist/oauth/pkce.d.ts.map +1 -0
  60. package/dist/oauth/pkce.js +34 -0
  61. package/dist/oauth/pkce.js.map +1 -0
  62. package/dist/oauth/provider.d.ts +79 -0
  63. package/dist/oauth/provider.d.ts.map +1 -0
  64. package/dist/oauth/provider.js +10 -0
  65. package/dist/oauth/provider.js.map +1 -0
  66. package/dist/oauth/providers/github.d.ts +75 -0
  67. package/dist/oauth/providers/github.d.ts.map +1 -0
  68. package/dist/oauth/providers/github.js +119 -0
  69. package/dist/oauth/providers/github.js.map +1 -0
  70. package/dist/oauth/providers/google.d.ts +115 -0
  71. package/dist/oauth/providers/google.d.ts.map +1 -0
  72. package/dist/oauth/providers/google.js +285 -0
  73. package/dist/oauth/providers/google.js.map +1 -0
  74. package/dist/sdk.d.ts +8 -0
  75. package/dist/sdk.d.ts.map +1 -0
  76. package/dist/sdk.js +8 -0
  77. package/dist/sdk.js.map +1 -0
  78. package/dist/server.d.ts +33 -0
  79. package/dist/server.d.ts.map +1 -0
  80. package/dist/server.js +287 -0
  81. package/dist/server.js.map +1 -0
  82. package/dist/signatures/approval-codes.d.ts +192 -0
  83. package/dist/signatures/approval-codes.d.ts.map +1 -0
  84. package/dist/signatures/approval-codes.js +407 -0
  85. package/dist/signatures/approval-codes.js.map +1 -0
  86. package/dist/signatures/commands.d.ts +108 -0
  87. package/dist/signatures/commands.d.ts.map +1 -0
  88. package/dist/signatures/commands.js +270 -0
  89. package/dist/signatures/commands.js.map +1 -0
  90. package/dist/signatures/devices.d.ts +165 -0
  91. package/dist/signatures/devices.d.ts.map +1 -0
  92. package/dist/signatures/devices.js +344 -0
  93. package/dist/signatures/devices.js.map +1 -0
  94. package/dist/signatures/email-config.d.ts +102 -0
  95. package/dist/signatures/email-config.d.ts.map +1 -0
  96. package/dist/signatures/email-config.js +188 -0
  97. package/dist/signatures/email-config.js.map +1 -0
  98. package/dist/signatures/email.d.ts +106 -0
  99. package/dist/signatures/email.d.ts.map +1 -0
  100. package/dist/signatures/email.js +180 -0
  101. package/dist/signatures/email.js.map +1 -0
  102. package/dist/signatures/fingerprint.d.ts +70 -0
  103. package/dist/signatures/fingerprint.d.ts.map +1 -0
  104. package/dist/signatures/fingerprint.js +123 -0
  105. package/dist/signatures/fingerprint.js.map +1 -0
  106. package/dist/signatures/guard.d.ts +118 -0
  107. package/dist/signatures/guard.d.ts.map +1 -0
  108. package/dist/signatures/guard.js +310 -0
  109. package/dist/signatures/guard.js.map +1 -0
  110. package/dist/signatures/resend.d.ts +84 -0
  111. package/dist/signatures/resend.d.ts.map +1 -0
  112. package/dist/signatures/resend.js +248 -0
  113. package/dist/signatures/resend.js.map +1 -0
  114. package/dist/token-requests.d.ts +80 -0
  115. package/dist/token-requests.d.ts.map +1 -0
  116. package/dist/token-requests.js +201 -0
  117. package/dist/token-requests.js.map +1 -0
  118. package/dist/tokens.d.ts +80 -0
  119. package/dist/tokens.d.ts.map +1 -0
  120. package/dist/tokens.js +150 -0
  121. package/dist/tokens.js.map +1 -0
  122. package/package.json +62 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Device management commands for unotoken CLI.
3
+ *
4
+ * Provides the logic behind `unotoken device list|rename|remove`:
5
+ * - List all approved devices with current-device indicator
6
+ * - Rename a device by fingerprint prefix
7
+ * - Remove a device (with confirmation and safety checks)
8
+ * - Remove all devices except current (nuclear option)
9
+ *
10
+ * Fingerprint prefix matching works like git SHA prefixes -- the user
11
+ * provides the first N characters and the command finds the unique match.
12
+ * If ambiguous, the command lists matches and asks the user to be more specific.
13
+ *
14
+ * @module
15
+ */
16
+ import { DevicesDatabase } from './devices.js';
17
+ import { generateDeviceFingerprint } from './fingerprint.js';
18
+ import { hasVerifiedEmail } from './email-config.js';
19
+ // ─── Constants ──────────────────────────────────────────────────────
20
+ /** Minimum fingerprint prefix length for matching */
21
+ const MIN_PREFIX_LENGTH = 8;
22
+ // ─── Prefix Resolution ─────────────────────────────────────────────
23
+ /**
24
+ * Resolve a fingerprint prefix to a single device.
25
+ *
26
+ * @param db - The DevicesDatabase instance
27
+ * @param prefix - The fingerprint prefix (minimum 8 chars)
28
+ * @returns Match result with the matched devices
29
+ */
30
+ export function resolvePrefix(db, prefix) {
31
+ if (prefix.length < MIN_PREFIX_LENGTH) {
32
+ return {
33
+ matches: [],
34
+ exact: false,
35
+ error: `Fingerprint prefix too short (minimum ${MIN_PREFIX_LENGTH} characters). Got ${prefix.length}.`,
36
+ };
37
+ }
38
+ const matches = db.findByFingerprintPrefix(prefix);
39
+ if (matches.length === 0) {
40
+ return {
41
+ matches: [],
42
+ exact: false,
43
+ error: `No device found matching prefix '${prefix}'.`,
44
+ };
45
+ }
46
+ if (matches.length > 1) {
47
+ return {
48
+ matches,
49
+ exact: false,
50
+ error: `Ambiguous prefix '${prefix}' matches ${matches.length} devices. Be more specific.`,
51
+ };
52
+ }
53
+ return {
54
+ matches,
55
+ exact: true,
56
+ };
57
+ }
58
+ // ─── List Devices ───────────────────────────────────────────────────
59
+ /**
60
+ * List all approved devices, marking the current device.
61
+ *
62
+ * @param baseDir - Optional base directory for config/database files
63
+ * @returns List of devices with current-device indicator
64
+ */
65
+ export async function listDevicesCommand(baseDir) {
66
+ const currentFingerprint = generateDeviceFingerprint(baseDir);
67
+ const db = await DevicesDatabase.open(baseDir);
68
+ try {
69
+ const devices = db.listDevices();
70
+ const entries = devices.map((d) => ({
71
+ ...d,
72
+ is_current: d.fingerprint === currentFingerprint,
73
+ }));
74
+ return { devices: entries, currentFingerprint };
75
+ }
76
+ finally {
77
+ db.close();
78
+ }
79
+ }
80
+ /**
81
+ * Format the device list for human-readable display.
82
+ *
83
+ * @param result - The list result from listDevicesCommand
84
+ * @returns Formatted string for terminal output
85
+ */
86
+ export function formatDeviceList(result) {
87
+ if (result.devices.length === 0) {
88
+ return 'No approved devices.\n';
89
+ }
90
+ const lines = [];
91
+ lines.push('Approved devices:\n');
92
+ for (const device of result.devices) {
93
+ const marker = device.is_current ? ' *' : ' ';
94
+ const fpShort = device.fingerprint.slice(0, 12) + '...';
95
+ lines.push(`${marker} ${device.name}`);
96
+ lines.push(` Fingerprint: ${fpShort}`);
97
+ lines.push(` Approved: ${formatDate(device.approved_at)}`);
98
+ lines.push(` Last seen: ${formatDate(device.last_seen_at)}`);
99
+ lines.push(` Method: ${device.approval_method}`);
100
+ lines.push('');
101
+ }
102
+ lines.push(`${result.devices.length} device(s). * = current device`);
103
+ return lines.join('\n') + '\n';
104
+ }
105
+ // ─── Rename Device ──────────────────────────────────────────────────
106
+ /**
107
+ * Rename a device by fingerprint prefix.
108
+ *
109
+ * @param prefix - The fingerprint prefix (minimum 8 chars)
110
+ * @param newName - The new device name
111
+ * @param baseDir - Optional base directory
112
+ * @returns Rename result
113
+ */
114
+ export async function renameDeviceCommand(prefix, newName, baseDir) {
115
+ const db = await DevicesDatabase.open(baseDir);
116
+ try {
117
+ const match = resolvePrefix(db, prefix);
118
+ if (!match.exact) {
119
+ return {
120
+ success: false,
121
+ fingerprint: '',
122
+ oldName: '',
123
+ newName,
124
+ error: match.error,
125
+ };
126
+ }
127
+ const device = match.matches[0];
128
+ const oldName = device.name;
129
+ const renamed = db.renameDevice(device.fingerprint, newName);
130
+ if (!renamed) {
131
+ return {
132
+ success: false,
133
+ fingerprint: device.fingerprint,
134
+ oldName,
135
+ newName,
136
+ error: 'Failed to rename device.',
137
+ };
138
+ }
139
+ return {
140
+ success: true,
141
+ fingerprint: device.fingerprint,
142
+ oldName,
143
+ newName,
144
+ };
145
+ }
146
+ finally {
147
+ db.close();
148
+ }
149
+ }
150
+ // ─── Remove Device ──────────────────────────────────────────────────
151
+ /**
152
+ * Remove a device by fingerprint prefix.
153
+ *
154
+ * Safety check: cannot remove the last device if no email is configured
155
+ * (would lock the user out with no recovery path).
156
+ *
157
+ * @param prefix - The fingerprint prefix (minimum 8 chars)
158
+ * @param baseDir - Optional base directory
159
+ * @returns Remove result (caller should handle confirmation UI)
160
+ */
161
+ export async function removeDeviceCommand(prefix, baseDir) {
162
+ const db = await DevicesDatabase.open(baseDir);
163
+ try {
164
+ const match = resolvePrefix(db, prefix);
165
+ if (!match.exact) {
166
+ return {
167
+ success: false,
168
+ fingerprint: '',
169
+ name: '',
170
+ wasCurrentDevice: false,
171
+ error: match.error,
172
+ };
173
+ }
174
+ const device = match.matches[0];
175
+ const currentFingerprint = generateDeviceFingerprint(baseDir);
176
+ const wasCurrentDevice = device.fingerprint === currentFingerprint;
177
+ // Safety: cannot remove the last device if no email is configured
178
+ if (db.countDevices() === 1 && !hasVerifiedEmail(baseDir)) {
179
+ return {
180
+ success: false,
181
+ fingerprint: device.fingerprint,
182
+ name: device.name,
183
+ wasCurrentDevice,
184
+ error: 'Cannot remove the last device when no email is configured. ' +
185
+ 'This would lock you out with no recovery path. ' +
186
+ 'Configure an email first: unotoken config email <address>',
187
+ };
188
+ }
189
+ const removed = db.removeDevice(device.fingerprint);
190
+ if (!removed) {
191
+ return {
192
+ success: false,
193
+ fingerprint: device.fingerprint,
194
+ name: device.name,
195
+ wasCurrentDevice,
196
+ error: 'Failed to remove device.',
197
+ };
198
+ }
199
+ return {
200
+ success: true,
201
+ fingerprint: device.fingerprint,
202
+ name: device.name,
203
+ wasCurrentDevice,
204
+ };
205
+ }
206
+ finally {
207
+ db.close();
208
+ }
209
+ }
210
+ // ─── Remove All Devices ─────────────────────────────────────────────
211
+ /**
212
+ * Remove all devices except the current one.
213
+ *
214
+ * Nuclear option for security incidents -- removes all approved devices
215
+ * except the device running this command.
216
+ *
217
+ * @param baseDir - Optional base directory
218
+ * @returns Remove-all result
219
+ */
220
+ export async function removeAllDevicesCommand(baseDir) {
221
+ const currentFingerprint = generateDeviceFingerprint(baseDir);
222
+ const db = await DevicesDatabase.open(baseDir);
223
+ try {
224
+ // Ensure the current device is actually in the database
225
+ if (!db.isDeviceKnown(currentFingerprint)) {
226
+ return {
227
+ success: false,
228
+ removedCount: 0,
229
+ error: 'Current device is not in the approved devices list. ' +
230
+ 'Cannot remove all devices without keeping the current one.',
231
+ };
232
+ }
233
+ const removedCount = db.removeAllExcept(currentFingerprint);
234
+ return {
235
+ success: true,
236
+ removedCount,
237
+ };
238
+ }
239
+ finally {
240
+ db.close();
241
+ }
242
+ }
243
+ // ─── Helpers ────────────────────────────────────────────────────────
244
+ /**
245
+ * Format an ambiguous prefix match result for display.
246
+ *
247
+ * @param matches - The ambiguous matches
248
+ * @returns Formatted string listing the matches
249
+ */
250
+ export function formatAmbiguousMatches(matches) {
251
+ const lines = [];
252
+ lines.push('Multiple devices match this prefix:\n');
253
+ for (const device of matches) {
254
+ lines.push(` ${device.fingerprint.slice(0, 16)}... ${device.name}`);
255
+ }
256
+ lines.push('\nProvide a longer prefix to uniquely identify the device.');
257
+ return lines.join('\n') + '\n';
258
+ }
259
+ function formatDate(isoDate) {
260
+ try {
261
+ const d = new Date(isoDate);
262
+ if (isNaN(d.getTime()))
263
+ return isoDate;
264
+ return d.toLocaleString();
265
+ }
266
+ catch {
267
+ return isoDate;
268
+ }
269
+ }
270
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/signatures/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Known devices registry for unotoken device signatures.
3
+ *
4
+ * Manages a SQLite database of approved devices. Each device is identified
5
+ * by its fingerprint (see fingerprint.ts). Devices must be approved before
6
+ * they can access the vault.
7
+ *
8
+ * The registry is stored at ~/.yokotoken/devices.db (separate from the
9
+ * encrypted vault) because device checks happen BEFORE vault unlock.
10
+ *
11
+ * Table: known_devices
12
+ * - device_id: UUID primary key
13
+ * - fingerprint: SHA-256 hex string (unique)
14
+ * - name: human-friendly device name (e.g., "stefan@macbook")
15
+ * - approved_at: ISO 8601 timestamp
16
+ * - last_seen_at: ISO 8601 timestamp (updated on each vault operation)
17
+ * - approval_method: 'email_code' | 'initial_setup'
18
+ *
19
+ * @module
20
+ */
21
+ export type ApprovalMethod = 'email_code' | 'initial_setup';
22
+ export interface KnownDevice {
23
+ device_id: string;
24
+ fingerprint: string;
25
+ name: string;
26
+ approved_at: string;
27
+ last_seen_at: string;
28
+ approval_method: ApprovalMethod;
29
+ }
30
+ /**
31
+ * Manages the known_devices SQLite database.
32
+ *
33
+ * Uses sql.js (SQLite compiled to WASM) for portable, single-file access.
34
+ * The database lives in memory and is persisted to disk after mutations.
35
+ */
36
+ export declare class DevicesDatabase {
37
+ private db;
38
+ private dbPath;
39
+ private constructor();
40
+ /**
41
+ * Open or create the devices database at the given path.
42
+ *
43
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
44
+ * @returns A ready-to-use DevicesDatabase instance
45
+ */
46
+ static open(baseDir?: string): Promise<DevicesDatabase>;
47
+ /**
48
+ * Initialize the known_devices table if it does not exist.
49
+ */
50
+ private initSchema;
51
+ /**
52
+ * Persist the in-memory database to disk.
53
+ */
54
+ private save;
55
+ /**
56
+ * Check whether a device with the given fingerprint is registered.
57
+ *
58
+ * @param fingerprint - The device fingerprint (SHA-256 hex)
59
+ * @returns true if the device is in the known_devices table
60
+ */
61
+ isDeviceKnown(fingerprint: string): boolean;
62
+ /**
63
+ * Register a new device in the known_devices table.
64
+ *
65
+ * @param fingerprint - The device fingerprint (SHA-256 hex)
66
+ * @param name - Human-friendly device name
67
+ * @param method - How the device was approved
68
+ * @returns The newly created KnownDevice record
69
+ * @throws Error if the fingerprint is already registered
70
+ */
71
+ registerDevice(fingerprint: string, name: string, method: ApprovalMethod): KnownDevice;
72
+ /**
73
+ * Get a known device by fingerprint.
74
+ *
75
+ * @param fingerprint - The device fingerprint (SHA-256 hex)
76
+ * @returns The device record, or null if not found
77
+ */
78
+ getDevice(fingerprint: string): KnownDevice | null;
79
+ /**
80
+ * Update the last_seen_at timestamp for a device.
81
+ *
82
+ * Called on each vault operation from a known device.
83
+ *
84
+ * @param fingerprint - The device fingerprint
85
+ */
86
+ updateLastSeen(fingerprint: string): void;
87
+ /**
88
+ * List all known (approved) devices.
89
+ *
90
+ * @returns Array of KnownDevice records, ordered by approved_at ascending
91
+ */
92
+ listDevices(): KnownDevice[];
93
+ /**
94
+ * Remove a device by fingerprint.
95
+ *
96
+ * @param fingerprint - The device fingerprint
97
+ * @returns true if a device was removed, false if not found
98
+ */
99
+ removeDevice(fingerprint: string): boolean;
100
+ /**
101
+ * Rename a device by fingerprint.
102
+ *
103
+ * @param fingerprint - The device fingerprint
104
+ * @param newName - The new human-friendly name
105
+ * @returns true if the device was renamed, false if not found
106
+ */
107
+ renameDevice(fingerprint: string, newName: string): boolean;
108
+ /**
109
+ * Find devices whose fingerprint starts with the given prefix.
110
+ *
111
+ * Works like git SHA prefix matching -- the user provides the first N
112
+ * characters of the fingerprint and we find matching devices.
113
+ *
114
+ * @param prefix - The fingerprint prefix (minimum 8 characters recommended)
115
+ * @returns Array of matching KnownDevice records
116
+ */
117
+ findByFingerprintPrefix(prefix: string): KnownDevice[];
118
+ /**
119
+ * Remove all devices except the one with the given fingerprint.
120
+ *
121
+ * Used as a nuclear option for security incidents -- removes all
122
+ * approved devices except the current one.
123
+ *
124
+ * @param keepFingerprint - The fingerprint to keep
125
+ * @returns The number of devices removed
126
+ */
127
+ removeAllExcept(keepFingerprint: string): number;
128
+ /**
129
+ * Count total known devices.
130
+ *
131
+ * @returns The number of registered devices
132
+ */
133
+ countDevices(): number;
134
+ /**
135
+ * Check if this is the very first device (no devices registered yet).
136
+ *
137
+ * Used during initial vault setup to auto-approve the first device
138
+ * without requiring an email code.
139
+ *
140
+ * @returns true if zero devices are registered
141
+ */
142
+ isFirstDevice(): boolean;
143
+ /**
144
+ * Close the database connection.
145
+ */
146
+ close(): void;
147
+ }
148
+ /**
149
+ * Check if the current device is known and auto-approve the first device.
150
+ *
151
+ * This is the main entry point for device checks:
152
+ * 1. If no devices exist yet (first setup), auto-approves current device
153
+ * 2. If devices exist, checks if current fingerprint is registered
154
+ *
155
+ * @param fingerprint - The current device's fingerprint
156
+ * @param deviceName - Human-friendly name for this device
157
+ * @param baseDir - Optional base directory for the devices database
158
+ * @returns Object with `known` boolean and `autoApproved` boolean
159
+ */
160
+ export declare function checkDevice(fingerprint: string, deviceName: string, baseDir?: string): Promise<{
161
+ known: boolean;
162
+ autoApproved: boolean;
163
+ device: KnownDevice | null;
164
+ }>;
165
+ //# sourceMappingURL=devices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.d.ts","sourceRoot":"","sources":["../../src/signatures/devices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,eAAe,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,cAAc,CAAC;CACjC;AAoBD;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAAkE;IAC5E,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO;IAQP;;;;;OAKG;WACU,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAuB7D;;OAEG;IACH,OAAO,CAAC,UAAU;IAclB;;OAEG;IACH,OAAO,CAAC,IAAI;IASZ;;;;;OAKG;IACH,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAc3C;;;;;;;;OAQG;IACH,cAAc,CACZ,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,GACrB,WAAW;IAqBd;;;;;OAKG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAqBlD;;;;;;OAMG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IASzC;;;;OAIG;IACH,WAAW,IAAI,WAAW,EAAE;IAoB5B;;;;;OAKG;IACH,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAY1C;;;;;;OAMG;IACH,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAW3D;;;;;;;;OAQG;IACH,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE;IAyBtD;;;;;;;;OAQG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM;IAahD;;;;OAIG;IACH,YAAY,IAAI,MAAM;IAWtB;;;;;;;OAOG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAAC,CAsBhF"}