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 +4 -4
- data/README.ja.md +48 -9
- data/README.md +48 -9
- data/lib/picoruby/cloudflare/build.rb +3 -3
- data/lib/picoruby/cloudflare/template/cli.rb +36 -5
- data/lib/picoruby/cloudflare/template/generator.rb +113 -7
- data/lib/picoruby/cloudflare/template/version.rb +1 -1
- data/templates/project/README.md.erb +22 -3
- data/templates/project/app.rb.erb +34 -3
- data/templates/project/build_config.rb.erb +1 -1
- data/templates/project/wrangler.jsonc.erb +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c224b29b75ac222a183233c6af8022623d9530324e9028a76a1056278f07437b
|
|
4
|
+
data.tar.gz: a5199135545e2927b66465fb436e846260e3303587140f498c82d341f0f9ce0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
48
|
-
`VERSION` とRubyGemsのメタデータ上のバージョンは、どちらも `0.1.0.
|
|
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
|
|
89
|
-
Rackは `
|
|
90
|
-
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
|
|
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.
|
|
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.
|
|
48
|
-
`VERSION` and the RubyGems metadata version are both `0.1.0.
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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 = "
|
|
12
|
-
RACK_REVISION = "
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -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
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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 = "<
|
|
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
|
}
|