Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import type { SubscriptionPlan, SubscriptionStatus } from "@app/shared-types";
import { resolvePlanFromProductId } from "./entitlements.js";
/**
* Butiksverifiering (spec §47, Del 13). Backend verifierar ALLTID köpet mot
* butiken klientens kvitto är bara ett påstående (spec §61.14).
*
* PRODUKTIONSINTEGRATION (implementeras i fas 7, se docs/utvecklingsfaser.md):
* - Apple: App Store Server API v2 verifiera JWS-signaturen i signedTransaction
* mot Apples publika nycklar, kontrollera bundleId + environment.
* - Google: Play Developer API purchases.subscriptionsv2.get med service account.
*
* Dev/CI: APP_STORE_MODE=sandbox accepterar särskilt formaterade
* sandbox-payloads så att hela flödet kan testas end-to-end utan butikskonton.
*/
export interface VerifiedPurchase {
provider: "apple" | "google";
productId: string;
plan: SubscriptionPlan;
originalTransactionId: string;
status: SubscriptionStatus;
purchasedAt: Date;
expiresAt: Date;
}
export type StoreVerificationResult =
{ ok: true; purchase: VerifiedPurchase } | { ok: false; error: string };
export interface StoreVerifierConfig {
mode: "production" | "sandbox";
appleBundleId?: string | undefined;
googlePackageName?: string | undefined;
}
export class StoreVerifier {
constructor(private readonly cfg: StoreVerifierConfig) {}
async verifyApple(signedTransaction: string): Promise<StoreVerificationResult> {
if (this.cfg.mode === "sandbox") {
return parseSandboxPayload("apple", signedTransaction);
}
// TODO(fas 7): App Store Server API v2 JWS-verifiering.
return {
ok: false,
error:
"Apple-verifiering i produktionsläge är inte konfigurerad ännu. " +
"Kräver APPLE_ISSUER_ID/KEY_ID/PRIVATE_KEY (se docs/subscriptions.md).",
};
}
async verifyGoogle(
packageName: string,
productId: string,
purchaseToken: string,
): Promise<StoreVerificationResult> {
if (this.cfg.mode === "sandbox") {
return parseSandboxPayload("google", purchaseToken, productId);
}
if (this.cfg.googlePackageName && packageName !== this.cfg.googlePackageName) {
return { ok: false, error: `Fel paketnamn: ${packageName}` };
}
// TODO(fas 7): Play Developer API purchases.subscriptionsv2.
return {
ok: false,
error:
"Google-verifiering i produktionsläge är inte konfigurerad ännu. " +
"Kräver GOOGLE_SERVICE_ACCOUNT_JSON (se docs/subscriptions.md).",
};
}
}
/**
* Sandbox-payload: JSON i klartext:
* {"productId":"<iosBundleId>.family_monthly","originalTransactionId":"sandbox-1","expiresInDays":30}
*/
function parseSandboxPayload(
provider: "apple" | "google",
payload: string,
fallbackProductId?: string,
): StoreVerificationResult {
try {
const data = JSON.parse(payload) as {
productId?: string;
originalTransactionId?: string;
expiresInDays?: number;
};
const productId = data.productId ?? fallbackProductId;
if (!productId) return { ok: false, error: "sandbox-payload saknar productId" };
const plan = resolvePlanFromProductId(productId);
if (!plan || plan === "free") {
return { ok: false, error: `Okänt productId: ${productId}` };
}
const now = new Date();
return {
ok: true,
purchase: {
provider,
productId,
plan,
originalTransactionId: data.originalTransactionId ?? `sandbox-${Date.now()}`,
status: "active",
purchasedAt: now,
expiresAt: new Date(now.getTime() + (data.expiresInDays ?? 30) * 86_400_000),
},
};
} catch {
return { ok: false, error: "Ogiltig sandbox-payload (förväntar JSON)" };
}
}