@dytsou/shorten-url 2.4.4 → 2.4.6

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,102 +3,67 @@ 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 /
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
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 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://your-worker-domain.example/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"
66
47
  "403":
67
- description: Cloudflare Access headers are missing
68
- content:
69
- application/json:
70
- schema:
71
- $ref: "#/components/schemas/ErrorResponse"
48
+ $ref: "#/components/responses/AccessDenied"
72
49
  "404":
73
- description: Invalid API path
74
- content:
75
- application/json:
76
- schema:
77
- $ref: "#/components/schemas/ErrorResponse"
50
+ $ref: "#/components/responses/NotFound"
78
51
  "500":
79
- description: Server error
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
- summary: Get homepage
93
- description: Fetches the configured URL shortener frontend.
54
+ tags: [Short links]
55
+ operationId: getHomepage
56
+ summary: Get the Shorten URL application
94
57
  responses:
95
58
  "200":
96
- description: Homepage HTML
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
- summary: Access shortened URL
110
- description: Redirect to the original URL
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 original URL
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: The URL to shorten
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: '^[a-zA-Z0-9\-_]+$'
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"
@@ -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.