@swarm-agent/sdk 0.1.47
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/APPLICATIONS.md +133 -0
- package/HEADLESS_UI.md +97 -0
- package/LICENSE +201 -0
- package/README.md +473 -0
- package/dist/app-results.d.ts +24 -0
- package/dist/app-results.js +167 -0
- package/dist/apps.d.ts +82 -0
- package/dist/apps.js +89 -0
- package/dist/auth.d.ts +45 -0
- package/dist/auth.js +89 -0
- package/dist/automations.d.ts +74 -0
- package/dist/automations.js +159 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +103 -0
- package/dist/deliverables.d.ts +42 -0
- package/dist/deliverables.js +83 -0
- package/dist/deploy/dockerfile.d.ts +14 -0
- package/dist/deploy/dockerfile.js +14 -0
- package/dist/deploy/index.d.ts +67 -0
- package/dist/deploy/index.js +226 -0
- package/dist/deploy/manifest.d.ts +17 -0
- package/dist/deploy/manifest.js +220 -0
- package/dist/deploy/providers/gcp-cloud-run.d.ts +9 -0
- package/dist/deploy/providers/gcp-cloud-run.js +12 -0
- package/dist/deploy/providers/gcp-compute.d.ts +9 -0
- package/dist/deploy/providers/gcp-compute.js +12 -0
- package/dist/deploy/types.d.ts +211 -0
- package/dist/deploy/types.js +4 -0
- package/dist/deploy/worker-cloud-deployer.d.ts +33 -0
- package/dist/deploy/worker-cloud-deployer.js +14 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +20 -0
- package/dist/notifications.d.ts +28 -0
- package/dist/notifications.js +65 -0
- package/dist/projects.d.ts +147 -0
- package/dist/projects.js +513 -0
- package/dist/provider-auth.d.ts +160 -0
- package/dist/provider-auth.js +54 -0
- package/dist/realtime.d.ts +240 -0
- package/dist/realtime.js +312 -0
- package/dist/sessions.d.ts +60 -0
- package/dist/sessions.js +194 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +26 -0
- package/dist/storage/adapters/memory.d.ts +13 -0
- package/dist/storage/adapters/memory.js +35 -0
- package/dist/storage/adapters/s3.d.ts +31 -0
- package/dist/storage/adapters/s3.js +171 -0
- package/dist/storage/client.d.ts +174 -0
- package/dist/storage/client.js +121 -0
- package/dist/storage/hub.d.ts +30 -0
- package/dist/storage/hub.js +372 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/runner.d.ts +48 -0
- package/dist/storage/runner.js +233 -0
- package/dist/storage/types.d.ts +198 -0
- package/dist/storage/types.js +1 -0
- package/dist/system.d.ts +14 -0
- package/dist/system.js +52 -0
- package/dist/transport.d.ts +17 -0
- package/dist/transport.js +243 -0
- package/dist/types.d.ts +1137 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +106 -0
- package/dist/usage.js +82 -0
- package/dist/workers.d.ts +146 -0
- package/dist/workers.js +822 -0
- package/dist/workspaces.d.ts +81 -0
- package/dist/workspaces.js +58 -0
- package/examples/headless-session.ts +85 -0
- package/package.json +56 -0
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { SwarmTimeoutError } from './errors.js';
|
|
2
|
+
function generateRequestId() {
|
|
3
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
4
|
+
return `sdk-session-${crypto.randomUUID()}`;
|
|
5
|
+
}
|
|
6
|
+
return `sdk-session-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 10)}`;
|
|
7
|
+
}
|
|
8
|
+
export class SwarmSessionsNamespace {
|
|
9
|
+
transport;
|
|
10
|
+
constructor(transport) {
|
|
11
|
+
this.transport = transport;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new durable V3 session.
|
|
15
|
+
*/
|
|
16
|
+
async create(params) {
|
|
17
|
+
const clientRequestId = params.client_request_id || generateRequestId();
|
|
18
|
+
const agentName = params.agent_name || params.agent || 'swarm';
|
|
19
|
+
const res = await this.transport.request('/v3/sessions', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: {
|
|
22
|
+
client_request_id: clientRequestId,
|
|
23
|
+
agent_name: agentName,
|
|
24
|
+
title: params.title,
|
|
25
|
+
workspace_id: params.workspace_id,
|
|
26
|
+
workspace_path: params.workspace_path,
|
|
27
|
+
mode: params.mode ?? 'auto',
|
|
28
|
+
metadata: params.metadata,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const data = res.data;
|
|
32
|
+
if (data && typeof data === 'object' && 'session' in data && data.session) {
|
|
33
|
+
return data.session;
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Lists durable sessions for the active account.
|
|
39
|
+
*/
|
|
40
|
+
async list(params = {}) {
|
|
41
|
+
const q = new URLSearchParams();
|
|
42
|
+
if (params.limit && params.limit > 0)
|
|
43
|
+
q.set('limit', params.limit.toString());
|
|
44
|
+
if (params.state)
|
|
45
|
+
q.set('state', params.state);
|
|
46
|
+
if (params.workspace_id)
|
|
47
|
+
q.set('workspace_id', params.workspace_id);
|
|
48
|
+
if (params.category)
|
|
49
|
+
q.set('category', params.category);
|
|
50
|
+
if (params.cursor)
|
|
51
|
+
q.set('cursor', params.cursor);
|
|
52
|
+
const queryStr = q.toString() ? `?${q.toString()}` : '';
|
|
53
|
+
const res = await this.transport.request(`/v3/sessions${queryStr}`, { method: 'GET' });
|
|
54
|
+
const data = res.data;
|
|
55
|
+
if (Array.isArray(data)) {
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
if (data && typeof data === 'object' && 'sessions' in data && Array.isArray(data.sessions)) {
|
|
59
|
+
return data.sessions;
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Retrieves full details, history, and state for a single session.
|
|
65
|
+
*/
|
|
66
|
+
async get(sessionId) {
|
|
67
|
+
const id = encodeURIComponent(sessionId.trim());
|
|
68
|
+
const res = await this.transport.request(`/v3/sessions/${id}`, {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
});
|
|
71
|
+
const data = res.data;
|
|
72
|
+
if (data && typeof data === 'object') {
|
|
73
|
+
const sessionObj = data.session || data;
|
|
74
|
+
return {
|
|
75
|
+
id: sessionObj.id || sessionObj.session_id || sessionId,
|
|
76
|
+
title: sessionObj.title || '',
|
|
77
|
+
workspace_id: sessionObj.workspace_id,
|
|
78
|
+
workspace_path: sessionObj.workspace_path,
|
|
79
|
+
state: sessionObj.state || data.projection?.state || 'idle',
|
|
80
|
+
mode: sessionObj.mode,
|
|
81
|
+
agent_name: sessionObj.agent_name,
|
|
82
|
+
created_at: sessionObj.created_at,
|
|
83
|
+
updated_at: sessionObj.updated_at,
|
|
84
|
+
archived: sessionObj.archived,
|
|
85
|
+
messages: data.messages,
|
|
86
|
+
last_run_id: sessionObj.last_run_id,
|
|
87
|
+
active_plan: data.active_plan,
|
|
88
|
+
raw: data,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return data;
|
|
92
|
+
}
|
|
93
|
+
/** Approve one exact pending tool call; no persistent rule or bypass is created. */
|
|
94
|
+
async approvePermissionOnce(sessionId, permissionId, reason) {
|
|
95
|
+
const res = await this.transport.request(`/v3/sessions/${encodeURIComponent(sessionId.trim())}/permissions/${encodeURIComponent(permissionId.trim())}/resolve`, { method: 'POST', body: { action: 'allow_once', reason } });
|
|
96
|
+
return res.data;
|
|
97
|
+
}
|
|
98
|
+
/** Resolve one exact pending tool permission; never changes account-wide policy. */
|
|
99
|
+
async denyPermission(sessionId, permissionId, reason) {
|
|
100
|
+
const res = await this.transport.request(`/v3/sessions/${encodeURIComponent(sessionId.trim())}/permissions/${encodeURIComponent(permissionId.trim())}/resolve`, { method: 'POST', body: { action: 'deny', reason } });
|
|
101
|
+
return res.data;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Archives a session.
|
|
105
|
+
*/
|
|
106
|
+
async archive(sessionId) {
|
|
107
|
+
const id = encodeURIComponent(sessionId.trim());
|
|
108
|
+
const res = await this.transport.request(`/v3/sessions/${id}/archive`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
});
|
|
111
|
+
return res.data?.ok === true;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Unarchives a previously archived session.
|
|
115
|
+
*/
|
|
116
|
+
async unarchive(sessionId) {
|
|
117
|
+
const res = await this.transport.request('/v3/sessions:unarchive', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
body: {
|
|
120
|
+
session_id: sessionId.trim(),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return res.data?.ok === true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Deletes a session and triggers cascading cancellation of associated runs and worktrees.
|
|
127
|
+
*/
|
|
128
|
+
async delete(sessionId) {
|
|
129
|
+
const id = encodeURIComponent(sessionId.trim());
|
|
130
|
+
const res = await this.transport.request(`/v3/sessions/${id}`, {
|
|
131
|
+
method: 'DELETE',
|
|
132
|
+
});
|
|
133
|
+
return res.data?.ok === true;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Appends a message to a session conversation.
|
|
137
|
+
*/
|
|
138
|
+
async sendMessage(sessionId, params) {
|
|
139
|
+
const id = encodeURIComponent(sessionId.trim());
|
|
140
|
+
const clientRequestId = params.client_request_id || generateRequestId();
|
|
141
|
+
const res = await this.transport.request(`/v3/sessions/${id}/messages`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: {
|
|
144
|
+
client_request_id: clientRequestId,
|
|
145
|
+
content: params.content,
|
|
146
|
+
role: params.role ?? 'user',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
return res.data;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Stops an active execution run for a session.
|
|
153
|
+
*/
|
|
154
|
+
async stopRun(sessionId, params) {
|
|
155
|
+
const id = encodeURIComponent(sessionId.trim());
|
|
156
|
+
let target = params.target_swarm_id?.trim();
|
|
157
|
+
if (!target) {
|
|
158
|
+
const session = await this.get(sessionId);
|
|
159
|
+
const rawSession = session.raw?.session;
|
|
160
|
+
const runtime = rawSession?.metadata?.swarm_v3_runtime_swarm_id;
|
|
161
|
+
target = typeof runtime === 'string' ? runtime.trim() : undefined;
|
|
162
|
+
if (!target)
|
|
163
|
+
throw new Error('Session has no authoritative runtime identity; supply target_swarm_id');
|
|
164
|
+
}
|
|
165
|
+
const res = await this.transport.request(`/v3/sessions/${id}/run/stop`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: {
|
|
168
|
+
run_id: params.run_id,
|
|
169
|
+
target_swarm_id: target,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
return res.data?.ok === true;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Helper that polls a session until its active run completes, or times out.
|
|
176
|
+
*/
|
|
177
|
+
async waitForRun(sessionId, options = {}) {
|
|
178
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
179
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1_000;
|
|
180
|
+
const deadline = Date.now() + timeoutMs;
|
|
181
|
+
while (Date.now() < deadline) {
|
|
182
|
+
const detail = await this.get(sessionId);
|
|
183
|
+
const raw = (detail.raw || {});
|
|
184
|
+
const activeRun = raw.active_run_intent;
|
|
185
|
+
const state = (detail.state || '').toLowerCase();
|
|
186
|
+
const isRunning = state.includes('running') || state.includes('in_progress') || !!activeRun;
|
|
187
|
+
if (!isRunning) {
|
|
188
|
+
return detail;
|
|
189
|
+
}
|
|
190
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
191
|
+
}
|
|
192
|
+
throw new SwarmTimeoutError(`Session run did not complete within ${timeoutMs}ms`, timeoutMs);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SwarmTransport } from './transport.js';
|
|
2
|
+
export interface ProviderStatus {
|
|
3
|
+
id: string;
|
|
4
|
+
ready: boolean;
|
|
5
|
+
runnable: boolean;
|
|
6
|
+
reason?: string;
|
|
7
|
+
run_reason?: string;
|
|
8
|
+
default_model?: string;
|
|
9
|
+
default_thinking?: string;
|
|
10
|
+
auth_methods?: {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
credential_type?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
export interface ModelAssignment {
|
|
18
|
+
provider: string;
|
|
19
|
+
model: string;
|
|
20
|
+
thinking: string;
|
|
21
|
+
service_tier?: string;
|
|
22
|
+
context_mode?: string;
|
|
23
|
+
}
|
|
24
|
+
export type SystemAgentSlot = 'compact' | 'finder' | 'coder' | 'designer' | 'router';
|
|
25
|
+
export interface AgentModelSettings {
|
|
26
|
+
account_scope_id: string;
|
|
27
|
+
updated_at: number;
|
|
28
|
+
swarm: {
|
|
29
|
+
action: ModelAssignment;
|
|
30
|
+
plan: ModelAssignment;
|
|
31
|
+
};
|
|
32
|
+
system_agents: Record<SystemAgentSlot, ModelAssignment>;
|
|
33
|
+
}
|
|
34
|
+
export interface AgentModelSettingsResponse {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
agent_model_settings: AgentModelSettings;
|
|
37
|
+
roles: Record<string, string>[];
|
|
38
|
+
}
|
|
39
|
+
/** Catalog extensions are preserved as unknown, never interpreted as runtime capability grants. */
|
|
40
|
+
export interface ModelCatalogEntry {
|
|
41
|
+
provider: string;
|
|
42
|
+
model: string;
|
|
43
|
+
display_name?: string;
|
|
44
|
+
provider_display_name?: string;
|
|
45
|
+
catalog_id?: string;
|
|
46
|
+
context_window: number;
|
|
47
|
+
max_output_tokens: number;
|
|
48
|
+
reasoning: boolean;
|
|
49
|
+
thinking_options?: string[];
|
|
50
|
+
default_thinking?: string;
|
|
51
|
+
service_tiers?: string[];
|
|
52
|
+
default_service_tier?: string;
|
|
53
|
+
context_modes?: {
|
|
54
|
+
mode: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
context_window?: number;
|
|
57
|
+
default?: boolean;
|
|
58
|
+
}[];
|
|
59
|
+
source: string;
|
|
60
|
+
fetched_at: number;
|
|
61
|
+
expires_at: number;
|
|
62
|
+
[extension: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
export interface ModelCatalogResponse {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
provider: string;
|
|
67
|
+
count: number;
|
|
68
|
+
records: ModelCatalogEntry[];
|
|
69
|
+
meta?: Record<string, unknown>;
|
|
70
|
+
catalog_status: {
|
|
71
|
+
configured: boolean;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Account-scoped settings. Requires the local administrative connection, not a session token. */
|
|
76
|
+
export declare class SwarmSettingsNamespace {
|
|
77
|
+
private readonly transport;
|
|
78
|
+
constructor(transport: SwarmTransport);
|
|
79
|
+
providers(): Promise<ProviderStatus[]>;
|
|
80
|
+
models(provider: string, limit?: number): Promise<ModelCatalogResponse>;
|
|
81
|
+
agentModels(): Promise<AgentModelSettingsResponse>;
|
|
82
|
+
setSwarmModel(slot: 'action' | 'plan', assignment: ModelAssignment): Promise<AgentModelSettingsResponse>;
|
|
83
|
+
setSystemAgentModel(slot: SystemAgentSlot, assignment: ModelAssignment): Promise<AgentModelSettingsResponse>;
|
|
84
|
+
private patch;
|
|
85
|
+
}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Account-scoped settings. Requires the local administrative connection, not a session token. */
|
|
2
|
+
export class SwarmSettingsNamespace {
|
|
3
|
+
transport;
|
|
4
|
+
constructor(transport) {
|
|
5
|
+
this.transport = transport;
|
|
6
|
+
}
|
|
7
|
+
async providers() {
|
|
8
|
+
return (await this.transport.request('/v1/providers')).data.providers;
|
|
9
|
+
}
|
|
10
|
+
async models(provider, limit = 500) {
|
|
11
|
+
const query = new URLSearchParams({ provider, limit: String(limit) });
|
|
12
|
+
return (await this.transport.request(`/v1/model/catalog?${query}`)).data;
|
|
13
|
+
}
|
|
14
|
+
async agentModels() {
|
|
15
|
+
return (await this.transport.request('/v1/agent-model-settings')).data;
|
|
16
|
+
}
|
|
17
|
+
async setSwarmModel(slot, assignment) {
|
|
18
|
+
return this.patch({ swarm: { [slot]: assignment } });
|
|
19
|
+
}
|
|
20
|
+
async setSystemAgentModel(slot, assignment) {
|
|
21
|
+
return this.patch({ system_agents: { [slot]: assignment } });
|
|
22
|
+
}
|
|
23
|
+
async patch(body) {
|
|
24
|
+
return (await this.transport.request('/v1/agent-model-settings', { method: 'PATCH', body })).data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StorageDriver } from '../types.js';
|
|
2
|
+
export declare class MemoryStorageDriver implements StorageDriver {
|
|
3
|
+
private store;
|
|
4
|
+
private textEncoder;
|
|
5
|
+
private textDecoder;
|
|
6
|
+
read(key: string): Promise<Uint8Array | null>;
|
|
7
|
+
readString(key: string): Promise<string | null>;
|
|
8
|
+
write(key: string, data: Uint8Array | string): Promise<void>;
|
|
9
|
+
delete(key: string): Promise<void>;
|
|
10
|
+
list(prefix: string): Promise<string[]>;
|
|
11
|
+
exists(key: string): Promise<boolean>;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class MemoryStorageDriver {
|
|
2
|
+
store = new Map();
|
|
3
|
+
textEncoder = new TextEncoder();
|
|
4
|
+
textDecoder = new TextDecoder();
|
|
5
|
+
async read(key) {
|
|
6
|
+
const val = this.store.get(key);
|
|
7
|
+
return val ? new Uint8Array(val) : null;
|
|
8
|
+
}
|
|
9
|
+
async readString(key) {
|
|
10
|
+
const bytes = await this.read(key);
|
|
11
|
+
return bytes ? this.textDecoder.decode(bytes) : null;
|
|
12
|
+
}
|
|
13
|
+
async write(key, data) {
|
|
14
|
+
const bytes = typeof data === 'string' ? this.textEncoder.encode(data) : new Uint8Array(data);
|
|
15
|
+
this.store.set(key, bytes);
|
|
16
|
+
}
|
|
17
|
+
async delete(key) {
|
|
18
|
+
this.store.delete(key);
|
|
19
|
+
}
|
|
20
|
+
async list(prefix) {
|
|
21
|
+
const keys = [];
|
|
22
|
+
for (const key of this.store.keys()) {
|
|
23
|
+
if (key.startsWith(prefix)) {
|
|
24
|
+
keys.push(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return keys.sort();
|
|
28
|
+
}
|
|
29
|
+
async exists(key) {
|
|
30
|
+
return this.store.has(key);
|
|
31
|
+
}
|
|
32
|
+
clear() {
|
|
33
|
+
this.store.clear();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StorageDriver } from '../types.js';
|
|
2
|
+
export interface S3DriverConfig {
|
|
3
|
+
bucket: string;
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
region?: string;
|
|
6
|
+
accessKeyId?: string;
|
|
7
|
+
secretAccessKey?: string;
|
|
8
|
+
sessionToken?: string;
|
|
9
|
+
forcePathStyle?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class S3StorageDriver implements StorageDriver {
|
|
12
|
+
private bucket;
|
|
13
|
+
private endpoint;
|
|
14
|
+
private region;
|
|
15
|
+
private accessKeyId?;
|
|
16
|
+
private secretAccessKey?;
|
|
17
|
+
private sessionToken?;
|
|
18
|
+
private forcePathStyle;
|
|
19
|
+
constructor(config: S3DriverConfig);
|
|
20
|
+
private getUrl;
|
|
21
|
+
private sha256Hex;
|
|
22
|
+
private hmacSha256;
|
|
23
|
+
private getSigningKey;
|
|
24
|
+
private signRequest;
|
|
25
|
+
read(key: string): Promise<Uint8Array | null>;
|
|
26
|
+
readString(key: string): Promise<string | null>;
|
|
27
|
+
write(key: string, data: Uint8Array | string, contentType?: string): Promise<void>;
|
|
28
|
+
delete(key: string): Promise<void>;
|
|
29
|
+
list(prefix: string): Promise<string[]>;
|
|
30
|
+
exists(key: string): Promise<boolean>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export class S3StorageDriver {
|
|
2
|
+
bucket;
|
|
3
|
+
endpoint;
|
|
4
|
+
region;
|
|
5
|
+
accessKeyId;
|
|
6
|
+
secretAccessKey;
|
|
7
|
+
sessionToken;
|
|
8
|
+
forcePathStyle;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.bucket = config.bucket;
|
|
11
|
+
this.region = config.region || 'us-east-1';
|
|
12
|
+
this.endpoint = (config.endpoint || `https://s3.${this.region}.amazonaws.com`).replace(/\/+$/, '');
|
|
13
|
+
this.accessKeyId = config.accessKeyId;
|
|
14
|
+
this.secretAccessKey = config.secretAccessKey;
|
|
15
|
+
this.sessionToken = config.sessionToken;
|
|
16
|
+
this.forcePathStyle = config.forcePathStyle ?? true;
|
|
17
|
+
}
|
|
18
|
+
getUrl(key) {
|
|
19
|
+
const cleanKey = key.replace(/^\/+/, '');
|
|
20
|
+
if (this.forcePathStyle) {
|
|
21
|
+
return new URL(`${this.endpoint}/${this.bucket}/${cleanKey}`);
|
|
22
|
+
}
|
|
23
|
+
const endpointUrl = new URL(this.endpoint);
|
|
24
|
+
return new URL(`${endpointUrl.protocol}//${this.bucket}.${endpointUrl.host}/${cleanKey}`);
|
|
25
|
+
}
|
|
26
|
+
async sha256Hex(data) {
|
|
27
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
28
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
|
|
29
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
30
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
31
|
+
.join('');
|
|
32
|
+
}
|
|
33
|
+
async hmacSha256(key, data) {
|
|
34
|
+
const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
35
|
+
const dataBytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
36
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, dataBytes);
|
|
37
|
+
return new Uint8Array(signature);
|
|
38
|
+
}
|
|
39
|
+
async getSigningKey(dateStamp) {
|
|
40
|
+
const kSecret = new TextEncoder().encode(`AWS4${this.secretAccessKey || ''}`);
|
|
41
|
+
const kDate = await this.hmacSha256(kSecret, dateStamp);
|
|
42
|
+
const kRegion = await this.hmacSha256(kDate, this.region);
|
|
43
|
+
const kService = await this.hmacSha256(kRegion, 's3');
|
|
44
|
+
return this.hmacSha256(kService, 'aws4_request');
|
|
45
|
+
}
|
|
46
|
+
async signRequest(method, url, headers, payloadHash) {
|
|
47
|
+
if (!this.accessKeyId || !this.secretAccessKey) {
|
|
48
|
+
return headers;
|
|
49
|
+
}
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
|
52
|
+
const dateStamp = amzDate.substring(0, 8);
|
|
53
|
+
const signedHeadersList = ['host', 'x-amz-content-sha256', 'x-amz-date'];
|
|
54
|
+
const reqHeaders = {
|
|
55
|
+
...headers,
|
|
56
|
+
host: url.host,
|
|
57
|
+
'x-amz-date': amzDate,
|
|
58
|
+
'x-amz-content-sha256': payloadHash,
|
|
59
|
+
};
|
|
60
|
+
if (this.sessionToken) {
|
|
61
|
+
reqHeaders['x-amz-security-token'] = this.sessionToken;
|
|
62
|
+
signedHeadersList.push('x-amz-security-token');
|
|
63
|
+
}
|
|
64
|
+
signedHeadersList.sort();
|
|
65
|
+
const signedHeadersStr = signedHeadersList.join(';');
|
|
66
|
+
const canonicalHeaders = signedHeadersList
|
|
67
|
+
.map((h) => `${h}:${reqHeaders[h].trim()}\n`)
|
|
68
|
+
.join('');
|
|
69
|
+
const canonicalUri = url.pathname;
|
|
70
|
+
const searchParams = new URLSearchParams(url.search);
|
|
71
|
+
searchParams.sort();
|
|
72
|
+
const canonicalQueryString = searchParams.toString();
|
|
73
|
+
const canonicalRequest = [
|
|
74
|
+
method,
|
|
75
|
+
canonicalUri,
|
|
76
|
+
canonicalQueryString,
|
|
77
|
+
canonicalHeaders,
|
|
78
|
+
signedHeadersStr,
|
|
79
|
+
payloadHash,
|
|
80
|
+
].join('\n');
|
|
81
|
+
const algorithm = 'AWS4-HMAC-SHA256';
|
|
82
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
83
|
+
const hashedCanonicalRequest = await this.sha256Hex(canonicalRequest);
|
|
84
|
+
const stringToSign = [
|
|
85
|
+
algorithm,
|
|
86
|
+
amzDate,
|
|
87
|
+
credentialScope,
|
|
88
|
+
hashedCanonicalRequest,
|
|
89
|
+
].join('\n');
|
|
90
|
+
const signingKey = await this.getSigningKey(dateStamp);
|
|
91
|
+
const signatureBytes = await this.hmacSha256(signingKey, stringToSign);
|
|
92
|
+
const signature = Array.from(signatureBytes)
|
|
93
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
94
|
+
.join('');
|
|
95
|
+
reqHeaders['Authorization'] = `${algorithm} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeadersStr}, Signature=${signature}`;
|
|
96
|
+
return reqHeaders;
|
|
97
|
+
}
|
|
98
|
+
async read(key) {
|
|
99
|
+
const url = this.getUrl(key);
|
|
100
|
+
const payloadHash = await this.sha256Hex('');
|
|
101
|
+
const headers = await this.signRequest('GET', url, {}, payloadHash);
|
|
102
|
+
const res = await fetch(url.toString(), { method: 'GET', headers });
|
|
103
|
+
if (res.status === 404) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(`S3 read error ${res.status}: ${await res.text()}`);
|
|
108
|
+
}
|
|
109
|
+
const buf = await res.arrayBuffer();
|
|
110
|
+
return new Uint8Array(buf);
|
|
111
|
+
}
|
|
112
|
+
async readString(key) {
|
|
113
|
+
const bytes = await this.read(key);
|
|
114
|
+
return bytes ? new TextDecoder().decode(bytes) : null;
|
|
115
|
+
}
|
|
116
|
+
async write(key, data, contentType = 'application/octet-stream') {
|
|
117
|
+
const url = this.getUrl(key);
|
|
118
|
+
const body = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
119
|
+
const payloadHash = await this.sha256Hex(body);
|
|
120
|
+
const headers = await this.signRequest('PUT', url, { 'content-type': contentType }, payloadHash);
|
|
121
|
+
const res = await fetch(url.toString(), {
|
|
122
|
+
method: 'PUT',
|
|
123
|
+
headers,
|
|
124
|
+
body: body,
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
throw new Error(`S3 write error ${res.status}: ${await res.text()}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async delete(key) {
|
|
131
|
+
const url = this.getUrl(key);
|
|
132
|
+
const payloadHash = await this.sha256Hex('');
|
|
133
|
+
const headers = await this.signRequest('DELETE', url, {}, payloadHash);
|
|
134
|
+
const res = await fetch(url.toString(), { method: 'DELETE', headers });
|
|
135
|
+
if (!res.ok && res.status !== 404) {
|
|
136
|
+
throw new Error(`S3 delete error ${res.status}: ${await res.text()}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async list(prefix) {
|
|
140
|
+
const cleanPrefix = prefix.replace(/^\/+/, '');
|
|
141
|
+
const url = this.forcePathStyle
|
|
142
|
+
? new URL(`${this.endpoint}/${this.bucket}?list-type=2&prefix=${encodeURIComponent(cleanPrefix)}`)
|
|
143
|
+
: new URL(`${new URL(this.endpoint).protocol}//${this.bucket}.${new URL(this.endpoint).host}?list-type=2&prefix=${encodeURIComponent(cleanPrefix)}`);
|
|
144
|
+
const payloadHash = await this.sha256Hex('');
|
|
145
|
+
const headers = await this.signRequest('GET', url, {}, payloadHash);
|
|
146
|
+
const res = await fetch(url.toString(), { method: 'GET', headers });
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw new Error(`S3 list error ${res.status}: ${await res.text()}`);
|
|
149
|
+
}
|
|
150
|
+
const xml = await res.text();
|
|
151
|
+
const keys = [];
|
|
152
|
+
const keyMatches = xml.matchAll(/<Key>(.*?)<\/Key>/g);
|
|
153
|
+
for (const match of keyMatches) {
|
|
154
|
+
if (match[1]) {
|
|
155
|
+
keys.push(match[1]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return keys;
|
|
159
|
+
}
|
|
160
|
+
async exists(key) {
|
|
161
|
+
const url = this.getUrl(key);
|
|
162
|
+
const payloadHash = await this.sha256Hex('');
|
|
163
|
+
const headers = await this.signRequest('HEAD', url, {}, payloadHash);
|
|
164
|
+
const res = await fetch(url.toString(), { method: 'HEAD', headers });
|
|
165
|
+
if (res.status === 200)
|
|
166
|
+
return true;
|
|
167
|
+
if (res.status === 404)
|
|
168
|
+
return false;
|
|
169
|
+
throw new Error(`S3 exists error ${res.status}`);
|
|
170
|
+
}
|
|
171
|
+
}
|