@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 +17 -50
- package/dist/email/index.d.ts +69 -0
- package/dist/email/index.js +213 -0
- package/dist/index.d.ts +55 -4
- package/dist/index.js +158 -3
- package/docs/README.md +23 -0
- package/docs/email.md +65 -0
- package/docs/login.md +54 -0
- package/docs/storage.md +40 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,65 +1,32 @@
|
|
|
1
1
|
# @clonecommand/cloud
|
|
2
2
|
|
|
3
|
-
The official SDK for CloneCommand managed services.
|
|
3
|
+
The official SDK for CloneCommand managed services.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
## Documentation
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
Full documentation is available in the [`docs/`](./docs/README.md) directory.
|
|
16
10
|
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import { ccc } from '@clonecommand/cloud';
|
|
15
|
+
## Installation
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
17
|
+
```bash
|
|
18
|
+
npm install @clonecommand/cloud
|
|
27
19
|
```
|
|
28
20
|
|
|
29
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
package/docs/storage.md
ADDED
|
@@ -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.
|
|
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
|
+
}
|