@pellux/goodvibes-tui 0.19.34 → 0.19.35

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.34",
3
+ "version": "0.19.35",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -91,7 +91,7 @@
91
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
92
92
  "@ast-grep/napi": "^0.42.0",
93
93
  "@aws/bedrock-token-generator": "^1.1.0",
94
- "@pellux/goodvibes-sdk": "0.25.10",
94
+ "@pellux/goodvibes-sdk": "0.25.11",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
@@ -601,7 +601,7 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
601
601
  }
602
602
  if (sub === 'revoke-session') {
603
603
  const token = rest[0];
604
- if (!token) return 'Usage: goodvibes auth revoke-session <token>';
604
+ if (!token) return 'Usage: goodvibes auth revoke-session <token-or-fingerprint>';
605
605
  return services.localUserAuthManager.revokeSession(token)
606
606
  ? 'Auth session revoked.'
607
607
  : 'No auth session found.';
@@ -637,7 +637,7 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
637
637
  if (sub === 'sessions') {
638
638
  return formatJsonOrText(runtime.cli)(snapshot.sessions, [
639
639
  `GoodVibes auth sessions (${snapshot.sessions.length})`,
640
- ...snapshot.sessions.map((session) => ` ${session.username} expires=${new Date(session.expiresAt).toISOString()} token=${session.token.slice(0, 8)}...`),
640
+ ...snapshot.sessions.map((session) => ` ${session.username} expires=${new Date(session.expiresAt).toISOString()} fingerprint=${session.tokenFingerprint}`),
641
641
  ].join('\n'));
642
642
  }
643
643
  return formatJsonOrText(runtime.cli)(value, [
@@ -53,6 +53,8 @@ export function registerCloudflareRuntimeCommands(registry: CommandRegistry): vo
53
53
  ` worker URL: ${status.config.workerBaseUrl || '(not set)'}`,
54
54
  ` queue: ${status.config.queueName || '(not set)'}`,
55
55
  ` DLQ: ${status.config.deadLetterQueueName || '(not set)'}`,
56
+ ` tunnel: ${status.config.tunnelName || '(not set)'} ${status.config.tunnelId ? `(${status.config.tunnelId})` : ''}`.trimEnd(),
57
+ ` access app: ${status.config.accessAppId || '(not set)'}`,
56
58
  ` batch mode: ${String(ctx.platform.configManager.get('batch.mode') ?? 'off')}`,
57
59
  ` queue backend: ${String(ctx.platform.configManager.get('batch.queueBackend') ?? 'local')}`,
58
60
  ...status.warnings.map((warning) => ` warning: ${warning}`),
@@ -153,6 +155,25 @@ export function registerCloudflareRuntimeCommands(registry: CommandRegistry): vo
153
155
  ...optionalString('workerBaseUrl', getFlag(parsed, 'worker-url')),
154
156
  ...optionalString('queueName', getFlag(parsed, 'queue') || getFlag(parsed, 'queue-name')),
155
157
  ...optionalString('deadLetterQueueName', getFlag(parsed, 'dlq') || getFlag(parsed, 'dead-letter-queue')),
158
+ ...optionalString('tunnelName', getFlag(parsed, 'tunnel-name')),
159
+ ...optionalString('tunnelId', getFlag(parsed, 'tunnel-id')),
160
+ ...optionalString('tunnelServiceUrl', getFlag(parsed, 'tunnel-service-url')),
161
+ ...optionalString('tunnelTokenRef', getFlag(parsed, 'tunnel-token-ref')),
162
+ ...optionalString('accessAppId', getFlag(parsed, 'access-app-id')),
163
+ ...optionalString('accessServiceTokenId', getFlag(parsed, 'access-service-token-id')),
164
+ ...optionalString('accessServiceTokenRef', getFlag(parsed, 'access-service-token-ref')),
165
+ ...optionalString('kvNamespaceName', getFlag(parsed, 'kv-namespace-name')),
166
+ ...optionalString('kvNamespaceId', getFlag(parsed, 'kv-namespace-id')),
167
+ ...optionalString('durableObjectNamespaceName', getFlag(parsed, 'do-namespace-name') || getFlag(parsed, 'durable-object-namespace-name')),
168
+ ...optionalString('durableObjectNamespaceId', getFlag(parsed, 'do-namespace-id') || getFlag(parsed, 'durable-object-namespace-id')),
169
+ ...optionalString('r2BucketName', getFlag(parsed, 'r2-bucket-name')),
170
+ ...optionalString('secretsStoreName', getFlag(parsed, 'secrets-store-name')),
171
+ ...optionalString('secretsStoreId', getFlag(parsed, 'secrets-store-id')),
172
+ ...optionalString('workerCron', getFlag(parsed, 'worker-cron')),
173
+ ...optionalString('operatorToken', getFlag(parsed, 'operator-token')),
174
+ ...optionalString('operatorTokenRef', getFlag(parsed, 'operator-token-ref')),
175
+ ...optionalString('workerClientToken', getFlag(parsed, 'worker-client-token')),
176
+ ...optionalString('workerClientTokenRef', getFlag(parsed, 'worker-client-token-ref')),
156
177
  ...optionalBatchMode(readBatchMode(parsed)),
157
178
  persistConfig: true,
158
179
  verify: !hasFlag(parsed, 'no-verify'),
@@ -164,6 +185,12 @@ export function registerCloudflareRuntimeCommands(registry: CommandRegistry): vo
164
185
  ` ok: ${result.ok ? 'yes' : 'no'}`,
165
186
  ...(result.worker ? [` worker: ${result.worker.name}${result.worker.baseUrl ? ` at ${result.worker.baseUrl}` : ''}`] : []),
166
187
  ...(result.queues ? [` queues: ${result.queues.queueName}; DLQ ${result.queues.deadLetterQueueName}`] : []),
188
+ ...(result.tunnel ? [` tunnel: ${result.tunnel.name} (${result.tunnel.id})${result.tunnel.hostname ? ` ${result.tunnel.hostname}` : ''}`] : []),
189
+ ...(result.access ? [` access: app=${result.access.appId || '(none)'} serviceToken=${result.access.serviceTokenId || '(none)'}`] : []),
190
+ ...(result.dns ? [` dns: ${result.dns.zoneName || result.dns.zoneId} records=${result.dns.records.length}`] : []),
191
+ ...(result.kv ? [` kv: ${result.kv.namespaceName} (${result.kv.namespaceId})`] : []),
192
+ ...(result.r2 ? [` r2: ${result.r2.bucketName}`] : []),
193
+ ...(result.secretsStore ? [` secrets store: ${result.secretsStore.storeName} (${result.secretsStore.storeId})`] : []),
167
194
  ...formatProvisionSteps(result.steps),
168
195
  ].join('\n'));
169
196
  return;
@@ -65,10 +65,10 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
65
65
  if (sub === 'revoke-session') {
66
66
  const token = args[1];
67
67
  if (!token) {
68
- ctx.print('Usage: /auth local revoke-session <token>');
68
+ ctx.print('Usage: /auth local revoke-session <token-or-fingerprint>');
69
69
  return;
70
70
  }
71
- ctx.print(auth.revokeSession(token) ? `Revoked session ${token.slice(0, 12)}…` : `Unknown session token: ${token}`);
71
+ ctx.print(auth.revokeSession(token) ? `Revoked session ${token.slice(0, 12)}…` : `Unknown session token or fingerprint: ${token}`);
72
72
  return;
73
73
  }
74
74
 
@@ -88,7 +88,7 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
88
88
  ` users: ${snapshot.userCount}`,
89
89
  ` sessions: ${snapshot.sessionCount}`,
90
90
  ...snapshot.users.map((user) => ` user: ${user.username} roles=${formatRoles(user.roles)}`),
91
- ...snapshot.sessions.map((session) => ` session: ${session.username} expires=${new Date(session.expiresAt).toISOString()} token=${session.token.slice(0, 12)}…`),
91
+ ...snapshot.sessions.map((session) => ` session: ${session.username} expires=${new Date(session.expiresAt).toISOString()} fingerprint=${session.tokenFingerprint}`),
92
92
  ].join('\n'));
