dxfl 0.1.9 → 0.1.11

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # v0.1.11
2
+
3
+ - Lower files upload concurrency (from 50 to 20) to avoid IO issues
4
+
5
+ # v0.1.10
6
+
7
+ - new `logout` command: removes dxfl config folder containing the credentials
8
+ - rename authentification config folder from "dfl" to "dxfl", for consistency and clarity
9
+ - `deuxfleurs.toml`: make "error.html" the default `error_page`'s value
10
+ - `deploy`/`empty` commands can now be used without being logged, by passing S3 id and secret keys as enviromnent variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` respectively)
11
+ - `deuxfleurs.toml` can now configure cross-origin requests
12
+
1
13
  # v0.1.9
2
14
 
3
15
  - `deuxfleurs.toml`: rename redirection property `if_error = 404` to `force = true`, to simplify and improve comprehension.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install -g dxfl
12
12
 
13
13
  _Warning: software still in an experimental state_
14
14
 
15
- Start by login with your username, for example for `john`:
15
+ Start by logging in with your username, for example for `john`:
16
16
 
17
17
  ```
18
18
  dxfl login john
@@ -60,8 +60,8 @@ Your `deuxfleurs.toml` should follow the following structure:
60
60
  # Default value: "index.html"
61
61
  index_page = "index.html"
62
62
  # Path to the file to serve in case of 404 error.
63
- # Default value: none
64
- error_page = "404.html"
63
+ # Default value: "error.html"
64
+ error_page = "error.html"
65
65
 
66
66
  # A redirect entry. There can be as many as desired.
67
67
  [[redirects]]
@@ -90,7 +90,57 @@ status = 301
90
90
  # applies the redirection.
91
91
  # Default value: false
92
92
  force = true
93
- ```
93
+
94
+ # Configuration to allow cross-origin requests.
95
+ # Multiple rules can be specified and will be matched in order.
96
+ # Each rule is defined in its own `[[cors]]` block
97
+ [[cors]]
98
+ # Origins that you want to allow cross-origin requests from.
99
+ allowed_origins = "https://www.example.com"
100
+ # Possible values:
101
+ # - allowed_origins = "*" : wildcard to allow all origins
102
+ # - allowed_origins = "https://www.example.com" : string of origin (protocol+domain)
103
+ # - allowed_origins = ["https://example1.com", "https://example2.com"] : array of origins
104
+ # Required parameter.
105
+
106
+ # HTTP methods allowed in a cross-origin request (in response to a preflight request).
107
+ allowed_method = "GET"
108
+ # Possible values:
109
+ # - allowed_method = "*" : wild card to allow all methods (only GET/HEAD are currently supported by the server)
110
+ # - allowed_method = "GET" : string of http method ("GET" or "HEAD")
111
+ # - allowed_method = ["GET", "HEAD"] : array of string http methods
112
+ # Optional parameter. Default value: ["GET", "HEAD"]
113
+
114
+ # Request headers allowed in a preflight request.
115
+ allowed_header = "Authorization"
116
+ # Possible values:
117
+ # - allowed_header = "*" : wildcard to allow all headers
118
+ # - allowed_header = "Authorization" : string of http header
119
+ # - allowed_header = ["Authorization", "Age"] : array of string http header
120
+ # Optional parameter. Default value : []
121
+
122
+ # Response headers you want to make available to JavaScript in response to a cross-origin request.
123
+ expose_header = "Content-Encoding"
124
+ # Possible values:
125
+ # - expose_header = "Content-Encoding" : string of http header
126
+ # - expose_header = ["Content-Encoding", "Kuma-Revision"] : array of string http header
127
+ # Optional Parameter. Default value : []
128
+ ```
129
+
130
+ ## Using dxfl in automated deployments
131
+
132
+ `dxfl login` is designed for interactive use, where you type the password in
133
+ your local computer. For automated deployments (e.g. deploying your website in a
134
+ continuous integration workflows), you can instead directly call `dxfl deploy`
135
+ (or `dxfl empty`) and pass website credentials using environment variables:
136
+
137
+ - `AWS_ACCESS_KEY_ID` for the S3 key id;
138
+ - `AWS_SECRET_ACCESS_KEY` for the S3 secret key.
139
+
140
+ The values for these variables can be found in the Guichet web interface for
141
+ your website in the "S3" tab.
142
+
143
+ Config examples for [Woodpecker CI](doc/workflows/woodpecker.yml) and [GitHub Actions](doc/workflows/gh-actions.yml) are also available to help you get started.
94
144
 
