@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.
Files changed (56) hide show
  1. package/PURPOSE.md +50 -0
  2. package/README.md +17 -0
  3. package/dist/host/auth.d.ts +10 -0
  4. package/dist/host/auth.js +76 -0
  5. package/dist/host/auth.js.map +1 -0
  6. package/dist/host/config.d.ts +10 -0
  7. package/dist/host/config.js +32 -0
  8. package/dist/host/config.js.map +1 -0
  9. package/dist/host/index.d.ts +6 -0
  10. package/dist/host/index.js +7 -0
  11. package/dist/host/index.js.map +1 -0
  12. package/dist/host/main.d.ts +1 -0
  13. package/dist/host/main.js +29 -0
  14. package/dist/host/main.js.map +1 -0
  15. package/dist/host/recipient.d.ts +7 -0
  16. package/dist/host/recipient.js +17 -0
  17. package/dist/host/recipient.js.map +1 -0
  18. package/dist/host/repository.d.ts +13 -0
  19. package/dist/host/repository.js +103 -0
  20. package/dist/host/repository.js.map +1 -0
  21. package/dist/host/resend.d.ts +10 -0
  22. package/dist/host/resend.js +33 -0
  23. package/dist/host/resend.js.map +1 -0
  24. package/dist/host/server.d.ts +12 -0
  25. package/dist/host/server.js +57 -0
  26. package/dist/host/server.js.map +1 -0
  27. package/dist/src/client.d.ts +7 -0
  28. package/dist/src/client.js +41 -0
  29. package/dist/src/client.js.map +1 -0
  30. package/dist/src/contract.d.ts +83 -0
  31. package/dist/src/contract.js +2 -0
  32. package/dist/src/contract.js.map +1 -0
  33. package/dist/src/errors.d.ts +5 -0
  34. package/dist/src/errors.js +9 -0
  35. package/dist/src/errors.js.map +1 -0
  36. package/dist/src/http.d.ts +6 -0
  37. package/dist/src/http.js +64 -0
  38. package/dist/src/http.js.map +1 -0
  39. package/dist/src/index.d.ts +7 -0
  40. package/dist/src/index.js +8 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/mcp.d.ts +3 -0
  43. package/dist/src/mcp.js +66 -0
  44. package/dist/src/mcp.js.map +1 -0
  45. package/dist/src/service.d.ts +2 -0
  46. package/dist/src/service.js +29 -0
  47. package/dist/src/service.js.map +1 -0
  48. package/dist/src/validation.d.ts +4 -0
  49. package/dist/src/validation.js +48 -0
  50. package/dist/src/validation.js.map +1 -0
  51. package/dist/src/worker.d.ts +15 -0
  52. package/dist/src/worker.js +46 -0
  53. package/dist/src/worker.js.map +1 -0
  54. package/package.json +40 -0
  55. package/skills/channels/SKILL.md +29 -0
  56. package/supabase/migrations/20260829000000_create_channels.sql +222 -0
