okengine 0.6.0 → 0.7.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 (162) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +98 -71
  3. package/package.json +4 -4
  4. package/site/content/docs/console/clock.mdx +25 -5
  5. package/site/content/docs/console/signals.mdx +13 -4
  6. package/site/content/docs/elements/ai.mdx +29 -2
  7. package/site/content/docs/elements/channel.mdx +90 -20
  8. package/site/content/docs/elements/clock.mdx +56 -24
  9. package/site/content/docs/elements/flow.mdx +2 -0
  10. package/site/content/docs/elements/gate.mdx +8 -1
  11. package/site/content/docs/elements/signal.mdx +173 -29
  12. package/site/content/docs/elements/store.mdx +464 -73
  13. package/site/content/docs/elements/vault.mdx +15 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +13 -6
  15. package/site/content/docs/get-started/installation.mdx +49 -13
  16. package/site/content/docs/get-started/introduction.mdx +1 -1
  17. package/site/content/docs/reference/cli.md +30 -2
  18. package/site/content/docs/reference/configuration.mdx +18 -17
  19. package/site/content/docs/reference/environment-variables.mdx +44 -18
  20. package/site/content/docs/reference/errors.mdx +12 -11
  21. package/site/content/docs/reference/fx.mdx +15 -13
  22. package/site/content/docs/reference/security.md +3 -1
  23. package/src/cli/ai-setup/ai-setup.test.ts +144 -0
  24. package/src/cli/ai-setup/apply.ts +214 -0
  25. package/src/cli/ai-setup/catalog.ts +263 -0
  26. package/src/cli/ai-setup/detect-ollama.ts +166 -0
  27. package/src/cli/ai-setup/index.ts +228 -0
  28. package/src/cli/ai-setup/prompts.ts +649 -0
  29. package/src/cli/ai-setup/recommend.test.ts +100 -0
  30. package/src/cli/ai-setup/recommend.ts +203 -0
  31. package/src/cli/ai.ts +38 -0
  32. package/src/cli/db-auto-push.test.ts +6 -1
  33. package/src/cli/db-auto-push.ts +8 -3
  34. package/src/cli/dev-db-push.test.ts +59 -0
  35. package/src/cli/dev.test.ts +40 -0
  36. package/src/cli/dev.ts +55 -1
  37. package/src/cli/docker-clean.test.ts +179 -0
  38. package/src/cli/docker-clean.ts +406 -0
  39. package/src/cli/docker.ts +16 -4
  40. package/src/cli/ensure-drizzle-config.ts +1 -1
  41. package/src/cli/hero-meta.test.ts +6 -5
  42. package/src/cli/hero-meta.ts +7 -1
  43. package/src/cli/index.ts +5 -0
  44. package/src/cli/load-config.ts +4 -4
  45. package/src/cli/openbao-bootstrap.test.ts +1 -0
  46. package/src/cli/openbao-bootstrap.ts +9 -2
  47. package/src/cli/openbao-restart.integration.test.ts +106 -97
  48. package/src/cli/registry.ts +79 -1
  49. package/src/client/live-gap.test.ts +35 -0
  50. package/src/client/transport.test.ts +21 -0
  51. package/src/client/transport.ts +27 -5
  52. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  53. package/src/config/define-config.test.ts +6 -6
  54. package/src/config/index.ts +1 -1
  55. package/src/console/server/console.test.ts +45 -0
  56. package/src/console/server/flows.ts +39 -9
  57. package/src/console/server/i18n.ts +41 -0
  58. package/src/console/server/vault.ts +12 -38
  59. package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
  60. package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
  61. package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
  62. package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
  63. package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
  64. package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
  65. package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
  66. package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
  67. package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
  68. package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
  69. package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
  70. package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
  71. package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
  72. package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
  73. package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
  74. package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
  75. package/src/console/ui/dist/index.html +1 -1
  76. package/src/console/ui/shell/components/ui.tsx +5 -1
  77. package/src/console/ui/shell/setup/Wizard.tsx +26 -5
  78. package/src/docker/cleanup.test.ts +193 -0
  79. package/src/docker/cleanup.ts +355 -0
  80. package/src/docker/docker.test.ts +4 -0
  81. package/src/docker/dockerfile.integration.test.ts +126 -119
  82. package/src/docker/index.ts +19 -0
  83. package/src/docker/recipes/ollama.ts +10 -5
  84. package/src/docker/stack.integration.test.ts +118 -102
  85. package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
  86. package/src/drivers/ai-ollama.integration.test.ts +3 -19
  87. package/src/drivers/ai-ollama.ts +45 -2
  88. package/src/drivers/ai-providers.test.ts +31 -0
  89. package/src/drivers/channel-fcm.ts +49 -53
  90. package/src/drivers/channel-msegat.ts +61 -0
  91. package/src/drivers/channel-sently-map.ts +57 -0
  92. package/src/drivers/channel-sently.test.ts +99 -0
  93. package/src/drivers/channel-sndr.ts +28 -0
  94. package/src/drivers/channel-taqnyat.ts +57 -0
  95. package/src/drivers/channel-types.ts +79 -2
  96. package/src/drivers/channel-unifonic.ts +26 -43
  97. package/src/drivers/channel-wa-cloud.ts +33 -47
  98. package/src/drivers/channel-webpush.ts +39 -239
  99. package/src/drivers/index.ts +7 -0
  100. package/src/drivers/signal-engine.ts +98 -25
  101. package/src/drivers/signal-nats.ts +4 -4
  102. package/src/drivers/signal-postgres.ts +158 -64
  103. package/src/drivers/signal-redis.ts +4 -4
  104. package/src/drivers/signal-types.ts +61 -2
  105. package/src/drivers/vault-driver-removal.test.ts +5 -1
  106. package/src/elements/channel/costs.test.ts +2 -2
  107. package/src/elements/channel/costs.ts +14 -2
  108. package/src/elements/channel/mime.ts +11 -0
  109. package/src/elements/channel/runtime.ts +94 -0
  110. package/src/elements/channel/sndr-webhooks.test.ts +26 -0
  111. package/src/elements/channel.ts +10 -1
  112. package/src/elements/clock/chaos-child.ts +150 -0
  113. package/src/elements/clock/chaos.test.ts +204 -0
  114. package/src/elements/clock/reconcile.ts +110 -0
  115. package/src/elements/clock/runtime.ts +11 -0
  116. package/src/elements/clock.test.ts +164 -2
  117. package/src/elements/clock.ts +1 -0
  118. package/src/elements/gate/runtime.ts +6 -0
  119. package/src/elements/index.ts +9 -0
  120. package/src/elements/signal/chaos-child.ts +41 -4
  121. package/src/elements/signal/declare.ts +4 -1
  122. package/src/elements/signal/delivery-modes.test.ts +179 -0
  123. package/src/elements/signal/key-ordering.test.ts +306 -0
  124. package/src/elements/signal/lease-reclaim.test.ts +123 -0
  125. package/src/elements/signal/optional-emit.test.ts +76 -0
  126. package/src/elements/signal/order-lifecycle.test.ts +130 -0
  127. package/src/elements/signal/orphan-messages.test.ts +152 -0
  128. package/src/elements/signal/runtime.ts +11 -4
  129. package/src/elements/signal/schema-emit.test.ts +107 -0
  130. package/src/elements/signal.test.ts +89 -5
  131. package/src/elements/store/files-fx.ts +104 -0
  132. package/src/elements/store/files-image.test.ts +251 -0
  133. package/src/elements/store/files-image.ts +494 -0
  134. package/src/elements/store/runtime.ts +60 -0
  135. package/src/elements/store.ts +24 -0
  136. package/src/elements/vault/boot-chain.ts +150 -0
  137. package/src/elements/vault/runtime.ts +8 -0
  138. package/src/kernel/app.ts +3 -1
  139. package/src/kernel/boot-bind/channel.test.ts +68 -3
  140. package/src/kernel/boot-bind/channel.ts +93 -2
  141. package/src/kernel/boot-bind/clock.ts +52 -7
  142. package/src/kernel/boot-bind/gate.ts +71 -3
  143. package/src/kernel/boot-bind/honor-config.test.ts +247 -0
  144. package/src/kernel/boot-bind/signal.ts +64 -7
  145. package/src/kernel/boot-bind/vault.ts +27 -8
  146. package/src/kernel/boot.ts +10 -2
  147. package/src/kernel/errors.ts +8 -2
  148. package/src/kernel/fx.test.ts +13 -0
  149. package/src/kernel/fx.ts +27 -4
  150. package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
  151. package/src/release/exports.test.ts +26 -0
  152. package/src/release/exports.ts +64 -5
  153. package/src/release/index.ts +5 -0
  154. package/src/release/measure.exports.test.ts +13 -1
  155. package/src/release/measure.ts +76 -13
  156. package/src/release/official-plugins.ts +46 -0
  157. package/src/release/readme.test.ts +30 -2
  158. package/src/runtime/dev-request-log.test.ts +20 -1
  159. package/src/runtime/dev-request-log.ts +38 -0
  160. package/src/term.test.ts +15 -0
  161. package/src/term.ts +8 -3
  162. package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Map sently channel send results into OKE {@link ChannelSendResult}.