95
145
  ## Development
96
146
 
package/dist/auth.js CHANGED
@@ -11,16 +11,62 @@ import { Configuration, WebsiteApi } from "guichet-sdk-ts";
11
11
  import { read } from "read";
12
12
  import path from "node:path";
13
13
  import fs from "node:fs/promises";
14
+ import { confirmationPrompt } from "./utils.js";
14
15
  function configPath() {
15
- let path = ".dfl/config.json";
16
+ let path = ".dxfl/config.json";
16
17
  if (process.env.XDG_CONFIG_HOME) {
17
- path = process.env.XDG_CONFIG_HOME + "/dfl/config.json";
18
+ path = process.env.XDG_CONFIG_HOME + "/dxfl/config.json";
18
19
  }
19
20
  else if (process.env.HOME) {
20
- path = process.env.HOME + "/.config/dfl/config.json";
21
+ path = process.env.HOME + "/.config/dxfl/config.json";
21
22
  }
22
23
  return path;
23
24
  }
25
+ // Rename old config folder (from dfl to dxfl)
26
+ function moveOldConfig() {
27
+ return __awaiter(this, void 0, void 0, function* () {
28
+ function oldConfigPath() {
29
+ let path = ".dfl/config.json";
30
+ if (process.env.XDG_CONFIG_HOME) {
31
+ path = process.env.XDG_CONFIG_HOME + "/dfl/config.json";
32
+ }
33
+ else if (process.env.HOME) {
34
+ path = process.env.HOME + "/.config/dfl/config.json";
35
+ }
36
+ return path;
37
+ }
38
+ const oldConfigFile = oldConfigPath();
39
+ try {
40
+ yield fs.access(oldConfigFile);
41
+ try {
42
+ const newConfigFile = configPath();
43
+ const oldConfigParent = path.dirname(oldConfigFile);
44
+ const newConfigParent = path.dirname(newConfigFile);
45
+ yield fs.mkdir(newConfigParent, { recursive: true });
46
+ yield fs.rename(oldConfigFile, newConfigFile);
47
+ yield fs.rmdir(oldConfigParent);
48
+ }
49
+ catch (err) {
50
+ console.error(err);
51
+ process.exit(1);
52
+ }
53
+ }
54
+ catch (e) {
55
+ // Doing nothing if no old config
56
+ }
57
+ });
58
+ }
59
+ export function apiConfExists() {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ try {
62
+ yield fs.access(configPath());
63
+ return true;
64
+ }
65
+ catch (_) {
66
+ return false;
67
+ }
68
+ });
69
+ }
24
70
  export function openApiConf() {
25
71
  return __awaiter(this, void 0, void 0, function* () {
26
72
  let strConf;
@@ -29,9 +75,15 @@ export function openApiConf() {
29
75
  try {
30
76
  strConf = yield fs.readFile(configFile, { encoding: "utf8" });
31
77
  }
32
- catch (err) {
33
- console.error(err, `\n\nUnable to read ${configFile}, run 'dfl login' first.`);
34
- process.exit(1);
78
+ catch (e) {
79
+ try {
80
+ yield moveOldConfig(); // Check for old config folder to rename
81
+ strConf = yield fs.readFile(configFile, { encoding: "utf8" });
82
+ }
83
+ catch (err) {
84
+ console.error(err, `\n\nUnable to read ${configFile}, run 'dxfl login' first.`);
85
+ process.exit(1);
86
+ }
35
87
  }
36
88
  try {
37
89
  dictConf = JSON.parse(strConf);
@@ -79,3 +131,39 @@ export function login(username) {
79
131
  console.log("ok");
80
132
  });
81
133
  }
134
+ export function logout(options) {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ const configFile = configPath();
137
+ const parentFolder = path.dirname(configFile);
138
+ // Check if config file exist
139
+ try {
140
+ yield fs.access(configFile);
141
+ }
142
+ catch (err) {
143
+ console.error(err, `\n\nUnable to find ${configFile}. Are you logged on this device?`);
144
+ process.exit(1);
145
+ }
146
+ // If not --yes: ask for confirmation before proceeding
147
+ if (!options.yes) {
148
+ process.stdout.write(`Are you sure to clean dxfl's config folder? (${parentFolder})\n`);
149
+ const ok = yield confirmationPrompt(() => {
150
+ process.stdout.write("Details of planned operations:\n");
151
+ process.stdout.write(` Delete file ${configFile}\n`);
152
+ process.stdout.write(` Delete folder ${parentFolder}\n`);
153
+ });
154
+ if (!ok) {
155
+ return;
156
+ }
157
+ }
158
+ try {
159
+ yield fs.rm(configFile);
160
+ yield fs.rm(parentFolder, { recursive: true });
161
+ process.stdout.write("Config successfully cleaned up!\n");
162
+ process.stdout.write("You can now go outside, feel the sun and touch grass, you are free! ☀️\n");
163
+ }
164
+ catch (err) {
165
+ console.error(err, `\n\nUnable to cleanup config folder. Do you have the permission to delete ${parentFolder}?`);
166
+ process.exit(1);
167
+ }
168
+ });
169
+ }
package/dist/bucket.js CHANGED
@@ -9,36 +9,68 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import fs from "fs";
11
11
  import mime from "mime";
12
+ import { WebsiteApi } from "guichet-sdk-ts";
12
13
  import { DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
13
14
  import { PromisePool } from "@supercharge/promise-pool";
15
+ import { apiConfExists, openApiConf } from "./auth.js";
14
16
  import { parseEtag, toChunks, formatBytesHuman } from "./utils.js";
15
- export function getBucket(api, vhost) {
17
+ export function getBucketCredentials(name) {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ if (yield apiConfExists()) {
20
+ const api = new WebsiteApi(yield openApiConf());
21
+ return yield credentialsFromApi(api, name);
22
+ }
23
+ else {
24
+ const creds = credentialsFromEnv();
25
+ if (creds === undefined) {
26
+ throw new Error("Failed to load credentials.\n" +
27
+ "You need to run 'dxfl login', " +
28
+ "or define the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.");
29
+ }
30
+ return creds;
31
+ }
32
+ });
33
+ }
34
+ function credentialsFromEnv() {
35
+ if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
36
+ return {
37
+ key: process.env.AWS_ACCESS_KEY_ID,
38
+ secret: process.env.AWS_SECRET_ACCESS_KEY,
39
+ };
40
+ }
41
+ else {
42
+ return undefined;
43
+ }
44
+ }
45
+ function credentialsFromApi(api, website) {
16
46
  return __awaiter(this, void 0, void 0, function* () {
17
47
  // Get website info from guichet (bucket name and keys)
18
- const bucket = yield api.getWebsite({ vhost }).catch(err => {
48
+ const bucket = yield api.getWebsite({ vhost: website }).catch(err => {
19
49
  var _a;
20
50
  if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) == 404) {
21
- console.error(`Error: website '${vhost}' does not exist`);
51
+ console.error(`Error: website '${website}' does not exist`);
22
52
  }
23
53
  else {
24
54
  console.error(err);
25
55
  }
26
56
  throw new Error(err);
27
57
  });
58
+ return { key: bucket.accessKeyId, secret: bucket.secretAccessKey };
59
+ });
60
+ }
61
+ export function getBucket(name, creds) {
62
+ return __awaiter(this, void 0, void 0, function* () {
28
63
  // Initialize a S3 Client
29
64
  const client = new S3Client({
30
65
  endpoint: "https://garage.deuxfleurs.fr",
31
66
  region: "garage",
32
67
  forcePathStyle: true,
33
68
  credentials: {
34
- accessKeyId: bucket.accessKeyId,
35
- secretAccessKey: bucket.secretAccessKey,
69
+ accessKeyId: creds.key,
70
+ secretAccessKey: creds.secret,
36
71
  },
37
72
  });
38
- return {
39
- client: client,
40
- name: bucket.vhost.name,
41
- };
73
+ return { client, name };
42
74
  });
43
75
  }
44
76
  export function getBucketFiles(bucket) {
package/dist/deploy.js CHANGED
@@ -9,12 +9,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import fs from "fs";
11
11
  import path from "path";
12
- import { WebsiteApi } from "guichet-sdk-ts";
13
12
  import { PromisePool } from "@supercharge/promise-pool";
14
- import { openApiConf } from "./auth.js";
15
- import { deleteBucketFile, deleteBucketFiles, getBucket, getBucketFiles, putEmptyObjectRedirect, uploadFile, } from "./bucket.js";
13
+ import { deleteBucketFile, deleteBucketFiles, getBucketCredentials, getBucket, getBucketFiles, putEmptyObjectRedirect, uploadFile, } from "./bucket.js";
16
14
  import { confirmationPrompt, filterMap, formatBytesHuman, formatCount, getFileMd5, sum, } from "./utils.js";
17
- import { equalBucketRedirect, getBucketConfig, putBucketWebsiteConfig, readConfigFile, } from "./website_config.js";
15
+ import { equalBucketRedirect, equalCorsRules, getBucketConfig, putBucketWebsiteConfig, putCorsRules, readConfigFile, } from "./website_config.js";
18
16
  // Walks through the local directory at path `dir`, and for each file it contains, returns :
19
17
  // - `localPath`: its path on the local filesystem (includes `dir`). On windows, this path
20
18
  // will typically use `\` as separator.
@@ -134,6 +132,10 @@ function computeDeployPlan(localFiles, remoteFiles, localCfg, remoteCfg) {
134
132
  from: remoteCfg.bucket_redirects,
135
133
  to: localCfg.bucket_redirects,
136
134
  },
135
+ cors_rules: {
136
+ from: remoteCfg.cors_rules,
137
+ to: localCfg.cors_rules,
138
+ },
137
139
  };
138
140
  }
