mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Cleanup of SaaS code (#6669)
# Description of Changes De-AI comments and fix ridiculously indented code
This commit is contained in:
+23
-34
@@ -24,6 +24,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
|
|||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.server.resource.OAuth2ProtectedResourceMetadata;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
||||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||||
@@ -201,40 +202,9 @@ public class McpSecurityConfig {
|
|||||||
.protectedResourceMetadata(
|
.protectedResourceMetadata(
|
||||||
prm ->
|
prm ->
|
||||||
prm.protectedResourceMetadataCustomizer(
|
prm.protectedResourceMetadataCustomizer(
|
||||||
builder -> {
|
builder ->
|
||||||
if (!auth.getResourceId()
|
buildResourceMetadata(
|
||||||
.isBlank()) {
|
builder, auth)))
|
||||||
builder.resource(
|
|
||||||
auth
|
|
||||||
.getResourceId());
|
|
||||||
}
|
|
||||||
if (!auth.getIssuerUri()
|
|
||||||
.isBlank()) {
|
|
||||||
builder.authorizationServer(
|
|
||||||
auth
|
|
||||||
.getIssuerUri());
|
|
||||||
}
|
|
||||||
// Only advertise the granular
|
|
||||||
// tool scopes when we actually
|
|
||||||
// enforce them. When scopes are
|
|
||||||
// disabled (e.g. the IdP only
|
|
||||||
// mints coarse tokens, like
|
|
||||||
// Supabase), advertising scopes
|
|
||||||
// the authorization server
|
|
||||||
// can't
|
|
||||||
// issue makes spec-compliant
|
|
||||||
// clients request them and get
|
|
||||||
// rejected with
|
|
||||||
// invalid_request.
|
|
||||||
if (applicationProperties
|
|
||||||
.getMcp()
|
|
||||||
.isScopesEnabled()) {
|
|
||||||
builder.scope(
|
|
||||||
"mcp.tools.read");
|
|
||||||
builder.scope(
|
|
||||||
"mcp.tools.write");
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.jwt(
|
.jwt(
|
||||||
jwt ->
|
jwt ->
|
||||||
jwt.decoder(mcpJwtDecoder)
|
jwt.decoder(mcpJwtDecoder)
|
||||||
@@ -243,6 +213,25 @@ public class McpSecurityConfig {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Populate the RFC 9728 protected-resource metadata document from the configured auth. */
|
||||||
|
private void buildResourceMetadata(
|
||||||
|
OAuth2ProtectedResourceMetadata.Builder builder, ApplicationProperties.Mcp.Auth auth) {
|
||||||
|
if (!auth.getResourceId().isBlank()) {
|
||||||
|
builder.resource(auth.getResourceId());
|
||||||
|
}
|
||||||
|
if (!auth.getIssuerUri().isBlank()) {
|
||||||
|
builder.authorizationServer(auth.getIssuerUri());
|
||||||
|
}
|
||||||
|
// Only advertise the granular tool scopes when we actually enforce them. When scopes are
|
||||||
|
// disabled (e.g. the IdP only mints coarse tokens, like Supabase), advertising scopes the
|
||||||
|
// authorization server can't issue makes spec-compliant clients request them and get
|
||||||
|
// rejected with invalid_request.
|
||||||
|
if (applicationProperties.getMcp().isScopesEnabled()) {
|
||||||
|
builder.scope("mcp.tools.read");
|
||||||
|
builder.scope("mcp.tools.write");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
JwtDecoder mcpJwtDecoder() {
|
JwtDecoder mcpJwtDecoder() {
|
||||||
ApplicationProperties.Mcp.Auth auth = applicationProperties.getMcp().getAuth();
|
ApplicationProperties.Mcp.Auth auth = applicationProperties.getMcp().getAuth();
|
||||||
|
|||||||
@@ -303,9 +303,9 @@ public class PaygWalletController {
|
|||||||
} else {
|
} else {
|
||||||
long capMinor = CapMoneyUnits.usdToCents(req.capUsd());
|
long capMinor = CapMoneyUnits.usdToCents(req.capUsd());
|
||||||
policy.setCapSourceMoney(capMinor);
|
policy.setCapSourceMoney(capMinor);
|
||||||
// Derived document allowance (design §10: store both the money intent and the unit
|
// Derived document allowance: store both the money intent and the unit translation.
|
||||||
// translation). The live snapshot recomputes from cap_source_money + current rate;
|
// The live snapshot recomputes from cap_source_money + current rate; this stored value
|
||||||
// this stored value is the enforcement fallback when the rate is unreachable.
|
// is the enforcement fallback when the rate is unreachable.
|
||||||
TeamBillingContext billing = billingService.forTeam(teamId);
|
TeamBillingContext billing = billingService.forTeam(teamId);
|
||||||
Optional<Long> docCap = billingService.docCapForMoney(billing, capMinor);
|
Optional<Long> docCap = billingService.docCapForMoney(billing, capMinor);
|
||||||
if (docCap.isPresent()) {
|
if (docCap.isPresent()) {
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ public record WalletSnapshotResponse(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* One row of the team-members table on the leader's Plan page — display-only per-member usage.
|
* One row of the team-members table on the leader's Plan page — display-only per-member usage.
|
||||||
* (Per-member sub-caps aren't enforced yet; see the follow-ups note. When they ship, a cap
|
* (Per-member sub-caps aren't enforced yet. When they ship, a cap field returns here.)
|
||||||
* field returns here.)
|
|
||||||
*/
|
*/
|
||||||
public record MemberRow(String userId, String name, String email, int spendUnits) {}
|
public record MemberRow(String userId, String name, String email, int spendUnits) {}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import stirling.software.saas.payg.model.FeatureSet;
|
|||||||
* effect, and the corresponding enabled {@link FeatureGate}s. No DB access, no caches — the caller
|
* effect, and the corresponding enabled {@link FeatureGate}s. No DB access, no caches — the caller
|
||||||
* (entitlement service) supplies the inputs.
|
* (entitlement service) supplies the inputs.
|
||||||
*
|
*
|
||||||
* <p>State transitions (matching {@code notes/PAYG_DESIGN.md} §3.6):
|
* <p>State transitions:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@code capUnits == null} → {@code FULL} / {@link FeatureSet#FULL} unconditionally.
|
* <li>{@code capUnits == null} → {@code FULL} / {@link FeatureSet#FULL} unconditionally.
|
||||||
@@ -78,10 +78,7 @@ public final class CapEvaluator {
|
|||||||
return full();
|
return full();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Default enabled gates for a given feature set. */
|
||||||
* Default enabled gates for a given feature set. Kept in sync with the design doc §3.7 mapping
|
|
||||||
* table.
|
|
||||||
*/
|
|
||||||
public static List<FeatureGate> gatesFor(FeatureSet set) {
|
public static List<FeatureGate> gatesFor(FeatureSet set) {
|
||||||
if (set == null) {
|
if (set == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|||||||
+9
-9
@@ -20,17 +20,17 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
/**
|
/**
|
||||||
* Read-only accessor for the Stripe Sync Engine schema ({@code stripe.*}). Gives the PAYG layer the
|
* Read-only accessor for the Stripe Sync Engine schema ({@code stripe.*}). Gives the PAYG layer the
|
||||||
* team's real billing window and the per-document rate of the Price its subscription bills against
|
* team's real billing window and the per-document rate of the Price its subscription bills against
|
||||||
* — both live in Stripe, mirrored into Postgres by the sync engine, and are NOT duplicated in
|
* - both live in Stripe, mirrored into Postgres by the sync engine, and are NOT duplicated in
|
||||||
* {@code stirling_pdf} (design §10: money lives in Stripe).
|
* {@code stirling_pdf} (money lives in Stripe).
|
||||||
*
|
*
|
||||||
* <p>PAYG prices are plain {@code per_unit} metered prices, so {@code stripe.prices.unit_amount}
|
* <p>PAYG prices are plain {@code per_unit} metered prices, so {@code stripe.prices.unit_amount}
|
||||||
* carries the rate directly. The free grant is deliberately NOT in Stripe — it's the one-time
|
* carries the rate directly. The free grant is deliberately NOT in Stripe - it's the one-time
|
||||||
* {@code pricing_policy.free_tier_units} pool, applied app-side (free units are never metered),
|
* {@code pricing_policy.free_tier_units} pool, applied app-side (free units are never metered),
|
||||||
* because un-subscribed teams get the same grant and have no Stripe Price at all.
|
* because un-subscribed teams get the same grant and have no Stripe Price at all.
|
||||||
*
|
*
|
||||||
* <p>Defensive by construction: the {@code stripe} schema only exists where the sync engine has run
|
* <p>Defensive by construction: the {@code stripe} schema only exists where the sync engine has run
|
||||||
* (dev + prod Supabase, not unit-test H2). Any {@link DataAccessException} — missing schema,
|
* (dev + prod Supabase, not unit-test H2). Any {@link DataAccessException} - missing schema,
|
||||||
* missing row, connectivity blip — degrades to {@link Optional#empty()} with a WARN so callers fall
|
* missing row, connectivity blip - degrades to {@link Optional#empty()} with a WARN so callers fall
|
||||||
* back to calendar-month windows rather than 500ing the wallet endpoint.
|
* back to calendar-month windows rather than 500ing the wallet endpoint.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -60,13 +60,13 @@ public class StripeSubscriptionDao {
|
|||||||
BigDecimal perDocMinor) {}
|
BigDecimal perDocMinor) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The per-document rate of a Price looked up directly (not via a subscription) — used to price
|
* The per-document rate of a Price looked up directly (not via a subscription) - used to price
|
||||||
* the cap estimate for un-subscribed teams, whose default policy points at Stripe Prices that
|
* the cap estimate for un-subscribed teams, whose default policy points at Stripe Prices that
|
||||||
* carry the same {@code unit_amount} they'd be billed at on subscribing.
|
* carry the same {@code unit_amount} they'd be billed at on subscribing.
|
||||||
*
|
*
|
||||||
* @param priceId the resolved Stripe Price id
|
* @param priceId the resolved Stripe Price id
|
||||||
* @param currency lower-case ISO 4217 of that Price
|
* @param currency lower-case ISO 4217 of that Price
|
||||||
* @param perDocMinor per-document rate in minor units (may be fractional); never null — a row
|
* @param perDocMinor per-document rate in minor units (may be fractional); never null - a row
|
||||||
* with no usable amount is filtered out rather than returned with a null rate
|
* with no usable amount is filtered out rather than returned with a null rate
|
||||||
*/
|
*/
|
||||||
public record PriceRate(String priceId, String currency, BigDecimal perDocMinor) {}
|
public record PriceRate(String priceId, String currency, BigDecimal perDocMinor) {}
|
||||||
@@ -130,12 +130,12 @@ public class StripeSubscriptionDao {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-document rate of the active {@code stripe.prices} row with the given {@code lookupKey} in
|
* Per-document rate of the active {@code stripe.prices} row with the given {@code lookupKey} in
|
||||||
* {@code currency} — the elegant mirror of {@link #findBilling}, reading the same synced table
|
* {@code currency} - the elegant mirror of {@link #findBilling}, reading the same synced table
|
||||||
* by Stripe Price {@code lookup_key} instead of via a subscription. The PAYG layer uses this to
|
* by Stripe Price {@code lookup_key} instead of via a subscription. The PAYG layer uses this to
|
||||||
* price the cap estimate for an un-subscribed team: there's no subscription to read a rate off,
|
* price the cap estimate for an un-subscribed team: there's no subscription to read a rate off,
|
||||||
* but the PAYG Price (lookup key {@code plan:processor}) carries the very rate they'd be billed
|
* but the PAYG Price (lookup key {@code plan:processor}) carries the very rate they'd be billed
|
||||||
* at. We resolve by lookup_key rather than the default policy's price ids because those aren't
|
* at. We resolve by lookup_key rather than the default policy's price ids because those aren't
|
||||||
* seeded — the lookup key is the stable, env-agnostic handle (same one the price-lookup edge
|
* seeded - the lookup key is the stable, env-agnostic handle (same one the price-lookup edge
|
||||||
* function uses).
|
* function uses).
|
||||||
*
|
*
|
||||||
* <p>Empty when the {@code stripe} schema is absent (H2 unit tests), no active matching row
|
* <p>Empty when the {@code stripe} schema is absent (H2 unit tests), no active matching row
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
-- restored on a first-step refund). Because the counter is authoritative, the wallet_ledger is
|
-- restored on a first-step refund). Because the counter is authoritative, the wallet_ledger is
|
||||||
-- no longer the source of truth for the grant and its old rows can be pruned after a retention
|
-- no longer the source of truth for the grant and its old rows can be pruned after a retention
|
||||||
-- window (separate future job).
|
-- window (separate future job).
|
||||||
--
|
|
||||||
-- See notes/payg-lifetime-free-grant-plan-2026-06-11.html.
|
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------------------------
|
||||||
-- 1. Rename the policy column — it is no longer "per cycle", it's the one-time grant size.
|
-- 1. Rename the policy column — it is no longer "per cycle", it's the one-time grant size.
|
||||||
|
|||||||
@@ -119,8 +119,11 @@ test.describe("1. Authentication and Login", () => {
|
|||||||
// Full page reload forces the SPA to re-check auth with the backend
|
// Full page reload forces the SPA to re-check auth with the backend
|
||||||
await page.reload({ waitUntil: "domcontentloaded" });
|
await page.reload({ waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
// Step 3: Verify the user is redirected to the login page
|
// Step 3: Verify the user is redirected to the login page. The redirect
|
||||||
await expect(page).toHaveURL(/\/login/, { timeout: 15000 });
|
// is driven client-side after the SPA re-bootstraps (session check +
|
||||||
|
// config fetch + backend probe), which can outrun a 15s budget on a
|
||||||
|
// loaded CI runner, so allow longer here.
|
||||||
|
await expect(page).toHaveURL(/\/login/, { timeout: 30000 });
|
||||||
|
|
||||||
// Step 5: Log in with valid credentials
|
// Step 5: Log in with valid credentials
|
||||||
await page.locator("#email").fill("admin");
|
await page.locator("#email").fill("admin");
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ describe("scoreMatch", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects unrelated words that share half their letters", () => {
|
it("rejects unrelated words that share half their letters", () => {
|
||||||
// Regression: the old fallback accepted 50% relative Levenshtein
|
// Unrelated words can share many letters (rotate vs update is edit
|
||||||
// similarity, so longer queries matched more junk (rotate vs update is
|
// distance 3 of 6) but must not be treated as a fuzzy match.
|
||||||
// edit distance 3 of 6)
|
|
||||||
expect(isFuzzyMatch("rotate", "update")).toBe(false);
|
expect(isFuzzyMatch("rotate", "update")).toBe(false);
|
||||||
expect(isFuzzyMatch("rotate", "create")).toBe(false);
|
expect(isFuzzyMatch("rotate", "create")).toBe(false);
|
||||||
expect(isFuzzyMatch("rotate", "private")).toBe(false);
|
expect(isFuzzyMatch("rotate", "private")).toBe(false);
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ function makeEntry(name: string, tags: string): ToolRegistryEntry {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real names and tags from the en-GB translations; these tools previously
|
// Real names and tags from the en-GB translations, chosen because they share
|
||||||
// polluted results for partial queries like "rotat" and "rotate"
|
// letters with "rotat"/"rotate" and stress the partial-query filtering.
|
||||||
const registry: Partial<ToolRegistry> = {
|
const registry: Partial<ToolRegistry> = {
|
||||||
rotate: makeEntry(
|
rotate: makeEntry(
|
||||||
"Rotate",
|
"Rotate",
|
||||||
@@ -72,9 +72,8 @@ describe("filterToolRegistryByQuery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("only returns Rotate for every prefix of 'rotate'", () => {
|
it("only returns Rotate for every prefix of 'rotate'", () => {
|
||||||
// Regression: "rotat" used to also pull in Redact, Annotate, Compare and
|
// Prefixes of "rotate" must not leak tools that merely share letters
|
||||||
// Adjust Colours/Contrast, and "rotate" additionally Scanner Effect,
|
// (Redact, Annotate, Compare, Adjust Colours/Contrast, etc.).
|
||||||
// Change Metadata, Add Password and Air-gapped Setup
|
|
||||||
expect(idsFor("rota")).toEqual(["rotate"]);
|
expect(idsFor("rota")).toEqual(["rotate"]);
|
||||||
expect(idsFor("rotat")).toEqual(["rotate"]);
|
expect(idsFor("rotat")).toEqual(["rotate"]);
|
||||||
expect(idsFor("rotate")).toEqual(["rotate"]);
|
expect(idsFor("rotate")).toEqual(["rotate"]);
|
||||||
|
|||||||
@@ -158,6 +158,6 @@ export const ListSection: Story = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Note: the setup + edit wizard now embeds the Watch Folders automation builder
|
// The setup + edit wizard embeds the Watch Folders automation builder (its
|
||||||
// (its Workflow step), which needs the ToolWorkflow context — so the wizard is
|
// Workflow step), which needs the ToolWorkflow context - so the wizard is
|
||||||
// exercised in-app, not via an isolated story here.
|
// exercised in-app, not via an isolated story here.
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ interface PolicyToolConfigStepProps {
|
|||||||
initialOperations: AutomationOperation[];
|
initialOperations: AutomationOperation[];
|
||||||
/**
|
/**
|
||||||
* The preset's default operations. Their params seed any tool whose saved
|
* The preset's default operations. Their params seed any tool whose saved
|
||||||
* value is still at the tool's own default — so e.g. a policy saved before the
|
* value is still at the tool's own default — so e.g. a policy saved with the
|
||||||
* PII patterns existed inherits them rather than running with an empty list.
|
* PII list at its default inherits the preset patterns rather than running empty.
|
||||||
*/
|
*/
|
||||||
presetOperations: AutomationOperation[];
|
presetOperations: AutomationOperation[];
|
||||||
/** Used to name the built pipeline definition. */
|
/** Used to name the built pipeline definition. */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* External store for real backend policy runs (Phase B: auto-run on upload).
|
* External store for real backend policy runs (auto-run on upload).
|
||||||
*
|
*
|
||||||
* The auto-run controller fires a backend run for each enabled policy × each
|
* The auto-run controller fires a backend run for each enabled policy × each
|
||||||
* newly-uploaded file and records it here; the detail view's activity feed reads
|
* newly-uploaded file and records it here; the detail view's activity feed reads
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* config page shows (one section per tool). Tools can be configured + toggled
|
* config page shows (one section per tool). Tools can be configured + toggled
|
||||||
* on/off, but not added or removed. Each id is a frontend tool-registry key (and
|
* on/off, but not added or removed. Each id is a frontend tool-registry key (and
|
||||||
* maps to that tool's backend endpoint via the registry's operationConfig).
|
* maps to that tool's backend endpoint via the registry's operationConfig).
|
||||||
*
|
|
||||||
* Only Security is wired today; other categories follow.
|
|
||||||
*/
|
*/
|
||||||
export const POLICY_TOOL_CHAINS: Record<string, string[]> = {
|
export const POLICY_TOOL_CHAINS: Record<string, string[]> = {
|
||||||
// Security: redact PII + watermark + sanitize (strips JS). Which are enabled
|
// Security: redact PII + watermark + sanitize (strips JS). Which are enabled
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-run controller (Phase B): every enabled policy enforces on every uploaded
|
* Auto-run controller: every enabled policy enforces on every uploaded
|
||||||
* file. Watches the session's files and, for each (active policy × not-yet-run
|
* file. Watches the session's files and, for each (active policy × not-yet-run
|
||||||
* file), fires a real backend run (`POST /api/v1/policies/{id}/run`) and polls it
|
* file), fires a real backend run (`POST /api/v1/policies/{id}/run`) and polls it
|
||||||
* to completion, recording progress in {@link policyRunStore} for the activity
|
* to completion, recording progress in {@link policyRunStore} for the activity
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ describe("policy definitions integrity", () => {
|
|||||||
it("every category has a matching config entry", () => {
|
it("every category has a matching config entry", () => {
|
||||||
for (const cat of POLICY_CATEGORIES) {
|
for (const cat of POLICY_CATEGORIES) {
|
||||||
expect(POLICY_CONFIG[cat.id], `config for ${cat.id}`).toBeDefined();
|
expect(POLICY_CONFIG[cat.id], `config for ${cat.id}`).toBeDefined();
|
||||||
// A category may have no policy-level setting fields (e.g. Security's were
|
// A category may have no policy-level setting fields; fields is required
|
||||||
// all unwired and removed); fields is required but can be empty.
|
// but can be empty.
|
||||||
expect(Array.isArray(POLICY_CONFIG[cat.id].fields)).toBe(true);
|
expect(Array.isArray(POLICY_CONFIG[cat.id].fields)).toBe(true);
|
||||||
expect(POLICY_CONFIG[cat.id].rules.length).toBeGreaterThan(0);
|
expect(POLICY_CONFIG[cat.id].rules.length).toBeGreaterThan(0);
|
||||||
// Every preset seeds a real, non-empty pipeline (the category→steps map).
|
// Every preset seeds a real, non-empty pipeline (the category→steps map).
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type {
|
|||||||
|
|
||||||
const ICON_SX = { fontSize: "1rem" } as const;
|
const ICON_SX = { fontSize: "1rem" } as const;
|
||||||
|
|
||||||
/** The 5 policy categories, in the prototype's narrative order. */
|
/** The 5 policy categories, in display order. */
|
||||||
export const POLICY_CATEGORIES: PolicyCategory[] = [
|
export const POLICY_CATEGORIES: PolicyCategory[] = [
|
||||||
{
|
{
|
||||||
id: "ingestion",
|
id: "ingestion",
|
||||||
@@ -69,7 +69,7 @@ export const POLICY_CATEGORIES: PolicyCategory[] = [
|
|||||||
* PII presets for the redact step: a label + the regex the /auto-redact endpoint
|
* PII presets for the redact step: a label + the regex the /auto-redact endpoint
|
||||||
* matches (via `wordsToRedact` + `useRegex`). Patterns are precise — validated
|
* matches (via `wordsToRedact` + `useRegex`). Patterns are precise — validated
|
||||||
* (SSN areas, card IINs, ABA prefixes), context- or separator-anchored — to keep
|
* (SSN areas, card IINs, ABA prefixes), context- or separator-anchored — to keep
|
||||||
* false positives down and avoid the backtracking the old broad patterns risked.
|
* false positives down and avoid catastrophic backtracking.
|
||||||
*/
|
*/
|
||||||
export const PII_PRESETS: { value: string; label: string; pattern: string }[] =
|
export const PII_PRESETS: { value: string; label: string; pattern: string }[] =
|
||||||
[
|
[
|
||||||
@@ -137,7 +137,7 @@ export const DEFAULT_PII_PATTERNS: string[] = [
|
|||||||
PII_PRESETS[1].pattern, // cards
|
PII_PRESETS[1].pattern, // cards
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Per-category narrative + editable fields (from the prototype's POLICY_CONFIG). */
|
/** Per-category narrative + editable fields. */
|
||||||
export const POLICY_CONFIG: Record<string, PolicyConfigDef> = {
|
export const POLICY_CONFIG: Record<string, PolicyConfigDef> = {
|
||||||
ingestion: {
|
ingestion: {
|
||||||
summary:
|
summary:
|
||||||
@@ -149,7 +149,7 @@ export const POLICY_CONFIG: Record<string, PolicyConfigDef> = {
|
|||||||
],
|
],
|
||||||
scopeLabel: "All PDFs on this device",
|
scopeLabel: "All PDFs on this device",
|
||||||
// Policy-level controls only — the per-tool params (OCR level, extract
|
// Policy-level controls only — the per-tool params (OCR level, extract
|
||||||
// tables, naming, normalize, rotate…) now live in the Workflow step.
|
// tables, naming, normalize, rotate...) live in the Workflow step.
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: "Min confidence",
|
label: "Min confidence",
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ function defaultState(): PolicyState {
|
|||||||
// Enforce on upload by default; export enforcement is the alternative.
|
// Enforce on upload by default; export enforcement is the alternative.
|
||||||
runOn: "upload",
|
runOn: "upload",
|
||||||
// Every catalog category is a shipped, built-in policy → default (not
|
// Every catalog category is a shipped, built-in policy → default (not
|
||||||
// deletable). User-created policies (later) will set this false.
|
// deletable).
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An obsolete reviewer email from earlier development, scrubbed from persisted
|
/** An obsolete reviewer email scrubbed from persisted state on read so it can
|
||||||
* state on read so it can re-default to the real signed-in user. */
|
* re-default to the real signed-in user. */
|
||||||
const STALE_REVIEWER_EMAIL = "[email protected]";
|
const STALE_REVIEWER_EMAIL = "[email protected]";
|
||||||
|
|
||||||
/** Read the full policy state, seeding + healing any missing categories. */
|
/** Read the full policy state, seeding + healing any missing categories. */
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
* A Policy is conceptually like a Watch Folder (a configured automation that
|
* A Policy is conceptually like a Watch Folder (a configured automation that
|
||||||
* runs over documents) but backend-driven and triggered by *sources/events*
|
* runs over documents) but backend-driven and triggered by *sources/events*
|
||||||
* (editor save/export, device sweeps, cloud connectors) rather than just a
|
* (editor save/export, device sweeps, cloud connectors) rather than just a
|
||||||
* folder. Per-policy state is persisted locally (localStorage) today; server
|
* folder. Per-policy state is persisted locally (localStorage). Activity + stats
|
||||||
* persistence and real enforcement land in a follow-up. Activity + stats shown
|
* shown in the detail view are derived live from the user's real uploaded files.
|
||||||
* in the detail view are derived live from the user's real uploaded files.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
* - the per-category breakdown comes from {@code categoryBreakdown}
|
* - the per-category breakdown comes from {@code categoryBreakdown}
|
||||||
* (wallet_category_summary view)
|
* (wallet_category_summary view)
|
||||||
* - money-equivalent display and the units↔money cap preview need
|
* - money-equivalent display and the units↔money cap preview need
|
||||||
* stripe.prices via Sync Engine (design §13 / PR-C2) and are deliberately
|
* stripe.prices via Sync Engine and are deliberately absent until that
|
||||||
* absent until that ships
|
* ships
|
||||||
* - the activity feed renders {@code wallet.recent}, which is {@code []} in
|
* - the activity feed renders {@code wallet.recent}, which is {@code []} in
|
||||||
* V1 — so it shows a real empty state, not fabricated rows
|
* V1 — so it shows a real empty state, not fabricated rows
|
||||||
*/
|
*/
|
||||||
@@ -437,10 +437,9 @@ const GATE_CAP_BEHAVIOR: { gate: Gate; staysAtCap: boolean }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folded-in "what happens at the cap" — previously its own GatesCard, now a
|
* Collapsed "what happens at the cap" disclosure rendered inside the cap
|
||||||
* collapsed disclosure rendered inside the cap card(s) to save vertical space.
|
* card(s) to save vertical space. Mirrors the {@link DocHelp} toggle pattern;
|
||||||
* Mirrors the {@link DocHelp} toggle pattern; default-collapsed so it costs no
|
* default-collapsed so it costs no height until the user opens it.
|
||||||
* height until the user opens it.
|
|
||||||
*/
|
*/
|
||||||
function CapReachedHelp() {
|
function CapReachedHelp() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -488,7 +487,7 @@ function CapReachedHelp() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Leader-only roster of each teammate's billable usage this period. Display-only — per-member
|
* Leader-only roster of each teammate's billable usage this period. Display-only — per-member
|
||||||
* sub-cap enforcement isn't shipped (see the follow-ups note), so there's no cap control here.
|
* sub-cap enforcement isn't shipped, so there's no cap control here.
|
||||||
*/
|
*/
|
||||||
function MemberUsage({ members }: { members: Wallet["members"] }) {
|
function MemberUsage({ members }: { members: Wallet["members"] }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
+7
-7
@@ -29,12 +29,12 @@
|
|||||||
*
|
*
|
||||||
* Stripe-touching code lives in Supabase edge functions, not the Java
|
* Stripe-touching code lives in Supabase edge functions, not the Java
|
||||||
* backend. This panel invokes {@code create-checkout-session}
|
* backend. This panel invokes {@code create-checkout-session}
|
||||||
* directly via {@code supabase.functions.invoke()} — same pattern {@link
|
* directly via {@code supabase.functions.invoke()} - same pattern {@link
|
||||||
* usePlans} already uses for {@code stripe-price-lookup}. The auth JWT is
|
* usePlans} already uses for {@code stripe-price-lookup}. The auth JWT is
|
||||||
* attached automatically by the Supabase client.
|
* attached automatically by the Supabase client.
|
||||||
*
|
*
|
||||||
* <p>The edge function (SaaS PR #300) is the canonical place Stripe Checkout
|
* <p>The edge function is the canonical place Stripe Checkout
|
||||||
* Sessions get created — it uses the Stripe Sync Engine tables, has dedicated
|
* Sessions get created - it uses the Stripe Sync Engine tables, has dedicated
|
||||||
* unit tests, and shares Stripe SDK / secret-key plumbing with the metering +
|
* unit tests, and shares Stripe SDK / secret-key plumbing with the metering +
|
||||||
* webhook edge functions. Routing through Java would have meant a useless
|
* webhook edge functions. Routing through Java would have meant a useless
|
||||||
* proxy hop + a second Stripe SDK to maintain.
|
* proxy hop + a second Stripe SDK to maintain.
|
||||||
@@ -170,10 +170,10 @@ const StripeCheckoutPanel: React.FC<StripeCheckoutPanelProps> = ({
|
|||||||
// React 18 strict-mode dev mounts effects twice. We use `cancelled` to
|
// React 18 strict-mode dev mounts effects twice. We use `cancelled` to
|
||||||
// discard the first mount's response (its setState calls become no-ops),
|
// discard the first mount's response (its setState calls become no-ops),
|
||||||
// and the second mount's response wins. We deliberately do NOT short-
|
// and the second mount's response wins. We deliberately do NOT short-
|
||||||
// circuit the second mount with a ref — earlier versions did that and
|
// circuit the second mount with a ref: that traps mount 1 as cancelled
|
||||||
// ran into the trap where mount 1 was cancelled but the live mount
|
// while the live mount never re-fetches, leaving `loading` stuck at true.
|
||||||
// never re-fetched, leaving `loading` stuck at true. Two network calls
|
// Two network calls in dev is an acceptable cost; prod has no strict mode
|
||||||
// in dev is an acceptable cost; prod has no strict mode → single fetch.
|
// -> single fetch.
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
async function createSession() {
|
async function createSession() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user