picoruby-cloudflare-template 0.1.0.rc1 → 0.1.0.rc2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0514b3333048a221efffa3fa803528f1c2aafac2c14b4f545cfb59484c67ac11
4
- data.tar.gz: b5db10ca44c30093aee69c5675055432e89c2761ed70279d518104408fda9c1b
3
+ metadata.gz: c224b29b75ac222a183233c6af8022623d9530324e9028a76a1056278f07437b
4
+ data.tar.gz: a5199135545e2927b66465fb436e846260e3303587140f498c82d341f0f9ce0d
5
5
  SHA512:
6
- metadata.gz: 7c0531c2e85494b47656f0b621de96bde954776951aa4ede77d8f0ce682ec8328cee3d220ffb6015002e0cbffb933326cc37292e37759b9672d36dd5cc572b77
7
- data.tar.gz: e99ef3c75479ee721aa68698a72366a4d8b278385b5d663505bf8214636f5faced03e5cc5ad1cc1abad5f40d29db31df7fe7ada861ac190e362e9c98461b76f4
6
+ metadata.gz: db0e353a8dc1e2a0fddd8c8d82bd12ee605416d4b3bde95a0b536991bd4b33a9843a5d99364f865e4afc679b57fa2282cc5b1ec3d2fcce7e578b694a05e30cef
7
+ data.tar.gz: fafcf2e5bb8a86065991b0dc388ab9521729ae2a692f458734e824a66ac9a3748c46ab8ff28f6fbb83c87c7eed4caa85c3548c3ba35c5f8337717caac10c1918
data/README.ja.md CHANGED
@@ -23,7 +23,7 @@ emsdkから切り替える場合は、`emsdk_env.sh`を読み込まないシェ
23
23
 
24
24
  ```sh
25
25
  bundle install
26
- bundle exec ruby exe/picoruby-cloudflare new ../my-worker --gem-path "$PWD"
26
+ bundle exec ruby exe/picoruby-cloudflare new ../my-worker --bindings --gem-path "$PWD"
27
27
  cd ../my-worker
28
28
  bundle install
29
29
  npm install
@@ -42,17 +42,23 @@ PicoRubyはsubmodule初期化済みのチェックアウトを指定します。
42
42
  上流のシェルコマンド展開の制限により、ビルド用パスでは空白やシェル特殊文字を拒否します。
43
43
 
44
44
  配布gemを試す場合は `gem build picoruby-cloudflare-template.gemspec`、
45
- `gem install ./picoruby-cloudflare-template-0.1.0.rc1.gem` の後、
45
+ `gem install ./picoruby-cloudflare-template-0.1.0.rc2.gem` の後、
46
46
  `picoruby-cloudflare new my-worker` を使えます。生成先で未公開版を解決するには `bundle install --local` を使用します。
47
- このリリース候補版を公開した後は `gem install picoruby-cloudflare-template --pre --version 0.1.0.rc1` でインストールできます。
48
- `VERSION` とRubyGemsのメタデータ上のバージョンは、どちらも `0.1.0.rc1` です。
47
+ このリリース候補版を公開した後は `gem install picoruby-cloudflare-template --pre --version 0.1.0.rc2` でインストールできます。
48
+ `VERSION` とRubyGemsのメタデータ上のバージョンは、どちらも `0.1.0.rc2` です。
49
49
 
50
50
  ## 生成物とビルド設定
51
51
 
52
- `new PATH [--name NAME] [--gem-path PATH]` はGemfile、Rakefile、build_config.rb、最小Rackアプリのapp.rb、
52
+ `new PATH [--bindings] [--name NAME] [--gem-path PATH]` はGemfile、Rakefile、build_config.rb、最小Rackアプリのapp.rb、
53
53
  src/index.js、package.json、wrangler.jsonc、.gitignore、README.mdを生成します。
54
54
  生成先が存在する場合は空ディレクトリでも上書きしません。Gemfile.lockとpackage-lock.jsonはアプリ側でコミットしてください。
55
55
 
56
+ `--bindings` を付けると、KV・Queue・Accessのユーザー情報取得例をapp.rbとwrangler.jsoncへ追加します。
57
+ `/access` の例には `CF_ACCESS_TEAM` と `CF_Authorization` Cookieが必要です。
58
+ 同時に生成する `.picoruby-cloudflare-template.json` は、この2つの管理対象例のハッシュを記録します。
59
+ 今後対応bindingが増えた版では、`picoruby-cloudflare bindings PROJECT` を実行すると例を再生成できます。
60
+ 再生成は書き込み前に管理対象をすべて検査し、いずれかを編集済みなら何も変更せず停止します。その場合は手動で差分をマージしてください。
61
+
56
62
  ```ruby
57
63
  require "picoruby/cloudflare/build"
58
64
 
