Skip to content

Commit

Permalink
fix: verify functions return map
Browse files Browse the repository at this point in the history
  • Loading branch information
ssssota committed Dec 11, 2024
1 parent 8fd7d75 commit 95339ab
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 81 deletions.
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Check apple-app-site-association file to verify universal links.

## Installation

```sh
npm install -D universal-links-test
```

## Usage

The `verify` function detects provided path lauches the app or not.
Expand All @@ -13,10 +19,29 @@ If you don't use macOS, you can use `universal-links-test/sim` instead that simu
import { verify, type AppleAppSiteAssociation } from "universal-links-test";
// import { verify, type AppleAppSiteAssociation } from "universal-links-test/sim";

const json = { applinks: { /* ... */ } } satisfies AppleAppSiteAssociation;

const result = await verify(json, "/universal-links/path?query#hash");
console.log(result); // 'match' | 'block' | 'unset'
const json = {
applinks: {
details: [
{
appID: "APP.ID1",
components: [{ "/": "/universal-links/path" }]
},
{
appID: "APP.ID2",
components: [{ "/": "/universal-links/path", exclude: true }]
},
{
appID: "APP.ID3",
components: []
}
]
}
} satisfies AppleAppSiteAssociation;

const result: Map<string, 'match' | 'block'> = await verify(json, "/universal-links/path?query#hash");
console.log(result.get("APP.ID1")); // 'match' : Universal links are working
console.log(result.get("APP.ID2")); // 'block' : Universal links are blocked
console.log(result.get("APP.ID3")); // undefined
```

Use `swcutil` command programmatically:
Expand Down
78 changes: 74 additions & 4 deletions demo/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { createVerify } from "universal-links-test/sim";
import { createVerify, validateAASA } from "universal-links-test/sim";
import Editor from "./Editor.svelte";
import { getHash, parseHash } from "./hash";
Expand Down Expand Up @@ -33,6 +33,20 @@
let json = $state(JSON.stringify(initial.json, null, 2));
let paths = $state(initial.paths.map((p) => ({ id: uuid(), value: p })));
let appIds = $derived.by(() => {
try {
const aasa = JSON.parse(json);
if (!validateAASA(aasa)) throw new Error("Invalid AASA");
return (
aasa.applinks?.details?.flatMap((d) => [
...(d.appIDs ?? []),
...(d.appID ? [d.appID] : []),
]) ?? []
);
} catch {
return [];
}
});
let verify = $derived(createVerify(json));
</script>

Expand Down Expand Up @@ -66,10 +80,66 @@
</button>
</div>
</header>
<main class="grid md:grid-cols-2">
<main class="grid md:grid-cols-[1fr_3fr]">
<div class="p-4 size-full bg-slate-100 md:order-last">
<h2 class="font-bold">Test Paths</h2>
<ul class="grid grid-cols-[auto_1fr_auto] gap-1 font-mono">
<table class="w-full font-mono">
<thead>
<tr>
<th></th>
<th>path</th>
{#each appIds as appId (appId)}
<th title={appId}>{appId}</th>
{/each}
</tr>
</thead>
<tbody>
{#each paths as path (path.id)}
<tr>
<td>
<button
type="button"
onclick={() => {
paths = paths.filter((p) => p.id !== path.id);
}}
title="Remove"
>
-
</button>
</td>
<td>
<span
class="grow bg-white focus-within:outline-2 outline-blue-400/50 rounded-sm"
>
<input
bind:value={path.value}
id={path.id}
class="p-1 border-b border-gray-300 size-full focus:outline-none"
type="text"
autocomplete="off"
data-1p-ignore
/>
</span>
</td>
{#await verify(path.value) then result}
{#each appIds as appId (appId)}
{@const res = result.get(appId)}
<td
class={res === "match"
? "text-emerald-700"
: res === "block"
? "text-orange-600"
: "text-gray-500"}
>
{res ?? "unset"}
</td>
{/each}
{/await}
</tr>
{/each}
</tbody>
</table>
<!-- <ul class="grid grid-cols-[auto_1fr_auto] gap-1 font-mono">
{#each paths as path (path.id)}
<li class="contents">
<button
Expand Down Expand Up @@ -120,7 +190,7 @@
+ Add more
</button>
</li>
</ul>
</ul> -->
</div>
<Editor bind:value={json} />
</main>
Expand Down
25 changes: 21 additions & 4 deletions src/sim/verify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { validateAASA } from "../aasa.js";
import type { CreateVerify, Verify } from "../types.js";
import type { CreateVerify, ResultMap, Verify } from "../types.js";
import { resolveJson } from "./json.js";
import { match } from "./match.js";

Expand All @@ -10,7 +10,8 @@ export const createVerify: CreateVerify = (json) => {
const aasa = await aasaPromise;
if (!validateAASA(aasa)) throw new Error("Invalid AASA");
const details = aasa.applinks?.details;
if (!details || details.length === 0) return "unset";
const ret: ResultMap = new Map();
if (!details || details.length === 0) return ret;

for (const detail of details) {
if (!detail.appID && (!detail.appIDs || detail.appIDs.length === 0))
Expand All @@ -29,11 +30,27 @@ export const createVerify: CreateVerify = (json) => {
aasa.applinks?.defaults?.caseSensitive;

if (match(url, { ...component, percentEncoded, caseSensitive })) {
return component.exclude ? "block" : "match";
const res = component.exclude ? "block" : "match";
if (detail.appIDs) {
for (const appID of detail.appIDs) {
if (!ret.has(appID)) ret.set(appID, res);
}
} else if (detail.appID) {
if (!ret.has(detail.appID)) ret.set(detail.appID, res);
}
break;
}
}
}
return "unset";
return ret;
};
};

/**
* Verify the URL with the apple-app-site-association file.
* This function simulates the `swcutil` command behavior.
* @param json apple-app-site-association file content or path
* @param path URL to verify
* @returns Map of appID and match/block
*/
export const verify: Verify = (json, path) => createVerify(json)(path);
7 changes: 4 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export type JsonOrPath = string | Record<string, unknown>;
export type PromiseOr<T> = T | Promise<T>;
export type VerifyResult = "match" | "block" | "unset";
export type ResultMap = Map<string, VerifyResult>;
export type VerifyResult = "match" | "block";

export type CreateVerify = (
json: JsonOrPath,
) => (path: string) => Promise<VerifyResult>;
export type Verify = (json: JsonOrPath, path: string) => Promise<VerifyResult>;
) => (path: string) => Promise<ResultMap>;
export type Verify = (json: JsonOrPath, path: string) => Promise<ResultMap>;
30 changes: 27 additions & 3 deletions src/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,33 @@ export const createVerify: CreateVerify = (json) => async (path) => {
url: path,
});
if (res.status !== 0) throw new Error(res.stderr);
/**
* @example
* ```
* { s = applinks, a = HOGE.com.example.app, d = www.example.com }: Pattern "/" matched.
* { s = applinks, a = FUGA.com.example.app, d = www.example.com }: Pattern "/" matched.
* { s = applinks, a = FOO.com.example.app, d = www.example.com }: Pattern "/" blocked match.
* { s = applinks, a = BAR.com.example.app, d = www.example.com }: Pattern "/" blocked match.
* ```
*/
const out = res.stdout.trimEnd();
if (out.endsWith("matched.")) return "match";
if (out.endsWith("blocked match.")) return "block";
return "unset";
const lines = out.split("\n");
const resultMap = lines
.map((line) => {
const appID = line.match(/a = ([^,]+),/)?.[1];
if (!appID) return undefined;
return [appID, line.endsWith("matched.") ? "match" : "block"] as const;
})
.filter((x) => x !== undefined);
return new Map(resultMap);
};

/**
* Verify the URL with the apple-app-site-association file.
* This function is a wrapper of the `swcutil` command.
* So you need to run on macOS and root permission is required.
* @param json apple-app-site-association file content or path
* @param path URL to verify
* @returns Map of appID and match/block
*/
export const verify: Verify = (json, path) => createVerify(json)(path);
Loading

0 comments on commit 95339ab

Please sign in to comment.