@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
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 @@
|
|
|
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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"}
|