@@ -85,9 +91,10 @@ Sinatra等のフレームワークはアプリ側で追加します。ABI固有
85
91
  revision属性のデフォルトはこのgemに組み込まれた値です。`nil` を代入するとデフォルトに戻ります。
86
92
  取得先の選択で `PICORUBY_WORKER_WASM_GEM_DIR` / `MRUBY_RACK_GEM_DIR` は参照せず、`worker:` / `rack:` 引数も受け取りません。
87
93
 
88
- 既定のWorker revisionは `e6235bca616dbd4cec619cc0141facdea59a5541`、
89
- Rackは `05ba46eb0ab490a624a5f2dcb33249670933ff6b` に固定しています。
90
- revisionがリモート未公開の場合はローカル指定が必要です。gem公開前に、新規チェックアウトから固定revisionを取得できることも確認してください。
94
+ 既定のWorker取得先は `67aaa676d8247beaf19cbbeaeecb78115e490529` に固定しています。
95
+ Rackは `30802024e263a0dde1f3a8467e648a70625adfd4` に固定しています。
96
+ 再現可能な依存関係が必要な場合は、Worker revisionをタグまたはcommit SHAで上書きしてください。
97
+ gem公開前には、新規チェックアウトから両方の固定取得先を取得できることを確認します。
91
98
 
92
99
  `worker_export` に渡す相対パスは `project_root` 基準(省略時はbuild_configのディレクトリ)です。
93
100
  生成されたRakefileはPicoRubyのRakeを別プロセスで実行し、ビルドをアプリ内の `.picoruby-build/` に分離します。
@@ -113,7 +120,7 @@ generated/worker/
113
120
  Ruby/C・HAL・共通JS bridgeは実行時ライブラリが、このgemはテンプレート・CrossBuild DSL・export処理・薄いcreateWorkerエントリを所有します。
114
121
  共通JSとregistry生成スクリプトは、Wasmをビルドした**同じmrbgemチェックアウト**からコピーします。
115
122
  現在の取得場所は `spike/src/` と `spike/scripts/` です。別コピーの実装をこのgemで管理しません。
116
- レイアウト変更時はexporterと固定revisionを一緒に更新します。
123
+ レイアウト変更時はexporterと設定済みWorker refを一緒に更新します。
117
124
 
118
125
  `createWorker` はリクエストごとにVMを生成・破棄し、異なるリクエストのenvを共有しません。
119
126
  低レベルの `createRuntime` / `dispatch` / `closeRuntime` も再exportします。
@@ -140,6 +147,38 @@ token = ENV["API_TOKEN"]
140
147
  ```
141
148
 
142
149
  Queueは現行APIに合わせてUTF-8文字列送信のみです。secretは.dev.varsまたは `wrangler secret put` で管理し、Gitへ追加しないでください。
150
+
151
+ AccessにはRackミドルウェア `Rack::Cloudflare::Access`(`Cloudflare::Access` の別名)を使います。
152
+
153
+ ```ruby
154
+ app = Rack::Builder.new do
155
+ use Rack::Cloudflare::Access, team: "my-team"
156
+ run lambda { |env|
157
+ identity = env["cloudflare.identity"]
158
+ [200, { "content-type" => "text/plain" }, [identity.email]]
159
+ }
160
+ end
161
+ Rackup::Handler::CloudflareWorker.run(app)
162
+ ```
163
+
164
+ `team:` を省略すると、リクエストのWorker環境から `CF_ACCESS_TEAM` を読みます。
165
+ 値は `<team>.cloudflareaccess.com` のteam部分です。
166
+ AccessはHTTPのユーザー情報取得APIなので、KV・Queueのようなリソースbindingは不要です。
167
+ ミドルウェアがCookieからidentityを取得・デコードし、`env["cloudflare.identity"]` に設定してから後続アプリを呼びます。
168
+ identityには `email`・`user_uuid`・`raw_data` があります。
169
+ `Cloudflare::Access.get_identity(token, team: "my-team")` で直接取得することもできます。
170
+ HTTP処理には汎用の `Cloudflare.fetch(url, method:, headers:, body:)` を使い、
171
+ `status`・`headers`・UTF-8の `body` を返します(最大1 MiB、タイムアウト10秒、リダイレクトなし)。
172
+ JWT署名・アプリケーションのaudienceをローカル検証する機能は含みません。
173
+ アプリをAccessで保護し、トークン検証は[Cloudflareの説明](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/application-token/)に従って行ってください。
174
+ ミドルウェアはteam未設定・不正なら503、Cookieなし・不正・Accessの401/403なら401、
175
+ 通信・レスポンスの異常なら502を返し、後続アプリを呼びません。
176
+ 生成例では `/access` だけにミドルウェアを適用し、他のサンプル経路は公開のままです。
177
+
178
+ Access対応はWorkerの `67aaa676d8247beaf19cbbeaeecb78115e490529` とmruby-rackの
179
+ `30802024e263a0dde1f3a8467e648a70625adfd4` に反映済みで、このテンプレートは標準でそれらを使用します。
180
+ ローカルmrbgem checkoutの指定は不要です。
181
+
143
182
  `npm run dev` / `npm run deploy` ではWranglerのcustom buildがRakeを実行します。
144
183
  `build.watch_dir` はapp.rbとbuild_config.rbです。Rubyファイルを増やしたときは監視対象も更新してください。
145
184
 
data/README.md CHANGED
@@ -23,7 +23,7 @@ Then run the following from this repository:
23
23
 
24
24
  ```sh
