mirror of
https://github.com/JonasunderscoreJones/analytics.jonasjones.dev.git
synced 2025-10-23 01:19:19 +02:00
Compare commits
3 commits
1715184876
...
53e9dd02fc
Author | SHA1 | Date | |
---|---|---|---|
53e9dd02fc | |||
8a52ed518e | |||
62bdced695 |
5 changed files with 635 additions and 1001 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -131,3 +131,4 @@ dist
|
||||||
|
|
||||||
|
|
||||||
.dev.vars
|
.dev.vars
|
||||||
|
/.wrangler
|
||||||
|
|
1161
package-lock.json
generated
1161
package-lock.json
generated
File diff suppressed because it is too large
Load diff
8
schema.sql
Normal file
8
schema.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
DROP TABLE IF EXISTS requests;
|
||||||
|
CREATE TABLE IF NOT EXISTS requests (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME, domain TEXT, method TEXT, path TEXT, country TEXT);
|
||||||
|
INSERT INTO requests (timestamp, domain, method, path, country)
|
||||||
|
VALUES
|
||||||
|
('1734918464464', 'example.com', 'GET', '/home', 'US'),
|
||||||
|
('1734918464464', 'anotherdomain.com', 'POST', '/login', 'DE'),
|
||||||
|
('1734918464464', 'yetanotherdomain.com', 'GET', '/products', 'GB'),
|
||||||
|
('1734918464464', 'coolwebsite.com', 'PUT', '/update', 'FR');
|
461
src/index.js
461
src/index.js
|
@ -1,10 +1,229 @@
|
||||||
addEventListener('fetch', event => {
|
export default {
|
||||||
event.respondWith(handleRequest(event.request))
|
async fetch(request, env, context) {
|
||||||
})
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const AUTH_KEY = AUTH_KEY_SECRET
|
if (url.pathname === '/requests/record' && request.method === 'POST') {
|
||||||
const FILE_KEY = 'analytics/requests.json'
|
return this.handleRecordRequest(request, env);
|
||||||
const ALLOWED_ORIGINS = [
|
} else if (url.pathname === '/requests/record/ipunknown' && request.method === 'POST') {
|
||||||
|
return this.handleRecordIpRequest(request, env);
|
||||||
|
} else if (url.pathname === '/requests/get/count') {
|
||||||
|
return this.handleGetCountRequest(request, env);
|
||||||
|
} else if (url.pathname.startsWith('/requests/get')) {
|
||||||
|
return this.handleGetRequest(url);
|
||||||
|
} else if (request.method === 'OPTIONS') {
|
||||||
|
return this.handleOptions(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// CORS handling for OPTIONS request
|
||||||
|
async handleOptions(request) {
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400', // Cache the preflight response for 1 day
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(null, { headers: headers });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle recording request with the provided data
|
||||||
|
async handleRecordRequest(request, env) {
|
||||||
|
const { headers } = request;
|
||||||
|
const authKey = headers.get('Authorization');
|
||||||
|
|
||||||
|
if (authKey !== env.AUTH_KEY_SECRET) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { timestamp, domain, method, path, country } = await request.json();
|
||||||
|
|
||||||
|
if (!timestamp || !domain || !method || !path || !country) {
|
||||||
|
return new Response('Bad Request: Missing required fields', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = { timestamp, domain, method, path, country };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store the new record in the database
|
||||||
|
await this.storeRecordInDB(record, env);
|
||||||
|
} catch (error) {
|
||||||
|
return new Response('Error storing record in DB', { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Recorded', { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error recording request:', error);
|
||||||
|
return new Response('Bad Request: Invalid JSON', { status: 400 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle recording request with IP country detection
|
||||||
|
async handleRecordIpRequest(request, env) {
|
||||||
|
const { headers } = request;
|
||||||
|
const authKey = headers.get('Authorization');
|
||||||
|
|
||||||
|
if (authKey !== env.AUTH_KEY_SECRET) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { timestamp, domain, method, path } = await request.json();
|
||||||
|
|
||||||
|
if (!timestamp || !domain || !method || !path) {
|
||||||
|
return new Response('Bad Request: Missing required fields', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const country = request.cf.country;
|
||||||
|
|
||||||
|
const record = { timestamp, domain, method, path, country };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store the new record in the database
|
||||||
|
await this.storeRecordInDB(record, env);
|
||||||
|
} catch (error) {
|
||||||
|
return new Response('Error storing record in DB', { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
||||||
|
|
||||||
|
return new Response('Recorded', { status: 200 }, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error recording request:', error);
|
||||||
|
return new Response('Bad Request: Invalid JSON', { status: 400 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle request to get count of records
|
||||||
|
async handleGetCountRequest(request, env) {
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get number of entries in DB
|
||||||
|
//const resp = await env.DB.prepare('SELECT COUNT(*) FROM requests').get();
|
||||||
|
//const count = resp['COUNT(*)'];
|
||||||
|
|
||||||
|
const result = await env.DB.prepare("SELECT COUNT(*) AS count FROM requests").all();
|
||||||
|
console.log(result);
|
||||||
|
const count = result.results[0].count;
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ count }), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching count from DB:', error);
|
||||||
|
return new Response('Error fetching count', { status: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle GET requests with filters (start, end, count, etc.)
|
||||||
|
async handleGetRequest(url) {
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
const start = parseInt(params.get('start')) || 0;
|
||||||
|
const end = parseInt(params.get('end')) || Number.MAX_SAFE_INTEGER;
|
||||||
|
const count = Math.min(parseInt(params.get('count')) || 100, 100);
|
||||||
|
const offset = parseInt(params.get('offset')) || 0;
|
||||||
|
const domain = params.get('domain');
|
||||||
|
const method = params.get('method');
|
||||||
|
const path = params.get('path');
|
||||||
|
const country = params.get('country');
|
||||||
|
|
||||||
|
// Get records from the database with filtering
|
||||||
|
const records = await this.getRecordsFromDB(env, start, end, domain, method, path, country, offset, count);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(records), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Store the new record in the database
|
||||||
|
async storeRecordInDB(record, env) {
|
||||||
|
await env.DB.prepare('INSERT INTO requests (timestamp, domain, method, path, country) VALUES (?, ?, ?, ?, ?)')
|
||||||
|
.bind(record.timestamp, record.domain, record.method, record.path, record.country)
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Retrieve filtered records from the database
|
||||||
|
async getRecordsFromDB(env, start, end, domain, method, path, country, offset, count) {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT * FROM requests
|
||||||
|
WHERE timestamp >= ? AND timestamp <= ?
|
||||||
|
${domain ? 'AND domain = ?' : ''}
|
||||||
|
${method ? 'AND method = ?' : ''}
|
||||||
|
${path ? 'AND path = ?' : ''}
|
||||||
|
${country ? 'AND country = ?' : ''}
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`;
|
||||||
|
const params = [
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
domain,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
country,
|
||||||
|
count,
|
||||||
|
offset,
|
||||||
|
].filter(param => param !== undefined);
|
||||||
|
|
||||||
|
const records = await env.DB.prepare(query).bind(...params).all();
|
||||||
|
|
||||||
|
return records.map(record => ({
|
||||||
|
timestamp: record.timestamp,
|
||||||
|
domain: record.domain,
|
||||||
|
method: record.method,
|
||||||
|
path: record.path,
|
||||||
|
country: record.country,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching records from DB:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Utility functions (e.g., country lookup, hashing, etc.)
|
||||||
|
async getCountryCode(ip) {
|
||||||
|
const response = await fetch(`https://ipinfo.io/${ip}/country?token=${IPINFO_TOKEN}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryCode = await response.text();
|
||||||
|
return countryCode.trim() || "unknown";
|
||||||
|
},
|
||||||
|
|
||||||
|
async hashString(input) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(input);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ALLOWED_ORIGINS = [
|
||||||
'https://jonasjones.dev',
|
'https://jonasjones.dev',
|
||||||
'https://www.jonasjones.dev',
|
'https://www.jonasjones.dev',
|
||||||
'https://blog.jonasjones.dev',
|
'https://blog.jonasjones.dev',
|
||||||
|
@ -14,231 +233,5 @@ const ALLOWED_ORIGINS = [
|
||||||
'https://kcomebacks.jonasjones.dev',
|
'https://kcomebacks.jonasjones.dev',
|
||||||
'https://jonasjonesstudios.com',
|
'https://jonasjonesstudios.com',
|
||||||
'https://lastlovedsyncify.jonasjones.dev',
|
'https://lastlovedsyncify.jonasjones.dev',
|
||||||
'https://syncify.jonasjones.dev'
|
'https://syncify.jonasjones.dev',
|
||||||
]
|
];
|
||||||
|
|
||||||
async function handleRequest(request) {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
|
|
||||||
if (url.pathname === '/requests/record' && request.method === 'POST') {
|
|
||||||
return handleRecordRequest(request)
|
|
||||||
} else if (url.pathname === '/requests/record/ipunknown' && request.method === 'POST') {
|
|
||||||
return handleRecordIpRequest(request)
|
|
||||||
} else if (url.pathname === '/requests/get/count') {
|
|
||||||
return handleGetCountRequest(request)
|
|
||||||
} else if (url.pathname.startsWith('/requests/get')) {
|
|
||||||
return handleGetRequest(url)
|
|
||||||
} else if (request.method === 'OPTIONS') {
|
|
||||||
return handleOptions(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response('Not Found', { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOptions(request) {
|
|
||||||
const origin = request.headers.get('Origin');
|
|
||||||
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
||||||
'Access-Control-Max-Age': '86400', // Cache the preflight response for 1 day
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
headers: headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function handleRecordRequest(request) {
|
|
||||||
const { headers } = request
|
|
||||||
const authKey = headers.get('Authorization')
|
|
||||||
|
|
||||||
if (authKey !== AUTH_KEY) {
|
|
||||||
return new Response('Unauthorized', { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { timestamp, domain, method, path, ipcountry } = await request.json()
|
|
||||||
|
|
||||||
if (!timestamp || !domain || !method || !path || !ipcountry) {
|
|
||||||
return new Response('Bad Request: Missing required fields', { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const record = { timestamp, domain, method, path, ipcountry }
|
|
||||||
|
|
||||||
// Retrieve existing records from R2
|
|
||||||
let records = await getRecordsFromR2()
|
|
||||||
if (!records) {
|
|
||||||
records = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new record to the list
|
|
||||||
records.push(record)
|
|
||||||
|
|
||||||
// Store the updated list back to R2
|
|
||||||
await uploadRecordsToR2(records)
|
|
||||||
|
|
||||||
return new Response('Recorded', { status: 200 })
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error recording request:', error)
|
|
||||||
return new Response('Bad Request: Invalid JSON', { status: 400 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRecordIpRequest(request) {
|
|
||||||
const { headers } = request
|
|
||||||
const authKey = headers.get('Authorization')
|
|
||||||
|
|
||||||
if (authKey !== AUTH_KEY) {
|
|
||||||
return new Response('Unauthorized', { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { timestamp, domain, method, path } = await request.json()
|
|
||||||
|
|
||||||
if (!timestamp || !domain || !method || !path) {
|
|
||||||
return new Response('Bad Request: Missing required fields', { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipcountry = request.cf.country
|
|
||||||
|
|
||||||
const record = { timestamp, domain, method, path, ipcountry }
|
|
||||||
|
|
||||||
// Retrieve existing records from R2
|
|
||||||
let records = await getRecordsFromR2()
|
|
||||||
if (!records) {
|
|
||||||
records = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new record to the list
|
|
||||||
records.push(record)
|
|
||||||
|
|
||||||
// Store the updated list back to R2
|
|
||||||
await uploadRecordsToR2(records)
|
|
||||||
|
|
||||||
const origin = request.headers.get('Origin');
|
|
||||||
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
|
||||||
|
|
||||||
return new Response('Recorded', { status: 200 }, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error recording request:', error)
|
|
||||||
return new Response('Bad Request: Invalid JSON', { status: 400 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleGetCountRequest(request) {
|
|
||||||
const origin = request.headers.get('Origin');
|
|
||||||
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
|
||||||
|
|
||||||
const records = await getRecordsFromR2() || []
|
|
||||||
const count = records.length
|
|
||||||
return new Response(JSON.stringify({ count }), {
|
|
||||||
headers: { 'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleGetRequest(url) {
|
|
||||||
const params = new URLSearchParams(url.search)
|
|
||||||
|
|
||||||
const start = parseInt(params.get('start')) || 0
|
|
||||||
const end = parseInt(params.get('end')) || Number.MAX_SAFE_INTEGER
|
|
||||||
const count = Math.min(parseInt(params.get('count')) || 100, 100)
|
|
||||||
const offset = parseInt(params.get('offset')) || 0
|
|
||||||
const domain = params.get('domain')
|
|
||||||
const method = params.get('method')
|
|
||||||
const path = params.get('path')
|
|
||||||
const ipcountry = params.get('ipcountry')
|
|
||||||
|
|
||||||
let records = await getRecordsFromR2() || []
|
|
||||||
|
|
||||||
let filteredRecords = records
|
|
||||||
.filter(record => {
|
|
||||||
return (
|
|
||||||
record.timestamp >= start &&
|
|
||||||
record.timestamp <= end &&
|
|
||||||
(!domain || record.domain === domain) &&
|
|
||||||
(!method || record.method === method) &&
|
|
||||||
(!path || record.path === path) &&
|
|
||||||
(!ipcountry || record.ipcountry === ipcountry)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.slice(offset, offset + count)
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(filteredRecords), {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRecordsFromR2() {
|
|
||||||
try {
|
|
||||||
const object = await CDN_BUCKET.get(FILE_KEY)
|
|
||||||
if (object === null) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const text = await object.text()
|
|
||||||
return JSON.parse(text)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching records:', error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadRecordsToR2(records) {
|
|
||||||
try {
|
|
||||||
const json = JSON.stringify(records)
|
|
||||||
await CDN_BUCKET.put(FILE_KEY, json)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading records:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCountryCode(ip) {
|
|
||||||
const response = await fetch(`https://ipinfo.io/${ip}/country?token=${IPINFO_TOKEN}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since the response is plain text, use response.text()
|
|
||||||
const countryCode = await response.text();
|
|
||||||
|
|
||||||
// Trim any whitespace (like newline characters) from the response
|
|
||||||
const trimmedCountryCode = countryCode.trim();
|
|
||||||
|
|
||||||
// Check if the response is a valid country code
|
|
||||||
if (!trimmedCountryCode) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmedCountryCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hashString(input) {
|
|
||||||
// Encode the input string as a Uint8Array (UTF-8)
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const data = encoder.encode(input);
|
|
||||||
|
|
||||||
// Hash the data using SHA-256
|
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
||||||
|
|
||||||
// Convert the hash to a hexadecimal string
|
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
|
|
||||||
return hashHex;
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,11 @@ workers_dev = true
|
||||||
binding = "CDN_BUCKET"
|
binding = "CDN_BUCKET"
|
||||||
bucket_name = "cdn"
|
bucket_name = "cdn"
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "REQUESTS"
|
||||||
|
database_id = "665c17d4-250a-4a6d-8317-9b4522c7842f"
|
||||||
|
|
||||||
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
|
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
|
||||||
# Note: Use secrets to store sensitive data.
|
# Note: Use secrets to store sensitive data.
|
||||||
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables
|
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue