@takuhon/cloudflare 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +165 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +341 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 Takuhon contributors
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
@takuhon/cloudflare
|
|
2
|
+
Copyright 2026 Takuhon contributors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by the Takuhon project
|
|
5
|
+
(https://github.com/takuhon-dev/takuhon).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
8
|
+
|
|
9
|
+
Third-party dependencies retain their own licenses; see this package's
|
|
10
|
+
node_modules listing for details.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @takuhon/cloudflare
|
|
2
|
+
|
|
3
|
+
Cloudflare Workers adapter for Takuhon. Wires the framework-agnostic Hono
|
|
4
|
+
handlers from `@takuhon/api` to a Workers KV-backed profile store, the colo-
|
|
5
|
+
local edge cache, and `console.log`-based audit logging.
|
|
6
|
+
|
|
7
|
+
## Routes
|
|
8
|
+
|
|
9
|
+
| Method | Path | Source |
|
|
10
|
+
| -------- | --------------------------- | -------------------------------------------- |
|
|
11
|
+
| `GET` | `/` | `@takuhon/api` `createPublicApp` |
|
|
12
|
+
| `GET` | `/api/profile` | `@takuhon/api` `createPublicApp` (KV-backed) |
|
|
13
|
+
| `GET` | `/api/schema` | `@takuhon/api` `createPublicApp` |
|
|
14
|
+
| `GET` | `/api/jsonld` | `@takuhon/api` `createPublicApp` |
|
|
15
|
+
| `GET` | `/takuhon.json` | `@takuhon/api` `createPublicApp` |
|
|
16
|
+
| `GET` | `/.well-known/takuhon.json` | `@takuhon/api` `createPublicApp` |
|
|
17
|
+
| `GET` | `/admin` | `@takuhon/api` `createAdminUiApp` (HTML) |
|
|
18
|
+
| `PUT` | `/api/admin/profile` | `@takuhon/api` `createAdminApiApp` |
|
|
19
|
+
| `DELETE` | `/api/admin/profile` | `@takuhon/api` `createAdminApiApp` |
|
|
20
|
+
|
|
21
|
+
`POST` / `PATCH` on admin paths returns `405 Method Not Allowed`. Schema
|
|
22
|
+
validation failures return `422 Unprocessable Entity` with an `errors[]`
|
|
23
|
+
list of JSON-Schema-style fragment pointers.
|
|
24
|
+
|
|
25
|
+
## Local development
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pnpm install
|
|
29
|
+
pnpm --filter @takuhon/cloudflare dev # wrangler dev
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Visit `http://127.0.0.1:8787/`. The first request returns the bundled
|
|
33
|
+
onboarding fixture; the admin UI at `/admin` lets you replace it once you
|
|
34
|
+
provision an admin token (next section).
|
|
35
|
+
|
|
36
|
+
## Production deploy
|
|
37
|
+
|
|
38
|
+
### 1. Create the KV namespace
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
wrangler kv namespace create TAKUHON_KV
|
|
42
|
+
wrangler kv namespace create TAKUHON_KV --preview
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Copy the printed ids into `wrangler.toml` (replace the
|
|
46
|
+
`REPLACE_WITH_*_NAMESPACE_ID` placeholders).
|
|
47
|
+
|
|
48
|
+
### 2. Provision the admin token
|
|
49
|
+
|
|
50
|
+
The admin bearer token must never live in `wrangler.toml` or source
|
|
51
|
+
control. Set it as a Wrangler secret:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
# 32 bytes of entropy, base64-encoded (43 chars).
|
|
55
|
+
TOKEN=$(openssl rand -base64 32)
|
|
56
|
+
echo "$TOKEN" | wrangler secret put TAKUHON_ADMIN_TOKEN
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Store `$TOKEN` securely (1Password, vault, etc.). Without it, every
|
|
60
|
+
`PUT/DELETE /api/admin/profile` returns `401 Unauthorized`.
|
|
61
|
+
|
|
62
|
+
#### Token rotation
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
# Generate a new token, push it as the new secret, then update any clients
|
|
66
|
+
# that hold the old one. There is no grace period — once the secret
|
|
67
|
+
# changes, requests bearing the old token are rejected.
|
|
68
|
+
echo "$NEW_TOKEN" | wrangler secret put TAKUHON_ADMIN_TOKEN
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Pin the admin Origin allowlist (optional but recommended)
|
|
72
|
+
|
|
73
|
+
Edit `wrangler.toml` to set the origins that may issue browser-borne admin
|
|
74
|
+
writes:
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
[vars]
|
|
78
|
+
TAKUHON_ADMIN_ORIGIN = "https://admin.example.com,https://localhost:3000"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Empty value disables the check — acceptable when `/admin` is the only
|
|
82
|
+
admin UI surface and you trust same-origin requests. Requests without an
|
|
83
|
+
`Origin` header (curl, native clients) are always allowed; the bearer
|
|
84
|
+
token is the primary auth boundary.
|
|
85
|
+
|
|
86
|
+
### 4. Configure Rate Limiting Rules
|
|
87
|
+
|
|
88
|
+
`/api/admin/*` is **not** rate-limited in code. Apply a Cloudflare WAF rule
|
|
89
|
+
in the dashboard:
|
|
90
|
+
|
|
91
|
+
1. Open **Security → WAF → Rate limiting Rules**.
|
|
92
|
+
2. Add a rule matching `URI Path` starts with `/api/admin/`.
|
|
93
|
+
3. Set the characteristic to the `Authorization` header (so each token
|
|
94
|
+
gets its own budget).
|
|
95
|
+
4. Limit: `10 requests per 1 minute`.
|
|
96
|
+
5. Action: `Block` with a custom JSON response body that mirrors RFC 7807
|
|
97
|
+
`application/problem+json` (or simply `Block` with default 429).
|
|
98
|
+
|
|
99
|
+
### 5. Deploy
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
pnpm --filter @takuhon/cloudflare typecheck # local TS check
|
|
103
|
+
wrangler deploy
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Operational notes
|
|
107
|
+
|
|
108
|
+
### Audit log retrieval
|
|
109
|
+
|
|
110
|
+
All admin auth attempts and profile mutations emit one line of JSON to
|
|
111
|
+
`console.log`. Tail them in real time:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
wrangler tail
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For long-term retention, configure **Workers → Logpush** to ship to R2,
|
|
118
|
+
S3, or a SIEM. Recommended retention: 90 days.
|
|
119
|
+
|
|
120
|
+
The actor identity in every event is `sha256:<hex>` over the presented
|
|
121
|
+
bearer token. The raw token never leaves the request boundary.
|
|
122
|
+
|
|
123
|
+
### Edge cache invalidation
|
|
124
|
+
|
|
125
|
+
After a successful admin write, the Worker calls `caches.default.delete`
|
|
126
|
+
for `/`, `/api/profile`, `/api/profile?lang=en|ja`, `/api/jsonld`,
|
|
127
|
+
`/api/jsonld?lang=en|ja`, and `/takuhon.json`. **This clears the current
|
|
128
|
+
colo's cache only**; other colos honour the response's `Cache-Control`
|
|
129
|
+
`s-maxage=300` (5 minutes) before refreshing.
|
|
130
|
+
|
|
131
|
+
If you need immediate global invalidation, hit the Cloudflare REST API
|
|
132
|
+
manually after a write (`POST /zones/{zone_id}/purge_cache`). Doing it
|
|
133
|
+
in-Worker requires a zone-scoped API token that we don't bundle by
|
|
134
|
+
default; expanding to multi-lang or a configurable lang list happens as
|
|
135
|
+
part of a later phase.
|
|
136
|
+
|
|
137
|
+
### Admin UI security
|
|
138
|
+
|
|
139
|
+
The `/admin` HTML editor runs under a tight CSP:
|
|
140
|
+
|
|
141
|
+
- `script-src 'self' 'nonce-<n>'` — no inline scripts without the
|
|
142
|
+
per-request nonce; no `unsafe-inline`.
|
|
143
|
+
- `style-src 'self' 'nonce-<n>'` — same for styles.
|
|
144
|
+
- `require-trusted-types-for 'script'` — DOM-XSS sinks are blocked even
|
|
145
|
+
if the nonce is leaked.
|
|
146
|
+
- `img-src 'self' blob:` — only same-origin or client-side previews.
|
|
147
|
+
|
|
148
|
+
The HTML is single-page, no build step: load `/admin`, paste the token,
|
|
149
|
+
edit JSON, save. The token never appears in the URL or cookies.
|
|
150
|
+
|
|
151
|
+
### Disabling admin entirely
|
|
152
|
+
|
|
153
|
+
Leave `TAKUHON_ADMIN_TOKEN` unset. Every admin write returns `401`. The
|
|
154
|
+
`/admin` UI is still served but its Save / Delete actions will fail
|
|
155
|
+
identically; treat that as a feature-flag for read-only deployments.
|
|
156
|
+
|
|
157
|
+
## Limitations & deferred work
|
|
158
|
+
|
|
159
|
+
| Concern | Status | Tracked phase |
|
|
160
|
+
| ---------------------------------- | ------------------------------------- | ------------- |
|
|
161
|
+
| `PATCH /api/admin/profile` | 405 (intentionally not implemented) | Phase 5+ |
|
|
162
|
+
| `POST /api/admin/assets` (R2) | Not yet wired | Phase 3.5 |
|
|
163
|
+
| Global cache purge (REST API) | Colo-local only via `Cache.delete` | Phase 5+ |
|
|
164
|
+
| CORS preflight for cross-origin | Not handled (admin UI is same-origin) | Phase 5+ |
|
|
165
|
+
| CLI scaffolding (`create-takuhon`) | Minimal Wrangler bootstrap | Phase 3.6 |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import { Takuhon } from '@takuhon/core';
|
|
3
|
+
|
|
4
|
+
interface Env {
|
|
5
|
+
TAKUHON_KV: KVNamespace;
|
|
6
|
+
/**
|
|
7
|
+
* Admin bearer token. Provision via `wrangler secret put TAKUHON_ADMIN_TOKEN`.
|
|
8
|
+
* Leave unset to disable admin writes entirely (every PUT/DELETE returns 401).
|
|
9
|
+
*/
|
|
10
|
+
TAKUHON_ADMIN_TOKEN?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Comma-separated Origin allowlist for browser-originating admin requests.
|
|
13
|
+
* Empty / unset disables the check (deploy without a configured allowlist is
|
|
14
|
+
* acceptable when the admin UI is same-origin; documented in the README).
|
|
15
|
+
*/
|
|
16
|
+
TAKUHON_ADMIN_ORIGIN?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Options accepted by {@link createTakuhonWorker}. */
|
|
19
|
+
interface CreateTakuhonWorkerOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Lazy producer for the fallback Takuhon document served when KV has no
|
|
22
|
+
* stored profile yet. Called at most once per Worker invocation, on the
|
|
23
|
+
* cold path where the storage layer returns no entry. Implementations
|
|
24
|
+
* typically import a bundled `takuhon.json`, validate it once, and return
|
|
25
|
+
* the resulting value.
|
|
26
|
+
*/
|
|
27
|
+
readonly fallback: () => Takuhon;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a Cloudflare Worker handler for the takuhon adapter. Wires
|
|
31
|
+
* `@takuhon/api`'s public/admin app factories to the KV-backed storage,
|
|
32
|
+
* Cloudflare edge cache purger, and console audit logger that ship with
|
|
33
|
+
* this package.
|
|
34
|
+
*
|
|
35
|
+
* This is the entry point used by projects scaffolded with
|
|
36
|
+
* `create-takuhon`: their `src/index.ts` imports `createTakuhonWorker`,
|
|
37
|
+
* passes a `fallback` that loads the project's own `takuhon.json`, and
|
|
38
|
+
* `export default`s the returned handler. The default export of this
|
|
39
|
+
* module is a convenience that calls the same factory with the monorepo's
|
|
40
|
+
* bundled `personal-profile` fixture.
|
|
41
|
+
*/
|
|
42
|
+
declare function createTakuhonWorker(opts: CreateTakuhonWorkerOptions): {
|
|
43
|
+
fetch: (request: Request, env: Env) => Response | Promise<Response>;
|
|
44
|
+
};
|
|
45
|
+
declare const _default: {
|
|
46
|
+
fetch: (request: Request, env: Env) => Response | Promise<Response>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { type CreateTakuhonWorkerOptions, type Env, createTakuhonWorker, _default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
ERROR_SLUGS,
|
|
4
|
+
createAdminApiApp,
|
|
5
|
+
createAdminUiApp,
|
|
6
|
+
createPublicApp,
|
|
7
|
+
problemResponse
|
|
8
|
+
} from "@takuhon/api";
|
|
9
|
+
import { validate } from "@takuhon/core";
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
|
|
12
|
+
// ../../examples/personal-profile/takuhon.json
|
|
13
|
+
var takuhon_default = {
|
|
14
|
+
schemaVersion: "0.1.0",
|
|
15
|
+
profile: {
|
|
16
|
+
displayName: {
|
|
17
|
+
en: "Pat Rivera",
|
|
18
|
+
ja: "\u30D1\u30C3\u30C8\u30FB\u30EA\u30D9\u30E9"
|
|
19
|
+
},
|
|
20
|
+
tagline: {
|
|
21
|
+
en: "Open-source maintainer and accessibility advocate",
|
|
22
|
+
ja: "\u30AA\u30FC\u30D7\u30F3\u30BD\u30FC\u30B9\u30E1\u30F3\u30C6\u30CA / \u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u5B9F\u8DF5\u8005"
|
|
23
|
+
},
|
|
24
|
+
bio: {
|
|
25
|
+
en: "Pat Rivera is a fictional persona used to exercise every field of the takuhon profile schema. They maintain a handful of open-source libraries focused on accessibility tooling and frequently speak at local meetups.",
|
|
26
|
+
ja: "Pat Rivera \u306F takuhon \u30D7\u30ED\u30D5\u30A3\u30FC\u30EB schema \u306E\u5168\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u793A\u3059\u305F\u3081\u306E\u67B6\u7A7A\u306E\u4EBA\u7269\u3067\u3059\u3002\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u7CFB\u306E\u30AA\u30FC\u30D7\u30F3\u30BD\u30FC\u30B9\u30E9\u30A4\u30D6\u30E9\u30EA\u3092\u4FDD\u5B88\u3057\u3001\u5730\u57DF\u30B3\u30DF\u30E5\u30CB\u30C6\u30A3\u3067\u767B\u58C7\u3057\u3066\u3044\u307E\u3059\u3002"
|
|
27
|
+
},
|
|
28
|
+
avatar: {
|
|
29
|
+
url: "/assets/avatar.webp",
|
|
30
|
+
alt: {
|
|
31
|
+
en: "Pat Rivera smiling, wearing round glasses, in front of a soft gradient.",
|
|
32
|
+
ja: "\u30BD\u30D5\u30C8\u306A\u30B0\u30E9\u30C7\u30FC\u30B7\u30E7\u30F3\u3092\u80CC\u666F\u306B\u5FAE\u7B11\u3080 Pat Rivera\u3002\u4E38\u773C\u93E1\u3092\u7740\u7528\u3002"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
location: {
|
|
36
|
+
country: "PT",
|
|
37
|
+
region: "Lisbon",
|
|
38
|
+
locality: {
|
|
39
|
+
en: "Lisbon",
|
|
40
|
+
ja: "\u30EA\u30B9\u30DC\u30F3"
|
|
41
|
+
},
|
|
42
|
+
display: {
|
|
43
|
+
en: "Lisbon, Portugal",
|
|
44
|
+
ja: "\u30DD\u30EB\u30C8\u30AC\u30EB\u30FB\u30EA\u30B9\u30DC\u30F3"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
links: [
|
|
49
|
+
{
|
|
50
|
+
id: "website",
|
|
51
|
+
type: "website",
|
|
52
|
+
label: { en: "Personal site" },
|
|
53
|
+
url: "https://example.com/pat",
|
|
54
|
+
featured: true,
|
|
55
|
+
order: 0
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "github",
|
|
59
|
+
type: "github",
|
|
60
|
+
url: "https://github.com/example-pat",
|
|
61
|
+
featured: true,
|
|
62
|
+
order: 1
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "mastodon",
|
|
66
|
+
type: "mastodon",
|
|
67
|
+
url: "https://example.social/@pat",
|
|
68
|
+
order: 2
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "blog",
|
|
72
|
+
type: "blog",
|
|
73
|
+
label: {
|
|
74
|
+
en: "Notes on accessible UI",
|
|
75
|
+
ja: "\u30A2\u30AF\u30BB\u30B7\u30D6\u30EB UI \u306E\u899A\u3048\u66F8\u304D"
|
|
76
|
+
},
|
|
77
|
+
url: "https://example.com/pat/blog",
|
|
78
|
+
order: 3
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "newsletter",
|
|
82
|
+
type: "custom",
|
|
83
|
+
label: {
|
|
84
|
+
en: "Weekly newsletter",
|
|
85
|
+
ja: "\u9031\u6B21\u30CB\u30E5\u30FC\u30B9\u30EC\u30BF\u30FC"
|
|
86
|
+
},
|
|
87
|
+
url: "https://example.com/pat/newsletter",
|
|
88
|
+
iconUrl: "https://example.com/assets/icons/newsletter.svg",
|
|
89
|
+
order: 4
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
careers: [
|
|
93
|
+
{
|
|
94
|
+
id: "stellar-ux",
|
|
95
|
+
organization: {
|
|
96
|
+
en: "Stellar UX Studio",
|
|
97
|
+
ja: "\u30B9\u30C6\u30E9 UX \u30B9\u30BF\u30B8\u30AA"
|
|
98
|
+
},
|
|
99
|
+
role: {
|
|
100
|
+
en: "Principal Accessibility Engineer",
|
|
101
|
+
ja: "\u30D7\u30EA\u30F3\u30B7\u30D1\u30EB\u30FB\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u30A8\u30F3\u30B8\u30CB\u30A2"
|
|
102
|
+
},
|
|
103
|
+
description: {
|
|
104
|
+
en: "Lead the accessibility engineering practice across product surfaces, drive WCAG 2.2 conformance reviews, and mentor a team of five engineers.",
|
|
105
|
+
ja: "\u30D7\u30ED\u30C0\u30AF\u30C8\u5168\u4F53\u306E\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u8A2D\u8A08\u3092\u7D71\u62EC\u3002WCAG 2.2 \u9069\u5408\u30EC\u30D3\u30E5\u30FC\u3092\u4E3B\u5C0E\u3057\u30015 \u540D\u306E\u30A8\u30F3\u30B8\u30CB\u30A2\u3092\u30E1\u30F3\u30BF\u30EA\u30F3\u30B0\u3002"
|
|
106
|
+
},
|
|
107
|
+
startDate: "2023-04",
|
|
108
|
+
endDate: null,
|
|
109
|
+
isCurrent: true,
|
|
110
|
+
url: "https://example.com/stellar",
|
|
111
|
+
order: 0
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "harbor-labs",
|
|
115
|
+
organization: {
|
|
116
|
+
en: "Harbor Labs",
|
|
117
|
+
ja: "\u30CF\u30FC\u30D0\u30FC\u30E9\u30DC"
|
|
118
|
+
},
|
|
119
|
+
role: {
|
|
120
|
+
en: "Senior Frontend Engineer",
|
|
121
|
+
ja: "\u30B7\u30CB\u30A2\u30D5\u30ED\u30F3\u30C8\u30A8\u30F3\u30C9\u30A8\u30F3\u30B8\u30CB\u30A2"
|
|
122
|
+
},
|
|
123
|
+
description: {
|
|
124
|
+
en: "Built the design system foundation and an accessible component library used across nine internal products.",
|
|
125
|
+
ja: "\u30C7\u30B6\u30A4\u30F3\u30B7\u30B9\u30C6\u30E0\u57FA\u76E4\u3068\u3001\u793E\u5185 9 \u30D7\u30ED\u30C0\u30AF\u30C8\u3067\u5229\u7528\u3055\u308C\u308B\u30A2\u30AF\u30BB\u30B7\u30D6\u30EB\u306A\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u30E9\u30A4\u30D6\u30E9\u30EA\u3092\u8A2D\u8A08\u3002"
|
|
126
|
+
},
|
|
127
|
+
startDate: "2019-06",
|
|
128
|
+
endDate: "2023-03",
|
|
129
|
+
order: 1
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
projects: [
|
|
133
|
+
{
|
|
134
|
+
id: "axe-helpers",
|
|
135
|
+
title: {
|
|
136
|
+
en: "axe-helpers",
|
|
137
|
+
ja: "axe-helpers"
|
|
138
|
+
},
|
|
139
|
+
description: {
|
|
140
|
+
en: "A tiny set of utilities that wraps axe-core for use in component-level integration tests.",
|
|
141
|
+
ja: "\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u5358\u4F4D\u306E\u7D71\u5408\u30C6\u30B9\u30C8\u5411\u3051\u306B axe-core \u3092\u30E9\u30C3\u30D7\u3059\u308B\u5C0F\u898F\u6A21\u30E6\u30FC\u30C6\u30A3\u30EA\u30C6\u30A3\u96C6\u3002"
|
|
142
|
+
},
|
|
143
|
+
url: "https://example.com/axe-helpers",
|
|
144
|
+
tags: ["accessibility", "testing", "typescript"],
|
|
145
|
+
relatedCareerId: "stellar-ux",
|
|
146
|
+
startDate: "2023-09",
|
|
147
|
+
highlighted: true,
|
|
148
|
+
order: 0
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "color-contrast-cli",
|
|
152
|
+
title: {
|
|
153
|
+
en: "color-contrast-cli",
|
|
154
|
+
ja: "color-contrast-cli"
|
|
155
|
+
},
|
|
156
|
+
description: {
|
|
157
|
+
en: "Command-line tool that audits design tokens for WCAG contrast ratios.",
|
|
158
|
+
ja: "\u30C7\u30B6\u30A4\u30F3\u30C8\u30FC\u30AF\u30F3\u306E WCAG \u30B3\u30F3\u30C8\u30E9\u30B9\u30C8\u6BD4\u3092\u76E3\u67FB\u3059\u308B\u30B3\u30DE\u30F3\u30C9\u30E9\u30A4\u30F3\u30C4\u30FC\u30EB\u3002"
|
|
159
|
+
},
|
|
160
|
+
url: "https://example.com/color-contrast-cli",
|
|
161
|
+
tags: ["accessibility", "cli", "design-tokens"],
|
|
162
|
+
startDate: "2021-02",
|
|
163
|
+
endDate: "2022-08",
|
|
164
|
+
order: 1
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "meetup-talks",
|
|
168
|
+
title: {
|
|
169
|
+
en: "Local meetup talks",
|
|
170
|
+
ja: "\u5730\u57DF\u30B3\u30DF\u30E5\u30CB\u30C6\u30A3\u767B\u58C7"
|
|
171
|
+
},
|
|
172
|
+
order: 2
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
skills: [
|
|
176
|
+
{ id: "typescript", label: "TypeScript", category: "programming", order: 0 },
|
|
177
|
+
{ id: "react", label: "React", category: "programming", order: 1 },
|
|
178
|
+
{ id: "wcag-2-2", label: "WCAG 2.2", category: "design", order: 2 },
|
|
179
|
+
{ id: "aria", label: "ARIA", category: "design", order: 3 },
|
|
180
|
+
{ id: "storybook", label: "Storybook", category: "programming", order: 4 },
|
|
181
|
+
{ id: "playwright", label: "Playwright", category: "programming", order: 5 },
|
|
182
|
+
{ id: "design-tokens", label: "Design tokens", category: "design", order: 6 },
|
|
183
|
+
{ id: "portuguese", label: "Portuguese (B2)", category: "language", order: 7 }
|
|
184
|
+
],
|
|
185
|
+
contact: {
|
|
186
|
+
email: "pat@example.com",
|
|
187
|
+
showEmail: false,
|
|
188
|
+
formUrl: "https://example.com/pat/contact"
|
|
189
|
+
},
|
|
190
|
+
settings: {
|
|
191
|
+
defaultLocale: "en",
|
|
192
|
+
fallbackLocale: "en",
|
|
193
|
+
availableLocales: ["en", "ja"],
|
|
194
|
+
theme: "default",
|
|
195
|
+
showPoweredBy: true,
|
|
196
|
+
enableJsonLd: true,
|
|
197
|
+
enableApi: true,
|
|
198
|
+
enableAnalytics: false
|
|
199
|
+
},
|
|
200
|
+
meta: {
|
|
201
|
+
createdAt: "2026-01-15T09:00:00Z",
|
|
202
|
+
updatedAt: "2026-05-12T08:30:00Z",
|
|
203
|
+
generator: "Takuhon",
|
|
204
|
+
contentLicense: {
|
|
205
|
+
spdxId: "CC-BY-4.0",
|
|
206
|
+
url: "https://creativecommons.org/licenses/by/4.0/",
|
|
207
|
+
attribution: {
|
|
208
|
+
name: "Pat Rivera",
|
|
209
|
+
url: "https://example.com/pat"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/admin/cloudflare-cache-purger.ts
|
|
216
|
+
var CloudflareCachePurger = class {
|
|
217
|
+
/**
|
|
218
|
+
* `getCache` is a thunk so the Workers-only `caches` global is touched
|
|
219
|
+
* lazily — public-only requests on this Worker never run admin handlers
|
|
220
|
+
* and must not pay (or fail under Node tests) for the lookup.
|
|
221
|
+
*/
|
|
222
|
+
constructor(getCache, opts) {
|
|
223
|
+
this.getCache = getCache;
|
|
224
|
+
this.origin = opts.origin.replace(/\/$/, "");
|
|
225
|
+
this.langs = opts.langs ?? ["en", "ja"];
|
|
226
|
+
}
|
|
227
|
+
getCache;
|
|
228
|
+
origin;
|
|
229
|
+
langs;
|
|
230
|
+
async profileUpdated() {
|
|
231
|
+
await this.purge();
|
|
232
|
+
}
|
|
233
|
+
async profileDeleted() {
|
|
234
|
+
await this.purge();
|
|
235
|
+
}
|
|
236
|
+
async purge() {
|
|
237
|
+
const cache = this.getCache();
|
|
238
|
+
const targets = ["/", "/api/profile", "/api/jsonld", "/takuhon.json"];
|
|
239
|
+
for (const lang of this.langs) {
|
|
240
|
+
const q = `?lang=${encodeURIComponent(lang)}`;
|
|
241
|
+
targets.push(`/api/profile${q}`, `/api/jsonld${q}`);
|
|
242
|
+
}
|
|
243
|
+
for (const path of targets) {
|
|
244
|
+
await cache.delete(new Request(this.origin + path), { ignoreMethod: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/admin/console-audit-logger.ts
|
|
250
|
+
var consoleAuditLogger = (event) => {
|
|
251
|
+
console.log(JSON.stringify(event));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/kv-storage.ts
|
|
255
|
+
import { ConflictError, NotFoundError } from "@takuhon/core";
|
|
256
|
+
var KV_KEY = "TAKUHON_DATA";
|
|
257
|
+
var KvTakuhonStorage = class {
|
|
258
|
+
constructor(kv) {
|
|
259
|
+
this.kv = kv;
|
|
260
|
+
}
|
|
261
|
+
kv;
|
|
262
|
+
async getProfile() {
|
|
263
|
+
const result = await this.kv.getWithMetadata(KV_KEY, "json");
|
|
264
|
+
if (result.value === null || !result.metadata?.version) {
|
|
265
|
+
throw new NotFoundError(`No profile is stored at KV key "${KV_KEY}".`);
|
|
266
|
+
}
|
|
267
|
+
return { data: result.value, version: result.metadata.version };
|
|
268
|
+
}
|
|
269
|
+
async saveProfile(data, ifMatch) {
|
|
270
|
+
if (ifMatch !== void 0) {
|
|
271
|
+
const current = await this.kv.getWithMetadata(KV_KEY, "json");
|
|
272
|
+
const currentVersion = current.metadata?.version;
|
|
273
|
+
if (currentVersion !== ifMatch) {
|
|
274
|
+
throw new ConflictError(
|
|
275
|
+
`If-Match preconditioned on version "${ifMatch}" but current is "${currentVersion ?? "absent"}".`,
|
|
276
|
+
{ currentVersion }
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const version = crypto.randomUUID();
|
|
281
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
282
|
+
await this.kv.put(KV_KEY, JSON.stringify(data), {
|
|
283
|
+
metadata: { version, updatedAt }
|
|
284
|
+
});
|
|
285
|
+
return { version };
|
|
286
|
+
}
|
|
287
|
+
async deleteProfile() {
|
|
288
|
+
await this.kv.delete(KV_KEY);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// src/index.ts
|
|
293
|
+
function parseOrigins(raw) {
|
|
294
|
+
if (raw === void 0 || raw === "") return [];
|
|
295
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
296
|
+
}
|
|
297
|
+
function createTakuhonWorker(opts) {
|
|
298
|
+
return {
|
|
299
|
+
fetch(request, env) {
|
|
300
|
+
const url = new URL(request.url);
|
|
301
|
+
const storage = new KvTakuhonStorage(env.TAKUHON_KV);
|
|
302
|
+
const cachePurger = new CloudflareCachePurger(() => caches.default, {
|
|
303
|
+
origin: url.origin
|
|
304
|
+
});
|
|
305
|
+
const auditLogger = consoleAuditLogger;
|
|
306
|
+
const router = new Hono();
|
|
307
|
+
router.notFound(
|
|
308
|
+
(c) => problemResponse(c, {
|
|
309
|
+
slug: ERROR_SLUGS.notFound,
|
|
310
|
+
status: 404,
|
|
311
|
+
title: "Not Found",
|
|
312
|
+
detail: `No route matches ${new URL(c.req.url).pathname}.`
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
router.route(
|
|
316
|
+
"/api/admin",
|
|
317
|
+
createAdminApiApp({
|
|
318
|
+
storage,
|
|
319
|
+
getAdminToken: () => env.TAKUHON_ADMIN_TOKEN,
|
|
320
|
+
getAdminOrigins: () => parseOrigins(env.TAKUHON_ADMIN_ORIGIN),
|
|
321
|
+
cachePurger,
|
|
322
|
+
auditLogger
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
router.route("/admin", createAdminUiApp());
|
|
326
|
+
router.route("/", createPublicApp({ storage, fallback: opts.fallback }));
|
|
327
|
+
return router.fetch(request, env);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function bundledFallback() {
|
|
332
|
+
const r = validate(takuhon_default);
|
|
333
|
+
if (!r.ok) throw new Error("Bundled fixture failed validation.");
|
|
334
|
+
return r.data;
|
|
335
|
+
}
|
|
336
|
+
var index_default = createTakuhonWorker({ fallback: bundledFallback });
|
|
337
|
+
export {
|
|
338
|
+
createTakuhonWorker,
|
|
339
|
+
index_default as default
|
|
340
|
+
};
|
|
341
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../../examples/personal-profile/takuhon.json","../src/admin/cloudflare-cache-purger.ts","../src/admin/console-audit-logger.ts","../src/kv-storage.ts"],"sourcesContent":["import {\n ERROR_SLUGS,\n createAdminApiApp,\n createAdminUiApp,\n createPublicApp,\n problemResponse,\n type AuditLogger,\n type CachePurger,\n} from '@takuhon/api';\nimport { validate, type Takuhon } from '@takuhon/core';\nimport { Hono } from 'hono';\n\nimport exampleJson from '../../../examples/personal-profile/takuhon.json' with { type: 'json' };\n\nimport { CloudflareCachePurger } from './admin/cloudflare-cache-purger.js';\nimport { consoleAuditLogger } from './admin/console-audit-logger.js';\nimport { KvTakuhonStorage } from './kv-storage.js';\n\nexport interface Env {\n TAKUHON_KV: KVNamespace;\n /**\n * Admin bearer token. Provision via `wrangler secret put TAKUHON_ADMIN_TOKEN`.\n * Leave unset to disable admin writes entirely (every PUT/DELETE returns 401).\n */\n TAKUHON_ADMIN_TOKEN?: string;\n /**\n * Comma-separated Origin allowlist for browser-originating admin requests.\n * Empty / unset disables the check (deploy without a configured allowlist is\n * acceptable when the admin UI is same-origin; documented in the README).\n */\n TAKUHON_ADMIN_ORIGIN?: string;\n}\n\n/** Options accepted by {@link createTakuhonWorker}. */\nexport interface CreateTakuhonWorkerOptions {\n /**\n * Lazy producer for the fallback Takuhon document served when KV has no\n * stored profile yet. Called at most once per Worker invocation, on the\n * cold path where the storage layer returns no entry. Implementations\n * typically import a bundled `takuhon.json`, validate it once, and return\n * the resulting value.\n */\n readonly fallback: () => Takuhon;\n}\n\nfunction parseOrigins(raw: string | undefined): string[] {\n if (raw === undefined || raw === '') return [];\n return raw\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s !== '');\n}\n\n/**\n * Build a Cloudflare Worker handler for the takuhon adapter. Wires\n * `@takuhon/api`'s public/admin app factories to the KV-backed storage,\n * Cloudflare edge cache purger, and console audit logger that ship with\n * this package.\n *\n * This is the entry point used by projects scaffolded with\n * `create-takuhon`: their `src/index.ts` imports `createTakuhonWorker`,\n * passes a `fallback` that loads the project's own `takuhon.json`, and\n * `export default`s the returned handler. The default export of this\n * module is a convenience that calls the same factory with the monorepo's\n * bundled `personal-profile` fixture.\n */\nexport function createTakuhonWorker(opts: CreateTakuhonWorkerOptions): {\n fetch: (request: Request, env: Env) => Response | Promise<Response>;\n} {\n return {\n fetch(request: Request, env: Env): Response | Promise<Response> {\n const url = new URL(request.url);\n const storage = new KvTakuhonStorage(env.TAKUHON_KV);\n const cachePurger: CachePurger = new CloudflareCachePurger(() => caches.default, {\n origin: url.origin,\n });\n const auditLogger: AuditLogger = consoleAuditLogger;\n\n const router = new Hono();\n router.notFound((c) =>\n problemResponse(c, {\n slug: ERROR_SLUGS.notFound,\n status: 404,\n title: 'Not Found',\n detail: `No route matches ${new URL(c.req.url).pathname}.`,\n }),\n );\n router.route(\n '/api/admin',\n createAdminApiApp({\n storage,\n getAdminToken: () => env.TAKUHON_ADMIN_TOKEN,\n getAdminOrigins: () => parseOrigins(env.TAKUHON_ADMIN_ORIGIN),\n cachePurger,\n auditLogger,\n }),\n );\n router.route('/admin', createAdminUiApp());\n router.route('/', createPublicApp({ storage, fallback: opts.fallback }));\n\n return router.fetch(request, env);\n },\n };\n}\n\nfunction bundledFallback(): Takuhon {\n const r = validate(exampleJson);\n if (!r.ok) throw new Error('Bundled fixture failed validation.');\n return r.data;\n}\n\nexport default createTakuhonWorker({ fallback: bundledFallback });\n","{\n \"schemaVersion\": \"0.1.0\",\n \"profile\": {\n \"displayName\": {\n \"en\": \"Pat Rivera\",\n \"ja\": \"パット・リベラ\"\n },\n \"tagline\": {\n \"en\": \"Open-source maintainer and accessibility advocate\",\n \"ja\": \"オープンソースメンテナ / アクセシビリティ実践者\"\n },\n \"bio\": {\n \"en\": \"Pat Rivera is a fictional persona used to exercise every field of the takuhon profile schema. They maintain a handful of open-source libraries focused on accessibility tooling and frequently speak at local meetups.\",\n \"ja\": \"Pat Rivera は takuhon プロフィール schema の全フィールドを示すための架空の人物です。アクセシビリティ系のオープンソースライブラリを保守し、地域コミュニティで登壇しています。\"\n },\n \"avatar\": {\n \"url\": \"/assets/avatar.webp\",\n \"alt\": {\n \"en\": \"Pat Rivera smiling, wearing round glasses, in front of a soft gradient.\",\n \"ja\": \"ソフトなグラデーションを背景に微笑む Pat Rivera。丸眼鏡を着用。\"\n }\n },\n \"location\": {\n \"country\": \"PT\",\n \"region\": \"Lisbon\",\n \"locality\": {\n \"en\": \"Lisbon\",\n \"ja\": \"リスボン\"\n },\n \"display\": {\n \"en\": \"Lisbon, Portugal\",\n \"ja\": \"ポルトガル・リスボン\"\n }\n }\n },\n \"links\": [\n {\n \"id\": \"website\",\n \"type\": \"website\",\n \"label\": { \"en\": \"Personal site\" },\n \"url\": \"https://example.com/pat\",\n \"featured\": true,\n \"order\": 0\n },\n {\n \"id\": \"github\",\n \"type\": \"github\",\n \"url\": \"https://github.com/example-pat\",\n \"featured\": true,\n \"order\": 1\n },\n {\n \"id\": \"mastodon\",\n \"type\": \"mastodon\",\n \"url\": \"https://example.social/@pat\",\n \"order\": 2\n },\n {\n \"id\": \"blog\",\n \"type\": \"blog\",\n \"label\": {\n \"en\": \"Notes on accessible UI\",\n \"ja\": \"アクセシブル UI の覚え書き\"\n },\n \"url\": \"https://example.com/pat/blog\",\n \"order\": 3\n },\n {\n \"id\": \"newsletter\",\n \"type\": \"custom\",\n \"label\": {\n \"en\": \"Weekly newsletter\",\n \"ja\": \"週次ニュースレター\"\n },\n \"url\": \"https://example.com/pat/newsletter\",\n \"iconUrl\": \"https://example.com/assets/icons/newsletter.svg\",\n \"order\": 4\n }\n ],\n \"careers\": [\n {\n \"id\": \"stellar-ux\",\n \"organization\": {\n \"en\": \"Stellar UX Studio\",\n \"ja\": \"ステラ UX スタジオ\"\n },\n \"role\": {\n \"en\": \"Principal Accessibility Engineer\",\n \"ja\": \"プリンシパル・アクセシビリティエンジニア\"\n },\n \"description\": {\n \"en\": \"Lead the accessibility engineering practice across product surfaces, drive WCAG 2.2 conformance reviews, and mentor a team of five engineers.\",\n \"ja\": \"プロダクト全体のアクセシビリティ設計を統括。WCAG 2.2 適合レビューを主導し、5 名のエンジニアをメンタリング。\"\n },\n \"startDate\": \"2023-04\",\n \"endDate\": null,\n \"isCurrent\": true,\n \"url\": \"https://example.com/stellar\",\n \"order\": 0\n },\n {\n \"id\": \"harbor-labs\",\n \"organization\": {\n \"en\": \"Harbor Labs\",\n \"ja\": \"ハーバーラボ\"\n },\n \"role\": {\n \"en\": \"Senior Frontend Engineer\",\n \"ja\": \"シニアフロントエンドエンジニア\"\n },\n \"description\": {\n \"en\": \"Built the design system foundation and an accessible component library used across nine internal products.\",\n \"ja\": \"デザインシステム基盤と、社内 9 プロダクトで利用されるアクセシブルなコンポーネントライブラリを設計。\"\n },\n \"startDate\": \"2019-06\",\n \"endDate\": \"2023-03\",\n \"order\": 1\n }\n ],\n \"projects\": [\n {\n \"id\": \"axe-helpers\",\n \"title\": {\n \"en\": \"axe-helpers\",\n \"ja\": \"axe-helpers\"\n },\n \"description\": {\n \"en\": \"A tiny set of utilities that wraps axe-core for use in component-level integration tests.\",\n \"ja\": \"コンポーネント単位の統合テスト向けに axe-core をラップする小規模ユーティリティ集。\"\n },\n \"url\": \"https://example.com/axe-helpers\",\n \"tags\": [\"accessibility\", \"testing\", \"typescript\"],\n \"relatedCareerId\": \"stellar-ux\",\n \"startDate\": \"2023-09\",\n \"highlighted\": true,\n \"order\": 0\n },\n {\n \"id\": \"color-contrast-cli\",\n \"title\": {\n \"en\": \"color-contrast-cli\",\n \"ja\": \"color-contrast-cli\"\n },\n \"description\": {\n \"en\": \"Command-line tool that audits design tokens for WCAG contrast ratios.\",\n \"ja\": \"デザイントークンの WCAG コントラスト比を監査するコマンドラインツール。\"\n },\n \"url\": \"https://example.com/color-contrast-cli\",\n \"tags\": [\"accessibility\", \"cli\", \"design-tokens\"],\n \"startDate\": \"2021-02\",\n \"endDate\": \"2022-08\",\n \"order\": 1\n },\n {\n \"id\": \"meetup-talks\",\n \"title\": {\n \"en\": \"Local meetup talks\",\n \"ja\": \"地域コミュニティ登壇\"\n },\n \"order\": 2\n }\n ],\n \"skills\": [\n { \"id\": \"typescript\", \"label\": \"TypeScript\", \"category\": \"programming\", \"order\": 0 },\n { \"id\": \"react\", \"label\": \"React\", \"category\": \"programming\", \"order\": 1 },\n { \"id\": \"wcag-2-2\", \"label\": \"WCAG 2.2\", \"category\": \"design\", \"order\": 2 },\n { \"id\": \"aria\", \"label\": \"ARIA\", \"category\": \"design\", \"order\": 3 },\n { \"id\": \"storybook\", \"label\": \"Storybook\", \"category\": \"programming\", \"order\": 4 },\n { \"id\": \"playwright\", \"label\": \"Playwright\", \"category\": \"programming\", \"order\": 5 },\n { \"id\": \"design-tokens\", \"label\": \"Design tokens\", \"category\": \"design\", \"order\": 6 },\n { \"id\": \"portuguese\", \"label\": \"Portuguese (B2)\", \"category\": \"language\", \"order\": 7 }\n ],\n \"contact\": {\n \"email\": \"pat@example.com\",\n \"showEmail\": false,\n \"formUrl\": \"https://example.com/pat/contact\"\n },\n \"settings\": {\n \"defaultLocale\": \"en\",\n \"fallbackLocale\": \"en\",\n \"availableLocales\": [\"en\", \"ja\"],\n \"theme\": \"default\",\n \"showPoweredBy\": true,\n \"enableJsonLd\": true,\n \"enableApi\": true,\n \"enableAnalytics\": false\n },\n \"meta\": {\n \"createdAt\": \"2026-01-15T09:00:00Z\",\n \"updatedAt\": \"2026-05-12T08:30:00Z\",\n \"generator\": \"Takuhon\",\n \"contentLicense\": {\n \"spdxId\": \"CC-BY-4.0\",\n \"url\": \"https://creativecommons.org/licenses/by/4.0/\",\n \"attribution\": {\n \"name\": \"Pat Rivera\",\n \"url\": \"https://example.com/pat\"\n }\n }\n }\n}\n","import type { CachePurger } from '@takuhon/api';\n\nexport interface CloudflareCachePurgerOptions {\n /**\n * Absolute origin (e.g. `https://example.com`) used to build the URLs\n * passed to `Cache.delete`. The Worker derives this from the incoming\n * request's URL so the same code works under any production hostname.\n */\n origin: string;\n /**\n * Locale codes to include when purging language-keyed cache entries.\n * Cloudflare caches `?lang=` query variants as distinct keys; we purge\n * a representative set on every write. Adapters can extend the list to\n * cover other locales the deploy serves.\n */\n langs?: string[];\n}\n\n/**\n * `CachePurger` backed by Cloudflare's colo-local `caches.default`.\n *\n * Limitations (documented in the adapter README):\n * - Cloudflare's `Cache.delete` clears the current colo only, not the\n * entire edge. Other colos honour the response's `Cache-Control`\n * `s-maxage` (5 minutes today) before refreshing.\n * - Truly global invalidation requires the REST `/purge_cache` API,\n * which needs a zone-scoped token; that's deferred to a later phase.\n */\nexport class CloudflareCachePurger implements CachePurger {\n private readonly origin: string;\n private readonly langs: string[];\n\n /**\n * `getCache` is a thunk so the Workers-only `caches` global is touched\n * lazily — public-only requests on this Worker never run admin handlers\n * and must not pay (or fail under Node tests) for the lookup.\n */\n constructor(\n private readonly getCache: () => Cache,\n opts: CloudflareCachePurgerOptions,\n ) {\n this.origin = opts.origin.replace(/\\/$/, '');\n this.langs = opts.langs ?? ['en', 'ja'];\n }\n\n async profileUpdated(): Promise<void> {\n await this.purge();\n }\n\n async profileDeleted(): Promise<void> {\n await this.purge();\n }\n\n private async purge(): Promise<void> {\n const cache = this.getCache();\n const targets = ['/', '/api/profile', '/api/jsonld', '/takuhon.json'];\n for (const lang of this.langs) {\n const q = `?lang=${encodeURIComponent(lang)}`;\n targets.push(`/api/profile${q}`, `/api/jsonld${q}`);\n }\n for (const path of targets) {\n await cache.delete(new Request(this.origin + path), { ignoreMethod: true });\n }\n }\n}\n","import type { AuditEvent, AuditLogger } from '@takuhon/api';\n\n/**\n * `AuditLogger` that writes a single line of JSON per event to `console.log`.\n *\n * Cloudflare captures these via Workers Tail / Logpush, where they can be\n * routed to R2, S3, or any downstream SIEM. Token bodies never reach this\n * sink — the upstream middleware only emits `sha256:<hex>` digests in\n * `actor.tokenHash`.\n */\nexport const consoleAuditLogger: AuditLogger = (event: AuditEvent): void => {\n console.log(JSON.stringify(event));\n};\n","import { ConflictError, NotFoundError, type Takuhon, type TakuhonStorage } from '@takuhon/core';\n\nexport const KV_KEY = 'TAKUHON_DATA';\n\nexport interface KvMetadata {\n version: string;\n updatedAt: string;\n}\n\n/**\n * Cloudflare KV implementation of the `TakuhonStorage` contract. Stores the\n * profile document as JSON under a single key (`TAKUHON_DATA`) and tracks the\n * optimistic-locking token inside KV value metadata.\n *\n * `version` is a fresh UUIDv4 on every successful write. Callers compare it\n * verbatim against the `If-Match` precondition; mismatches raise\n * `ConflictError` with `currentVersion` so the API layer can build the RFC\n * 7807 envelope without an extra round trip.\n */\nexport class KvTakuhonStorage implements TakuhonStorage {\n constructor(private readonly kv: KVNamespace) {}\n\n async getProfile(): Promise<{ data: Takuhon; version: string }> {\n const result = await this.kv.getWithMetadata<Takuhon, KvMetadata>(KV_KEY, 'json');\n if (result.value === null || !result.metadata?.version) {\n throw new NotFoundError(`No profile is stored at KV key \"${KV_KEY}\".`);\n }\n return { data: result.value, version: result.metadata.version };\n }\n\n async saveProfile(data: Takuhon, ifMatch?: string): Promise<{ version: string }> {\n if (ifMatch !== undefined) {\n const current = await this.kv.getWithMetadata<Takuhon, KvMetadata>(KV_KEY, 'json');\n const currentVersion = current.metadata?.version;\n if (currentVersion !== ifMatch) {\n throw new ConflictError(\n `If-Match preconditioned on version \"${ifMatch}\" but current is \"${currentVersion ?? 'absent'}\".`,\n { currentVersion },\n );\n }\n }\n const version = crypto.randomUUID();\n const updatedAt = new Date().toISOString();\n await this.kv.put(KV_KEY, JSON.stringify(data), {\n metadata: { version, updatedAt } satisfies KvMetadata,\n });\n return { version };\n }\n\n async deleteProfile(): Promise<void> {\n await this.kv.delete(KV_KEY);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@takuhon/cloudflare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cloudflare Workers adapter for takuhon — public API, Workers Assets, KV, R2 integrations",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Takuhon contributors",
|
|
7
|
+
"homepage": "https://github.com/takuhon-dev/takuhon/tree/main/adapters/cloudflare#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/takuhon-dev/takuhon.git",
|
|
11
|
+
"directory": "adapters/cloudflare"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/takuhon-dev/takuhon/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"takuhon",
|
|
18
|
+
"profile",
|
|
19
|
+
"cloudflare-workers",
|
|
20
|
+
"workers",
|
|
21
|
+
"adapter",
|
|
22
|
+
"hono"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"NOTICE"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"hono": "^4.12.19",
|
|
47
|
+
"@takuhon/core": "0.1.0",
|
|
48
|
+
"@takuhon/api": "0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@cloudflare/workers-types": "^4.20251101.0",
|
|
52
|
+
"wrangler": "^4.30.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"typecheck": "tsc",
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"dev": "wrangler dev"
|
|
59
|
+
}
|
|
60
|
+
}
|