@fabricorg/ports 0.3.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/dist/index.js.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// ── 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// ── 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\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\texpect(!Number.isNaN(Date.parse(claims.issuedAt)), \"issuedAt is not a parseable timestamp\");\n\t\t\t\texpect(!Number.isNaN(Date.parse(claims.expiresAt)), \"expiresAt is not a parseable timestamp\");\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\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\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":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabricorg/ports",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Vendor-neutral port interfaces, a DTCG token resolver, and the contract-test kit every adapter must pass.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,6 +17,16 @@
17
17
  "types": "./dist/index.d.cts",
18
18
  "default": "./dist/index.cjs"
19
19
  }
20
+ },
21
+ "./catalog": {
22
+ "import": {
23
+ "types": "./dist/catalog.d.ts",
24
+ "default": "./dist/catalog.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/catalog.d.cts",
28
+ "default": "./dist/catalog.cjs"
29
+ }
20
30
  }
21
31
  },
22
32
  "files": [
@@ -30,7 +40,7 @@
30
40
  "tsup": "^8.5.0",
31
41
  "typescript": "^5.7.3",
32
42
  "vitest": "^4.1.5",
33
- "@fabricorg/platform": "1.2.0"
43
+ "@fabricorg/platform": "1.3.0"
34
44
  },
35
45
  "publishConfig": {
36
46
  "access": "public"