139
141
  function diffBucketRedirects(from, to) {
@@ -184,6 +186,8 @@ function printPlan(plan, details) {
184
186
  const oredirects_added = plan.redirects.filter(r => r.action == "add");
185
187
  const oredirects_updated = plan.redirects.filter(r => r.action == "update");
186
188
  const oredirects_deleted = plan.redirects.filter(r => r.action == "delete");
189
+ // check whether CORS rules changed
190
+ const cors_changed = !equalCorsRules(plan.cors_rules.from, plan.cors_rules.to);
187
191
  // print
188
192
  if (details == "summary") {
189
193
  const sizeRemote = sum([...plan.remoteFiles.values()].map(f => { var _a; return (_a = f.size) !== null && _a !== void 0 ? _a : 0; }));
@@ -211,6 +215,9 @@ function printPlan(plan, details) {
211
215
  if (sentence != "") {
212
216
  process.stdout.write(` ${sentence}\n`);
213
217
  }
218
+ if (cors_changed) {
219
+ process.stdout.write(" CORS rules modified\n");
220
+ }
214
221
  process.stdout.write(` size: ${formatBytesHuman(sizeRemote)} -> ${formatBytesHuman(sizeLocal)}\n`);
215
222
  }
216
223
  else {
@@ -239,6 +246,20 @@ function printPlan(plan, details) {
239
246
  for (const { source, prev_target } of oredirects_deleted) {
240
247
  process.stdout.write(` Delete redirect ${source} -> ${prev_target}\n`);
241
248
  }
249
+ if (cors_changed) {
250
+ process.stdout.write(" Set CORS rules:\n");
251
+ for (const rule of plan.cors_rules.to) {
252
+ function pp(a) {
253
+ return a.length == 1
254
+ ? `"${a[0]}"`
255
+ : `[${a.map(s => `"${s}"`).join(", ")}]`;
256
+ }
257
+ process.stdout.write(` - allowed_origins = ${pp(rule.allowed_origins)}\n`);
258
+ process.stdout.write(` allowed_methods = ${pp(rule.allowed_methods)}\n`);
259
+ process.stdout.write(` allowed_headers = ${pp(rule.allowed_headers)}\n`);
260
+ process.stdout.write(` expose_headers = ${pp(rule.expose_headers)}\n`);
261
+ }
262
+ }
242
263
  }
243
264
  // print other (index, error page)
244
265
  function s(s) {
@@ -279,7 +300,7 @@ function applyDeployPlan(bucket, plan) {
279
300
  }));
280
301
  // Upload files
281
302
  yield PromisePool.for(plan.filesToUpload)
282
- .withConcurrency(50)
303
+ .withConcurrency(20)
283
304
  .handleError(err => {
284
305
  throw err;
285
306
  })
@@ -289,6 +310,10 @@ function applyDeployPlan(bucket, plan) {
289
310
  }));
290
311
  // Apply bucket redirects & global config
291
312
  yield putBucketWebsiteConfig(bucket, plan.index_page.to, plan.error_page.to, plan.bucket_redirects.to);
313
+ // Apply CORS rules
314
+ if (!equalCorsRules(plan.cors_rules.from, plan.cors_rules.to)) {
315
+ yield putCorsRules(bucket, plan.cors_rules.to);
316
+ }
292
317
  });