25
25
  bundle install
26
- bundle exec ruby exe/picoruby-cloudflare new ../my-worker --gem-path "$PWD"
26
+ bundle exec ruby exe/picoruby-cloudflare new ../my-worker --bindings --gem-path "$PWD"
27
27
  cd ../my-worker
28
28
  bundle install
29
29
  npm install
@@ -42,17 +42,23 @@ The build also checks that the Emscripten version is supported. Use a Node.js ve
42
42
  Build paths containing spaces or shell metacharacters are rejected because of upstream shell command expansion limitations.
43
43
 
44
44
  To try a packaged gem, run `gem build picoruby-cloudflare-template.gemspec`, followed by
45
- `gem install ./picoruby-cloudflare-template-0.1.0.rc1.gem` and
45
+ `gem install ./picoruby-cloudflare-template-0.1.0.rc2.gem` and
46
46
  `picoruby-cloudflare new my-worker`. Use `bundle install --local` in the generated project to resolve the unpublished version.
47
- After this release candidate is published, install it with `gem install picoruby-cloudflare-template --pre --version 0.1.0.rc1`.
48
- `VERSION` and the RubyGems metadata version are both `0.1.0.rc1`.
47
+ After this release candidate is published, install it with `gem install picoruby-cloudflare-template --pre --version 0.1.0.rc2`.
48
+ `VERSION` and the RubyGems metadata version are both `0.1.0.rc2`.
49
49
 
50
50
  ## Generated files and build configuration
51
51
 
52
- `new PATH [--name NAME] [--gem-path PATH]` generates a Gemfile, Rakefile, build_config.rb, a minimal Rack app in app.rb,
52
+ `new PATH [--bindings] [--name NAME] [--gem-path PATH]` generates a Gemfile, Rakefile, build_config.rb, a minimal Rack app in app.rb,
53
53
  src/index.js, package.json, wrangler.jsonc, .gitignore, and README.md.
54
54
  An existing destination is never overwritten, even if it is an empty directory. Commit Gemfile.lock and package-lock.json in your application repository.
55
55
 
56
+ Pass `--bindings` to include KV, Queue and Access identity examples in app.rb and wrangler.jsonc.
57
+ The `/access` example requires `CF_ACCESS_TEAM` and a `CF_Authorization` cookie.
58
+ The generated `.picoruby-cloudflare-template.json` records hashes of those two managed examples.
59
+ Run `picoruby-cloudflare bindings PROJECT` with a future template version to refresh them as supported bindings expand.
60
+ Regeneration checks every managed file before writing and changes nothing if either was edited; merge those changes manually instead.
61
+
56
62
  ```ruby
57
63
  require "picoruby/cloudflare/build"
58
64
 
@@ -85,9 +91,10 @@ A directory takes precedence over its revision, and relative directory paths are
85
91
  Revision attributes default to the values bundled in this gem; assigning `nil` restores those defaults.
86
92
  Dependency source selection no longer reads `PICORUBY_WORKER_WASM_GEM_DIR` or `MRUBY_RACK_GEM_DIR`, or accepts `worker:` / `rack:` arguments.
87
93
 
88
- The default Worker revision is pinned to `e6235bca616dbd4cec619cc0141facdea59a5541`,
89
- and Rack to `05ba46eb0ab490a624a5f2dcb33249670933ff6b`.
90
- Use a local checkout if a revision has not been published to the remote repository. Before publishing this gem, verify that a fresh checkout can fetch the pinned revisions.
94
+ The default Worker source is pinned to `67aaa676d8247beaf19cbbeaeecb78115e490529`.
95
+ Rack is pinned to `30802024e263a0dde1f3a8467e648a70625adfd4`.
96
+ Override the Worker revision with a tag or commit SHA when a reproducible dependency is required.
97
+ Before publishing this gem, verify that a fresh checkout can fetch both pinned sources.
91
98
 
92
99
  Relative paths passed to `worker_export` are resolved against `project_root`, which defaults to the build_config directory.
93
100
  The generated Rakefile runs PicoRuby's Rake in a separate process and keeps build output in the application's `.picoruby-build/` directory.
@@ -113,7 +120,7 @@ generated/worker/
113
120
  The runtime library owns the Ruby/C code, HAL, and shared JS bridge. This gem owns the templates, CrossBuild DSL, export logic, and thin createWorker entry point.
114
121
  Shared JS and registry generation scripts are copied from **the same mrbgem checkout** used to build Wasm.
115
122
  Their current locations are `spike/src/` and `spike/scripts/`. This gem does not maintain a separate copy of those implementations.
116
- If that layout changes, update the exporter and pinned revision together.
123
+ If that layout changes, update the exporter and configured Worker ref together.
117
124
 
118
125
  `createWorker` creates and closes a VM for each request, without sharing env between requests.
119
126
  The low-level `createRuntime` / `dispatch` / `closeRuntime` functions are also re-exported.
@@ -140,6 +147,38 @@ token = ENV["API_TOKEN"]
140
147
  ```
