@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/CHANGELOG.md +11 -0
- package/README.md +4 -3
- package/docs/foundation-artifacts/operator-contract.json +284 -112
- package/package.json +2 -2
- package/src/cli/management.ts +2 -2
- package/src/input/commands/cloudflare-runtime.ts +27 -0
- package/src/input/commands/local-auth-runtime.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +45 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +5 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +5 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +117 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +3 -41
- package/src/renderer/settings-modal-helpers.ts +8 -0
- package/src/runtime/cloudflare-control-plane.ts +21 -0
- package/src/runtime/onboarding/apply.ts +9 -8
- package/src/runtime/onboarding/derivation.ts +1 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
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.
|
|
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",
|
package/src/cli/management.ts
CHANGED
|
@@ -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()}
|
|
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()}
|
|
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
|
-
|
|
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
|
|
360
|
+
const existingSessionFingerprints = new Set(before.sessions
|
|
360
361
|
.filter((session) => session.username === username)
|
|
361
|
-
.map((session) => session.
|
|
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 =
|
|
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 && !
|
|
374
|
-
auth.revokeSession(session.
|
|
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(
|
|
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.
|
|
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;
|