293
318
  }
294
319
  function deployMain(website, localFolder, options) {
@@ -308,9 +333,7 @@ function deployMain(website, localFolder, options) {
308
333
  getLocalFilesWithInfo(localFolder),
309
334
  // Get the bucket, list of files stored in the bucket, and bucket website config
310
335
  (() => __awaiter(this, void 0, void 0, function* () {
311
- const guichet = yield openApiConf();
312
- const api = new WebsiteApi(guichet);
313
- const bucket = yield getBucket(api, website);
336
+ const bucket = yield getBucket(website, yield getBucketCredentials(website));
314
337
  const remoteFiles = yield getBucketFiles(bucket);
315
338
  // This can be slow because it needs to query each object in the bucket.
316
339
  const remoteWebsiteConfig = yield getBucketConfig(bucket, [
@@ -360,7 +383,6 @@ export function deploy(website, localFolder, options) {
360
383
  console.error(`Error: ${err}`);
361
384
  }
362
385
  else {
363
- console.error("Error:");
364
386
  console.error(err);
365
387
  }
366
388
  process.exit(1);
package/dist/empty.js CHANGED
@@ -7,9 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { WebsiteApi } from "guichet-sdk-ts";
11
- import { openApiConf } from "./auth.js";
12
- import { getBucket, getBucketFiles, deleteBucketFiles } from "./bucket.js";
10
+ import { getBucket, getBucketCredentials, getBucketFiles, deleteBucketFiles, } from "./bucket.js";
13
11
  import { confirmationPrompt, formatBytesHuman, formatCount, sum, } from "./utils.js";
14
12
  function emptyMain(website, options) {
15
13
  return __awaiter(this, void 0, void 0, function* () {
@@ -17,9 +15,7 @@ function emptyMain(website, options) {
17
15
  console.error("Error: options --yes and --dry-run cannot be passed at the same time");
18
16
  }
19
17
  process.stdout.write("Fetching the website configuration and metadata...\n");
20
- const guichet = yield openApiConf();
21
- const api = new WebsiteApi(guichet);
22
- const bucket = yield getBucket(api, website);
18
+ const bucket = yield getBucket(website, yield getBucketCredentials(website));
23
19
  const filesToDelete = [...(yield getBucketFiles(bucket))].map(([name, { size }]) => ({
24
20
  name,
25
21
  size,
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from "@commander-js/extra-typings";
3
- import { login } from "./auth.js";
3
+ import { login, logout } from "./auth.js";
4
4
  import { deploy } from "./deploy.js";
5
5
  import { empty } from "./empty.js";
6
6
  import { vhostsList } from "./vhosts.js";
7
- program.name("dxfl").description("Deuxfleurs CLI tool").version("0.1.9");
7
+ program.name("dxfl").description("Deuxfleurs CLI tool").version("0.1.11");
8
8
  program
9
9
  .command("login")
10
10
  .description("Link your Deuxfleurs account with this tool.")
@@ -29,4 +29,9 @@ program
29
29
  .option("-n, --dry-run", "do a trial run without making actual changes")
30
30
  .option("-y, --yes", "apply the changes without asking for confirmation")
31
31
  .action(empty);
32
+ program
33
+ .command("logout")
34
+ .description("Cleanup dxfl config containing your credentials")
35
+ .option("-y, --yes", "clean without asking for confirmation")
36
+ .action(logout);
32
37
  program.parse();
@@ -12,7 +12,7 @@ import TOML from "smol-toml";
12
12
  import URI from "fast-uri";
13
13
  import { z as zod } from "zod";
14
14
  import { fromError as zodError } from "zod-validation-error";
15
- import { GetBucketWebsiteCommand, PutBucketWebsiteCommand, } from "@aws-sdk/client-s3";
15
+ import { GetBucketCorsCommand, GetBucketWebsiteCommand, PutBucketWebsiteCommand, PutBucketCorsCommand, } from "@aws-sdk/client-s3";
16
16
  import { getBucketFilesDetails } from "./bucket.js";
17
17
  ////////////// Utilities
18
18
  export function equalBucketRedirect(b1, b2) {
@@ -34,6 +34,21 @@ export function equalBucketRedirect(b1, b2) {
34
34
  b1.protocol === b2.protocol &&
35
35
  equalTo(b1.to, b2.to));
36
36
  }
37
+ export function equalCorsRules(c1, c2) {
38
+ function eqArr(eqT, x, y) {
39
+ return x.length === y.length && x.every((v, i) => eqT(y[i], v));
40
+ }
41
+ function eqString(s1, s2) {
42
+ return s1 == s2;
43
+ }
44
+ function eqRule(c1, c2) {
45
+ return (eqArr(eqString, c1.allowed_origins, c2.allowed_origins) &&
46
+ eqArr(eqString, c1.allowed_headers, c2.allowed_headers) &&
47
+ eqArr(eqString, c1.allowed_methods, c2.allowed_methods) &&
48
+ eqArr(eqString, c1.expose_headers, c2.expose_headers));
49
+ }
50
+ return eqArr(eqRule, c1, c2);
51
+ }
37
52
  ////////////// Parsing from a TOML config file
38
53
  // Parsing: TOML -> untyped object
39
54
  function readConfigFileObject(filename) {
@@ -67,10 +82,17 @@ const RawRedirectSchema = zod.object({
67
82
  force: zod.boolean().optional(),
68
83
  status: zod.number().int().positive().optional(),
69
84
  });
85
+ const RawCorsSchema = zod.object({
86
+ allowed_origins: zod.union([zod.string(), zod.string().array()]),
87
+ allowed_methods: zod.union([zod.string(), zod.string().array()]).optional(),
88
+ allowed_headers: zod.union([zod.string(), zod.string().array()]).optional(),
89
+ expose_headers: zod.union([zod.string(), zod.string().array()]).optional(),
90
+ });
70
91
  const RawConfigSchema = zod.object({
71
92
  index_page: zod.string().optional(),
72
93
  error_page: zod.string().optional(),
73
94
  redirects: zod.array(RawRedirectSchema).optional(),
95
+ cors: zod.array(RawCorsSchema).optional(),
74
96
  });
75
97
  function interpRawConfig(cfg) {
76
98
  try {
@@ -114,7 +136,7 @@ function unescape(s) {
114
136
  }
115
137
  }
116
138
  function interpConfig(rawcfg) {
117
- var _a, _b;
139
+ var _a, _b, _c, _d, _e;
118
140
  function interpPath(name, str) {
119
141
  try {
120
142
  return unescape(str);
@@ -228,14 +250,29 @@ function interpConfig(rawcfg) {
228
250
  }
229
251
  return redirect;
230
252
  }
253
+ // Default handling for an optional string:
254
+ // undefined -> def
255
+ // "" -> undefined
256
+ // other -> other.
257
+ //
258
+ // `""` is "disable this", while `undefined` is "use the default".
259
+ // This works as long as the empty string is not a valid value for `s`.
260
+ let withDefault = (s, def) => {
261
+ if (s === "") {
262
+ return undefined;
263
+ }
264
+ return s !== null && s !== void 0 ? s : def;
265
+ };
231
266
  let cfg = {
232
267
  // use index.html as default index if not specified explicitly
233
- index_page: (_a = rawcfg.index_page) !== null && _a !== void 0 ? _a : "index.html",
234
- error_page: rawcfg.error_page,
268
+ index_page: withDefault(rawcfg.index_page, "index.html"),
269
+ // use error.html as default error page
270
+ error_page: withDefault(rawcfg.error_page, "error.html"),
235
271
  bucket_redirects: [],
236
272
  object_redirects: new Map(),
273
+ cors_rules: [],
237
274
  };
238
- for (const [i, raw] of ((_b = rawcfg.redirects) !== null && _b !== void 0 ? _b : []).entries()) {
275
+ for (const [i, raw] of ((_a = rawcfg.redirects) !== null && _a !== void 0 ? _a : []).entries()) {
239
276
  // `i+1` is only used for display: start counting redirects from 1 instead of 0
240
277
  const r = interpRedirect(i + 1, raw);
241
278
  if (r.kind == "bucket") {
@@ -248,6 +285,17 @@ function interpConfig(rawcfg) {
248
285
  cfg.object_redirects.set(r.from, r.to);
249
286
  }
250
287
  }
288
+ for (const [_, raw] of ((_b = rawcfg.cors) !== null && _b !== void 0 ? _b : []).entries()) {
289
+ const interpRawArray = (x) => {
290
+ return typeof x === "string" ? [x] : x;
291
+ };
292
+ cfg.cors_rules.push({
293
+ allowed_origins: interpRawArray(raw.allowed_origins),
294
+ allowed_methods: interpRawArray((_c = raw.allowed_methods) !== null && _c !== void 0 ? _c : ["GET", "HEAD"]),
295
+ allowed_headers: interpRawArray((_d = raw.allowed_methods) !== null && _d !== void 0 ? _d : []),
296
+ expose_headers: interpRawArray((_e = raw.expose_headers) !== null && _e !== void 0 ? _e : []),
297
+ });
298
+ }
251
299
  return cfg;
252
300
  }
253
301
  export function readConfigFile(filename) {
@@ -260,16 +308,27 @@ export function getBucketConfig(bucket, files) {
260
308
  return __awaiter(this, void 0, void 0, function* () {
261
309
  // This function performs the following S3 requests:
262
310
  // - GetBucketWebsite to get bucket-level redirects;
311
+ // - GetBucketCors to get the bucket CORS configuration;
263
312
  // - A Head command for each file in the bucket to collect object redirects.
264
313
  // (This can become relatively slow for buckets with thousands of files,
265
314
  // but I don't know of a better way.)
266
- var _a, _b;
267
- const [response, details] = yield Promise.all([
315
+ var _a, _b, _c, _d, _e, _f;
316
+ const [website, cors, details] = yield Promise.all([
268
317
  bucket.client.send(new GetBucketWebsiteCommand({ Bucket: bucket.name })),
318
+ bucket.client.send(new GetBucketCorsCommand({ Bucket: bucket.name })),
269
319
  getBucketFilesDetails(bucket, files),
270
320
  ]);
271
- if (response && response.$metadata.httpStatusCode != 200) {
272
- throw `Error sending GetBucketWebsite: ${response}`;
321
+ if (website && website.$metadata.httpStatusCode != 200) {
322
+ throw `Error sending GetBucketWebsite: ${JSON.stringify(website)}`;
323
+ }
324
+ if (!cors ||
325
+ (cors.$metadata.httpStatusCode != 200 &&
326
+ cors.$metadata.httpStatusCode != 204)) {
327
+ throw `Error sending GetBucketCors: ${JSON.stringify(cors)}`;
328
+ }
329
+ // 204 "No Content" is returned when there are no existing CORS rules
330
+ if (cors.$metadata.httpStatusCode == 204) {
331
+ cors.CORSRules = [];
273
332
  }
274
333
  // Collect object redirects
275
334
  const object_redirects = new Map();
@@ -279,16 +338,16 @@ export function getBucketConfig(bucket, files) {
279
338
  }
280
339
  }
281
340
  // Interpret bucket redirects
282
- if (response.RedirectAllRequestsTo) {
341
+ if (website.RedirectAllRequestsTo) {
283
342
  // NB: garage does not currently support RedirectAllRequestsTo so this should never happen
284
343
  throw (`remote website configuration: RedirectAllRequestsTo is specified; ` +
285
344
  `this is currently unsupported by dxfl`);
286
345
  }
287
- const index_page = (_a = response.IndexDocument) === null || _a === void 0 ? void 0 : _a.Suffix;
288
- const error_page = (_b = response.ErrorDocument) === null || _b === void 0 ? void 0 : _b.Key;
346
+ const index_page = (_a = website.IndexDocument) === null || _a === void 0 ? void 0 : _a.Suffix;
347
+ const error_page = (_b = website.ErrorDocument) === null || _b === void 0 ? void 0 : _b.Key;
289
348
  let bucket_redirects = [];
290
- if (response.RoutingRules) {
291
- for (const rule of response.RoutingRules) {
349
+ if (website.RoutingRules) {
350
+ for (const rule of website.RoutingRules) {
292
351
  // If no Condition is specified, then the redirect always applies, which is equivalent
293
352
  // to matching on an empty prefix
294
353
  let prefix = "";
@@ -350,7 +409,25 @@ export function getBucketConfig(bucket, files) {
350
409
  });
351
410
  }
352
411
  }
353
- return { index_page, error_page, bucket_redirects, object_redirects };
412
+ // Interpret CORS rules
413
+ let cors_rules = [];
414
+ if (cors.CORSRules) {
415
+ for (const rule of cors.CORSRules) {
416
+ cors_rules.push({
417
+ allowed_origins: (_c = rule.AllowedOrigins) !== null && _c !== void 0 ? _c : [],
418
+ allowed_methods: (_d = rule.AllowedMethods) !== null && _d !== void 0 ? _d : [],
419
+ allowed_headers: (_e = rule.AllowedHeaders) !== null && _e !== void 0 ? _e : [],
420
+ expose_headers: (_f = rule.ExposeHeaders) !== null && _f !== void 0 ? _f : [],
421
+ });
422
+ }
423
+ }
424
+ return {
425
+ index_page,
426
+ error_page,
427
+ bucket_redirects,
428
+ object_redirects,
429
+ cors_rules,
430
+ };
354
431
  });
355
432
  }
356
433
  ////////////// Applying a configuration to S3
@@ -404,3 +481,24 @@ export function putBucketWebsiteConfig(bucket, index_page, error_page, bucket_re
404
481
  }
405
482
  });
406
483
  }
484
+ // applies CORS rules
485
+ export function putCorsRules(bucket, cors_rules) {
486
+ return __awaiter(this, void 0, void 0, function* () {
487
+ let CORSRules = [];
488
+ for (const rule of cors_rules) {
489
+ CORSRules.push({
490
+ AllowedOrigins: rule.allowed_origins,
491
+ AllowedMethods: rule.allowed_methods,
492
+ AllowedHeaders: rule.allowed_headers,
493
+ ExposeHeaders: rule.expose_headers,
494
+ });
495
+ }
496
+ const resp = yield bucket.client.send(new PutBucketCorsCommand({
497
+ Bucket: bucket.name,
498
+ CORSConfiguration: { CORSRules },
499
+ }));
500
+ if (resp && resp.$metadata.httpStatusCode != 200) {
501
+ throw resp;
502
+ }
503
+ });
504
+ }
@@ -0,0 +1,19 @@
1
+ name: Deploy with GitHub Actions
2
+
3
+ # You will need to:
4
+ # 1. Get (from Guichet web interface):
5
+ # - Your access keys (ID and secret): in the "S3" tab of your website
6
+ # - The website id: your sub-domain (the part before .web.deuxfleurs.fr) or your full custom domain (e.g. your-domain.com)
7
+ # 2. Setup the access keys as secrets in GitHub web interface (https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/using-secrets-in-github-actions), named in this example key_id and key_secret
8
+ # 3. If you use a deuxfleurs.toml config, be sure the dxfl deploy command is launch in the current directory its in.
9
+
10
+ jobs:
11
+ build:
12
+ # Build your website in a dedicated step to avoid to expose your access keys to external software
13
+ deploy:
14
+ runs-on: node:lts-alpine
15
+ steps:
16
+ - run: npx --yes dxfl deploy your-website-id build/ --yes
17
+ env:
18
+ AWS_ACCESS_KEY_ID: ${{ secrets.key_id }}
19
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.key_secret }}
@@ -0,0 +1,21 @@
1
+ ## Deploy with Woodpecker CI
2
+ #
3
+ # You will need to:
4
+ # 1. Get (from Guichet web interface):
5
+ # - Your access keys (ID and secret): in the "S3" tab of your website
6
+ # - The website id: your sub-domain (the part before .web.deuxfleurs.fr) or your full custom domain (e.g. your-domain.com)
7
+ # 2. Setup the access keys as secrets in Woodpecker web interface (https://woodpecker-ci.org/docs/usage/secrets), named in this example key_id and key_secret
8
+ # 3. If you use a deuxfleurs.toml config, be sure the dxfl deploy command is launch in the current directory its in.
9
+
10
+ steps:
11
+ - name: build
12
+ # Build your website in a dedicated step to avoid to expose your access keys to external software
13
+ - name: deploy
14
+ image: node:lts-alpine
15
+ environment:
16
+ AWS_ACCESS_KEY_ID:
17
+ from_secret: key_id
18
+ AWS_SECRET_ACCESS_KEY:
19
+ from_secret: key_secret
20
+ commands:
21
+ - npx --yes dxfl deploy your-website-id build/ --yes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dxfl",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Deuxfleurs Team <coucou@deuxfleurs.fr>",