93
93
  }
94
94
 
@@ -97,7 +97,7 @@ export function registerLocalAuthRuntimeCommands(registry: CommandRegistry): voi
97
97
  name: 'local-auth',
98
98
  aliases: ['auth-local'],
99
99
  description: 'Inspect and manage local daemon/listener auth users, sessions, and bootstrap credentials',
100
- usage: '[review|panel|add-user <username> <password> [roles]|delete-user <username>|rotate-password <username> <password>|revoke-session <token>|clear-bootstrap-file]',
100
+ usage: '[review|panel|add-user <username> <password> [roles]|delete-user <username>|rotate-password <username> <password>|revoke-session <token-or-fingerprint>|clear-bootstrap-file]',
101
101
  handler(args, ctx) {
102
102
  handleLocalAuthCommand(args, ctx);
103
103
  },
@@ -234,6 +234,10 @@ function addCloudflareOperations(
234
234
  setConfig('cloudflare.deadLetterQueueName', controller.getStringFieldValue('cloudflare.dead-letter-queue-name', config?.deadLetterQueueName ?? 'goodvibes-batch-dlq'));
235
235
  setConfig('cloudflare.tunnelName', controller.getStringFieldValue('cloudflare.tunnel-name', config?.tunnelName ?? 'goodvibes-daemon'));
236
236
  setConfig('cloudflare.tunnelId', controller.getStringFieldValue('cloudflare.tunnel-id', config?.tunnelId ?? ''));
237
+ setConfig('cloudflare.tunnelTokenRef', controller.getStringFieldValue('cloudflare.tunnel-token-ref', config?.tunnelTokenRef ?? ''));
238
+ setConfig('cloudflare.accessAppId', controller.getStringFieldValue('cloudflare.access-app-id', config?.accessAppId ?? ''));
239
+ setConfig('cloudflare.accessServiceTokenId', controller.getStringFieldValue('cloudflare.access-service-token-id', config?.accessServiceTokenId ?? ''));
240
+ setConfig('cloudflare.accessServiceTokenRef', controller.getStringFieldValue('cloudflare.access-service-token-ref', config?.accessServiceTokenRef ?? ''));
237
241
  setConfig('cloudflare.kvNamespaceName', controller.getStringFieldValue('cloudflare.kv-namespace-name', config?.kvNamespaceName ?? 'goodvibes-runtime'));
238
242
  setConfig('cloudflare.kvNamespaceId', controller.getStringFieldValue('cloudflare.kv-namespace-id', config?.kvNamespaceId ?? ''));
239
243
  setConfig('cloudflare.durableObjectNamespaceName', controller.getStringFieldValue('cloudflare.do-namespace-name', config?.durableObjectNamespaceName ?? 'GoodVibesCoordinator'));
@@ -238,6 +238,51 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
238
238
  placeholder: 'optional tunnel id',
239
239
  defaultValue: config?.tunnelId ?? '',
240
240
  },
