Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions compute-js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,7 @@ async function handleRequest(event) {
}
// Other .well-known files (apple-app-site-association, assetlinks.json)
console.log('Handling subdomain .well-known file:', url.pathname);
const wkResponse = await publisherServer.serveRequest(request);
// Guard: if publisher returns text/html, it's the SPA fallback, not the real file
if (wkResponse != null && wkResponse.status === 200 && !wkResponse.headers.get('Content-Type')?.includes('text/html')) {
const headers = new Headers(wkResponse.headers);
const contentType = url.pathname.endsWith('.json') || url.pathname.endsWith('/apple-app-site-association')
? 'application/json'
: headers.get('Content-Type') || 'application/octet-stream';
headers.set('Content-Type', contentType);
headers.set('Cache-Control', 'public, max-age=3600');
return new Response(wkResponse.body, { status: 200, headers });
}
return new Response('Not Found', { status: 404 });
return await serveWellKnownFile(request, url.pathname);
}

// Subdomain profile - serve SPA with injected user data
Expand Down Expand Up @@ -127,24 +116,16 @@ async function handleRequest(event) {
// apple-app-site-association has no file extension, so the static publisher
// cannot detect its content type - we handle it explicitly here.
console.log('Handling .well-known file:', url.pathname);
const wkResponse = await publisherServer.serveRequest(request);
// Guard: if publisher returns text/html, it's the SPA fallback, not the real file
if (wkResponse != null && wkResponse.status === 200 && !wkResponse.headers.get('Content-Type')?.includes('text/html')) {
const wkResponse = await serveWellKnownFile(request, url.pathname);
if (wkResponse.status === 200) {
const headers = new Headers(wkResponse.headers);
// Ensure correct content type for app association files
const contentType = url.pathname.endsWith('.json') || url.pathname.endsWith('/apple-app-site-association')
? 'application/json'
: headers.get('Content-Type') || 'application/octet-stream';
headers.set('Content-Type', contentType);
headers.set('Cache-Control', 'public, max-age=3600');
headers.append('Vary', 'X-Original-Host');
return new Response(wkResponse.body, {
status: 200,
headers,
});
}
// File not found in KV - return 404 instead of SPA fallback
return new Response('Not Found', { status: 404 });
return wkResponse;
}

// 5. Handle dynamic OG meta tags for crawler requests
Expand Down Expand Up @@ -1320,3 +1301,40 @@ function humanizeCategoryName(name) {
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}

/**
* Serve .well-known association files from static publish output.
* Falls back to root filenames when needed to tolerate publisher path quirks.
*/
async function serveWellKnownFile(request, pathname) {
const candidatePaths = [pathname];

if (pathname === '/.well-known/assetlinks.json') {
candidatePaths.push('/assetlinks.json');
} else if (pathname === '/.well-known/apple-app-site-association') {
candidatePaths.push('/apple-app-site-association');
}

for (const candidatePath of candidatePaths) {
const candidateUrl = new URL(request.url);
candidateUrl.pathname = candidatePath;
candidateUrl.search = '';

const candidateRequest = new Request(candidateUrl.toString(), request);
const wkResponse = await publisherServer.serveRequest(candidateRequest);
const contentType = wkResponse?.headers.get('Content-Type') || '';

// Static publish returns HTML when falling back to SPA; ignore those responses.
if (wkResponse != null && wkResponse.status === 200 && !contentType.includes('text/html')) {
const headers = new Headers(wkResponse.headers);
if (pathname.endsWith('.json') || pathname.endsWith('/apple-app-site-association')) {
headers.set('Content-Type', 'application/json');
}
headers.set('Cache-Control', 'public, max-age=3600');
return new Response(wkResponse.body, { status: 200, headers });
}
}

// File not found in KV - return 404 instead of SPA fallback.
return new Response('Not Found', { status: 404 });
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"fastly:local": "npm run build && cd compute-js && npm run dev:publish && npm run dev:start",
"fastly:deploy": "npm run build && cd compute-js && npm run fastly:deploy",
"fastly:publish": "npm run build && cd compute-js && npm run fastly:publish",
"fastly:release": "npm run fastly:deploy && npm run fastly:publish",
"verify:well-known": "node scripts/verify-well-known.mjs",
"precalculate-thumbnails": "tsx scripts/precalculate-hashtag-thumbnails.ts",
"generate-icons": "node scripts/generate-icons.js",
"test:visual": "playwright test",
Expand Down
4 changes: 4 additions & 0 deletions public/.well-known/apple-app-site-association
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"/": "/search/*",
"comment": "Search deep links"
},
{
"/": "/invite/*",
"comment": "Invite redemption deep links"
},
{
"/": "/app/callback",
"comment": "OAuth callback for Divine app"
Expand Down
67 changes: 67 additions & 0 deletions scripts/verify-well-known.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// ABOUTME: Verifies deep-link association files are reachable and valid JSON.
// ABOUTME: Smoke check for .well-known apple-app-site-association and assetlinks.json.

const baseUrl = process.argv[2] || 'https://divine.video';

async function fetchJson(pathname) {
const url = new URL(pathname, baseUrl);
const response = await fetch(url);
const body = await response.text();

if (!response.ok) {
throw new Error(`${url.toString()} returned ${response.status}: ${body.slice(0, 200)}`);
}

const contentType = response.headers.get('content-type') || '';
if (!contentType.toLowerCase().includes('application/json')) {
throw new Error(`${url.toString()} returned unexpected Content-Type: ${contentType}`);
}

let json;
try {
json = JSON.parse(body);
} catch (error) {
throw new Error(`${url.toString()} returned invalid JSON: ${error.message}`);
}

return { url: url.toString(), json };
}

function assertInviteRoute(aasa) {
const components = aasa?.applinks?.details?.[0]?.components;
if (!Array.isArray(components)) {
throw new Error('AASA missing applinks.details[0].components array');
}

const hasInvite = components.some((component) => component?.['/'] === '/invite/*');
if (!hasInvite) {
throw new Error('AASA does not include /invite/* component');
}
}

function assertAssetLinks(assetLinks) {
if (!Array.isArray(assetLinks) || assetLinks.length === 0) {
throw new Error('assetlinks.json must be a non-empty array');
}

const hasOpenvineTarget = assetLinks.some((entry) => entry?.target?.package_name === 'co.openvine.app');
if (!hasOpenvineTarget) {
throw new Error('assetlinks.json does not include co.openvine.app target');
}
}

async function main() {
const aasa = await fetchJson('/.well-known/apple-app-site-association');
assertInviteRoute(aasa.json);

const assetLinks = await fetchJson('/.well-known/assetlinks.json');
assertAssetLinks(assetLinks.json);

console.log(`OK ${aasa.url}`);
console.log(`OK ${assetLinks.url}`);
}

main().catch((error) => {
console.error(error.message);
process.exit(1);
});
Loading