@clonecommand/cloud 0.0.1 β†’ 0.0.3

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/README.md CHANGED
@@ -1,65 +1,32 @@
1
1
  # @clonecommand/cloud
2
2
 
3
- The official SDK for CloneCommand managed services. This library simplifies integration with ephemeral preview environments and provides access to CloneCommand Cloud services.
3
+ The official SDK for CloneCommand managed services.
4
4
 
5
- ## Installation
6
-
7
- ```bash
8
- npm install @clonecommand/cloud
9
- ```
10
-
11
- ## πŸ” Login Service: OAuth Proxy
5
+ This library simplifies integration with ephemeral preview environments and provides access to CloneCommand Cloud services like Login Proxy, Storage, and Email.
12
6
 
13
- OAuth providers (LinkedIn, Google, GitHub, etc.) require a strict whitelist of redirect URLs. This makes testing on dynamic, branch-specific preview environments (`*.clonecommand.app`) difficult.
7
+ ## Documentation
14
8
 
15
- The **CloneCommand Login Service** acts as a trusted intermediary, providing a single, stable redirect URL for all your environments.
9
+ Full documentation is available in the [`docs/`](./docs/README.md) directory.
16
10
 
17
- ### 1. Initialize the SDK
11
+ - [**πŸ” Login**](./docs/login.md) - OAuth Proxy for consistent redirect URIs.
12
+ - [**πŸ“¦ Storage**](./docs/storage.md) - Managed file hosting.
13
+ - [**πŸ“§ Email**](./docs/email.md) - Send and receive emails.
18
14
 
19
- For client-side applications (e.g., Vite, Nuxt), initialize the SDK with your Project ID.
20
-
21
- ```typescript
22
- import { ccc } from '@clonecommand/cloud';
15
+ ## Installation
23
16
 
24
- ccc.init({
25
- projectId: 'your-project-id' // Found in your CloneCommand Dashboard
26
- });
17
+ ```bash
18
+ npm install @clonecommand/cloud
27
19
  ```
28
20
 
29
- > [!NOTE]
30
- > In managed CloneCommand deployments, your Project ID and Service Tokens are automatically detectedβ€”no manual initialization is required for server-side operations.
31
-
32
- ### 2. Wrap your Login URL
33
-
34
- Instead of redirecting directly to the OAuth provider, wrap your generated URL with `ccc.login.proxy()`.
21
+ ## Quick Start
35
22
 
36
23
  ```typescript
37
- // Before
38
- const loginUrl = 'https://www.linkedin.com/oauth/v2/authorization?...';
39
- window.location.href = loginUrl;
40
-
41
- // After (Works on localhost, preview branches, and production!)
42
24
  import { ccc } from '@clonecommand/cloud';
43
25
 
44
- const loginUrl = 'https://www.linkedin.com/oauth/v2/authorization?...';
45
- window.location.href = ccc.login.proxy(loginUrl);
46
- ```
47
-
48
- ### ⚠️ The "Redirect URI Paradox" (Important Quirk)
49
- When using the proxy, the OAuth provider sees the **Proxy Callback URL** as your application's redirect URI.
50
-
51
- 1. **Provider Config**: In your OAuth provider settings (e.g., LinkedIn Developer Portal), you **only** need to whitelist this single URL:
52
- `https://graph.clonecommand.com/login/proxy/callback`
53
-
54
- 2. **Backend Token Exchange**: When your application receives the `code` and you call the provider's API to swap it for an `access_token`, you **must** use that same proxy callback URL as the `redirect_uri` parameter:
55
- ```typescript
56
- // Even if your app is on localhost:3000, you MUST use the proxy callback here
57
- const redirect_uri = 'https://graph.clonecommand.com/login/proxy/callback';
58
- ```
59
- *CloneCommand handles the final hop back to your specific environment automatically.*
26
+ // Initialize (optional in managed environments)
27
+ ccc.init({ projectId: '...' });
60
28
 
