@dytsou/shorten-url 2.4.3 → 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.
@@ -3,148 +3,522 @@ info:
3
3
  title: Shorten URL Service
4
4
  version: 1.0.0
5
5
  description: |
6
- A modern, fast URL shortener built with Cloudflare Workers and a responsive web interface.
7
- - Custom slugs are supported
8
- - The homepage is served at /home
9
- - API is available at / (POST) for shortening URLs
10
- - Short URL access is via /{key} (GET)
11
- - Custom error pages for 404, security warnings, and redirects
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
- - url: https://url.tsou.me
18
+ - url: /
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.
15
25
 
16
26
  paths:
17
- /shorten:
27
+ /:
18
28
  post:
29
+ tags: [Short links]
30
+ operationId: shortenUrl
19
31
  summary: Shorten a URL
20
- description: Create a new shortened URL, optionally with a custom slug.
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
- type: object
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
- description: URL successfully shortened
44
- content:
45
- application/json:
46
- schema:
47
- type: object
48
- properties:
49
- short_url:
50
- type: string
51
- format: uri
52
- example: https://url.tsou.me/abc123
44
+ $ref: "#/components/responses/ShortenCreated"
53
45
  "400":
54
- description: Bad request
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"
47
+ "403":
48
+ $ref: "#/components/responses/AccessDenied"
49
+ "404":
50
+ $ref: "#/components/responses/NotFound"
66
51
  "500":
67
- description: Server error
52
+ $ref: "#/components/responses/ServerError"
53
+ get:
54
+ tags: [Short links]
55
+ operationId: getHomepage
56
+ summary: Get the Shorten URL application
57
+ responses:
58
+ "200":
59
+ description: Worker-hosted application HTML.
68
60
  content:
69
- application/json:
61
+ text/html:
70
62
  schema:
71
- type: object
72
- properties:
73
- status:
74
- type: integer
75
- example: 500
76
- message:
77
- type: string
78
- example: Server error
63
+ type: string
64
+ options:
65
+ tags: [Short links]
66
+ operationId: preflightHomepage
67
+ summary: Handle CORS preflight
68
+ responses:
69
+ "204":
70
+ description: Preflight response
79
71
 
80
72
  /{key}:
81
73
  get:
82
- summary: Access shortened URL
83
- description: Redirect to the original URL
74
+ tags: [Short links]
75
+ operationId: followShortUrl
76
+ summary: Follow a shortened URL
84
77
  parameters:
85
78
  - in: path
86
79
  name: key
87
80
  required: true
81
+ description: Short URL key.
88
82
  schema:
89
83
  type: string
90
- description: The short URL key
91
84
  example: abc123
92
85
  responses:
93
86
  "302":
94
- description: Redirect to original URL
87
+ description: Redirect to the stored destination.
95
88
  headers:
96
89
  Location:
90
+ description: Destination URL.
97
91
  schema:
98
92
  type: string
99
93
  format: uri
100
94
  "404":
101
- description: Short URL not found
95
+ description: Short URL was not found.
102
96
  content:
103
97
  text/html:
104
98
  schema:
105
99
  type: string
106
100
 
107
- /home:
101
+ /settings/api/csrf:
108
102
  get:
109
- summary: Get homepage
110
- description: Serves the URL shortener frontend
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: []
111
113
  responses:
112
114
  "200":
113
- description: Homepage HTML
115
+ description: CSRF token created.
114
116
  content:
115
- text/html:
117
+ application/json:
116
118
  schema:
117
- type: string
118
- example: URL shortener frontend with form interface
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"
119
262
 
120
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
121
283
  schemas:
122
284
  ShortenRequest:
123
285
  type: object
124
- required:
125
- - url
286
+ required: [url]
126
287
  properties:
127
288
  url:
128
289
  type: string
129
290
  format: uri
130
- description: The URL to shorten
291
+ description: Destination URL to shorten.
292
+ example: https://www.example.com/very/long/url/path
131
293
  custom_slug:
132
294
  type: string
133
- pattern: '^[a-zA-Z0-9\-_]+$'
295
+ pattern: "^[A-Za-z0-9_-]{1,50}$"
134
296
  maxLength: 50
135
- description: Optional custom slug
136
-
297
+ description: Optional custom slug; omitted or blank values use a generated key.
298
+ example: spring-launch
137
299
  ShortenResponse:
138
300
  type: object
301
+ required: [short_url]
139
302
  properties:
140
303
  short_url:
141
304
  type: string
142
305
  format: uri
143
-
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"
144
456
  ErrorResponse:
145
457
  type: object
458
+ required: [status, message]
146
459
  properties:
147
460
  status:
148
461
  type: integer
149
462
  message:
150
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"
@@ -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.
package/docs/index.html CHANGED
@@ -529,7 +529,7 @@
529
529
  }
530
530
 
531
531
  // Make API call to your Cloudflare Worker
532
- const response = await fetch(window.location.origin + "/shorten", {
532
+ const response = await fetch(window.location.origin + "/", {
533
533
  method: "POST",
534
534
  headers: {
535
535
  "Content-Type": "application/json",
@@ -660,7 +660,7 @@
660
660
  }
661
661
 
662
662
  // Make API call to shorten URL
663
- const response = await fetch(window.location.origin + "/shorten", {
663
+ const response = await fetch(window.location.origin + "/", {
664
664
  method: "POST",
665
665
  headers: {
666
666
  "Content-Type": "application/json",