141
148
 
142
149
  Queue sending currently supports UTF-8 strings only, matching the runtime API. Manage secrets through .dev.vars or `wrangler secret put`, and keep them out of Git.
150
+
151
+ Access uses Rack middleware `Rack::Cloudflare::Access` (an alias of `Cloudflare::Access`):
152
+
153
+ ```ruby
154
+ app = Rack::Builder.new do
155
+ use Rack::Cloudflare::Access, team: "my-team"
156
+ run lambda { |env|
157
+ identity = env["cloudflare.identity"]
158
+ [200, { "content-type" => "text/plain" }, [identity.email]]
159
+ }
160
+ end
161
+ Rackup::Handler::CloudflareWorker.run(app)
162
+ ```
163
+
164
+ Omit `team:` to read `CF_ACCESS_TEAM` from the request's Worker environment.
165
+ Use the prefix of `<team>.cloudflareaccess.com`, not the full URL.
166
+ Access is an HTTP identity API, so it needs no KV/Queue-style resource binding.
167
+ Before calling the application, the middleware reads the authorization cookie, fetches and decodes
168
+ the identity, and stores an `AccessIdentity` (`email`, `user_uuid`, `raw_data`) in the Rack env.
169
+ The helper `Cloudflare::Access.get_identity(token, team: "my-team")` is also available.
170
+ It uses the generic `Cloudflare.fetch(url, method:, headers:, body:)` wrapper, which returns
171
+ `status`, `headers` and a buffered UTF-8 `body` (up to 1 MiB, ten-second timeout, no redirects).
172
+ Identity lookup does not locally validate JWT signatures or application audience. Protect the application with Access and validate
173
+ tokens as described in the [Cloudflare documentation](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/application-token/).
174
+ The middleware returns 503 for missing/invalid configuration, 401 for a missing/invalid cookie
175
+ or an Access 401/403 response, and 502 for upstream/protocol failures, without calling the application.
176
+ The generated example applies middleware only to `/access`; other example routes remain public.
177
+
178
+ Access support is included in Worker commit `67aaa676d8247beaf19cbbeaeecb78115e490529` and mruby-rack commit
179
+ `30802024e263a0dde1f3a8467e648a70625adfd4`, which this template uses by default.
180
+ No local mrbgem checkout overrides are required.
181
+
143
182
  With `npm run dev` / `npm run deploy`, Wrangler's custom build runs Rake.
144
183
  `build.watch_dir` covers app.rb and build_config.rb. Update the watch list when adding Ruby files.
145
184
 
@@ -8,11 +8,11 @@ end
8
8
 
9
9
  module Picoruby::Cloudflare::Template
10
10
  module CrossBuild
11
- WORKER_REVISION = "e6235bca616dbd4cec619cc0141facdea59a5541".freeze
12
- RACK_REVISION = "05ba46eb0ab490a624a5f2dcb33249670933ff6b".freeze
11
+ WORKER_REVISION = "67aaa676d8247beaf19cbbeaeecb78115e490529".freeze
12
+ RACK_REVISION = "30802024e263a0dde1f3a8467e648a70625adfd4".freeze
13
13
  CORE_GEMS = %w[mruby-array-ext mruby-catch mruby-class-ext mruby-enum-ext
14
14
  mruby-hash-ext mruby-kernel-ext mruby-metaprog mruby-method
15
- mruby-numeric-ext mruby-object-ext mruby-proc-ext mruby-sprintf
15
+ mruby-numeric-ext mruby-object-ext mruby-pack mruby-proc-ext mruby-sprintf
16
16
  mruby-string-ext mruby-struct mruby-regexp].freeze
17
17
 
18
18
  attr_accessor :picoruby_cloudflare_worker_wasm_mgem_dir, :mruby_rack_mgem_dir
