@dytsou/shorten-url 2.4.4 → 2.4.5
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/README.md +88 -50
- package/config/config.example.js +137 -0
- package/config/config.js +19 -0
- package/docs/about.md +360 -0
- package/docs/api/openapi.yaml +441 -81
- package/docs/flagship.md +58 -0
- package/docs/plans/2026-09-06-001-refactor-worker-hosted-react-assets-plan.md +308 -0
- package/frontend/about-template.html +124 -0
- package/frontend/index.html +17 -0
- package/frontend/package.json +20 -0
- package/frontend/pnpm-lock.yaml +1129 -0
- package/frontend/src/App.jsx +1264 -0
- package/frontend/src/api.js +272 -0
- package/frontend/src/local-flag-mock.js +55 -0
- package/frontend/src/main.jsx +4 -0
- package/frontend/src/styles/app.css +2253 -0
- package/frontend/static/favicon.svg +5 -0
- package/frontend/vite.config.js +34 -0
- package/package.json +6 -4
- package/src/lib/access.js +241 -1
- package/src/lib/assets.js +58 -0
- package/src/lib/endpoints.js +33 -8
- package/src/lib/feature-toggle.js +464 -0
- package/src/lib/flag-management.js +137 -0
- package/src/lib/flag-routes.js +155 -0
- package/src/lib/flagship.js +269 -0
- package/src/lib/observability.js +34 -0
- package/src/lib/responses.js +39 -3
- package/src/lib/runtime-config.js +90 -0
- package/src/lib/safety.js +15 -0
- package/src/lib/shortener.js +2 -2
- package/src/lib/worker-handler.js +122 -0
- package/src/worker.js +3 -66
package/docs/api/openapi.yaml
CHANGED
|
@@ -3,102 +3,67 @@ info:
|
|
|
3
3
|
title: Shorten URL Service
|
|
4
4
|
version: 1.0.0
|
|
5
5
|
description: |
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
URL shortening and first-party Flagship management API for the Shorten URL Worker.
|
|
7
|
+
|
|
8
|
+
Shortening requests use `POST /`, regardless of the page path. The production
|
|
9
|
+
branch may mount the product page at `/shorten`; API routes remain rooted at `/`.
|
|
10
|
+
Short links resolve through `GET /{key}`. Flag definitions are managed at
|
|
11
|
+
`/settings/api/*`; these routes require Cloudflare Access. Mutating management
|
|
12
|
+
requests also require a CSRF token obtained from `/settings/api/csrf`.
|
|
13
|
+
|
|
14
|
+
The interactive API reference is published separately from the Worker at the
|
|
15
|
+
documentation site's `/api/` path.
|
|
12
16
|
|
|
13
17
|
servers:
|
|
14
18
|
- url: /
|
|
15
19
|
|
|
20
|
+
tags:
|
|
21
|
+
- name: Short links
|
|
22
|
+
description: Create and follow shortened URLs.
|
|
23
|
+
- name: Flagship management
|
|
24
|
+
description: Manage the `shorten-routing` flag through the Worker control plane.
|
|
25
|
+
|
|
16
26
|
paths:
|
|
17
27
|
/:
|
|
18
28
|
post:
|
|
29
|
+
tags: [Short links]
|
|
30
|
+
operationId: shortenUrl
|
|
19
31
|
summary: Shorten a URL
|
|
20
|
-
description: Create a
|
|
32
|
+
description: Create a short URL, optionally with a custom slug.
|
|
33
|
+
security:
|
|
34
|
+
- CloudflareAccessJwt: []
|
|
35
|
+
CloudflareAccessUserEmail: []
|
|
21
36
|
requestBody:
|
|
22
37
|
required: true
|
|
23
38
|
content:
|
|
24
39
|
application/json:
|
|
25
40
|
schema:
|
|
26
|
-
|
|
27
|
-
required:
|
|
28
|
-
- url
|
|
29
|
-
properties:
|
|
30
|
-
url:
|
|
31
|
-
type: string
|
|
32
|
-
format: uri
|
|
33
|
-
description: The URL to shorten
|
|
34
|
-
example: https://www.example.com/very/long/url/path
|
|
35
|
-
custom_slug:
|
|
36
|
-
type: string
|
|
37
|
-
pattern: '^[a-zA-Z0-9\-_]+$'
|
|
38
|
-
maxLength: 50
|
|
39
|
-
description: Optional custom slug for the short URL
|
|
40
|
-
example: my-custom-link
|
|
41
|
+
$ref: "#/components/schemas/ShortenRequest"
|
|
41
42
|
responses:
|
|
42
43
|
"201":
|
|
43
|
-
|
|
44
|
-
content:
|
|
45
|
-
application/json:
|
|
46
|
-
schema:
|
|
47
|
-
type: object
|
|
48
|
-
properties:
|
|
49
|
-
short_url:
|
|
50
|
-
type: string
|
|
51
|
-
format: uri
|
|
52
|
-
example: https://your-worker-domain.example/abc123
|
|
44
|
+
$ref: "#/components/responses/ShortenCreated"
|
|
53
45
|
"400":
|
|
54
|
-
|
|
55
|
-
content:
|
|
56
|
-
application/json:
|
|
57
|
-
schema:
|
|
58
|
-
type: object
|
|
59
|
-
properties:
|
|
60
|
-
status:
|
|
61
|
-
type: integer
|
|
62
|
-
example: 400
|
|
63
|
-
message:
|
|
64
|
-
type: string
|
|
65
|
-
example: Invalid URL format
|
|
46
|
+
$ref: "#/components/responses/BadRequest"
|
|
66
47
|
"403":
|
|
67
|
-
|
|
68
|
-
content:
|
|
69
|
-
application/json:
|
|
70
|
-
schema:
|
|
71
|
-
$ref: "#/components/schemas/ErrorResponse"
|
|
48
|
+
$ref: "#/components/responses/AccessDenied"
|
|
72
49
|
"404":
|
|
73
|
-
|
|
74
|
-
content:
|
|
75
|
-
application/json:
|
|
76
|
-
schema:
|
|
77
|
-
$ref: "#/components/schemas/ErrorResponse"
|
|
50
|
+
$ref: "#/components/responses/NotFound"
|
|
78
51
|
"500":
|
|
79
|
-
|
|
80
|
-
content:
|
|
81
|
-
application/json:
|
|
82
|
-
schema:
|
|
83
|
-
type: object
|
|
84
|
-
properties:
|
|
85
|
-
status:
|
|
86
|
-
type: integer
|
|
87
|
-
example: 500
|
|
88
|
-
message:
|
|
89
|
-
type: string
|
|
90
|
-
example: Server error
|
|
52
|
+
$ref: "#/components/responses/ServerError"
|
|
91
53
|
get:
|
|
92
|
-
|
|
93
|
-
|
|
54
|
+
tags: [Short links]
|
|
55
|
+
operationId: getHomepage
|
|
56
|
+
summary: Get the Shorten URL application
|
|
94
57
|
responses:
|
|
95
58
|
"200":
|
|
96
|
-
description:
|
|
59
|
+
description: Worker-hosted application HTML.
|
|
97
60
|
content:
|
|
98
61
|
text/html:
|
|
99
62
|
schema:
|
|
100
63
|
type: string
|
|
101
64
|
options:
|
|
65
|
+
tags: [Short links]
|
|
66
|
+
operationId: preflightHomepage
|
|
102
67
|
summary: Handle CORS preflight
|
|
103
68
|
responses:
|
|
104
69
|
"204":
|
|
@@ -106,59 +71,454 @@ paths:
|
|
|
106
71
|
|
|
107
72
|
/{key}:
|
|
108
73
|
get:
|
|
109
|
-
|
|
110
|
-
|
|
74
|
+
tags: [Short links]
|
|
75
|
+
operationId: followShortUrl
|
|
76
|
+
summary: Follow a shortened URL
|
|
111
77
|
parameters:
|
|
112
78
|
- in: path
|
|
113
79
|
name: key
|
|
114
80
|
required: true
|
|
81
|
+
description: Short URL key.
|
|
115
82
|
schema:
|
|
116
83
|
type: string
|
|
117
|
-
description: The short URL key
|
|
118
84
|
example: abc123
|
|
119
85
|
responses:
|
|
120
86
|
"302":
|
|
121
|
-
description: Redirect to
|
|
87
|
+
description: Redirect to the stored destination.
|
|
122
88
|
headers:
|
|
123
89
|
Location:
|
|
90
|
+
description: Destination URL.
|
|
124
91
|
schema:
|
|
125
92
|
type: string
|
|
126
93
|
format: uri
|
|
127
94
|
"404":
|
|
128
|
-
description: Short URL not found
|
|
95
|
+
description: Short URL was not found.
|
|
129
96
|
content:
|
|
130
97
|
text/html:
|
|
131
98
|
schema:
|
|
132
99
|
type: string
|
|
133
100
|
|
|
101
|
+
/settings/api/csrf:
|
|
102
|
+
get:
|
|
103
|
+
tags: [Flagship management]
|
|
104
|
+
operationId: getSettingsCsrfToken
|
|
105
|
+
summary: Get a CSRF token for management writes
|
|
106
|
+
description: |
|
|
107
|
+
Requires Cloudflare Access authentication. The token is scoped to the request
|
|
108
|
+
origin and authenticated user's email. Use it in the `X-CSRF-Token` header
|
|
109
|
+
on each management POST or PUT request.
|
|
110
|
+
security:
|
|
111
|
+
- CloudflareAccessJwt: []
|
|
112
|
+
CloudflareAccessUserEmail: []
|
|
113
|
+
responses:
|
|
114
|
+
"200":
|
|
115
|
+
description: CSRF token created.
|
|
116
|
+
content:
|
|
117
|
+
application/json:
|
|
118
|
+
schema:
|
|
119
|
+
$ref: "#/components/schemas/CsrfTokenResponse"
|
|
120
|
+
"403":
|
|
121
|
+
$ref: "#/components/responses/AccessDenied"
|
|
122
|
+
"503":
|
|
123
|
+
$ref: "#/components/responses/SettingsUnavailable"
|
|
124
|
+
|
|
125
|
+
/settings/api/flags:
|
|
126
|
+
get:
|
|
127
|
+
tags: [Flagship management]
|
|
128
|
+
operationId: listShorteningFlags
|
|
129
|
+
summary: Read the managed shortening flag
|
|
130
|
+
description: Returns the `shorten-routing` flag when it exists.
|
|
131
|
+
security:
|
|
132
|
+
- CloudflareAccessJwt: []
|
|
133
|
+
CloudflareAccessUserEmail: []
|
|
134
|
+
responses:
|
|
135
|
+
"200":
|
|
136
|
+
description: Managed flag list.
|
|
137
|
+
content:
|
|
138
|
+
application/json:
|
|
139
|
+
schema:
|
|
140
|
+
$ref: "#/components/schemas/FlagListResponse"
|
|
141
|
+
"403":
|
|
142
|
+
$ref: "#/components/responses/AccessDenied"
|
|
143
|
+
"502":
|
|
144
|
+
$ref: "#/components/responses/FlagshipError"
|
|
145
|
+
|
|
146
|
+
/settings/api/flag:
|
|
147
|
+
post:
|
|
148
|
+
tags: [Flagship management]
|
|
149
|
+
operationId: createShorteningFlag
|
|
150
|
+
summary: Create the shortening flag
|
|
151
|
+
description: |
|
|
152
|
+
Creates the `shorten-routing` flag. Send the same-origin `Origin` header
|
|
153
|
+
(or a same-origin `Referer` when `Origin` is absent) and the CSRF token
|
|
154
|
+
returned by `GET /settings/api/csrf`.
|
|
155
|
+
security:
|
|
156
|
+
- CloudflareAccessJwt: []
|
|
157
|
+
CloudflareAccessUserEmail: []
|
|
158
|
+
parameters:
|
|
159
|
+
- $ref: "#/components/parameters/CsrfToken"
|
|
160
|
+
requestBody:
|
|
161
|
+
required: true
|
|
162
|
+
content:
|
|
163
|
+
application/json:
|
|
164
|
+
schema:
|
|
165
|
+
$ref: "#/components/schemas/FlagDefinition"
|
|
166
|
+
responses:
|
|
167
|
+
"201":
|
|
168
|
+
description: Flag created.
|
|
169
|
+
content:
|
|
170
|
+
application/json:
|
|
171
|
+
schema:
|
|
172
|
+
$ref: "#/components/schemas/FlagResponse"
|
|
173
|
+
"400":
|
|
174
|
+
$ref: "#/components/responses/BadRequest"
|
|
175
|
+
"403":
|
|
176
|
+
$ref: "#/components/responses/AccessDenied"
|
|
177
|
+
"413":
|
|
178
|
+
$ref: "#/components/responses/PayloadTooLarge"
|
|
179
|
+
"415":
|
|
180
|
+
$ref: "#/components/responses/JsonRequired"
|
|
181
|
+
"502":
|
|
182
|
+
$ref: "#/components/responses/FlagshipError"
|
|
183
|
+
|
|
184
|
+
/settings/api/flag/shorten-routing:
|
|
185
|
+
put:
|
|
186
|
+
tags: [Flagship management]
|
|
187
|
+
operationId: saveShorteningFlag
|
|
188
|
+
summary: Save a shortening flag draft
|
|
189
|
+
description: |
|
|
190
|
+
Saves the draft using optimistic concurrency. `expectedUpdatedAt` must
|
|
191
|
+
match the currently saved version. Include the same-origin `Origin`
|
|
192
|
+
header (or a same-origin `Referer` when absent) and the CSRF token.
|
|
193
|
+
security:
|
|
194
|
+
- CloudflareAccessJwt: []
|
|
195
|
+
CloudflareAccessUserEmail: []
|
|
196
|
+
parameters:
|
|
197
|
+
- $ref: "#/components/parameters/CsrfToken"
|
|
198
|
+
requestBody:
|
|
199
|
+
required: true
|
|
200
|
+
content:
|
|
201
|
+
application/json:
|
|
202
|
+
schema:
|
|
203
|
+
$ref: "#/components/schemas/SaveFlagRequest"
|
|
204
|
+
responses:
|
|
205
|
+
"200":
|
|
206
|
+
description: Draft saved.
|
|
207
|
+
content:
|
|
208
|
+
application/json:
|
|
209
|
+
schema:
|
|
210
|
+
$ref: "#/components/schemas/FlagResponse"
|
|
211
|
+
"400":
|
|
212
|
+
$ref: "#/components/responses/BadRequest"
|
|
213
|
+
"403":
|
|
214
|
+
$ref: "#/components/responses/AccessDenied"
|
|
215
|
+
"409":
|
|
216
|
+
$ref: "#/components/responses/VersionConflict"
|
|
217
|
+
"413":
|
|
218
|
+
$ref: "#/components/responses/PayloadTooLarge"
|
|
219
|
+
"415":
|
|
220
|
+
$ref: "#/components/responses/JsonRequired"
|
|
221
|
+
"502":
|
|
222
|
+
$ref: "#/components/responses/FlagshipError"
|
|
223
|
+
|
|
224
|
+
/settings/api/flag/shorten-routing/publish:
|
|
225
|
+
post:
|
|
226
|
+
tags: [Flagship management]
|
|
227
|
+
operationId: publishShorteningFlag
|
|
228
|
+
summary: Publish the saved shortening flag
|
|
229
|
+
description: |
|
|
230
|
+
Publishes the current saved version. The required `expectedUpdatedAt`
|
|
231
|
+
prevents publishing a draft that changed after it was loaded. Include the
|
|
232
|
+
same-origin `Origin` header (or a same-origin `Referer` when absent) and
|
|
233
|
+
the CSRF token.
|
|
234
|
+
security:
|
|
235
|
+
- CloudflareAccessJwt: []
|
|
236
|
+
CloudflareAccessUserEmail: []
|
|
237
|
+
parameters:
|
|
238
|
+
- $ref: "#/components/parameters/CsrfToken"
|
|
239
|
+
requestBody:
|
|
240
|
+
required: true
|
|
241
|
+
content:
|
|
242
|
+
application/json:
|
|
243
|
+
schema:
|
|
244
|
+
$ref: "#/components/schemas/PublishFlagRequest"
|
|
245
|
+
responses:
|
|
246
|
+
"200":
|
|
247
|
+
description: Flag published.
|
|
248
|
+
content:
|
|
249
|
+
application/json:
|
|
250
|
+
schema:
|
|
251
|
+
$ref: "#/components/schemas/FlagResponse"
|
|
252
|
+
"403":
|
|
253
|
+
$ref: "#/components/responses/AccessDenied"
|
|
254
|
+
"409":
|
|
255
|
+
$ref: "#/components/responses/VersionConflict"
|
|
256
|
+
"413":
|
|
257
|
+
$ref: "#/components/responses/PayloadTooLarge"
|
|
258
|
+
"415":
|
|
259
|
+
$ref: "#/components/responses/JsonRequired"
|
|
260
|
+
"502":
|
|
261
|
+
$ref: "#/components/responses/FlagshipError"
|
|
262
|
+
|
|
134
263
|
components:
|
|
264
|
+
securitySchemes:
|
|
265
|
+
CloudflareAccessJwt:
|
|
266
|
+
type: apiKey
|
|
267
|
+
in: header
|
|
268
|
+
name: Cf-Access-Jwt-Assertion
|
|
269
|
+
description: Cloudflare Access JWT assertion supplied by the Access-protected hostname.
|
|
270
|
+
CloudflareAccessUserEmail:
|
|
271
|
+
type: apiKey
|
|
272
|
+
in: header
|
|
273
|
+
name: Cf-Access-Authenticated-User-Email
|
|
274
|
+
description: Authenticated user's email supplied by Cloudflare Access.
|
|
275
|
+
parameters:
|
|
276
|
+
CsrfToken:
|
|
277
|
+
in: header
|
|
278
|
+
name: X-CSRF-Token
|
|
279
|
+
required: true
|
|
280
|
+
description: Token returned by `GET /settings/api/csrf`.
|
|
281
|
+
schema:
|
|
282
|
+
type: string
|
|
135
283
|
schemas:
|
|
136
284
|
ShortenRequest:
|
|
137
285
|
type: object
|
|
138
|
-
required:
|
|
139
|
-
- url
|
|
286
|
+
required: [url]
|
|
140
287
|
properties:
|
|
141
288
|
url:
|
|
142
289
|
type: string
|
|
143
290
|
format: uri
|
|
144
|
-
description:
|
|
291
|
+
description: Destination URL to shorten.
|
|
292
|
+
example: https://www.example.com/very/long/url/path
|
|
145
293
|
custom_slug:
|
|
146
294
|
type: string
|
|
147
|
-
pattern:
|
|
295
|
+
pattern: "^[A-Za-z0-9_-]{1,50}$"
|
|
148
296
|
maxLength: 50
|
|
149
|
-
description: Optional custom slug
|
|
150
|
-
|
|
297
|
+
description: Optional custom slug; omitted or blank values use a generated key.
|
|
298
|
+
example: spring-launch
|
|
151
299
|
ShortenResponse:
|
|
152
300
|
type: object
|
|
301
|
+
required: [short_url]
|
|
153
302
|
properties:
|
|
154
303
|
short_url:
|
|
155
304
|
type: string
|
|
156
305
|
format: uri
|
|
157
|
-
|
|
306
|
+
example: https://your-worker-domain.example/abc123
|
|
307
|
+
CsrfTokenResponse:
|
|
308
|
+
type: object
|
|
309
|
+
required: [csrfToken]
|
|
310
|
+
properties:
|
|
311
|
+
csrfToken:
|
|
312
|
+
type: string
|
|
313
|
+
description: Origin- and user-scoped token required for management writes.
|
|
314
|
+
FlagDefinition:
|
|
315
|
+
type: object
|
|
316
|
+
description: Draft definition for the single `shorten-routing` flag.
|
|
317
|
+
required: [enabled, defaultVariant, variants]
|
|
318
|
+
properties:
|
|
319
|
+
key:
|
|
320
|
+
type: string
|
|
321
|
+
const: shorten-routing
|
|
322
|
+
description:
|
|
323
|
+
type: string
|
|
324
|
+
maxLength: 512
|
|
325
|
+
enabled:
|
|
326
|
+
type: boolean
|
|
327
|
+
defaultVariant:
|
|
328
|
+
type: string
|
|
329
|
+
description: Key of the default destination variant.
|
|
330
|
+
variants:
|
|
331
|
+
type: array
|
|
332
|
+
minItems: 1
|
|
333
|
+
items:
|
|
334
|
+
$ref: "#/components/schemas/FlagVariant"
|
|
335
|
+
rules:
|
|
336
|
+
type: array
|
|
337
|
+
items:
|
|
338
|
+
$ref: "#/components/schemas/TargetingRule"
|
|
339
|
+
FlagVariant:
|
|
340
|
+
type: object
|
|
341
|
+
required: [key, value]
|
|
342
|
+
properties:
|
|
343
|
+
key:
|
|
344
|
+
type: string
|
|
345
|
+
pattern: "^[A-Za-z0-9_-]{1,64}$"
|
|
346
|
+
value:
|
|
347
|
+
type: object
|
|
348
|
+
required: [url]
|
|
349
|
+
properties:
|
|
350
|
+
url:
|
|
351
|
+
type: string
|
|
352
|
+
format: uri
|
|
353
|
+
description: Safe HTTP or HTTPS destination for this variant.
|
|
354
|
+
TargetingRule:
|
|
355
|
+
type: object
|
|
356
|
+
required: [priority, variant]
|
|
357
|
+
properties:
|
|
358
|
+
priority:
|
|
359
|
+
type: integer
|
|
360
|
+
minimum: 1
|
|
361
|
+
description: Unique rule priority; lower numbers are evaluated first.
|
|
362
|
+
variant:
|
|
363
|
+
type: string
|
|
364
|
+
description: Variant key to serve when this rule matches.
|
|
365
|
+
conditions:
|
|
366
|
+
type: array
|
|
367
|
+
maxItems: 16
|
|
368
|
+
description: All conditions must match; an empty list matches every visitor.
|
|
369
|
+
items:
|
|
370
|
+
$ref: "#/components/schemas/TargetingCondition"
|
|
371
|
+
rolloutPercentage:
|
|
372
|
+
type: number
|
|
373
|
+
minimum: 0
|
|
374
|
+
maximum: 100
|
|
375
|
+
multipleOf: 0.01
|
|
376
|
+
description: Optional stable per-visitor rollout percentage from 0 to 100.
|
|
377
|
+
TargetingCondition:
|
|
378
|
+
type: object
|
|
379
|
+
required: [attribute, operator, value]
|
|
380
|
+
properties:
|
|
381
|
+
attribute:
|
|
382
|
+
type: string
|
|
383
|
+
enum:
|
|
384
|
+
- country
|
|
385
|
+
- regionCode
|
|
386
|
+
- continent
|
|
387
|
+
- timezone
|
|
388
|
+
- language
|
|
389
|
+
- utmSource
|
|
390
|
+
- utmMedium
|
|
391
|
+
- utmCampaign
|
|
392
|
+
operator:
|
|
393
|
+
type: string
|
|
394
|
+
enum:
|
|
395
|
+
- equals
|
|
396
|
+
- not_equals
|
|
397
|
+
- contains
|
|
398
|
+
- starts_with
|
|
399
|
+
- ends_with
|
|
400
|
+
- in
|
|
401
|
+
- not_in
|
|
402
|
+
value:
|
|
403
|
+
oneOf:
|
|
404
|
+
- type: string
|
|
405
|
+
minLength: 1
|
|
406
|
+
maxLength: 256
|
|
407
|
+
- type: array
|
|
408
|
+
minItems: 1
|
|
409
|
+
maxItems: 50
|
|
410
|
+
items:
|
|
411
|
+
type: string
|
|
412
|
+
minLength: 1
|
|
413
|
+
maxLength: 256
|
|
414
|
+
description: Use a string except with `in` and `not_in`, which require a string list. Country values must be two-letter codes.
|
|
415
|
+
SaveFlagRequest:
|
|
416
|
+
allOf:
|
|
417
|
+
- $ref: "#/components/schemas/FlagDefinition"
|
|
418
|
+
- type: object
|
|
419
|
+
required: [expectedUpdatedAt]
|
|
420
|
+
properties:
|
|
421
|
+
expectedUpdatedAt:
|
|
422
|
+
type: string
|
|
423
|
+
minLength: 1
|
|
424
|
+
description: Version identifier returned with the currently saved flag.
|
|
425
|
+
PublishFlagRequest:
|
|
426
|
+
type: object
|
|
427
|
+
required: [expectedUpdatedAt]
|
|
428
|
+
properties:
|
|
429
|
+
expectedUpdatedAt:
|
|
430
|
+
type: string
|
|
431
|
+
minLength: 1
|
|
432
|
+
description: Version identifier returned with the currently saved flag.
|
|
433
|
+
SavedFlag:
|
|
434
|
+
allOf:
|
|
435
|
+
- $ref: "#/components/schemas/FlagDefinition"
|
|
436
|
+
- type: object
|
|
437
|
+
required: [key, updatedAt]
|
|
438
|
+
properties:
|
|
439
|
+
updatedAt:
|
|
440
|
+
type: [string, "null"]
|
|
441
|
+
description: Timestamp/version for optimistic concurrency.
|
|
442
|
+
FlagResponse:
|
|
443
|
+
type: object
|
|
444
|
+
required: [flag]
|
|
445
|
+
properties:
|
|
446
|
+
flag:
|
|
447
|
+
$ref: "#/components/schemas/SavedFlag"
|
|
448
|
+
FlagListResponse:
|
|
449
|
+
type: object
|
|
450
|
+
required: [flags]
|
|
451
|
+
properties:
|
|
452
|
+
flags:
|
|
453
|
+
type: array
|
|
454
|
+
items:
|
|
455
|
+
$ref: "#/components/schemas/SavedFlag"
|
|
158
456
|
ErrorResponse:
|
|
159
457
|
type: object
|
|
458
|
+
required: [status, message]
|
|
160
459
|
properties:
|
|
161
460
|
status:
|
|
162
461
|
type: integer
|
|
163
462
|
message:
|
|
164
463
|
type: string
|
|
464
|
+
responses:
|
|
465
|
+
ShortenCreated:
|
|
466
|
+
description: URL successfully shortened.
|
|
467
|
+
content:
|
|
468
|
+
application/json:
|
|
469
|
+
schema:
|
|
470
|
+
$ref: "#/components/schemas/ShortenResponse"
|
|
471
|
+
BadRequest:
|
|
472
|
+
description: Invalid request body or flag definition.
|
|
473
|
+
content:
|
|
474
|
+
application/json:
|
|
475
|
+
schema:
|
|
476
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
477
|
+
AccessDenied:
|
|
478
|
+
description: Cloudflare Access authentication, origin validation, or CSRF validation failed.
|
|
479
|
+
content:
|
|
480
|
+
application/json:
|
|
481
|
+
schema:
|
|
482
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
483
|
+
NotFound:
|
|
484
|
+
description: API route was not found.
|
|
485
|
+
content:
|
|
486
|
+
application/json:
|
|
487
|
+
schema:
|
|
488
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
489
|
+
ServerError:
|
|
490
|
+
description: Shortening request failed.
|
|
491
|
+
content:
|
|
492
|
+
application/json:
|
|
493
|
+
schema:
|
|
494
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
495
|
+
SettingsUnavailable:
|
|
496
|
+
description: Settings API is not configured.
|
|
497
|
+
content:
|
|
498
|
+
application/json:
|
|
499
|
+
schema:
|
|
500
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
501
|
+
FlagshipError:
|
|
502
|
+
description: Flagship provider request failed.
|
|
503
|
+
content:
|
|
504
|
+
application/json:
|
|
505
|
+
schema:
|
|
506
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
507
|
+
VersionConflict:
|
|
508
|
+
description: Expected version is missing or no longer current.
|
|
509
|
+
content:
|
|
510
|
+
application/json:
|
|
511
|
+
schema:
|
|
512
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
513
|
+
PayloadTooLarge:
|
|
514
|
+
description: Request body exceeds the maximum allowed size.
|
|
515
|
+
content:
|
|
516
|
+
application/json:
|
|
517
|
+
schema:
|
|
518
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
519
|
+
JsonRequired:
|
|
520
|
+
description: Management writes require an application/json request body.
|
|
521
|
+
content:
|
|
522
|
+
application/json:
|
|
523
|
+
schema:
|
|
524
|
+
$ref: "#/components/schemas/ErrorResponse"
|
package/docs/flagship.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Flagship shortening controls
|
|
2
|
+
|
|
3
|
+
The two maintained Worker entrypoints use the first-party Cloudflare Flagship
|
|
4
|
+
binding when it is provisioned as `env.FLAGS`. The binding is intentionally
|
|
5
|
+
behind a typed adapter in `src/lib/flagship.js`: if the binding is absent, its
|
|
6
|
+
response is malformed, or evaluation reports an error, requests use the
|
|
7
|
+
existing shortening page and redirect behavior.
|
|
8
|
+
|
|
9
|
+
## Provisioning
|
|
10
|
+
|
|
11
|
+
1. Create a Flagship app and replace the placeholder in the `[[flagship]]`
|
|
12
|
+
block in `wrangler.toml` (see `wrangler.toml.example`).
|
|
13
|
+
2. Create the JSON flag `shorten-routing`. Variations must be objects with a
|
|
14
|
+
single `url` field. Rules can target the `country` attribute with `in` and
|
|
15
|
+
two-letter country codes, apply a percentage rollout, or combine both.
|
|
16
|
+
Percentage rules use Flagship's `targetingKey` bucketing attribute. The
|
|
17
|
+
Worker assigns an opaque first-party cookie so a browser keeps its canary
|
|
18
|
+
assignment between visits. Set the control variation as the default; users
|
|
19
|
+
outside a rollout receive its destination.
|
|
20
|
+
A single 5% rule gives a 5% canary and sends the rest to the default. When
|
|
21
|
+
splitting one audience across multiple variants, use cumulative thresholds:
|
|
22
|
+
30%, 70%, and 100% produce a 30/40/30 split. Repeat the same audience
|
|
23
|
+
conditions and bucketing attribute on those rules.
|
|
24
|
+
3. Configure the protected settings API secrets/variables:
|
|
25
|
+
`CLOUDFLARE_ACCOUNT_ID`, `FLAGSHIP_APP_ID`, `FLAGSHIP_API_TOKEN`,
|
|
26
|
+
`FLAGSHIP_CSRF_SECRET`, `CF_ACCESS_ISSUER`, and `CF_ACCESS_AUDIENCE`.
|
|
27
|
+
`CF_ACCESS_JWKS_URL` is optional when the standard Access certificate URL
|
|
28
|
+
is available.
|
|
29
|
+
4. Deploy behind the same Cloudflare Access application that protects
|
|
30
|
+
`/shorten`. Operators open `/` in the deployed Worker and switch to the Flagship tab; there is
|
|
31
|
+
no standalone settings page route.
|
|
32
|
+
|
|
33
|
+
When the built `frontend/` app is deployed to GitHub Pages or another static
|
|
34
|
+
host, set its `shorten-url-worker-origin` meta tag to the public Worker origin.
|
|
35
|
+
The frontend tabs and settings API requests use that origin. Leave the tag
|
|
36
|
+
empty when the Worker serves the page so the current origin remains the
|
|
37
|
+
fallback. The origin is public and must also match `frontend.workerOrigin` in
|
|
38
|
+
the local config example; do not put any Access, Flagship, or CSRF secret in
|
|
39
|
+
the HTML or frontend config.
|
|
40
|
+
|
|
41
|
+
The settings API uses the documented Cloudflare Flagship management API for
|
|
42
|
+
country targeting, percentage rollouts, listing, creating, updating, and
|
|
43
|
+
enabling the published flag. The API's full-replacement update is the atomic
|
|
44
|
+
publication boundary; there is no second Worker-owned draft store. API tokens
|
|
45
|
+
stay server-side. Writes require
|
|
46
|
+
a verified Access JWT, JSON requests from the Worker or configured frontend
|
|
47
|
+
origin, and an HMAC CSRF token returned by the settings API. A stale
|
|
48
|
+
`updated_at` value is rejected with
|
|
49
|
+
`409` instead of overwriting another operator's edit.
|
|
50
|
+
|
|
51
|
+
The installed repository tooling does not provide a `Flagship` TypeScript
|
|
52
|
+
declaration or management client package, so the adapter uses the documented
|
|
53
|
+
binding method (`getObjectDetails`) and an explicit JSDoc interface. This
|
|
54
|
+
keeps local Vitest runs runnable without inventing a second KV-backed flag
|
|
55
|
+
authority. Until the official binding is configured, settings management
|
|
56
|
+
returns a non-sensitive `503` and visitor traffic falls back safely. Mutations
|
|
57
|
+
accept the Worker origin or the configured origin that hosts `frontend/`, and
|
|
58
|
+
the Worker returns credential-compatible CORS headers for that frontend.
|