241
+ {
242
+ kind: 'text',
243
+ id: 'cloudflare.tunnel-service-url',
244
+ label: 'Tunnel service URL',
245
+ hint: 'Optional origin service URL for Tunnel ingress. Leave blank to use the daemon base URL.',
246
+ placeholder: 'http://127.0.0.1:3421',
247
+ defaultValue: '',
248
+ },
249
+ {
250
+ kind: 'text',
251
+ id: 'cloudflare.tunnel-token-ref',
252
+ label: 'Tunnel token secret ref',
253
+ hint: 'Optional existing goodvibes:// secret ref for a Cloudflare Tunnel token.',
254
+ placeholder: 'goodvibes://secrets/goodvibes/CLOUDFLARE_TUNNEL_TOKEN',
255
+ defaultValue: config?.tunnelTokenRef ?? '',
256
+ },
257
+ );
258
+ }
259
+
260
+ if (components.zeroTrustAccess) {
261
+ fields.push(
262
+ {
263
+ kind: 'text',
264
+ id: 'cloudflare.access-app-id',
265
+ label: 'Access app id',
266
+ hint: 'Optional existing Cloudflare Access application id. Leave blank to let provisioning create or discover one.',
267
+ placeholder: 'optional Access app id',
268
+ defaultValue: config?.accessAppId ?? '',
269
+ },
270
+ {
271
+ kind: 'text',
272
+ id: 'cloudflare.access-service-token-id',
273
+ label: 'Access service token id',
274
+ hint: 'Optional existing Access service token id.',
275
+ placeholder: 'optional service token id',
276
+ defaultValue: config?.accessServiceTokenId ?? '',
277
+ },
278
+ {
279
+ kind: 'text',
280
+ id: 'cloudflare.access-service-token-ref',
281
+ label: 'Access service token secret ref',
282
+ hint: 'Optional existing goodvibes:// secret ref for Access service token material.',
283
+ placeholder: 'goodvibes://secrets/goodvibes/CLOUDFLARE_ACCESS_SERVICE_TOKEN',
284
+ defaultValue: config?.accessServiceTokenRef ?? '',
285
+ },
241
286
  );
242
287
  }