@@ -6,12 +6,17 @@ require_relative "project"
6
6
 
7
7
  module Picoruby::Cloudflare::Template
8
8
  class CLI
9
+ BOLD_GREEN = "\e[1;32m"
10
+ CYAN = "\e[36m"
11
+ RESET = "\e[0m"
12
+
9
13
  HELP = <<~TEXT.freeze
10
14
  Usage: picoruby-cloudflare COMMAND [OPTIONS]
11
15
 
12
16
  Commands:
13
- new PATH Generate a PicoRuby Cloudflare Worker project
14
- doctor [PROJECT] Check local Worker build prerequisites (default: current directory)
17
+ new PATH [--bindings] Generate a PicoRuby Cloudflare Worker project
18
+ bindings PROJECT Add or refresh KV, Queue and Access examples
19
+ doctor [PROJECT] Check local Worker build prerequisites (default: current directory)
15
20
 
16
21
  Options:
17
22
  -h, --help Show this help
@@ -29,7 +34,8 @@ module Picoruby::Cloudflare::Template
29
34
 
30
35
  options = {}
31
36
  parser = OptionParser.new do |opts|
32
- opts.banner = "Usage: picoruby-cloudflare new PATH [--name NAME] [--gem-path PATH]\n picoruby-cloudflare doctor [PROJECT]"
37
+ opts.banner = "Usage: picoruby-cloudflare new PATH [--bindings] [--name NAME] [--gem-path PATH]\n picoruby-cloudflare bindings PROJECT\n picoruby-cloudflare doctor [PROJECT]"
38
+ opts.on("--bindings", "Generate KV, Queue and Access examples") { options[:bindings] = true }
33
39
  opts.on("--name NAME", "Worker name (defaults to directory name)") { options[:name] = _1 }
34
40
  opts.on("--gem-path PATH", "Use an unpublished local template gem") { options[:gem_path] = _1 }
35
41
  end
@@ -37,8 +43,28 @@ module Picoruby::Cloudflare::Template
37
43
  case command
38
44
  when "new"
39
45
  raise Error, parser.to_s unless argv.length == 1
40
- path = Generator.new(argv.first, **options).generate
41
- out.puts "Created #{path}\nNext: cd #{path}\n # On macOS: brew install emscripten (see README for PATH setup)\n bundle install\n npm install\n # Set PICORUBY_ROOT, then:\n bundle exec rake doctor\n npm run dev"
46
+ destination = argv.first
47
+ path = Generator.new(destination, **options).generate do |file|
48
+ out.puts " #{style("generate", BOLD_GREEN, out)} #{File.join(destination, file)}"
49
+ end
50
+ out.puts "\nCreated #{path}\nNext:"
51
+ out.puts " #{style("cd #{path}", CYAN, out)}"
52
+ out.puts " # On macOS (see README for PATH setup):"
53
+ out.puts " #{style("brew install emscripten", CYAN, out)}"
54
+ out.puts " #{style("bundle install", CYAN, out)}"
55
+ out.puts " #{style("npm install", CYAN, out)}"
56
+ out.puts " # Set PICORUBY_ROOT, then:"
57
+ out.puts " #{style("bundle exec rake doctor", CYAN, out)}"
58
+ out.puts " #{style("npm run dev", CYAN, out)}"
59
+ when "bindings"
60
+ raise Error, parser.to_s unless argv.length == 1 && options.empty?
61
+ destination = argv.first
62
+ path = BindingsGenerator.new(destination).generate do |status, file|
63
+ out.puts " #{style(status, BOLD_GREEN, out)} #{File.join(destination, file)}"
64
+ end
65
+ out.puts "\nBindings ready in #{path}"
66
+ out.puts " #{style("cd #{path}", CYAN, out)}"
67
+ out.puts " #{style("bundle exec rake", CYAN, out)}"
42
68
  when "doctor"
43
69
  raise Error, parser.to_s unless argv.length <= 1 && options.empty?
44
70
  Project.new(root: argv.first || Dir.pwd).doctor(out: out)
@@ -50,5 +76,10 @@ module Picoruby::Cloudflare::Template
50
76
  err.puts e.message
51
77
  1
52
78
  end
79
+
80
+ def self.style(text, escape, out)
81
+ return text unless out.respond_to?(:tty?) && out.tty?
82
+ "#{escape}#{text}#{RESET}"
83
+ end
53
84
  end
54
85
  end
@@ -1,18 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require "digest"
4
5
  require "fileutils"
6
+ require "json"
7
+ require "tempfile"
5
8
  require_relative "../template"
6
9
 
7
10
  module Picoruby::Cloudflare::Template
8
11
  class Generator
9
12
  TEMPLATES = File.expand_path("../../../../templates", __dir__)