package/PURPOSE.md ADDED
@@ -0,0 +1,50 @@
1
+ # Amalgm Channels
2
+
3
+ ## Purpose
4
+
5
+ Channels is the sole owner of delivering a message from Amalgm to the current
6
+ user. A caller supplies content and an optional channel preference; Channels
7
+ resolves the authenticated user's destination, persists one durable delivery,
8
+ and hands it to the configured provider. Callers never choose another user,
9
+ handle provider credentials, or infer whether delivery happened from a network
10
+ attempt.
11
+
12
+ The SDK owns notification validation, idempotency, delivery state, and history.
13
+ The HTTP API, MCP server, CLI, worker, and Fly host are adapters over that SDK.
14
+ Supabase is the durable authority; providers are effects supplied by the host.
15
+ The hosted service is its own deployable and does not run inside or depend on
16
+ Amalgm Gateway.
17
+
18
+ ## Axioms
19
+
20
+ 1. Every delivery belongs to exactly one authenticated user.
21
+ 2. A public send input never contains a user id, email address, phone number,
22
+ provider credential, or other recipient coordinate.
23
+ 3. One idempotency key names at most one delivery for a user.
24
+ 4. A provider attempt begins only after a durable, exclusively leased delivery
25
+ exists; completion is accepted only from that lease.
26
+ 5. Failed and expired leases remain visible and retryable. They are never
27
+ reported as delivered.
28
+ 6. Supabase is authoritative for pending work and permanent delivery history.
29
+ 7. Provider responses are reduced to bounded identifiers and safe errors before
30
+ persistence; secrets and recipient coordinates never enter public records.
31
+ 8. The SDK is the behavior. HTTP, MCP, CLI, workers, and provider adapters stay
32
+ thin and do not create another send path.
33
+ 9. Browser sessions use verified user identity. Machines use Core's exact
34
+ `amalgm-channels` audience, scope checks, and one-time DPoP proof.
35
+ 10. Channels is a standalone service. Gateway does not compose, proxy, schedule,
36
+ or deliver its work.
37
+
38
+ ## Tree
39
+
40
+ ```text
41
+ src/contract.ts public SDK shapes
42
+ src/service.ts validation and user-bound behavior
43
+ src/worker.ts lease-to-provider delivery pump
44
+ src/http.ts HTTP adapter
45
+ src/client.ts browser/host API client
46
+ src/mcp.ts agent adapter
47
+ host/ Supabase, auth, provider, server, and Fly composition
48
+ supabase/migrations/ durable schema and atomic RPCs
49
+ skills/channels/ agent usage guidance
50
+ ```
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @amalgm/channels
2
+
3
+ The SDK and standalone hosted service for sending notifications to the current
4
+ Amalgm user.
5
+
6
+ ```ts
7
+ const channels = service.for({ userId, scopes: ['channels:send'] });
8
+ await channels.notify({ message: 'Call Mom', idempotencyKey: runId });
9
+ ```
10
+
11
+ Use `createChannelsApi` for HTTP, `createChannelsMcpServer` for agents, and
12
+ `createChannelsHost` for the Fly service. See [PURPOSE.md](./PURPOSE.md) for the
13
+ ownership and security laws.
14
+
15
+ ```bash
16
+ npm run verify
17
+ ```
@@ -0,0 +1,10 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { type AuthorizationScope } from '@amalgm/core/authorization';
3
+ import type { ChannelPrincipal, ChannelScope } from '../src/contract.js';
4
+ export declare function createChannelsAuthenticator(options: {
5
+ readonly issuer: string;
6
+ readonly supabase: SupabaseClient;
7
+ readonly now?: () => number;
8
+ }): (request: Request) => Promise<ChannelPrincipal>;
9
+ export declare function machineHeaders(authorization: string, dpop: string): Readonly<Record<string, string>>;
10
+ export type ChannelsAuthorizationScope = Extract<AuthorizationScope, ChannelScope>;
@@ -0,0 +1,76 @@
1
+ import { authorizeMachineResource, } from '@amalgm/core/authorization';
2
+ import { calculateJwkThumbprint, createRemoteJWKSet, decodeProtectedHeader, importJWK, jwtVerify, } from 'jose';
3
+ import { createHash } from 'node:crypto';
4
+ import { ChannelsError } from '../src/errors.js';
5
+ export function createChannelsAuthenticator(options) {
6
+ const jwks = createRemoteJWKSet(new URL(`${options.issuer}/.well-known/jwks.json`));
7
+ const ports = resourcePorts(options, jwks);
8
+ return async (request) => {
9
+ const authorization = request.headers.get('authorization');
10
+ if (authorization?.startsWith('Bearer ')) {
11
+ const token = authorization.slice('Bearer '.length).trim();
12
+ const result = await options.supabase.auth.getUser(token);
13
+ if (result.error || !result.data.user)
14
+ throw new ChannelsError('forbidden', 'User authorization denied');
15
+ return Object.freeze({
16
+ userId: result.data.user.id,
17
+ scopes: Object.freeze(['channels:read', 'channels:send']),
18
+ });
19
+ }
20
+ const required = request.method.toUpperCase() === 'POST'
21
+ ? ['channels:send']
22
+ : ['channels:read'];
23
+ const principal = await authorizeMachineResource({
24
+ authorization,
25
+ dpop: request.headers.get('dpop'),
26
+ method: request.method,
27
+ url: request.url,
28
+ issuer: options.issuer,
29
+ audience: 'amalgm-channels',
30
+ requiredScopes: required,
31
+ }, ports);
32
+ return Object.freeze({
33
+ userId: principal.userId,
34
+ scopes: principal.scopes.filter((scope) => scope.startsWith('channels:')),
35
+ });
36
+ };
37
+ }
38
+ function resourcePorts(options, jwks) {
39
+ return {
40
+ nowSeconds: options.now ?? (() => Math.floor(Date.now() / 1000)),
41
+ async verifyAccessToken(token) {
42
+ const verified = await jwtVerify(token, jwks, {
43
+ issuer: options.issuer,
44
+ audience: 'amalgm-channels',
45
+ algorithms: ['ES256'],
46
+ });
47
+ return { header: verified.protectedHeader, claims: verified.payload };
48
+ },
49
+ async verifyDpopProof(proof) {
50
+ const header = decodeProtectedHeader(proof);
51
+ const jwk = header.jwk;
52
+ if (!jwk)
53
+ throw new Error('DPoP proof has no public key');
54
+ const verified = await jwtVerify(proof, await importJWK(jwk, 'ES256'), {
55
+ algorithms: ['ES256'],
56
+ typ: 'dpop+jwt',
57
+ });
58
+ return { header: verified.protectedHeader, claims: verified.payload };
59
+ },
60
+ jwkThumbprint: (jwk) => calculateJwkThumbprint(jwk, 'sha256'),
61
+ accessTokenHash: async (token) => createHash('sha256').update(token).digest('base64url'),
62
+ async consumeProof(keyThumbprint, jti) {
63
+ const result = await options.supabase.rpc('consume_authorization_dpop_proof', {
64
+ p_key_thumbprint: keyThumbprint,
65
+ p_jti: jti,
66
+ });
67
+ if (result.error)
68
+ throw result.error;
69
+ return result.data === true;
70
+ },
71
+ };
72
+ }
73
+ export function machineHeaders(authorization, dpop) {
74
+ return Object.freeze({ authorization, dpop });
75
+ }
76
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../host/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export interface ChannelsHostConfig {
2
+ readonly port: number;
3
+ readonly supabaseUrl: string;
4
+ readonly supabaseServiceRoleKey: string;
5
+ readonly authorizationIssuer: string;
6
+ readonly resendApiKey: string;
7
+ readonly emailFrom: string;
8
+ readonly pollIntervalMs: number;
9
+ }
10
+ export declare function channelsHostConfig(env?: Readonly<Record<string, string | undefined>>): ChannelsHostConfig;
@@ -0,0 +1,32 @@
1
+ export function channelsHostConfig(env = process.env) {
2
+ return Object.freeze({
3
+ port: integer(env.PORT, 8080, 1, 65_535),
4
+ supabaseUrl: httpsUrl(env.SUPABASE_URL ?? env.NEXT_PUBLIC_SUPABASE_URL, 'SUPABASE_URL'),
5
+ supabaseServiceRoleKey: required(env.SUPABASE_SERVICE_ROLE_KEY, 'SUPABASE_SERVICE_ROLE_KEY'),
6
+ authorizationIssuer: httpsUrl(env.AMALGM_AUTHORIZATION_ISSUER, 'AMALGM_AUTHORIZATION_ISSUER'),
7
+ resendApiKey: required(env.RESEND_API_KEY, 'RESEND_API_KEY'),
8
+ emailFrom: required(env.CHANNELS_EMAIL_FROM, 'CHANNELS_EMAIL_FROM'),
9
+ pollIntervalMs: integer(env.CHANNELS_POLL_INTERVAL_MS, 1_000, 250, 60_000),
10
+ });
11
+ }
12
+ function required(value, name) {
13
+ if (!value?.trim())
14
+ throw new Error(`${name} is required`);
15
+ return value.trim();
16
+ }
17
+ function httpsUrl(value, name) {
18
+ const parsed = new URL(required(value, name));
19
+ const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
20
+ if (parsed.protocol !== 'https:' && !(local && parsed.protocol === 'http:')) {
21
+ throw new Error(`${name} must use HTTPS`);
22
+ }
23
+ return parsed.toString().replace(/\/$/, '');
24
+ }
25
+ function integer(value, fallback, minimum, maximum) {
26
+ const parsed = value === undefined ? fallback : Number(value);
27
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
28
+ throw new Error(`Expected an integer from ${minimum} to ${maximum}`);
29
+ }
30
+ return parsed;
31
+ }
32
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../host/config.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,kBAAkB,CAChC,GAAG,GAAiD,OAAO,CAAC,GAAG;IAE/D,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC;QACxC,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,wBAAwB,EAAE,cAAc,CAAC;QACvF,sBAAsB,EAAE,QAAQ,CAAC,GAAG,CAAC,yBAAyB,EAAE,2BAA2B,CAAC;QAC5F,mBAAmB,EAAE,QAAQ,CAAC,GAAG,CAAC,2BAA2B,EAAE,6BAA6B,CAAC;QAC7F,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,cAAc,EAAE,gBAAgB,CAAC;QAC5D,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,mBAAmB,EAAE,qBAAqB,CAAC;QACnE,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC;KAC3E,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB,EAAE,IAAY;IACvD,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB,EAAE,IAAY;IACvD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,CAAC;IACjF,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,iBAAiB,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,OAAO,CACd,KAAyB,EACzB,QAAgB,EAChB,OAAe,EACf,OAAe;IAEf,MAAM,MAAM,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,OAAO,IAAI,MAAM,GAAG,OAAO,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,4BAA4B,OAAO,OAAO,OAAO,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from './auth.js';
2
+ export * from './config.js';
3
+ export * from './recipient.js';
4
+ export * from './repository.js';
5
+ export * from './resend.js';
6
+ export * from './server.js';
@@ -0,0 +1,7 @@
1
+ export * from './auth.js';
2
+ export * from './config.js';
3
+ export * from './recipient.js';
4
+ export * from './repository.js';
5
+ export * from './resend.js';
6
+ export * from './server.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../host/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { createChannelsApi } from '../src/http.js';
3
+ import { createChannelsService } from '../src/service.js';
4
+ import { ChannelsWorker } from '../src/worker.js';
5
+ import { createChannelsAuthenticator } from './auth.js';
6
+ import { channelsHostConfig } from './config.js';
7
+ import { SupabaseRecipientDirectory } from './recipient.js';
8
+ import { SupabaseDeliveryRepository } from './repository.js';
9
+ import { ResendEmailProvider } from './resend.js';
10
+ import { createChannelsHost } from './server.js';
11
+ const config = channelsHostConfig();
12
+ const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
13
+ auth: { persistSession: false, autoRefreshToken: false },
14
+ });
15
+ const repository = new SupabaseDeliveryRepository(supabase);
16
+ const worker = new ChannelsWorker(repository, new SupabaseRecipientDirectory(supabase), new ResendEmailProvider(config.resendApiKey, config.emailFrom), { log });
17
+ const api = createChannelsApi({
18
+ service: createChannelsService(repository),
19
+ authenticate: createChannelsAuthenticator({ issuer: config.authorizationIssuer, supabase }),
20
+ });
21
+ const host = createChannelsHost({ api, worker, pollIntervalMs: config.pollIntervalMs, log });
22
+ host.server.listen(config.port, '0.0.0.0', () => log('host.ready', { port: config.port }));
23
+ for (const signal of ['SIGINT', 'SIGTERM']) {
24
+ process.once(signal, () => void host.close().finally(() => process.exit(0)));
25
+ }
26
+ function log(event, details = {}) {
27
+ console.log(JSON.stringify({ service: 'amalgm-channels', event, ...details }));
28
+ }
29
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../host/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,2BAA2B,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;AACpC,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,sBAAsB,EAAE;IAC/E,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE;CACzD,CAAC,CAAC;AACH,MAAM,UAAU,GAAG,IAAI,0BAA0B,CAAC,QAAQ,CAAC,CAAC;AAC5D,MAAM,MAAM,GAAG,IAAI,cAAc,CAC/B,UAAU,EACV,IAAI,0BAA0B,CAAC,QAAQ,CAAC,EACxC,IAAI,mBAAmB,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,SAAS,CAAC,EAC9D,EAAE,GAAG,EAAE,CACR,CAAC;AACF,MAAM,GAAG,GAAG,iBAAiB,CAAC;IAC5B,OAAO,EAAE,qBAAqB,CAAC,UAAU,CAAC;IAC1C,YAAY,EAAE,2BAA2B,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,mBAAmB,EAAE,QAAQ,EAAE,CAAC;CAC5F,CAAC,CAAC;AACH,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;AAE7F,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC3F,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,GAAG,CAAC,KAAa,EAAE,OAAO,GAAsC,EAAE;IACzE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC;AACjF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { RecipientDirectory } from '../src/contract.js';
3
+ export declare class SupabaseRecipientDirectory implements RecipientDirectory {
4
+ private readonly client;
5
+ constructor(client: SupabaseClient);
6
+ emailForUser(userId: string): Promise<string>;
7
+ }
@@ -0,0 +1,17 @@
1
+ import { ChannelsError } from '../src/errors.js';
2
+ export class SupabaseRecipientDirectory {
3
+ client;
4
+ constructor(client) {
5
+ this.client = client;
6
+ }
7
+ async emailForUser(userId) {
8
+ const result = await this.client.auth.admin.getUserById(userId);
9
+ if (result.error)
10
+ throw new ChannelsError('internal', 'Recipient lookup failed');
11
+ const email = result.data.user?.email?.trim().toLowerCase();
12
+ if (!email)
13
+ throw new ChannelsError('provider', 'The current user has no email channel');
14
+ return email;
15
+ }
16
+ }
17
+ //# sourceMappingURL=recipient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipient.js","sourceRoot":"","sources":["../../host/recipient.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,OAAO,0BAA0B;IACR,MAAM;IAAnC,YAA6B,MAAsB;sBAAtB,MAAM;IAAmB,CAAC;IAEvD,KAAK,CAAC,YAAY,CAAC,MAAc;QAC/B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,KAAK;YAAE,MAAM,IAAI,aAAa,CAAC,UAAU,EAAE,yBAAyB,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC5D,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,aAAa,CAAC,UAAU,EAAE,uCAAuC,CAAC,CAAC;QACzF,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { Delivery, DeliveryPage, DeliveryRepository, NormalizedListDeliveries, NotifyUserInput, PendingDelivery } from '../src/contract.js';
3
+ export declare class SupabaseDeliveryRepository implements DeliveryRepository {
4
+ #private;
5
+ private readonly client;
6
+ constructor(client: SupabaseClient);
7
+ enqueue(userId: string, input: Required<NotifyUserInput>): Promise<Delivery>;
8
+ list(userId: string, query: NormalizedListDeliveries): Promise<DeliveryPage>;
9
+ get(userId: string, deliveryId: string): Promise<Delivery | null>;
10
+ claim(limit: number, leaseSeconds: number): Promise<readonly PendingDelivery[]>;
11
+ complete(item: PendingDelivery, providerId: string, now: Date): Promise<boolean>;
12
+ fail(item: PendingDelivery, error: string, now: Date): Promise<boolean>;
13
+ }
@@ -0,0 +1,103 @@
1
+ import { ChannelsError } from '../src/errors.js';
2
+ export class SupabaseDeliveryRepository {
3
+ client;
4
+ constructor(client) {
5
+ this.client = client;
6
+ }
7
+ async enqueue(userId, input) {
8
+ return delivery(await this.#rpc('enqueue_amalgm_channel_delivery', {
9
+ p_user_id: userId,
10
+ p_delivery: input,
11
+ }));
12
+ }
13
+ async list(userId, query) {
14
+ const page = await this.#rpc('list_amalgm_channel_deliveries', {
15
+ p_user_id: userId,
16
+ p_status: query.status ?? null,
17
+ p_limit: query.limit,
18
+ p_offset: query.offset,
19
+ });
20
+ return {
21
+ items: page.items.map(delivery),
22
+ total: page.total,
23
+ limit: query.limit,
24
+ offset: query.offset,
25
+ hasMore: query.offset + page.items.length < page.total,
26
+ };
27
+ }
28
+ async get(userId, deliveryId) {
29
+ const row = await this.#rpc('get_amalgm_channel_delivery', {
30
+ p_user_id: userId,
31
+ p_delivery_id: deliveryId,
32
+ });
33
+ return row ? delivery(row) : null;
34
+ }
35
+ async claim(limit, leaseSeconds) {
36
+ const rows = await this.#rpc('claim_amalgm_channel_deliveries', {
37
+ p_limit: limit,
38
+ p_lease_seconds: leaseSeconds,
39
+ });
40
+ return rows.map((row) => {
41
+ const title = optional(row.title);
42
+ return {
43
+ id: text(row.id),
44
+ userId: text(row.user_id),
45
+ channel: 'email',
46
+ ...(title === undefined ? {} : { title }),
47
+ message: text(row.message),
48
+ idempotencyKey: text(row.idempotency_key),
49
+ leaseToken: text(row.lease_token),
50
+ attempts: number(row.attempts),
51
+ };
52
+ });
53
+ }
54
+ complete(item, providerId, now) {
55
+ return this.#rpc('complete_amalgm_channel_delivery', {
56
+ p_delivery_id: item.id,
57
+ p_lease_token: item.leaseToken,
58
+ p_provider_id: providerId,
59
+ p_now: now.toISOString(),
60
+ });
61
+ }
62
+ fail(item, error, now) {
63
+ return this.#rpc('fail_amalgm_channel_delivery', {
64
+ p_delivery_id: item.id,
65
+ p_lease_token: item.leaseToken,
66
+ p_error: error,
67
+ p_now: now.toISOString(),
68
+ });
69
+ }
70
+ async #rpc(name, arguments_) {
71
+ const { data, error } = await this.client.rpc(name, arguments_);
72
+ if (error) {
73
+ if (error.code === '23505')
74
+ throw new ChannelsError('validation', error.message);
75
+ throw new ChannelsError('internal', `Channels storage failed: ${error.message}`);
76
+ }
77
+ return data;
78
+ }
79
+ }
80
+ function delivery(row) {
81
+ const title = optional(row.title);
82
+ const providerId = optional(row.provider_id);
83
+ const error = optional(row.error);
84
+ return {
85
+ id: text(row.id),
86
+ channel: 'email',
87
+ ...(title === undefined ? {} : { title }),
88
+ message: text(row.message),
89
+ idempotencyKey: text(row.idempotency_key),
90
+ status: text(row.status),
91
+ attempts: number(row.attempts),
92
+ createdAt: timestamp(row.created_at),
93
+ updatedAt: timestamp(row.updated_at),
94
+ ...(optional(row.delivered_at) ? { deliveredAt: timestamp(row.delivered_at) } : {}),
95
+ ...(providerId === undefined ? {} : { providerId }),
96
+ ...(error === undefined ? {} : { error }),
97
+ };
98
+ }
99
+ const text = (value) => typeof value === 'string' ? value : String(value ?? '');
100
+ const optional = (value) => typeof value === 'string' && value ? value : undefined;
101
+ const number = (value) => Number(value) || 0;
102
+ const timestamp = (value) => new Date(String(value)).toISOString();
103
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../../host/repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { EmailProvider } from '../src/contract.js';
2
+ export declare class ResendEmailProvider implements EmailProvider {
3
+ private readonly apiKey;
4
+ private readonly from;
5
+ private readonly fetch;
6
+ constructor(apiKey: string, from: string, fetch?: typeof globalThis.fetch);
7
+ send(input: Parameters<EmailProvider['send']>[0]): Promise<{
8
+ providerId: string;
9
+ }>;
10
+ }
@@ -0,0 +1,33 @@
1
+ import { ChannelsError } from '../src/errors.js';
2
+ export class ResendEmailProvider {
3
+ apiKey;
4
+ from;
5
+ fetch;
6
+ constructor(apiKey, from, fetch = globalThis.fetch) {
7
+ this.apiKey = apiKey;
8
+ this.from = from;
9
+ this.fetch = fetch;
10
+ }
11
+ async send(input) {
12
+ const response = await this.fetch('https://api.resend.com/emails', {
13
+ method: 'POST',
14
+ headers: {
15
+ authorization: `Bearer ${this.apiKey}`,
16
+ 'content-type': 'application/json',
17
+ 'idempotency-key': input.idempotencyKey,
18
+ },
19
+ body: JSON.stringify({
20
+ from: this.from,
21
+ to: [input.to],
22
+ subject: input.subject,
23
+ text: input.text,
24
+ }),
25
+ });
26
+ const body = await response.json().catch(() => ({}));
27
+ if (!response.ok || !body.id) {
28
+ throw new ChannelsError('provider', body.message?.slice(0, 300) || `Email provider returned ${response.status}`);
29
+ }
30
+ return { providerId: body.id };
31
+ }
32
+ }
33
+ //# sourceMappingURL=resend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resend.js","sourceRoot":"","sources":["../../host/resend.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,OAAO,mBAAmB;IAEX,MAAM;IACN,IAAI;IACJ,KAAK;IAHxB,YACmB,MAAc,EACd,IAAY,EACZ,KAAK,GAA4B,UAAU,CAAC,KAAK;sBAFjD,MAAM;oBACN,IAAI;qBACJ,KAAK;IACrB,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,KAA2C;QACpD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,+BAA+B,EAAE;YACjE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;gBACtC,cAAc,EAAE,kBAAkB;gBAClC,iBAAiB,EAAE,KAAK,CAAC,cAAc;aACxC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;gBACd,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;aACjB,CAAC;SACH,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAsC,CAAC;QAC1F,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YAC7B,MAAM,IAAI,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,2BAA2B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACnH,CAAC;QACD,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ import { createServer } from 'node:http';
2
+ import type { ChannelsWorker } from '../src/worker.js';
3
+ export interface ChannelsHost {
4
+ readonly server: ReturnType<typeof createServer>;
5
+ close(): Promise<void>;
6
+ }
7
+ export declare function createChannelsHost(options: {
8
+ readonly api: (request: Request) => Promise<Response>;
9
+ readonly worker: ChannelsWorker;
10
+ readonly pollIntervalMs: number;
11
+ readonly log?: (event: string, details?: Readonly<Record<string, unknown>>) => void;
12
+ }): ChannelsHost;
@@ -0,0 +1,57 @@
1
+ import { createServer } from 'node:http';
2
+ export function createChannelsHost(options) {
3
+ const log = options.log ?? (() => { });
4
+ let draining = null;
5
+ const drain = () => {
6
+ if (draining)
7
+ return;
8
+ draining = options.worker.drainOnce()
9
+ .then((count) => { if (count)
10
+ log('worker.drained', { count }); return count; })
11
+ .catch((error) => { log('worker.failed', { error: safe(error) }); return 0; })
12
+ .finally(() => { draining = null; });
13
+ };
14
+ const timer = setInterval(drain, options.pollIntervalMs);
15
+ timer.unref();
16
+ drain();
17
+ const server = createServer(async (incoming, outgoing) => {
18
+ try {
19
+ if (incoming.url === '/healthz')
20
+ return send(outgoing, Response.json({ ok: true }));
21
+ const response = await options.api(await webRequest(incoming));
22
+ send(outgoing, response);
23
+ if (incoming.method === 'POST' && incoming.url === '/v1/notifications')
24
+ drain();
25
+ }
26
+ catch (error) {
27
+ log('request.failed', { error: safe(error) });
28
+ send(outgoing, Response.json({ error: 'Channels service failed' }, { status: 500 }));
29
+ }
30
+ });
31
+ return {
32
+ server,
33
+ async close() {
34
+ clearInterval(timer);
35
+ await draining;
36
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
37
+ },
38
+ };
39
+ }
40
+ async function webRequest(request) {
41
+ const origin = `http://${request.headers.host ?? '127.0.0.1'}`;
42
+ const chunks = [];
43
+ for await (const chunk of request)
44
+ chunks.push(Buffer.from(chunk));
45
+ const body = Buffer.concat(chunks);
46
+ return new Request(new URL(request.url ?? '/', origin), {
47
+ method: request.method ?? 'GET',
48
+ headers: request.headers,
49
+ ...(body.length ? { body } : {}),
50
+ });
51
+ }
52
+ async function send(response, source) {
53
+ response.writeHead(source.status, Object.fromEntries(source.headers));
54
+ response.end(Buffer.from(await source.arrayBuffer()));
55
+ }
56
+ const safe = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 500);
57
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../host/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Channels } from './contract.js';
2
+ export type ChannelsHeaders = (method: string, url: string) => Promise<Readonly<Record<string, string>>> | Readonly<Record<string, string>>;
3
+ export declare function createChannelsClient(options: {
4
+ readonly baseUrl: string;
5
+ readonly headers: ChannelsHeaders;
6
+ readonly fetch?: typeof globalThis.fetch;
7
+ }): Channels;
@@ -0,0 +1,41 @@
1
+ import { ChannelsError } from './errors.js';
2
+ export function createChannelsClient(options) {
3
+ const base = options.baseUrl.replace(/\/$/, '');
4
+ const fetch = options.fetch ?? globalThis.fetch;
5
+ async function request(path, method, body) {
6
+ const url = `${base}${path}`;
7
+ const response = await fetch(url, {
8
+ method,
9
+ headers: {
10
+ accept: 'application/json',
11
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
12
+ ...await options.headers(method, url),
13
+ },
14
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
15
+ });
16
+ const payload = await response.json().catch(() => ({}));
17
+ if (!response.ok)
18
+ throw new ChannelsError(payload.code === 'forbidden' || payload.code === 'not_found' || payload.code === 'provider'
19
+ ? payload.code : response.status >= 500 ? 'internal' : 'validation', payload.error ?? `Channels API returned ${response.status}`);
20
+ return payload;
21
+ }
22
+ return Object.freeze({
23
+ notify: (input) => request('/v1/notifications', 'POST', input),
24
+ list: (query = {}) => request(`/v1/deliveries${queryString(query)}`, 'GET'),
25
+ get: async (deliveryId) => {
26
+ try {
27
+ return await request(`/v1/deliveries/${encodeURIComponent(deliveryId)}`, 'GET');
28
+ }
29
+ catch (error) {
30
+ if (error instanceof ChannelsError && error.code === 'not_found')
31
+ return null;
32
+ throw error;
33
+ }
34
+ },
35
+ });
36
+ }
37
+ function queryString(query) {
38
+ const entries = Object.entries(query).filter(([, value]) => value !== undefined);
39
+ return entries.length ? `?${new URLSearchParams(entries.map(([key, value]) => [key, String(value)]))}` : '';
40
+ }
41
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}