243
288
 
@@ -178,6 +178,11 @@ export function buildCloudflareProvisionRequest(controller: OnboardingWizardCont
178
178
  deadLetterQueueName: controller.getStringFieldValue('cloudflare.dead-letter-queue-name', controller.runtimeSnapshot?.config.cloudflare.deadLetterQueueName ?? 'goodvibes-batch-dlq'),
179
179
  tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-daemon'),
180
180
  tunnelId: controller.getStringFieldValue('cloudflare.tunnel-id', controller.runtimeSnapshot?.config.cloudflare.tunnelId ?? ''),
181
+ tunnelServiceUrl: controller.getStringFieldValue('cloudflare.tunnel-service-url', ''),
182
+ tunnelTokenRef: controller.getStringFieldValue('cloudflare.tunnel-token-ref', controller.runtimeSnapshot?.config.cloudflare.tunnelTokenRef ?? ''),
183
+ accessAppId: controller.getStringFieldValue('cloudflare.access-app-id', controller.runtimeSnapshot?.config.cloudflare.accessAppId ?? ''),
184
+ accessServiceTokenId: controller.getStringFieldValue('cloudflare.access-service-token-id', controller.runtimeSnapshot?.config.cloudflare.accessServiceTokenId ?? ''),
185
+ accessServiceTokenRef: controller.getStringFieldValue('cloudflare.access-service-token-ref', controller.runtimeSnapshot?.config.cloudflare.accessServiceTokenRef ?? ''),
181
186
  kvNamespaceName: controller.getStringFieldValue('cloudflare.kv-namespace-name', controller.runtimeSnapshot?.config.cloudflare.kvNamespaceName ?? 'goodvibes-runtime'),
182
187
  kvNamespaceId: controller.getStringFieldValue('cloudflare.kv-namespace-id', controller.runtimeSnapshot?.config.cloudflare.kvNamespaceId ?? ''),
183
188
  durableObjectNamespaceName: controller.getStringFieldValue('cloudflare.do-namespace-name', controller.runtimeSnapshot?.config.cloudflare.durableObjectNamespaceName ?? 'GoodVibesCoordinator'),
@@ -42,7 +42,7 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
42
42
  id: 'external-integrations',
43
43
  label: 'Connect GoodVibes to external apps and services',
44
44
  selected: false,
45
- detail: 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.',
45
+ detail: 'Enable setup screens for Slack, Discord, Telegram, Home Assistant, Teams, Matrix, and other app surfaces you choose.',
46
46
  },
