Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/add-comms-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@imessage-sdk/comms': minor
'imessage-cli': minor
---

Add Comms by Osis as an official provider with direct text sending, idempotency, conversation
continuation, provider-specific read APIs, and webhook endpoint management. Bundle the provider
with `imessage-cli`.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ BLOOIO_TEST_IMAGE_URL=
BLOOIO_TEST_VIDEO_URL=
BLOOIO_TEST_FILE_URL=

COMMS_API_KEY=
COMMS_TEST_RECIPIENT=

PHOTON_PROJECT_ID=
PHOTON_PROJECT_SECRET=
PHOTON_PHONE_NUMBER=
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const client = createIMessageClient({

P.S. remember to create your own [Blooio account](https://app.blooio.com/signup?ref=BLOO-2NS4AJM8) and configure the provider with your credentials.

The stable v0.1 providers are Blooio, Photon, and Sendblue. See the
The official providers are Blooio, Comms by Osis, Photon, and Sendblue. See the
[provider feature matrix](./packages/providers/README.md) for their verified surfaces and current
limitations.

Expand Down Expand Up @@ -60,6 +60,7 @@ packages/
├── imessage-sdk/ Provider-neutral core package
├── providers/
│ ├── blooio/ @imessage-sdk/blooio
│ ├── comms/ @imessage-sdk/comms
│ ├── photon/ @imessage-sdk/photon
│ └── sendblue/ @imessage-sdk/sendblue
├── chat-adapter/ @imessage-sdk/chat-adapter
Expand Down
11 changes: 6 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ requests, npm trusted publishing, and package-specific GitHub Releases.
| ----------------------------- | ---------------------------- | ------ |
| `packages/imessage-sdk` | `imessage-sdk` | Public |
| `packages/providers/blooio` | `@imessage-sdk/blooio` | Public |
| `packages/providers/comms` | `@imessage-sdk/comms` | Public |
| `packages/providers/photon` | `@imessage-sdk/photon` | Public |
| `packages/providers/sendblue` | `@imessage-sdk/sendblue` | Public |
| `packages/chat-adapter` | `@imessage-sdk/chat-adapter` | Public |
Expand Down Expand Up @@ -101,8 +102,8 @@ changelogs.
## Trusted publishing on npm

Configure trusted publishing separately in the settings for `imessage-sdk`,
`@imessage-sdk/blooio`, `@imessage-sdk/photon`, `@imessage-sdk/sendblue`, and
`@imessage-sdk/chat-adapter`, and `imessage-cli`:
`@imessage-sdk/blooio`, `@imessage-sdk/comms`, `@imessage-sdk/photon`,
`@imessage-sdk/sendblue`, `@imessage-sdk/chat-adapter`, and `imessage-cli`:

```text
Provider: GitHub Actions
Expand Down Expand Up @@ -173,12 +174,12 @@ npm publish "$PACKAGE_DIR/imessage-cli-0.1.0-beta.0.tgz" \
--provenance=false
```

Add `--tag beta` when bootstrapping a prerelease. For the initial stable Sendblue release, use:
Add `--tag beta` when bootstrapping a prerelease. For an initial stable provider release, use:

```bash
PACKAGE_DIR=$(mktemp -d)
pnpm --filter @imessage-sdk/sendblue pack --pack-destination "$PACKAGE_DIR"
npm publish "$PACKAGE_DIR/imessage-sdk-sendblue-0.1.0.tgz" \
pnpm --filter @imessage-sdk/<provider> pack --pack-destination "$PACKAGE_DIR"
npm publish "$PACKAGE_DIR/imessage-sdk-<provider>-0.1.0.tgz" \
--access public \
--provenance=false
```
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ provider accounts. It sends messages and performs supported provider mutations,
start a webhook server or create a tunnel.

Create an ignored repository-root `.env.cli-test` file with the credentials and dedicated test
recipient/assets for all three providers. It must explicitly opt in:
recipient/assets for all four providers. It must explicitly opt in:

```dotenv
IMESSAGE_CLI_RUN_LIVE=1

BLOOIO_API_KEY=
BLOOIO_FROM_NUMBER=
COMMS_API_KEY=
IMESSAGE_CLI_TEST_RECIPIENT=
IMESSAGE_CLI_TEST_IMAGE_URL=
IMESSAGE_CLI_TEST_VIDEO_URL=
Expand Down Expand Up @@ -78,6 +79,7 @@ Every provider published from `packages/providers/*` is bundled with the CLI:

```text
blooio
comms
photon
sendblue
```
Expand Down Expand Up @@ -147,6 +149,7 @@ imessage-cli send \
Current provider restrictions still apply:

- Blooio requires public attachment URLs.
- Comms currently supports plain-text messages only.
- Photon accepts URLs and local files.
- Sendblue accepts one URL or local file per message.
- Group messaging remains disabled for the current official provider configurations.
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"dependencies": {
"@hono/node-server": "^2.0.10",
"@imessage-sdk/blooio": "workspace:^",
"@imessage-sdk/comms": "workspace:^",
"@imessage-sdk/photon": "workspace:^",
"@imessage-sdk/sendblue": "workspace:^",
"@inquirer/prompts": "^8.5.2",
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/commands/provider-options-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { BaseCommand } from './base-command.js';

export abstract class ProviderOptionsCommand extends BaseCommand {
provider = Option.String('--provider', {
description: 'Use Blooio, Photon, or Sendblue (and its configured default connection).',
description: 'Use Blooio, Comms, Photon, or Sendblue (and its configured default connection).',
});

noInput = Option.Boolean('--no-input', false, {
description: 'Never prompt for missing credentials or settings.',
});

apiKey = Option.String('--api-key', { description: 'One-time Blooio or Sendblue API key.' });
apiKey = Option.String('--api-key', {
description: 'One-time Blooio, Comms, or Sendblue API key.',
});
apiSecret = Option.String('--api-secret', { description: 'One-time Sendblue API secret.' });
projectId = Option.String('--project-id', { description: 'One-time Photon project ID.' });
projectSecret = Option.String('--project-secret', {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const ConnectionConfigSchema = z
const DefaultConnectionsSchema = z
.object({
blooio: ConnectionNameSchema.optional(),
comms: ConnectionNameSchema.optional(),
photon: ConnectionNameSchema.optional(),
sendblue: ConnectionNameSchema.optional(),
})
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/help.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export function rootHelp(): string {
return `Usage: imessage-cli <command> [options]

Send and interact with iMessage through Blooio, Photon, or Sendblue.
Send and interact with iMessage through Blooio, Comms, Photon, or Sendblue.

Options:
-V, -v, --version output the version number
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/provider-names.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const BUILT_IN_PROVIDER_NAMES = ['blooio', 'photon', 'sendblue'] as const;
export const BUILT_IN_PROVIDER_NAMES = ['blooio', 'comms', 'photon', 'sendblue'] as const;

export type BuiltInProviderName = (typeof BUILT_IN_PROVIDER_NAMES)[number];
56 changes: 56 additions & 0 deletions packages/cli/src/providers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { BlooioProvider } from '@imessage-sdk/blooio';
import type { CommsProvider } from '@imessage-sdk/comms';
import type { PhotonProvider } from '@imessage-sdk/photon';
import type { SendblueProvider } from '@imessage-sdk/sendblue';
import type { AnyIMessageProvider, IMessageCapabilities } from 'imessage-sdk';
import { blooio, BLOOIO_CAPABILITIES } from '@imessage-sdk/blooio';
import { comms, COMMS_CAPABILITIES } from '@imessage-sdk/comms';
import { photon, PHOTON_CAPABILITIES } from '@imessage-sdk/photon';
import { sendblue, SENDBLUE_CAPABILITIES } from '@imessage-sdk/sendblue';
import { IMessageSDKError, ValidationError } from 'imessage-sdk';
Expand Down Expand Up @@ -47,6 +49,7 @@ export interface ProviderDoctorResult {

interface BuiltInProviderMap {
readonly blooio: BlooioProvider;
readonly comms: CommsProvider;
readonly photon: PhotonProvider;
readonly sendblue: SendblueProvider<false> | SendblueProvider<true>;
}
Expand Down Expand Up @@ -158,6 +161,16 @@ function createPhoton(values: ProviderValues): PhotonProvider {
});
}

function createComms(values: ProviderValues): CommsProvider {
const apiKey = optionalString('comms', values, 'apiKey');
const baseUrl = optionalString('comms', values, 'baseUrl');

return comms({
...(apiKey === undefined ? {} : { apiKey }),
...(baseUrl === undefined ? {} : { baseUrl }),
});
}

function createSendblue(values: ProviderValues): SendblueProvider<false> | SendblueProvider<true> {
const apiKey = optionalString('sendblue', values, 'apiKey');
const apiSecret = optionalString('sendblue', values, 'apiSecret');
Expand Down Expand Up @@ -272,6 +285,23 @@ async function doctorPhoton(values: ProviderValues): Promise<ProviderDoctorResul
}
}

async function doctorComms(values: ProviderValues): Promise<ProviderDoctorResult> {
try {
const provider = createComms(values);
const messages = await provider.messages.list({
since: new Date(0),
limit: 1,
});
return {
status: 'ok',
message: 'Comms credentials and read access were verified.',
details: { sampledMessageCount: messages.length },
};
} catch (error) {
return doctorError(error, values, ['apiKey']);
}
}

async function doctorSendblue(values: ProviderValues): Promise<ProviderDoctorResult> {
try {
createSendblue(values);
Expand Down Expand Up @@ -350,6 +380,32 @@ export const providerRegistry: ProviderRegistry = {
create: createBlooio,
doctor: doctorBlooio,
},
comms: {
name: 'comms',
displayName: 'Comms by Osis',
packageName: '@imessage-sdk/comms',
description: 'Comms hosted iMessage and SMS API by Osis.',
capabilities: COMMS_CAPABILITIES,
fields: [
{
key: 'apiKey',
label: 'API key',
kind: 'secret',
env: 'COMMS_API_KEY',
requiredFor: ['api', 'doctor'],
description: 'Bearer credential for Comms API operations.',
},
{
key: 'baseUrl',
label: 'API base URL',
kind: 'setting',
requiredFor: [],
description: 'Trusted Comms API endpoint override.',
},
],
create: createComms,
doctor: doctorComms,
},
photon: {
name: 'photon',
displayName: 'Photon Cloud',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export function providerValuesFromOverrides(
switch (provider) {
case 'blooio':
return { ...shared, sender: overrides.fromNumber };
case 'comms':
return shared;
case 'photon':
return {
projectId: overrides.projectId,
Expand Down
18 changes: 16 additions & 2 deletions packages/cli/test/cli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const PROVIDER_CREDENTIAL_ENVIRONMENT_VARIABLES = [
'BLOOIO_API_KEY',
'BLOOIO_FROM_NUMBER',
'BLOOIO_WEBHOOK_SECRET',
'COMMS_API_KEY',
'PHOTON_PROJECT_ID',
'PHOTON_PROJECT_SECRET',
'PHOTON_PHONE_NUMBER',
Expand All @@ -40,6 +41,7 @@ interface SentMessage {
const secretValues = [
process.env['BLOOIO_API_KEY'],
process.env['BLOOIO_WEBHOOK_SECRET'],
process.env['COMMS_API_KEY'],
process.env['PHOTON_PROJECT_SECRET'],
process.env['PHOTON_WEBHOOK_SECRET'],
process.env['SENDBLUE_API_KEY'],
Expand Down Expand Up @@ -130,6 +132,15 @@ describe.skipIf(!enabled)('imessage-cli live provider API', () => {
await testCommonInteractions('photon', text);
}, 180_000);

it('exercises Comms through the built CLI', async () => {
const recipient = required('IMESSAGE_CLI_TEST_RECIPIENT');
const conversation = await openConversation('comms', recipient);
const text = await sendText('comms', conversation, 'Comms text');

expect(text.providerMessageId).toBeTruthy();
expect(text.conversationId).toBeTruthy();
}, 60_000);

it('exercises Sendblue through the built CLI', async () => {
const recipient = required('IMESSAGE_CLI_TEST_RECIPIENT');
const imageUrl = required('IMESSAGE_CLI_TEST_IMAGE_URL');
Expand Down Expand Up @@ -205,7 +216,10 @@ describe.skipIf(!enabled)('imessage-cli live provider API', () => {
}, 180_000);
});

async function openConversation(provider: 'blooio' | 'photon' | 'sendblue', recipient: string) {
async function openConversation(
provider: 'blooio' | 'comms' | 'photon' | 'sendblue',
recipient: string,
) {
const result = await command([
'conversation',
'open',
Expand All @@ -220,7 +234,7 @@ async function openConversation(provider: 'blooio' | 'photon' | 'sendblue', reci
}

async function sendText(
provider: 'blooio' | 'photon' | 'sendblue',
provider: 'blooio' | 'comms' | 'photon' | 'sendblue',
conversation: string,
label: string,
): Promise<SentMessage> {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe('imessage-cli', () => {
schemaVersion: 1,
ok: true,
command: 'provider.list',
data: [{ name: 'blooio' }, { name: 'photon' }, { name: 'sendblue' }],
data: [{ name: 'blooio' }, { name: 'comms' }, { name: 'photon' }, { name: 'sendblue' }],
});
expect(test.stderr.text()).toBe('');
});
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/test/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('built-in provider registry', () => {
};

expect(requiredWebhookFields('blooio')).toEqual(['webhookSecret']);
expect(requiredWebhookFields('comms')).toEqual([]);
expect(requiredWebhookFields('photon')).toEqual(['webhookSecret']);
expect(requiredWebhookFields('sendblue')).toEqual(['fromNumber', 'webhookSecret']);
});
Expand All @@ -73,6 +74,9 @@ describe('built-in provider registry', () => {
projectSecret: 'project-secret',
phone: '+15550000002',
});
const comms = createProvider('comms', {
apiKey: 'comms-key',
});
const sendblue = createProvider('sendblue', {
apiKey: 'sendblue-key',
apiSecret: 'sendblue-secret',
Expand All @@ -81,6 +85,7 @@ describe('built-in provider registry', () => {
});

expect(blooio.name).toBe('blooio');
expect(comms.name).toBe('comms');
expect(photon.name).toBe('photon');
expect(sendblue.name).toBe('sendblue');
expect(sendblue.capabilities.conversations.markRead).toBe(true);
Expand Down Expand Up @@ -160,6 +165,36 @@ describe('built-in provider registry', () => {
expect(fetchMock).not.toHaveBeenCalled();
});

it('verifies Comms credentials through a non-mutating message lookup', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
void input;
void init;
return new Response(JSON.stringify({ messages: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
});
vi.stubGlobal('fetch', fetchMock);

const result = await providerRegistry.comms.doctor({
apiKey: 'comms-key',
baseUrl: 'https://comms.test/api/v1/comms',
});

expect(result).toMatchObject({
status: 'ok',
details: { sampledMessageCount: 0 },
});
expect(fetchMock).toHaveBeenCalledWith(
'https://comms.test/api/v1/comms/messages?since=1970-01-01T00%3A00%3A00.000Z&limit=1',
expect.objectContaining({
headers: expect.any(Headers),
}),
);
const headers = new Headers(fetchMock.mock.calls[0]?.[1]?.headers);
expect(headers.get('authorization')).toBe('Bearer comms-key');
});

it('returns safe doctor failures without exposing secret values', async () => {
const secret = 'blooio-secret-value';
vi.stubGlobal(
Expand Down
Loading
Loading