13
+ BINDINGS_MANIFEST = ".picoruby-cloudflare-template.json"
14
+ BINDINGS_FILES = %w[app.rb wrangler.jsonc].freeze
15
+ BINDINGS_FEATURES = %w[kv queue access].freeze
10
16
 
11
- def initialize(destination, name: nil, gem_path: nil)
17
+ def initialize(destination, name: nil, gem_path: nil, bindings: false)
12
18
  @destination = File.expand_path(destination)
13
19
  @name = name || File.basename(@destination)
14
20
  @gem_path = File.expand_path(gem_path) if gem_path
15
- unless @name.match?(/\A[a-z0-9][a-z0-9-]{0,62}\z/)
21
+ @bindings = bindings
22
+ unless @name.is_a?(String) && @name.match?(/\A[a-z0-9][a-z0-9-]{0,62}\z/)
16
23
  raise Error, "Worker name must be 1–63 lowercase letters, digits or hyphens, starting with a letter or digit"
17
24
  end
18
25
  if @gem_path && !File.file?(File.join(@gem_path, "picoruby-cloudflare-template.gemspec"))
@@ -25,11 +32,7 @@ module Picoruby::Cloudflare::Template
25
32
  raise Error, "Destination already exists: #{@destination}; choose a new directory"
26
33
  end
27
34
  # Render everything before creating the project. Never overwrite an existing project.
28
- files = Dir[File.join(TEMPLATES, "project", "**", "*.erb")].sort.to_h do |source|
29
- relative = source.delete_prefix("#{TEMPLATES}/project/").delete_suffix(".erb")
30
- relative = ".gitignore" if relative == "gitignore"
31
- [relative, ERB.new(File.read(source), trim_mode: "-").result(binding)]
32
- end
35
+ files = rendered_files
33
36
  raise Error, "Project templates are missing from the installed gem" if files.empty?
34
37
  FileUtils.mkdir_p(File.dirname(@destination))
35
38
  Dir.mkdir(@destination)
@@ -37,8 +40,111 @@ module Picoruby::Cloudflare::Template
37
40
  path = File.join(@destination, relative)
38
41
  FileUtils.mkdir_p(File.dirname(path))
39
42
  File.write(path, content)
43
+ yield relative if block_given?
44
+ end
45
+ @destination
46
+ end
47
+
48
+ def rendered_files
49
+ files = Dir[File.join(TEMPLATES, "project", "**", "*.erb")].sort.to_h do |source|
50
+ relative = source.delete_prefix("#{TEMPLATES}/project/").delete_suffix(".erb")
51
+ relative = ".gitignore" if relative == "gitignore"
52
+ [relative, ERB.new(File.read(source), trim_mode: "-").result(binding)]
53
+ end
54
+ if @bindings
55
+ hashes = BINDINGS_FILES.to_h { [_1, Digest::SHA256.hexdigest(files.fetch(_1))] }
56
+ files[BINDINGS_MANIFEST] = JSON.pretty_generate({
57
+ format_version: 1, generator_version: VERSION,
58
+ features: BINDINGS_FEATURES, files: hashes,
59
+ }) + "\n"
60
+ end
61
+ files
62
+ end
63
+ end
64
+
65
+ class BindingsGenerator
66
+ def initialize(destination)
67
+ @destination = File.expand_path(destination)
68
+ end
69
+
70
+ def generate
71
+ unless File.directory?(@destination) && !File.symlink?(@destination)
72
+ raise Error, "Bindings target must be an existing project directory: #{@destination}"
73
+ end
74
+ package = read_json("package.json", "Generated project package.json is missing or invalid")
75
+ name = package["name"]
76
+ desired = Generator.new(@destination, name: name, bindings: true).rendered_files
77
+ base = Generator.new(@destination, name: name).rendered_files
78
+ previous = load_manifest
79
+ changes = []
80
+ conflicts = []
81
+
82
+ Generator::BINDINGS_FILES.each do |relative|
83
+ path = File.join(@destination, relative)
84
+ current = read_regular_file(path, relative)
85
+ wanted = desired.fetch(relative)
86
+ previous_hash = previous&.dig("files", relative)
87
+ safe = current == wanted || Digest::SHA256.hexdigest(current) == previous_hash || (!previous && current == base.fetch(relative))
88
+ conflicts << relative unless safe
89
+ changes << [relative, wanted, current == wanted ? :identical : :update]
90
+ end
91
+ unless conflicts.empty?
92
+ raise Error, "Bindings regeneration conflicts with modified files: #{conflicts.join(', ')}; no files changed"
93
+ end
94
+
95
+ manifest = Generator::BINDINGS_MANIFEST
96
+ manifest_path = File.join(@destination, manifest)
97
+ current_manifest = File.file?(manifest_path) && !File.symlink?(manifest_path) ? File.binread(manifest_path) : nil
98
+ manifest_status = if current_manifest.nil?
99
+ :generate
100
+ elsif current_manifest == desired.fetch(manifest)
101
+ :identical
102
+ else
103
+ :update
104
+ end
105
+ changes << [manifest, desired.fetch(manifest), manifest_status]
106
+ changes.each do |relative, content, status|
107
+ write(relative, content) unless status == :identical
108
+ yield status, relative if block_given?
40
109
  end
