Add admin Google Play Closed Testing access via Cloud Identity Groups.
Admins can enable or revoke store access per viewer; membership syncs to a configurable Google Group without changing app auth. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
04d6a38234
commit
0eb0c5d367
18 changed files with 1599 additions and 329 deletions
187
GOOGLE_PLAY_ACCESS_SETUP.md
Normal file
187
GOOGLE_PLAY_ACCESS_SETUP.md
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
|
||||||
|
# Google Play Closed Testing — server-side setup
|
||||||
|
|
||||||
|
Deze functionaliteit beheert **distributietoegang** tot de Android-app via Google Play Closed Testing.
|
||||||
|
Het vervangt **niet** de bestaande app-login of API-autorisatie.
|
||||||
|
|
||||||
|
## Architectuur
|
||||||
|
|
||||||
|
```
|
||||||
|
Adminpaneel → backend → Google Cloud Identity Groups API → Google Group
|
||||||
|
↓
|
||||||
|
Google Play Closed Testing track
|
||||||
|
```
|
||||||
|
|
||||||
|
- Gebruikers worden **niet** individueel via de Play Developer API beheerd.
|
||||||
|
- De Closed Testing-track wordt **eenmalig** gekoppeld aan `PLAY_ACCESS_GROUP_EMAIL`.
|
||||||
|
- Het adminpaneel beheert alleen lidmaatschap van die groep.
|
||||||
|
|
||||||
|
## Benodigde environment variables
|
||||||
|
|
||||||
|
Stel deze in op de **master-api** server (niet in de admin UI, niet in Git):
|
||||||
|
|
||||||
|
| Variabele | Verplicht | Beschrijving |
|
||||||
|
|-----------|-----------|--------------|
|
||||||
|
| `GOOGLE_CLOUD_PROJECT_ID` | Ja | Google Cloud project ID |
|
||||||
|
| `GOOGLE_SERVICE_ACCOUNT_EMAIL` | Ja | Service account e-mail |
|
||||||
|
| `GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY` | Ja | PEM private key ( `\n` als `\n` in env) |
|
||||||
|
| `PLAY_ACCESS_GROUP_ID` | Ja | Group resource ID (zonder `groups/` prefix mag ook) |
|
||||||
|
| `PLAY_ACCESS_GROUP_EMAIL` | Ja | E-mailadres van de Google Group |
|
||||||
|
| `GOOGLE_WORKSPACE_ADMIN_EMAIL` | Meestal ja | Admin voor domain-wide delegation |
|
||||||
|
| `GOOGLE_WORKSPACE_CUSTOMER_ID` | Optioneel | Workspace customer ID |
|
||||||
|
| `GOOGLE_PLAY_OPT_IN_URL` | Aanbevolen | Publieke Closed Testing opt-in URL |
|
||||||
|
| `GOOGLE_PLAY_SYNC_INTERVAL_MS` | Optioneel | Reconciliatie-interval (default: 86400000 = 24u) |
|
||||||
|
|
||||||
|
### Waar instellen
|
||||||
|
|
||||||
|
- **Lokaal:** `apps/master-api/.env`
|
||||||
|
- **Productie/Dokploy:** omgeving van de master-api container (`deploy/master/.env` template)
|
||||||
|
|
||||||
|
## Google Cloud — handmatige stappen
|
||||||
|
|
||||||
|
### 1. APIs inschakelen
|
||||||
|
|
||||||
|
In Google Cloud Console → APIs & Services:
|
||||||
|
|
||||||
|
- **Cloud Identity Groups API** (`cloudidentity.googleapis.com`)
|
||||||
|
|
||||||
|
### 2. Service account aanmaken
|
||||||
|
|
||||||
|
1. IAM & Admin → Service Accounts → Create
|
||||||
|
2. Download JSON key **of** kopieer private key naar `GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY`
|
||||||
|
3. Noteer `client_email` → `GOOGLE_SERVICE_ACCOUNT_EMAIL`
|
||||||
|
|
||||||
|
### 3. Domain-wide delegation (Workspace)
|
||||||
|
|
||||||
|
Als de group in Google Workspace staat:
|
||||||
|
|
||||||
|
1. Service account → Enable Domain-Wide Delegation
|
||||||
|
2. Google Admin Console → Security → API Controls → Domain-wide delegation
|
||||||
|
3. Voeg scope toe: `https://www.googleapis.com/auth/cloud-identity.groups`
|
||||||
|
4. Stel `GOOGLE_WORKSPACE_ADMIN_EMAIL` in op een super admin
|
||||||
|
|
||||||
|
### 4. Service account rechten op de group
|
||||||
|
|
||||||
|
De service account (of geïmpersonateerde admin) moet leden kunnen beheren:
|
||||||
|
|
||||||
|
- Google Group → **Group member manager** of **Owner**
|
||||||
|
- Of via Cloud Identity met de juiste IAM-rollen
|
||||||
|
|
||||||
|
### 5. Google Group aanmaken
|
||||||
|
|
||||||
|
Maak een **technische ACL-groep** (geen hardcoded naam in code):
|
||||||
|
|
||||||
|
- Niet publiek vindbaar
|
||||||
|
- Self-join uit
|
||||||
|
- Alleen beheerders beheren leden
|
||||||
|
- Ledenlijst niet zichtbaar voor gewone members
|
||||||
|
- Posten/conversations uit indien mogelijk
|
||||||
|
- Externe accounts alleen toestaan als nodig (bijv. `@gmail.com` testers)
|
||||||
|
|
||||||
|
Noteer:
|
||||||
|
|
||||||
|
- Group e-mail → `PLAY_ACCESS_GROUP_EMAIL`
|
||||||
|
- Group ID → `PLAY_ACCESS_GROUP_ID`
|
||||||
|
|
||||||
|
#### Group ID vinden
|
||||||
|
|
||||||
|
Via [Cloud Identity Groups API lookup](https://cloud.google.com/identity/docs/reference/rest/v1/groups/lookup):
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://cloudidentity.googleapis.com/v1/groups:lookup?groupKey.id=GROUP_EMAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
Het veld `name` is bijv. `groups/03abc123` → `PLAY_ACCESS_GROUP_ID=03abc123`
|
||||||
|
|
||||||
|
### 6. Google Play Console — Closed Testing koppelen
|
||||||
|
|
||||||
|
1. Play Console → app → Testing → Closed testing
|
||||||
|
2. Maak of open een track
|
||||||
|
3. Testers → **Google Groups**
|
||||||
|
4. Voeg `PLAY_ACCESS_GROUP_EMAIL` toe
|
||||||
|
5. Kopieer de **opt-in URL** → `GOOGLE_PLAY_OPT_IN_URL`
|
||||||
|
|
||||||
|
## Gebruik in adminpaneel
|
||||||
|
|
||||||
|
1. Ga naar **Kijkers**
|
||||||
|
2. Per kijker: sectie **Google Play access**
|
||||||
|
- **Google Play email** — het Google-account voor Play (fallback: account-e-mail)
|
||||||
|
- **Store access** — ON/OFF
|
||||||
|
- **Status** — sync-status (ACTIVE, ERROR, …)
|
||||||
|
- **Retry synchronization** — bij ERROR
|
||||||
|
3. Pagina-acties:
|
||||||
|
- **Sync all** — alle kijkers synchroniseren
|
||||||
|
- **Reconcile** — lokale status vergelijken met Google Group
|
||||||
|
|
||||||
|
## Testgebruiker toevoegen
|
||||||
|
|
||||||
|
1. Maak kijker aan (of bestaande)
|
||||||
|
2. Vul Google Play e-mail in (bijv. `user@gmail.com`)
|
||||||
|
3. Zet **Store access** aan
|
||||||
|
4. Status wordt **ACTIVE** na succesvolle sync
|
||||||
|
5. Gebruiker opent `GOOGLE_PLAY_OPT_IN_URL` terwijl ingelogd met hetzelfde Google-account
|
||||||
|
6. Installeert app via Play Store
|
||||||
|
|
||||||
|
## Toegang intrekken
|
||||||
|
|
||||||
|
1. Zet **Store access** uit in adminpaneel
|
||||||
|
2. Backend verwijdert lidmaatschap uit de group
|
||||||
|
3. Nieuwe installs via Play zijn niet meer mogelijk
|
||||||
|
4. **Let op:** reeds geïnstalleerde apps blijven op het toestel — app-login/API blijft leidend voor contenttoegang
|
||||||
|
|
||||||
|
## Sync & reconciliatie
|
||||||
|
|
||||||
|
| Lokaal | Google Group | Actie |
|
||||||
|
|--------|--------------|-------|
|
||||||
|
| ON | member | OK |
|
||||||
|
| ON | geen member | toevoegen |
|
||||||
|
| OFF | member | verwijderen |
|
||||||
|
| OFF | geen member | OK |
|
||||||
|
|
||||||
|
- **Sync all:** past gewenste status toe voor alle kijkers
|
||||||
|
- **Reconcile:** vergelijkt en herstelt afwijkingen
|
||||||
|
- **Scheduled:** elke `GOOGLE_PLAY_SYNC_INTERVAL_MS` (default 24u)
|
||||||
|
|
||||||
|
## Foutafhandeling
|
||||||
|
|
||||||
|
Bij Google API-fouten:
|
||||||
|
|
||||||
|
- Gewenste instelling blijft bewaard
|
||||||
|
- Status → **ERROR**
|
||||||
|
- Fout staat in admin UI + audit log
|
||||||
|
- Gebruik **Retry synchronization**
|
||||||
|
|
||||||
|
Audit log tabel: `google_play_access_audit_logs` (geen tokens/keys gelogd).
|
||||||
|
|
||||||
|
## API endpoints (admin only)
|
||||||
|
|
||||||
|
| Method | Route |
|
||||||
|
|--------|-------|
|
||||||
|
| GET | `/api/v1/admin/google-play-access/config` |
|
||||||
|
| PATCH | `/api/v1/admin/viewers/:id/google-play-access` |
|
||||||
|
| POST | `/api/v1/admin/viewers/:id/google-play-access/retry` |
|
||||||
|
| POST | `/api/v1/admin/google-play-access/sync-all` |
|
||||||
|
| POST | `/api/v1/admin/google-play-access/reconcile` |
|
||||||
|
|
||||||
|
Body PATCH:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"email": "user@gmail.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wat jij nog handmatig moet invullen
|
||||||
|
|
||||||
|
Stop hier — vul **geen** credentials in deze repo:
|
||||||
|
|
||||||
|
1. `GOOGLE_CLOUD_PROJECT_ID`
|
||||||
|
2. `GOOGLE_SERVICE_ACCOUNT_EMAIL`
|
||||||
|
3. `GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY`
|
||||||
|
4. `GOOGLE_WORKSPACE_ADMIN_EMAIL` (indien Workspace)
|
||||||
|
5. `PLAY_ACCESS_GROUP_ID`
|
||||||
|
6. `PLAY_ACCESS_GROUP_EMAIL`
|
||||||
|
7. `GOOGLE_PLAY_OPT_IN_URL`
|
||||||
|
|
||||||
|
Daarna master-api herstarten en in adminpaneel testen met één tester-account.
|
||||||
|
|
@ -11,36 +11,85 @@ type Device = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GooglePlaySyncStatus =
|
||||||
|
| "NOT_ENABLED"
|
||||||
|
| "PENDING_ADD"
|
||||||
|
| "ACTIVE"
|
||||||
|
| "PENDING_REMOVE"
|
||||||
|
| "REMOVED"
|
||||||
|
| "ERROR";
|
||||||
|
|
||||||
type Viewer = {
|
type Viewer = {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
appAccess: boolean;
|
appAccess: boolean;
|
||||||
|
googlePlayEmail: string | null;
|
||||||
|
googlePlayAccess: boolean;
|
||||||
|
googlePlayMembershipId: string | null;
|
||||||
|
googlePlaySyncStatus: GooglePlaySyncStatus;
|
||||||
|
googlePlayLastSyncedAt: string | null;
|
||||||
|
googlePlaySyncError: string | null;
|
||||||
|
googlePlayResolvedEmail: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
deviceCount: number;
|
deviceCount: number;
|
||||||
devices: Device[];
|
devices: Device[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PlayConfig = {
|
||||||
|
configured: boolean;
|
||||||
|
groupEmail: string | null;
|
||||||
|
groupId: string | null;
|
||||||
|
optInUrl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SYNC_LABELS: Record<GooglePlaySyncStatus, string> = {
|
||||||
|
NOT_ENABLED: "Niet ingeschakeld",
|
||||||
|
PENDING_ADD: "Bezig met toevoegen…",
|
||||||
|
ACTIVE: "Actief",
|
||||||
|
PENDING_REMOVE: "Bezig met verwijderen…",
|
||||||
|
REMOVED: "Verwijderd",
|
||||||
|
ERROR: "Fout",
|
||||||
|
};
|
||||||
|
|
||||||
export default function ViewersPage() {
|
export default function ViewersPage() {
|
||||||
useAuth();
|
useAuth();
|
||||||
const [viewers, setViewers] = useState<Viewer[]>([]);
|
const [viewers, setViewers] = useState<Viewer[]>([]);
|
||||||
|
const [playConfig, setPlayConfig] = useState<PlayConfig | null>(null);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [msg, setMsg] = useState<string | null>(null);
|
const [msg, setMsg] = useState<string | null>(null);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
const [playEmails, setPlayEmails] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => setViewers(d.viewers ?? []))
|
.then((d) => {
|
||||||
|
const list: Viewer[] = d.viewers ?? [];
|
||||||
|
setViewers(list);
|
||||||
|
setPlayEmails((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
for (const v of list) {
|
||||||
|
if (next[v.id] === undefined) {
|
||||||
|
next[v.id] = v.googlePlayEmail ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
})
|
||||||
.catch(() => setErr("Kon kijkers niet laden"));
|
.catch(() => setErr("Kon kijkers niet laden"));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
|
fetch("/api/v1/admin/google-play-access/config", { credentials: "include" })
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => setPlayConfig(d.config ?? null))
|
||||||
|
.catch(() => undefined);
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
async function createViewer(e: FormEvent) {
|
async function createViewer(e: FormEvent) {
|
||||||
|
|
@ -80,6 +129,64 @@ export default function ViewersPage() {
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setPlayAccess(v: Viewer, enabled: boolean) {
|
||||||
|
setMsg(null);
|
||||||
|
setErr(null);
|
||||||
|
const res = await fetch(`/api/v1/admin/viewers/${v.id}/google-play-access`, {
|
||||||
|
method: "PATCH",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled,
|
||||||
|
email: playEmails[v.id]?.trim() || null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr(data.error?.message ?? "Google Play-toegang bijwerken mislukt");
|
||||||
|
} else {
|
||||||
|
setMsg(data.message ?? (enabled ? "Google Play access enabled." : "Google Play access removed."));
|
||||||
|
if (data.syncError) setErr(data.syncError);
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryPlaySync(v: Viewer) {
|
||||||
|
setMsg(null);
|
||||||
|
setErr(null);
|
||||||
|
const res = await fetch(`/api/v1/admin/viewers/${v.id}/google-play-access/retry`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr(data.error?.message ?? "Synchronisatie mislukt");
|
||||||
|
} else {
|
||||||
|
setMsg(data.message ?? "Synchronization completed.");
|
||||||
|
if (data.syncError) setErr(data.syncError);
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkAction(path: "sync-all" | "reconcile") {
|
||||||
|
setMsg(null);
|
||||||
|
setErr(null);
|
||||||
|
const res = await fetch(`/api/v1/admin/google-play-access/${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr(data.error?.message ?? "Actie mislukt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const s = data.summary;
|
||||||
|
setMsg(
|
||||||
|
`Scanned: ${s.scanned} · Correct: ${s.correct} · Added: ${s.added} · Removed: ${s.removed} · Errors: ${s.errors}`
|
||||||
|
);
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
async function revokeDevice(deviceId: string) {
|
async function revokeDevice(deviceId: string) {
|
||||||
if (!confirm("Apparaat intrekken?")) return;
|
if (!confirm("Apparaat intrekken?")) return;
|
||||||
await fetch(`/api/v1/admin/viewers/devices/${deviceId}/revoke`, {
|
await fetch(`/api/v1/admin/viewers/devices/${deviceId}/revoke`, {
|
||||||
|
|
@ -104,6 +211,27 @@ export default function ViewersPage() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{playConfig && (
|
||||||
|
<section className="card-block play-banner">
|
||||||
|
<h2>Google Play access</h2>
|
||||||
|
<p className="muted">
|
||||||
|
{playConfig.configured
|
||||||
|
? `Groep: ${playConfig.groupEmail ?? playConfig.groupId}`
|
||||||
|
: "Google Play-toegang is nog niet geconfigureerd op de server (zie GOOGLE_PLAY_ACCESS_SETUP.md)."}
|
||||||
|
</p>
|
||||||
|
{playConfig.configured && (
|
||||||
|
<div className="viewer-actions">
|
||||||
|
<button type="button" onClick={() => bulkAction("sync-all")}>
|
||||||
|
Sync all
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => bulkAction("reconcile")}>
|
||||||
|
Reconcile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="card-block">
|
<section className="card-block">
|
||||||
<h2>Nieuwe kijker</h2>
|
<h2>Nieuwe kijker</h2>
|
||||||
<form className="form-grid" onSubmit={createViewer}>
|
<form className="form-grid" onSubmit={createViewer}>
|
||||||
|
|
@ -148,7 +276,7 @@ export default function ViewersPage() {
|
||||||
<article key={v.id} className="viewer-row">
|
<article key={v.id} className="viewer-row">
|
||||||
<div>
|
<div>
|
||||||
<strong>{v.name || v.email}</strong>
|
<strong>{v.name || v.email}</strong>
|
||||||
<div className="muted">{v.email}</div>
|
<div className="muted">Account email: {v.email}</div>
|
||||||
<div className="muted">
|
<div className="muted">
|
||||||
{v.enabled ? "Actief" : "Uit"} · app {v.appAccess ? "aan" : "uit"} ·{" "}
|
{v.enabled ? "Actief" : "Uit"} · app {v.appAccess ? "aan" : "uit"} ·{" "}
|
||||||
{v.deviceCount} apparaat(en)
|
{v.deviceCount} apparaat(en)
|
||||||
|
|
@ -162,6 +290,74 @@ export default function ViewersPage() {
|
||||||
{v.appAccess ? "App uit" : "App aan"}
|
{v.appAccess ? "App uit" : "App aan"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="play-section">
|
||||||
|
<h3>Google Play access</h3>
|
||||||
|
<label className="play-field">
|
||||||
|
Google Play email
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder={v.email}
|
||||||
|
value={playEmails[v.id] ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPlayEmails((prev) => ({ ...prev, [v.id]: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="muted">
|
||||||
|
Fallback: {v.googlePlayResolvedEmail || v.email}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="play-meta">
|
||||||
|
<span>
|
||||||
|
Store access:{" "}
|
||||||
|
<strong>{v.googlePlayAccess ? "ON" : "OFF"}</strong>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Status:{" "}
|
||||||
|
<strong className={v.googlePlaySyncStatus === "ERROR" ? "status-err" : ""}>
|
||||||
|
{SYNC_LABELS[v.googlePlaySyncStatus] ?? v.googlePlaySyncStatus}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
{v.googlePlayLastSyncedAt && (
|
||||||
|
<span className="muted">
|
||||||
|
Last synchronized:{" "}
|
||||||
|
{new Date(v.googlePlayLastSyncedAt).toLocaleString("nl-NL")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{v.googlePlaySyncError && (
|
||||||
|
<span className="form-error">{v.googlePlaySyncError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="viewer-actions">
|
||||||
|
{!v.googlePlayAccess ? (
|
||||||
|
<button type="button" onClick={() => setPlayAccess(v, true)}>
|
||||||
|
Store access aan
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={() => setPlayAccess(v, false)}>
|
||||||
|
Store access uit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{v.googlePlaySyncStatus === "ERROR" && (
|
||||||
|
<button type="button" onClick={() => retryPlaySync(v)}>
|
||||||
|
Retry synchronization
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{v.googlePlayAccess &&
|
||||||
|
v.googlePlaySyncStatus === "ACTIVE" &&
|
||||||
|
playConfig?.optInUrl && (
|
||||||
|
<a
|
||||||
|
href={playConfig.optInUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn-link"
|
||||||
|
>
|
||||||
|
Open Google Play
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{v.devices.length > 0 && (
|
{v.devices.length > 0 && (
|
||||||
<ul className="device-list">
|
<ul className="device-list">
|
||||||
{v.devices.map((d) => (
|
{v.devices.map((d) => (
|
||||||
|
|
@ -198,6 +394,41 @@ export default function ViewersPage() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.play-section {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.play-section h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.play-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
.play-field input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.play-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.play-banner .viewer-actions {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.status-err {
|
||||||
|
color: #ff8b8b;
|
||||||
|
}
|
||||||
|
.btn-link {
|
||||||
|
color: #8ec5ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.device-list {
|
.device-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,13 @@ PLAYBACK_ABSOLUTE_LIFETIME_SECONDS=43200
|
||||||
NODE_HEARTBEAT_INTERVAL_SECONDS=30
|
NODE_HEARTBEAT_INTERVAL_SECONDS=30
|
||||||
NODE_OFFLINE_THRESHOLD_SECONDS=90
|
NODE_OFFLINE_THRESHOLD_SECONDS=90
|
||||||
HIDE_UNAVAILABLE_TITLES=true
|
HIDE_UNAVAILABLE_TITLES=true
|
||||||
|
|
||||||
|
# Google Play Closed Testing (optioneel — zie GOOGLE_PLAY_ACCESS_SETUP.md)
|
||||||
|
# GOOGLE_CLOUD_PROJECT_ID=
|
||||||
|
# GOOGLE_SERVICE_ACCOUNT_EMAIL=
|
||||||
|
# GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=
|
||||||
|
# GOOGLE_WORKSPACE_ADMIN_EMAIL=
|
||||||
|
# PLAY_ACCESS_GROUP_ID=
|
||||||
|
# PLAY_ACCESS_GROUP_EMAIL=
|
||||||
|
# GOOGLE_PLAY_OPT_IN_URL=
|
||||||
|
# GOOGLE_PLAY_SYNC_INTERVAL_MS=86400000
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"test": "tsx src/metadata/filename-parser.test.ts",
|
"test": "tsx src/metadata/filename-parser.test.ts && tsx src/google-play/service.test.ts",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "prisma migrate deploy",
|
"db:migrate": "prisma migrate deploy",
|
||||||
"db:migrate:dev": "prisma migrate dev",
|
"db:migrate:dev": "prisma migrate dev",
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"@prisma/client": "^6.3.1",
|
"@prisma/client": "^6.3.1",
|
||||||
"argon2": "^0.41.1",
|
"argon2": "^0.41.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"fastify": "^5.2.1",
|
"google-auth-library": "^9.15.1",
|
||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.3.1",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- Google Play Closed Testing group membership tracking on viewer users.
|
||||||
|
|
||||||
|
CREATE TYPE "GooglePlaySyncStatus" AS ENUM (
|
||||||
|
'NOT_ENABLED',
|
||||||
|
'PENDING_ADD',
|
||||||
|
'ACTIVE',
|
||||||
|
'PENDING_REMOVE',
|
||||||
|
'REMOVED',
|
||||||
|
'ERROR'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "viewer_users"
|
||||||
|
ADD COLUMN "google_play_email" TEXT,
|
||||||
|
ADD COLUMN "google_play_access" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "google_play_membership_id" TEXT,
|
||||||
|
ADD COLUMN "google_play_sync_status" "GooglePlaySyncStatus" NOT NULL DEFAULT 'NOT_ENABLED',
|
||||||
|
ADD COLUMN "google_play_last_synced_at" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "google_play_sync_error" TEXT;
|
||||||
|
|
||||||
|
CREATE TABLE "google_play_access_audit_logs" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"admin_user_id" TEXT,
|
||||||
|
"admin_email" TEXT,
|
||||||
|
"viewer_user_id" TEXT NOT NULL,
|
||||||
|
"target_email" TEXT NOT NULL,
|
||||||
|
"action" TEXT NOT NULL,
|
||||||
|
"result" TEXT NOT NULL,
|
||||||
|
"membership_id" TEXT,
|
||||||
|
"error_message" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "google_play_access_audit_logs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "google_play_access_audit_logs_viewer_user_id_created_at_idx"
|
||||||
|
ON "google_play_access_audit_logs"("viewer_user_id", "created_at");
|
||||||
|
|
||||||
|
ALTER TABLE "google_play_access_audit_logs"
|
||||||
|
ADD CONSTRAINT "google_play_access_audit_logs_viewer_user_id_fkey"
|
||||||
|
FOREIGN KEY ("viewer_user_id") REFERENCES "viewer_users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -43,6 +43,15 @@ enum DownloadJobStatus {
|
||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum GooglePlaySyncStatus {
|
||||||
|
NOT_ENABLED
|
||||||
|
PENDING_ADD
|
||||||
|
ACTIVE
|
||||||
|
PENDING_REMOVE
|
||||||
|
REMOVED
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
|
|
@ -65,6 +74,14 @@ model ViewerUser {
|
||||||
name String?
|
name String?
|
||||||
enabled Boolean @default(true)
|
enabled Boolean @default(true)
|
||||||
appAccess Boolean @default(true) @map("app_access")
|
appAccess Boolean @default(true) @map("app_access")
|
||||||
|
/** Google-account voor Play Closed Testing; fallback = email */
|
||||||
|
googlePlayEmail String? @map("google_play_email")
|
||||||
|
/** Gewenste Play-distributietoegang (sync met Google Group) */
|
||||||
|
googlePlayAccess Boolean @default(false) @map("google_play_access")
|
||||||
|
googlePlayMembershipId String? @map("google_play_membership_id")
|
||||||
|
googlePlaySyncStatus GooglePlaySyncStatus @default(NOT_ENABLED) @map("google_play_sync_status")
|
||||||
|
googlePlayLastSyncedAt DateTime? @map("google_play_last_synced_at")
|
||||||
|
googlePlaySyncError String? @map("google_play_sync_error") @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
|
@ -72,6 +89,7 @@ model ViewerUser {
|
||||||
progress WatchProgress[]
|
progress WatchProgress[]
|
||||||
shelfPrefs ViewerShelfPreference[]
|
shelfPrefs ViewerShelfPreference[]
|
||||||
linkCodes DeviceLinkCode[]
|
linkCodes DeviceLinkCode[]
|
||||||
|
googlePlayAuditLogs GooglePlayAccessAuditLog[]
|
||||||
|
|
||||||
@@map("viewer_users")
|
@@map("viewer_users")
|
||||||
}
|
}
|
||||||
|
|
@ -568,3 +586,22 @@ model LibrarySubtitle {
|
||||||
@@index([imdbId])
|
@@index([imdbId])
|
||||||
@@map("library_subtitles")
|
@@map("library_subtitles")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Audit trail voor Google Play group-lidmaatschap (geen secrets). */
|
||||||
|
model GooglePlayAccessAuditLog {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
adminUserId String? @map("admin_user_id")
|
||||||
|
adminEmail String? @map("admin_email")
|
||||||
|
viewerUserId String @map("viewer_user_id")
|
||||||
|
targetEmail String @map("target_email")
|
||||||
|
action String
|
||||||
|
result String
|
||||||
|
membershipId String? @map("membership_id")
|
||||||
|
errorMessage String? @map("error_message") @db.Text
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
viewer ViewerUser @relation(fields: [viewerUserId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([viewerUserId, createdAt])
|
||||||
|
@@map("google_play_access_audit_logs")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import { registerInstallRoutes } from "./install/routes";
|
||||||
import { registerDownloadRoutes } from "./downloads/routes";
|
import { registerDownloadRoutes } from "./downloads/routes";
|
||||||
import { registerSettingsRoutes } from "./settings/routes";
|
import { registerSettingsRoutes } from "./settings/routes";
|
||||||
import { registerViewerRoutes } from "./viewer/routes";
|
import { registerViewerRoutes } from "./viewer/routes";
|
||||||
|
import { registerGooglePlayRoutes } from "./google-play/routes";
|
||||||
|
import { startGooglePlayReconciliationPoller } from "./google-play/service";
|
||||||
import { nodeConnectionManager } from "./websocket/manager";
|
import { nodeConnectionManager } from "./websocket/manager";
|
||||||
import { MetadataService } from "./metadata/service";
|
import { MetadataService } from "./metadata/service";
|
||||||
import { toErrorResponse } from "./security/errors";
|
import { toErrorResponse } from "./security/errors";
|
||||||
|
|
@ -77,6 +79,8 @@ async function main() {
|
||||||
registerSettingsRoutes(app, config);
|
registerSettingsRoutes(app, config);
|
||||||
const downloads = registerDownloadRoutes(app, config);
|
const downloads = registerDownloadRoutes(app, config);
|
||||||
registerViewerRoutes(app, config, downloads);
|
registerViewerRoutes(app, config, downloads);
|
||||||
|
const googlePlay = registerGooglePlayRoutes(app, config);
|
||||||
|
startGooglePlayReconciliationPoller(googlePlay, config.GOOGLE_PLAY_SYNC_INTERVAL_MS);
|
||||||
|
|
||||||
app.get("/api/v1/node/connect", { websocket: true }, (socket) => {
|
app.get("/api/v1/node/connect", { websocket: true }, (socket) => {
|
||||||
void nodeConnectionManager.handleConnection(socket);
|
void nodeConnectionManager.handleConnection(socket);
|
||||||
|
|
@ -381,6 +385,46 @@ async function main() {
|
||||||
CONSTRAINT "viewer_shelf_preferences_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "viewer_shelf_preferences_pkey" PRIMARY KEY ("id")
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "GooglePlaySyncStatus" AS ENUM (
|
||||||
|
'NOT_ENABLED', 'PENDING_ADD', 'ACTIVE', 'PENDING_REMOVE', 'REMOVED', 'ERROR'
|
||||||
|
);
|
||||||
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
|
`);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "viewer_users" ADD COLUMN IF NOT EXISTS "google_play_email" TEXT`
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "viewer_users" ADD COLUMN IF NOT EXISTS "google_play_access" BOOLEAN NOT NULL DEFAULT false`
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "viewer_users" ADD COLUMN IF NOT EXISTS "google_play_membership_id" TEXT`
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "viewer_users" ADD COLUMN IF NOT EXISTS "google_play_sync_status" "GooglePlaySyncStatus" NOT NULL DEFAULT 'NOT_ENABLED'`
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "viewer_users" ADD COLUMN IF NOT EXISTS "google_play_last_synced_at" TIMESTAMP(3)`
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`ALTER TABLE "viewer_users" ADD COLUMN IF NOT EXISTS "google_play_sync_error" TEXT`
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "google_play_access_audit_logs" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"admin_user_id" TEXT,
|
||||||
|
"admin_email" TEXT,
|
||||||
|
"viewer_user_id" TEXT NOT NULL,
|
||||||
|
"target_email" TEXT NOT NULL,
|
||||||
|
"action" TEXT NOT NULL,
|
||||||
|
"result" TEXT NOT NULL,
|
||||||
|
"membership_id" TEXT,
|
||||||
|
"error_message" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "google_play_access_audit_logs_pkey" PRIMARY KEY ("id")
|
||||||
|
)
|
||||||
|
`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Could not ensure viewer client tables:", err);
|
console.warn("Could not ensure viewer client tables:", err);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,18 @@ const configSchema = z.object({
|
||||||
NODE_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().default(30),
|
NODE_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().default(30),
|
||||||
NODE_OFFLINE_THRESHOLD_SECONDS: z.coerce.number().default(90),
|
NODE_OFFLINE_THRESHOLD_SECONDS: z.coerce.number().default(90),
|
||||||
HIDE_UNAVAILABLE_TITLES: z.coerce.boolean().default(true),
|
HIDE_UNAVAILABLE_TITLES: z.coerce.boolean().default(true),
|
||||||
|
/** Google Cloud Identity Groups — Closed Testing access control */
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID: z.string().optional(),
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_EMAIL: z.string().email().optional(),
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: z.string().optional(),
|
||||||
|
GOOGLE_WORKSPACE_CUSTOMER_ID: z.string().optional(),
|
||||||
|
GOOGLE_WORKSPACE_ADMIN_EMAIL: z.string().email().optional(),
|
||||||
|
PLAY_ACCESS_GROUP_ID: z.string().optional(),
|
||||||
|
PLAY_ACCESS_GROUP_EMAIL: z.string().email().optional(),
|
||||||
|
/** Public Closed Testing opt-in URL (shown in admin UI) */
|
||||||
|
GOOGLE_PLAY_OPT_IN_URL: z.string().url().optional(),
|
||||||
|
/** Periodic reconciliation interval (ms); 0 = disabled. Default: 24h */
|
||||||
|
GOOGLE_PLAY_SYNC_INTERVAL_MS: z.coerce.number().default(86_400_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Config = z.infer<typeof configSchema>;
|
export type Config = z.infer<typeof configSchema>;
|
||||||
|
|
|
||||||
28
apps/master-api/src/google-play/audit.ts
Normal file
28
apps/master-api/src/google-play/audit.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { prisma } from "../database/client";
|
||||||
|
|
||||||
|
export type AuditAction = "ADD" | "REMOVE" | "SYNC" | "RECONCILE" | "RETRY";
|
||||||
|
export type AuditResult = "SUCCESS" | "ERROR" | "SKIPPED";
|
||||||
|
|
||||||
|
export async function logGooglePlayAudit(input: {
|
||||||
|
adminUserId?: string | null;
|
||||||
|
adminEmail?: string | null;
|
||||||
|
viewerUserId: string;
|
||||||
|
targetEmail: string;
|
||||||
|
action: AuditAction;
|
||||||
|
result: AuditResult;
|
||||||
|
membershipId?: string | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
}) {
|
||||||
|
await prisma.googlePlayAccessAuditLog.create({
|
||||||
|
data: {
|
||||||
|
adminUserId: input.adminUserId ?? null,
|
||||||
|
adminEmail: input.adminEmail ?? null,
|
||||||
|
viewerUserId: input.viewerUserId,
|
||||||
|
targetEmail: input.targetEmail,
|
||||||
|
action: input.action,
|
||||||
|
result: input.result,
|
||||||
|
membershipId: input.membershipId ?? null,
|
||||||
|
errorMessage: input.errorMessage ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
148
apps/master-api/src/google-play/client.ts
Normal file
148
apps/master-api/src/google-play/client.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { JWT } from "google-auth-library";
|
||||||
|
import type { Config } from "../config";
|
||||||
|
|
||||||
|
const IDENTITY_BASE = "https://cloudidentity.googleapis.com/v1";
|
||||||
|
const SCOPE = "https://www.googleapis.com/auth/cloud-identity.groups";
|
||||||
|
const REQUEST_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
export type GooglePlayRuntimeConfig = {
|
||||||
|
projectId: string;
|
||||||
|
serviceAccountEmail: string;
|
||||||
|
privateKey: string;
|
||||||
|
workspaceAdminEmail?: string;
|
||||||
|
groupId: string;
|
||||||
|
groupEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getGooglePlayRuntimeConfig(config: Config): GooglePlayRuntimeConfig | null {
|
||||||
|
const {
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID,
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_EMAIL,
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY,
|
||||||
|
GOOGLE_WORKSPACE_ADMIN_EMAIL,
|
||||||
|
PLAY_ACCESS_GROUP_ID,
|
||||||
|
PLAY_ACCESS_GROUP_EMAIL,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!GOOGLE_CLOUD_PROJECT_ID ||
|
||||||
|
!GOOGLE_SERVICE_ACCOUNT_EMAIL ||
|
||||||
|
!GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY ||
|
||||||
|
!PLAY_ACCESS_GROUP_ID ||
|
||||||
|
!PLAY_ACCESS_GROUP_EMAIL
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId: GOOGLE_CLOUD_PROJECT_ID,
|
||||||
|
serviceAccountEmail: GOOGLE_SERVICE_ACCOUNT_EMAIL,
|
||||||
|
privateKey: GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||||
|
workspaceAdminEmail: GOOGLE_WORKSPACE_ADMIN_EMAIL,
|
||||||
|
groupId: PLAY_ACCESS_GROUP_ID.replace(/^groups\//, ""),
|
||||||
|
groupEmail: PLAY_ACCESS_GROUP_EMAIL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GoogleIdentityGroupsClient {
|
||||||
|
private jwt: JWT | null = null;
|
||||||
|
|
||||||
|
constructor(private readonly cfg: GooglePlayRuntimeConfig) {}
|
||||||
|
|
||||||
|
private auth(): JWT {
|
||||||
|
if (!this.jwt) {
|
||||||
|
this.jwt = new JWT({
|
||||||
|
email: this.cfg.serviceAccountEmail,
|
||||||
|
key: this.cfg.privateKey,
|
||||||
|
scopes: [SCOPE],
|
||||||
|
subject: this.cfg.workspaceAdminEmail || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async accessToken(): Promise<string> {
|
||||||
|
const tokenResponse = await this.auth().getAccessToken();
|
||||||
|
const token =
|
||||||
|
typeof tokenResponse === "string" ? tokenResponse : tokenResponse?.token ?? null;
|
||||||
|
if (!token) throw new Error("Kon geen Google access token verkrijgen");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(
|
||||||
|
path: string,
|
||||||
|
init: RequestInit = {}
|
||||||
|
): Promise<{ ok: boolean; status: number; data: T | null; raw: string }> {
|
||||||
|
const token = await this.accessToken();
|
||||||
|
const res = await fetch(`${IDENTITY_BASE}/${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(init.headers || {}),
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||||
|
});
|
||||||
|
const raw = await res.text();
|
||||||
|
let data: T | null = null;
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw) as T;
|
||||||
|
} catch {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: res.ok, status: res.status, data, raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
async lookupMembership(email: string): Promise<{ name: string } | null> {
|
||||||
|
const q = new URLSearchParams({ "memberKey.id": email });
|
||||||
|
const { ok, status, data } = await this.request<{ name?: string }>(
|
||||||
|
`groups/${encodeURIComponent(this.cfg.groupId)}/memberships:lookup?${q}`
|
||||||
|
);
|
||||||
|
if (status === 404) return null;
|
||||||
|
if (!ok || !data?.name) {
|
||||||
|
throw new Error(`Membership lookup mislukt (${status})`);
|
||||||
|
}
|
||||||
|
return { name: data.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMembership(email: string): Promise<{ name: string }> {
|
||||||
|
const existing = await this.lookupMembership(email);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const { ok, status, data, raw } = await this.request<{ name?: string }>(
|
||||||
|
`groups/${encodeURIComponent(this.cfg.groupId)}/memberships`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
preferredMemberKey: { id: email },
|
||||||
|
roles: [{ name: "MEMBER" }],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 409) {
|
||||||
|
const again = await this.lookupMembership(email);
|
||||||
|
if (again) return again;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok || !data?.name) {
|
||||||
|
throw new Error(`Membership toevoegen mislukt (${status}): ${raw.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
return { name: data.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMembership(membershipName: string): Promise<void> {
|
||||||
|
const name = membershipName.replace(/^\//, "");
|
||||||
|
const { ok, status, raw } = await this.request<unknown>(name, { method: "DELETE" });
|
||||||
|
if (status === 404 || ok) return;
|
||||||
|
throw new Error(`Membership verwijderen mislukt (${status}): ${raw.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMembershipByEmail(email: string): Promise<void> {
|
||||||
|
const found = await this.lookupMembership(email);
|
||||||
|
if (!found) return;
|
||||||
|
await this.deleteMembership(found.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
apps/master-api/src/google-play/email.ts
Normal file
23
apps/master-api/src/google-play/email.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
|
/** Trim + lowercase for internal comparison and Google API calls. */
|
||||||
|
export function normalizeEmail(raw: string): string {
|
||||||
|
return raw.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidEmail(raw: string): boolean {
|
||||||
|
const n = normalizeEmail(raw);
|
||||||
|
return n.length > 0 && EMAIL_RE.test(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Play-account voor group membership.
|
||||||
|
* Fallback: primair account-e-mailadres van de kijker.
|
||||||
|
*/
|
||||||
|
export function resolveGooglePlayEmail(
|
||||||
|
viewer: { email: string; googlePlayEmail?: string | null }
|
||||||
|
): string {
|
||||||
|
const explicit = viewer.googlePlayEmail?.trim();
|
||||||
|
if (explicit) return normalizeEmail(explicit);
|
||||||
|
return normalizeEmail(viewer.email);
|
||||||
|
}
|
||||||
84
apps/master-api/src/google-play/routes.ts
Normal file
84
apps/master-api/src/google-play/routes.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import type { Config } from "../config";
|
||||||
|
import { requireAdmin, type AuthenticatedRequest } from "../auth/routes";
|
||||||
|
import { AppError } from "../security/errors";
|
||||||
|
import { GooglePlayAccessService } from "./service";
|
||||||
|
|
||||||
|
function adminFromRequest(request: import("fastify").FastifyRequest) {
|
||||||
|
const user = (request as AuthenticatedRequest).user;
|
||||||
|
return { adminUserId: user.id, adminEmail: user.email };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerGooglePlayRoutes(app: FastifyInstance, config: Config) {
|
||||||
|
const playAccess = new GooglePlayAccessService(config);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
"/api/v1/admin/google-play-access/config",
|
||||||
|
{ preHandler: requireAdmin },
|
||||||
|
async () => ({ config: playAccess.getPublicConfig() })
|
||||||
|
);
|
||||||
|
|
||||||
|
app.patch(
|
||||||
|
"/api/v1/admin/viewers/:id/google-play-access",
|
||||||
|
{ preHandler: requireAdmin },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const body = request.body as { enabled?: boolean; email?: string | null };
|
||||||
|
if (typeof body.enabled !== "boolean") {
|
||||||
|
throw new AppError("INVALID_REQUEST", "enabled (boolean) is verplicht", 400);
|
||||||
|
}
|
||||||
|
const result = await playAccess.setAccess(
|
||||||
|
id,
|
||||||
|
{ enabled: body.enabled, email: body.email },
|
||||||
|
adminFromRequest(request)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
viewer: result.viewer,
|
||||||
|
message: body.enabled
|
||||||
|
? result.syncError
|
||||||
|
? "Google Play access could not be synchronized. The local setting has been saved and synchronization can be retried."
|
||||||
|
: "Google Play access enabled."
|
||||||
|
: result.syncError
|
||||||
|
? "Google Play access could not be synchronized. The local setting has been saved and synchronization can be retried."
|
||||||
|
: "Google Play access removed.",
|
||||||
|
syncError: result.syncError ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/api/v1/admin/viewers/:id/google-play-access/retry",
|
||||||
|
{ preHandler: requireAdmin },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const result = await playAccess.retrySync(id, adminFromRequest(request));
|
||||||
|
return {
|
||||||
|
viewer: result.viewer,
|
||||||
|
message: result.syncError
|
||||||
|
? "Google Play access could not be synchronized. The local setting has been saved and synchronization can be retried."
|
||||||
|
: "Synchronization completed.",
|
||||||
|
syncError: result.syncError ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/api/v1/admin/google-play-access/sync-all",
|
||||||
|
{ preHandler: requireAdmin },
|
||||||
|
async (request) => {
|
||||||
|
const summary = await playAccess.syncAllUsers(adminFromRequest(request));
|
||||||
|
return { summary };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/api/v1/admin/google-play-access/reconcile",
|
||||||
|
{ preHandler: requireAdmin },
|
||||||
|
async (request) => {
|
||||||
|
const summary = await playAccess.reconcileAll(adminFromRequest(request));
|
||||||
|
return { summary };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return playAccess;
|
||||||
|
}
|
||||||
108
apps/master-api/src/google-play/service.test.ts
Normal file
108
apps/master-api/src/google-play/service.test.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { getGooglePlayRuntimeConfig } from "./client";
|
||||||
|
import { isValidEmail, normalizeEmail, resolveGooglePlayEmail } from "./email";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string): asserts condition {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mock Google Groups client for unit tests (no network). */
|
||||||
|
export class MockGroupsClient {
|
||||||
|
members = new Map<string, string>();
|
||||||
|
|
||||||
|
async lookupMembership(email: string) {
|
||||||
|
const name = this.members.get(normalizeEmail(email));
|
||||||
|
return name ? { name } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMembership(email: string) {
|
||||||
|
const key = normalizeEmail(email);
|
||||||
|
if (this.members.has(key)) return { name: this.members.get(key)! };
|
||||||
|
const name = `groups/test/memberships/${key}`;
|
||||||
|
this.members.set(key, name);
|
||||||
|
return { name };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMembership(membershipName: string) {
|
||||||
|
for (const [email, name] of this.members.entries()) {
|
||||||
|
if (name === membershipName) this.members.delete(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMembershipByEmail(email: string) {
|
||||||
|
this.members.delete(normalizeEmail(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEmailUtils() {
|
||||||
|
assert(normalizeEmail(" User@Gmail.COM ") === "user@gmail.com", "normalize");
|
||||||
|
assert(isValidEmail("a@b.co"), "valid email");
|
||||||
|
assert(!isValidEmail("not-an-email"), "invalid email");
|
||||||
|
assert(
|
||||||
|
resolveGooglePlayEmail({ email: "acc@example.com", googlePlayEmail: null }) === "acc@example.com",
|
||||||
|
"fallback email"
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
resolveGooglePlayEmail({ email: "acc@example.com", googlePlayEmail: "play@gmail.com" }) ===
|
||||||
|
"play@gmail.com",
|
||||||
|
"explicit play email"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRuntimeConfig() {
|
||||||
|
const cfg = getGooglePlayRuntimeConfig({
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID: "proj",
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_EMAIL: "sa@test.iam.gserviceaccount.com",
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: "-----BEGIN PRIVATE KEY-----\\nabc\\n-----END PRIVATE KEY-----",
|
||||||
|
PLAY_ACCESS_GROUP_ID: "groups/abc123",
|
||||||
|
PLAY_ACCESS_GROUP_EMAIL: "group@example.com",
|
||||||
|
} as never);
|
||||||
|
assert(cfg !== null, "config present");
|
||||||
|
assert(cfg!.groupId === "abc123", "strip groups/ prefix");
|
||||||
|
assert(cfg!.privateKey.includes("\n"), "unescape private key");
|
||||||
|
|
||||||
|
const missing = getGooglePlayRuntimeConfig({} as never);
|
||||||
|
assert(missing === null, "missing config returns null");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testMockClientIdempotency() {
|
||||||
|
const mock = new MockGroupsClient();
|
||||||
|
const first = await mock.createMembership("user@gmail.com");
|
||||||
|
const second = await mock.createMembership("user@gmail.com");
|
||||||
|
assert(first.name === second.name, "duplicate add idempotent");
|
||||||
|
assert(mock.members.size === 1, "single member");
|
||||||
|
|
||||||
|
await mock.deleteMembershipByEmail("user@gmail.com");
|
||||||
|
assert(mock.members.size === 0, "removed");
|
||||||
|
await mock.deleteMembershipByEmail("user@gmail.com");
|
||||||
|
assert(mock.members.size === 0, "remove missing is ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCredentialsNeverInPublicConfig() {
|
||||||
|
const { GooglePlayAccessService } = require("./service") as typeof import("./service");
|
||||||
|
const svc = new GooglePlayAccessService({
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID: "proj",
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_EMAIL: "sa@test.iam.gserviceaccount.com",
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: "secret-key-material",
|
||||||
|
PLAY_ACCESS_GROUP_ID: "abc",
|
||||||
|
PLAY_ACCESS_GROUP_EMAIL: "g@example.com",
|
||||||
|
GOOGLE_PLAY_OPT_IN_URL: "https://play.google.com/apps/testing/com.example",
|
||||||
|
} as never);
|
||||||
|
const pub = svc.getPublicConfig();
|
||||||
|
const json = JSON.stringify(pub);
|
||||||
|
assert(!json.includes("secret-key-material"), "private key not exposed");
|
||||||
|
assert(!json.includes("PRIVATE KEY"), "no key in public config");
|
||||||
|
assert(pub.optInUrl?.startsWith("https://"), "opt-in url public");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
testEmailUtils();
|
||||||
|
testRuntimeConfig();
|
||||||
|
await testMockClientIdempotency();
|
||||||
|
testCredentialsNeverInPublicConfig();
|
||||||
|
console.log("google-play/service.test.ts: all tests passed");
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
482
apps/master-api/src/google-play/service.ts
Normal file
482
apps/master-api/src/google-play/service.ts
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
import type { GooglePlaySyncStatus, ViewerUser } from "@prisma/client";
|
||||||
|
import { prisma } from "../database/client";
|
||||||
|
import { AppError } from "../security/errors";
|
||||||
|
import type { Config } from "../config";
|
||||||
|
import { logGooglePlayAudit } from "./audit";
|
||||||
|
import {
|
||||||
|
GoogleIdentityGroupsClient,
|
||||||
|
getGooglePlayRuntimeConfig,
|
||||||
|
} from "./client";
|
||||||
|
import { isValidEmail, normalizeEmail, resolveGooglePlayEmail } from "./email";
|
||||||
|
|
||||||
|
const VIEWER_SELECT = {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
enabled: true,
|
||||||
|
appAccess: true,
|
||||||
|
googlePlayEmail: true,
|
||||||
|
googlePlayAccess: true,
|
||||||
|
googlePlayMembershipId: true,
|
||||||
|
googlePlaySyncStatus: true,
|
||||||
|
googlePlayLastSyncedAt: true,
|
||||||
|
googlePlaySyncError: true,
|
||||||
|
createdAt: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ViewerGooglePlayFields = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
appAccess: boolean;
|
||||||
|
googlePlayEmail: string | null;
|
||||||
|
googlePlayAccess: boolean;
|
||||||
|
googlePlayMembershipId: string | null;
|
||||||
|
googlePlaySyncStatus: GooglePlaySyncStatus;
|
||||||
|
googlePlayLastSyncedAt: Date | null;
|
||||||
|
googlePlaySyncError: string | null;
|
||||||
|
googlePlayResolvedEmail: string;
|
||||||
|
createdAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SyncResult = {
|
||||||
|
viewerId: string;
|
||||||
|
email: string;
|
||||||
|
action: "added" | "removed" | "ok" | "error";
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BulkSyncSummary = {
|
||||||
|
scanned: number;
|
||||||
|
correct: number;
|
||||||
|
added: number;
|
||||||
|
removed: number;
|
||||||
|
errors: number;
|
||||||
|
results: SyncResult[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminContext = {
|
||||||
|
adminUserId?: string;
|
||||||
|
adminEmail?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GooglePlayAccessService {
|
||||||
|
private client: GoogleIdentityGroupsClient | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: Config,
|
||||||
|
/** Test hook: override Google API client factory */
|
||||||
|
private readonly clientOverride?: GoogleIdentityGroupsClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
isConfigured(): boolean {
|
||||||
|
return getGooglePlayRuntimeConfig(this.config) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublicConfig() {
|
||||||
|
const cfg = getGooglePlayRuntimeConfig(this.config);
|
||||||
|
return {
|
||||||
|
configured: cfg !== null,
|
||||||
|
groupEmail: cfg?.groupEmail ?? null,
|
||||||
|
groupId: cfg?.groupId ?? null,
|
||||||
|
optInUrl: this.config.GOOGLE_PLAY_OPT_IN_URL ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupsClient(): GoogleIdentityGroupsClient {
|
||||||
|
if (this.clientOverride) return this.clientOverride;
|
||||||
|
if (this.client) return this.client;
|
||||||
|
const cfg = getGooglePlayRuntimeConfig(this.config);
|
||||||
|
if (!cfg) {
|
||||||
|
throw new AppError(
|
||||||
|
"GOOGLE_PLAY_NOT_CONFIGURED",
|
||||||
|
"Google Play-toegang is niet geconfigureerd op de server",
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.client = new GoogleIdentityGroupsClient(cfg);
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
toPublicFields(viewer: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
appAccess: boolean;
|
||||||
|
googlePlayEmail: string | null;
|
||||||
|
googlePlayAccess: boolean;
|
||||||
|
googlePlayMembershipId: string | null;
|
||||||
|
googlePlaySyncStatus: GooglePlaySyncStatus;
|
||||||
|
googlePlayLastSyncedAt: Date | null;
|
||||||
|
googlePlaySyncError: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
}): ViewerGooglePlayFields {
|
||||||
|
return {
|
||||||
|
...viewer,
|
||||||
|
googlePlayResolvedEmail: resolveGooglePlayEmail(viewer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getViewerOrThrow(id: string) {
|
||||||
|
const viewer = await prisma.viewerUser.findUnique({ where: { id }, select: VIEWER_SELECT });
|
||||||
|
if (!viewer) throw new AppError("NOT_FOUND", "Kijker niet gevonden", 404);
|
||||||
|
return viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAccess(
|
||||||
|
viewerId: string,
|
||||||
|
input: { enabled: boolean; email?: string | null },
|
||||||
|
admin?: AdminContext
|
||||||
|
): Promise<{ viewer: ViewerGooglePlayFields; syncError?: string }> {
|
||||||
|
const viewer = await this.getViewerOrThrow(viewerId);
|
||||||
|
|
||||||
|
let googlePlayEmail = viewer.googlePlayEmail;
|
||||||
|
if (input.email !== undefined) {
|
||||||
|
if (input.email === null || input.email.trim() === "") {
|
||||||
|
googlePlayEmail = null;
|
||||||
|
} else {
|
||||||
|
if (!isValidEmail(input.email)) {
|
||||||
|
throw new AppError("INVALID_REQUEST", "Ongeldig Google Play e-mailadres", 400);
|
||||||
|
}
|
||||||
|
googlePlayEmail = normalizeEmail(input.email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStatus: GooglePlaySyncStatus = input.enabled ? "PENDING_ADD" : "PENDING_REMOVE";
|
||||||
|
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewerId },
|
||||||
|
data: {
|
||||||
|
googlePlayEmail,
|
||||||
|
googlePlayAccess: input.enabled,
|
||||||
|
googlePlaySyncStatus: input.enabled ? nextStatus : nextStatus,
|
||||||
|
googlePlaySyncError: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sync = await this.syncUser(viewerId, admin, input.enabled ? "ADD" : "REMOVE");
|
||||||
|
const updated = await this.getViewerOrThrow(viewerId);
|
||||||
|
return {
|
||||||
|
viewer: this.toPublicFields(updated),
|
||||||
|
syncError: sync.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrySync(viewerId: string, admin?: AdminContext) {
|
||||||
|
const viewer = await this.getViewerOrThrow(viewerId);
|
||||||
|
const action = viewer.googlePlayAccess ? "RETRY" : "RETRY";
|
||||||
|
const sync = await this.syncUser(viewerId, admin, action);
|
||||||
|
const updated = await this.getViewerOrThrow(viewerId);
|
||||||
|
return { viewer: this.toPublicFields(updated), syncError: sync.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncUser(
|
||||||
|
viewerId: string,
|
||||||
|
admin?: AdminContext,
|
||||||
|
auditAction: "ADD" | "REMOVE" | "SYNC" | "RETRY" | "RECONCILE" = "SYNC"
|
||||||
|
): Promise<SyncResult> {
|
||||||
|
const viewer = await this.getViewerOrThrow(viewerId);
|
||||||
|
const email = resolveGooglePlayEmail(viewer);
|
||||||
|
|
||||||
|
if (!viewer.googlePlayAccess) {
|
||||||
|
return this.removeAccess(viewer, email, admin, auditAction);
|
||||||
|
}
|
||||||
|
return this.addAccess(viewer, email, admin, auditAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addAccess(
|
||||||
|
viewer: Awaited<ReturnType<GooglePlayAccessService["getViewerOrThrow"]>>,
|
||||||
|
email: string,
|
||||||
|
admin: AdminContext | undefined,
|
||||||
|
auditAction: "ADD" | "REMOVE" | "SYNC" | "RETRY" | "RECONCILE"
|
||||||
|
): Promise<SyncResult> {
|
||||||
|
if (!this.isConfigured()) {
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewer.id },
|
||||||
|
data: {
|
||||||
|
googlePlaySyncStatus: "ERROR",
|
||||||
|
googlePlaySyncError: "Google Play-toegang is niet geconfigureerd op de server",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewer.id,
|
||||||
|
targetEmail: email,
|
||||||
|
action: auditAction,
|
||||||
|
result: "ERROR",
|
||||||
|
errorMessage: "Not configured",
|
||||||
|
});
|
||||||
|
return { viewerId: viewer.id, email, action: "error", error: "Not configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = this.groupsClient();
|
||||||
|
const membership = await client.createMembership(email);
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewer.id },
|
||||||
|
data: {
|
||||||
|
googlePlayAccess: true,
|
||||||
|
googlePlayMembershipId: membership.name,
|
||||||
|
googlePlaySyncStatus: "ACTIVE",
|
||||||
|
googlePlayLastSyncedAt: new Date(),
|
||||||
|
googlePlaySyncError: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewer.id,
|
||||||
|
targetEmail: email,
|
||||||
|
action: auditAction,
|
||||||
|
result: "SUCCESS",
|
||||||
|
membershipId: membership.name,
|
||||||
|
});
|
||||||
|
return { viewerId: viewer.id, email, action: "added" };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewer.id },
|
||||||
|
data: {
|
||||||
|
googlePlaySyncStatus: "ERROR",
|
||||||
|
googlePlaySyncError: message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewer.id,
|
||||||
|
targetEmail: email,
|
||||||
|
action: auditAction,
|
||||||
|
result: "ERROR",
|
||||||
|
errorMessage: message,
|
||||||
|
});
|
||||||
|
return { viewerId: viewer.id, email, action: "error", error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeAccess(
|
||||||
|
viewer: Awaited<ReturnType<GooglePlayAccessService["getViewerOrThrow"]>>,
|
||||||
|
email: string,
|
||||||
|
admin: AdminContext | undefined,
|
||||||
|
auditAction: "ADD" | "REMOVE" | "SYNC" | "RETRY" | "RECONCILE"
|
||||||
|
): Promise<SyncResult> {
|
||||||
|
if (!this.isConfigured()) {
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewer.id },
|
||||||
|
data: {
|
||||||
|
googlePlayAccess: false,
|
||||||
|
googlePlayMembershipId: null,
|
||||||
|
googlePlaySyncStatus: "REMOVED",
|
||||||
|
googlePlayLastSyncedAt: new Date(),
|
||||||
|
googlePlaySyncError: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { viewerId: viewer.id, email, action: "ok" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = this.groupsClient();
|
||||||
|
if (viewer.googlePlayMembershipId) {
|
||||||
|
await client.deleteMembership(viewer.googlePlayMembershipId);
|
||||||
|
} else {
|
||||||
|
await client.deleteMembershipByEmail(email);
|
||||||
|
}
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewer.id },
|
||||||
|
data: {
|
||||||
|
googlePlayAccess: false,
|
||||||
|
googlePlayMembershipId: null,
|
||||||
|
googlePlaySyncStatus: "REMOVED",
|
||||||
|
googlePlayLastSyncedAt: new Date(),
|
||||||
|
googlePlaySyncError: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewer.id,
|
||||||
|
targetEmail: email,
|
||||||
|
action: auditAction,
|
||||||
|
result: "SUCCESS",
|
||||||
|
});
|
||||||
|
return { viewerId: viewer.id, email, action: "removed" };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewer.id },
|
||||||
|
data: {
|
||||||
|
googlePlaySyncStatus: "ERROR",
|
||||||
|
googlePlaySyncError: message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewer.id,
|
||||||
|
targetEmail: email,
|
||||||
|
action: auditAction,
|
||||||
|
result: "ERROR",
|
||||||
|
errorMessage: message,
|
||||||
|
});
|
||||||
|
return { viewerId: viewer.id, email, action: "error", error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasAccess(email: string): Promise<boolean> {
|
||||||
|
if (!this.isConfigured()) return false;
|
||||||
|
const client = this.groupsClient();
|
||||||
|
const found = await client.lookupMembership(normalizeEmail(email));
|
||||||
|
return found !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMembership(email: string): Promise<{ name: string } | null> {
|
||||||
|
if (!this.isConfigured()) return null;
|
||||||
|
const client = this.groupsClient();
|
||||||
|
return client.lookupMembership(normalizeEmail(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
async reconcileUser(viewerId: string, admin?: AdminContext): Promise<SyncResult> {
|
||||||
|
const viewer = await this.getViewerOrThrow(viewerId);
|
||||||
|
const email = resolveGooglePlayEmail(viewer);
|
||||||
|
|
||||||
|
if (!this.isConfigured()) {
|
||||||
|
return { viewerId, email, action: "error", error: "Not configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = this.groupsClient();
|
||||||
|
const member = await client.lookupMembership(email);
|
||||||
|
const isMember = member !== null;
|
||||||
|
|
||||||
|
if (viewer.googlePlayAccess && isMember) {
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewerId },
|
||||||
|
data: {
|
||||||
|
googlePlayMembershipId: member!.name,
|
||||||
|
googlePlaySyncStatus: "ACTIVE",
|
||||||
|
googlePlayLastSyncedAt: new Date(),
|
||||||
|
googlePlaySyncError: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewerId,
|
||||||
|
targetEmail: email,
|
||||||
|
action: "RECONCILE",
|
||||||
|
result: "SUCCESS",
|
||||||
|
membershipId: member!.name,
|
||||||
|
});
|
||||||
|
return { viewerId, email, action: "ok" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewer.googlePlayAccess && !isMember) {
|
||||||
|
return this.addAccess(viewer, email, admin, "RECONCILE");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewer.googlePlayAccess && isMember) {
|
||||||
|
return this.removeAccess(
|
||||||
|
{ ...viewer, googlePlayMembershipId: member!.name },
|
||||||
|
email,
|
||||||
|
admin,
|
||||||
|
"RECONCILE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewerId },
|
||||||
|
data: {
|
||||||
|
googlePlayMembershipId: null,
|
||||||
|
googlePlaySyncStatus: viewer.googlePlayAccess ? "ACTIVE" : "REMOVED",
|
||||||
|
googlePlayLastSyncedAt: new Date(),
|
||||||
|
googlePlaySyncError: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewerId,
|
||||||
|
targetEmail: email,
|
||||||
|
action: "RECONCILE",
|
||||||
|
result: "SUCCESS",
|
||||||
|
});
|
||||||
|
return { viewerId, email, action: "ok" };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
await logGooglePlayAudit({
|
||||||
|
...admin,
|
||||||
|
viewerUserId: viewerId,
|
||||||
|
targetEmail: email,
|
||||||
|
action: "RECONCILE",
|
||||||
|
result: "ERROR",
|
||||||
|
errorMessage: message,
|
||||||
|
});
|
||||||
|
return { viewerId, email, action: "error", error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncAllUsers(admin?: AdminContext): Promise<BulkSyncSummary> {
|
||||||
|
const viewers = await prisma.viewerUser.findMany({ select: { id: true } });
|
||||||
|
return this.runBulk(viewers.map((v) => v.id), (id) => this.syncUser(id, admin, "SYNC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async reconcileAll(admin?: AdminContext): Promise<BulkSyncSummary> {
|
||||||
|
const viewers = await prisma.viewerUser.findMany({ select: { id: true } });
|
||||||
|
return this.runBulk(viewers.map((v) => v.id), (id) => this.reconcileUser(id, admin));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runBulk(
|
||||||
|
ids: string[],
|
||||||
|
fn: (id: string) => Promise<SyncResult>
|
||||||
|
): Promise<BulkSyncSummary> {
|
||||||
|
const summary: BulkSyncSummary = {
|
||||||
|
scanned: ids.length,
|
||||||
|
correct: 0,
|
||||||
|
added: 0,
|
||||||
|
removed: 0,
|
||||||
|
errors: 0,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const result = await fn(id);
|
||||||
|
summary.results.push(result);
|
||||||
|
if (result.action === "ok") summary.correct++;
|
||||||
|
else if (result.action === "added") summary.added++;
|
||||||
|
else if (result.action === "removed") summary.removed++;
|
||||||
|
else if (result.action === "error") summary.errors++;
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verwijder Play-lidmaatschap wanneer kijker wordt gedeactiveerd. */
|
||||||
|
async onViewerDisabled(viewerId: string, admin?: AdminContext): Promise<void> {
|
||||||
|
const viewer = await this.getViewerOrThrow(viewerId);
|
||||||
|
if (!viewer.googlePlayAccess && viewer.googlePlaySyncStatus === "NOT_ENABLED") return;
|
||||||
|
|
||||||
|
await prisma.viewerUser.update({
|
||||||
|
where: { id: viewerId },
|
||||||
|
data: {
|
||||||
|
googlePlayAccess: false,
|
||||||
|
googlePlaySyncStatus: "PENDING_REMOVE",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.removeAccess(
|
||||||
|
{ ...viewer, googlePlayAccess: false },
|
||||||
|
resolveGooglePlayEmail(viewer),
|
||||||
|
admin,
|
||||||
|
"REMOVE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startGooglePlayReconciliationPoller(
|
||||||
|
service: GooglePlayAccessService,
|
||||||
|
intervalMs: number
|
||||||
|
): () => void {
|
||||||
|
if (!service.isConfigured() || intervalMs <= 0) {
|
||||||
|
return () => undefined;
|
||||||
|
}
|
||||||
|
const handle = setInterval(() => {
|
||||||
|
service.reconcileAll().catch((err) => {
|
||||||
|
console.error("[google-play] Scheduled reconciliation failed:", err);
|
||||||
|
});
|
||||||
|
}, intervalMs);
|
||||||
|
return () => clearInterval(handle);
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import type { FastifyInstance } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
import { AppError } from "../security/errors";
|
import { AppError } from "../security/errors";
|
||||||
import { requireAdmin } from "../auth/routes";
|
import { requireAdmin, type AuthenticatedRequest } from "../auth/routes";
|
||||||
import { PlaybackService } from "../playback/service";
|
import { PlaybackService } from "../playback/service";
|
||||||
import type { Config } from "../config";
|
import type { Config } from "../config";
|
||||||
import { ViewerService } from "./service";
|
import { ViewerService } from "./service";
|
||||||
import type { DownloadService } from "../downloads/service";
|
import type { DownloadService } from "../downloads/service";
|
||||||
import type { SubtitleSource } from "../opensubtitles/client";
|
import type { SubtitleSource } from "../opensubtitles/client";
|
||||||
|
import { GooglePlayAccessService } from "../google-play/service";
|
||||||
|
|
||||||
export function registerViewerRoutes(
|
export function registerViewerRoutes(
|
||||||
app: FastifyInstance,
|
app: FastifyInstance,
|
||||||
|
|
@ -14,6 +15,7 @@ export function registerViewerRoutes(
|
||||||
) {
|
) {
|
||||||
const playback = new PlaybackService(config);
|
const playback = new PlaybackService(config);
|
||||||
const viewers = new ViewerService(config, playback, downloads);
|
const viewers = new ViewerService(config, playback, downloads);
|
||||||
|
const googlePlay = new GooglePlayAccessService(config);
|
||||||
|
|
||||||
// ——— Admin: kijkers ———
|
// ——— Admin: kijkers ———
|
||||||
app.get("/api/v1/admin/viewers", { preHandler: requireAdmin }, async () => {
|
app.get("/api/v1/admin/viewers", { preHandler: requireAdmin }, async () => {
|
||||||
|
|
@ -45,6 +47,13 @@ export function registerViewerRoutes(
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
const viewer = await viewers.updateViewer(id, body);
|
const viewer = await viewers.updateViewer(id, body);
|
||||||
|
if (body.enabled === false) {
|
||||||
|
const admin = (request as AuthenticatedRequest).user;
|
||||||
|
await googlePlay.onViewerDisabled(id, {
|
||||||
|
adminUserId: admin.id,
|
||||||
|
adminEmail: admin.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
return { viewer };
|
return { viewer };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,13 @@ export class ViewerService {
|
||||||
name: v.name,
|
name: v.name,
|
||||||
enabled: v.enabled,
|
enabled: v.enabled,
|
||||||
appAccess: v.appAccess,
|
appAccess: v.appAccess,
|
||||||
|
googlePlayEmail: v.googlePlayEmail,
|
||||||
|
googlePlayAccess: v.googlePlayAccess,
|
||||||
|
googlePlayMembershipId: v.googlePlayMembershipId,
|
||||||
|
googlePlaySyncStatus: v.googlePlaySyncStatus,
|
||||||
|
googlePlayLastSyncedAt: v.googlePlayLastSyncedAt,
|
||||||
|
googlePlaySyncError: v.googlePlaySyncError,
|
||||||
|
googlePlayResolvedEmail: v.googlePlayEmail?.trim() || v.email,
|
||||||
createdAt: v.createdAt,
|
createdAt: v.createdAt,
|
||||||
deviceCount: v._count.devices,
|
deviceCount: v._count.devices,
|
||||||
devices: v.devices,
|
devices: v.devices,
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,13 @@ PLAYBACK_ABSOLUTE_LIFETIME_SECONDS=43200
|
||||||
NODE_HEARTBEAT_INTERVAL_SECONDS=30
|
NODE_HEARTBEAT_INTERVAL_SECONDS=30
|
||||||
NODE_OFFLINE_THRESHOLD_SECONDS=90
|
NODE_OFFLINE_THRESHOLD_SECONDS=90
|
||||||
HIDE_UNAVAILABLE_TITLES=true
|
HIDE_UNAVAILABLE_TITLES=true
|
||||||
|
|
||||||
|
# Google Play Closed Testing (optioneel — zie GOOGLE_PLAY_ACCESS_SETUP.md)
|
||||||
|
# GOOGLE_CLOUD_PROJECT_ID=
|
||||||
|
# GOOGLE_SERVICE_ACCOUNT_EMAIL=
|
||||||
|
# GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=
|
||||||
|
# GOOGLE_WORKSPACE_ADMIN_EMAIL=
|
||||||
|
# PLAY_ACCESS_GROUP_ID=
|
||||||
|
# PLAY_ACCESS_GROUP_EMAIL=
|
||||||
|
# GOOGLE_PLAY_OPT_IN_URL=
|
||||||
|
# GOOGLE_PLAY_SYNC_INTERVAL_MS=86400000
|
||||||
|
|
|
||||||
459
pnpm-lock.yaml
459
pnpm-lock.yaml
|
|
@ -62,9 +62,9 @@ importers:
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
fastify:
|
google-auth-library:
|
||||||
specifier: ^5.2.1
|
specifier: ^9.15.1
|
||||||
version: 5.12.1
|
version: 9.15.1(supports-color@5.5.0)
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.4.2
|
specifier: ^5.4.2
|
||||||
version: 5.11.1(supports-color@5.5.0)
|
version: 5.11.1(supports-color@5.5.0)
|
||||||
|
|
@ -274,30 +274,12 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@fastify/ajv-compiler@4.0.6':
|
|
||||||
resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==}
|
|
||||||
|
|
||||||
'@fastify/cookie@11.1.2':
|
'@fastify/cookie@11.1.2':
|
||||||
resolution: {integrity: sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==}
|
resolution: {integrity: sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==}
|
||||||
|
|
||||||
'@fastify/cors@10.1.0':
|
'@fastify/cors@10.1.0':
|
||||||
resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==}
|
resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==}
|
||||||
|
|
||||||
'@fastify/error@4.2.0':
|
|
||||||
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
|
|
||||||
|
|
||||||
'@fastify/fast-json-stringify-compiler@5.1.0':
|
|
||||||
resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==}
|
|
||||||
|
|
||||||
'@fastify/forwarded@3.0.2':
|
|
||||||
resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==}
|
|
||||||
|
|
||||||
'@fastify/merge-json-schemas@0.2.1':
|
|
||||||
resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
|
|
||||||
|
|
||||||
'@fastify/proxy-addr@5.1.0':
|
|
||||||
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
|
||||||
|
|
||||||
'@fastify/rate-limit@10.3.0':
|
'@fastify/rate-limit@10.3.0':
|
||||||
resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==}
|
resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==}
|
||||||
|
|
||||||
|
|
@ -523,9 +505,6 @@ packages:
|
||||||
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0':
|
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
|
||||||
|
|
||||||
'@prisma/client@6.19.3':
|
'@prisma/client@6.19.3':
|
||||||
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
@ -576,23 +555,13 @@ packages:
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
abstract-logging@2.0.1:
|
|
||||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
ajv-formats@3.0.1:
|
agent-base@7.1.4:
|
||||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||||
peerDependencies:
|
engines: {node: '>= 14'}
|
||||||
ajv: ^8.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
ajv:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ajv@8.20.0:
|
|
||||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
|
||||||
|
|
||||||
ansi-escapes@3.2.0:
|
ansi-escapes@3.2.0:
|
||||||
resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==}
|
resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==}
|
||||||
|
|
@ -620,17 +589,19 @@ packages:
|
||||||
array-flatten@3.0.0:
|
array-flatten@3.0.0:
|
||||||
resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==}
|
resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
avvio@9.3.0:
|
bignumber.js@9.3.1:
|
||||||
resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==}
|
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||||
|
|
||||||
body-parser@1.20.6:
|
body-parser@1.20.6:
|
||||||
resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==}
|
resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -713,10 +684,6 @@ packages:
|
||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
cookie@1.1.1:
|
|
||||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
cookie@2.0.1:
|
cookie@2.0.1:
|
||||||
resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==}
|
resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
@ -760,10 +727,6 @@ packages:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
dequal@2.0.3:
|
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
destr@2.0.5:
|
destr@2.0.5:
|
||||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||||
|
|
||||||
|
|
@ -786,6 +749,9 @@ packages:
|
||||||
duplexify@4.1.3:
|
duplexify@4.1.3:
|
||||||
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
|
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
|
|
@ -838,6 +804,9 @@ packages:
|
||||||
exsolve@1.1.1:
|
exsolve@1.1.1:
|
||||||
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
|
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
|
||||||
|
|
||||||
|
extend@3.0.2:
|
||||||
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
|
|
||||||
external-editor@3.1.0:
|
external-editor@3.1.0:
|
||||||
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
|
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
@ -846,36 +815,12 @@ packages:
|
||||||
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
|
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
fast-decode-uri-component@1.0.1:
|
|
||||||
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
|
||||||
|
|
||||||
fast-deep-equal@3.1.3:
|
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
|
||||||
|
|
||||||
fast-json-stringify@7.0.1:
|
|
||||||
resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==}
|
|
||||||
|
|
||||||
fast-querystring@1.1.2:
|
|
||||||
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
|
|
||||||
|
|
||||||
fast-uri@3.1.6:
|
|
||||||
resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==}
|
|
||||||
|
|
||||||
fast-uri@4.1.3:
|
|
||||||
resolution: {integrity: sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw==}
|
|
||||||
|
|
||||||
fastify-plugin@5.1.0:
|
fastify-plugin@5.1.0:
|
||||||
resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
|
resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
|
||||||
|
|
||||||
fastify-plugin@6.0.0:
|
fastify-plugin@6.0.0:
|
||||||
resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==}
|
resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==}
|
||||||
|
|
||||||
fastify@5.12.1:
|
|
||||||
resolution: {integrity: sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==}
|
|
||||||
|
|
||||||
fastq@1.20.1:
|
|
||||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
|
||||||
|
|
||||||
figures@2.0.0:
|
figures@2.0.0:
|
||||||
resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
|
resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
@ -884,10 +829,6 @@ packages:
|
||||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
find-my-way@9.9.0:
|
|
||||||
resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
@ -904,6 +845,14 @@ packages:
|
||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
gaxios@6.7.1:
|
||||||
|
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
gcp-metadata@6.1.1:
|
||||||
|
resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -916,10 +865,22 @@ packages:
|
||||||
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
google-auth-library@9.15.1:
|
||||||
|
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
google-logging-utils@0.0.2:
|
||||||
|
resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
gopd@1.2.0:
|
gopd@1.2.0:
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
gtoken@7.1.0:
|
||||||
|
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
has-flag@3.0.0:
|
has-flag@3.0.0:
|
||||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
@ -936,6 +897,10 @@ packages:
|
||||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
https-proxy-agent@7.0.6:
|
||||||
|
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -955,14 +920,14 @@ packages:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
ipaddr.js@2.5.0:
|
|
||||||
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
|
|
||||||
is-fullwidth-code-point@2.0.0:
|
is-fullwidth-code-point@2.0.0:
|
||||||
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
|
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
is-stream@2.0.1:
|
||||||
|
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-wsl@1.1.0:
|
is-wsl@1.1.0:
|
||||||
resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==}
|
resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
@ -971,14 +936,14 @@ packages:
|
||||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
json-schema-ref-resolver@3.0.0:
|
json-bigint@1.0.0:
|
||||||
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
|
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0:
|
jwa@2.0.1:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||||
|
|
||||||
light-my-request@6.6.0:
|
jws@4.0.1:
|
||||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
lodash@4.18.1:
|
lodash@4.18.1:
|
||||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||||
|
|
@ -1108,10 +1073,6 @@ packages:
|
||||||
ohash@2.0.12:
|
ohash@2.0.12:
|
||||||
resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
|
resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2:
|
|
||||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@2.4.1:
|
||||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -1150,16 +1111,6 @@ packages:
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
pino-abstract-transport@3.0.0:
|
|
||||||
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
|
|
||||||
|
|
||||||
pino-std-serializers@7.1.0:
|
|
||||||
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
|
|
||||||
|
|
||||||
pino@10.3.1:
|
|
||||||
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
pkg-types@2.3.1:
|
pkg-types@2.3.1:
|
||||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||||
|
|
||||||
|
|
@ -1177,12 +1128,6 @@ packages:
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
process-warning@4.0.1:
|
|
||||||
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
|
||||||
|
|
||||||
process-warning@5.1.0:
|
|
||||||
resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==}
|
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
@ -1194,9 +1139,6 @@ packages:
|
||||||
resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
|
resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
|
||||||
|
|
||||||
range-parser@1.2.1:
|
range-parser@1.2.1:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
@ -1225,13 +1167,6 @@ packages:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
real-require@0.2.0:
|
|
||||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
|
||||||
engines: {node: '>= 12.13.0'}
|
|
||||||
|
|
||||||
real-require@1.0.0:
|
|
||||||
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
|
|
||||||
|
|
||||||
redis-errors@1.2.0:
|
redis-errors@1.2.0:
|
||||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
@ -1240,25 +1175,10 @@ packages:
|
||||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
require-from-string@2.0.2:
|
|
||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
restore-cursor@2.0.0:
|
restore-cursor@2.0.0:
|
||||||
resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
|
resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
ret@0.5.0:
|
|
||||||
resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
reusify@1.1.0:
|
|
||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
|
||||||
|
|
||||||
rfdc@1.4.1:
|
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
|
||||||
|
|
||||||
router@1.3.8:
|
router@1.3.8:
|
||||||
resolution: {integrity: sha512-461UFH44NtSfIlS83PUg2N7OZo86BC/kB3dY77gJdsODsBhhw7+2uE0tzTINxrY9CahCUVk1VhpWCA5i1yoIEg==}
|
resolution: {integrity: sha512-461UFH44NtSfIlS83PUg2N7OZo86BC/kB3dY77gJdsODsBhhw7+2uE0tzTINxrY9CahCUVk1VhpWCA5i1yoIEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -1274,23 +1194,12 @@ packages:
|
||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
safe-regex2@5.1.1:
|
|
||||||
resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
safe-stable-stringify@2.5.0:
|
|
||||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
secure-json-parse@4.1.0:
|
|
||||||
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
|
||||||
|
|
||||||
semver@5.7.2:
|
semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -1308,9 +1217,6 @@ packages:
|
||||||
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
set-cookie-parser@2.7.2:
|
|
||||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
|
||||||
|
|
||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
|
|
@ -1337,17 +1243,10 @@ packages:
|
||||||
signal-exit@3.0.7:
|
signal-exit@3.0.7:
|
||||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
|
|
||||||
sonic-boom@4.2.1:
|
|
||||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
split2@4.2.0:
|
|
||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
|
||||||
engines: {node: '>= 10.x'}
|
|
||||||
|
|
||||||
standard-as-callback@2.1.0:
|
standard-as-callback@2.1.0:
|
||||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||||
|
|
||||||
|
|
@ -1397,10 +1296,6 @@ packages:
|
||||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
|
||||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
|
|
||||||
through@2.3.8:
|
through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
|
|
||||||
|
|
@ -1457,6 +1352,11 @@ packages:
|
||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
uuid@9.0.1:
|
||||||
|
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||||
|
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -1570,12 +1470,6 @@ snapshots:
|
||||||
'@esbuild/win32-x64@0.28.2':
|
'@esbuild/win32-x64@0.28.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@fastify/ajv-compiler@4.0.6':
|
|
||||||
dependencies:
|
|
||||||
ajv: 8.20.0
|
|
||||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
|
||||||
fast-uri: 4.1.3
|
|
||||||
|
|
||||||
'@fastify/cookie@11.1.2':
|
'@fastify/cookie@11.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie: 2.0.1
|
cookie: 2.0.1
|
||||||
|
|
@ -1586,23 +1480,6 @@ snapshots:
|
||||||
fastify-plugin: 5.1.0
|
fastify-plugin: 5.1.0
|
||||||
mnemonist: 0.40.0
|
mnemonist: 0.40.0
|
||||||
|
|
||||||
'@fastify/error@4.2.0': {}
|
|
||||||
|
|
||||||
'@fastify/fast-json-stringify-compiler@5.1.0':
|
|
||||||
dependencies:
|
|
||||||
fast-json-stringify: 7.0.1
|
|
||||||
|
|
||||||
'@fastify/forwarded@3.0.2': {}
|
|
||||||
|
|
||||||
'@fastify/merge-json-schemas@0.2.1':
|
|
||||||
dependencies:
|
|
||||||
dequal: 2.0.3
|
|
||||||
|
|
||||||
'@fastify/proxy-addr@5.1.0':
|
|
||||||
dependencies:
|
|
||||||
'@fastify/forwarded': 3.0.2
|
|
||||||
ipaddr.js: 2.5.0
|
|
||||||
|
|
||||||
'@fastify/rate-limit@10.3.0':
|
'@fastify/rate-limit@10.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lukeed/ms': 2.0.2
|
'@lukeed/ms': 2.0.2
|
||||||
|
|
@ -1747,8 +1624,6 @@ snapshots:
|
||||||
|
|
||||||
'@phc/format@1.0.0': {}
|
'@phc/format@1.0.0': {}
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
|
||||||
|
|
||||||
'@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)':
|
'@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
prisma: 6.19.3(typescript@5.9.3)
|
prisma: 6.19.3(typescript@5.9.3)
|
||||||
|
|
@ -1806,23 +1681,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.20.1
|
'@types/node': 22.20.1
|
||||||
|
|
||||||
abstract-logging@2.0.1: {}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
negotiator: 0.6.3
|
negotiator: 0.6.3
|
||||||
|
|
||||||
ajv-formats@3.0.1(ajv@8.20.0):
|
agent-base@7.1.4: {}
|
||||||
optionalDependencies:
|
|
||||||
ajv: 8.20.0
|
|
||||||
|
|
||||||
ajv@8.20.0:
|
|
||||||
dependencies:
|
|
||||||
fast-deep-equal: 3.1.3
|
|
||||||
fast-uri: 3.1.6
|
|
||||||
json-schema-traverse: 1.0.0
|
|
||||||
require-from-string: 2.0.2
|
|
||||||
|
|
||||||
ansi-escapes@3.2.0: {}
|
ansi-escapes@3.2.0: {}
|
||||||
|
|
||||||
|
|
@ -1844,12 +1708,9 @@ snapshots:
|
||||||
|
|
||||||
array-flatten@3.0.0: {}
|
array-flatten@3.0.0: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
avvio@9.3.0:
|
bignumber.js@9.3.1: {}
|
||||||
dependencies:
|
|
||||||
'@fastify/error': 4.2.0
|
|
||||||
fastq: 1.20.1
|
|
||||||
|
|
||||||
body-parser@1.20.6(supports-color@5.5.0):
|
body-parser@1.20.6(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -1868,6 +1729,8 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
c12@3.1.0:
|
c12@3.1.0:
|
||||||
|
|
@ -1945,8 +1808,6 @@ snapshots:
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
cookie@1.1.1: {}
|
|
||||||
|
|
||||||
cookie@2.0.1: {}
|
cookie@2.0.1: {}
|
||||||
|
|
||||||
cors@2.8.6:
|
cors@2.8.6:
|
||||||
|
|
@ -1976,8 +1837,6 @@ snapshots:
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
|
||||||
|
|
||||||
destr@2.0.5: {}
|
destr@2.0.5: {}
|
||||||
|
|
||||||
destroy@1.2.0: {}
|
destroy@1.2.0: {}
|
||||||
|
|
@ -2000,6 +1859,10 @@ snapshots:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
stream-shift: 1.0.3
|
stream-shift: 1.0.3
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
effect@3.21.0:
|
effect@3.21.0:
|
||||||
|
|
@ -2096,6 +1959,8 @@ snapshots:
|
||||||
|
|
||||||
exsolve@1.1.1: {}
|
exsolve@1.1.1: {}
|
||||||
|
|
||||||
|
extend@3.0.2: {}
|
||||||
|
|
||||||
external-editor@3.1.0:
|
external-editor@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
chardet: 0.7.0
|
chardet: 0.7.0
|
||||||
|
|
@ -2106,53 +1971,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
pure-rand: 6.1.0
|
pure-rand: 6.1.0
|
||||||
|
|
||||||
fast-decode-uri-component@1.0.1: {}
|
|
||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
|
||||||
|
|
||||||
fast-json-stringify@7.0.1:
|
|
||||||
dependencies:
|
|
||||||
'@fastify/merge-json-schemas': 0.2.1
|
|
||||||
ajv: 8.20.0
|
|
||||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
|
||||||
fast-uri: 4.1.3
|
|
||||||
json-schema-ref-resolver: 3.0.0
|
|
||||||
rfdc: 1.4.1
|
|
||||||
|
|
||||||
fast-querystring@1.1.2:
|
|
||||||
dependencies:
|
|
||||||
fast-decode-uri-component: 1.0.1
|
|
||||||
|
|
||||||
fast-uri@3.1.6: {}
|
|
||||||
|
|
||||||
fast-uri@4.1.3: {}
|
|
||||||
|
|
||||||
fastify-plugin@5.1.0: {}
|
fastify-plugin@5.1.0: {}
|
||||||
|
|
||||||
fastify-plugin@6.0.0: {}
|
fastify-plugin@6.0.0: {}
|
||||||
|
|
||||||
fastify@5.12.1:
|
|
||||||
dependencies:
|
|
||||||
'@fastify/ajv-compiler': 4.0.6
|
|
||||||
'@fastify/error': 4.2.0
|
|
||||||
'@fastify/fast-json-stringify-compiler': 5.1.0
|
|
||||||
'@fastify/proxy-addr': 5.1.0
|
|
||||||
abstract-logging: 2.0.1
|
|
||||||
avvio: 9.3.0
|
|
||||||
fast-json-stringify: 7.0.1
|
|
||||||
find-my-way: 9.9.0
|
|
||||||
light-my-request: 6.6.0
|
|
||||||
pino: 10.3.1
|
|
||||||
process-warning: 5.1.0
|
|
||||||
rfdc: 1.4.1
|
|
||||||
secure-json-parse: 4.1.0
|
|
||||||
semver: 7.8.5
|
|
||||||
toad-cache: 3.7.4
|
|
||||||
|
|
||||||
fastq@1.20.1:
|
|
||||||
dependencies:
|
|
||||||
reusify: 1.1.0
|
|
||||||
|
|
||||||
figures@2.0.0:
|
figures@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
|
|
@ -2169,12 +1991,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
find-my-way@9.9.0:
|
|
||||||
dependencies:
|
|
||||||
fast-deep-equal: 3.1.3
|
|
||||||
fast-querystring: 1.1.2
|
|
||||||
safe-regex2: 5.1.1
|
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
|
|
@ -2184,6 +2000,26 @@ snapshots:
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
gaxios@6.7.1(supports-color@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
extend: 3.0.2
|
||||||
|
https-proxy-agent: 7.0.6(supports-color@5.5.0)
|
||||||
|
is-stream: 2.0.1
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
uuid: 9.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
gcp-metadata@6.1.1(supports-color@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
gaxios: 6.7.1(supports-color@5.5.0)
|
||||||
|
google-logging-utils: 0.0.2
|
||||||
|
json-bigint: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
|
@ -2211,8 +2047,30 @@ snapshots:
|
||||||
nypm: 0.6.9
|
nypm: 0.6.9
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
google-auth-library@9.15.1(supports-color@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
gaxios: 6.7.1(supports-color@5.5.0)
|
||||||
|
gcp-metadata: 6.1.1(supports-color@5.5.0)
|
||||||
|
gtoken: 7.1.0(supports-color@5.5.0)
|
||||||
|
jws: 4.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
google-logging-utils@0.0.2: {}
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
|
gtoken@7.1.0(supports-color@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
gaxios: 6.7.1(supports-color@5.5.0)
|
||||||
|
jws: 4.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
has-flag@3.0.0: {}
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
has-symbols@1.1.0: {}
|
has-symbols@1.1.0: {}
|
||||||
|
|
@ -2229,6 +2087,13 @@ snapshots:
|
||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
https-proxy-agent@7.0.6(supports-color@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
agent-base: 7.1.4
|
||||||
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
@ -2265,25 +2130,28 @@ snapshots:
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
ipaddr.js@2.5.0: {}
|
|
||||||
|
|
||||||
is-fullwidth-code-point@2.0.0: {}
|
is-fullwidth-code-point@2.0.0: {}
|
||||||
|
|
||||||
|
is-stream@2.0.1: {}
|
||||||
|
|
||||||
is-wsl@1.1.0: {}
|
is-wsl@1.1.0: {}
|
||||||
|
|
||||||
jiti@2.7.0: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
json-schema-ref-resolver@3.0.0:
|
json-bigint@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
bignumber.js: 9.3.1
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
jwa@2.0.1:
|
||||||
|
|
||||||
light-my-request@6.6.0:
|
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie: 1.1.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
process-warning: 4.0.1
|
ecdsa-sig-formatter: 1.0.11
|
||||||
set-cookie-parser: 2.7.2
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jws@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
jwa: 2.0.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
lodash@4.18.1: {}
|
lodash@4.18.1: {}
|
||||||
|
|
||||||
|
|
@ -2374,8 +2242,6 @@ snapshots:
|
||||||
|
|
||||||
ohash@2.0.12: {}
|
ohash@2.0.12: {}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@2.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
|
|
@ -2406,26 +2272,6 @@ snapshots:
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
pino-abstract-transport@3.0.0:
|
|
||||||
dependencies:
|
|
||||||
split2: 4.2.0
|
|
||||||
|
|
||||||
pino-std-serializers@7.1.0: {}
|
|
||||||
|
|
||||||
pino@10.3.1:
|
|
||||||
dependencies:
|
|
||||||
'@pinojs/redact': 0.4.0
|
|
||||||
atomic-sleep: 1.0.0
|
|
||||||
on-exit-leak-free: 2.1.2
|
|
||||||
pino-abstract-transport: 3.0.0
|
|
||||||
pino-std-serializers: 7.1.0
|
|
||||||
process-warning: 5.1.0
|
|
||||||
quick-format-unescaped: 4.0.4
|
|
||||||
real-require: 0.2.0
|
|
||||||
safe-stable-stringify: 2.5.0
|
|
||||||
sonic-boom: 4.2.1
|
|
||||||
thread-stream: 4.2.0
|
|
||||||
|
|
||||||
pkg-types@2.3.1:
|
pkg-types@2.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
confbox: 0.2.4
|
confbox: 0.2.4
|
||||||
|
|
@ -2447,10 +2293,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
process-warning@4.0.1: {}
|
|
||||||
|
|
||||||
process-warning@5.1.0: {}
|
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
|
|
@ -2463,8 +2305,6 @@ snapshots:
|
||||||
es-define-property: 1.0.1
|
es-define-property: 1.0.1
|
||||||
side-channel: 1.1.1
|
side-channel: 1.1.1
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
|
|
||||||
raw-body@2.5.3:
|
raw-body@2.5.3:
|
||||||
|
|
@ -2494,29 +2334,17 @@ snapshots:
|
||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
real-require@0.2.0: {}
|
|
||||||
|
|
||||||
real-require@1.0.0: {}
|
|
||||||
|
|
||||||
redis-errors@1.2.0: {}
|
redis-errors@1.2.0: {}
|
||||||
|
|
||||||
redis-parser@3.0.0:
|
redis-parser@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
redis-errors: 1.2.0
|
redis-errors: 1.2.0
|
||||||
|
|
||||||
require-from-string@2.0.2: {}
|
|
||||||
|
|
||||||
restore-cursor@2.0.0:
|
restore-cursor@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
onetime: 2.0.1
|
onetime: 2.0.1
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
|
|
||||||
ret@0.5.0: {}
|
|
||||||
|
|
||||||
reusify@1.1.0: {}
|
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
|
||||||
|
|
||||||
router@1.3.8(supports-color@5.5.0):
|
router@1.3.8(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-flatten: 3.0.0
|
array-flatten: 3.0.0
|
||||||
|
|
@ -2537,21 +2365,14 @@ snapshots:
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safe-regex2@5.1.1:
|
|
||||||
dependencies:
|
|
||||||
ret: 0.5.0
|
|
||||||
|
|
||||||
safe-stable-stringify@2.5.0: {}
|
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
secure-json-parse@4.1.0: {}
|
|
||||||
|
|
||||||
semver@5.7.2: {}
|
semver@5.7.2: {}
|
||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5:
|
||||||
|
optional: true
|
||||||
|
|
||||||
send@0.19.2(supports-color@5.5.0):
|
send@0.19.2(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -2580,8 +2401,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
set-cookie-parser@2.7.2: {}
|
|
||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
sharp@0.34.5:
|
sharp@0.34.5:
|
||||||
|
|
@ -2646,14 +2465,8 @@ snapshots:
|
||||||
|
|
||||||
signal-exit@3.0.7: {}
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
sonic-boom@4.2.1:
|
|
||||||
dependencies:
|
|
||||||
atomic-sleep: 1.0.0
|
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
split2@4.2.0: {}
|
|
||||||
|
|
||||||
standard-as-callback@2.1.0: {}
|
standard-as-callback@2.1.0: {}
|
||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
@ -2705,10 +2518,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 3.0.0
|
has-flag: 3.0.0
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
|
||||||
dependencies:
|
|
||||||
real-require: 1.0.0
|
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
|
|
||||||
tinyexec@1.3.0: {}
|
tinyexec@1.3.0: {}
|
||||||
|
|
@ -2748,6 +2557,8 @@ snapshots:
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
uuid@9.0.1: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue