Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/import-msa-adjudication.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
environment: msa-adjudication-production
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Sign in to Azure with OIDC
uses: azure/login@v2
uses: azure/login@v3
with:
client-id: ${{ vars.ADG_AZURE_CLIENT_ID }}
tenant-id: ${{ vars.ADG_AZURE_TENANT_ID }}
Expand Down Expand Up @@ -110,7 +110,7 @@ jobs:
--base main `
--head $branch `
--title "evidence: import anonymized MSA adjudication" `
--body "Imports signed, PII-free submissions from ads.sbay.sa. Human identity remains in private Azure storage. Every artifact still requires repository review before use."
--body "Imports signed, PII-free submissions from adg.sbay.sa. Human identity remains in private Azure storage. Every artifact still requires repository review before use."
"url=$url" | Out-File $env:GITHUB_OUTPUT -Append

- name: Archive accepted queue items
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/msa-adjudication-portal-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
Set-Content security/reports/msa-adjudication-portal.json

- name: Upload security report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: msa-adjudication-portal-security
path: security/reports/msa-adjudication-portal.json
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ This public repository is the official community-facing language repository for:
Arabic teachers and linguists can evaluate the parser without GitHub or
command-line knowledge at:

**https://ads.sbay.sa**
**https://adg.sbay.sa**

The Arabic-first portal explains every criterion, hides parser predictions,
keeps participant identity encrypted outside GitHub, and is designed to import
only signed, pseudonymous linguistic evidence through a review pull request.
Passkey accounts and encrypted draft resumption are live; central submission
remains disabled until the import workflow is merged and revalidated. Its
source and security model are under `tools\msa-adjudication-workbench`.
Passkey accounts, encrypted draft resumption, optional private social
usernames, and central submission are live. Ready-made WhatsApp and X/Twitter
buttons help invite other experts. Every submitted artifact still enters the
repository through an automated validation and review pull request. Its source
and security model are under `tools\msa-adjudication-workbench`.

## Start Here

Expand Down
8 changes: 6 additions & 2 deletions tools/msa-adjudication-workbench/DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ADS deployment

Target: `https://ads.sbay.sa`
Canonical target: `https://adg.sbay.sa`

Legacy target: `https://ads.sbay.sa` (HTTP 308 redirect)

## Azure resources

Expand Down Expand Up @@ -59,7 +61,9 @@ Entra ordinary variables:

The Entra application must include this Web redirect URI:

`https://ads.sbay.sa/signin-microsoft`
`https://adg.sbay.sa/signin-microsoft`

The Turnstile widget hostname allowlist must contain `adg.sbay.sa`.

It requires delegated `User.Read` and the existing application permission
`RoleManagement.Read.Directory` with tenant-admin consent. The administrative
Expand Down
14 changes: 9 additions & 5 deletions tools/msa-adjudication-workbench/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## البيانات التي نجمعها

- الاسم والبريد ورقم الهاتف؛
- الاسم والبريد؛
- أسماء المستخدم الاختيارية في حسابات التواصل، ومنها اسم مستخدم
واتساب بدل رقم الهاتف؛
- سنوات الخبرة والتخصص والجهة الاختيارية؛
- الموافقات وتعهدات الاستقلال والتعمية؛
- القرارات اللغوية المسجلة في العينة.
Expand All @@ -12,7 +14,7 @@
تُشفّر بيانات الهوية أولًا بملف EntityCrypt Matryoshka العشوائي
`AES-256-GCM` ومفتاح محفوظ في Azure Key Vault، ثم تحفظ في حاوية Azure خاصة.
تُحفظ النتيجة اللغوية في حاوية منفصلة بمعرف عشوائي وتوقيع HMAC. لا يصل إلى
GitHub الاسم أو البريد أو الهاتف أو الجهة.
GitHub الاسم أو البريد أو حسابات التواصل أو الجهة.

عند إنشاء حساب الاستكمال، يسجل المتصفح مفتاح مرور عام فقط، وتبقى مادته
الخاصة داخل الجهاز أو مدير مفاتيح المرور. تحفظ قاعدة D1 المفتاح العام والعداد
Expand All @@ -26,9 +28,11 @@ GitHub الاسم أو البريد أو الهاتف أو الجهة.

## الاحتفاظ والوصول

يجب على مشغل المنصة تحديد مدة احتفاظ معلنة قبل دعوة المحكّمين، وقصر الوصول
إلى سجلات الهوية على منسق التقييم المخول. حذف الهوية لا يقتضي حذف النتيجة
المجهّلة إذا تعذر عمليًا ربطها بالشخص.
تُحفظ المسودات غير النشطة مدة 90 يومًا، وتُحفظ بيانات الهوية والتواصل مدة
12 شهرًا من إغلاق جولة التحكيم ثم تحذف، ما لم يطلب صاحبها الحذف قبل ذلك أو
توجد موافقة صريحة على جولة لاحقة. يمكن الاحتفاظ بالنتيجة المجهّلة بوصفها
دليلًا بحثيًا بعد حذف الهوية إذا تعذر عمليًا ربطها بالشخص. يقتصر الوصول إلى
سجلات الهوية على منسق التقييم المخول.

لوحة المتابعة منفصلة على `/admin/`، ولا تفك بيانات التواصل لعرضها إلا بعد
دخول Microsoft Entra والتحقق الخادمي من دور Global Administrator. لا تمنح
Expand Down
8 changes: 7 additions & 1 deletion tools/msa-adjudication-workbench/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Arabic Adjudication Studio (ADS)

`ads.sbay.sa` is the Arabic-first human adjudication portal for ADG-Lang. It is
`adg.sbay.sa` is the Arabic-first human adjudication portal for ADG-Lang. It is
designed for experienced Arabic teachers who should not need GitHub, JSON, or
command-line knowledge.

The previous `ads.sbay.sa` address redirects to the canonical domain so old
invitations remain usable. The public page includes ready-made WhatsApp and
X/Twitter invitation actions.

## User workflow

1. Read the Arabic criteria and parser summary.
Expand All @@ -26,6 +30,8 @@ organization member has authoritative Global Administrator proof.
- Parser predictions are never displayed.
- Packet and submission roots are recomputed in the browser and in .NET.
- Azure stores identity separately from linguistic evidence.
- Optional social usernames, including the WhatsApp username rather than a
phone number, remain encrypted with the private identity record.
- GitHub receives only a pseudonymous, HMAC-signed envelope.
- Cloudflare Turnstile and same-origin checks protect the public endpoint.
- D1 stores opaque account identifiers, Passkey public keys and counters,
Expand Down
4 changes: 2 additions & 2 deletions tools/msa-adjudication-workbench/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tools/msa-adjudication-workbench/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adg-msa-adjudication-workbench",
"version": "14.1.0",
"version": "14.2.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
18 changes: 18 additions & 0 deletions tools/msa-adjudication-workbench/public/admin/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,24 @@
font-size: .83rem;
}

.social-handles {
display: flex;
flex-wrap: wrap;
gap: .25rem .4rem;
margin-top: .4rem;
}

.participant-name .social-handles a,
.participant-name .social-handles span {
display: inline-flex;
padding: .15rem .4rem;
border-radius: .45rem;
color: var(--green-800);
background: var(--green-100);
font-size: .72rem;
text-decoration: none;
}

.participant-name span,
.assignment small,
.muted {
Expand Down
70 changes: 64 additions & 6 deletions tools/msa-adjudication-workbench/public/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ function renderParticipants() {
const haystack = [
participant.fullName,
participant.email,
participant.phone,
participant.affiliation
participant.affiliation,
...Object.values(participant.socialAccounts || {})
].filter(Boolean).join(" ").toLocaleLowerCase("ar");
return matchesStatus && (!query || haystack.includes(query));
});
Expand Down Expand Up @@ -142,16 +142,74 @@ function participantCell(participant) {
const email = document.createElement("a");
email.href = `mailto:${participant.email}`;
email.textContent = participant.email;
const phone = document.createElement("a");
phone.href = `tel:${participant.phone.replace(/[ ()-]/g, "")}`;
phone.textContent = participant.phone;
const affiliation = document.createElement("span");
affiliation.textContent = participant.affiliation || "بلا جهة مسجلة";
wrapper.append(name, email, phone, affiliation);
wrapper.append(name, email, affiliation);
const social = socialAccountsElement(participant.socialAccounts);
if (social) wrapper.append(social);
cell.append(wrapper);
return cell;
}

function socialAccountsElement(accounts = {}) {
const labels = {
whatsapp: "واتساب",
x: "X",
tiktok: "TikTok",
instagram: "Instagram",
threads: "Threads",
telegram: "Telegram",
snapchat: "Snapchat",
facebook: "Facebook",
linkedin: "LinkedIn",
youtube: "YouTube",
bluesky: "Bluesky"
};
const links = {
x: handle => `https://x.com/${encodeURIComponent(handle)}`,
tiktok: handle =>
`https://www.tiktok.com/@${encodeURIComponent(handle)}`,
instagram: handle =>
`https://www.instagram.com/${encodeURIComponent(handle)}`,
threads: handle =>
`https://www.threads.net/@${encodeURIComponent(handle)}`,
telegram: handle => `https://t.me/${encodeURIComponent(handle)}`,
snapchat: handle =>
`https://www.snapchat.com/add/${encodeURIComponent(handle)}`,
facebook: handle =>
`https://www.facebook.com/${encodeURIComponent(handle)}`,
linkedin: handle =>
`https://www.linkedin.com/in/${encodeURIComponent(handle)}`,
youtube: handle =>
`https://www.youtube.com/@${encodeURIComponent(handle)}`,
bluesky: handle =>
`https://bsky.app/profile/${encodeURIComponent(handle)}`
};
const container = document.createElement("div");
container.className = "social-handles";
for (const [key, label] of Object.entries(labels)) {
const handle = accounts[key];
if (!handle) continue;
const element = links[key]
? document.createElement("a")
: document.createElement("span");
if (links[key]) {
element.href = links[key](handle);
element.target = "_blank";
element.rel = "noopener noreferrer";
}
element.textContent = `${label}: @${handle}`;
container.append(element);
}
if (accounts.otherPlatform && accounts.otherUsername) {
const other = document.createElement("span");
other.textContent =
`${accounts.otherPlatform}: @${accounts.otherUsername}`;
container.append(other);
}
return container.childElementCount === 0 ? null : container;
}

function experienceCell(participant) {
const label = specializationLabel(participant.specialization);
return textCell(`${label} · ${participant.experienceYears} سنة`);
Expand Down
Loading