@fabricorg/ports 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +166 -0
- package/README.md +17 -1
- package/dist/catalog.cjs +457 -0
- package/dist/catalog.cjs.map +1 -0
- package/dist/catalog.d.cts +53 -0
- package/dist/catalog.d.ts +53 -0
- package/dist/catalog.js +77 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunk-FH6OL7JX.js +657 -0
- package/dist/chunk-FH6OL7JX.js.map +1 -0
- package/dist/index.cjs +480 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +309 -2
- package/dist/index.d.ts +309 -2
- package/dist/index.js +32 -187
- package/dist/index.js.map +1 -1
- package/package.json +13 -2
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["/**\n * Port interfaces are declared here and implemented by vendor adapters elsewhere.\n * Nothing in this package imports a vendor SDK: a port is a shape, and an adapter\n * is anything that satisfies it. Where an open standard exists the port speaks it,\n * so the incumbent vendor is already just one provider behind the interface.\n */\n\n// ── Feature flags (OpenFeature-shaped) ──────────────────────────────────────\n\nexport type FlagValue = boolean | string | number | { [key: string]: unknown };\n\nexport interface EvaluationContext {\n\t/** Stable identifier for the subject of evaluation, usually a tenant or actor. */\n\ttargetingKey?: string;\n\t[attribute: string]: unknown;\n}\n\nexport type ResolutionReason = \"STATIC\" | \"DEFAULT\" | \"TARGETING_MATCH\" | \"SPLIT\" | \"CACHED\" | \"ERROR\";\n\nexport interface ResolutionDetails<T extends FlagValue> {\n\tvalue: T;\n\treason: ResolutionReason;\n\tvariant?: string;\n\terrorCode?: string;\n}\n\n/**\n * Structurally compatible with an OpenFeature provider's evaluation surface, so an\n * OpenFeature provider is a thin adapter rather than a translation layer.\n */\nexport interface FlagsPort {\n\tresolveBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): Promise<ResolutionDetails<boolean>>;\n\tresolveString(flagKey: string, defaultValue: string, context?: EvaluationContext): Promise<ResolutionDetails<string>>;\n\tresolveNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): Promise<ResolutionDetails<number>>;\n}\n\n// ── Design tokens (W3C DTCG) ────────────────────────────────────────────────\n\nexport interface DtcgToken {\n\t$value: unknown;\n\t$type?: string;\n\t$description?: string;\n}\n\nexport interface DtcgGroup {\n\t$type?: string;\n\t$description?: string;\n\t[member: string]: DtcgToken | DtcgGroup | string | undefined;\n}\n\nexport interface ResolvedToken {\n\tname: string;\n\tvalue: unknown;\n\ttype?: string;\n\tdescription?: string;\n}\n\nexport interface DesignTokensPort {\n\t/** Resolved tokens for one theme. Aliases are already followed. */\n\tresolve(theme: string): Promise<ResolvedToken[]>;\n}\n\nconst ALIAS = /^\\{([^}]+)\\}$/;\n\nconst isToken = (value: unknown): value is DtcgToken =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && \"$value\" in value;\n\nconst isGroup = (value: unknown): value is DtcgGroup =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && !(\"$value\" in value);\n\n/**\n * Flattens a DTCG document and follows aliases. `$type` is inherited from the\n * nearest ancestor group that declares one, which is what the format specifies\n * and what makes a theme file readable.\n */\nexport function resolveDesignTokens(document: DtcgGroup): ResolvedToken[] {\n\tconst flat = new Map<string, { token: DtcgToken; type?: string }>();\n\n\tconst walk = (node: DtcgGroup, path: string[], inheritedType?: string): void => {\n\t\tconst groupType = typeof node.$type === \"string\" ? node.$type : inheritedType;\n\t\tfor (const [key, member] of Object.entries(node)) {\n\t\t\tif (key.startsWith(\"$\")) continue;\n\t\t\tconst next = [...path, key];\n\t\t\tif (isToken(member)) {\n\t\t\t\tflat.set(next.join(\".\"), { token: member, type: member.$type ?? groupType });\n\t\t\t} else if (isGroup(member)) {\n\t\t\t\twalk(member, next, groupType);\n\t\t\t}\n\t\t}\n\t};\n\twalk(document, []);\n\n\tconst resolving = new Set<string>();\n\tconst resolved = new Map<string, unknown>();\n\n\tconst valueOf = (name: string): unknown => {\n\t\tif (resolved.has(name)) return resolved.get(name);\n\t\tconst entry = flat.get(name);\n\t\tif (!entry) throw new Error(`Design token alias \"{${name}}\" does not resolve to a declared token.`);\n\t\tif (resolving.has(name)) {\n\t\t\tthrow new Error(`Design token alias cycle: ${[...resolving, name].join(\" -> \")}.`);\n\t\t}\n\t\tconst raw = entry.token.$value;\n\t\tif (typeof raw !== \"string\") {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tconst alias = ALIAS.exec(raw.trim());\n\t\tif (!alias) {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tresolving.add(name);\n\t\tconst target = valueOf(alias[1]!);\n\t\tresolving.delete(name);\n\t\tresolved.set(name, target);\n\t\treturn target;\n\t};\n\n\treturn [...flat.entries()].map(([name, entry]) => {\n\t\tconst value = valueOf(name);\n\t\treturn {\n\t\t\tname,\n\t\t\tvalue,\n\t\t\t...(entry.type === undefined ? {} : { type: entry.type }),\n\t\t\t...(entry.token.$description === undefined ? {} : { description: entry.token.$description }),\n\t\t};\n\t});\n}\n\n// ── Requirement satisfaction ────────────────────────────────────────────────\n\nexport interface PortRequirement {\n\tname: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\n/** The subset of capability metadata that declares what ports it needs. */\nexport interface PortRequiringCapability {\n\tnamespace: string;\n\trequirements?: { ports?: PortRequirement[] };\n}\n\nexport interface RegisteredAdapter {\n\t/** Port this adapter implements, matching the requirement's `name`. */\n\tport: string;\n\tvendor: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\nexport type PortFindingCode = \"unsatisfied_port\" | \"standard_not_spoken\" | \"version_mismatch\";\n\nexport interface PortFinding {\n\tcode: PortFindingCode;\n\tport: string;\n\tmessage: string;\n}\n\nexport interface PortValidationResult {\n\tvalid: boolean;\n\tfindings: PortFinding[];\n}\n\n/**\n * Checks that every port a capability declares is backed by a registered adapter.\n * `CapabilityPortRequirement` is otherwise a name that resolves against nothing;\n * this is what turns the declaration into a deployment-time gate.\n *\n * Version matching is exact. Range negotiation belongs to whatever installs the\n * adapters, and pretending to do semver here would be worse than not doing it.\n */\nexport function validatePortRequirements(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n}): PortValidationResult {\n\tconst findings: PortFinding[] = [];\n\tfor (const requirement of input.capability.requirements?.ports ?? []) {\n\t\tconst candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);\n\t\tif (candidates.length === 0) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"unsatisfied_port\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `capability \"${input.capability.namespace}\" requires port \"${requirement.name}\" and no adapter is registered for it`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (requirement.standard) {\n\t\t\tconst speaking = candidates.filter((adapter) => adapter.standard?.name === requirement.standard!.name);\n\t\t\tif (speaking.length === 0) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tcode: \"standard_not_spoken\",\n\t\t\t\t\tport: requirement.name,\n\t\t\t\t\tmessage: `port \"${requirement.name}\" must speak \"${requirement.standard.name}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? \"no standard\"})`).join(\", \")}`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (requirement.version && !candidates.some((adapter) => adapter.version === requirement.version)) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"version_mismatch\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `port \"${requirement.name}\" requires version \"${requirement.version}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}@${adapter.version ?? \"unversioned\"}`).join(\", \")}`,\n\t\t\t});\n\t\t}\n\t}\n\treturn { valid: findings.length === 0, findings };\n}\n\nexport function assertPortRequirementsSatisfied(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n}): void {\n\tconst result = validatePortRequirements(input);\n\tif (result.valid) return;\n\tthrow new Error([\n\t\t`Port requirements for \"${input.capability.namespace}\" are not satisfied:`,\n\t\t...result.findings.map((finding) => ` - ${finding.message}`),\n\t].join(\"\\n\"));\n}\n\n// ── Adapter contract kit ────────────────────────────────────────────────────\n\nexport interface PortCheck<TPort> {\n\tid: string;\n\ttitle: string;\n\t/** Throws on failure, exactly as an assertion would. */\n\trun(port: TPort): Promise<void>;\n}\n\nclass PortContractFailure extends Error {\n\toverride readonly name = \"PortContractFailure\";\n}\n\nfunction expect(condition: unknown, message: string): asserts condition {\n\tif (!condition) throw new PortContractFailure(message);\n}\n\n/**\n * Every flags adapter must pass these, so swapping vendors is an adapter build\n * plus a configuration change rather than a migration.\n */\nexport function flagsPortChecks(): PortCheck<FlagsPort>[] {\n\tconst unknownKey = \"fabric.contract.definitely-not-configured\";\n\treturn [\n\t\t{\n\t\t\tid: \"flags.default-on-unknown-key\",\n\t\t\ttitle: \"an unknown flag resolves to the supplied default rather than throwing\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.resolveBoolean(unknownKey, true);\n\t\t\t\texpect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);\n\t\t\t\texpect(\n\t\t\t\t\tresult.reason === \"DEFAULT\" || result.reason === \"ERROR\",\n\t\t\t\t\t`an unknown flag resolved with reason \"${result.reason}\"; a default or error was expected`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.default-is-typed\",\n\t\t\ttitle: \"each typed resolver returns its own type\",\n\t\t\tasync run(port) {\n\t\t\t\texpect(typeof (await port.resolveBoolean(unknownKey, false)).value === \"boolean\", \"resolveBoolean did not return a boolean\");\n\t\t\t\texpect(typeof (await port.resolveString(unknownKey, \"fallback\")).value === \"string\", \"resolveString did not return a string\");\n\t\t\t\texpect(typeof (await port.resolveNumber(unknownKey, 42)).value === \"number\", \"resolveNumber did not return a number\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.evaluation-is-pure\",\n\t\t\ttitle: \"the same key and context resolve the same way twice\",\n\t\t\tasync run(port) {\n\t\t\t\tconst context = { targetingKey: \"fabric-contract-subject\" };\n\t\t\t\tconst first = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\tconst second = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\texpect(first.value === second.value, `the same evaluation returned \"${first.value}\" then \"${second.value}\"`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.tolerates-absent-context\",\n\t\t\ttitle: \"evaluation without a context does not throw\",\n\t\t\tasync run(port) {\n\t\t\t\tawait port.resolveBoolean(unknownKey, false);\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"tokens.theme-resolves\",\n\t\t\ttitle: \"a known theme resolves to at least one token\",\n\t\t\tasync run(port) {\n\t\t\t\tconst tokens = await port.resolve(theme);\n\t\t\t\texpect(Array.isArray(tokens) && tokens.length > 0, `theme \"${theme}\" resolved to no tokens`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.no-unresolved-aliases\",\n\t\t\ttitle: \"no resolved token still carries an alias\",\n\t\t\tasync run(port) {\n\t\t\t\tfor (const token of await port.resolve(theme)) {\n\t\t\t\t\texpect(\n\t\t\t\t\t\ttypeof token.value !== \"string\" || !ALIAS.test(token.value.trim()),\n\t\t\t\t\t\t`token \"${token.name}\" resolved to the unfollowed alias ${String(token.value)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.names-are-unique\",\n\t\t\ttitle: \"token names are unique within a theme\",\n\t\t\tasync run(port) {\n\t\t\t\tconst names = (await port.resolve(theme)).map((token) => token.name);\n\t\t\t\texpect(new Set(names).size === names.length, \"the theme resolved duplicate token names\");\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** Runs a contract suite and returns every failure, rather than stopping at the first. */\nexport async function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{ passed: boolean; failures: Array<{ id: string; message: string }> }> {\n\tconst failures: Array<{ id: string; message: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tawait check.run(port);\n\t\t} catch (error) {\n\t\t\tfailures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: failures.length === 0, failures };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["/**\n * Port interfaces are declared here and implemented by vendor adapters elsewhere.\n * Nothing in this package imports a vendor SDK: a port is a shape, and an adapter\n * is anything that satisfies it. Where an open standard exists the port speaks it,\n * so the incumbent vendor is already just one provider behind the interface.\n */\n\n// ── Feature flags (OpenFeature-shaped) ──────────────────────────────────────\n\nexport type FlagValue = boolean | string | number | { [key: string]: unknown };\n\nexport interface EvaluationContext {\n\t/** Stable identifier for the subject of evaluation, usually a tenant or actor. */\n\ttargetingKey?: string;\n\t[attribute: string]: unknown;\n}\n\nexport type ResolutionReason = \"STATIC\" | \"DEFAULT\" | \"TARGETING_MATCH\" | \"SPLIT\" | \"CACHED\" | \"ERROR\";\n\nexport interface ResolutionDetails<T extends FlagValue> {\n\tvalue: T;\n\treason: ResolutionReason;\n\tvariant?: string;\n\terrorCode?: string;\n}\n\n/**\n * Structurally compatible with an OpenFeature provider's evaluation surface, so an\n * OpenFeature provider is a thin adapter rather than a translation layer.\n */\nexport interface FlagsPort {\n\tresolveBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): Promise<ResolutionDetails<boolean>>;\n\tresolveString(flagKey: string, defaultValue: string, context?: EvaluationContext): Promise<ResolutionDetails<string>>;\n\tresolveNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): Promise<ResolutionDetails<number>>;\n}\n\n// ── Design tokens (W3C DTCG) ────────────────────────────────────────────────\n\nexport interface DtcgToken {\n\t$value: unknown;\n\t$type?: string;\n\t$description?: string;\n}\n\nexport interface DtcgGroup {\n\t$type?: string;\n\t$description?: string;\n\t[member: string]: DtcgToken | DtcgGroup | string | undefined;\n}\n\nexport interface ResolvedToken {\n\tname: string;\n\tvalue: unknown;\n\ttype?: string;\n\tdescription?: string;\n}\n\nexport interface DesignTokensPort {\n\t/** Resolved tokens for one theme. Aliases are already followed. */\n\tresolve(theme: string): Promise<ResolvedToken[]>;\n}\n\nconst ALIAS = /^\\{([^}]+)\\}$/;\n\nconst isToken = (value: unknown): value is DtcgToken =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && \"$value\" in value;\n\nconst isGroup = (value: unknown): value is DtcgGroup =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && !(\"$value\" in value);\n\n/**\n * Flattens a DTCG document and follows aliases. `$type` is inherited from the\n * nearest ancestor group that declares one, which is what the format specifies\n * and what makes a theme file readable.\n */\nexport function resolveDesignTokens(document: DtcgGroup): ResolvedToken[] {\n\tconst flat = new Map<string, { token: DtcgToken; type?: string }>();\n\n\tconst walk = (node: DtcgGroup, path: string[], inheritedType?: string): void => {\n\t\tconst groupType = typeof node.$type === \"string\" ? node.$type : inheritedType;\n\t\tfor (const [key, member] of Object.entries(node)) {\n\t\t\tif (key.startsWith(\"$\")) continue;\n\t\t\tconst next = [...path, key];\n\t\t\tif (isToken(member)) {\n\t\t\t\tflat.set(next.join(\".\"), { token: member, type: member.$type ?? groupType });\n\t\t\t} else if (isGroup(member)) {\n\t\t\t\twalk(member, next, groupType);\n\t\t\t}\n\t\t}\n\t};\n\twalk(document, []);\n\n\tconst resolving = new Set<string>();\n\tconst resolved = new Map<string, unknown>();\n\n\tconst valueOf = (name: string): unknown => {\n\t\tif (resolved.has(name)) return resolved.get(name);\n\t\tconst entry = flat.get(name);\n\t\tif (!entry) throw new Error(`Design token alias \"{${name}}\" does not resolve to a declared token.`);\n\t\tif (resolving.has(name)) {\n\t\t\tthrow new Error(`Design token alias cycle: ${[...resolving, name].join(\" -> \")}.`);\n\t\t}\n\t\tconst raw = entry.token.$value;\n\t\tif (typeof raw !== \"string\") {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tconst alias = ALIAS.exec(raw.trim());\n\t\tif (!alias) {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tresolving.add(name);\n\t\tconst target = valueOf(alias[1]!);\n\t\tresolving.delete(name);\n\t\tresolved.set(name, target);\n\t\treturn target;\n\t};\n\n\treturn [...flat.entries()].map(([name, entry]) => {\n\t\tconst value = valueOf(name);\n\t\treturn {\n\t\t\tname,\n\t\t\tvalue,\n\t\t\t...(entry.type === undefined ? {} : { type: entry.type }),\n\t\t\t...(entry.token.$description === undefined ? {} : { description: entry.token.$description }),\n\t\t};\n\t});\n}\n\n// ── Identity (OAuth2 / OIDC-shaped) ─────────────────────────────────────────\n\n/**\n * Actor kinds a verified credential can resolve to. This mirrors `ActorType` in\n * `@fabricorg/platform`, restated here so this package keeps zero runtime\n * dependencies. `identityPortChecks` asserts the two stay identical.\n */\nexport type IdentityActorType =\n\t| \"natural_person\"\n\t| \"agent\"\n\t| \"system\"\n\t| \"service_account\"\n\t| \"external_system\"\n\t| \"integration\";\n\nexport const IDENTITY_ACTOR_TYPES: readonly IdentityActorType[] = [\n\t\"natural_person\",\n\t\"agent\",\n\t\"system\",\n\t\"service_account\",\n\t\"external_system\",\n\t\"integration\",\n];\n\n/**\n * The scopes a credential covers. `\"tenant-wide\"` is spelled out rather than\n * left as an absent field, so a credential that simply forgot to carry space\n * coverage can never be read as covering everything.\n */\nexport type ActorSpaceCoverage = readonly string[] | \"tenant-wide\";\n\n/**\n * Identity resolved from a verified credential. Every governed action,\n * projection decision, grant and audit record derives from actor context, so\n * this is the shape a gateway must produce before the platform is called.\n *\n * It carries no credential material. `credentialId` is an opaque, non-secret\n * reference retained for audit, matching the convention used by\n * `authorizationBindingId`.\n */\nexport interface ActorClaims {\n\t/** Stable subject identifier; becomes `actorId` on a governed submission. */\n\tsubject: string;\n\tactorType: IdentityActorType;\n\ttenantId: string;\n\tspaceIds: ActorSpaceCoverage;\n\t/** Granted scopes, if the issuer expresses authority that way. */\n\tscopes?: readonly string[];\n\tissuer: string;\n\t/** RFC 3339 timestamps. */\n\tissuedAt: string;\n\texpiresAt: string;\n\t/** Opaque, non-secret reference to the presented credential, for audit. */\n\tcredentialId?: string;\n}\n\n/**\n * Verifies a presented credential and resolves it to actor claims.\n *\n * An implementation returns `null` for any credential it cannot positively\n * verify — expired, malformed, wrong issuer, bad signature. It never returns\n * partially trusted claims, and it never echoes credential material back.\n */\nexport interface IdentityPort {\n\tverify(credential: string): Promise<ActorClaims | null>;\n}\n\nexport type ActorScopeRejection =\n\t| \"malformed_claims\"\n\t| \"expired\"\n\t| \"not_yet_valid\"\n\t| \"tenant_mismatch\"\n\t| \"space_not_covered\"\n\t| \"unknown_actor_type\";\n\nexport class ActorScopeError extends Error {\n\toverride readonly name = \"ActorScopeError\";\n\tconstructor(\n\t\treadonly rejection: ActorScopeRejection,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n/**\n * Confirm verified claims actually cover the tenant and space being acted on.\n *\n * Verification proves who the caller is. It does not prove they may act here.\n * Calling this before a governed submission is what stops a valid credential\n * for one tenant from being replayed against another.\n */\nexport function assertActorClaimsCoverScope(\n\tclaims: ActorClaims,\n\tscope: { tenantId: string; spaceId: string },\n\tnow: Date = new Date(),\n): void {\n\t// Every comparison against an invalid clock is false, which would let an\n\t// expired credential through. Reject the clock before trusting it.\n\tconst currentTime = now.getTime();\n\tif (Number.isNaN(currentTime)) {\n\t\tthrow new ActorScopeError(\"malformed_claims\", \"Actor scope was evaluated against an invalid clock.\");\n\t}\n\n\tif (typeof claims.actorType !== \"string\" || !IDENTITY_ACTOR_TYPES.includes(claims.actorType)) {\n\t\tthrow new ActorScopeError(\"unknown_actor_type\", `Actor type \"${String(claims.actorType)}\" is not a known actor kind.`);\n\t}\n\tif (typeof claims.subject !== \"string\" || claims.subject.length === 0) {\n\t\tthrow new ActorScopeError(\"malformed_claims\", \"Actor claims carry no subject.\");\n\t}\n\tif (typeof claims.tenantId !== \"string\" || claims.tenantId.length === 0) {\n\t\tthrow new ActorScopeError(\"malformed_claims\", \"Actor claims carry no tenant.\");\n\t}\n\n\t// Date.parse accepts timezone-less strings and reads them in the host's\n\t// local zone, so the same credential would expire at different instants on\n\t// different machines. Require an explicit offset.\n\tconst issuedAt = parseRfc3339(claims.issuedAt);\n\tconst expiresAt = parseRfc3339(claims.expiresAt);\n\tif (issuedAt === undefined || expiresAt === undefined) {\n\t\tthrow new ActorScopeError(\"malformed_claims\", \"Actor claims carry an issuedAt or expiresAt that is not an RFC 3339 timestamp with an explicit offset.\");\n\t}\n\tif (currentTime >= expiresAt) {\n\t\tthrow new ActorScopeError(\"expired\", `Actor claims expired at ${claims.expiresAt}.`);\n\t}\n\tif (currentTime < issuedAt) {\n\t\tthrow new ActorScopeError(\"not_yet_valid\", `Actor claims are not valid until ${claims.issuedAt}.`);\n\t}\n\n\tif (claims.tenantId !== scope.tenantId) {\n\t\tthrow new ActorScopeError(\n\t\t\t\"tenant_mismatch\",\n\t\t\t`Actor claims cover tenant \"${claims.tenantId}\" but the request targets \"${scope.tenantId}\".`,\n\t\t);\n\t}\n\n\t// A malformed adapter returning a bare string would otherwise reach\n\t// String.prototype.includes, where \"space_10\" covers \"space_1\".\n\tif (claims.spaceIds === \"tenant-wide\") return;\n\tif (!Array.isArray(claims.spaceIds) || !claims.spaceIds.every((space) => typeof space === \"string\")) {\n\t\tthrow new ActorScopeError(\"malformed_claims\", 'Actor claims spaceIds must be an array of strings or the literal \"tenant-wide\".');\n\t}\n\tif (!claims.spaceIds.includes(scope.spaceId)) {\n\t\tthrow new ActorScopeError(\n\t\t\t\"space_not_covered\",\n\t\t\t`Actor claims do not cover space \"${scope.spaceId}\".`,\n\t\t);\n\t}\n}\n\n/** RFC 3339 with a mandatory offset, so an instant means the same thing everywhere. */\nconst RFC3339 = /^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$/;\n\nfunction parseRfc3339(value: string): number | undefined {\n\tif (typeof value !== \"string\" || !RFC3339.test(value)) return undefined;\n\tconst parsed = Date.parse(value);\n\treturn Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/**\n * The actor fields a governed submission needs, derived from verified claims\n * after {@link assertActorClaimsCoverScope} has accepted them. Taking these\n * from claims rather than from request input is what keeps the audit trail\n * tied to something that was actually proven.\n */\nexport function actorContextFromClaims(\n\tclaims: ActorClaims,\n\tscope: { tenantId: string; spaceId: string },\n\tnow: Date = new Date(),\n): { actorId: string; actorType: IdentityActorType; tenantId: string; spaceId: string } {\n\tassertActorClaimsCoverScope(claims, scope, now);\n\treturn {\n\t\tactorId: claims.subject,\n\t\tactorType: claims.actorType,\n\t\ttenantId: claims.tenantId,\n\t\tspaceId: scope.spaceId,\n\t};\n}\n\n// ── Content (headless CMS-shaped) ───────────────────────────────────────────\n\n/**\n * A resolved piece of authored content.\n *\n * Content is copy and layout, never permission. A content system deciding what\n * a screen says is expected; a content system deciding what a screen may reach\n * is the failure this whole boundary exists to prevent, which is why nothing\n * here carries a capability reference.\n */\nexport interface ContentEntry {\n\tkey: string;\n\tlocale: string;\n\t/** Opaque revision, stable for identical content. Use it for cache keys. */\n\trevision: string;\n\tvalue: JsonLike;\n}\n\nexport type JsonLike = string | number | boolean | null | JsonLike[] | { [key: string]: JsonLike };\n\nexport interface ContentQuery {\n\tkey: string;\n\tlocale: string;\n\t/** Optional variant, for an experiment arm already enumerated in a release. */\n\tvariant?: string;\n}\n\nexport interface ContentPort {\n\t/** Resolves to `null` when the key is not authored, rather than inventing a default. */\n\tresolve(query: ContentQuery): Promise<ContentEntry | null>;\n}\n\n// ── Payments ────────────────────────────────────────────────────────────────\n\n/**\n * Money as an integer in the currency's minor unit.\n *\n * Never a float. A payment expressed as 10.1 is a payment that will eventually\n * be off by a cent, and reconciling that costs more than the type ever did.\n */\nexport interface MinorUnitAmount {\n\t/** Integer in the minor unit: 1050 is USD 10.50. */\n\tamount: number;\n\t/** ISO 4217 alphabetic code. */\n\tcurrency: string;\n}\n\n/**\n * The outcome of asking a provider to move money.\n *\n * `ambiguous` is the one that matters and the one most interfaces omit. A\n * timeout tells you nothing about whether the charge landed, and guessing is\n * how a customer gets billed twice. It maps onto the platform's own\n * `AdapterOutcomeKind`, so an ambiguous payment routes to reconciliation with\n * durable evidence instead of being retried.\n */\nexport type PaymentOutcome = \"succeeded\" | \"failed\" | \"ambiguous\";\n\nexport interface PaymentResult {\n\toutcome: PaymentOutcome;\n\t/** The provider's own identifier, for reconciliation. Required when known. */\n\tproviderReference?: string;\n\t/** Provider-supplied reason, for evidence rather than for branching. */\n\treason?: string;\n}\n\nexport interface PaymentRequest {\n\t/** Caller-stable key. The same key must never move money twice. */\n\tidempotencyKey: string;\n\tamount: MinorUnitAmount;\n}\n\nexport interface PaymentsPort {\n\tcharge(request: PaymentRequest): Promise<PaymentResult>;\n}\n\n// ── Search and recommendation ───────────────────────────────────────────────\n\n/**\n * A latency-critical read path, deliberately outside the governed pipeline.\n *\n * Search results are pointers, not authority. An identifier appearing in a\n * result set says the index knows about it, never that this actor may read it,\n * so anything the viewer then opens still goes through `ProjectionHost`.\n */\nexport interface SearchHit {\n\t/** Identifier a governed read can resolve. Never the record itself. */\n\tid: string;\n\tscore: number;\n}\n\nexport interface SearchQuery {\n\ttext: string;\n\tlimit: number;\n\t/** Narrowing the index can apply cheaply. Never a substitute for authorization. */\n\tfilters?: Record<string, string>;\n}\n\nexport interface SearchPort {\n\tquery(query: SearchQuery): Promise<{ hits: SearchHit[]; total: number }>;\n}\n\n// ── Customer relationship ───────────────────────────────────────────────────\n\n/**\n * An outbound record of something that already happened.\n *\n * A CRM is downstream of the governed pipeline, never upstream of it. It is\n * told; it does not decide. Keeping this one-way is what stops a marketing\n * system becoming a source of business truth.\n */\nexport interface CustomerEvent {\n\t/** Stable key so redelivery does not duplicate the record. */\n\teventId: string;\n\tsubjectId: string;\n\ttype: string;\n\toccurredAt: string;\n\tattributes?: Record<string, JsonLike>;\n}\n\nexport interface CustomerRecordPort {\n\trecord(event: CustomerEvent): Promise<void>;\n}\n\n// ── Defining a port ─────────────────────────────────────────────────────────\n\n/**\n * A port, as a first-class thing rather than a name in a string.\n *\n * Fabric ships a handful of port definitions because they speak open standards\n * and everyone needs them. It cannot ship the rest: an enterprise integrates\n * hundreds of external systems, and a framework that requires a pull request\n * for each one is not a framework. A vertical, a partner or a vendor defines\n * its own port with {@link definePort} and every mechanism here applies to it\n * unchanged — Fabric never learns what the system behind it is.\n *\n * The definition binds an id to the suite that certifies an adapter for it.\n * That binding is the point: without it, \"this port is satisfied\" only ever\n * meant \"somebody registered something claiming to be it\".\n */\nexport interface PortDefinition<TPort, TFixtures = void> {\n\t/**\n\t * Namespaced identifier, for example `fabric.flags` or `acme.docusign`.\n\t * Namespacing is required so a port defined outside this repository cannot\n\t * collide with one defined inside it.\n\t */\n\tid: string;\n\t/**\n\t * Version of the port contract itself, not of any adapter.\n\t *\n\t * Bump it when the interface changes shape. Certification is recorded\n\t * against a version, so raising it correctly invalidates every adapter\n\t * certified against the old contract instead of silently carrying them\n\t * forward.\n\t */\n\tversion: string;\n\t/** The open standard this port speaks, where one exists. */\n\tstandard?: { name: string; version?: string };\n\t/** One-line statement of what an adapter behind this port is responsible for. */\n\tdescription: string;\n\t/** The suite every adapter must pass. */\n\tchecks(fixtures: TFixtures): readonly PortCheck<TPort>[];\n}\n\n// Every segment takes the same shape the manifest permits, so a name a\n// capability can declare is a name a port can define. A dot is still required:\n// namespacing is what keeps a port defined outside this repository from\n// colliding with one defined inside it.\nconst PORT_ID = /^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$/;\n\n/**\n * Declare a port. Validates the definition itself, because a malformed port\n * definition produces adapters that certify against nothing.\n */\nexport function definePort<TPort, TFixtures = void>(\n\tdefinition: PortDefinition<TPort, TFixtures>,\n): PortDefinition<TPort, TFixtures> {\n\tif (!PORT_ID.test(definition.id)) {\n\t\tthrow new Error(`Port id \"${definition.id}\" must be namespaced lowercase, for example \"acme.docusign\".`);\n\t}\n\tif (!/^\\d+\\.\\d+\\.\\d+$/.test(definition.version)) {\n\t\tthrow new Error(`Port \"${definition.id}\" version must be a semver version.`);\n\t}\n\tif (!definition.description.trim()) {\n\t\tthrow new Error(`Port \"${definition.id}\" must describe what an adapter behind it is responsible for.`);\n\t}\n\tif (typeof definition.checks !== \"function\") {\n\t\tthrow new Error(`Port \"${definition.id}\" must supply a conformance suite.`);\n\t}\n\treturn definition;\n}\n\n/**\n * Confirm a port's suite can actually fail, and that its check ids are unique.\n *\n * A suite whose checks all pass against a deliberately broken adapter certifies\n * nothing while looking rigorous, which is worse than having no suite at all.\n * Run this against a stub that does the wrong thing when you author a port.\n */\nexport async function assertPortSuiteHasTeeth<TPort, TFixtures>(\n\tdefinition: PortDefinition<TPort, TFixtures>,\n\tfixtures: TFixtures,\n\tbrokenAdapters: TPort | readonly TPort[],\n): Promise<void> {\n\tconst checks = definition.checks(fixtures);\n\tif (checks.length === 0) {\n\t\tthrow new Error(`Port \"${definition.id}\" declares no checks, so no adapter can be certified against it.`);\n\t}\n\tconst ids = new Set<string>();\n\tfor (const check of checks) {\n\t\tif (ids.has(check.id)) throw new Error(`Port \"${definition.id}\" declares check \"${check.id}\" twice.`);\n\t\tids.add(check.id);\n\t}\n\t// Every check must be caught by *some* adapter in the set, not all of them\n\t// by one. Demanding a single stub fail every check is unsatisfiable for any\n\t// suite containing a fixture-liveness guard, because such a guard bites on\n\t// the complement of what the checks it guards bite on: an adapter returning\n\t// nothing trips the guard, and an adapter returning something malformed\n\t// trips the checks. Supply one adapter per way of being wrong.\n\tconst adapters = Array.isArray(brokenAdapters) ? brokenAdapters : [brokenAdapters as TPort];\n\tif (adapters.length === 0) {\n\t\tthrow new Error(`Port \"${definition.id}\" needs at least one deliberately broken adapter to prove its suite bites.`);\n\t}\n\n\tconst uncaught = new Set(checks.map((check) => check.id));\n\tfor (const adapter of adapters) {\n\t\tfor (const check of checks) {\n\t\t\tif (!uncaught.has(check.id)) continue;\n\t\t\ttry {\n\t\t\t\tawait check.run(adapter);\n\t\t\t} catch {\n\t\t\t\tuncaught.delete(check.id);\n\t\t\t}\n\t\t}\n\t}\n\tif (uncaught.size > 0) {\n\t\tthrow new Error(\n\t\t\t`Port \"${definition.id}\" has ${uncaught.size} check(s) that no supplied broken adapter trips, so they prove nothing: ${[...uncaught].join(\", \")}.`,\n\t\t);\n\t}\n}\n\n// ── Certifying an adapter ───────────────────────────────────────────────────\n\n/** Evidence that an adapter passed a port's suite, and which contract it passed. */\nexport interface AdapterCertification {\n\tportId: string;\n\t/** Port contract version the adapter was certified against. */\n\tportVersion: string;\n\tchecks: string[];\n\tcertifiedAt: string;\n}\n\nexport interface CertifiedAdapter extends RegisteredAdapter {\n\tcertification: AdapterCertification;\n}\n\n/**\n * Run a port's suite against an adapter and record what it passed.\n *\n * Registration is a claim; certification is evidence. Deployment gates can then\n * require the second rather than accepting the first.\n */\nexport async function certifyAdapter<TPort, TFixtures>(input: {\n\tdefinition: PortDefinition<TPort, TFixtures>;\n\tfixtures: TFixtures;\n\tadapter: TPort;\n\tvendor: string;\n\tversion?: string;\n\tnow?: Date;\n}): Promise<CertifiedAdapter> {\n\t// Validate the definition here too: a definition not built through\n\t// definePort can carry a version like \"1.0\", and the certification it\n\t// produces is then reported downstream as missing rather than malformed.\n\tdefinePort(input.definition);\n\tconst checks = input.definition.checks(input.fixtures);\n\tif (checks.length === 0) {\n\t\tthrow new Error(`Port \"${input.definition.id}\" declares no checks, so no adapter can be certified against it.`);\n\t}\n\tconst passed: string[] = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tawait check.run(input.adapter);\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Adapter \"${input.vendor}\" failed port \"${input.definition.id}\" check \"${check.id}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\t\tpassed.push(check.id);\n\t}\n\treturn {\n\t\tport: input.definition.id,\n\t\tvendor: input.vendor,\n\t\t...(input.version === undefined ? {} : { version: input.version }),\n\t\t...(input.definition.standard ? { standard: input.definition.standard } : {}),\n\t\tcertification: {\n\t\t\tportId: input.definition.id,\n\t\t\tportVersion: input.definition.version,\n\t\t\tchecks: passed,\n\t\t\tcertifiedAt: (input.now ?? new Date()).toISOString(),\n\t\t},\n\t};\n}\n\n/**\n * Whether an adapter carries certification evidence *for this port*.\n *\n * Testing only that a `certification` property exists made the evidence a claim\n * again: any hand-written object with the right property name satisfied the\n * gate. The evidence has to be internally consistent and about the port being\n * required, or it is decoration.\n */\nfunction isCertifiedFor(adapter: RegisteredAdapter, portName: string): adapter is CertifiedAdapter {\n\tconst certification = (adapter as CertifiedAdapter).certification;\n\tif (!certification || typeof certification !== \"object\") return false;\n\tif (certification.portId !== portName) return false;\n\tif (certification.portId !== adapter.port) return false;\n\tif (!/^\\d+\\.\\d+\\.\\d+$/.test(certification.portVersion ?? \"\")) return false;\n\tif (!Array.isArray(certification.checks) || certification.checks.length === 0) return false;\n\tif (Number.isNaN(Date.parse(certification.certifiedAt ?? \"\"))) return false;\n\treturn true;\n}\n\n// ── Requirement satisfaction ────────────────────────────────────────────────\n\nexport interface PortRequirement {\n\tname: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\n/** The subset of capability metadata that declares what ports it needs. */\nexport interface PortRequiringCapability {\n\tnamespace: string;\n\trequirements?: { ports?: PortRequirement[] };\n}\n\nexport interface RegisteredAdapter {\n\t/** Port this adapter implements, matching the requirement's `name`. */\n\tport: string;\n\tvendor: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\nexport type PortFindingCode =\n\t| \"unsatisfied_port\"\n\t| \"standard_not_spoken\"\n\t| \"version_mismatch\"\n\t| \"uncertified_adapter\"\n\t| \"stale_certification\";\n\nexport interface PortFinding {\n\tcode: PortFindingCode;\n\tport: string;\n\tmessage: string;\n}\n\nexport interface PortValidationResult {\n\tvalid: boolean;\n\tfindings: PortFinding[];\n}\n\n/**\n * Checks that every port a capability declares is backed by a registered adapter.\n * `CapabilityPortRequirement` is otherwise a name that resolves against nothing;\n * this is what turns the declaration into a deployment-time gate.\n *\n * Version matching is exact. Range negotiation belongs to whatever installs the\n * adapters, and pretending to do semver here would be worse than not doing it.\n */\nexport function validatePortRequirements(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n\t/**\n\t * Port definitions in force. Supplying them checks that whatever\n\t * certification is present was issued against the current contract version.\n\t *\n\t * They do not by themselves require certification to exist: pass\n\t * `requireCertification` for that. Definitions alone catch a stale\n\t * certification, not a missing one, and an adapter carrying none passes.\n\t */\n\tdefinitions?: readonly PortDefinition<never, never>[];\n\t/** Require certification for every required port. Defaults to false. */\n\trequireCertification?: boolean;\n}): PortValidationResult {\n\tconst definitions = new Map((input.definitions ?? []).map((definition) => [definition.id, definition]));\n\tconst findings: PortFinding[] = [];\n\tfor (const requirement of input.capability.requirements?.ports ?? []) {\n\t\tconst candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);\n\t\tif (candidates.length === 0) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"unsatisfied_port\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `capability \"${input.capability.namespace}\" requires port \"${requirement.name}\" and no adapter is registered for it`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\t// Narrowed as each constraint is applied, so one adapter cannot satisfy\n\t\t// the standard while a different one satisfies the version.\n\t\tlet eligible = candidates;\n\t\tif (requirement.standard) {\n\t\t\teligible = eligible.filter((adapter) => adapter.standard?.name === requirement.standard!.name);\n\t\t\tif (eligible.length === 0) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tcode: \"standard_not_spoken\",\n\t\t\t\t\tport: requirement.name,\n\t\t\t\t\tmessage: `port \"${requirement.name}\" must speak \"${requirement.standard.name}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? \"no standard\"})`).join(\", \")}`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (requirement.standard.version) {\n\t\t\t\tconst atVersion = eligible.filter((adapter) => adapter.standard?.version === requirement.standard!.version);\n\t\t\t\tif (atVersion.length === 0) {\n\t\t\t\t\tfindings.push({\n\t\t\t\t\t\tcode: \"standard_not_spoken\",\n\t\t\t\t\t\tport: requirement.name,\n\t\t\t\t\t\tmessage: `port \"${requirement.name}\" must speak \"${requirement.standard.name}\" version \"${requirement.standard.version}\"; registered adapters speak ${eligible.map((adapter) => adapter.standard?.version ?? \"an unstated version\").join(\", \")}`,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\teligible = atVersion;\n\t\t\t}\n\t\t}\n\t\tif (requirement.version) {\n\t\t\tconst atVersion = eligible.filter((adapter) => adapter.version === requirement.version);\n\t\t\tif (atVersion.length === 0) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tcode: \"version_mismatch\",\n\t\t\t\t\tport: requirement.name,\n\t\t\t\t\tmessage: `port \"${requirement.name}\" requires version \"${requirement.version}\"; eligible adapters are ${eligible.map((adapter) => `${adapter.vendor}@${adapter.version ?? \"unversioned\"}`).join(\", \")}`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\teligible = atVersion;\n\t\t}\n\n\t\t// Registration is a claim. Certification is evidence that the adapter\n\t\t// passed the port's own suite, against the contract version in force.\n\t\tconst definition = definitions.get(requirement.name);\n\t\tif (input.requireCertification || definition) {\n\t\t\tconst certified = eligible.filter((adapter) => isCertifiedFor(adapter, requirement.name));\n\t\t\tif (certified.length === 0) {\n\t\t\t\tif (input.requireCertification) {\n\t\t\t\t\tfindings.push({\n\t\t\t\t\t\tcode: \"uncertified_adapter\",\n\t\t\t\t\t\tport: requirement.name,\n\t\t\t\t\t\tmessage: `port \"${requirement.name}\" has eligible adapters (${eligible.map((adapter) => adapter.vendor).join(\", \")}) but none carries certification evidence`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (definition && !certified.some((adapter) => adapter.certification.portVersion === definition.version)) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tcode: \"stale_certification\",\n\t\t\t\t\tport: requirement.name,\n\t\t\t\t\tmessage: `port \"${requirement.name}\" is at contract version ${definition.version}; adapters are certified against ${certified.map((adapter) => `${adapter.vendor}@${adapter.certification.portVersion}`).join(\", \")}`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn { valid: findings.length === 0, findings };\n}\n\nexport function assertPortRequirementsSatisfied(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n\tdefinitions?: readonly PortDefinition<never, never>[];\n\trequireCertification?: boolean;\n}): void {\n\tconst result = validatePortRequirements(input);\n\tif (result.valid) return;\n\tthrow new Error([\n\t\t`Port requirements for \"${input.capability.namespace}\" are not satisfied:`,\n\t\t...result.findings.map((finding) => ` - ${finding.message}`),\n\t].join(\"\\n\"));\n}\n\n// ── Adapter contract kit ────────────────────────────────────────────────────\n\nexport interface PortCheck<TPort> {\n\tid: string;\n\ttitle: string;\n\t/** Throws on failure, exactly as an assertion would. */\n\trun(port: TPort): Promise<void>;\n}\n\nclass PortContractFailure extends Error {\n\toverride readonly name = \"PortContractFailure\";\n}\n\nfunction expect(condition: unknown, message: string): asserts condition {\n\tif (!condition) throw new PortContractFailure(message);\n}\n\n/**\n * Every flags adapter must pass these, so swapping vendors is an adapter build\n * plus a configuration change rather than a migration.\n */\nexport function flagsPortChecks(): PortCheck<FlagsPort>[] {\n\tconst unknownKey = \"fabric.contract.definitely-not-configured\";\n\treturn [\n\t\t{\n\t\t\tid: \"flags.default-on-unknown-key\",\n\t\t\ttitle: \"an unknown flag resolves to the supplied default rather than throwing\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.resolveBoolean(unknownKey, true);\n\t\t\t\texpect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);\n\t\t\t\texpect(\n\t\t\t\t\tresult.reason === \"DEFAULT\" || result.reason === \"ERROR\",\n\t\t\t\t\t`an unknown flag resolved with reason \"${result.reason}\"; a default or error was expected`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.default-is-typed\",\n\t\t\ttitle: \"each typed resolver returns its own type\",\n\t\t\tasync run(port) {\n\t\t\t\texpect(typeof (await port.resolveBoolean(unknownKey, false)).value === \"boolean\", \"resolveBoolean did not return a boolean\");\n\t\t\t\texpect(typeof (await port.resolveString(unknownKey, \"fallback\")).value === \"string\", \"resolveString did not return a string\");\n\t\t\t\texpect(typeof (await port.resolveNumber(unknownKey, 42)).value === \"number\", \"resolveNumber did not return a number\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.evaluation-is-pure\",\n\t\t\ttitle: \"the same key and context resolve the same way twice\",\n\t\t\tasync run(port) {\n\t\t\t\tconst context = { targetingKey: \"fabric-contract-subject\" };\n\t\t\t\tconst first = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\tconst second = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\texpect(first.value === second.value, `the same evaluation returned \"${first.value}\" then \"${second.value}\"`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.tolerates-absent-context\",\n\t\t\ttitle: \"evaluation without a context does not throw\",\n\t\t\tasync run(port) {\n\t\t\t\tawait port.resolveBoolean(unknownKey, false);\n\t\t\t},\n\t\t},\n\t];\n}\n\n/**\n * The suite an identity adapter must pass before it can stand behind\n * `IdentityPort`. Every check here is a fail-closed property: the cost of\n * getting one wrong is a credential being trusted further than it proves.\n *\n * `validCredential` must be a credential the adapter verifies successfully,\n * and `expected` the claims it should resolve to.\n */\nexport function identityPortChecks(fixtures: {\n\tvalidCredential: string;\n\texpected: Pick<ActorClaims, \"subject\" | \"tenantId\">;\n\texpiredCredential?: string;\n}): PortCheck<IdentityPort>[] {\n\tconst checks: PortCheck<IdentityPort>[] = [\n\t\t{\n\t\t\tid: \"identity.rejects-garbage\",\n\t\t\ttitle: \"an unverifiable credential resolves to null rather than partial claims\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.verify(\"not-a-credential\");\n\t\t\t\texpect(result === null, \"an unverifiable credential resolved to claims instead of null\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"identity.rejects-empty\",\n\t\t\ttitle: \"an empty credential resolves to null\",\n\t\t\tasync run(port) {\n\t\t\t\texpect((await port.verify(\"\")) === null, \"an empty credential resolved to claims\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"identity.resolves-valid\",\n\t\t\ttitle: \"a valid credential resolves to the expected subject and tenant\",\n\t\t\tasync run(port) {\n\t\t\t\tconst claims = await port.verify(fixtures.validCredential);\n\t\t\t\texpect(claims !== null, \"a valid credential failed to verify\");\n\t\t\t\texpect(claims?.subject === fixtures.expected.subject, `subject was \"${claims?.subject}\"`);\n\t\t\t\texpect(claims?.tenantId === fixtures.expected.tenantId, `tenantId was \"${claims?.tenantId}\"`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"identity.claims-are-complete\",\n\t\t\ttitle: \"resolved claims carry every field the platform derives actor context from\",\n\t\t\tasync run(port) {\n\t\t\t\tconst claims = await port.verify(fixtures.validCredential);\n\t\t\t\texpect(claims !== null, \"a valid credential failed to verify\");\n\t\t\t\tif (!claims) return;\n\t\t\t\texpect(IDENTITY_ACTOR_TYPES.includes(claims.actorType), `actorType \"${claims.actorType}\" is not a known actor kind`);\n\t\t\t\texpect(typeof claims.issuer === \"string\" && claims.issuer.length > 0, \"claims carry no issuer\");\n\t\t\t\t// The same grammar assertActorClaimsCoverScope enforces. Certifying\n\t\t\t\t// a looser one produces an adapter that passes here and is refused\n\t\t\t\t// on every request.\n\t\t\t\texpect(RFC3339.test(claims.issuedAt), `issuedAt \"${claims.issuedAt}\" is not RFC 3339 with an explicit offset`);\n\t\t\t\texpect(RFC3339.test(claims.expiresAt), `expiresAt \"${claims.expiresAt}\" is not RFC 3339 with an explicit offset`);\n\t\t\t\texpect(typeof claims.tenantId === \"string\" && claims.tenantId.length > 0, \"claims carry no tenant\");\n\t\t\t\texpect(\n\t\t\t\t\tclaims.spaceIds === \"tenant-wide\" || Array.isArray(claims.spaceIds),\n\t\t\t\t\t\"spaceIds must be an explicit list or the literal \\\"tenant-wide\\\"\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"identity.carries-no-credential-material\",\n\t\t\ttitle: \"resolved claims never echo the credential back\",\n\t\t\tasync run(port) {\n\t\t\t\tconst claims = await port.verify(fixtures.validCredential);\n\t\t\t\texpect(claims !== null, \"a valid credential failed to verify, so this check proved nothing\");\n\t\t\t\tif (!claims) return;\n\t\t\t\tconst serialized = JSON.stringify(claims);\n\t\t\t\texpect(\n\t\t\t\t\t!serialized.includes(fixtures.validCredential),\n\t\t\t\t\t\"resolved claims contain the presented credential; claims must carry an opaque reference instead\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"identity.verification-is-stable\",\n\t\t\ttitle: \"the same credential resolves the same subject twice\",\n\t\t\tasync run(port) {\n\t\t\t\tconst first = await port.verify(fixtures.validCredential);\n\t\t\t\tconst second = await port.verify(fixtures.validCredential);\n\t\t\t\texpect(first?.subject === second?.subject, \"the same credential resolved to two different subjects\");\n\t\t\t},\n\t\t},\n\t];\n\n\tif (fixtures.expiredCredential !== undefined) {\n\t\tchecks.push({\n\t\t\tid: \"identity.rejects-expired\",\n\t\t\ttitle: \"an expired credential resolves to null rather than stale claims\",\n\t\t\tasync run(port) {\n\t\t\t\tconst expiredCredential = fixtures.expiredCredential as string;\n\t\t\t\texpect((await port.verify(expiredCredential)) === null, \"an expired credential still resolved to claims\");\n\t\t\t},\n\t\t});\n\t}\n\n\treturn checks;\n}\n\n/** The suite a content adapter must pass. */\nexport function contentPortChecks(fixtures: {\n\tpresent: ContentQuery;\n\tabsentKey: string;\n}): PortCheck<ContentPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"content.missing-resolves-null\",\n\t\t\ttitle: \"an unauthored key resolves to null rather than an invented default\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.resolve({ ...fixtures.present, key: fixtures.absentKey });\n\t\t\t\texpect(result === null, \"an unauthored key resolved to content instead of null\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"content.revision-is-stable\",\n\t\t\ttitle: \"identical content resolves the same revision twice\",\n\t\t\tasync run(port) {\n\t\t\t\tconst first = await port.resolve(fixtures.present);\n\t\t\t\tconst second = await port.resolve(fixtures.present);\n\t\t\t\texpect(first !== null && second !== null, \"the fixture key is not authored\");\n\t\t\t\texpect(first?.revision === second?.revision, \"the same content reported two revisions, so it cannot be cached\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"content.carries-no-capability-reference\",\n\t\t\ttitle: \"authored content never carries a capability reference\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.resolve(fixtures.present);\n\t\t\t\texpect(\n\t\t\t\t\t!JSON.stringify(result ?? {}).includes(\"capability://\"),\n\t\t\t\t\t\"content carried a capability reference; what a screen may reach is decided by a release, not by an author\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** The suite a payments adapter must pass. */\nexport function paymentsPortChecks(fixtures: {\n\trequest: PaymentRequest;\n}): PortCheck<PaymentsPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"payments.rejects-non-integer-amount\",\n\t\t\ttitle: \"a fractional minor-unit amount is refused rather than rounded\",\n\t\t\tasync run(port) {\n\t\t\t\tlet refused = false;\n\t\t\t\ttry {\n\t\t\t\t\tawait port.charge({ ...fixtures.request, amount: { ...fixtures.request.amount, amount: 10.5 } });\n\t\t\t\t} catch {\n\t\t\t\t\trefused = true;\n\t\t\t\t}\n\t\t\t\texpect(refused, \"a fractional minor-unit amount was accepted; money is an integer or it drifts\");\n\t\t\t\t// The same request without the fraction must actually charge, or an\n\t\t\t\t// adapter that refuses everything, or succeeds at nothing, passes\n\t\t\t\t// every check in this suite.\n\t\t\t\tconst control = await port.charge({\n\t\t\t\t\t...fixtures.request,\n\t\t\t\t\tidempotencyKey: `${fixtures.request.idempotencyKey}:integer-probe`,\n\t\t\t\t});\n\t\t\t\texpect(\n\t\t\t\t\tcontrol.outcome === \"succeeded\",\n\t\t\t\t\t`the fixture charge resolved \"${control.outcome}\"; a suite run against an adapter that never succeeds certifies nothing`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"payments.requires-idempotency-key\",\n\t\t\ttitle: \"a charge without a stable key is refused\",\n\t\t\tasync run(port) {\n\t\t\t\tlet refused = false;\n\t\t\t\ttry {\n\t\t\t\t\tawait port.charge({ ...fixtures.request, idempotencyKey: \"\" });\n\t\t\t\t} catch {\n\t\t\t\t\trefused = true;\n\t\t\t\t}\n\t\t\t\texpect(refused, \"a charge with no idempotency key was accepted; a retry would move money twice\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"payments.same-key-moves-money-once\",\n\t\t\ttitle: \"the same idempotency key resolves to the same provider reference\",\n\t\t\tasync run(port) {\n\t\t\t\tconst first = await port.charge(fixtures.request);\n\t\t\t\tconst second = await port.charge(fixtures.request);\n\t\t\t\texpect(\n\t\t\t\t\tfirst.outcome === second.outcome,\n\t\t\t\t\t`the same key produced \"${first.outcome}\" then \"${second.outcome}\"`,\n\t\t\t\t);\n\t\t\t\t// Without a reference there is nothing to compare, and a\n\t\t\t\t// double-charger would pass on outcome equality alone. A\n\t\t\t\t// succeeded charge must be identifiable or it cannot be reconciled.\n\t\t\t\texpect(\n\t\t\t\t\tfirst.outcome !== \"succeeded\" || first.providerReference !== undefined,\n\t\t\t\t\t\"a succeeded charge carried no provider reference, so a retry cannot be told from a second charge\",\n\t\t\t\t);\n\t\t\t\texpect(\n\t\t\t\t\tfirst.providerReference === second.providerReference,\n\t\t\t\t\t\"the same key produced two provider references, so a retry charged twice\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"payments.outcome-is-declared\",\n\t\t\ttitle: \"an outcome is one of the three the platform can act on\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.charge(fixtures.request);\n\t\t\t\texpect(\n\t\t\t\t\tresult.outcome === \"succeeded\" || result.outcome === \"failed\" || result.outcome === \"ambiguous\",\n\t\t\t\t\t`outcome \"${result.outcome}\" is not one the platform can reconcile`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** The suite a search adapter must pass. */\nexport function searchPortChecks(fixtures: {\n\tquery: SearchQuery;\n}): PortCheck<SearchPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"search.fixture-returns-hits\",\n\t\t\ttitle: \"the fixture query matches something, so the checks below are not vacuous\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.query(fixtures.query);\n\t\t\t\texpect(\n\t\t\t\t\tresult.hits.length > 0,\n\t\t\t\t\t\"the fixture query returned nothing, so every check below passes without exercising the adapter\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"search.respects-limit\",\n\t\t\ttitle: \"a result set never exceeds the requested limit\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.query({ ...fixtures.query, limit: 1 });\n\t\t\t\texpect(result.hits.length <= 1, `returned ${result.hits.length} hits for a limit of 1`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"search.returns-identifiers-not-records\",\n\t\t\ttitle: \"a hit carries an identifier a governed read can resolve, not the record\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.query(fixtures.query);\n\t\t\t\tfor (const hit of result.hits) {\n\t\t\t\t\texpect(typeof hit.id === \"string\" && hit.id.length > 0, \"a hit carried no identifier\");\n\t\t\t\t\texpect(\n\t\t\t\t\t\tObject.keys(hit).every((key) => key === \"id\" || key === \"score\"),\n\t\t\t\t\t\t\"a hit carried record fields; search results are pointers, and reading one still goes through ProjectionHost\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** The suite a customer-record adapter must pass. */\nexport function customerRecordPortChecks(fixtures: {\n\tevent: CustomerEvent;\n}): PortCheck<CustomerRecordPort>[] {\n\treturn [\n\t\t{\n\t\t\t// This proves redelivery does not *fail*. Whether it duplicates is not\n\t\t\t// observable through this port, which has no read side by design, so\n\t\t\t// the suite says what it can and does not imply more.\n\t\t\tid: \"crm.redelivery-does-not-fail\",\n\t\t\ttitle: \"recording the same event twice is not an error\",\n\t\t\tasync run(port) {\n\t\t\t\tawait port.record(fixtures.event);\n\t\t\t\tawait port.record(fixtures.event);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"crm.rejects-unidentified-event\",\n\t\t\ttitle: \"an event with no stable id is refused\",\n\t\t\tasync run(port) {\n\t\t\t\tlet refused = false;\n\t\t\t\ttry {\n\t\t\t\t\tawait port.record({ ...fixtures.event, eventId: \"\" });\n\t\t\t\t} catch {\n\t\t\t\t\trefused = true;\n\t\t\t\t}\n\t\t\t\texpect(refused, \"an event with no id was accepted; redelivery would duplicate the record\");\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"tokens.theme-resolves\",\n\t\t\ttitle: \"a known theme resolves to at least one token\",\n\t\t\tasync run(port) {\n\t\t\t\tconst tokens = await port.resolve(theme);\n\t\t\t\texpect(Array.isArray(tokens) && tokens.length > 0, `theme \"${theme}\" resolved to no tokens`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.no-unresolved-aliases\",\n\t\t\ttitle: \"no resolved token still carries an alias\",\n\t\t\tasync run(port) {\n\t\t\t\tfor (const token of await port.resolve(theme)) {\n\t\t\t\t\texpect(\n\t\t\t\t\t\ttypeof token.value !== \"string\" || !ALIAS.test(token.value.trim()),\n\t\t\t\t\t\t`token \"${token.name}\" resolved to the unfollowed alias ${String(token.value)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.names-are-unique\",\n\t\t\ttitle: \"token names are unique within a theme\",\n\t\t\tasync run(port) {\n\t\t\t\tconst names = (await port.resolve(theme)).map((token) => token.name);\n\t\t\t\texpect(new Set(names).size === names.length, \"the theme resolved duplicate token names\");\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** Runs a contract suite and returns every failure, rather than stopping at the first. */\nexport async function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{ passed: boolean; failures: Array<{ id: string; message: string }> }> {\n\tconst failures: Array<{ id: string; message: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tawait check.run(port);\n\t\t} catch (error) {\n\t\t\tfailures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: failures.length === 0, failures };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8DA,IAAM,QAAQ;AAEd,IAAM,UAAU,CAAC,UAChB,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,YAAY;AAE9E,IAAM,UAAU,CAAC,UAChB,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,YAAY;AAOzE,SAAS,oBAAoB,UAAsC;AACzE,QAAM,OAAO,oBAAI,IAAiD;AAElE,QAAM,OAAO,CAAC,MAAiB,MAAgB,kBAAiC;AAC/E,UAAM,YAAY,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAChE,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,IAAI,GAAG;AACjD,UAAI,IAAI,WAAW,GAAG,EAAG;AACzB,YAAM,OAAO,CAAC,GAAG,MAAM,GAAG;AAC1B,UAAI,QAAQ,MAAM,GAAG;AACpB,aAAK,IAAI,KAAK,KAAK,GAAG,GAAG,EAAE,OAAO,QAAQ,MAAM,OAAO,SAAS,UAAU,CAAC;AAAA,MAC5E,WAAW,QAAQ,MAAM,GAAG;AAC3B,aAAK,QAAQ,MAAM,SAAS;AAAA,MAC7B;AAAA,IACD;AAAA,EACD;AACA,OAAK,UAAU,CAAC,CAAC;AAEjB,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,WAAW,oBAAI,IAAqB;AAE1C,QAAM,UAAU,CAAC,SAA0B;AAC1C,QAAI,SAAS,IAAI,IAAI,EAAG,QAAO,SAAS,IAAI,IAAI;AAChD,UAAM,QAAQ,KAAK,IAAI,IAAI;AAC3B,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,wBAAwB,IAAI,0CAA0C;AAClG,QAAI,UAAU,IAAI,IAAI,GAAG;AACxB,YAAM,IAAI,MAAM,6BAA6B,CAAC,GAAG,WAAW,IAAI,EAAE,KAAK,MAAM,CAAC,GAAG;AAAA,IAClF;AACA,UAAM,MAAM,MAAM,MAAM;AACxB,QAAI,OAAO,QAAQ,UAAU;AAC5B,eAAS,IAAI,MAAM,GAAG;AACtB,aAAO;AAAA,IACR;AACA,UAAM,QAAQ,MAAM,KAAK,IAAI,KAAK,CAAC;AACnC,QAAI,CAAC,OAAO;AACX,eAAS,IAAI,MAAM,GAAG;AACtB,aAAO;AAAA,IACR;AACA,cAAU,IAAI,IAAI;AAClB,UAAM,SAAS,QAAQ,MAAM,CAAC,CAAE;AAChC,cAAU,OAAO,IAAI;AACrB,aAAS,IAAI,MAAM,MAAM;AACzB,WAAO;AAAA,EACR;AAEA,SAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;AACjD,UAAM,QAAQ,QAAQ,IAAI;AAC1B,WAAO;AAAA,MACN;AAAA,MACA;AAAA,MACA,GAAI,MAAM,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,MAAM,KAAK;AAAA,MACvD,GAAI,MAAM,MAAM,iBAAiB,SAAY,CAAC,IAAI,EAAE,aAAa,MAAM,MAAM,aAAa;AAAA,IAC3F;AAAA,EACD,CAAC;AACF;AAiBO,IAAM,uBAAqD;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAqDO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAE1C,YACU,WACT,SACC;AACD,UAAM,OAAO;AAHJ;AAAA,EAIV;AAAA,EAJU;AAAA,EAFQ,OAAO;AAO1B;AASO,SAAS,4BACf,QACA,OACA,MAAY,oBAAI,KAAK,GACd;AAGP,QAAM,cAAc,IAAI,QAAQ;AAChC,MAAI,OAAO,MAAM,WAAW,GAAG;AAC9B,UAAM,IAAI,gBAAgB,oBAAoB,qDAAqD;AAAA,EACpG;AAEA,MAAI,OAAO,OAAO,cAAc,YAAY,CAAC,qBAAqB,SAAS,OAAO,SAAS,GAAG;AAC7F,UAAM,IAAI,gBAAgB,sBAAsB,eAAe,OAAO,OAAO,SAAS,CAAC,8BAA8B;AAAA,EACtH;AACA,MAAI,OAAO,OAAO,YAAY,YAAY,OAAO,QAAQ,WAAW,GAAG;AACtE,UAAM,IAAI,gBAAgB,oBAAoB,gCAAgC;AAAA,EAC/E;AACA,MAAI,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,WAAW,GAAG;AACxE,UAAM,IAAI,gBAAgB,oBAAoB,+BAA+B;AAAA,EAC9E;AAKA,QAAM,WAAW,aAAa,OAAO,QAAQ;AAC7C,QAAM,YAAY,aAAa,OAAO,SAAS;AAC/C,MAAI,aAAa,UAAa,cAAc,QAAW;AACtD,UAAM,IAAI,gBAAgB,oBAAoB,wGAAwG;AAAA,EACvJ;AACA,MAAI,eAAe,WAAW;AAC7B,UAAM,IAAI,gBAAgB,WAAW,2BAA2B,OAAO,SAAS,GAAG;AAAA,EACpF;AACA,MAAI,cAAc,UAAU;AAC3B,UAAM,IAAI,gBAAgB,iBAAiB,oCAAoC,OAAO,QAAQ,GAAG;AAAA,EAClG;AAEA,MAAI,OAAO,aAAa,MAAM,UAAU;AACvC,UAAM,IAAI;AAAA,MACT;AAAA,MACA,8BAA8B,OAAO,QAAQ,8BAA8B,MAAM,QAAQ;AAAA,IAC1F;AAAA,EACD;AAIA,MAAI,OAAO,aAAa,cAAe;AACvC,MAAI,CAAC,MAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,OAAO,UAAU,QAAQ,GAAG;AACpG,UAAM,IAAI,gBAAgB,oBAAoB,iFAAiF;AAAA,EAChI;AACA,MAAI,CAAC,OAAO,SAAS,SAAS,MAAM,OAAO,GAAG;AAC7C,UAAM,IAAI;AAAA,MACT;AAAA,MACA,oCAAoC,MAAM,OAAO;AAAA,IAClD;AAAA,EACD;AACD;AAGA,IAAM,UAAU;AAEhB,SAAS,aAAa,OAAmC;AACxD,MAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,KAAK,EAAG,QAAO;AAC9D,QAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,SAAO,OAAO,MAAM,MAAM,IAAI,SAAY;AAC3C;AAQO,SAAS,uBACf,QACA,OACA,MAAY,oBAAI,KAAK,GACkE;AACvF,8BAA4B,QAAQ,OAAO,GAAG;AAC9C,SAAO;AAAA,IACN,SAAS,OAAO;AAAA,IAChB,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,IACjB,SAAS,MAAM;AAAA,EAChB;AACD;AA0KA,IAAM,UAAU;AAMT,SAAS,WACf,YACmC;AACnC,MAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,GAAG;AACjC,UAAM,IAAI,MAAM,YAAY,WAAW,EAAE,8DAA8D;AAAA,EACxG;AACA,MAAI,CAAC,kBAAkB,KAAK,WAAW,OAAO,GAAG;AAChD,UAAM,IAAI,MAAM,SAAS,WAAW,EAAE,qCAAqC;AAAA,EAC5E;AACA,MAAI,CAAC,WAAW,YAAY,KAAK,GAAG;AACnC,UAAM,IAAI,MAAM,SAAS,WAAW,EAAE,+DAA+D;AAAA,EACtG;AACA,MAAI,OAAO,WAAW,WAAW,YAAY;AAC5C,UAAM,IAAI,MAAM,SAAS,WAAW,EAAE,oCAAoC;AAAA,EAC3E;AACA,SAAO;AACR;AASA,eAAsB,wBACrB,YACA,UACA,gBACgB;AAChB,QAAM,SAAS,WAAW,OAAO,QAAQ;AACzC,MAAI,OAAO,WAAW,GAAG;AACxB,UAAM,IAAI,MAAM,SAAS,WAAW,EAAE,kEAAkE;AAAA,EACzG;AACA,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,SAAS,QAAQ;AAC3B,QAAI,IAAI,IAAI,MAAM,EAAE,EAAG,OAAM,IAAI,MAAM,SAAS,WAAW,EAAE,qBAAqB,MAAM,EAAE,UAAU;AACpG,QAAI,IAAI,MAAM,EAAE;AAAA,EACjB;AAOA,QAAM,WAAW,MAAM,QAAQ,cAAc,IAAI,iBAAiB,CAAC,cAAuB;AAC1F,MAAI,SAAS,WAAW,GAAG;AAC1B,UAAM,IAAI,MAAM,SAAS,WAAW,EAAE,4EAA4E;AAAA,EACnH;AAEA,QAAM,WAAW,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AACxD,aAAW,WAAW,UAAU;AAC/B,eAAW,SAAS,QAAQ;AAC3B,UAAI,CAAC,SAAS,IAAI,MAAM,EAAE,EAAG;AAC7B,UAAI;AACH,cAAM,MAAM,IAAI,OAAO;AAAA,MACxB,QAAQ;AACP,iBAAS,OAAO,MAAM,EAAE;AAAA,MACzB;AAAA,IACD;AAAA,EACD;AACA,MAAI,SAAS,OAAO,GAAG;AACtB,UAAM,IAAI;AAAA,MACT,SAAS,WAAW,EAAE,SAAS,SAAS,IAAI,2EAA2E,CAAC,GAAG,QAAQ,EAAE,KAAK,IAAI,CAAC;AAAA,IAChJ;AAAA,EACD;AACD;AAuBA,eAAsB,eAAiC,OAOzB;AAI7B,aAAW,MAAM,UAAU;AAC3B,QAAM,SAAS,MAAM,WAAW,OAAO,MAAM,QAAQ;AACrD,MAAI,OAAO,WAAW,GAAG;AACxB,UAAM,IAAI,MAAM,SAAS,MAAM,WAAW,EAAE,kEAAkE;AAAA,EAC/G;AACA,QAAM,SAAmB,CAAC;AAC1B,aAAW,SAAS,QAAQ;AAC3B,QAAI;AACH,YAAM,MAAM,IAAI,MAAM,OAAO;AAAA,IAC9B,SAAS,OAAO;AACf,YAAM,IAAI;AAAA,QACT,YAAY,MAAM,MAAM,kBAAkB,MAAM,WAAW,EAAE,YAAY,MAAM,EAAE,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,MAC9I;AAAA,IACD;AACA,WAAO,KAAK,MAAM,EAAE;AAAA,EACrB;AACA,SAAO;AAAA,IACN,MAAM,MAAM,WAAW;AAAA,IACvB,QAAQ,MAAM;AAAA,IACd,GAAI,MAAM,YAAY,SAAY,CAAC,IAAI,EAAE,SAAS,MAAM,QAAQ;AAAA,IAChE,GAAI,MAAM,WAAW,WAAW,EAAE,UAAU,MAAM,WAAW,SAAS,IAAI,CAAC;AAAA,IAC3E,eAAe;AAAA,MACd,QAAQ,MAAM,WAAW;AAAA,MACzB,aAAa,MAAM,WAAW;AAAA,MAC9B,QAAQ;AAAA,MACR,cAAc,MAAM,OAAO,oBAAI,KAAK,GAAG,YAAY;AAAA,IACpD;AAAA,EACD;AACD;AAUA,SAAS,eAAe,SAA4B,UAA+C;AAClG,QAAM,gBAAiB,QAA6B;AACpD,MAAI,CAAC,iBAAiB,OAAO,kBAAkB,SAAU,QAAO;AAChE,MAAI,cAAc,WAAW,SAAU,QAAO;AAC9C,MAAI,cAAc,WAAW,QAAQ,KAAM,QAAO;AAClD,MAAI,CAAC,kBAAkB,KAAK,cAAc,eAAe,EAAE,EAAG,QAAO;AACrE,MAAI,CAAC,MAAM,QAAQ,cAAc,MAAM,KAAK,cAAc,OAAO,WAAW,EAAG,QAAO;AACtF,MAAI,OAAO,MAAM,KAAK,MAAM,cAAc,eAAe,EAAE,CAAC,EAAG,QAAO;AACtE,SAAO;AACR;AAkDO,SAAS,yBAAyB,OAchB;AACxB,QAAM,cAAc,IAAI,KAAK,MAAM,eAAe,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC,WAAW,IAAI,UAAU,CAAC,CAAC;AACtG,QAAM,WAA0B,CAAC;AACjC,aAAW,eAAe,MAAM,WAAW,cAAc,SAAS,CAAC,GAAG;AACrE,UAAM,aAAa,MAAM,SAAS,OAAO,CAAC,YAAY,QAAQ,SAAS,YAAY,IAAI;AACvF,QAAI,WAAW,WAAW,GAAG;AAC5B,eAAS,KAAK;AAAA,QACb,MAAM;AAAA,QACN,MAAM,YAAY;AAAA,QAClB,SAAS,eAAe,MAAM,WAAW,SAAS,oBAAoB,YAAY,IAAI;AAAA,MACvF,CAAC;AACD;AAAA,IACD;AAGA,QAAI,WAAW;AACf,QAAI,YAAY,UAAU;AACzB,iBAAW,SAAS,OAAO,CAAC,YAAY,QAAQ,UAAU,SAAS,YAAY,SAAU,IAAI;AAC7F,UAAI,SAAS,WAAW,GAAG;AAC1B,iBAAS,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM,YAAY;AAAA,UAClB,SAAS,SAAS,YAAY,IAAI,iBAAiB,YAAY,SAAS,IAAI,8BAA8B,WAAW,IAAI,CAAC,YAAY,GAAG,QAAQ,MAAM,IAAI,QAAQ,UAAU,QAAQ,aAAa,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,QAClN,CAAC;AACD;AAAA,MACD;AACA,UAAI,YAAY,SAAS,SAAS;AACjC,cAAM,YAAY,SAAS,OAAO,CAAC,YAAY,QAAQ,UAAU,YAAY,YAAY,SAAU,OAAO;AAC1G,YAAI,UAAU,WAAW,GAAG;AAC3B,mBAAS,KAAK;AAAA,YACb,MAAM;AAAA,YACN,MAAM,YAAY;AAAA,YAClB,SAAS,SAAS,YAAY,IAAI,iBAAiB,YAAY,SAAS,IAAI,cAAc,YAAY,SAAS,OAAO,gCAAgC,SAAS,IAAI,CAAC,YAAY,QAAQ,UAAU,WAAW,qBAAqB,EAAE,KAAK,IAAI,CAAC;AAAA,UAC/O,CAAC;AACD;AAAA,QACD;AACA,mBAAW;AAAA,MACZ;AAAA,IACD;AACA,QAAI,YAAY,SAAS;AACxB,YAAM,YAAY,SAAS,OAAO,CAAC,YAAY,QAAQ,YAAY,YAAY,OAAO;AACtF,UAAI,UAAU,WAAW,GAAG;AAC3B,iBAAS,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM,YAAY;AAAA,UAClB,SAAS,SAAS,YAAY,IAAI,uBAAuB,YAAY,OAAO,4BAA4B,SAAS,IAAI,CAAC,YAAY,GAAG,QAAQ,MAAM,IAAI,QAAQ,WAAW,aAAa,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,QACtM,CAAC;AACD;AAAA,MACD;AACA,iBAAW;AAAA,IACZ;AAIA,UAAM,aAAa,YAAY,IAAI,YAAY,IAAI;AACnD,QAAI,MAAM,wBAAwB,YAAY;AAC7C,YAAM,YAAY,SAAS,OAAO,CAAC,YAAY,eAAe,SAAS,YAAY,IAAI,CAAC;AACxF,UAAI,UAAU,WAAW,GAAG;AAC3B,YAAI,MAAM,sBAAsB;AAC/B,mBAAS,KAAK;AAAA,YACb,MAAM;AAAA,YACN,MAAM,YAAY;AAAA,YAClB,SAAS,SAAS,YAAY,IAAI,4BAA4B,SAAS,IAAI,CAAC,YAAY,QAAQ,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA,UACnH,CAAC;AAAA,QACF;AACA;AAAA,MACD;AACA,UAAI,cAAc,CAAC,UAAU,KAAK,CAAC,YAAY,QAAQ,cAAc,gBAAgB,WAAW,OAAO,GAAG;AACzG,iBAAS,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM,YAAY;AAAA,UAClB,SAAS,SAAS,YAAY,IAAI,4BAA4B,WAAW,OAAO,oCAAoC,UAAU,IAAI,CAAC,YAAY,GAAG,QAAQ,MAAM,IAAI,QAAQ,cAAc,WAAW,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,QACpN,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AACA,SAAO,EAAE,OAAO,SAAS,WAAW,GAAG,SAAS;AACjD;AAEO,SAAS,gCAAgC,OAKvC;AACR,QAAM,SAAS,yBAAyB,KAAK;AAC7C,MAAI,OAAO,MAAO;AAClB,QAAM,IAAI,MAAM;AAAA,IACf,0BAA0B,MAAM,WAAW,SAAS;AAAA,IACpD,GAAG,OAAO,SAAS,IAAI,CAAC,YAAY,OAAO,QAAQ,OAAO,EAAE;AAAA,EAC7D,EAAE,KAAK,IAAI,CAAC;AACb;AAWA,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACrB,OAAO;AAC1B;AAEA,SAAS,OAAO,WAAoB,SAAoC;AACvE,MAAI,CAAC,UAAW,OAAM,IAAI,oBAAoB,OAAO;AACtD;AAMO,SAAS,kBAA0C;AACzD,QAAM,aAAa;AACnB,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,eAAe,YAAY,IAAI;AACzD,eAAO,OAAO,UAAU,MAAM,4BAA4B,OAAO,OAAO,KAAK,CAAC,kCAAkC;AAChH;AAAA,UACC,OAAO,WAAW,aAAa,OAAO,WAAW;AAAA,UACjD,yCAAyC,OAAO,MAAM;AAAA,QACvD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,eAAO,QAAQ,MAAM,KAAK,eAAe,YAAY,KAAK,GAAG,UAAU,WAAW,yCAAyC;AAC3H,eAAO,QAAQ,MAAM,KAAK,cAAc,YAAY,UAAU,GAAG,UAAU,UAAU,uCAAuC;AAC5H,eAAO,QAAQ,MAAM,KAAK,cAAc,YAAY,EAAE,GAAG,UAAU,UAAU,uCAAuC;AAAA,MACrH;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,UAAU,EAAE,cAAc,0BAA0B;AAC1D,cAAM,QAAQ,MAAM,KAAK,cAAc,YAAY,YAAY,OAAO;AACtE,cAAM,SAAS,MAAM,KAAK,cAAc,YAAY,YAAY,OAAO;AACvE,eAAO,MAAM,UAAU,OAAO,OAAO,iCAAiC,MAAM,KAAK,WAAW,OAAO,KAAK,GAAG;AAAA,MAC5G;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,KAAK,eAAe,YAAY,KAAK;AAAA,MAC5C;AAAA,IACD;AAAA,EACD;AACD;AAUO,SAAS,mBAAmB,UAIL;AAC7B,QAAM,SAAoC;AAAA,IACzC;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,OAAO,kBAAkB;AACnD,eAAO,WAAW,MAAM,+DAA+D;AAAA,MACxF;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,eAAQ,MAAM,KAAK,OAAO,EAAE,MAAO,MAAM,wCAAwC;AAAA,MAClF;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,OAAO,SAAS,eAAe;AACzD,eAAO,WAAW,MAAM,qCAAqC;AAC7D,eAAO,QAAQ,YAAY,SAAS,SAAS,SAAS,gBAAgB,QAAQ,OAAO,GAAG;AACxF,eAAO,QAAQ,aAAa,SAAS,SAAS,UAAU,iBAAiB,QAAQ,QAAQ,GAAG;AAAA,MAC7F;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,OAAO,SAAS,eAAe;AACzD,eAAO,WAAW,MAAM,qCAAqC;AAC7D,YAAI,CAAC,OAAQ;AACb,eAAO,qBAAqB,SAAS,OAAO,SAAS,GAAG,cAAc,OAAO,SAAS,6BAA6B;AACnH,eAAO,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,SAAS,GAAG,wBAAwB;AAI9F,eAAO,QAAQ,KAAK,OAAO,QAAQ,GAAG,aAAa,OAAO,QAAQ,2CAA2C;AAC7G,eAAO,QAAQ,KAAK,OAAO,SAAS,GAAG,cAAc,OAAO,SAAS,2CAA2C;AAChH,eAAO,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS,GAAG,wBAAwB;AAClG;AAAA,UACC,OAAO,aAAa,iBAAiB,MAAM,QAAQ,OAAO,QAAQ;AAAA,UAClE;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,OAAO,SAAS,eAAe;AACzD,eAAO,WAAW,MAAM,mEAAmE;AAC3F,YAAI,CAAC,OAAQ;AACb,cAAM,aAAa,KAAK,UAAU,MAAM;AACxC;AAAA,UACC,CAAC,WAAW,SAAS,SAAS,eAAe;AAAA,UAC7C;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,eAAe;AACxD,cAAM,SAAS,MAAM,KAAK,OAAO,SAAS,eAAe;AACzD,eAAO,OAAO,YAAY,QAAQ,SAAS,wDAAwD;AAAA,MACpG;AAAA,IACD;AAAA,EACD;AAEA,MAAI,SAAS,sBAAsB,QAAW;AAC7C,WAAO,KAAK;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,oBAAoB,SAAS;AACnC,eAAQ,MAAM,KAAK,OAAO,iBAAiB,MAAO,MAAM,gDAAgD;AAAA,MACzG;AAAA,IACD,CAAC;AAAA,EACF;AAEA,SAAO;AACR;AAGO,SAAS,kBAAkB,UAGL;AAC5B,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,QAAQ,EAAE,GAAG,SAAS,SAAS,KAAK,SAAS,UAAU,CAAC;AAClF,eAAO,WAAW,MAAM,uDAAuD;AAAA,MAChF;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,QAAQ,MAAM,KAAK,QAAQ,SAAS,OAAO;AACjD,cAAM,SAAS,MAAM,KAAK,QAAQ,SAAS,OAAO;AAClD,eAAO,UAAU,QAAQ,WAAW,MAAM,iCAAiC;AAC3E,eAAO,OAAO,aAAa,QAAQ,UAAU,iEAAiE;AAAA,MAC/G;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,QAAQ,SAAS,OAAO;AAClD;AAAA,UACC,CAAC,KAAK,UAAU,UAAU,CAAC,CAAC,EAAE,SAAS,eAAe;AAAA,UACtD;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;AAGO,SAAS,mBAAmB,UAEL;AAC7B,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,YAAI,UAAU;AACd,YAAI;AACH,gBAAM,KAAK,OAAO,EAAE,GAAG,SAAS,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ,QAAQ,QAAQ,KAAK,EAAE,CAAC;AAAA,QAChG,QAAQ;AACP,oBAAU;AAAA,QACX;AACA,eAAO,SAAS,+EAA+E;AAI/F,cAAM,UAAU,MAAM,KAAK,OAAO;AAAA,UACjC,GAAG,SAAS;AAAA,UACZ,gBAAgB,GAAG,SAAS,QAAQ,cAAc;AAAA,QACnD,CAAC;AACD;AAAA,UACC,QAAQ,YAAY;AAAA,UACpB,gCAAgC,QAAQ,OAAO;AAAA,QAChD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,YAAI,UAAU;AACd,YAAI;AACH,gBAAM,KAAK,OAAO,EAAE,GAAG,SAAS,SAAS,gBAAgB,GAAG,CAAC;AAAA,QAC9D,QAAQ;AACP,oBAAU;AAAA,QACX;AACA,eAAO,SAAS,+EAA+E;AAAA,MAChG;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,OAAO;AAChD,cAAM,SAAS,MAAM,KAAK,OAAO,SAAS,OAAO;AACjD;AAAA,UACC,MAAM,YAAY,OAAO;AAAA,UACzB,0BAA0B,MAAM,OAAO,WAAW,OAAO,OAAO;AAAA,QACjE;AAIA;AAAA,UACC,MAAM,YAAY,eAAe,MAAM,sBAAsB;AAAA,UAC7D;AAAA,QACD;AACA;AAAA,UACC,MAAM,sBAAsB,OAAO;AAAA,UACnC;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,OAAO,SAAS,OAAO;AACjD;AAAA,UACC,OAAO,YAAY,eAAe,OAAO,YAAY,YAAY,OAAO,YAAY;AAAA,UACpF,YAAY,OAAO,OAAO;AAAA,QAC3B;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;AAGO,SAAS,iBAAiB,UAEL;AAC3B,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK;AAC9C;AAAA,UACC,OAAO,KAAK,SAAS;AAAA,UACrB;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,MAAM,EAAE,GAAG,SAAS,OAAO,OAAO,EAAE,CAAC;AAC/D,eAAO,OAAO,KAAK,UAAU,GAAG,YAAY,OAAO,KAAK,MAAM,wBAAwB;AAAA,MACvF;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK;AAC9C,mBAAW,OAAO,OAAO,MAAM;AAC9B,iBAAO,OAAO,IAAI,OAAO,YAAY,IAAI,GAAG,SAAS,GAAG,6BAA6B;AACrF;AAAA,YACC,OAAO,KAAK,GAAG,EAAE,MAAM,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,OAAO;AAAA,YAC/D;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;AAGO,SAAS,yBAAyB,UAEL;AACnC,SAAO;AAAA,IACN;AAAA;AAAA;AAAA;AAAA,MAIC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,KAAK,OAAO,SAAS,KAAK;AAChC,cAAM,KAAK,OAAO,SAAS,KAAK;AAAA,MACjC;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,YAAI,UAAU;AACd,YAAI;AACH,gBAAM,KAAK,OAAO,EAAE,GAAG,SAAS,OAAO,SAAS,GAAG,CAAC;AAAA,QACrD,QAAQ;AACP,oBAAU;AAAA,QACX;AACA,eAAO,SAAS,yEAAyE;AAAA,MAC1F;AAAA,IACD;AAAA,EACD;AACD;AAEO,SAAS,uBAAuB,OAA8C;AACpF,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,QAAQ,KAAK;AACvC,eAAO,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,GAAG,UAAU,KAAK,yBAAyB;AAAA,MAC5F;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,mBAAW,SAAS,MAAM,KAAK,QAAQ,KAAK,GAAG;AAC9C;AAAA,YACC,OAAO,MAAM,UAAU,YAAY,CAAC,MAAM,KAAK,MAAM,MAAM,KAAK,CAAC;AAAA,YACjE,UAAU,MAAM,IAAI,sCAAsC,OAAO,MAAM,KAAK,CAAC;AAAA,UAC9E;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,GAAG,IAAI,CAAC,UAAU,MAAM,IAAI;AACnE,eAAO,IAAI,IAAI,KAAK,EAAE,SAAS,MAAM,QAAQ,0CAA0C;AAAA,MACxF;AAAA,IACD;AAAA,EACD;AACD;AAGA,eAAsB,gBAAuB,MAAa,QAAqH;AAC9K,QAAM,WAAmD,CAAC;AAC1D,aAAW,SAAS,QAAQ;AAC3B,QAAI;AACH,YAAM,MAAM,IAAI,IAAI;AAAA,IACrB,SAAS,OAAO;AACf,eAAS,KAAK,EAAE,IAAI,MAAM,IAAI,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IAChG;AAAA,EACD;AACA,SAAO,EAAE,QAAQ,SAAS,WAAW,GAAG,SAAS;AAClD;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -54,6 +54,270 @@ interface DesignTokensPort {
|
|
|
54
54
|
* and what makes a theme file readable.
|
|
55
55
|
*/
|
|
56
56
|
declare function resolveDesignTokens(document: DtcgGroup): ResolvedToken[];
|
|
57
|
+
/**
|
|
58
|
+
* Actor kinds a verified credential can resolve to. This mirrors `ActorType` in
|
|
59
|
+
* `@fabricorg/platform`, restated here so this package keeps zero runtime
|
|
60
|
+
* dependencies. `identityPortChecks` asserts the two stay identical.
|
|
61
|
+
*/
|
|
62
|
+
type IdentityActorType = "natural_person" | "agent" | "system" | "service_account" | "external_system" | "integration";
|
|
63
|
+
declare const IDENTITY_ACTOR_TYPES: readonly IdentityActorType[];
|
|
64
|
+
/**
|
|
65
|
+
* The scopes a credential covers. `"tenant-wide"` is spelled out rather than
|
|
66
|
+
* left as an absent field, so a credential that simply forgot to carry space
|
|
67
|
+
* coverage can never be read as covering everything.
|
|
68
|
+
*/
|
|
69
|
+
type ActorSpaceCoverage = readonly string[] | "tenant-wide";
|
|
70
|
+
/**
|
|
71
|
+
* Identity resolved from a verified credential. Every governed action,
|
|
72
|
+
* projection decision, grant and audit record derives from actor context, so
|
|
73
|
+
* this is the shape a gateway must produce before the platform is called.
|
|
74
|
+
*
|
|
75
|
+
* It carries no credential material. `credentialId` is an opaque, non-secret
|
|
76
|
+
* reference retained for audit, matching the convention used by
|
|
77
|
+
* `authorizationBindingId`.
|
|
78
|
+
*/
|
|
79
|
+
interface ActorClaims {
|
|
80
|
+
/** Stable subject identifier; becomes `actorId` on a governed submission. */
|
|
81
|
+
subject: string;
|
|
82
|
+
actorType: IdentityActorType;
|
|
83
|
+
tenantId: string;
|
|
84
|
+
spaceIds: ActorSpaceCoverage;
|
|
85
|
+
/** Granted scopes, if the issuer expresses authority that way. */
|
|
86
|
+
scopes?: readonly string[];
|
|
87
|
+
issuer: string;
|
|
88
|
+
/** RFC 3339 timestamps. */
|
|
89
|
+
issuedAt: string;
|
|
90
|
+
expiresAt: string;
|
|
91
|
+
/** Opaque, non-secret reference to the presented credential, for audit. */
|
|
92
|
+
credentialId?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Verifies a presented credential and resolves it to actor claims.
|
|
96
|
+
*
|
|
97
|
+
* An implementation returns `null` for any credential it cannot positively
|
|
98
|
+
* verify — expired, malformed, wrong issuer, bad signature. It never returns
|
|
99
|
+
* partially trusted claims, and it never echoes credential material back.
|
|
100
|
+
*/
|
|
101
|
+
interface IdentityPort {
|
|
102
|
+
verify(credential: string): Promise<ActorClaims | null>;
|
|
103
|
+
}
|
|
104
|
+
type ActorScopeRejection = "malformed_claims" | "expired" | "not_yet_valid" | "tenant_mismatch" | "space_not_covered" | "unknown_actor_type";
|
|
105
|
+
declare class ActorScopeError extends Error {
|
|
106
|
+
readonly rejection: ActorScopeRejection;
|
|
107
|
+
readonly name = "ActorScopeError";
|
|
108
|
+
constructor(rejection: ActorScopeRejection, message: string);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Confirm verified claims actually cover the tenant and space being acted on.
|
|
112
|
+
*
|
|
113
|
+
* Verification proves who the caller is. It does not prove they may act here.
|
|
114
|
+
* Calling this before a governed submission is what stops a valid credential
|
|
115
|
+
* for one tenant from being replayed against another.
|
|
116
|
+
*/
|
|
117
|
+
declare function assertActorClaimsCoverScope(claims: ActorClaims, scope: {
|
|
118
|
+
tenantId: string;
|
|
119
|
+
spaceId: string;
|
|
120
|
+
}, now?: Date): void;
|
|
121
|
+
/**
|
|
122
|
+
* The actor fields a governed submission needs, derived from verified claims
|
|
123
|
+
* after {@link assertActorClaimsCoverScope} has accepted them. Taking these
|
|
124
|
+
* from claims rather than from request input is what keeps the audit trail
|
|
125
|
+
* tied to something that was actually proven.
|
|
126
|
+
*/
|
|
127
|
+
declare function actorContextFromClaims(claims: ActorClaims, scope: {
|
|
128
|
+
tenantId: string;
|
|
129
|
+
spaceId: string;
|
|
130
|
+
}, now?: Date): {
|
|
131
|
+
actorId: string;
|
|
132
|
+
actorType: IdentityActorType;
|
|
133
|
+
tenantId: string;
|
|
134
|
+
spaceId: string;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* A resolved piece of authored content.
|
|
138
|
+
*
|
|
139
|
+
* Content is copy and layout, never permission. A content system deciding what
|
|
140
|
+
* a screen says is expected; a content system deciding what a screen may reach
|
|
141
|
+
* is the failure this whole boundary exists to prevent, which is why nothing
|
|
142
|
+
* here carries a capability reference.
|
|
143
|
+
*/
|
|
144
|
+
interface ContentEntry {
|
|
145
|
+
key: string;
|
|
146
|
+
locale: string;
|
|
147
|
+
/** Opaque revision, stable for identical content. Use it for cache keys. */
|
|
148
|
+
revision: string;
|
|
149
|
+
value: JsonLike;
|
|
150
|
+
}
|
|
151
|
+
type JsonLike = string | number | boolean | null | JsonLike[] | {
|
|
152
|
+
[key: string]: JsonLike;
|
|
153
|
+
};
|
|
154
|
+
interface ContentQuery {
|
|
155
|
+
key: string;
|
|
156
|
+
locale: string;
|
|
157
|
+
/** Optional variant, for an experiment arm already enumerated in a release. */
|
|
158
|
+
variant?: string;
|
|
159
|
+
}
|
|
160
|
+
interface ContentPort {
|
|
161
|
+
/** Resolves to `null` when the key is not authored, rather than inventing a default. */
|
|
162
|
+
resolve(query: ContentQuery): Promise<ContentEntry | null>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Money as an integer in the currency's minor unit.
|
|
166
|
+
*
|
|
167
|
+
* Never a float. A payment expressed as 10.1 is a payment that will eventually
|
|
168
|
+
* be off by a cent, and reconciling that costs more than the type ever did.
|
|
169
|
+
*/
|
|
170
|
+
interface MinorUnitAmount {
|
|
171
|
+
/** Integer in the minor unit: 1050 is USD 10.50. */
|
|
172
|
+
amount: number;
|
|
173
|
+
/** ISO 4217 alphabetic code. */
|
|
174
|
+
currency: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The outcome of asking a provider to move money.
|
|
178
|
+
*
|
|
179
|
+
* `ambiguous` is the one that matters and the one most interfaces omit. A
|
|
180
|
+
* timeout tells you nothing about whether the charge landed, and guessing is
|
|
181
|
+
* how a customer gets billed twice. It maps onto the platform's own
|
|
182
|
+
* `AdapterOutcomeKind`, so an ambiguous payment routes to reconciliation with
|
|
183
|
+
* durable evidence instead of being retried.
|
|
184
|
+
*/
|
|
185
|
+
type PaymentOutcome = "succeeded" | "failed" | "ambiguous";
|
|
186
|
+
interface PaymentResult {
|
|
187
|
+
outcome: PaymentOutcome;
|
|
188
|
+
/** The provider's own identifier, for reconciliation. Required when known. */
|
|
189
|
+
providerReference?: string;
|
|
190
|
+
/** Provider-supplied reason, for evidence rather than for branching. */
|
|
191
|
+
reason?: string;
|
|
192
|
+
}
|
|
193
|
+
interface PaymentRequest {
|
|
194
|
+
/** Caller-stable key. The same key must never move money twice. */
|
|
195
|
+
idempotencyKey: string;
|
|
196
|
+
amount: MinorUnitAmount;
|
|
197
|
+
}
|
|
198
|
+
interface PaymentsPort {
|
|
199
|
+
charge(request: PaymentRequest): Promise<PaymentResult>;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* A latency-critical read path, deliberately outside the governed pipeline.
|
|
203
|
+
*
|
|
204
|
+
* Search results are pointers, not authority. An identifier appearing in a
|
|
205
|
+
* result set says the index knows about it, never that this actor may read it,
|
|
206
|
+
* so anything the viewer then opens still goes through `ProjectionHost`.
|
|
207
|
+
*/
|
|
208
|
+
interface SearchHit {
|
|
209
|
+
/** Identifier a governed read can resolve. Never the record itself. */
|
|
210
|
+
id: string;
|
|
211
|
+
score: number;
|
|
212
|
+
}
|
|
213
|
+
interface SearchQuery {
|
|
214
|
+
text: string;
|
|
215
|
+
limit: number;
|
|
216
|
+
/** Narrowing the index can apply cheaply. Never a substitute for authorization. */
|
|
217
|
+
filters?: Record<string, string>;
|
|
218
|
+
}
|
|
219
|
+
interface SearchPort {
|
|
220
|
+
query(query: SearchQuery): Promise<{
|
|
221
|
+
hits: SearchHit[];
|
|
222
|
+
total: number;
|
|
223
|
+
}>;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* An outbound record of something that already happened.
|
|
227
|
+
*
|
|
228
|
+
* A CRM is downstream of the governed pipeline, never upstream of it. It is
|
|
229
|
+
* told; it does not decide. Keeping this one-way is what stops a marketing
|
|
230
|
+
* system becoming a source of business truth.
|
|
231
|
+
*/
|
|
232
|
+
interface CustomerEvent {
|
|
233
|
+
/** Stable key so redelivery does not duplicate the record. */
|
|
234
|
+
eventId: string;
|
|
235
|
+
subjectId: string;
|
|
236
|
+
type: string;
|
|
237
|
+
occurredAt: string;
|
|
238
|
+
attributes?: Record<string, JsonLike>;
|
|
239
|
+
}
|
|
240
|
+
interface CustomerRecordPort {
|
|
241
|
+
record(event: CustomerEvent): Promise<void>;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A port, as a first-class thing rather than a name in a string.
|
|
245
|
+
*
|
|
246
|
+
* Fabric ships a handful of port definitions because they speak open standards
|
|
247
|
+
* and everyone needs them. It cannot ship the rest: an enterprise integrates
|
|
248
|
+
* hundreds of external systems, and a framework that requires a pull request
|
|
249
|
+
* for each one is not a framework. A vertical, a partner or a vendor defines
|
|
250
|
+
* its own port with {@link definePort} and every mechanism here applies to it
|
|
251
|
+
* unchanged — Fabric never learns what the system behind it is.
|
|
252
|
+
*
|
|
253
|
+
* The definition binds an id to the suite that certifies an adapter for it.
|
|
254
|
+
* That binding is the point: without it, "this port is satisfied" only ever
|
|
255
|
+
* meant "somebody registered something claiming to be it".
|
|
256
|
+
*/
|
|
257
|
+
interface PortDefinition<TPort, TFixtures = void> {
|
|
258
|
+
/**
|
|
259
|
+
* Namespaced identifier, for example `fabric.flags` or `acme.docusign`.
|
|
260
|
+
* Namespacing is required so a port defined outside this repository cannot
|
|
261
|
+
* collide with one defined inside it.
|
|
262
|
+
*/
|
|
263
|
+
id: string;
|
|
264
|
+
/**
|
|
265
|
+
* Version of the port contract itself, not of any adapter.
|
|
266
|
+
*
|
|
267
|
+
* Bump it when the interface changes shape. Certification is recorded
|
|
268
|
+
* against a version, so raising it correctly invalidates every adapter
|
|
269
|
+
* certified against the old contract instead of silently carrying them
|
|
270
|
+
* forward.
|
|
271
|
+
*/
|
|
272
|
+
version: string;
|
|
273
|
+
/** The open standard this port speaks, where one exists. */
|
|
274
|
+
standard?: {
|
|
275
|
+
name: string;
|
|
276
|
+
version?: string;
|
|
277
|
+
};
|
|
278
|
+
/** One-line statement of what an adapter behind this port is responsible for. */
|
|
279
|
+
description: string;
|
|
280
|
+
/** The suite every adapter must pass. */
|
|
281
|
+
checks(fixtures: TFixtures): readonly PortCheck<TPort>[];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Declare a port. Validates the definition itself, because a malformed port
|
|
285
|
+
* definition produces adapters that certify against nothing.
|
|
286
|
+
*/
|
|
287
|
+
declare function definePort<TPort, TFixtures = void>(definition: PortDefinition<TPort, TFixtures>): PortDefinition<TPort, TFixtures>;
|
|
288
|
+
/**
|
|
289
|
+
* Confirm a port's suite can actually fail, and that its check ids are unique.
|
|
290
|
+
*
|
|
291
|
+
* A suite whose checks all pass against a deliberately broken adapter certifies
|
|
292
|
+
* nothing while looking rigorous, which is worse than having no suite at all.
|
|
293
|
+
* Run this against a stub that does the wrong thing when you author a port.
|
|
294
|
+
*/
|
|
295
|
+
declare function assertPortSuiteHasTeeth<TPort, TFixtures>(definition: PortDefinition<TPort, TFixtures>, fixtures: TFixtures, brokenAdapters: TPort | readonly TPort[]): Promise<void>;
|
|
296
|
+
/** Evidence that an adapter passed a port's suite, and which contract it passed. */
|
|
297
|
+
interface AdapterCertification {
|
|
298
|
+
portId: string;
|
|
299
|
+
/** Port contract version the adapter was certified against. */
|
|
300
|
+
portVersion: string;
|
|
301
|
+
checks: string[];
|
|
302
|
+
certifiedAt: string;
|
|
303
|
+
}
|
|
304
|
+
interface CertifiedAdapter extends RegisteredAdapter {
|
|
305
|
+
certification: AdapterCertification;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Run a port's suite against an adapter and record what it passed.
|
|
309
|
+
*
|
|
310
|
+
* Registration is a claim; certification is evidence. Deployment gates can then
|
|
311
|
+
* require the second rather than accepting the first.
|
|
312
|
+
*/
|
|
313
|
+
declare function certifyAdapter<TPort, TFixtures>(input: {
|
|
314
|
+
definition: PortDefinition<TPort, TFixtures>;
|
|
315
|
+
fixtures: TFixtures;
|
|
316
|
+
adapter: TPort;
|
|
317
|
+
vendor: string;
|
|
318
|
+
version?: string;
|
|
319
|
+
now?: Date;
|
|
320
|
+
}): Promise<CertifiedAdapter>;
|
|
57
321
|
interface PortRequirement {
|
|
58
322
|
name: string;
|
|
59
323
|
version?: string;
|
|
@@ -79,7 +343,7 @@ interface RegisteredAdapter {
|
|
|
79
343
|
version?: string;
|
|
80
344
|
};
|
|
81
345
|
}
|
|
82
|
-
type PortFindingCode = "unsatisfied_port" | "standard_not_spoken" | "version_mismatch";
|
|
346
|
+
type PortFindingCode = "unsatisfied_port" | "standard_not_spoken" | "version_mismatch" | "uncertified_adapter" | "stale_certification";
|
|
83
347
|
interface PortFinding {
|
|
84
348
|
code: PortFindingCode;
|
|
85
349
|
port: string;
|
|
@@ -100,10 +364,23 @@ interface PortValidationResult {
|
|
|
100
364
|
declare function validatePortRequirements(input: {
|
|
101
365
|
capability: PortRequiringCapability;
|
|
102
366
|
adapters: readonly RegisteredAdapter[];
|
|
367
|
+
/**
|
|
368
|
+
* Port definitions in force. Supplying them checks that whatever
|
|
369
|
+
* certification is present was issued against the current contract version.
|
|
370
|
+
*
|
|
371
|
+
* They do not by themselves require certification to exist: pass
|
|
372
|
+
* `requireCertification` for that. Definitions alone catch a stale
|
|
373
|
+
* certification, not a missing one, and an adapter carrying none passes.
|
|
374
|
+
*/
|
|
375
|
+
definitions?: readonly PortDefinition<never, never>[];
|
|
376
|
+
/** Require certification for every required port. Defaults to false. */
|
|
377
|
+
requireCertification?: boolean;
|
|
103
378
|
}): PortValidationResult;
|
|
104
379
|
declare function assertPortRequirementsSatisfied(input: {
|
|
105
380
|
capability: PortRequiringCapability;
|
|
106
381
|
adapters: readonly RegisteredAdapter[];
|
|
382
|
+
definitions?: readonly PortDefinition<never, never>[];
|
|
383
|
+
requireCertification?: boolean;
|
|
107
384
|
}): void;
|
|
108
385
|
interface PortCheck<TPort> {
|
|
109
386
|
id: string;
|
|
@@ -116,6 +393,36 @@ interface PortCheck<TPort> {
|
|
|
116
393
|
* plus a configuration change rather than a migration.
|
|
117
394
|
*/
|
|
118
395
|
declare function flagsPortChecks(): PortCheck<FlagsPort>[];
|
|
396
|
+
/**
|
|
397
|
+
* The suite an identity adapter must pass before it can stand behind
|
|
398
|
+
* `IdentityPort`. Every check here is a fail-closed property: the cost of
|
|
399
|
+
* getting one wrong is a credential being trusted further than it proves.
|
|
400
|
+
*
|
|
401
|
+
* `validCredential` must be a credential the adapter verifies successfully,
|
|
402
|
+
* and `expected` the claims it should resolve to.
|
|
403
|
+
*/
|
|
404
|
+
declare function identityPortChecks(fixtures: {
|
|
405
|
+
validCredential: string;
|
|
406
|
+
expected: Pick<ActorClaims, "subject" | "tenantId">;
|
|
407
|
+
expiredCredential?: string;
|
|
408
|
+
}): PortCheck<IdentityPort>[];
|
|
409
|
+
/** The suite a content adapter must pass. */
|
|
410
|
+
declare function contentPortChecks(fixtures: {
|
|
411
|
+
present: ContentQuery;
|
|
412
|
+
absentKey: string;
|
|
413
|
+
}): PortCheck<ContentPort>[];
|
|
414
|
+
/** The suite a payments adapter must pass. */
|
|
415
|
+
declare function paymentsPortChecks(fixtures: {
|
|
416
|
+
request: PaymentRequest;
|
|
417
|
+
}): PortCheck<PaymentsPort>[];
|
|
418
|
+
/** The suite a search adapter must pass. */
|
|
419
|
+
declare function searchPortChecks(fixtures: {
|
|
420
|
+
query: SearchQuery;
|
|
421
|
+
}): PortCheck<SearchPort>[];
|
|
422
|
+
/** The suite a customer-record adapter must pass. */
|
|
423
|
+
declare function customerRecordPortChecks(fixtures: {
|
|
424
|
+
event: CustomerEvent;
|
|
425
|
+
}): PortCheck<CustomerRecordPort>[];
|
|
119
426
|
declare function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[];
|
|
120
427
|
/** Runs a contract suite and returns every failure, rather than stopping at the first. */
|
|
121
428
|
declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{
|
|
@@ -126,4 +433,4 @@ declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<
|
|
|
126
433
|
}>;
|
|
127
434
|
}>;
|
|
128
435
|
|
|
129
|
-
export { type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, type PortCheck, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, assertPortRequirementsSatisfied, designTokensPortChecks, flagsPortChecks, resolveDesignTokens, runPortContract, validatePortRequirements };
|
|
436
|
+
export { type ActorClaims, ActorScopeError, type ActorScopeRejection, type ActorSpaceCoverage, type AdapterCertification, type CertifiedAdapter, type ContentEntry, type ContentPort, type ContentQuery, type CustomerEvent, type CustomerRecordPort, type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, IDENTITY_ACTOR_TYPES, type IdentityActorType, type IdentityPort, type JsonLike, type MinorUnitAmount, type PaymentOutcome, type PaymentRequest, type PaymentResult, type PaymentsPort, type PortCheck, type PortDefinition, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, type SearchHit, type SearchPort, type SearchQuery, actorContextFromClaims, assertActorClaimsCoverScope, assertPortRequirementsSatisfied, assertPortSuiteHasTeeth, certifyAdapter, contentPortChecks, customerRecordPortChecks, definePort, designTokensPortChecks, flagsPortChecks, identityPortChecks, paymentsPortChecks, resolveDesignTokens, runPortContract, searchPortChecks, validatePortRequirements };
|