3
+ */
4
+
5
+ import { toChannelSendResult, type AnySendResult } from "sently/channel-result";
6
+ import type { ChannelSendResult } from "./channel-types.ts";
7
+
8
+ /**
9
+ * Convert a sently email/SMS/WhatsApp/push result into an OKE channel result.
10
+ *
11
+ * @param driverId - OKE driver id fallback when provider is missing
12
+ * @param result - Sently channel-specific send result
13
+ * @param at - Attempt timestamp
14
+ */
15
+ export function mapSentlySendResult(
16
+ driverId: string,
17
+ result: AnySendResult,
18
+ at: number = Date.now(),
19
+ ): ChannelSendResult {
20
+ const normalized = toChannelSendResult(result);
21
+ const id = normalized.provider ?? driverId;
22
+ return {
23
+ ok: normalized.accepted,
24
+ messageId: normalized.messageId || crypto.randomUUID(),
25
+ driverId: id,
26
+ attempts: [
27
+ {
28
+ driverId: id,
29
+ ok: normalized.accepted,
30
+ at,
31
+ ...(normalized.messageId ? { messageId: normalized.messageId } : {}),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Build a failed OKE channel result from a thrown error.
39
+ *
40
+ * @param driverId - Driver id
41
+ * @param err - Error
42
+ * @param at - Timestamp
43
+ */
44
+ export function mapSentlySendError(
45
+ driverId: string,
46
+ err: unknown,
47
+ at: number = Date.now(),
48
+ ): ChannelSendResult {
49
+ const error = err instanceof Error ? err.message : String(err);
50
+ const id = crypto.randomUUID();
51
+ return {
52
+ ok: false,
53
+ messageId: id,
54
+ driverId,
55
+ attempts: [{ driverId, ok: false, error, at }],
56
+ };
57
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Sently-backed channel drivers — construction and message mapping.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { openFcmChannel } from "./channel-fcm.ts";
7
+ import { openMsegatChannel } from "./channel-msegat.ts";
8
+ import { openSndrChannel } from "./channel-sndr.ts";
9
+ import { openTaqnyatChannel } from "./channel-taqnyat.ts";
10
+ import { openUnifonicChannel } from "./channel-unifonic.ts";
11
+ import { openWaCloudChannel } from "./channel-wa-cloud.ts";
12
+ import { openWebPushChannel } from "./channel-webpush.ts";
13
+
14
+ describe("sently channel drivers", () => {
15
+ test("sndr requires apiKey", () => {
16
+ expect(() => openSndrChannel({})).toThrow("apiKey");
17
+ const d = openSndrChannel({ apiKey: "sndr_test_x" });
18
+ expect(d.id).toBe("sndr");
19
+ expect(d.transport?.provider).toBe("sndr");
20
+ });
21
+
22
+ test("taqnyat requires bearer + sender", () => {
23
+ expect(() => openTaqnyatChannel({ bearerToken: "t" })).toThrow("sender");
24
+ const d = openTaqnyatChannel({ bearerToken: "t", sender: "Brand" });
25
+ expect(d.id).toBe("taqnyat");
26
+ expect(d.channel?.mediums).toContain("sms");
27
+ });
28
+
29
+ test("msegat requires userName + apiKey + sender", () => {
30
+ expect(() => openMsegatChannel({ userName: "u", apiKey: "k" })).toThrow("sender");
31
+ const d = openMsegatChannel({ userName: "u", apiKey: "k", sender: "Brand" });
32
+ expect(d.id).toBe("msegat");
33
+ expect(d.smsTransport?.provider).toBe("msegat");
34
+ });
35
+
36
+ test("unifonic requires appSid and exposes smsTransport", () => {
37
+ expect(() => openUnifonicChannel({})).toThrow("appSid");
38
+ const d = openUnifonicChannel({ appSid: "sid", sender: "Brand" });
39
+ expect(d.id).toBe("unifonic");
40
+ expect(d.smsTransport?.provider).toBe("unifonic");
41
+ });
42
+
43
+ test("fcm accepts access-token mode", () => {
44
+ const d = openFcmChannel({ from: "my-project", token: "ya29.test" });
45
+ expect(d.id).toBe("fcm");
46
+ expect(d.pushTransport?.provider).toBe("fcm");
47
+ });
48
+
49
+ test("wa-cloud maps text and template sends", async () => {
50
+ const calls: unknown[] = [];
51
+ const originalFetch = globalThis.fetch;
52
+ globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
53
+ calls.push(JSON.parse(String(init?.body ?? "{}")));
54
+ return new Response(JSON.stringify({ messages: [{ id: "wamid.1" }] }), { status: 200 });
55
+ }) as typeof fetch;
56
+ try {
57
+ const d = openWaCloudChannel({ token: "tok", from: "123" });
58
+ const text = await d.channel!.send({
59
+ medium: "whatsapp",
60
+ to: "15551234567",
61
+ text: "hi",
62
+ });
63
+ expect(text.ok).toBe(true);
64
+ expect(text.messageId).toBe("wamid.1");
65
+ expect(calls[0]).toMatchObject({
66
+ messaging_product: "whatsapp",
67
+ to: "15551234567",
68
+ type: "text",
69
+ });
70
+
71
+ const tpl = await d.channel!.send({
72
+ medium: "whatsapp",
73
+ to: "15551234567",
74
+ template: "welcome",
75
+ locale: "en_US",
76
+ });
77
+ expect(tpl.ok).toBe(true);
78
+ expect(calls[1]).toMatchObject({
79
+ type: "template",
80
+ template: { name: "welcome", language: { code: "en_US" } },
81
+ });
82
+ } finally {
83
+ globalThis.fetch = originalFetch;
84
+ }
85
+ });
86
+
87
+ test("webpush requires subscription keys", async () => {
88
+ const d = openWebPushChannel({
89
+ vapidPublicKey: "BPtestpublickeythatislongenoughforvapidxxxxxxxxxxxx",
90
+ vapidPrivateKey: "dGVzdC1wcml2YXRlLWtleS0zMmJ5dGVzLW9rISEh",
91
+ vapidSubject: "mailto:ops@example.com",
92
+ });
93
+ expect(d.id).toBe("webpush");
94
+ expect(d.pushTransport?.provider).toBe("webpush");
95
+ await expect(
96
+ d.channel!.send({ medium: "push", to: "https://fcm.googleapis.com/fcm/send/x" }),
97
+ ).rejects.toThrow("pushSubscription");
98
+ });
99
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `sndr` channel driver — wraps sently's SNDR transport unchanged.
3
+ */
4
+
5
+ import { SndrTransport } from "sently/transports/sndr";
6
+ import type { ChannelDriver, ChannelOpenOptions } from "./channel-types.ts";
7
+
8
+ /**
9
+ * Open an SNDR channel driver.
10
+ *
11
+ * @param options - API key
12
+ */
13
+ export function openSndrChannel(options: ChannelOpenOptions = {}): ChannelDriver {
14
+ if (!options.apiKey) {
15
+ throw new Error("sndr channel: apiKey is required");
16
+ }
17
+ const transport = new SndrTransport({
18
+ apiKey: options.apiKey,
19
+ ...(options.url ? { baseUrl: options.url } : {}),
20
+ });
21
+ return { id: "sndr", transport };
22
+ }
23
+
24
+ /** SNDR driver factory. */
25
+ export const sndrChannelDriver = {
26
+ id: "sndr" as const,
27
+ open: openSndrChannel,
28
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `taqnyat` channel driver — SMS via sently's Taqnyat transport.
3
+ */
4
+
5
+ import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
6
+ import { mapSentlySendError, mapSentlySendResult } from "./channel-sently-map.ts";
7
+ import type {
8
+ ChannelDriver,
9
+ ChannelMessage,
10
+ ChannelOpenOptions,
11
+ ChannelSendResult,
12
+ ChannelTransport,
13
+ } from "./channel-types.ts";
14
+
15
+ /**
16
+ * Open a Taqnyat SMS driver.
17
+ *
18
+ * @param options - `bearerToken`/`token`/`apiKey` + `sender`/`from`
19
+ */
20
+ export function openTaqnyatChannel(options: ChannelOpenOptions = {}): ChannelDriver {
21
+ const bearerToken = options.bearerToken ?? options.token ?? options.apiKey;
22
+ const sender = options.sender ?? options.from;
23
+ if (!bearerToken) {
24
+ throw new Error("taqnyat channel: bearerToken (or token/apiKey) is required");
25
+ }
26
+ if (!sender) {
27
+ throw new Error("taqnyat channel: sender (or from) is required");
28
+ }
29
+
30
+ const transport = new TaqnyatSmsTransport({ bearerToken, sender });
31
+
32
+ const channel: ChannelTransport = {
33
+ provider: "taqnyat",
34
+ mediums: ["sms"],
35
+ async send(message: ChannelMessage): Promise<ChannelSendResult> {
36
+ try {
37
+ const result = await transport.send({
38
+ to: message.to,
39
+ body: message.text ?? String(message.data?.code ?? ""),
40
+ ...(message.from ? { from: message.from } : {}),
41
+ });
42
+ return mapSentlySendResult("taqnyat", result);
43
+ } catch (err) {
44
+ return mapSentlySendError("taqnyat", err);
45
+ }
46
+ },
47
+ verify: () => transport.verify(),
48
+ };
49
+
50
+ return { id: "taqnyat", channel, smsTransport: transport };
51
+ }
52
+
53
+ /** Taqnyat driver factory. */
54
+ export const taqnyatChannelDriver = {
55
+ id: "taqnyat" as const,
56
+ open: openTaqnyatChannel,
57
+ };
@@ -2,7 +2,8 @@
2
2
  * Protocol-named channel driver contracts.
3
3
  *
4
4
  * Email transports implement sently's {@link Transport} so sently transports
5
- * run unchanged. Non-email media (sms / whatsapp / push) use {@link ChannelTransport}.
5
+ * run unchanged. Non-email media expose sently SMS / WhatsApp / Push transports
6
+ * plus a {@link ChannelTransport} adapter for the OKE runtime.
6
7
  */
7
8
 
8
9
  import type { MailOptions, SendResult, Transport, VerifyResult } from "sently";
@@ -19,11 +20,66 @@ export type {
19
20
  RetryConfig,
20
21
  } from "sently";
21
22
 
23
+ /**
24
+ * Sently-compatible SMS transport (structural — sently keeps `SmsTransport` on
25
+ * internal sms-types; OKE drivers wrap concrete sently transports).
26
+ */
27
+ export interface SmsTransport {
28
+ readonly provider?: string;
29
+ send(options: {
30
+ readonly to: string;
31
+ readonly body: string;
32
+ readonly from?: string;
33
+ readonly messageId?: string;
34
+ }): Promise<{
35
+ readonly messageId: string;
36
+ readonly to: string;
37
+ readonly status: string;
38
+ readonly response: string;
39
+ readonly provider?: string;
40
+ readonly providerIndex?: number;
41
+ }>;
42
+ verify?(): Promise<VerifyResult>;
43
+ close?(): Promise<void>;
44
+ }
45
+
46
+ /** Sently-compatible WhatsApp transport (structural). */
47
+ export interface WhatsAppTransport {
48
+ readonly provider?: string;
49
+ send(options: unknown): Promise<{
50
+ readonly messageId: string;
51
+ readonly to: string;
52
+ readonly status: string;
53
+ readonly response: string;
54
+ readonly provider?: string;
55
+ readonly providerIndex?: number;
56
+ }>;
57
+ verify?(): Promise<VerifyResult>;
58
+ close?(): Promise<void>;
59
+ }
60
+
61
+ /** Sently-compatible push transport (structural). */
62
+ export interface PushTransport {
63
+ readonly provider?: string;
64
+ send(options: unknown): Promise<{
65
+ readonly messageId: string;
66
+ readonly status: string;
67
+ readonly response: string;
68
+ readonly provider?: string;
69
+ readonly providerIndex?: number;
70
+ }>;
71
+ verify?(): Promise<VerifyResult>;
72
+ close?(): Promise<void>;
73
+ }
74
+
22
75
  /** Protocol ids for channel drivers. */
23
76
  export type ChannelDriverId =
24
77
  | "console"
25
78
  | "smtp"
26
79
  | "resend"
80
+ | "sndr"
81
+ | "taqnyat"
82
+ | "msegat"
27
83
  | "unifonic"
28
84
  | "wa-cloud"
29
85
  | "fcm"
@@ -82,12 +138,19 @@ export interface ChannelTransport {
82
138
 
83
139
  /**
84
140
  * Dual-shape driver: email drivers expose a sently {@link Transport};
85
- * others expose a {@link ChannelTransport}.
141
+ * SMS / WhatsApp / push expose the matching sently transport plus a
142
+ * {@link ChannelTransport} adapter.
86
143
  */
87
144
  export interface ChannelDriver {
88
145
  readonly id: ChannelDriverId;
89
146
  /** Sently-compatible email transport (smtp / resend / console-email). */
90
147
  readonly transport?: Transport;
148
+ /** Sently SMS transport (used by runtime FallbackTransport chains). */
149
+ readonly smsTransport?: SmsTransport;
150
+ /** Sently WhatsApp transport. */
151
+ readonly whatsappTransport?: WhatsAppTransport;
152
+ /** Sently push transport (webpush / fcm). */
153
+ readonly pushTransport?: PushTransport;
91
154
  /** Medium-agnostic transport. */
92
155
  readonly channel?: ChannelTransport;
93
156
  }
@@ -102,6 +165,20 @@ export interface ChannelOpenOptions {
102
165
  readonly pass?: string;
103
166
  readonly url?: string;
104
167
  readonly token?: string;
168
+ /** Taqnyat bearer (alias of `token` / `apiKey`). */
169
+ readonly bearerToken?: string;
170
+ /** Msegat account username (alias of `user`). */
171
+ readonly userName?: string;
172
+ /** SMS alphanumeric sender id (alias of `from`). */
173
+ readonly sender?: string;
174
+ /** Unifonic AppSid (alias of `apiKey`). */
175
+ readonly appSid?: string;
176
+ /** FCM / GCP project id (alias of `from`). */
177
+ readonly projectId?: string;
178
+ /** FCM service-account client email. */
179
+ readonly clientEmail?: string;
180
+ /** FCM service-account PEM private key. */
181
+ readonly privateKey?: string;
105
182
  /** VAPID keys for webpush. */
106
183
  readonly vapidPublicKey?: string;
107
184
  readonly vapidPrivateKey?: string;
@@ -1,7 +1,9 @@
1
1
  /**
2
- * `unifonic` channel driver — SMS via Unifonic REST API.
2
+ * `unifonic` channel driver — SMS via sently's Unifonic transport.
3
3
  */
4
4
 
5
+ import { UnifonicTransport } from "sently/transports/unifonic";
6
+ import { mapSentlySendError, mapSentlySendResult } from "./channel-sently-map.ts";
5
7
  import type {
6
8
  ChannelDriver,
7
9
  ChannelMessage,
@@ -13,59 +15,40 @@ import type {
13
15
  /**
14
16
  * Open a Unifonic SMS driver.
15
17
  *
16
- * @param options - `apiKey` + optional `from` (sender id)
18
+ * @param options - `apiKey`/`appSid` (AppSid) + optional `sender`/`from` (SenderID)
17
19
  */
18
20
  export function openUnifonicChannel(options: ChannelOpenOptions = {}): ChannelDriver {
19
- const apiKey = options.apiKey;
20
- const from = options.from ?? "OKE";
21
- const fetchFn = options.fetch ?? globalThis.fetch;
22
- const base = options.url ?? "https://el.cloud.unifonic.com";
21
+ const appSid = options.appSid ?? options.apiKey;
22
+ const senderId = options.sender ?? options.from;
23
+ if (!appSid) {
24
+ throw new Error("unifonic channel: appSid (or apiKey) is required");
25
+ }
26
+
27
+ const transport = new UnifonicTransport({
28
+ appSid,
29
+ ...(senderId ? { senderId } : {}),
30
+ ...(options.url?.startsWith("https://") ? { statusCallback: options.url } : {}),
31
+ });
23
32
 
24
33
  const channel: ChannelTransport = {
25
34
  provider: "unifonic",
26
35
  mediums: ["sms"],
27
36
  async send(message: ChannelMessage): Promise<ChannelSendResult> {
28
- if (!apiKey) {
29
- throw new Error("unifonic: apiKey is required");
30
- }
31
- const body = new URLSearchParams({
32
- AppSid: apiKey,
33
- Recipient: message.to,
34
- Body: message.text ?? String(message.data?.code ?? ""),
35
- SenderID: message.from ?? from,
36
- });
37
- const res = await fetchFn(`${base}/rest/SMS/messages`, {
38
- method: "POST",
39
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
40
- body,
41
- });
42
- const text = await res.text();
43
- const id = crypto.randomUUID();
44
- if (!res.ok) {
45
- return {
46
- ok: false,
47
- messageId: id,
48
- driverId: "unifonic",
49
- attempts: [
50
- {
51
- driverId: "unifonic",
52
- ok: false,
53
- error: text || `HTTP ${res.status}`,
54
- at: Date.now(),
55
- },
56
- ],
57
- };
37
+ try {
38
+ const result = await transport.send({
39
+ to: message.to,
40
+ body: message.text ?? String(message.data?.code ?? ""),
41
+ ...(message.from ? { from: message.from } : {}),
42
+ });
43
+ return mapSentlySendResult("unifonic", result);
44
+ } catch (err) {
45
+ return mapSentlySendError("unifonic", err);
58
46
  }
59
- return {
60
- ok: true,
61
- messageId: id,
62
- driverId: "unifonic",
63
- attempts: [{ driverId: "unifonic", ok: true, at: Date.now(), messageId: id }],
64
- };
65
47
  },
48
+ verify: () => transport.verify(),
66
49
  };
67
50
 
68
- return { id: "unifonic", channel };
51
+ return { id: "unifonic", channel, smsTransport: transport };
69
52
  }
70
53
 
71
54
  /** Unifonic driver factory. */
@@ -1,7 +1,9 @@
1
1
  /**
2
- * `wa-cloud` channel driver — WhatsApp Cloud API (Meta).
2
+ * `wa-cloud` channel driver — WhatsApp Cloud API via sently.
3
3
  */
4
4
 
5
+ import { WhatsAppCloudTransport } from "sently/transports/whatsapp-cloud";
6
+ import { mapSentlySendError, mapSentlySendResult } from "./channel-sently-map.ts";
5
7
  import type {
6
8
  ChannelDriver,
7
9
  ChannelMessage,
@@ -13,64 +15,48 @@ import type {
13
15
  /**
14
16
  * Open a WhatsApp Cloud API driver.
15
17
  *
16
- * @param options - `token` (access token) + `from` (phone number id)
18
+ * @param options - `token`/`apiKey` (access token) + `from` (phone number id)
17
19
  */
18
20
  export function openWaCloudChannel(options: ChannelOpenOptions = {}): ChannelDriver {
19
- const token = options.token ?? options.apiKey;
21
+ const accessToken = options.token ?? options.apiKey;
20
22
  const phoneNumberId = options.from;
21
- const fetchFn = options.fetch ?? globalThis.fetch;
22
- const base = options.url ?? "https://graph.facebook.com/v19.0";
23
+ if (!accessToken || !phoneNumberId) {
24
+ throw new Error("wa-cloud: token and from (phone number id) are required");
25
+ }
26
+
27
+ const transport = new WhatsAppCloudTransport({ accessToken, phoneNumberId });
23
28
 
24
29
  const channel: ChannelTransport = {
25
30
  provider: "wa-cloud",
26
31
  mediums: ["whatsapp"],
27
32
  async send(message: ChannelMessage): Promise<ChannelSendResult> {
28
- if (!token || !phoneNumberId) {
29
- throw new Error("wa-cloud: token and from (phone number id) are required");
30
- }
31
- const res = await fetchFn(`${base}/${phoneNumberId}/messages`, {
32
- method: "POST",
33
- headers: {
34
- Authorization: `Bearer ${token}`,
35
- "Content-Type": "application/json",
36
- },
37
- body: JSON.stringify({
38
- messaging_product: "whatsapp",
39
- to: message.to,
40
- type: "text",
41
- text: { body: message.text ?? "" },
42
- }),
43
- });
44
- const body = (await res.json().catch(() => ({}))) as {
45
- messages?: Array<{ id?: string }>;
46
- error?: { message?: string };
47
- };
48
- const id = body.messages?.[0]?.id ?? crypto.randomUUID();
49
- if (!res.ok) {
50
- return {
51
- ok: false,
52
- messageId: id,
53
- driverId: "wa-cloud",
54
- attempts: [
55
- {
56
- driverId: "wa-cloud",
57
- ok: false,
58
- error: body.error?.message ?? `HTTP ${res.status}`,
59
- at: Date.now(),
60
- },
61
- ],
62
- };
33
+ try {
34
+ const templateName =
35
+ message.template ??
36
+ (typeof message.data?.template === "string" ? message.data.template : undefined);
37
+ const language =
38
+ message.locale ??
39
+ (typeof message.data?.language === "string" ? message.data.language : "en_US");
40
+
41
+ const result = templateName
42
+ ? await transport.send({
43
+ to: message.to,
44
+ template: { name: templateName, language },
45
+ })
46
+ : await transport.send({
47
+ to: message.to,
48
+ text: message.text ?? "",
49
+ });
50
+
51
+ return mapSentlySendResult("wa-cloud", result);
52
+ } catch (err) {
53
+ return mapSentlySendError("wa-cloud", err);
63
54
  }
64
- return {
65
- ok: true,
66
- messageId: id,
67
- driverId: "wa-cloud",
68
- attempts: [{ driverId: "wa-cloud", ok: true, at: Date.now(), messageId: id }],
69
- };
70
55
  },
56
+ verify: () => transport.verify(),
71
57
  };
72
58
 
73
- return { id: "wa-cloud", channel };
59
+ return { id: "wa-cloud", channel, whatsappTransport: transport };
74
60
  }
75
61
 
76
62
  /** WhatsApp Cloud driver factory. */