47
47
  {
48
48
  id: 'cloudflare-batch',
@@ -102,6 +102,7 @@ export const INBOUND_EXTERNAL_SURFACE_IDS = new Set<string>([
102
102
  'bluebubbles',
103
103
  'discord',
104
104
  'googleChat',
105
+ 'homeassistant',
105
106
  'imessage',
106
107
  'mattermost',
107
108
  'matrix',
@@ -123,6 +124,9 @@ export const REQUIRED_EXTERNAL_SETUP_FIELD_IDS = new Set<string>([
123
124
  'external-services.telegram.bot-token',
124
125
  'external-services.telegram.webhook-secret',
125
126
  'external-services.webhook.default-target',
127
+ 'external-services.homeassistant.instance-url',
128
+ 'external-services.homeassistant.access-token',
129
+ 'external-services.homeassistant.webhook-secret',
126
130
  'external-services.google-chat.webhook-url',
127
131
  'external-services.google-chat.verification-token',
128
132
  'external-services.signal.bridge-url',
@@ -0,0 +1,117 @@
1
+ import type { ExternalSurfaceSpec } from './onboarding-wizard-external-surfaces.ts';
2
+
3
+ export const WEBHOOK_SURFACE_SPEC: ExternalSurfaceSpec = {
4
+ id: 'webhook',
5
+ enabledFieldId: 'external-services.webhook',
6
+ enabledConfigKey: 'surfaces.webhook.enabled',
7
+ label: 'Outbound webhook surface',
8
+ hint: 'Enable outbound webhook delivery targets.',
9
+ defaultEnabled: (snapshot) => snapshot?.config.surfaces.webhook.enabled ?? false,
10
+ fields: [
11
+ {
12
+ id: 'external-services.webhook.default-target',
13
+ configKey: 'surfaces.webhook.defaultTarget',
14
+ kind: 'text',
15
+ label: 'Default webhook target',
16
+ hint: 'Fallback URL used for outbound webhook deliveries.',
17
+ placeholder: 'https://example.com/goodvibes',
18
+ defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.defaultTarget ?? '',
19
+ },
20
+ {
21
+ id: 'external-services.webhook.secret',
22
+ configKey: 'surfaces.webhook.secret',
23
+ kind: 'masked',
24
+ label: 'Webhook signing secret',
25
+ hint: 'Secret used to sign outbound webhook payloads.',
26
+ placeholder: 'secret',
27
+ defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.secret ?? '',
28
+ },
29
+ {
30
+ id: 'external-services.webhook.timeout-ms',
31
+ configKey: 'surfaces.webhook.timeoutMs',
32
+ kind: 'text',
33
+ valueType: 'number',
34
+ label: 'Webhook timeout ms',
35
+ hint: 'Request timeout for outbound webhook deliveries.',
36
+ placeholder: '10000',
37
+ defaultNumber: 10000,
38
+ min: 1000,
39
+ max: 60000,
40
+ defaultValue: (snapshot) => String(snapshot?.config.surfaces.webhook.timeoutMs ?? 10000),
41
+ },
42
+ ],
43
+ };
44
+
45
+ export const HOME_ASSISTANT_SURFACE_SPEC: ExternalSurfaceSpec = {
46
+ id: 'homeassistant',
47
+ enabledFieldId: 'external-services.homeassistant',
48
+ enabledConfigKey: 'surfaces.homeassistant.enabled',
49
+ label: 'Home Assistant surface',
50
+ hint: 'Enable the Home Assistant companion surface, daemon callbacks, and event delivery.',
51
+ defaultEnabled: (snapshot) => snapshot?.config.surfaces.homeassistant.enabled ?? false,
52
+ fields: [
53
+ {
54
+ id: 'external-services.homeassistant.instance-url',
55
+ configKey: 'surfaces.homeassistant.instanceUrl',
56
+ kind: 'text',
57
+ label: 'Home Assistant URL',
58
+ hint: 'Base URL of the Home Assistant instance.',
59
+ placeholder: 'http://homeassistant.local:8123',
60
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.instanceUrl ?? '',
61
+ },
62
+ {
63
+ id: 'external-services.homeassistant.access-token',
64
+ configKey: 'surfaces.homeassistant.accessToken',
65
+ kind: 'masked',
66
+ label: 'Home Assistant access token',
67
+ hint: 'Long-lived Home Assistant access token or goodvibes:// secret reference.',
68
+ placeholder: 'long-lived access token',
69
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.accessToken ?? '',
70
+ },
71
+ {
72
+ id: 'external-services.homeassistant.webhook-secret',
73
+ configKey: 'surfaces.homeassistant.webhookSecret',
74
+ kind: 'masked',
75
+ label: 'Home Assistant webhook secret',
76
+ hint: 'Shared secret used to verify inbound Home Assistant callbacks.',
77
+ placeholder: 'webhook secret',
78
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.webhookSecret ?? '',
79
+ },
80
+ {
81
+ id: 'external-services.homeassistant.default-conversation-id',
82
+ configKey: 'surfaces.homeassistant.defaultConversationId',
83
+ kind: 'text',
84
+ label: 'Default conversation ID',
85
+ hint: 'Default Home Assistant conversation id used for route binding.',
86
+ placeholder: 'goodvibes',
87
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.defaultConversationId ?? 'goodvibes',
88
+ },
89
+ {
90
+ id: 'external-services.homeassistant.device-id',
91
+ configKey: 'surfaces.homeassistant.deviceId',
92
+ kind: 'text',
93
+ label: 'Home Assistant device ID',
94
+ hint: 'Stable device identifier exposed by the GoodVibes daemon.',
95
+ placeholder: 'goodvibes-daemon',
96
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceId ?? 'goodvibes-daemon',
97
+ },
98
+ {
99
+ id: 'external-services.homeassistant.device-name',
100
+ configKey: 'surfaces.homeassistant.deviceName',
101
+ kind: 'text',
102
+ label: 'Home Assistant device name',
103
+ hint: 'Display name for the GoodVibes daemon device in Home Assistant.',
104
+ placeholder: 'GoodVibes Daemon',
105
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceName ?? 'GoodVibes Daemon',
106
+ },
107
+ {
108
+ id: 'external-services.homeassistant.event-type',
109
+ configKey: 'surfaces.homeassistant.eventType',
110
+ kind: 'text',
111
+ label: 'Home Assistant event type',
112
+ hint: 'Event type used for daemon-to-Home Assistant deliveries.',
113
+ placeholder: 'goodvibes_message',
114
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.eventType ?? 'goodvibes_message',
115
+ },
116
+ ],
117
+ };
@@ -7,6 +7,7 @@ import {
7
7
  import { DEFAULT_CONFIG, type ConfigKey } from '../../config/index.ts';
8
8
  import type { OnboardingSnapshotState } from '../../runtime/onboarding/index.ts';
9
9
  import { TELEGRAM_MODE_OPTIONS, WHATSAPP_PROVIDER_OPTIONS } from './onboarding-wizard-constants.ts';
10
+ import { HOME_ASSISTANT_SURFACE_SPEC, WEBHOOK_SURFACE_SPEC } from './onboarding-wizard-external-surface-extra-specs.ts';
10
11
  import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
11
12
 
12
13
  export interface ExternalSurfaceSetupFieldSpec {
@@ -333,47 +334,8 @@ export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
333
334
  },
334
335
  ],
335
336
  },
336
- {
337
- id: 'webhook',
338
- enabledFieldId: 'external-services.webhook',
339
- enabledConfigKey: 'surfaces.webhook.enabled',
340
- label: 'Outbound webhook surface',
341
- hint: 'Enable outbound webhook delivery targets.',
342
- defaultEnabled: (snapshot) => snapshot?.config.surfaces.webhook.enabled ?? false,
343
- fields: [
344
- {
345
- id: 'external-services.webhook.default-target',
346
- configKey: 'surfaces.webhook.defaultTarget',
347
- kind: 'text',
348
- label: 'Default webhook target',
349
- hint: 'Fallback URL used for outbound webhook deliveries.',
350
- placeholder: 'https://example.com/goodvibes',
351
- defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.defaultTarget ?? '',
352
- },
353
- {
354
- id: 'external-services.webhook.secret',
355
- configKey: 'surfaces.webhook.secret',
356
- kind: 'masked',
357
- label: 'Webhook signing secret',
358
- hint: 'Secret used to sign outbound webhook payloads.',
359
- placeholder: 'secret',
360
- defaultValue: (snapshot) => snapshot?.config.surfaces.webhook.secret ?? '',
361
- },
362
- {
363
- id: 'external-services.webhook.timeout-ms',
364
- configKey: 'surfaces.webhook.timeoutMs',
365
- kind: 'text',
366
- valueType: 'number',
367
- label: 'Webhook timeout ms',
368
- hint: 'Request timeout for outbound webhook deliveries.',
369
- placeholder: '10000',
370
- defaultNumber: 10000,
371
- min: 1000,
372
- max: 60000,
373
- defaultValue: (snapshot) => String(snapshot?.config.surfaces.webhook.timeoutMs ?? 10000),
374
- },
375
- ],
376
- },
337
+ WEBHOOK_SURFACE_SPEC,
338
+ HOME_ASSISTANT_SURFACE_SPEC,
377
339
  {
378
340
  id: 'googleChat',
379
341
  enabledFieldId: 'external-services.google-chat',
@@ -139,6 +139,14 @@ export const SETTING_LABELS: Partial<Record<string, string>> = {
139
139
  'surfaces.ntfy.remoteTopic': 'ntfy Daemon-Only Remote Topic',
140
140
  'surfaces.ntfy.token': 'ntfy Token',
141
141
  'surfaces.ntfy.defaultPriority': 'ntfy Default Priority',
142
+ 'surfaces.homeassistant.enabled': 'Home Assistant Enabled',
143
+ 'surfaces.homeassistant.instanceUrl': 'Home Assistant URL',
144
+ 'surfaces.homeassistant.accessToken': 'Home Assistant Access Token',
145
+ 'surfaces.homeassistant.webhookSecret': 'Home Assistant Webhook Secret',
146
+ 'surfaces.homeassistant.defaultConversationId': 'Home Assistant Conversation ID',
147
+ 'surfaces.homeassistant.deviceId': 'Home Assistant Device ID',
148
+ 'surfaces.homeassistant.deviceName': 'Home Assistant Device Name',
149
+ 'surfaces.homeassistant.eventType': 'Home Assistant Event Type',
142
150
  };
143
151
 
144
152
  export function getSettingLabel(entry: SettingEntry): string {
@@ -123,7 +123,24 @@ export interface CloudflareProvisionResult {
123
123
  readonly account?: { readonly id: string; readonly name: string };
124
124
  readonly worker?: { readonly name: string; readonly baseUrl?: string; readonly subdomain?: string; readonly hostname?: string; readonly cron?: string };
125
125
  readonly queues?: { readonly queueName: string; readonly queueId: string; readonly deadLetterQueueName: string; readonly deadLetterQueueId: string; readonly consumerId?: string };
126
+ readonly tunnel?: { readonly id: string; readonly name: string; readonly hostname?: string; readonly tokenRef?: string };
127
+ readonly access?: { readonly appId?: string; readonly serviceTokenId?: string; readonly serviceTokenRef?: string };
128
+ readonly dns?: {
129
+ readonly zoneId: string;
130
+ readonly zoneName?: string;
131
+ readonly records: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly type?: string; readonly content?: string }>;
132
+ };
133
+ readonly kv?: { readonly namespaceName: string; readonly namespaceId: string };
134
+ readonly durableObjects?: { readonly namespaceName: string; readonly namespaceId?: string };
135
+ readonly r2?: { readonly bucketName: string; readonly storageClass: 'Standard' };
136
+ readonly secretsStore?: { readonly storeName: string; readonly storeId: string };
126
137
  readonly verification?: CloudflareVerifyResult;
138
+ readonly generatedSecrets?: {
139
+ readonly workerClientToken?: string;
140
+ readonly tunnelToken?: string;
141
+ readonly accessServiceTokenClientId?: string;
142
+ readonly accessServiceTokenClientSecret?: string;
143
+ };
127
144
  }
128
145
 
129
146
  export interface CloudflareVerifyResult {
@@ -187,6 +204,10 @@ export interface CloudflareProvisionRequest extends CloudflareDiscoverRequest {
187
204
  readonly tunnelName?: string;
188
205
  readonly tunnelId?: string;
189
206
  readonly tunnelServiceUrl?: string;
207
+ readonly tunnelTokenRef?: string;
208
+ readonly accessAppId?: string;
209
+ readonly accessServiceTokenId?: string;
210
+ readonly accessServiceTokenRef?: string;
190
211
  readonly kvNamespaceName?: string;
191
212
  readonly kvNamespaceId?: string;
192
213
  readonly durableObjectNamespaceName?: string;
@@ -353,25 +353,26 @@ function buildAuthRollbackAction(
353
353
  ): RollbackAction {
354
354
  validateAuthOperation(deps, operation);
355
355
  const auth = deps.auth!;
356
+ const mutable = auth as unknown as MutableAuthManager;
356
357
  const username = operation.username.trim();
357
358
  const before = auth.inspect();
358
359
  const existingUser = before.users.find((user) => user.username === username);
359
- const existingSessionTokens = new Set(before.sessions
360
+ const existingSessionFingerprints = new Set(before.sessions
360
361
  .filter((session) => session.username === username)
361
- .map((session) => session.token));
362
+ .map((session) => session.tokenFingerprint));
362
363
  const userStoreSnapshot = existsSync(before.userStorePath) ? readFileSync(before.userStorePath, 'utf-8') : null;
363
364
  const bootstrapCredentialSnapshot = existsSync(before.bootstrapCredentialPath)
364
365
  ? readFileSync(before.bootstrapCredentialPath, 'utf-8')
365
366
  : null;
366
367
  const bootstrapCredential = parseBootstrapCredential(bootstrapCredentialSnapshot);
367
- const beforeSessions = before.sessions.map((session) => ({ ...session }));
368
+ const beforeSessions = mutable.sessions instanceof Map
369
+ ? [...mutable.sessions.entries()].map(([token, session]) => [token, { ...session }] as const)
370
+ : [];
368
371
 
369
372
  return () => {
370
- const mutable = auth as unknown as MutableAuthManager;
371
-
372
373
  for (const session of auth.inspect().sessions) {
373
- if (session.username === username && !existingSessionTokens.has(session.token)) {
374
- auth.revokeSession(session.token);
374
+ if (session.username === username && !existingSessionFingerprints.has(session.tokenFingerprint)) {
375
+ auth.revokeSession(session.tokenFingerprint);
375
376
  }
376
377
  }
377
378
 
@@ -402,7 +403,7 @@ function buildAuthRollbackAction(
402
403
 
403
404
  if (mutable.sessions instanceof Map) {
404
405
  mutable.sessions.clear();
405
- for (const session of beforeSessions) mutable.sessions.set(session.token, session);
406
+ for (const [token, session] of beforeSessions) mutable.sessions.set(token, session);
406
407
  }
407
408
  };
408
409
  }
@@ -281,7 +281,7 @@ function describeExternalIntegrations(snapshot: OnboardingSnapshotState): string
281
281
  ]).size;
282
282
 
283
283
  if (integrationCount === 0) {
284
- return 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.';
284
+ return 'Enable setup screens for Slack, Discord, Telegram, Home Assistant, Teams, Matrix, and other app surfaces you choose.';
285
285
  }
286
286
 
287
287
  return `Review and configure ${integrationCount} detected external app, service, or surface integration signal(s).`;
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.34';
9
+ let _version = '0.19.35';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;