41
110
  @destination
42
111
  end
112
+
113
+ private
114
+
115
+ def load_manifest
116
+ path = File.join(@destination, Generator::BINDINGS_MANIFEST)
117
+ raise Error, "Bindings manifest must be a regular file" if File.symlink?(path)
118
+ return unless File.exist?(path)
119
+ raise Error, "Bindings manifest must be a regular file" unless File.file?(path)
120
+ data = JSON.parse(File.read(path))
121
+ unless data["format_version"] == 1 && data["files"].is_a?(Hash)
122
+ raise Error, "Bindings manifest has an unsupported format"
123
+ end
124
+ data
125
+ rescue JSON::ParserError
126
+ raise Error, "Bindings manifest is invalid JSON"
127
+ end
128
+
129
+ def read_json(relative, message)
130
+ JSON.parse(read_regular_file(File.join(@destination, relative), relative))
131
+ rescue JSON::ParserError
132
+ raise Error, message
133
+ end
134
+
135
+ def read_regular_file(path, relative)
136
+ raise Error, "Generated project file is missing or unsafe: #{relative}" unless File.file?(path) && !File.symlink?(path)
137
+ File.binread(path)
138
+ end
139
+
140
+ def write(relative, content)
141
+ path = File.join(@destination, relative)
142
+ Tempfile.create(["bindings", ".tmp"], @destination) do |temp|
143
+ temp.binmode
144
+ temp.write(content)
145
+ temp.close
146
+ File.rename(temp.path, path)
147
+ end
148
+ end
43
149
  end
44
150
  end
@@ -3,7 +3,7 @@
3
3
  module Picoruby
4
4
  module Cloudflare
5
5
  module Template
6
- VERSION = "0.1.0.rc1"
6
+ VERSION = "0.1.0.rc2"
7
7
  end
8
8
  end
9
9
  end
