@ystemsrx/cfshare 0.1.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.
package/src/policy.ts ADDED
@@ -0,0 +1,175 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import ignore from "ignore";
4
+ import type { CfsharePluginConfig, CfsharePolicy } from "./types.js";
5
+
6
+ export const DEFAULT_POLICY: CfsharePolicy = {
7
+ defaultTtlSeconds: 3600,
8
+ maxTtlSeconds: 86400,
9
+ defaultExposePortAccess: "token",
10
+ defaultExposeFilesAccess: "none",
11
+ blockedPorts: [22, 2375, 2376],
12
+ allowedPathRoots: [],
13
+ tunnel: {
14
+ edgeIpVersion: "4",
15
+ protocol: "http2",
16
+ },
17
+ rateLimit: {
18
+ enabled: true,
19
+ windowMs: 60_000,
20
+ maxRequests: 240,
21
+ },
22
+ };
23
+
24
+ export type LoadedPolicy = {
25
+ effective: CfsharePolicy;
26
+ warnings: string[];
27
+ matcher: ReturnType<typeof ignore>;
28
+ };
29
+
30
+ function isAccessMode(value: unknown): value is CfsharePolicy["defaultExposePortAccess"] {
31
+ return value === "token" || value === "basic" || value === "none";
32
+ }
33
+
34
+ function normalizeAccess(
35
+ value: unknown,
36
+ fallback: CfsharePolicy["defaultExposePortAccess"],
37
+ ): CfsharePolicy["defaultExposePortAccess"] {
38
+ return isAccessMode(value) ? value : fallback;
39
+ }
40
+
41
+ function asPortArray(input: unknown): number[] | undefined {
42
+ if (!Array.isArray(input)) {
43
+ return undefined;
44
+ }
45
+ const ports = input
46
+ .map((value) => (typeof value === "number" ? Math.trunc(value) : Number.NaN))
47
+ .filter((value) => Number.isInteger(value) && value > 0 && value <= 65535);
48
+ return Array.from(new Set(ports));
49
+ }
50
+
51
+ function asStringArray(input: unknown): string[] | undefined {
52
+ if (!Array.isArray(input)) {
53
+ return undefined;
54
+ }
55
+ return input
56
+ .filter((entry): entry is string => typeof entry === "string")
57
+ .map((entry) => entry.trim())
58
+ .filter(Boolean);
59
+ }
60
+
61
+ export function mergePolicy(
62
+ defaults: CfsharePolicy,
63
+ pluginConfig: CfsharePluginConfig,
64
+ fileConfig: Record<string, unknown>,
65
+ ): CfsharePolicy {
66
+ const fileTunnel = (fileConfig.tunnel as Record<string, unknown> | undefined) ?? {};
67
+ const fileRateLimit = (fileConfig.rateLimit as Record<string, unknown> | undefined) ?? {};
68
+
69
+ const merged: CfsharePolicy = {
70
+ defaultTtlSeconds:
71
+ typeof fileConfig.defaultTtlSeconds === "number"
72
+ ? fileConfig.defaultTtlSeconds
73
+ : pluginConfig.defaultTtlSeconds ?? defaults.defaultTtlSeconds,
74
+ maxTtlSeconds:
75
+ typeof fileConfig.maxTtlSeconds === "number"
76
+ ? fileConfig.maxTtlSeconds
77
+ : pluginConfig.maxTtlSeconds ?? defaults.maxTtlSeconds,
78
+ defaultExposePortAccess: normalizeAccess(
79
+ fileConfig.defaultExposePortAccess,
80
+ normalizeAccess(pluginConfig.defaultExposePortAccess, defaults.defaultExposePortAccess),
81
+ ),
82
+ defaultExposeFilesAccess: normalizeAccess(
83
+ fileConfig.defaultExposeFilesAccess,
84
+ normalizeAccess(pluginConfig.defaultExposeFilesAccess, defaults.defaultExposeFilesAccess),
85
+ ),
86
+ blockedPorts:
87
+ asPortArray(fileConfig.blockedPorts) ??
88
+ asPortArray(pluginConfig.blockedPorts) ??
89
+ defaults.blockedPorts,
90
+ allowedPathRoots:
91
+ asStringArray(fileConfig.allowedPathRoots) ??
92
+ asStringArray(pluginConfig.allowedPathRoots) ??
93
+ defaults.allowedPathRoots,
94
+ tunnel: {
95
+ ...defaults.tunnel,
96
+ ...(pluginConfig.tunnel ?? {}),
97
+ ...(fileTunnel as Partial<CfsharePolicy["tunnel"]>),
98
+ },
99
+ rateLimit: {
100
+ ...defaults.rateLimit,
101
+ ...(pluginConfig.rateLimit ?? {}),
102
+ ...(fileRateLimit as Partial<CfsharePolicy["rateLimit"]>),
103
+ },
104
+ };
105
+
106
+ merged.defaultTtlSeconds = Math.max(60, Math.trunc(merged.defaultTtlSeconds));
107
+ merged.maxTtlSeconds = Math.max(merged.defaultTtlSeconds, Math.trunc(merged.maxTtlSeconds));
108
+
109
+ const edge = merged.tunnel.edgeIpVersion;
110
+ if (edge !== "4" && edge !== "6" && edge !== "auto") {
111
+ merged.tunnel.edgeIpVersion = defaults.tunnel.edgeIpVersion;
112
+ }
113
+ const protocol = merged.tunnel.protocol;
114
+ if (protocol !== "http2" && protocol !== "quic" && protocol !== "auto") {
115
+ merged.tunnel.protocol = defaults.tunnel.protocol;
116
+ }
117
+
118
+ merged.rateLimit.enabled = merged.rateLimit.enabled !== false;
119
+ merged.rateLimit.windowMs = Math.min(3_600_000, Math.max(1000, Math.trunc(merged.rateLimit.windowMs)));
120
+ merged.rateLimit.maxRequests = Math.min(
121
+ 100_000,
122
+ Math.max(1, Math.trunc(merged.rateLimit.maxRequests)),
123
+ );
124
+
125
+ return merged;
126
+ }
127
+
128
+ export async function loadPolicy(params: {
129
+ policyFile: string;
130
+ ignoreFile: string;
131
+ pluginConfig: CfsharePluginConfig;
132
+ }): Promise<LoadedPolicy> {
133
+ const warnings: string[] = [];
134
+ let fileConfig: Record<string, unknown> = {};
135
+
136
+ try {
137
+ const raw = await fs.readFile(params.policyFile, "utf8");
138
+ const parsed = JSON.parse(raw) as unknown;
139
+ if (parsed && typeof parsed === "object") {
140
+ fileConfig = parsed as Record<string, unknown>;
141
+ } else {
142
+ warnings.push(`policy file is not an object: ${params.policyFile}`);
143
+ }
144
+ } catch (error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") {
147
+ warnings.push(`failed to read policy file (${params.policyFile}): ${message}`);
148
+ }
149
+ }
150
+
151
+ const effective = mergePolicy(DEFAULT_POLICY, params.pluginConfig, fileConfig);
152
+
153
+ const matcher = ignore();
154
+ matcher.add([".git/**", ".openclaw/**"]);
155
+
156
+ try {
157
+ const ignoreText = await fs.readFile(params.ignoreFile, "utf8");
158
+ matcher.add(ignoreText.split(/\r?\n/));
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ warnings.push(`failed to read ignore file (${params.ignoreFile}): ${message}`);
163
+ }
164
+ }
165
+
166
+ const cwdIgnore = path.join(process.cwd(), ".gitignore");
167
+ try {
168
+ const ignoreText = await fs.readFile(cwdIgnore, "utf8");
169
+ matcher.add(ignoreText.split(/\r?\n/));
170
+ } catch {
171
+ // ignore missing cwd .gitignore
172
+ }
173
+
174
+ return { effective, warnings, matcher };
175
+ }
package/src/schemas.ts ADDED
@@ -0,0 +1,255 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { stringEnum } from "openclaw/plugin-sdk";
3
+
4
+ const AccessMode = stringEnum(["token", "basic", "none"] as const, {
5
+ description: "Access mode",
6
+ });
7
+
8
+ const FileMode = stringEnum(["normal", "zip"] as const, {
9
+ description: "File exposure mode",
10
+ });
11
+
12
+ const FilePresentationMode = stringEnum(["download", "preview", "raw"] as const, {
13
+ description: "How files should be served to clients",
14
+ });
15
+
16
+ const ComponentMode = stringEnum(["tunnel", "origin", "all"] as const, {
17
+ description: "Log component",
18
+ });
19
+
20
+ const ExposureType = stringEnum(["port", "files"] as const, {
21
+ description: "Exposure type",
22
+ });
23
+
24
+ const ExposureStatus = stringEnum(["starting", "running", "stopped", "error", "expired"] as const, {
25
+ description: "Exposure status",
26
+ });
27
+
28
+ const ExposureGetField = stringEnum(
29
+ [
30
+ "id",
31
+ "type",
32
+ "status",
33
+ "port",
34
+ "public_url",
35
+ "expires_at",
36
+ "local_url",
37
+ "stats",
38
+ "file_sharing",
39
+ "last_error",
40
+ "manifest",
41
+ "created_at",
42
+ ] as const,
43
+ {
44
+ description: "Fields to return",
45
+ },
46
+ );
47
+
48
+ const MaintenanceAction = stringEnum(["start_guard", "run_gc", "set_policy"] as const, {
49
+ description: "Maintenance action",
50
+ });
51
+
52
+ const PortOptsSchema = Type.Object(
53
+ {
54
+ ttl_seconds: Type.Optional(Type.Number({ minimum: 60, maximum: 604800 })),
55
+ access: Type.Optional(AccessMode),
56
+ protect_origin: Type.Optional(Type.Boolean()),
57
+ allowlist_paths: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 128 })),
58
+ },
59
+ { additionalProperties: false },
60
+ );
61
+
62
+ const FilesOptsSchema = Type.Object(
63
+ {
64
+ mode: Type.Optional(FileMode),
65
+ presentation: Type.Optional(FilePresentationMode),
66
+ ttl_seconds: Type.Optional(Type.Number({ minimum: 60, maximum: 604800 })),
67
+ access: Type.Optional(AccessMode),
68
+ max_downloads: Type.Optional(Type.Number({ minimum: 1, maximum: 1_000_000 })),
69
+ },
70
+ { additionalProperties: false },
71
+ );
72
+
73
+ export const EnvCheckSchema = Type.Object({}, { additionalProperties: false });
74
+
75
+ export const ExposePortSchema = Type.Object(
76
+ {
77
+ port: Type.Number({ minimum: 1, maximum: 65535 }),
78
+ opts: Type.Optional(PortOptsSchema),
79
+ },
80
+ { additionalProperties: false },
81
+ );
82
+
83
+ export const ExposeFilesSchema = Type.Object(
84
+ {
85
+ paths: Type.Array(Type.String(), { minItems: 1, maxItems: 256 }),
86
+ opts: Type.Optional(FilesOptsSchema),
87
+ },
88
+ { additionalProperties: false },
89
+ );
90
+
91
+ export const ExposureListSchema = Type.Object({}, { additionalProperties: false });
92
+
93
+ const ExposureGetOptsSchema = Type.Object(
94
+ {
95
+ probe_public: Type.Optional(Type.Boolean()),
96
+ },
97
+ { additionalProperties: false },
98
+ );
99
+
100
+ const ExposureGetFieldsSchema = Type.Array(ExposureGetField, {
101
+ minItems: 1,
102
+ maxItems: 32,
103
+ uniqueItems: true,
104
+ });
105
+
106
+ const ExposureGetFilterSchema = Type.Object(
107
+ {
108
+ status: Type.Optional(ExposureStatus),
109
+ type: Type.Optional(ExposureType),
110
+ },
111
+ { additionalProperties: false },
112
+ );
113
+
114
+ export const ExposureGetSchema = Type.Union(
115
+ [
116
+ Type.Object(
117
+ {
118
+ id: Type.String({ minLength: 1 }),
119
+ fields: Type.Optional(ExposureGetFieldsSchema),
120
+ opts: Type.Optional(ExposureGetOptsSchema),
121
+ },
122
+ { additionalProperties: false },
123
+ ),
124
+ Type.Object(
125
+ {
126
+ ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 4096 }),
127
+ fields: Type.Optional(ExposureGetFieldsSchema),
128
+ opts: Type.Optional(ExposureGetOptsSchema),
129
+ },
130
+ { additionalProperties: false },
131
+ ),
132
+ Type.Object(
133
+ {
134
+ filter: ExposureGetFilterSchema,
135
+ fields: Type.Optional(ExposureGetFieldsSchema),
136
+ opts: Type.Optional(ExposureGetOptsSchema),
137
+ },
138
+ { additionalProperties: false },
139
+ ),
140
+ ],
141
+ { description: "Get one/many exposures, or query by filter" },
142
+ );
143
+
144
+ const ExposureStopOptsSchema = Type.Object(
145
+ {
146
+ reason: Type.Optional(Type.String()),
147
+ },
148
+ { additionalProperties: false },
149
+ );
150
+
151
+ export const ExposureStopSchema = Type.Union(
152
+ [
153
+ Type.Object(
154
+ {
155
+ id: Type.String({
156
+ minLength: 1,
157
+ description: `Exposure id or "all"`,
158
+ }),
159
+ opts: Type.Optional(ExposureStopOptsSchema),
160
+ },
161
+ { additionalProperties: false },
162
+ ),
163
+ Type.Object(
164
+ {
165
+ ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 4096 }),
166
+ opts: Type.Optional(ExposureStopOptsSchema),
167
+ },
168
+ { additionalProperties: false },
169
+ ),
170
+ ],
171
+ { description: "Stop one/many exposures, or all" },
172
+ );
173
+
174
+ const ExposureLogsOptsSchema = Type.Object(
175
+ {
176
+ lines: Type.Optional(Type.Number({ minimum: 1, maximum: 10_000 })),
177
+ since_seconds: Type.Optional(Type.Number({ minimum: 1, maximum: 365 * 24 * 3600 })),
178
+ component: Type.Optional(ComponentMode),
179
+ },
180
+ { additionalProperties: false },
181
+ );
182
+
183
+ export const ExposureLogsSchema = Type.Union(
184
+ [
185
+ Type.Object(
186
+ {
187
+ id: Type.String({ minLength: 1 }),
188
+ opts: Type.Optional(ExposureLogsOptsSchema),
189
+ },
190
+ { additionalProperties: false },
191
+ ),
192
+ Type.Object(
193
+ {
194
+ ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 4096 }),
195
+ opts: Type.Optional(ExposureLogsOptsSchema),
196
+ },
197
+ { additionalProperties: false },
198
+ ),
199
+ ],
200
+ { description: "Read logs for one/many exposures" },
201
+ );
202
+
203
+ export const MaintenanceSchema = Type.Object(
204
+ {
205
+ action: MaintenanceAction,
206
+ opts: Type.Optional(
207
+ Type.Object(
208
+ {
209
+ policy: Type.Optional(Type.Any({ description: "Policy patch object" })),
210
+ ignore_patterns: Type.Optional(Type.Array(Type.String(), { minItems: 1, maxItems: 4096 })),
211
+ },
212
+ { additionalProperties: false },
213
+ ),
214
+ ),
215
+ },
216
+ { additionalProperties: false },
217
+ );
218
+
219
+ export const AuditQuerySchema = Type.Object(
220
+ {
221
+ filters: Type.Optional(
222
+ Type.Object(
223
+ {
224
+ id: Type.Optional(Type.String()),
225
+ event: Type.Optional(Type.String()),
226
+ type: Type.Optional(ExposureType),
227
+ from_ts: Type.Optional(Type.String({ description: "ISO timestamp" })),
228
+ to_ts: Type.Optional(Type.String({ description: "ISO timestamp" })),
229
+ limit: Type.Optional(Type.Number({ minimum: 1, maximum: 10000 })),
230
+ },
231
+ { additionalProperties: false },
232
+ ),
233
+ ),
234
+ },
235
+ { additionalProperties: false },
236
+ );
237
+
238
+ export const AuditExportSchema = Type.Object(
239
+ {
240
+ range: Type.Optional(
241
+ Type.Object(
242
+ {
243
+ from_ts: Type.Optional(Type.String({ description: "ISO timestamp" })),
244
+ to_ts: Type.Optional(Type.String({ description: "ISO timestamp" })),
245
+ id: Type.Optional(Type.String()),
246
+ event: Type.Optional(Type.String()),
247
+ type: Type.Optional(ExposureType),
248
+ output_path: Type.Optional(Type.String()),
249
+ },
250
+ { additionalProperties: false },
251
+ ),
252
+ ),
253
+ },
254
+ { additionalProperties: false },
255
+ );