@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 +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/package.json +5 -3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clonecommand/cloud",
|
|
3
|
-
"version": "0.0.
|
|
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
|
+
}
|