Level 2 · Lesson 8 · optional
Cut a delivery door
Level 2 · Lesson 8Give the app a web trigger, a URL other systems can send menus to, guarded by a door code kept in an encrypted variable.
Behind, or starting here? Download the code as it should be before this lesson.
This lesson is optional. Some menus come from other systems, such as a planning tool, not from a person at the desk. A web trigger gives the app a URL they can call. Anyone who has the URL can call it, so the door only opens for requests that carry a secret code.
Another system calls the web trigger URL with the shared secret.
In the kitchen: Someone knocks at the delivery door with the code.
Runs in the Atlassian cloud · Back of house
Web trigger The delivery door
A webtrigger module gives your app a URL that outside systems can call over HTTPS.
A door at the back where other systems can drop off a menu without coming through the front.
Select any box in the diagram to see what it is.
Step 1: Make a door code and lock it in the safe
Locked until the step before it is done.
Runs in the Atlassian cloud · Back of house
Shared secret · the door code
A shared secret is a value only the app and the caller know. The caller sends it in a header; the app compares it with an encrypted variable before it does anything else. Compare in constant time, so how long the check takes reveals nothing, and never write the secret to the log.
In the kitchen The delivery door only opens for someone who knows the code, and the code lives in the safe.
Make a long random code on your laptop, then put it in the safe. The CLI asks for the value and hides it:
openssl rand -hex 32
forge variables set --encrypt DOOR_CODEKeep the code where the calling system can use it, never in its source code.
Stuck? I set or changed a variable
Next step: next
Step 2: Only open for the door code
Locked until the step before it is done.
Runs in the Atlassian cloud · Back of house
Web trigger · the delivery door
A webtrigger module gives the app a URL on Atlassian’s domain that outside systems can call over HTTPS. The function gets the request’s method, headers and body, and returns a status code, headers and a body. The URL works without any Atlassian login, which is why the function must check who is calling.
In the kitchen A door at the back where other systems can drop off a menu without coming through the front.
A new file checks the code first, then the menu, then accepts it exactly as the desk does: a jar, a slip on the rail, and a receipt.
src/door.jsA new file: check the code, check the menu, then accept it like the desk does.
Added: import { timingSafeEqual } from 'node:crypto'; |
Added: import { acceptMenu } from './menus'; |
Added: import { validateMenu } from './lib/validate'; |
Added: |
Added: const reply = (statusCode, body) => ({ |
Added: statusCode, |
Added: headers: { 'Content-Type': ['application/json'] }, |
Added: body: JSON.stringify(body), |
Added: }); |
Added: |
Added: function header(request, name) { |
Added: const key = Object.keys(request.headers ?? {}).find((k) => k.toLowerCase() === name); |
Added: const value = key ? request.headers[key] : undefined; |
Added: return Array.isArray(value) ? value[0] : value; |
Added: } |
Added: |
Added: // Compare in constant time, so how long the answer takes gives nothing away. |
Added: function sameSecret(given, expected) { |
Added: const a = Buffer.from(String(given ?? '')); |
Added: const b = Buffer.from(String(expected ?? '')); |
Added: return a.length > 0 && a.length === b.length && timingSafeEqual(a, b); |
Added: } |
Added: |
Added: /** |
Added: * The delivery door: other systems can drop off a menu if they know the door code. |
Added: * The URL is public, so check the code before anything else, and never log it. |
Added: */ |
Added: export async function run(request) { |
Added: if (!sameSecret(header(request, 'x-door-code'), process.env.DOOR_CODE)) { |
Added: console.warn('Delivery door: refused a request without the right code.'); |
Added: return reply(401, { error: 'Unauthorized' }); |
Added: } |
Added: let payload; |
Added: try { |
Added: payload = JSON.parse(request.body ?? ''); |
Added: } catch { |
Added: return reply(400, { error: 'Send the menu as JSON.' }); |
Added: } |
Added: const check = validateMenu(payload); |
Added: if (!check.ok) return reply(400, { error: check.error }); |
Added: const menuId = await acceptMenu({ ...check.menu, requestedBy: 'delivery-door' }); |
Added: return reply(202, { menuId }); |
Added: } |
Why: Web trigger · Shared secret · Environment variable
Stuck? I changed code in src/
Next step: next
Step 3: Cut the door into the licence
Locked until the step before it is done.
manifest.ymlChange 1 of 2A web trigger: a URL other systems can call.
type: string |
required: true |
description: The research to publish, as plain text. |
Added: webtrigger: |
Added: - key: delivery-door |
Added: function: door |
Added: response: |
Added: type: dynamic |
function: |
- key: resolver |
handler: index.handler |
manifest.ymlChange 2 of 2The function behind it.
handler: rovo.researchDish |
- key: publish-notes-fn |
handler: rovo.publishNotes |
Added: - key: door |
Added: handler: door.run |
resources: |
- key: main |
path: src/frontend/index.jsx |
Why: Web trigger
type: dynamic lets the function choose its own status code, such as 401 for a wrong code or 202 for an accepted menu.
Stuck? I changed manifest.yml
Next step: next
Step 4: Deploy and get the door’s address
Locked until the step before it is done.
npm run build:ui
forge deploy
forge webtrigger create --functionKey delivery-doorChoose the Jira installation on your developer site. The CLI prints the URL. Treat it as private too: it is half of what a caller needs.
Stuck? Deploy stops after a permission change
Next step: next
Step 5: Knock, with and without the code
Locked until the step before it is done.
Send a menu with the code. Replace the placeholders with your URL and code:
curl -i -X POST '<your-web-trigger-url>' \
-H 'Content-Type: application/json' \
-H 'X-Door-Code: <your-door-code>' \
-d '{"title": "Delivery menu", "notes": "Ana: add a green papaya salad. Ken: research a fish sauce supplier first.", "attendees": "Ana, Ken"}'The answer is 202 with a menuId, and a few seconds later the dish tickets appear on the board, written by the prep cook as before. Send the same request without the X-Door-Code line: the answer is 401, and nothing is saved.
Stuck? My menu stays “waiting” or ends “failed”
Next step: next
Compare with yours: download the code after this lesson.