61
- ### πŸ€– Why this is great for Agents
62
- Using `ccc.login.proxy()` ensures that any new feature you build will have working authentication immediately upon deployment to a preview environment, without requiring human intervention to update provider settings.
63
-
64
- ---
65
- Built with ❀️ by [CloneCommand](https://clonecommand.com)
29
+ // Use services
30
+ const file = await ccc.storage.importFileFromUrl('...');
31
+ const messageId = await ccc.email.send({ to: '...', subject: '...' });
32
+ ```
@@ -0,0 +1,69 @@
1
+ import { CloneCommandCloud } from '../index.js';
2
+ export interface EmailConfig {
3
+ id: string;
4
+ projectId: string;
5
+ domainId: string;
6
+ isEnabled: boolean;
7
+ sesIdentityArn?: string;
8
+ sesVerificationStatus?: string;
9
+ dkimTokens?: string;
10
+ }
11
+ export interface InboundEmail {
12
+ id: string;
13
+ projectId: string;
14
+ sender: string;
15
+ recipient: string;
16
+ subject: string;
17
+ snippet: string;
18
+ bucketPath: string;
19
+ receivedAt: string;
20
+ isRead: boolean;
21
+ hasAttachments: boolean;
22
+ rawSize: number;
23
+ threadId?: string;
24
+ messageId?: string;
25
+ inReplyTo?: string;
26
+ references?: string;
27
+ }
28
+ export interface SendEmailOptions {
29
+ from?: string;
30
+ to: string | string[];
31
+ subject: string;
32
+ html?: string;
33
+ text?: string;
34
+ }
35
+ export declare class EmailClient {
36
+ private ccc;
37
+ constructor(ccc: CloneCommandCloud);
38
+ /**
39
+ * Get email configuration for the project.
40
+ */
41
+ getConfig(projectId?: string): Promise<EmailConfig | null>;
42
+ /**
43
+ * Enable email service for a domain.
44
+ */
45
+ enable(domainId: string, projectId?: string): Promise<EmailConfig>;
46
+ /**
47
+ * Send an email.
48
+ */
49
+ send(options: SendEmailOptions, projectId?: string): Promise<string>;
50
+ /**
51
+ * Get inbound emails.
52
+ */
53
+ getInbound(options?: {
54
+ limit?: number;
55
+ before?: Date;
56
+ }, projectId?: string): Promise<InboundEmail[]>;
57
+ /**
58
+ * Get a specific email by ID.
59
+ */
60
+ get(emailId: string): Promise<InboundEmail | null>;
61
+ /**
62
+ * Mark an email as read.
63
+ */
64
+ markAsRead(emailId: string): Promise<boolean>;
65
+ /**
66
+ * Delete an email.
67
+ */
68
+ delete(emailId: string): Promise<boolean>;
69
+ }
@@ -0,0 +1,213 @@
1
+ export class EmailClient {
2
+ ccc;
3
+ constructor(ccc) {
4
+ this.ccc = ccc;
5
+ }
6
+ /**
7
+ * Get email configuration for the project.
8
+ */
9
+ async getConfig(projectId) {
10
+ const pid = projectId || this.ccc.projectId;
11
+ if (!pid)
12
+ throw new Error("projectId is required");
13
+ const query = `
14
+ query EmailConfig($projectId: ID!) {
15
+ emailConfig(projectId: $projectId) {
16
+ id
17
+ projectId
18
+ domainId
19
+ isEnabled
20
+ sesVerificationStatus
21
+ dkimTokens
22
+ }
23
+ }
24
+ `;
25
+ const result = await this.ccc.request('/graphql', {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({
29
+ query,
30
+ variables: { projectId: pid }
31
+ })
32
+ });
33
+ if (result.errors)
34
+ throw new Error(result.errors[0].message);
35
+ return result.data.emailConfig;
36
+ }
37
+ /**
38
+ * Enable email service for a domain.
39
+ */
40
+ async enable(domainId, projectId) {
41
+ const pid = projectId || this.ccc.projectId;
42
+ if (!pid)
43
+ throw new Error("projectId is required");
44
+ const query = `
45
+ mutation EnableEmail($projectId: ID!, $domainId: ID!) {
46
+ enableEmail(projectId: $projectId, domainId: $domainId) {
47
+ id
48
+ projectId
49
+ domainId
50
+ isEnabled
51
+ sesVerificationStatus
52
+ dkimTokens
53
+ }
54
+ }
55
+ `;
56
+ const result = await this.ccc.request('/graphql', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({
60
+ query,
61
+ variables: { projectId: pid, domainId }
62
+ })
63
+ });
64
+ if (result.errors)
65
+ throw new Error(result.errors[0].message);
66
+ return result.data.enableEmail;
67
+ }
68
+ /**
69
+ * Send an email.
70
+ */
71
+ async send(options, projectId) {
72
+ const pid = projectId || this.ccc.projectId;
73
+ if (!pid)
74
+ throw new Error("projectId is required");
75
+ const query = `
76
+ mutation SendEmail($projectId: ID!, $email: SendEmailInput!) {
77
+ sendEmail(projectId: $projectId, email: $email)
78
+ }
79
+ `;
80
+ const result = await this.ccc.request('/graphql', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({
84
+ query,
85
+ variables: {
86
+ projectId: pid,
87
+ email: {
88
+ from: options.from,
89
+ to: Array.isArray(options.to) ? options.to : [options.to],
90
+ subject: options.subject,
91
+ html: options.html,
92
+ text: options.text
93
+ }
94
+ }
95
+ })
96
+ });
97
+ if (result.errors)
98
+ throw new Error(result.errors[0].message);
99
+ return result.data.sendEmail;
100
+ }
101
+ /**
102
+ * Get inbound emails.
103
+ */
104
+ async getInbound(options = {}, projectId) {
105
+ const pid = projectId || this.ccc.projectId;
106
+ if (!pid)
107
+ throw new Error("projectId is required");
108
+ const query = `
109
+ query InboundEmails($projectId: ID!, $limit: Int, $before: DateTime) {
110
+ inboundEmails(projectId: $projectId, limit: $limit, before: $before) {
111
+ id
112
+ projectId
113
+ sender
114
+ recipient
115
+ subject
116
+ snippet
117
+ receivedAt
118
+ isRead
119
+ hasAttachments
120
+ }
121
+ }
122
+ `;
123
+ const result = await this.ccc.request('/graphql', {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({
127
+ query,
128
+ variables: {
129
+ projectId: pid,
130
+ limit: options.limit,
131
+ before: options.before
132
+ }
133
+ })
134
+ });
135
+ if (result.errors)
136
+ throw new Error(result.errors[0].message);
137
+ return result.data.inboundEmails;
138
+ }
139
+ /**
140
+ * Get a specific email by ID.
141
+ */
142
+ async get(emailId) {
143
+ const query = `
144
+ query Email($id: ID!) {
145
+ email(id: $id) {
146
+ id
147
+ projectId
148
+ sender
149
+ recipient
150
+ subject
151
+ snippet
152
+ body
153
+ receivedAt
154
+ isRead
155
+ hasAttachments
156
+ }
157
+ }
158
+ `;
159
+ const result = await this.ccc.request('/graphql', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify({
163
+ query,
164
+ variables: { id: emailId }
165
+ })
166
+ });
167
+ if (result.errors)
168
+ throw new Error(result.errors[0].message);
169
+ return result.data.email;
170
+ }
171
+ /**
172
+ * Mark an email as read.
173
+ */
174
+ async markAsRead(emailId) {
175
+ const query = `
176
+ mutation MarkEmailAsRead($emailId: ID!) {
177
+ markEmailAsRead(emailId: $emailId)
178
+ }
179
+ `;
180
+ const result = await this.ccc.request('/graphql', {
181
+ method: 'POST',
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify({
184
+ query,
185
+ variables: { emailId }
186
+ })
187
+ });
188
+ if (result.errors)
189
+ throw new Error(result.errors[0].message);
190
+ return result.data.markEmailAsRead;
191
+ }
192
+ /**
193
+ * Delete an email.
194
+ */
195
+ async delete(emailId) {
196
+ const query = `
197
+ mutation DeleteEmail($emailId: ID!) {
198
+ deleteEmail(emailId: $emailId)
199
+ }
200
+ `;
201
+ const result = await this.ccc.request('/graphql', {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json' },
204
+ body: JSON.stringify({
205
+ query,
206
+ variables: { emailId }
207
+ })
208
+ });
209
+ if (result.errors)
210
+ throw new Error(result.errors[0].message);
211
+ return result.data.deleteEmail;
212
+ }
213
+ }
package/dist/index.d.ts CHANGED
@@ -2,17 +2,29 @@ export interface CCCConfig {
2
2
  projectId?: string;
3
3
  baseUrl?: string;
4
4
  }
5
+ export interface StorageFile {
6
+ id: string;
7
+ projectId: string;
8
+ fileId: string;
9
+ originalFilename?: string;
10
+ contentType: string;
11
+ sizeBytes?: number;
12
+ extension?: string;
13
+ publicUrl: string;
14
+ createdAt: string;
15
+ }
16
+ import { EmailClient } from './email/index.js';
5
17
  export declare class CloneCommandCloud {
6
18
  private config;
7
19
  init(config: CCCConfig): void;
8
- private get projectId();
9
- private get baseUrl();
10
- private get serviceToken();
20
+ get projectId(): string | undefined;
21
+ get baseUrl(): string;
22
+ get serviceToken(): string | undefined;
11
23
  /**
12
24
  * Internal helper to make authenticated requests to the CloneCommand API.
13
25
  * This is used by server-side methods (e.g., image uploads).
14
26
  */
15
- private request;
27
+ request(path: string, options?: RequestInit): Promise<any>;
16
28
  get login(): {
17
29
  /**
18
30
  * Wraps a standard OAuth authorization URL with the CloneCommand Proxy.
@@ -24,5 +36,44 @@ export declare class CloneCommandCloud {
24
36
  */
25
37
  proxy: (originalUrl: string) => string;
26
38
  };
39
+ get storage(): {
40
+ /**
41
+ * Import a file from a URL and upload it to CloneCommand Storage.
42
+ *
43
+ * @param url The URL of the file to import
44
+ * @param contentType Optional content type (auto-detected if not provided)
45
+ * @returns StorageFile object with publicUrl
46
+ *
47
+ * @example
48
+ * const file = await ccc.storage.importFileFromUrl('https://example.com/image.png');
49
+ * console.log(file.publicUrl); // https://media.clonecommand.com/:projectId/:fileId/original.png
50
+ */
51
+ importFileFromUrl: (url: string, contentType?: string) => Promise<StorageFile>;
52
+ /**
53
+ * Import a file from a Buffer and upload it to CloneCommand Storage.
54
+ *
55
+ * @param buffer The file buffer
56
+ * @param contentType The content type of the file
57
+ * @returns StorageFile object with publicUrl
58
+ *
59
+ * @example
60
+ * const buffer = fs.readFileSync('image.png');
61
+ * const file = await ccc.storage.importFileFromBuffer(buffer, 'image/png');
62
+ */
63
+ importFileFromBuffer: (buffer: Buffer, contentType: string, filename?: string) => Promise<StorageFile>;
64
+ /**
65
+ * Get the public URL for a file.
66
+ *
67
+ * @param fileId The file ID
68
+ * @param projectSlug Optional project slug (uses projectId from config if not provided)
69
+ * @returns The public URL
70
+ *
71
+ * @example
72
+ * const url = ccc.storage.getFileUrl('abc-123-def', 'my-project');
73
+ * // => https://my-project.clonecommand.media/abc-123-def/original
74
+ */
75
+ getFileUrl: (fileId: string, projectSlug?: string) => string;
76
+ };
77
+ get email(): EmailClient;
27
78
  }
28
79
  export declare const ccc: CloneCommandCloud;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { EmailClient } from './email/index.js';
1
2
  export class CloneCommandCloud {
2
3
  config = {};
3
4
  init(config) {
@@ -7,13 +8,33 @@ export class CloneCommandCloud {
7
8
  };
8
9
  }
9
10
  get projectId() {
10
- return this.config.projectId;
11
+ if (this.config.projectId)
12
+ return this.config.projectId;
13
+ if (typeof process !== 'undefined' && process.env.CC_PROJECT_ID)
14
+ return process.env.CC_PROJECT_ID;
15
+ // Fallback: Extract from Token (JWT)
16
+ const token = this.serviceToken;
17
+ if (token) {
18
+ try {
19
+ const parts = token.split('.');
20
+ if (parts.length === 3) {
21
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
22
+ if (payload.projectId) {
23
+ return payload.projectId;
24
+ }
25
+ }
26
+ }
27
+ catch (e) {
28
+ // Ignore parse errors
29
+ }
30
+ }
31
+ return undefined;
11
32
  }
12
33
  get baseUrl() {
13
34
  return this.config.baseUrl || 'https://graph.clonecommand.com';
14
35
  }
15
36
  get serviceToken() {
16
- return typeof process !== 'undefined' ? process.env.CC_TOKEN : undefined;
37
+ return typeof process !== 'undefined' ? process.env.CC_PROJECT_TOKEN : undefined;
17
38
  }
18
39
  /**
19
40
  * Internal helper to make authenticated requests to the CloneCommand API.
@@ -49,12 +70,146 @@ export class CloneCommandCloud {
49
70
  const pid = this.projectId;
50
71
  if (!pid) {
51
72
  throw new Error("CloneCommand: projectId is required. Provide it in ccc.init({ projectId }) " +
52
- "or set the CLONECOMMAND_PROJECT_ID environment variable.");
73
+ "or set the CC_PROJECT_ID environment variable.");
53
74
  }
54
75
  const encodedUrl = encodeURIComponent(originalUrl);
55
76
  return `${this.baseUrl}/login/proxy/start?projectId=${pid}&url=${encodedUrl}`;
56
77
  }
57
78
  };
58
79
  }
80
+ get storage() {
81
+ return {
82
+ /**
83
+ * Import a file from a URL and upload it to CloneCommand Storage.
84
+ *
85
+ * @param url The URL of the file to import
86
+ * @param contentType Optional content type (auto-detected if not provided)
87
+ * @returns StorageFile object with publicUrl
88
+ *
89
+ * @example
90
+ * const file = await ccc.storage.importFileFromUrl('https://example.com/image.png');
91
+ * console.log(file.publicUrl); // https://media.clonecommand.com/:projectId/:fileId/original.png
92
+ */
93
+ importFileFromUrl: async (url, contentType) => {
94
+ const token = this.serviceToken;
95
+ const pid = this.projectId;
96
+ if (!token) {
97
+ throw new Error("CloneCommand Storage: CC_PROJECT_TOKEN is required. " +
98
+ "Ensure your service is deployed via CloneCommand or use 'clones run' for local development.");
99
+ }
100
+ if (!pid) {
101
+ throw new Error("CloneCommand Storage: projectId is required. " +
102
+ "Set CC_PROJECT_ID environment variable or call ccc.init({ projectId }).");
103
+ }
104
+ const query = `
105
+ mutation UploadFileFromUrl($projectId: String!, $url: String!, $contentType: String) {
106
+ uploadFileFromUrl(projectId: $projectId, url: $url, contentType: $contentType) {
107
+ id
108
+ projectId
109
+ fileId
110
+ originalFilename
111
+ contentType
112
+ sizeBytes
113
+ extension
114
+ publicUrl
115
+ createdAt
116
+ }
117
+ }
118
+ `;
119
+ const result = await this.request('/graphql', {
120
+ method: 'POST',
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ },
124
+ body: JSON.stringify({
125
+ query,
126
+ variables: { projectId: pid, url, contentType },
127
+ }),
128
+ });
129
+ if (result.errors) {
130
+ throw new Error(`Upload failed: ${result.errors[0].message}`);
131
+ }
132
+ return result.data.uploadFileFromUrl;
133
+ },
134
+ /**
135
+ * Import a file from a Buffer and upload it to CloneCommand Storage.
136
+ *
137
+ * @param buffer The file buffer
138
+ * @param contentType The content type of the file
139
+ * @returns StorageFile object with publicUrl
140
+ *
141
+ * @example
142
+ * const buffer = fs.readFileSync('image.png');
143
+ * const file = await ccc.storage.importFileFromBuffer(buffer, 'image/png');
144
+ */
145
+ importFileFromBuffer: async (buffer, contentType, filename = 'file') => {
146
+ const token = this.serviceToken;
147
+ const pid = this.projectId;
148
+ if (!token) {
149
+ throw new Error("CloneCommand Storage: CC_PROJECT_TOKEN is required.");
150
+ }
151
+ if (!pid) {
152
+ throw new Error("CloneCommand Storage: projectId is required.");
153
+ }
154
+ const query = `
155
+ mutation UploadFileFromBuffer($projectId: String!, $buffer: String!, $contentType: String!, $filename: String!) {
156
+ uploadFileFromBuffer(projectId: $projectId, buffer: $buffer, contentType: $contentType, filename: $filename) {
157
+ id
158
+ projectId
159
+ fileId
160
+ originalFilename
161
+ contentType
162
+ sizeBytes
163
+ extension
164
+ publicUrl
165
+ createdAt
166
+ }
167
+ }
168
+ `;
169
+ const result = await this.request('/graphql', {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({
173
+ query,
174
+ variables: {
175
+ projectId: pid,
176
+ buffer: buffer.toString('base64'),
177
+ contentType,
178
+ filename
179
+ },
180
+ }),
181
+ });
182
+ if (result.errors) {
183
+ throw new Error(`Upload failed: ${result.errors[0].message}`);
184
+ }
185
+ return result.data.uploadFileFromBuffer;
186
+ },
187
+ /**
188
+ * Get the public URL for a file.
189
+ *
190
+ * @param fileId The file ID
191
+ * @param projectSlug Optional project slug (uses projectId from config if not provided)
192
+ * @returns The public URL
193
+ *
194
+ * @example
195
+ * const url = ccc.storage.getFileUrl('abc-123-def', 'my-project');
196
+ * // => https://my-project.clonecommand.media/abc-123-def/original
197
+ */
198
+ getFileUrl: (fileId, projectSlug) => {
199
+ const pid = this.projectId;
200
+ const slug = projectSlug || pid;
201
+ if (!slug) {
202
+ throw new Error("CloneCommand Storage: projectId or projectSlug is required. " +
203
+ "Set CC_PROJECT_ID environment variable or call ccc.init({ projectId }).");
204
+ }
205
+ // Note: This returns the default projectslug.clonecommand.media URL
206
+ // Custom domains are handled server-side
207
+ return `https://${slug}.clonecommand.media/${fileId}/original`;
208
+ },
209
+ };
210
+ }
211
+ get email() {
212
+ return new EmailClient(this);
213
+ }
59
214
  }