@@ -33,12 +33,12 @@ end
33
33
  ```
34
34
 
35
35
  The block receives the CrossBuild object itself and runs before build setup.
36
- Calling without a block is also supported. Both directory attributes default to `nil`, which selects GitHub sources at the
37
- revisions bundled in the template gem. Override those revisions in the same block using
36
+ Calling without a block is also supported. Both directory attributes default to `nil`, which selects the GitHub sources
37
+ configured by the template gem. Override those refs in the same block using
38
38
  `cf.picoruby_cloudflare_worker_wasm_revision` and `cf.mruby_rack_mgem_revision`.
39
39
  Directory attributes take precedence over revision attributes; relative paths
40
40
  are resolved against `build_config.rb`. These sources are not selected through
41
- environment variables. See the template gem README for tested revisions.
41
+ environment variables. The Worker source is pinned by the template gem; see its README for the current revision.
42
42
 
43
43
  `build_config.rb` selects mrbgems and exports an ES module to `generated/worker/`.
44
44
  Do not edit generated files. The generated Wasm, JS and app bytecode are one unit;
@@ -49,6 +49,25 @@ Ruby can use `env["cloudflare.env"].CACHE_KV` or
49
49
  `Cloudflare::KV.from_env(env, "CACHE_KV")`, and similarly `Cloudflare::Queue.from_env`.
50
50
  String/JSON vars and secrets are accessible through Ruby `ENV`.
51
51
 
52
+ If this project was generated with `--bindings`, app.rb exposes `/kv`, `/queue` and `/access`
53
+ examples and `.picoruby-cloudflare-template.json` tracks their generated state.
54
+ Run `picoruby-cloudflare bindings .` with a newer template to refresh unedited
55
+ binding examples. The command refuses to overwrite an edited app.rb or wrangler.jsonc.
56
+
57
+ For `/access`, set `CF_ACCESS_TEAM` to your team name (not its URL) in Wrangler vars
58
+ or .dev.vars, and send a `CF_Authorization` cookie issued by Access.
59
+ `Rack::Cloudflare::Access.new(app, team: "my-team")` is Rack middleware.
60
+ Omit `team:` to read `CF_ACCESS_TEAM` from the Worker environment.
61
+ Before calling the app, it stores the decoded identity in `env["cloudflare.identity"]`,
62
+ with `email`, `user_uuid` and `raw_data`. The generated example wraps only `/access`.
63
+ The helper `Cloudflare::Access.get_identity(token, team: "my-team")` calls
64
+ `Cloudflare.fetch` from Ruby. Missing/invalid configuration returns 503; missing/invalid
65
+ cookies and Access 401/403 responses return 401; upstream/protocol failures return 502.
66
+ Failures do not call the downstream app. This does not locally validate JWT signatures or audience.
67
+ Protect your application with Access and follow Cloudflare's token validation guidance.
68
+ The default Worker and mruby-rack revisions include Access, cookie parsing and
69
+ middleware keyword forwarding. No local mrbgem checkout overrides are required.
70
+
52
71
  Keep secrets in `.dev.vars` locally and use `npx wrangler secret put NAME` remotely.
53
72
  Never put them in `app.rb`, `build_config.rb` or committed config files.
54
73
 
@@ -1,10 +1,41 @@
1
- class App
2
- def self.call(env)
1
+ app = lambda do |env|
2
+ begin
3
+ <% if @bindings -%>
4
+ value = case env["PATH_INFO"]
5
+ when "/kv"
6
+ kv = Cloudflare::KV.from_env(env, "CACHE_KV")
7
+ kv.put("greeting", "Hello from Cloudflare KV!", ttl: 60)
8
+ kv.get("greeting")
9
+ when "/queue"
10
+ Cloudflare::Queue.from_env(env, "EVENTS").send("Hello from PicoRuby!")
11
+ "Message sent to Cloudflare Queue!"
12
+ when "/access"
13
+ identity = env["cloudflare.identity"]
14
+ identity.email || "Access identity has no email"
15
+ else
16
+ ENV["GREETING"] || "Hello from PicoRuby on Cloudflare!"
17
+ end
18
+ [200, { "content-type" => "text/plain; charset=utf-8" }, [value + "\n"]]
19
+ <% else -%>
3
20
  # env["cloudflare.env"].CACHE_KV and
4
21
  # Cloudflare::KV.from_env(env, "CACHE_KV") refer to the same binding.
5
22
  message = ENV["GREETING"] || "Hello from PicoRuby on Cloudflare!"
6
23
  [200, { "content-type" => "text/plain; charset=utf-8" }, [message + "\n"]]
24
+ <% end -%>
25
+ rescue
26
+ [500, { "content-type" => "text/plain; charset=utf-8" }, ["Internal Server Error\n"]]
7
27
  end
8
28
  end
9
29
 
10
- Rackup::Handler::CloudflareWorker.run(App)
30
+ <% if @bindings -%>
31
+ # Apply Access middleware to the identity example.
32
+ access_app = Rack::Builder.new do
33
+ use Rack::Cloudflare::Access
34
+ run app
35
+ end
36
+ Rackup::Handler::CloudflareWorker.run(lambda do |env|
37
+ env["PATH_INFO"] == "/access" ? access_app.call(env) : app.call(env)
38
+ end)
39
+ <% else -%>
40
+ Rackup::Handler::CloudflareWorker.run(app)
41
+ <% end -%>
@@ -5,7 +5,7 @@ MRuby::CrossBuild.new("worker") do |conf|
5
5
  # Optional overrides, applied before build setup:
6
6
  # cf.picoruby_cloudflare_worker_wasm_mgem_dir = "vendor/picoruby-cloudflare-worker-wasm"
7
7
  # cf.mruby_rack_mgem_dir = "vendor/mruby-rack"
8
- # cf.picoruby_cloudflare_worker_wasm_revision = "<commit SHA>"
8
+ # cf.picoruby_cloudflare_worker_wasm_revision = "<git ref>"
9
9
  # cf.mruby_rack_mgem_revision = "<commit SHA>"
10
10
  end
11
11
 
@@ -10,9 +10,25 @@
10
10
  "rules": [
11
11
  { "type": "Data", "globs": ["**/*.bin"], "fallthrough": true }
12
12
  ],
13
+ <% if @bindings -%>
14
+ "kv_namespaces": [
15
+ { "binding": "CACHE_KV" }
16
+ ],
17
+ "queues": {
18
+ "producers": [
19
+ { "binding": "EVENTS", "queue": "events" }
20
+ ]
21
+ },
22
+ <% end -%>
13
23
  "vars": {
24
+ <% if @bindings -%>
25
+ // Set your team name (the prefix of <team>.cloudflareaccess.com).
26
+ "CF_ACCESS_TEAM": "",
27
+ <% end -%>
14
28
  "GREETING": "Hello from PicoRuby on Cloudflare!"
15
29
  }
30
+ <% unless @bindings -%>
16
31
  // Add kv_namespaces / queues.producers here, then rebuild.
32
+ <% end -%>
17
33
  // Keep secrets out of this file; use .dev.vars and wrangler secret put.
18
34
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: picoruby-cloudflare-template
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.rc1
4
+ version: 0.1.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo