@amalgm/channels 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.
- package/PURPOSE.md +50 -0
- package/README.md +17 -0
- package/dist/host/auth.d.ts +10 -0
- package/dist/host/auth.js +76 -0
- package/dist/host/auth.js.map +1 -0
- package/dist/host/config.d.ts +10 -0
- package/dist/host/config.js +32 -0
- package/dist/host/config.js.map +1 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +7 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/main.d.ts +1 -0
- package/dist/host/main.js +29 -0
- package/dist/host/main.js.map +1 -0
- package/dist/host/recipient.d.ts +7 -0
- package/dist/host/recipient.js +17 -0
- package/dist/host/recipient.js.map +1 -0
- package/dist/host/repository.d.ts +13 -0
- package/dist/host/repository.js +103 -0
- package/dist/host/repository.js.map +1 -0
- package/dist/host/resend.d.ts +10 -0
- package/dist/host/resend.js +33 -0
- package/dist/host/resend.js.map +1 -0
- package/dist/host/server.d.ts +12 -0
- package/dist/host/server.js +57 -0
- package/dist/host/server.js.map +1 -0
- package/dist/src/client.d.ts +7 -0
- package/dist/src/client.js +41 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/contract.d.ts +83 -0
- package/dist/src/contract.js +2 -0
- package/dist/src/contract.js.map +1 -0
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/http.d.ts +6 -0
- package/dist/src/http.js +64 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp.d.ts +3 -0
- package/dist/src/mcp.js +66 -0
- package/dist/src/mcp.js.map +1 -0
- package/dist/src/service.d.ts +2 -0
- package/dist/src/service.js +29 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/validation.d.ts +4 -0
- package/dist/src/validation.js +48 -0
- package/dist/src/validation.js.map +1 -0
- package/dist/src/worker.d.ts +15 -0
- package/dist/src/worker.js +46 -0
- package/dist/src/worker.js.map +1 -0
- package/package.json +40 -0
- package/skills/channels/SKILL.md +29 -0
- package/supabase/migrations/20260829000000_create_channels.sql +222 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type ChannelScope = 'channels:read' | 'channels:send';
|
|
2
|
+
export type ChannelPreference = 'auto' | 'email';
|
|
3
|
+
export type DeliveryStatus = 'pending' | 'sending' | 'delivered' | 'failed';
|
|
4
|
+
export interface ChannelPrincipal {
|
|
5
|
+
readonly userId: string;
|
|
6
|
+
readonly scopes: readonly ChannelScope[];
|
|
7
|
+
}
|
|
8
|
+
export interface NotifyUserInput {
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly title?: string;
|
|
11
|
+
readonly channel?: ChannelPreference;
|
|
12
|
+
readonly idempotencyKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Delivery {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly channel: Exclude<ChannelPreference, 'auto'>;
|
|
17
|
+
readonly title?: string;
|
|
18
|
+
readonly message: string;
|
|
19
|
+
readonly idempotencyKey: string;
|
|
20
|
+
readonly status: DeliveryStatus;
|
|
21
|
+
readonly attempts: number;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
readonly updatedAt: string;
|
|
24
|
+
readonly deliveredAt?: string;
|
|
25
|
+
readonly providerId?: string;
|
|
26
|
+
readonly error?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface DeliveryPage {
|
|
29
|
+
readonly items: readonly Delivery[];
|
|
30
|
+
readonly total: number;
|
|
31
|
+
readonly limit: number;
|
|
32
|
+
readonly offset: number;
|
|
33
|
+
readonly hasMore: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface ListDeliveries {
|
|
36
|
+
readonly status?: DeliveryStatus;
|
|
37
|
+
readonly limit?: number;
|
|
38
|
+
readonly offset?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface NormalizedListDeliveries {
|
|
41
|
+
readonly status?: DeliveryStatus;
|
|
42
|
+
readonly limit: number;
|
|
43
|
+
readonly offset: number;
|
|
44
|
+
}
|
|
45
|
+
export interface Channels {
|
|
46
|
+
notify(input: NotifyUserInput): Promise<Delivery>;
|
|
47
|
+
list(query?: ListDeliveries): Promise<DeliveryPage>;
|
|
48
|
+
get(deliveryId: string): Promise<Delivery | null>;
|
|
49
|
+
}
|
|
50
|
+
export interface ChannelsService {
|
|
51
|
+
for(principal: ChannelPrincipal): Channels;
|
|
52
|
+
}
|
|
53
|
+
export interface PendingDelivery {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly userId: string;
|
|
56
|
+
readonly channel: 'email';
|
|
57
|
+
readonly title?: string;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
readonly idempotencyKey: string;
|
|
60
|
+
readonly leaseToken: string;
|
|
61
|
+
readonly attempts: number;
|
|
62
|
+
}
|
|
63
|
+
export interface DeliveryRepository {
|
|
64
|
+
enqueue(userId: string, input: Required<NotifyUserInput>): Promise<Delivery>;
|
|
65
|
+
list(userId: string, query: NormalizedListDeliveries): Promise<DeliveryPage>;
|
|
66
|
+
get(userId: string, deliveryId: string): Promise<Delivery | null>;
|
|
67
|
+
claim(limit: number, leaseSeconds: number): Promise<readonly PendingDelivery[]>;
|
|
68
|
+
complete(delivery: PendingDelivery, providerId: string, now: Date): Promise<boolean>;
|
|
69
|
+
fail(delivery: PendingDelivery, error: string, now: Date): Promise<boolean>;
|
|
70
|
+
}
|
|
71
|
+
export interface RecipientDirectory {
|
|
72
|
+
emailForUser(userId: string): Promise<string>;
|
|
73
|
+
}
|
|
74
|
+
export interface EmailProvider {
|
|
75
|
+
send(input: Readonly<{
|
|
76
|
+
to: string;
|
|
77
|
+
subject: string;
|
|
78
|
+
text: string;
|
|
79
|
+
idempotencyKey: string;
|
|
80
|
+
}>): Promise<{
|
|
81
|
+
readonly providerId: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/contract.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAE3B,IAAI;IADf,YACW,IAAuB,EAChC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;oBAHN,IAAI;QAIb,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChannelPrincipal, ChannelsService } from './contract.js';
|
|
2
|
+
export type AuthenticateChannelsRequest = (request: Request) => Promise<ChannelPrincipal>;
|
|
3
|
+
export declare function createChannelsApi(options: {
|
|
4
|
+
readonly service: ChannelsService;
|
|
5
|
+
readonly authenticate: AuthenticateChannelsRequest;
|
|
6
|
+
}): (request: Request) => Promise<Response>;
|
package/dist/src/http.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ChannelsError } from './errors.js';
|
|
2
|
+
export function createChannelsApi(options) {
|
|
3
|
+
return async (request) => {
|
|
4
|
+
try {
|
|
5
|
+
const sdk = options.service.for(await options.authenticate(request));
|
|
6
|
+
const url = new URL(request.url);
|
|
7
|
+
const parts = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
8
|
+
const method = request.method.toUpperCase();
|
|
9
|
+
if (parts[0] !== 'v1')
|
|
10
|
+
return json(404, { error: 'Not found' });
|
|
11
|
+
if (parts[1] === 'notifications' && parts.length === 2 && method === 'POST') {
|
|
12
|
+
const body = await objectBody(request);
|
|
13
|
+
return json(202, await sdk.notify({
|
|
14
|
+
message: String(body.message ?? ''),
|
|
15
|
+
...(body.title === undefined ? {} : { title: String(body.title) }),
|
|
16
|
+
...(body.channel === undefined ? {} : { channel: body.channel }),
|
|
17
|
+
...(body.idempotencyKey === undefined ? {} : { idempotencyKey: String(body.idempotencyKey) }),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
if (parts[1] === 'deliveries' && parts.length === 2 && method === 'GET') {
|
|
21
|
+
return json(200, await sdk.list({
|
|
22
|
+
...(url.searchParams.has('status') ? { status: url.searchParams.get('status') } : {}),
|
|
23
|
+
...(url.searchParams.has('limit') ? { limit: Number(url.searchParams.get('limit')) } : {}),
|
|
24
|
+
...(url.searchParams.has('offset') ? { offset: Number(url.searchParams.get('offset')) } : {}),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
if (parts[1] === 'deliveries' && parts[2] && parts.length === 3 && method === 'GET') {
|
|
28
|
+
const delivery = await sdk.get(parts[2]);
|
|
29
|
+
return delivery ? json(200, delivery) : json(404, { error: 'Not found', code: 'not_found' });
|
|
30
|
+
}
|
|
31
|
+
return json(405, { error: 'Method not allowed' });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof ChannelsError)
|
|
35
|
+
return json(status(error.code), { error: error.message, code: error.code });
|
|
36
|
+
const code = error instanceof Error && 'code' in error ? String(error.code) : 'internal';
|
|
37
|
+
return json(code.startsWith('dpop_') || code.includes('access') ? 401 : 500, {
|
|
38
|
+
error: code === 'internal' ? 'Channels service failed' : 'Authorization denied', code,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function objectBody(request) {
|
|
44
|
+
const body = await request.json().catch(() => null);
|
|
45
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
46
|
+
throw new ChannelsError('validation', 'Request body must be a JSON object');
|
|
47
|
+
}
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
function json(status, body) {
|
|
51
|
+
return Response.json(body, { status, headers: { 'cache-control': 'no-store' } });
|
|
52
|
+
}
|
|
53
|
+
function status(code) {
|
|
54
|
+
if (code === 'validation')
|
|
55
|
+
return 400;
|
|
56
|
+
if (code === 'forbidden')
|
|
57
|
+
return 403;
|
|
58
|
+
if (code === 'not_found')
|
|
59
|
+
return 404;
|
|
60
|
+
if (code === 'provider')
|
|
61
|
+
return 502;
|
|
62
|
+
return 500;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
package/dist/src/mcp.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
|
+
const outputSchema = { result: z.unknown() };
|
|
4
|
+
export function createChannelsMcpServer(channels) {
|
|
5
|
+
const server = new McpServer({ name: 'amalgm-channels-mcp-server', version: '0.1.0' });
|
|
6
|
+
server.registerTool('amalgm_channels_notify_user', {
|
|
7
|
+
title: 'Notify current user',
|
|
8
|
+
description: 'Queue one durable notification to the authenticated Amalgm user. The caller cannot select another recipient. Supply an idempotency key when retrying the same logical message.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
message: z.string().min(1).max(4_000).describe('Notification text.'),
|
|
11
|
+
title: z.string().min(1).max(160).optional().describe('Short notification title.'),
|
|
12
|
+
channel: z.enum(['auto', 'email']).optional().describe('Delivery preference. Auto currently resolves to email.'),
|
|
13
|
+
idempotency_key: z.string().min(1).max(200).optional().describe('Stable key for safe retries of this exact notification.'),
|
|
14
|
+
},
|
|
15
|
+
outputSchema,
|
|
16
|
+
annotations: {
|
|
17
|
+
readOnlyHint: false,
|
|
18
|
+
destructiveHint: false,
|
|
19
|
+
idempotentHint: true,
|
|
20
|
+
openWorldHint: true,
|
|
21
|
+
},
|
|
22
|
+
}, async ({ message, title, channel, idempotency_key }) => result(await channels.notify({
|
|
23
|
+
message,
|
|
24
|
+
...(title === undefined ? {} : { title }),
|
|
25
|
+
...(channel === undefined ? {} : { channel }),
|
|
26
|
+
...(idempotency_key === undefined ? {} : { idempotencyKey: idempotency_key }),
|
|
27
|
+
})));
|
|
28
|
+
server.registerTool('amalgm_channels_list_deliveries', {
|
|
29
|
+
title: 'List notification deliveries',
|
|
30
|
+
description: 'List the authenticated user\'s durable notification delivery history.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
status: z.enum(['pending', 'sending', 'delivered', 'failed']).optional(),
|
|
33
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
34
|
+
offset: z.number().int().min(0).optional(),
|
|
35
|
+
},
|
|
36
|
+
outputSchema,
|
|
37
|
+
annotations: readOnly(),
|
|
38
|
+
}, async ({ status, limit, offset }) => result(await channels.list({
|
|
39
|
+
...(status === undefined ? {} : { status }),
|
|
40
|
+
...(limit === undefined ? {} : { limit }),
|
|
41
|
+
...(offset === undefined ? {} : { offset }),
|
|
42
|
+
})));
|
|
43
|
+
server.registerTool('amalgm_channels_get_delivery', {
|
|
44
|
+
title: 'Get notification delivery',
|
|
45
|
+
description: 'Get one durable notification delivery by id for the authenticated user.',
|
|
46
|
+
inputSchema: { delivery_id: z.string().min(1).max(200) },
|
|
47
|
+
outputSchema,
|
|
48
|
+
annotations: readOnly(),
|
|
49
|
+
}, async ({ delivery_id }) => result(await channels.get(delivery_id)));
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
function result(value) {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
55
|
+
structuredContent: { result: value },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function readOnly() {
|
|
59
|
+
return {
|
|
60
|
+
readOnlyHint: true,
|
|
61
|
+
destructiveHint: false,
|
|
62
|
+
idempotentHint: true,
|
|
63
|
+
openWorldHint: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ChannelsError } from './errors.js';
|
|
2
|
+
import { deliveryQuery, identifier, notification } from './validation.js';
|
|
3
|
+
export function createChannelsService(repository) {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
for(principal) {
|
|
6
|
+
const userId = identifier(principal.userId, 'principal.userId');
|
|
7
|
+
const scopes = new Set(principal.scopes);
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
async notify(input) {
|
|
10
|
+
requireScope(scopes, 'channels:send');
|
|
11
|
+
return repository.enqueue(userId, notification(input));
|
|
12
|
+
},
|
|
13
|
+
async list(query) {
|
|
14
|
+
requireScope(scopes, 'channels:read');
|
|
15
|
+
return repository.list(userId, deliveryQuery(query));
|
|
16
|
+
},
|
|
17
|
+
async get(deliveryId) {
|
|
18
|
+
requireScope(scopes, 'channels:read');
|
|
19
|
+
return repository.get(userId, identifier(deliveryId, 'deliveryId'));
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function requireScope(scopes, required) {
|
|
26
|
+
if (!scopes.has(required))
|
|
27
|
+
throw new ChannelsError('forbidden', `Missing scope: ${required}`);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE1E,MAAM,UAAU,qBAAqB,CAAC,UAA8B;IAClE,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,CAAC,SAA2B;YAC7B,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;YAChE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAS,SAAS,CAAC,MAAM,CAAC,CAAC;YACjD,OAAO,MAAM,CAAC,MAAM,CAAC;gBACnB,KAAK,CAAC,MAAM,CAAC,KAAwC;oBACnD,YAAY,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;oBACtC,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;gBACzD,CAAC;gBACD,KAAK,CAAC,IAAI,CAAC,KAAuC;oBAChD,YAAY,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;oBACtC,OAAO,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;gBACvD,CAAC;gBACD,KAAK,CAAC,GAAG,CAAC,UAAkB;oBAC1B,YAAY,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;oBACtC,OAAO,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;gBACtE,CAAC;aACF,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,MAA2B,EAAE,QAAgB;IACjE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,kBAAkB,QAAQ,EAAE,CAAC,CAAC;AAChG,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ListDeliveries, NormalizedListDeliveries, NotifyUserInput } from './contract.js';
|
|
2
|
+
export declare function notification(input: NotifyUserInput): Required<NotifyUserInput>;
|
|
3
|
+
export declare function deliveryQuery(input?: ListDeliveries): NormalizedListDeliveries;
|
|
4
|
+
export declare function identifier(value: string, name: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ChannelsError } from './errors.js';
|
|
3
|
+
const statuses = new Set(['pending', 'sending', 'delivered', 'failed']);
|
|
4
|
+
export function notification(input) {
|
|
5
|
+
const message = bounded(input.message, 'message', 1, 4_000);
|
|
6
|
+
const title = input.title === undefined
|
|
7
|
+
? 'Amalgm reminder'
|
|
8
|
+
: bounded(input.title, 'title', 1, 160);
|
|
9
|
+
const channel = input.channel ?? 'auto';
|
|
10
|
+
if (channel !== 'auto' && channel !== 'email') {
|
|
11
|
+
throw new ChannelsError('validation', 'channel must be auto or email');
|
|
12
|
+
}
|
|
13
|
+
const idempotencyKey = input.idempotencyKey === undefined
|
|
14
|
+
? randomUUID()
|
|
15
|
+
: bounded(input.idempotencyKey, 'idempotencyKey', 1, 200);
|
|
16
|
+
return Object.freeze({ message, title, channel, idempotencyKey });
|
|
17
|
+
}
|
|
18
|
+
export function deliveryQuery(input = {}) {
|
|
19
|
+
const limit = input.limit ?? 50;
|
|
20
|
+
const offset = input.offset ?? 0;
|
|
21
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
22
|
+
throw new ChannelsError('validation', 'limit must be an integer from 1 to 100');
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
25
|
+
throw new ChannelsError('validation', 'offset must be a non-negative integer');
|
|
26
|
+
}
|
|
27
|
+
if (input.status !== undefined && !statuses.has(input.status)) {
|
|
28
|
+
throw new ChannelsError('validation', 'status is invalid');
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
32
|
+
limit,
|
|
33
|
+
offset,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function identifier(value, name) {
|
|
37
|
+
return bounded(value, name, 1, 200);
|
|
38
|
+
}
|
|
39
|
+
function bounded(value, name, minimum, maximum) {
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
throw new ChannelsError('validation', `${name} must be a string`);
|
|
42
|
+
const normalized = value.trim();
|
|
43
|
+
if (normalized.length < minimum || normalized.length > maximum) {
|
|
44
|
+
throw new ChannelsError('validation', `${name} must be ${minimum}-${maximum} characters`);
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DeliveryRepository, EmailProvider, RecipientDirectory } from './contract.js';
|
|
2
|
+
export interface ChannelsWorkerOptions {
|
|
3
|
+
readonly batchSize?: number;
|
|
4
|
+
readonly leaseSeconds?: number;
|
|
5
|
+
readonly now?: () => Date;
|
|
6
|
+
readonly log?: (event: string, details: Readonly<Record<string, unknown>>) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare class ChannelsWorker {
|
|
9
|
+
#private;
|
|
10
|
+
private readonly repository;
|
|
11
|
+
private readonly directory;
|
|
12
|
+
private readonly email;
|
|
13
|
+
constructor(repository: DeliveryRepository, directory: RecipientDirectory, email: EmailProvider, options?: ChannelsWorkerOptions);
|
|
14
|
+
drainOnce(): Promise<number>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class ChannelsWorker {
|
|
2
|
+
repository;
|
|
3
|
+
directory;
|
|
4
|
+
email;
|
|
5
|
+
#batchSize;
|
|
6
|
+
#leaseSeconds;
|
|
7
|
+
#now;
|
|
8
|
+
#log;
|
|
9
|
+
constructor(repository, directory, email, options = {}) {
|
|
10
|
+
this.repository = repository;
|
|
11
|
+
this.directory = directory;
|
|
12
|
+
this.email = email;
|
|
13
|
+
this.#batchSize = options.batchSize ?? 20;
|
|
14
|
+
this.#leaseSeconds = options.leaseSeconds ?? 60;
|
|
15
|
+
this.#now = options.now ?? (() => new Date());
|
|
16
|
+
this.#log = options.log ?? (() => { });
|
|
17
|
+
}
|
|
18
|
+
async drainOnce() {
|
|
19
|
+
const deliveries = await this.repository.claim(this.#batchSize, this.#leaseSeconds);
|
|
20
|
+
await Promise.all(deliveries.map((delivery) => this.#deliver(delivery)));
|
|
21
|
+
return deliveries.length;
|
|
22
|
+
}
|
|
23
|
+
async #deliver(delivery) {
|
|
24
|
+
try {
|
|
25
|
+
const to = await this.directory.emailForUser(delivery.userId);
|
|
26
|
+
const result = await this.email.send({
|
|
27
|
+
to,
|
|
28
|
+
subject: delivery.title ?? 'Amalgm reminder',
|
|
29
|
+
text: delivery.message,
|
|
30
|
+
idempotencyKey: delivery.idempotencyKey,
|
|
31
|
+
});
|
|
32
|
+
await this.repository.complete(delivery, result.providerId, this.#now());
|
|
33
|
+
this.#log('delivery.completed', { deliveryId: delivery.id, attempts: delivery.attempts });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const message = safeError(error);
|
|
37
|
+
await this.repository.fail(delivery, message, this.#now());
|
|
38
|
+
this.#log('delivery.failed', { deliveryId: delivery.id, error: message });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function safeError(error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : 'Provider delivery failed';
|
|
44
|
+
return message.replace(/[\r\n]+/g, ' ').slice(0, 500);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/worker.ts"],"names":[],"mappings":"AAcA,MAAM,OAAO,cAAc;IAON,UAAU;IACV,SAAS;IACT,KAAK;IARf,UAAU,CAAS;IACnB,aAAa,CAAS;IACtB,IAAI,CAAa;IACjB,IAAI,CAA4C;IAEzD,YACmB,UAA8B,EAC9B,SAA6B,EAC7B,KAAoB,EACrC,OAAO,GAA0B,EAAE;0BAHlB,UAAU;yBACV,SAAS;qBACT,KAAK;QAGtB,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QACpF,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACzE,OAAO,UAAU,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,QAAyB;QACtC,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBACnC,EAAE;gBACF,OAAO,EAAE,QAAQ,CAAC,KAAK,IAAI,iBAAiB;gBAC5C,IAAI,EAAE,QAAQ,CAAC,OAAO;gBACtB,cAAc,EAAE,QAAQ,CAAC,cAAc;aACxC,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACzE,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5F,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YACjC,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC3D,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;CACF;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC;IACpF,OAAO,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amalgm/channels",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Amalgm's SDK and hosted service for durable user notification delivery.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/amalgm-inc/amalgm-channels.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"exports": {
|
|
13
|
+
".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" },
|
|
14
|
+
"./mcp": { "types": "./dist/src/mcp.d.ts", "import": "./dist/src/mcp.js" },
|
|
15
|
+
"./host": { "types": "./dist/host/index.d.ts", "import": "./dist/host/index.js" }
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist", "supabase", "skills", "PURPOSE.md", "README.md"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
20
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
21
|
+
"check": "tsx scripts/check-tree.ts && tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"test": "tsx --test test/*.test.ts",
|
|
23
|
+
"verify": "npm run check && npm test && npm run build",
|
|
24
|
+
"prepack": "npm run build",
|
|
25
|
+
"start": "node dist/host/main.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": { "node": ">=20" },
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@amalgm/core": "0.4.2",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
31
|
+
"@supabase/supabase-js": "2.57.4",
|
|
32
|
+
"jose": "^6.1.0",
|
|
33
|
+
"zod": "^4.4.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^26.1.2",
|
|
37
|
+
"tsx": "^4.23.1",
|
|
38
|
+
"typescript": "^7.0.2"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: amalgm-channels
|
|
3
|
+
description: Send a durable notification to the current Amalgm user or inspect their delivery history through the Channels tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Amalgm Channels
|
|
7
|
+
|
|
8
|
+
Use Channels when the user asks to be notified, reminded, emailed, or contacted
|
|
9
|
+
by an automation. Channels always addresses the authenticated current user. Do
|
|
10
|
+
not ask for or pass their email address.
|
|
11
|
+
|
|
12
|
+
## Send
|
|
13
|
+
|
|
14
|
+
Call `amalgm_channels_notify_user` with concise `message` text. Add a short
|
|
15
|
+
`title` when it improves recognition. Use `channel: auto` unless the user
|
|
16
|
+
explicitly asks for email.
|
|
17
|
+
|
|
18
|
+
When the notification belongs to an automation run, always pass that durable
|
|
19
|
+
run id as `idempotency_key`. Retrying the same run will then return the original
|
|
20
|
+
delivery instead of sending twice.
|
|
21
|
+
|
|
22
|
+
The returned `pending` state means the message is durably queued, not yet
|
|
23
|
+
delivered. Use `amalgm_channels_get_delivery` if confirmed delivery matters.
|
|
24
|
+
|
|
25
|
+
## Inspect
|
|
26
|
+
|
|
27
|
+
Use `amalgm_channels_list_deliveries` for history and status filtering. Delivery
|
|
28
|
+
records expose content, attempts, safe provider status, and timestamps; they do
|
|
29
|
+
not expose recipient coordinates or provider credentials.
|