feat(core-consent): scaffold package with types, IConsent, withConsent brand wrapper
- Add packages/core-consent with ConsentCategory, ConsentState, UserConsentState types and IConsent interface - Add withConsent wrapper attaching __consentChecked brand at bind time; unit tests assert brand attachment and factory passthrough - Add ConsentChecked<F> type to core-shared/conformance/brands.ts and isConsentChecked helper to brand-runtime.ts - Extend FeatureManifest with requiresConsent?: readonly string[] field - Extend assertFeatureConformance to require __consentChecked brand when requiresConsent.length > 0; synthetic fixture tests cover pass/fail cases - Propagate __consentChecked in withSpan PROPAGATED_BRANDS so the outermost binding carries the brand Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-19T10:16:15.209Z",
|
"generatedAt": "2026-05-19T10:39:50.009Z",
|
||||||
"commit": "8cf9f4b",
|
"commit": "f5d08dc",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 96.48,
|
"statements": 96.5,
|
||||||
"branches": 91.72,
|
"branches": 91.82,
|
||||||
"functions": 96.83,
|
"functions": 96.89,
|
||||||
"lines": 96.48,
|
"lines": 96.5,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 4264,
|
"lf": 4291,
|
||||||
"lh": 4114,
|
"lh": 4141,
|
||||||
"brf": 809,
|
"brf": 819,
|
||||||
"brh": 742,
|
"brh": 752,
|
||||||
"fnf": 252,
|
"fnf": 257,
|
||||||
"fnh": 244
|
"fnh": 249
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"byPackage": {
|
"byPackage": {
|
||||||
@@ -58,18 +58,32 @@
|
|||||||
"fnh": 10
|
"fnh": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/core-shared": {
|
"@repo/core-consent": {
|
||||||
"statements": 98,
|
"statements": 100,
|
||||||
"branches": 95.81,
|
"branches": 100,
|
||||||
"functions": 92,
|
"functions": 100,
|
||||||
"lines": 98,
|
"lines": 100,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 1050,
|
"lf": 10,
|
||||||
"lh": 1029,
|
"lh": 10,
|
||||||
"brf": 310,
|
"brf": 4,
|
||||||
"brh": 297,
|
"brh": 4,
|
||||||
"fnf": 100,
|
"fnf": 4,
|
||||||
"fnh": 92
|
"fnh": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@repo/core-shared": {
|
||||||
|
"statements": 98.03,
|
||||||
|
"branches": 95.89,
|
||||||
|
"functions": 92.08,
|
||||||
|
"lines": 98.03,
|
||||||
|
"counts": {
|
||||||
|
"lf": 1067,
|
||||||
|
"lh": 1046,
|
||||||
|
"brf": 316,
|
||||||
|
"brh": 303,
|
||||||
|
"fnf": 101,
|
||||||
|
"fnh": 93
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/marketing-pages": {
|
"@repo/marketing-pages": {
|
||||||
|
|||||||
29
packages/core-consent/AGENTS.md
Normal file
29
packages/core-consent/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# @repo/core-consent
|
||||||
|
|
||||||
|
Optional core package providing a vendor-neutral consent management interface. Scaffold via `pnpm turbo gen core-package consent` (once the generator supports it).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
consent-types.ts # ConsentCategory, ConsentState, UserConsentState
|
||||||
|
consent.interface.ts # IConsent — isGranted, grant, withdraw, getCategories
|
||||||
|
with-consent.ts # withConsent wrapper attaching ConsentChecked brand
|
||||||
|
index.ts # Barrel export
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
`IConsent` exposes four methods:
|
||||||
|
|
||||||
|
- `isGranted(category)` — synchronous check whether consent is granted
|
||||||
|
- `grant(category)` — record consent grant for a category
|
||||||
|
- `withdraw(category)` — record consent withdrawal for a category
|
||||||
|
- `getCategories()` — list all known consent states
|
||||||
|
|
||||||
|
The interface is vendor-neutral: no storage implementation is bundled here. Concrete implementations (e.g. a Payload-backed store) are wired at DI bind time in `bind-production` (Story 04).
|
||||||
|
|
||||||
|
`withConsent` wraps a use-case factory at bind time, attaches the `__consentChecked` brand, and is the innermost wrapper in the composition chain:
|
||||||
|
`withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)`
|
||||||
|
|
||||||
|
See `docs/architecture/agent-first-workflow-and-conformance.md` for the dependency-injection conventions.
|
||||||
3
packages/core-consent/eslint.config.js
Normal file
3
packages/core-consent/eslint.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import baseConfig from "@repo/core-eslint/base";
|
||||||
|
|
||||||
|
export default baseConfig;
|
||||||
26
packages/core-consent/package.json
Normal file
26
packages/core-consent/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/core-consent",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run --passWithNoTests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/core-shared": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/core-eslint": "workspace:*",
|
||||||
|
"@repo/core-testing": "workspace:*",
|
||||||
|
"@repo/core-typescript": "workspace:*",
|
||||||
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
|
"typescript": "^5.8.0",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/core-consent/src/consent-types.ts
Normal file
22
packages/core-consent/src/consent-types.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Known consent categories. The `(string & {})` escape hatch keeps the union
|
||||||
|
* open for custom categories while still providing autocomplete for the
|
||||||
|
* standard values.
|
||||||
|
*/
|
||||||
|
export type ConsentCategory =
|
||||||
|
| "necessary"
|
||||||
|
| "functional"
|
||||||
|
| "analytics"
|
||||||
|
| "marketing"
|
||||||
|
| (string & {});
|
||||||
|
|
||||||
|
/** Whether a subject has granted or denied consent for a category. */
|
||||||
|
export type ConsentState = "granted" | "denied" | "pending";
|
||||||
|
|
||||||
|
/** Per-category consent record for a single subject. */
|
||||||
|
export type UserConsentState = {
|
||||||
|
readonly category: ConsentCategory;
|
||||||
|
readonly state: ConsentState;
|
||||||
|
readonly grantedAt?: Date;
|
||||||
|
readonly withdrawnAt?: Date;
|
||||||
|
};
|
||||||
22
packages/core-consent/src/consent.interface.ts
Normal file
22
packages/core-consent/src/consent.interface.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ConsentCategory, UserConsentState } from "./consent-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor-neutral consent management interface.
|
||||||
|
*
|
||||||
|
* Feature binders that receive a consent instance operate through this
|
||||||
|
* interface. Concrete implementations (Payload-backed, in-memory, etc.) are
|
||||||
|
* wired at DI bind time and never imported by feature packages directly.
|
||||||
|
*/
|
||||||
|
export interface IConsent {
|
||||||
|
/** Synchronous check — true when the subject has granted the category. */
|
||||||
|
isGranted(category: ConsentCategory): boolean;
|
||||||
|
|
||||||
|
/** Record a consent grant for the given category. */
|
||||||
|
grant(category: ConsentCategory): Promise<void>;
|
||||||
|
|
||||||
|
/** Record a consent withdrawal for the given category. */
|
||||||
|
withdraw(category: ConsentCategory): Promise<void>;
|
||||||
|
|
||||||
|
/** Return the full list of per-category consent states. */
|
||||||
|
getCategories(): UserConsentState[];
|
||||||
|
}
|
||||||
8
packages/core-consent/src/index.ts
Normal file
8
packages/core-consent/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type {
|
||||||
|
ConsentCategory,
|
||||||
|
ConsentState,
|
||||||
|
UserConsentState,
|
||||||
|
} from "./consent-types";
|
||||||
|
export type { IConsent } from "./consent.interface";
|
||||||
|
export type { ConsentChecked } from "./with-consent";
|
||||||
|
export { withConsent } from "./with-consent";
|
||||||
55
packages/core-consent/src/with-consent.test.ts
Normal file
55
packages/core-consent/src/with-consent.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, it, expect, expectTypeOf } from "vitest";
|
||||||
|
import { withConsent, type ConsentChecked } from "@/with-consent";
|
||||||
|
import type { IConsent } from "@/consent.interface";
|
||||||
|
import { isConsentChecked } from "@repo/core-shared/conformance";
|
||||||
|
|
||||||
|
function makeConsent(): IConsent {
|
||||||
|
return {
|
||||||
|
isGranted: () => true,
|
||||||
|
grant: () => Promise.resolve(),
|
||||||
|
withdraw: () => Promise.resolve(),
|
||||||
|
getCategories: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("withConsent", () => {
|
||||||
|
it("returns a ConsentChecked<F>", () => {
|
||||||
|
const consent = makeConsent();
|
||||||
|
const fn = async (_input: { id: string }) => ({ ok: true });
|
||||||
|
const wrapped = withConsent(consent, fn);
|
||||||
|
expectTypeOf(wrapped).toMatchTypeOf<ConsentChecked<typeof fn>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attaches __consentChecked as a non-enumerable property on the wrapped function", () => {
|
||||||
|
const consent = makeConsent();
|
||||||
|
const fn = async () => ({ ok: true });
|
||||||
|
const wrapped = withConsent(consent, fn);
|
||||||
|
expect(isConsentChecked(wrapped)).toBe(true);
|
||||||
|
expect(Object.keys(wrapped)).not.toContain("__consentChecked");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT pollute the original input function with the brand", () => {
|
||||||
|
const consent = makeConsent();
|
||||||
|
const fn = async () => ({ ok: true });
|
||||||
|
const wrapped = withConsent(consent, fn);
|
||||||
|
expect(isConsentChecked(fn)).toBe(false);
|
||||||
|
expect(wrapped).not.toBe(fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes input and output through unchanged", async () => {
|
||||||
|
const consent = makeConsent();
|
||||||
|
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
||||||
|
const wrapped = withConsent(consent, fn);
|
||||||
|
const result = await wrapped({ id: "abc" });
|
||||||
|
expect(result).toEqual({ ok: true, id: "abc" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates errors", async () => {
|
||||||
|
const consent = makeConsent();
|
||||||
|
const err = new Error("boom");
|
||||||
|
const wrapped = withConsent(consent, async () => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
await expect(wrapped()).rejects.toBe(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
packages/core-consent/src/with-consent.ts
Normal file
30
packages/core-consent/src/with-consent.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { IConsent } from "./consent.interface";
|
||||||
|
import type { ConsentChecked } from "@repo/core-shared/conformance";
|
||||||
|
import { attachBrand } from "@repo/core-shared/conformance";
|
||||||
|
|
||||||
|
export type { ConsentChecked };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use-case wrapper applied at DI bind time. The wrapper is a thin closure
|
||||||
|
* that forwards to `fn` unchanged and carries the `__consentChecked` brand.
|
||||||
|
* The forward closure keeps the brand on a fresh function so the original
|
||||||
|
* `fn` reference is not mutated — important when the same factory output is
|
||||||
|
* used elsewhere unwrapped (dev-seed paths, tests).
|
||||||
|
*
|
||||||
|
* Composition order (innermost to outermost):
|
||||||
|
* withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)
|
||||||
|
*
|
||||||
|
* The wrapper exists to:
|
||||||
|
* (1) require callers to pass the consent instance at bind time (dep is available)
|
||||||
|
* (2) attach the `__consentChecked` brand so the boot-time assertion can verify
|
||||||
|
* consent-gated use cases were bound through the consent-aware path.
|
||||||
|
*/
|
||||||
|
export function withConsent<Args extends unknown[], R>(
|
||||||
|
consent: IConsent,
|
||||||
|
fn: (...args: Args) => Promise<R>,
|
||||||
|
): ConsentChecked<(...args: Args) => Promise<R>> {
|
||||||
|
void consent;
|
||||||
|
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
||||||
|
attachBrand(wrapped, "__consentChecked");
|
||||||
|
return wrapped as ConsentChecked<(...args: Args) => Promise<R>>;
|
||||||
|
}
|
||||||
12
packages/core-consent/tsconfig.json
Normal file
12
packages/core-consent/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/core-typescript/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
4
packages/core-consent/turbo.json
Normal file
4
packages/core-consent/turbo.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": ["//"],
|
||||||
|
"tags": ["core"]
|
||||||
|
}
|
||||||
15
packages/core-consent/vitest.config.ts
Normal file
15
packages/core-consent/vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
|
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
nodeVitestConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: { "@": path.resolve(__dirname, "./src") },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -285,6 +285,82 @@ describe("assertFeatureConformance", () => {
|
|||||||
).toThrow(/Analyzed|__analyzed/);
|
).toThrow(/Analyzed|__analyzed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws when feature requiresConsent but binding is missing __consentChecked brand", () => {
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.processData");
|
||||||
|
const ctx = makeCtx();
|
||||||
|
const wrappedNoConsent = withSpan(
|
||||||
|
ctx.tracer,
|
||||||
|
{ name: "test.processData", op: "use-case" },
|
||||||
|
withCapture(
|
||||||
|
ctx.logger,
|
||||||
|
{ feature: "test", layer: "use-case" },
|
||||||
|
async (x: number) => x,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
container.bind(sym).toConstantValue(wrappedNoConsent);
|
||||||
|
|
||||||
|
const manifest = defineFeature({
|
||||||
|
name: "test",
|
||||||
|
requiredCores: [],
|
||||||
|
useCases: {
|
||||||
|
processData: {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realtimeChannels: [],
|
||||||
|
jobs: [],
|
||||||
|
requiresConsent: ["analytics"],
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { processData: sym }, ctx),
|
||||||
|
).toThrow(ConformanceError);
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { processData: sym }, ctx),
|
||||||
|
).toThrow(/__consentChecked/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes when feature requiresConsent and binding carries __consentChecked brand", () => {
|
||||||
|
const container = new Container();
|
||||||
|
const sym = Symbol("test.processData");
|
||||||
|
const ctx = makeCtx();
|
||||||
|
const base = withSpan(
|
||||||
|
ctx.tracer,
|
||||||
|
{ name: "test.processData", op: "use-case" },
|
||||||
|
withCapture(
|
||||||
|
ctx.logger,
|
||||||
|
{ feature: "test", layer: "use-case" },
|
||||||
|
async (x: number) => x,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
attachBrand(base, "__consentChecked");
|
||||||
|
container.bind(sym).toConstantValue(base);
|
||||||
|
|
||||||
|
const manifest = defineFeature({
|
||||||
|
name: "test",
|
||||||
|
requiredCores: [],
|
||||||
|
useCases: {
|
||||||
|
processData: {
|
||||||
|
mutates: false,
|
||||||
|
audits: [],
|
||||||
|
publishes: [],
|
||||||
|
consumes: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
realtimeChannels: [],
|
||||||
|
jobs: [],
|
||||||
|
requiresConsent: ["analytics"],
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
assertFeatureConformance(container, manifest, { processData: sym }, ctx),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("passes when analyticsEvents is empty and __analyzed brand is absent", () => {
|
it("passes when analyticsEvents is empty and __analyzed brand is absent", () => {
|
||||||
const container = new Container();
|
const container = new Container();
|
||||||
const sym = Symbol("test.signIn");
|
const sym = Symbol("test.signIn");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isCaptured,
|
isCaptured,
|
||||||
isAudited,
|
isAudited,
|
||||||
isAnalyzed,
|
isAnalyzed,
|
||||||
|
isConsentChecked,
|
||||||
} from "./brand-runtime";
|
} from "./brand-runtime";
|
||||||
import { ConformanceError } from "./conformance-error";
|
import { ConformanceError } from "./conformance-error";
|
||||||
import type { BindContext } from "../di/bind-context";
|
import type { BindContext } from "../di/bind-context";
|
||||||
@@ -70,5 +71,12 @@ export function assertFeatureConformance(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ((manifest.requiresConsent?.length ?? 0) > 0) {
|
||||||
|
if (!isConsentChecked(bound)) {
|
||||||
|
throw new ConformanceError(
|
||||||
|
`${manifest.name}.${name}: feature declares requiresConsent but binding is missing __consentChecked brand — was withConsent applied at bind time?`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,14 @@
|
|||||||
* commitment, not a mutable flag.
|
* commitment, not a mutable flag.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Analyzed } from "./brands";
|
import type { Analyzed, ConsentChecked } from "./brands";
|
||||||
|
|
||||||
type Brand = "__instrumented" | "__captured" | "__audited" | "__analyzed";
|
type Brand =
|
||||||
|
| "__instrumented"
|
||||||
|
| "__captured"
|
||||||
|
| "__audited"
|
||||||
|
| "__analyzed"
|
||||||
|
| "__consentChecked";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches the brand as a non-enumerable property on the given function.
|
* Attaches the brand as a non-enumerable property on the given function.
|
||||||
@@ -57,3 +62,9 @@ export function isAudited(fn: unknown): boolean {
|
|||||||
export function isAnalyzed<F extends object>(fn: unknown): fn is Analyzed<F> {
|
export function isAnalyzed<F extends object>(fn: unknown): fn is Analyzed<F> {
|
||||||
return hasBrand(fn, "__analyzed");
|
return hasBrand(fn, "__analyzed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isConsentChecked<F extends object>(
|
||||||
|
fn: unknown,
|
||||||
|
): fn is ConsentChecked<F> {
|
||||||
|
return hasBrand(fn, "__consentChecked");
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,3 +10,4 @@
|
|||||||
export type Instrumented<F> = F & { readonly __instrumented: true };
|
export type Instrumented<F> = F & { readonly __instrumented: true };
|
||||||
export type Captured<F> = F & { readonly __captured: true };
|
export type Captured<F> = F & { readonly __captured: true };
|
||||||
export type Analyzed<F> = F & { readonly __analyzed: true };
|
export type Analyzed<F> = F & { readonly __analyzed: true };
|
||||||
|
export type ConsentChecked<F> = F & { readonly __consentChecked: true };
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ export type FeatureManifest = {
|
|||||||
readonly realtimeChannels: readonly string[];
|
readonly realtimeChannels: readonly string[];
|
||||||
readonly jobs: readonly string[];
|
readonly jobs: readonly string[];
|
||||||
readonly coverage?: CoverageManifest;
|
readonly coverage?: CoverageManifest;
|
||||||
|
/**
|
||||||
|
* Consent categories this feature's use cases require before processing
|
||||||
|
* personal data. When non-empty, `assertFeatureConformance` requires every
|
||||||
|
* bound use case to carry the `__consentChecked` brand from `withConsent`.
|
||||||
|
* Defaults to `[]` — existing features with no consent requirements omit
|
||||||
|
* or declare an empty array.
|
||||||
|
*/
|
||||||
|
readonly requiresConsent?: readonly string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export type { Instrumented, Captured, Analyzed } from "./brands";
|
export type {
|
||||||
|
Instrumented,
|
||||||
|
Captured,
|
||||||
|
Analyzed,
|
||||||
|
ConsentChecked,
|
||||||
|
} from "./brands";
|
||||||
export type { FeatureManifest, UseCaseManifest } from "./define-feature";
|
export type { FeatureManifest, UseCaseManifest } from "./define-feature";
|
||||||
export { defineFeature } from "./define-feature";
|
export { defineFeature } from "./define-feature";
|
||||||
export type {
|
export type {
|
||||||
@@ -23,6 +28,7 @@ export {
|
|||||||
isCaptured,
|
isCaptured,
|
||||||
isAudited,
|
isAudited,
|
||||||
isAnalyzed,
|
isAnalyzed,
|
||||||
|
isConsentChecked,
|
||||||
} from "./brand-runtime";
|
} from "./brand-runtime";
|
||||||
export { ConformanceError } from "./conformance-error";
|
export { ConformanceError } from "./conformance-error";
|
||||||
export { assertFeatureConformance } from "./assert-bindings";
|
export { assertFeatureConformance } from "./assert-bindings";
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import type { ITracer, SpanOpts } from "./tracer.interface";
|
|||||||
import type { Instrumented } from "../conformance/brands";
|
import type { Instrumented } from "../conformance/brands";
|
||||||
import { attachBrand } from "../conformance/brand-runtime";
|
import { attachBrand } from "../conformance/brand-runtime";
|
||||||
|
|
||||||
const PROPAGATED_BRANDS = ["__captured", "__audited", "__analyzed"] as const;
|
const PROPAGATED_BRANDS = [
|
||||||
|
"__captured",
|
||||||
|
"__audited",
|
||||||
|
"__analyzed",
|
||||||
|
"__consentChecked",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
export function withSpan<Args extends unknown[], R, Extra extends object>(
|
||||||
tracer: ITracer,
|
tracer: ITracer,
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -25,6 +25,9 @@ importers:
|
|||||||
"@types/node":
|
"@types/node":
|
||||||
specifier: ^22.0.0
|
specifier: ^22.0.0
|
||||||
version: 22.19.17
|
version: 22.19.17
|
||||||
|
"@typescript-eslint/parser":
|
||||||
|
specifier: ^8.25.0
|
||||||
|
version: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||||
fallow:
|
fallow:
|
||||||
specifier: ^2.73.0
|
specifier: ^2.73.0
|
||||||
version: 2.73.0
|
version: 2.73.0
|
||||||
@@ -584,6 +587,31 @@ importers:
|
|||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)
|
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
|
|
||||||
|
packages/core-consent:
|
||||||
|
dependencies:
|
||||||
|
"@repo/core-shared":
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core-shared
|
||||||
|
devDependencies:
|
||||||
|
"@repo/core-eslint":
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core-eslint
|
||||||
|
"@repo/core-testing":
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core-testing
|
||||||
|
"@repo/core-typescript":
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core-typescript
|
||||||
|
"@vitest/coverage-v8":
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.8.0
|
||||||
|
version: 5.9.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)
|
||||||
|
|
||||||
packages/core-eslint:
|
packages/core-eslint:
|
||||||
dependencies:
|
dependencies:
|
||||||
globals:
|
globals:
|
||||||
|
|||||||
Reference in New Issue
Block a user