@clonecommand/cloud 0.0.1 → 0.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonecommand/cloud",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "description": "The official SDK for CloneCommand managed services",
6
6
  "main": "dist/index.js",
@@ -14,10 +14,12 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsc",
17
- "dev": "tsc -w"
17
+ "dev": "tsc -w",
18
+ "test:features": "npx ts-node --esm scripts/test-cloud-features.ts",
19
+ "test:manual": "npx tsx scripts/test-manual.ts"
18
20
  },
19
21
  "license": "MIT",
20
22
  "devDependencies": {
21
23
  "typescript": "^5.0.0"
22
24
  }
23
- }
25
+ }