sharetribe-flex-build-sdk 1.15.0

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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Multipart form-data implementation using Node.js streams
3
+ *
4
+ * No external dependencies - implements multipart/form-data manually
5
+ */
6
+
7
+ import { randomBytes } from 'node:crypto';
8
+ import { createReadStream } from 'node:fs';
9
+
10
+ export interface MultipartField {
11
+ name: string;
12
+ value: string | Buffer;
13
+ filename?: string;
14
+ contentType?: string;
15
+ }
16
+
17
+ /**
18
+ * Generates a random boundary for multipart form-data
19
+ * Returns the boundary without the -- prefix (that gets added when building the body)
20
+ */
21
+ function generateBoundary(): string {
22
+ return `WebKitFormBoundary${randomBytes(16).toString('hex')}`;
23
+ }
24
+
25
+ /**
26
+ * Creates multipart form-data body from fields
27
+ */
28
+ export function createMultipartBody(fields: MultipartField[]): {
29
+ body: Buffer;
30
+ boundary: string;
31
+ contentType: string;
32
+ } {
33
+ const boundary = generateBoundary();
34
+ const parts: Buffer[] = [];
35
+
36
+ for (const field of fields) {
37
+ // Add boundary
38
+ parts.push(Buffer.from(`--${boundary}\r\n`));
39
+
40
+ // Add Content-Disposition header
41
+ if (field.filename) {
42
+ parts.push(
43
+ Buffer.from(
44
+ `Content-Disposition: form-data; name="${field.name}"; filename="${field.filename}"\r\n`
45
+ )
46
+ );
47
+
48
+ // Add Content-Type if specified
49
+ if (field.contentType) {
50
+ parts.push(Buffer.from(`Content-Type: ${field.contentType}\r\n`));
51
+ }
52
+ } else {
53
+ parts.push(Buffer.from(`Content-Disposition: form-data; name="${field.name}"\r\n`));
54
+ }
55
+
56
+ parts.push(Buffer.from('\r\n'));
57
+
58
+ // Add value
59
+ if (typeof field.value === 'string') {
60
+ parts.push(Buffer.from(field.value));
61
+ } else {
62
+ parts.push(field.value);
63
+ }
64
+
65
+ parts.push(Buffer.from('\r\n'));
66
+ }
67
+
68
+ // Add final boundary
69
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
70
+
71
+ const body = Buffer.concat(parts);
72
+
73
+ return {
74
+ body,
75
+ boundary,
76
+ contentType: `multipart/form-data; boundary=${boundary}`,
77
+ };
78
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Transit format encoding/decoding for Sharetribe API
3
+ *
4
+ * The Build API expects Transit format (application/transit+json) for certain endpoints.
5
+ * Transit is a data format that extends JSON with support for additional data types.
6
+ */
7
+
8
+ import transit from 'transit-js';
9
+
10
+ /**
11
+ * Encodes data to Transit JSON format
12
+ *
13
+ * @param data - The data to encode (can include keywords, dates, etc.)
14
+ * @returns Transit-encoded string
15
+ */
16
+ export function encodeTransit(data: unknown): string {
17
+ const writer = transit.writer('json', {
18
+ handlers: transit.map([
19
+ // Add any custom handlers here if needed
20
+ ])
21
+ });
22
+ return writer.write(data);
23
+ }
24
+
25
+ /**
26
+ * Converts Transit values (maps, keywords, etc.) to plain JavaScript objects
27
+ *
28
+ * @param val - Transit value
29
+ * @returns Plain JavaScript value
30
+ */
31
+ function transitToJS(val: any): any {
32
+ // Handle Transit maps
33
+ if (val && typeof val.get === 'function' && val._entries) {
34
+ const obj: Record<string, any> = {};
35
+ for (let i = 0; i < val._entries.length; i += 2) {
36
+ const key = val._entries[i];
37
+ const value = val._entries[i + 1];
38
+ // Convert keyword keys to strings (e.g., :data -> "data")
39
+ const keyStr = key._name || String(key);
40
+ obj[keyStr] = transitToJS(value);
41
+ }
42
+ return obj;
43
+ }
44
+
45
+ // Handle Transit keywords
46
+ if (val && val._name !== undefined) {
47
+ return val._name;
48
+ }
49
+
50
+ // Handle arrays
51
+ if (Array.isArray(val)) {
52
+ return val.map(transitToJS);
53
+ }
54
+
55
+ // Return primitives as-is
56
+ return val;
57
+ }
58
+
59
+ /**
60
+ * Decodes Transit JSON format to JavaScript objects
61
+ *
62
+ * @param transitString - Transit-encoded string
63
+ * @returns Decoded JavaScript object
64
+ */
65
+ export function decodeTransit(transitString: string): unknown {
66
+ const reader = transit.reader('json');
67
+ const transitValue = reader.read(transitString);
68
+ return transitToJS(transitValue);
69
+ }
70
+
71
+ /**
72
+ * Creates a Transit keyword (used for Clojure-style keywords in the API)
73
+ *
74
+ * @param name - The keyword name (e.g., "default-booking")
75
+ * @returns Transit keyword object
76
+ */
77
+ export function keyword(name: string): unknown {
78
+ return transit.keyword(name);
79
+ }
80
+
81
+ /**
82
+ * Creates a Transit map with keyword keys
83
+ *
84
+ * This is needed because the Sharetribe API expects Transit maps with keyword keys,
85
+ * not string keys. In Clojure/Transit: {:name :value} not {"name" :value}
86
+ *
87
+ * @param obj - Plain JavaScript object with string keys
88
+ * @returns Transit map with keyword keys
89
+ */
90
+ export function keywordMap(obj: Record<string, unknown>): unknown {
91
+ const entries: unknown[] = [];
92
+ for (const [key, value] of Object.entries(obj)) {
93
+ entries.push(transit.keyword(key), value);
94
+ }
95
+ return transit.map(entries);
96
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Asset management functions
3
+ *
4
+ * Programmatic API for managing marketplace assets
5
+ */
6
+
7
+ import { apiGet, apiPostMultipart, type MultipartField } from './api/client.js';
8
+
9
+ export interface Asset {
10
+ path: string;
11
+ dataRaw: string; // base64 encoded
12
+ contentHash?: string;
13
+ }
14
+
15
+ export interface PullAssetsResult {
16
+ version: string;
17
+ assets: Asset[];
18
+ }
19
+
20
+ export interface PushAssetsResult {
21
+ version: string;
22
+ assets: Array<{ path: string; contentHash: string }>;
23
+ }
24
+
25
+ /**
26
+ * Pulls assets from remote
27
+ *
28
+ * @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
29
+ * @param marketplace - Marketplace ID
30
+ * @param options - Pull options
31
+ * @returns Assets and version information
32
+ */
33
+ export async function pullAssets(
34
+ apiKey: string | undefined,
35
+ marketplace: string,
36
+ options?: { version?: string }
37
+ ): Promise<PullAssetsResult> {
38
+ const params: Record<string, string> = { marketplace };
39
+
40
+ if (options?.version) {
41
+ params.version = options.version;
42
+ } else {
43
+ params['version-alias'] = 'latest';
44
+ }
45
+
46
+ const response = await apiGet<{
47
+ data: Array<{
48
+ path: string;
49
+ 'data-raw': string;
50
+ 'content-hash'?: string;
51
+ }>;
52
+ meta: { version?: string; 'aliased-version'?: string };
53
+ }>(apiKey, '/assets/pull', params);
54
+
55
+ const version = response.meta.version || response.meta['aliased-version'];
56
+ if (!version) {
57
+ throw new Error('No version information in response');
58
+ }
59
+
60
+ return {
61
+ version,
62
+ assets: response.data.map(a => ({
63
+ path: a.path,
64
+ dataRaw: a['data-raw'],
65
+ contentHash: a['content-hash'],
66
+ })),
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Pushes assets to remote
72
+ *
73
+ * @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
74
+ * @param marketplace - Marketplace ID
75
+ * @param currentVersion - Current version (use 'nil' if first push)
76
+ * @param operations - Array of operations to perform
77
+ * @returns New version and asset metadata
78
+ */
79
+ export async function pushAssets(
80
+ apiKey: string | undefined,
81
+ marketplace: string,
82
+ currentVersion: string,
83
+ operations: Array<{
84
+ path: string;
85
+ op: 'upsert' | 'delete';
86
+ data?: Buffer;
87
+ }>
88
+ ): Promise<PushAssetsResult> {
89
+ const fields: MultipartField[] = [
90
+ { name: 'current-version', value: currentVersion },
91
+ ];
92
+
93
+ for (let i = 0; i < operations.length; i++) {
94
+ const op = operations[i];
95
+ fields.push({ name: `path-${i}`, value: op.path });
96
+ fields.push({ name: `op-${i}`, value: op.op });
97
+ if (op.op === 'upsert' && op.data) {
98
+ fields.push({ name: `data-raw-${i}`, value: op.data });
99
+ }
100
+ }
101
+
102
+ const response = await apiPostMultipart<{
103
+ data: {
104
+ version: string;
105
+ 'asset-meta': { assets: Array<{ path: string; 'content-hash': string }> };
106
+ };
107
+ }>(apiKey, '/assets/push', { marketplace }, fields);
108
+
109
+ return {
110
+ version: response.data.version,
111
+ assets: response.data['asset-meta'].assets.map(a => ({
112
+ path: a.path,
113
+ contentHash: a['content-hash'],
114
+ })),
115
+ };
116
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Authentication storage - manages ~/.config/flex-cli/auth.edn
3
+ *
4
+ * Must maintain 100% compatibility with flex-cli's auth.edn format
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import edn from 'jsedn';
11
+
12
+ const CONFIG_DIR = join(homedir(), '.config', 'flex-cli');
13
+ const AUTH_FILE = join(CONFIG_DIR, 'auth.edn');
14
+
15
+ export interface AuthData {
16
+ apiKey: string;
17
+ }
18
+
19
+ /**
20
+ * Reads authentication data from ~/.config/flex-cli/auth.edn
21
+ *
22
+ * Returns null if file doesn't exist or is invalid
23
+ */
24
+ export function readAuth(): AuthData | null {
25
+ try {
26
+ if (!existsSync(AUTH_FILE)) {
27
+ return null;
28
+ }
29
+
30
+ const content = readFileSync(AUTH_FILE, 'utf-8');
31
+ const parsed = edn.parse(content);
32
+
33
+ // EDN keys are symbols, get :api-key
34
+ const apiKeySymbol = edn.kw(':api-key');
35
+ const apiKey = parsed.at(apiKeySymbol);
36
+
37
+ if (typeof apiKey !== 'string') {
38
+ return null;
39
+ }
40
+
41
+ return { apiKey };
42
+ } catch (error) {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Writes authentication data to ~/.config/flex-cli/auth.edn
49
+ *
50
+ * Format must match flex-cli exactly: {:api-key "..."}
51
+ */
52
+ export function writeAuth(data: AuthData): void {
53
+ // Ensure config directory exists
54
+ if (!existsSync(CONFIG_DIR)) {
55
+ mkdirSync(CONFIG_DIR, { recursive: true });
56
+ }
57
+
58
+ // Create EDN map with :api-key
59
+ const authMap = new (edn as any).Map([edn.kw(':api-key'), data.apiKey]);
60
+ const ednString = edn.encode(authMap);
61
+
62
+ writeFileSync(AUTH_FILE, ednString, 'utf-8');
63
+ }
64
+
65
+ /**
66
+ * Clears authentication data (deletes auth.edn file)
67
+ */
68
+ export async function clearAuth(): Promise<void> {
69
+ try {
70
+ if (existsSync(AUTH_FILE)) {
71
+ const fs = await import('node:fs/promises');
72
+ await fs.unlink(AUTH_FILE);
73
+ }
74
+ } catch (error) {
75
+ // Ignore errors if file doesn't exist
76
+ }
77
+ }
package/src/deploy.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Process deployment functions
3
+ *
4
+ * High-level functions for deploying processes to aliases
5
+ */
6
+
7
+ import { createProcess, pushProcess, createAlias, updateAlias } from './processes.js';
8
+ import { serializeProcess } from './edn-process.js';
9
+ import type { ProcessDefinition } from './types.js';
10
+
11
+ export interface DeployProcessOptions {
12
+ /** Process name */
13
+ process: string;
14
+ /** Target alias to deploy to */
15
+ alias: string;
16
+ /** Path to process.edn file (used for display only) */
17
+ path?: string;
18
+ /** Process definition to deploy */
19
+ processDefinition: ProcessDefinition;
20
+ }
21
+
22
+ export interface DeployProcessResult {
23
+ /** Whether a new process was created */
24
+ processCreated: boolean;
25
+ /** Process version number */
26
+ version: number;
27
+ /** Whether a new alias was created */
28
+ aliasCreated: boolean;
29
+ /** Alias name */
30
+ alias: string;
31
+ }
32
+
33
+ /**
34
+ * Deploys a process to an alias
35
+ *
36
+ * This high-level function handles the complete deployment workflow:
37
+ * 1. Creates the process if it doesn't exist
38
+ * 2. Pushes the process definition to create a new version
39
+ * 3. Creates or updates the alias to point to the new version
40
+ *
41
+ * @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
42
+ * @param marketplace - Marketplace ID
43
+ * @param options - Deployment options
44
+ * @returns Deployment result including version and alias information
45
+ */
46
+ export async function deployProcess(
47
+ apiKey: string | undefined,
48
+ marketplace: string,
49
+ options: DeployProcessOptions
50
+ ): Promise<DeployProcessResult> {
51
+ const { process, alias, processDefinition } = options;
52
+
53
+ // Serialize process definition to EDN format
54
+ const processEdn = serializeProcess(processDefinition);
55
+
56
+ // Step 1: Try to create the process (will fail if it already exists, which is fine)
57
+ let processCreated = false;
58
+ try {
59
+ await createProcess(apiKey, marketplace, process, processEdn);
60
+ processCreated = true;
61
+ } catch (error: any) {
62
+ // If process already exists, continue
63
+ if (error.code !== 'already-exists') {
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ // Step 2: Push the process to create a new version
69
+ const pushResult = await pushProcess(apiKey, marketplace, process, processEdn);
70
+
71
+ // Ensure we have a version number
72
+ if (pushResult.version === undefined) {
73
+ throw new Error('Failed to get version number from push result');
74
+ }
75
+
76
+ // Step 3: Create or update the alias
77
+ let aliasCreated = false;
78
+ try {
79
+ await createAlias(apiKey, marketplace, process, pushResult.version, alias);
80
+ aliasCreated = true;
81
+ } catch (error: any) {
82
+ // If alias already exists, update it
83
+ if (error.code === 'already-exists') {
84
+ await updateAlias(apiKey, marketplace, process, pushResult.version, alias);
85
+ } else {
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ return {
91
+ processCreated,
92
+ version: pushResult.version,
93
+ aliasCreated,
94
+ alias,
95
+ };
96
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Process.edn file parser
3
+ *
4
+ * Parses transaction process definitions from EDN format
5
+ */
6
+
7
+ import edn from 'jsedn';
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ export interface ProcessState {
11
+ name: string;
12
+ in: string[];
13
+ out: string[];
14
+ }
15
+
16
+ export interface ProcessTransition {
17
+ name: string;
18
+ from: string;
19
+ to: string;
20
+ actor: string;
21
+ privileged?: boolean;
22
+ actions?: Array<{ name: string; config?: unknown }>;
23
+ }
24
+
25
+ export interface ProcessNotification {
26
+ name: string;
27
+ on: string;
28
+ to: string;
29
+ template: string;
30
+ }
31
+
32
+ export interface ProcessDefinition {
33
+ name: string;
34
+ version?: number;
35
+ states: ProcessState[];
36
+ transitions: ProcessTransition[];
37
+ notifications: ProcessNotification[];
38
+ }
39
+
40
+ /**
41
+ * Converts EDN keyword to string
42
+ */
43
+ function ednKeywordToString(kw: unknown): string {
44
+ if (kw && typeof kw === 'object' && 'name' in kw) {
45
+ return (kw as { name: string }).name;
46
+ }
47
+ return String(kw);
48
+ }
49
+
50
+ /**
51
+ * Parses a process.edn file
52
+ */
53
+ export function parseProcessFile(filePath: string): ProcessDefinition {
54
+ const content = readFileSync(filePath, 'utf-8');
55
+ const parsed = edn.parse(content);
56
+
57
+ // Extract process data from EDN map
58
+ const nameKw = edn.kw(':name');
59
+ const statesKw = edn.kw(':states');
60
+ const transitionsKw = edn.kw(':transitions');
61
+ const notificationsKw = edn.kw(':notifications');
62
+
63
+ const name = ednKeywordToString(parsed.at(nameKw));
64
+ const statesData = parsed.at(statesKw) || [];
65
+ const transitionsData = parsed.at(transitionsKw) || [];
66
+ const notificationsData = parsed.at(notificationsKw) || [];
67
+
68
+ // Parse states
69
+ const states: ProcessState[] = [];
70
+ if (Array.isArray(statesData)) {
71
+ for (const state of statesData) {
72
+ states.push({
73
+ name: ednKeywordToString(state.at(edn.kw(':name'))),
74
+ in: (state.at(edn.kw(':in')) || []).map(ednKeywordToString),
75
+ out: (state.at(edn.kw(':out')) || []).map(ednKeywordToString),
76
+ });
77
+ }
78
+ }
79
+
80
+ // Parse transitions
81
+ const transitions: ProcessTransition[] = [];
82
+ if (Array.isArray(transitionsData)) {
83
+ for (const transition of transitionsData) {
84
+ transitions.push({
85
+ name: ednKeywordToString(transition.at(edn.kw(':name'))),
86
+ from: ednKeywordToString(transition.at(edn.kw(':from'))),
87
+ to: ednKeywordToString(transition.at(edn.kw(':to'))),
88
+ actor: ednKeywordToString(transition.at(edn.kw(':actor'))),
89
+ privileged: transition.at(edn.kw(':privileged?')) || false,
90
+ actions: transition.at(edn.kw(':actions')) || [],
91
+ });
92
+ }
93
+ }
94
+
95
+ // Parse notifications
96
+ const notifications: ProcessNotification[] = [];
97
+ if (Array.isArray(notificationsData)) {
98
+ for (const notification of notificationsData) {
99
+ notifications.push({
100
+ name: ednKeywordToString(notification.at(edn.kw(':name'))),
101
+ on: ednKeywordToString(notification.at(edn.kw(':on'))),
102
+ to: ednKeywordToString(notification.at(edn.kw(':to'))),
103
+ template: ednKeywordToString(notification.at(edn.kw(':template'))),
104
+ });
105
+ }
106
+ }
107
+
108
+ return {
109
+ name,
110
+ states,
111
+ transitions,
112
+ notifications,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Serializes a process definition to EDN format
118
+ */
119
+ export function serializeProcess(process: ProcessDefinition): string {
120
+ // For now, return a simplified EDN representation
121
+ // A full implementation would properly serialize to EDN format
122
+ return `{:name :${process.name}
123
+ :states [${process.states.map((s) => `{:name :${s.name} :in [${s.in.map((i) => `:${i}`).join(' ')}] :out [${s.out.map((o) => `:${o}`).join(' ')}]}`).join('\n ')}]
124
+ :transitions [${process.transitions.map((t) => `{:name :${t.name} :from :${t.from} :to :${t.to} :actor :${t.actor}}`).join('\n ')}]
125
+ :notifications [${process.notifications.map((n) => `{:name :${n.name} :on :${n.on} :to :${n.to} :template :${n.template}}`).join('\n ')}]}`;
126
+ }