feat: add Linq channel — real iMessage via API, no Mac required

Adds a complete Linq iMessage channel adapter that replaces the existing
iMessage channel's Mac Mini + dedicated Apple ID + SSH wrapper + Full Disk
Access setup with a single API key and phone number.

Core implementation (src/linq/):
- types.ts: Linq webhook event and message types
- accounts.ts: Multi-account resolution from config (env/file/inline token)
- send.ts: REST outbound via Linq Blue V3 API (messages, typing, reactions)
- probe.ts: Health check via GET /v3/phonenumbers
- monitor.ts: Webhook HTTP server with HMAC-SHA256 signature verification,
  replay protection, inbound debouncing, and full dispatch pipeline integration

Extension plugin (extensions/linq/):
- ChannelPlugin implementation with config, security, setup, outbound,
  gateway, and status adapters
- Supports direct and group chats, reactions, and media

Wiring:
- Channel registry, dock, config schema, plugin-sdk exports, and plugin
  runtime all updated to include the new linq channel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
George McCain
2026-02-13 15:51:57 -05:00
committed by Peter Steinberger
parent 95024d1671
commit d4a142fd8f
17 changed files with 1392 additions and 15 deletions

View File

@@ -1010,3 +1010,65 @@ export const MSTeamsConfigSchema = z
'channels.msteams.dmPolicy="open" requires channels.msteams.allowFrom to include "*"',
});
});
// ── Linq ─────────────────────────────────────────────────────────────────────
const LinqAllowFromEntry = z.union([z.string(), z.number()]);
const LinqAccountSchemaBase = z
.object({
name: z.string().optional(),
enabled: z.boolean().optional(),
apiToken: z.string().optional().register(sensitive),
tokenFile: z.string().optional(),
fromPhone: z.string().optional(),
dmPolicy: DmPolicySchema.optional(),
allowFrom: z.array(LinqAllowFromEntry).optional(),
groupPolicy: GroupPolicySchema.optional(),
groupAllowFrom: z.array(LinqAllowFromEntry).optional(),
mediaMaxMb: z.number().positive().optional(),
textChunkLimit: z.number().positive().optional(),
webhookUrl: z.string().optional(),
webhookSecret: z.string().optional().register(sensitive),
webhookPath: z.string().optional(),
webhookHost: z.string().optional(),
historyLimit: z.number().nonnegative().optional(),
blockStreaming: z.boolean().optional(),
groups: z
.record(
z.string(),
z
.object({
requireMention: z.boolean().optional(),
tools: ToolPolicySchema,
toolsBySender: ToolPolicyBySenderSchema,
})
.strict()
.optional(),
)
.optional(),
responsePrefix: z.string().optional(),
})
.strict();
const LinqAccountSchema = LinqAccountSchemaBase.superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message: 'channels.linq.dmPolicy="open" requires channels.linq.allowFrom to include "*"',
});
});
export const LinqConfigSchema = LinqAccountSchemaBase.extend({
accounts: z.record(z.string(), LinqAccountSchema.optional()).optional(),
}).superRefine((value, ctx) => {
requireOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
path: ["allowFrom"],
message: 'channels.linq.dmPolicy="open" requires channels.linq.allowFrom to include "*"',
});
});