60
215
  export const ccc = new CloneCommandCloud();
package/docs/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # CloneCommand Cloud SDK Documentation
2
+
3
+ Welcome to the `@clonecommand/cloud` SDK documentation.
4
+
5
+ ## Modules
6
+
7
+ - [**πŸ” Login**](./login.md): OAuth Proxy service for preview environments.
8
+ - [**πŸ“¦ Storage**](./storage.md): Managed file hosting with CDN and custom domains.
9
+ - [**πŸ“§ Email**](./email.md): Managed email sending and receiving with inbound processing.
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { ccc } from '@clonecommand/cloud';
15
+
16
+ // 1. Initialize (Optional in managed environments)
17
+ ccc.init({ projectId: 'your-project-id' });
18
+
19
+ // 2. Use services
20
+ const loginUrl = ccc.login.proxy('https://google.com/oauth...');
21
+ const file = await ccc.storage.importFileFromUrl('...');
22
+ const config = await ccc.email.getConfig();
23
+ ```
package/docs/email.md ADDED
@@ -0,0 +1,65 @@
1
+ # πŸ“§ CloneCommand Email Service
2
+
3
+ Managed email sending and receiving with inbound processing and domain verification.
4
+
5
+ ## Features
6
+
7
+ - **Send Emails**: Send transactional emails via Amazon SES (managed backend).
8
+ - **Inbound Processing**: Receive emails, store them in GCS, and access them via API.
9
+ - **Domain Verification**: Automates DNS verification for custom domains.
10
+
11
+ ## Usage
12
+
13
+ ### Getting Configuration
14
+
15
+ Check if email is enabled for the current project.
16
+
17
+ ```typescript
18
+ import { ccc } from '@clonecommand/cloud';
19
+
20
+ const config = await ccc.email.getConfig();
21
+
22
+ if (config?.isEnabled) {
23
+ console.log('Email is active!');
24
+ }
25
+ ```
26
+
27
+ ### Sending Emails
28
+
29
+ Send simple text or HTML emails.
30
+
31
+ ```typescript
32
+ const messageId = await ccc.email.send({
33
+ to: 'user@example.com',
34
+ subject: 'Welcome!',
35
+ html: '<h1>Welcome to our platform</h1>',
36
+ text: 'Welcome to our platform'
37
+ });
38
+ ```
39
+
40
+ ### Retrieving Inbound Emails
41
+
42
+ Fetch the latest emails received by the project's inbound address.
43
+
44
+ ```typescript
45
+ const emails = await ccc.email.getInbound({ limit: 5 });
46
+
47
+ for (const email of emails) {
48
+ console.log(`Received from: ${email.sender}`);
49
+ console.log(`Subject: ${email.subject}`);
50
+
51
+ // Fetch full details including body
52
+ const fullEmail = await ccc.email.get(email.id);
53
+ console.log(fullEmail.body);
54
+ }
55
+ ```
56
+
57
+ ## Agent Capabilities
58
+
59
+ The email service is designed to be easily used by AI agents to build automated workflows:
60
+ - **Auto-Reply Bots**: Poll `getInbound()` and `send()` replies.
61
+ - **Verification flows**: Trigger emails and read the verification codes from the inbox.
62
+
63
+ ## Authentication
64
+
65
+ Requires the `CC_PROJECT_TOKEN` environment variable, automatically injected in CloneCommand deployments.
package/docs/login.md ADDED
@@ -0,0 +1,54 @@
1
+ # πŸ” CloneCommand Login Service (OAuth Proxy)
2
+
3
+ The **CloneCommand Login Service** acts as a trusted intermediary for OAuth flows, solving the "Redirect URI Paradox" in preview environments.
4
+
5
+ ## Why is this needed?
6
+
7
+ OAuth providers (Google, GitHub, LinkedIn, etc.) require whitelisting specific Redirect URIs. This is problematic for:
8
+ 1. **Preview Environments**: Each pull request creates a unique URL (e.g., `pr-123.clonecommand.app`). You can't whitelist infinite dynamic URLs.
9
+ 2. **Local Development**: Managing `localhost` vs production URLs often leads to config drift.
10
+
11
+ ## How it works
12
+
13
+ Instead of redirecting users directly to the OAuth provider, you redirect them to the **CloneCommand Proxy**. The proxy handles the redirection to the provider using a single, stable callback URL (`https://graph.clonecommand.com/login/proxy/callback`), and then redirects the user back to your specific environment.
14
+
15
+ ## Usage
16
+
17
+ ### 1. Wrap your Authorization URL
18
+
19
+ ```typescript
20
+ import { ccc } from '@clonecommand/cloud';
21
+
22
+ const originalUrl = 'https://github.com/login/oauth/authorize?client_id=...';
23
+ const proxyUrl = ccc.login.proxy(originalUrl);
24
+
25
+ window.location.href = proxyUrl;
26
+ ```
27
+
28
+ ### 2. Configure your OAuth Provider
29
+
30
+ Whitelist **only** this URL in your provider's developer console:
31
+
32
+ `https://graph.clonecommand.com/login/proxy/callback`
33
+
34
+ ### 3. Handle the Callback
35
+
36
+ When exchanging the `code` for an access token on your backend, you **must** use the proxy callback URL as the `redirect_uri`, regardless of where your app is actually running.
37
+
38
+ ```typescript
39
+ // Backend code to exchange code for token
40
+ const response = await fetch('https://github.com/login/oauth/access_token', {
41
+ method: 'POST',
42
+ body: JSON.stringify({
43
+ client_id: '...',
44
+ client_secret: '...',
45
+ code: req.query.code,
46
+ redirect_uri: 'https://graph.clonecommand.com/login/proxy/callback' // <--- CRITICAL
47
+ })
48
+ });
49
+ ```
50
+
51
+ ## Agent Notes
52
+
53
+ - **Stability**: This feature ensures that authentication flows work immediately in any new environment an agent creates.
54
+ - **Zero Config**: No need to ask the user to update their OAuth provider settings when deploying new branches.
@@ -0,0 +1,40 @@
1
+ # πŸ“¦ CloneCommand Storage Service
2
+
3
+ Managed file storage with global CDN, automatic SSL, and custom domain support.
4
+
5
+ ## Usage
6
+
7
+ ### Uploading Files
8
+
9
+ The `importFileFromUrl` method allows you to upload assets from external sources (e.g., social media avatars, public APIs) directly to your project's storage bucket.
10
+
11
+ ```typescript
12
+ import { ccc } from '@clonecommand/cloud';
13
+
14
+ const file = await ccc.storage.importFileFromUrl('https://example.com/image.png');
15
+
16
+ console.log(file);
17
+ // {
18
+ // id: "...",
19
+ // fileId: "xyz-123",
20
+ // publicUrl: "https://my-project.clonecommand.media/xyz-123/original.png",
21
+ // ...
22
+ // }
23
+ ```
24
+
25
+ ### Retrieving File URLs
26
+
27
+ Generate the public CDN URL for a file using its `fileId`.
28
+
29
+ ```typescript
30
+ const url = ccc.storage.getFileUrl('xyz-123');
31
+ // -> https://my-project.clonecommand.media/xyz-123/original
32
+ ```
33
+
34
+ ## Custom Domains
35
+
36
+ If your project has a custom media domain configured (e.g., `cdn.myapp.com`), `getFileUrl` and `importFileFromUrl` will automatically return URLs using that domain.
37
+
38
+ ## Authentication
39
+
40
+ Storage operations require the `CC_PROJECT_TOKEN` environment variable, which is automatically injected in CloneCommand deployments. For local development, use `clones run`.
package/package.json CHANGED
@@ -1,23 +1,26 @@
1
1
  {
2
2
  "name": "@clonecommand/cloud",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "The official SDK for CloneCommand managed services",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "README.md"
10
+ "README.md",
11
+ "docs"
11
12
  ],
12
13
  "publishConfig": {
13
14
  "access": "public"
14
15
  },
15
16
  "scripts": {
16
17
  "build": "tsc",
17
- "dev": "tsc -w"
18
+ "dev": "tsc -w",
19
+ "test:features": "npx ts-node --esm scripts/test-cloud-features.ts",
20
+ "test:manual": "npx tsx scripts/test-manual.ts"
18
21
  },
19
22
  "license": "MIT",
20
23
  "devDependencies": {
21
24
  "typescript": "^5.0.0"
22
25
  }
23
- }
26
+ }