@fabricorg/ports 0.2.0 → 0.3.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 +39 -0
- package/README.md +17 -1
- package/dist/index.cjs +162 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -1
- package/dist/index.d.ts +93 -1
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @fabricorg/ports
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 89982ba: Add `IdentityPort`, the OAuth2/OIDC-shaped seam a gateway implements to turn a presented credential
|
|
8
|
+
into verified `ActorClaims`. Every governed action, projection decision, grant and audit record
|
|
9
|
+
derives from actor context, which until now the platform accepted on trust from its caller.
|
|
10
|
+
|
|
11
|
+
`assertActorClaimsCoverScope` enforces the half that verification does not: a credential proves who
|
|
12
|
+
the caller is, never that they may act in the tenant and space a request names. It refuses expired,
|
|
13
|
+
not-yet-valid, unparseable, cross-tenant and out-of-space claims, and `actorContextFromClaims`
|
|
14
|
+
derives submission fields only from claims that passed. Space coverage is spelled `"tenant-wide"`
|
|
15
|
+
rather than left absent, so claims that simply omit coverage can never be read as covering
|
|
16
|
+
everything. `identityPortChecks` is the suite an adapter must pass, including that resolved claims
|
|
17
|
+
never echo credential material back.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 6f5471b: Close the gaps an independent review found in the grant, remote and identity work.
|
|
22
|
+
|
|
23
|
+
A capability reference object was treated as a leaf, so a granted reference could shield ungranted
|
|
24
|
+
ones parked beside it on the same object; sibling keys are now walked. Prop recursion is depth
|
|
25
|
+
bounded, so cyclic programmatic input reports a finding instead of exhausting the stack. Federated
|
|
26
|
+
remotes now compare their exposed-module mapping, not only entry and integrity, and the locked remote
|
|
27
|
+
is carried onto the promoted release so two releases differing only in their remote cannot share a
|
|
28
|
+
digest. Pack lookup matches full pack identity rather than namespace and name alone, and a stored
|
|
29
|
+
lockfile that declares one pack or capability twice is rejected rather than resolved by first match.
|
|
30
|
+
|
|
31
|
+
Grant digests omit `variants` and `fragments` when absent, so a release that declares neither digests
|
|
32
|
+
exactly as it did before those fields existed and previously promoted releases keep verifying.
|
|
33
|
+
Variants are hashed in id order, so reordering the same set no longer churns the digest. Fragment
|
|
34
|
+
grants written as a `{ __proto__: … }` object literal are rejected, because that sets the prototype
|
|
35
|
+
rather than creating the entry and the narrowing would silently disappear.
|
|
36
|
+
|
|
37
|
+
`assertActorClaimsCoverScope` no longer fails open: an invalid clock is rejected instead of making
|
|
38
|
+
every temporal comparison false, timestamps must be RFC 3339 with an explicit offset so a credential
|
|
39
|
+
cannot expire at different instants on different hosts, and `spaceIds` must be an array of strings so
|
|
40
|
+
a bare string cannot reach substring matching where `"space_10"` would cover `"space_1"`.
|
|
41
|
+
|
|
3
42
|
## 0.2.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,7 +10,23 @@ pnpm add @fabricorg/ports
|
|
|
10
10
|
|
|
11
11
|
## Where a standard exists, the port speaks it
|
|
12
12
|
|
|
13
|
-
`FlagsPort` is structurally compatible with an OpenFeature provider's evaluation surface, so an OpenFeature provider is a thin adapter rather than a translation layer. `DesignTokensPort` consumes W3C DTCG documents, so a design tool is one exporter among any.
|
|
13
|
+
`FlagsPort` is structurally compatible with an OpenFeature provider's evaluation surface, so an OpenFeature provider is a thin adapter rather than a translation layer. `DesignTokensPort` consumes W3C DTCG documents, so a design tool is one exporter among any. `IdentityPort` is OAuth2/OIDC-shaped, so a gateway that already validates JWTs implements it without a second identity model.
|
|
14
|
+
|
|
15
|
+
## Identity is where the platform stops trusting its caller
|
|
16
|
+
|
|
17
|
+
Every governed action, projection decision, grant and audit record derives from actor context. `IdentityPort.verify` turns a presented credential into `ActorClaims`, and returns `null` for anything it cannot positively verify rather than partially trusted claims.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { actorContextFromClaims } from "@fabricorg/ports";
|
|
21
|
+
|
|
22
|
+
const claims = await identity.verify(credential);
|
|
23
|
+
if (!claims) throw new Error("unauthenticated");
|
|
24
|
+
|
|
25
|
+
// Refuses expired, not-yet-valid, cross-tenant and out-of-space claims.
|
|
26
|
+
const actor = actorContextFromClaims(claims, { tenantId, spaceId });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Verification proves who the caller is. `assertActorClaimsCoverScope` enforces the half it does not: that they may act in the tenant and space this request names. A credential valid for one tenant replayed against another is refused there, not later. Space coverage is spelled `"tenant-wide"` rather than left absent, so claims that simply omit coverage can never be read as covering everything.
|
|
14
30
|
|
|
15
31
|
Nothing here imports a vendor SDK. A port is a shape; an adapter is anything that satisfies it.
|
|
16
32
|
|
package/dist/index.cjs
CHANGED
|
@@ -20,9 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ActorScopeError: () => ActorScopeError,
|
|
24
|
+
IDENTITY_ACTOR_TYPES: () => IDENTITY_ACTOR_TYPES,
|
|
25
|
+
actorContextFromClaims: () => actorContextFromClaims,
|
|
26
|
+
assertActorClaimsCoverScope: () => assertActorClaimsCoverScope,
|
|
23
27
|
assertPortRequirementsSatisfied: () => assertPortRequirementsSatisfied,
|
|
24
28
|
designTokensPortChecks: () => designTokensPortChecks,
|
|
25
29
|
flagsPortChecks: () => flagsPortChecks,
|
|
30
|
+
identityPortChecks: () => identityPortChecks,
|
|
26
31
|
resolveDesignTokens: () => resolveDesignTokens,
|
|
27
32
|
runPortContract: () => runPortContract,
|
|
28
33
|
validatePortRequirements: () => validatePortRequirements
|
|
@@ -81,6 +86,79 @@ function resolveDesignTokens(document) {
|
|
|
81
86
|
};
|
|
82
87
|
});
|
|
83
88
|
}
|
|
89
|
+
var IDENTITY_ACTOR_TYPES = [
|
|
90
|
+
"natural_person",
|
|
91
|
+
"agent",
|
|
92
|
+
"system",
|
|
93
|
+
"service_account",
|
|
94
|
+
"external_system",
|
|
95
|
+
"integration"
|
|
96
|
+
];
|
|
97
|
+
var ActorScopeError = class extends Error {
|
|
98
|
+
constructor(rejection, message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.rejection = rejection;
|
|
101
|
+
}
|
|
102
|
+
rejection;
|
|
103
|
+
name = "ActorScopeError";
|
|
104
|
+
};
|
|
105
|
+
function assertActorClaimsCoverScope(claims, scope, now = /* @__PURE__ */ new Date()) {
|
|
106
|
+
const currentTime = now.getTime();
|
|
107
|
+
if (Number.isNaN(currentTime)) {
|
|
108
|
+
throw new ActorScopeError("malformed_claims", "Actor scope was evaluated against an invalid clock.");
|
|
109
|
+
}
|
|
110
|
+
if (typeof claims.actorType !== "string" || !IDENTITY_ACTOR_TYPES.includes(claims.actorType)) {
|
|
111
|
+
throw new ActorScopeError("unknown_actor_type", `Actor type "${String(claims.actorType)}" is not a known actor kind.`);
|
|
112
|
+
}
|
|
113
|
+
if (typeof claims.subject !== "string" || claims.subject.length === 0) {
|
|
114
|
+
throw new ActorScopeError("malformed_claims", "Actor claims carry no subject.");
|
|
115
|
+
}
|
|
116
|
+
if (typeof claims.tenantId !== "string" || claims.tenantId.length === 0) {
|
|
117
|
+
throw new ActorScopeError("malformed_claims", "Actor claims carry no tenant.");
|
|
118
|
+
}
|
|
119
|
+
const issuedAt = parseRfc3339(claims.issuedAt);
|
|
120
|
+
const expiresAt = parseRfc3339(claims.expiresAt);
|
|
121
|
+
if (issuedAt === void 0 || expiresAt === void 0) {
|
|
122
|
+
throw new ActorScopeError("malformed_claims", "Actor claims carry an issuedAt or expiresAt that is not an RFC 3339 timestamp with an explicit offset.");
|
|
123
|
+
}
|
|
124
|
+
if (currentTime >= expiresAt) {
|
|
125
|
+
throw new ActorScopeError("expired", `Actor claims expired at ${claims.expiresAt}.`);
|
|
126
|
+
}
|
|
127
|
+
if (currentTime < issuedAt) {
|
|
128
|
+
throw new ActorScopeError("not_yet_valid", `Actor claims are not valid until ${claims.issuedAt}.`);
|
|
129
|
+
}
|
|
130
|
+
if (claims.tenantId !== scope.tenantId) {
|
|
131
|
+
throw new ActorScopeError(
|
|
132
|
+
"tenant_mismatch",
|
|
133
|
+
`Actor claims cover tenant "${claims.tenantId}" but the request targets "${scope.tenantId}".`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (claims.spaceIds === "tenant-wide") return;
|
|
137
|
+
if (!Array.isArray(claims.spaceIds) || !claims.spaceIds.every((space) => typeof space === "string")) {
|
|
138
|
+
throw new ActorScopeError("malformed_claims", 'Actor claims spaceIds must be an array of strings or the literal "tenant-wide".');
|
|
139
|
+
}
|
|
140
|
+
if (!claims.spaceIds.includes(scope.spaceId)) {
|
|
141
|
+
throw new ActorScopeError(
|
|
142
|
+
"space_not_covered",
|
|
143
|
+
`Actor claims do not cover space "${scope.spaceId}".`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
148
|
+
function parseRfc3339(value) {
|
|
149
|
+
if (typeof value !== "string" || !RFC3339.test(value)) return void 0;
|
|
150
|
+
const parsed = Date.parse(value);
|
|
151
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
152
|
+
}
|
|
153
|
+
function actorContextFromClaims(claims, scope, now = /* @__PURE__ */ new Date()) {
|
|
154
|
+
assertActorClaimsCoverScope(claims, scope, now);
|
|
155
|
+
return {
|
|
156
|
+
actorId: claims.subject,
|
|
157
|
+
actorType: claims.actorType,
|
|
158
|
+
tenantId: claims.tenantId,
|
|
159
|
+
spaceId: scope.spaceId
|
|
160
|
+
};
|
|
161
|
+
}
|
|
84
162
|
function validatePortRequirements(input) {
|
|
85
163
|
const findings = [];
|
|
86
164
|
for (const requirement of input.capability.requirements?.ports ?? []) {
|
|
@@ -171,6 +249,85 @@ function flagsPortChecks() {
|
|
|
171
249
|
}
|
|
172
250
|
];
|
|
173
251
|
}
|
|
252
|
+
function identityPortChecks(fixtures) {
|
|
253
|
+
const checks = [
|
|
254
|
+
{
|
|
255
|
+
id: "identity.rejects-garbage",
|
|
256
|
+
title: "an unverifiable credential resolves to null rather than partial claims",
|
|
257
|
+
async run(port) {
|
|
258
|
+
const result = await port.verify("not-a-credential");
|
|
259
|
+
expect(result === null, "an unverifiable credential resolved to claims instead of null");
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "identity.rejects-empty",
|
|
264
|
+
title: "an empty credential resolves to null",
|
|
265
|
+
async run(port) {
|
|
266
|
+
expect(await port.verify("") === null, "an empty credential resolved to claims");
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "identity.resolves-valid",
|
|
271
|
+
title: "a valid credential resolves to the expected subject and tenant",
|
|
272
|
+
async run(port) {
|
|
273
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
274
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
275
|
+
expect(claims?.subject === fixtures.expected.subject, `subject was "${claims?.subject}"`);
|
|
276
|
+
expect(claims?.tenantId === fixtures.expected.tenantId, `tenantId was "${claims?.tenantId}"`);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "identity.claims-are-complete",
|
|
281
|
+
title: "resolved claims carry every field the platform derives actor context from",
|
|
282
|
+
async run(port) {
|
|
283
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
284
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
285
|
+
if (!claims) return;
|
|
286
|
+
expect(IDENTITY_ACTOR_TYPES.includes(claims.actorType), `actorType "${claims.actorType}" is not a known actor kind`);
|
|
287
|
+
expect(typeof claims.issuer === "string" && claims.issuer.length > 0, "claims carry no issuer");
|
|
288
|
+
expect(!Number.isNaN(Date.parse(claims.issuedAt)), "issuedAt is not a parseable timestamp");
|
|
289
|
+
expect(!Number.isNaN(Date.parse(claims.expiresAt)), "expiresAt is not a parseable timestamp");
|
|
290
|
+
expect(
|
|
291
|
+
claims.spaceIds === "tenant-wide" || Array.isArray(claims.spaceIds),
|
|
292
|
+
'spaceIds must be an explicit list or the literal "tenant-wide"'
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: "identity.carries-no-credential-material",
|
|
298
|
+
title: "resolved claims never echo the credential back",
|
|
299
|
+
async run(port) {
|
|
300
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
301
|
+
if (!claims) return;
|
|
302
|
+
const serialized = JSON.stringify(claims);
|
|
303
|
+
expect(
|
|
304
|
+
!serialized.includes(fixtures.validCredential),
|
|
305
|
+
"resolved claims contain the presented credential; claims must carry an opaque reference instead"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "identity.verification-is-stable",
|
|
311
|
+
title: "the same credential resolves the same subject twice",
|
|
312
|
+
async run(port) {
|
|
313
|
+
const first = await port.verify(fixtures.validCredential);
|
|
314
|
+
const second = await port.verify(fixtures.validCredential);
|
|
315
|
+
expect(first?.subject === second?.subject, "the same credential resolved to two different subjects");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
if (fixtures.expiredCredential !== void 0) {
|
|
320
|
+
checks.push({
|
|
321
|
+
id: "identity.rejects-expired",
|
|
322
|
+
title: "an expired credential resolves to null rather than stale claims",
|
|
323
|
+
async run(port) {
|
|
324
|
+
const expiredCredential = fixtures.expiredCredential;
|
|
325
|
+
expect(await port.verify(expiredCredential) === null, "an expired credential still resolved to claims");
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return checks;
|
|
330
|
+
}
|
|
174
331
|
function designTokensPortChecks(theme) {
|
|
175
332
|
return [
|
|
176
333
|
{
|
|
@@ -216,9 +373,14 @@ async function runPortContract(port, checks) {
|
|
|
216
373
|
}
|
|
217
374
|
// Annotate the CommonJS export names for ESM import in node:
|
|
218
375
|
0 && (module.exports = {
|
|
376
|
+
ActorScopeError,
|
|
377
|
+
IDENTITY_ACTOR_TYPES,
|
|
378
|
+
actorContextFromClaims,
|
|
379
|
+
assertActorClaimsCoverScope,
|
|
219
380
|
assertPortRequirementsSatisfied,
|
|
220
381
|
designTokensPortChecks,
|
|
221
382
|
flagsPortChecks,
|
|
383
|
+
identityPortChecks,
|
|
222
384
|
resolveDesignTokens,
|
|
223
385
|
runPortContract,
|
|
224
386
|
validatePortRequirements
|
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// ── 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":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -54,6 +54,85 @@ 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
|
+
};
|
|
57
136
|
interface PortRequirement {
|
|
58
137
|
name: string;
|
|
59
138
|
version?: string;
|
|
@@ -116,6 +195,19 @@ interface PortCheck<TPort> {
|
|
|
116
195
|
* plus a configuration change rather than a migration.
|
|
117
196
|
*/
|
|
118
197
|
declare function flagsPortChecks(): PortCheck<FlagsPort>[];
|
|
198
|
+
/**
|
|
199
|
+
* The suite an identity adapter must pass before it can stand behind
|
|
200
|
+
* `IdentityPort`. Every check here is a fail-closed property: the cost of
|
|
201
|
+
* getting one wrong is a credential being trusted further than it proves.
|
|
202
|
+
*
|
|
203
|
+
* `validCredential` must be a credential the adapter verifies successfully,
|
|
204
|
+
* and `expected` the claims it should resolve to.
|
|
205
|
+
*/
|
|
206
|
+
declare function identityPortChecks(fixtures: {
|
|
207
|
+
validCredential: string;
|
|
208
|
+
expected: Pick<ActorClaims, "subject" | "tenantId">;
|
|
209
|
+
expiredCredential?: string;
|
|
210
|
+
}): PortCheck<IdentityPort>[];
|
|
119
211
|
declare function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[];
|
|
120
212
|
/** Runs a contract suite and returns every failure, rather than stopping at the first. */
|
|
121
213
|
declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{
|
|
@@ -126,4 +218,4 @@ declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<
|
|
|
126
218
|
}>;
|
|
127
219
|
}>;
|
|
128
220
|
|
|
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 };
|
|
221
|
+
export { type ActorClaims, ActorScopeError, type ActorScopeRejection, type ActorSpaceCoverage, type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, IDENTITY_ACTOR_TYPES, type IdentityActorType, type IdentityPort, type PortCheck, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, actorContextFromClaims, assertActorClaimsCoverScope, assertPortRequirementsSatisfied, designTokensPortChecks, flagsPortChecks, identityPortChecks, resolveDesignTokens, runPortContract, validatePortRequirements };
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,85 @@ 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
|
+
};
|
|
57
136
|
interface PortRequirement {
|
|
58
137
|
name: string;
|
|
59
138
|
version?: string;
|
|
@@ -116,6 +195,19 @@ interface PortCheck<TPort> {
|
|
|
116
195
|
* plus a configuration change rather than a migration.
|
|
117
196
|
*/
|
|
118
197
|
declare function flagsPortChecks(): PortCheck<FlagsPort>[];
|
|
198
|
+
/**
|
|
199
|
+
* The suite an identity adapter must pass before it can stand behind
|
|
200
|
+
* `IdentityPort`. Every check here is a fail-closed property: the cost of
|
|
201
|
+
* getting one wrong is a credential being trusted further than it proves.
|
|
202
|
+
*
|
|
203
|
+
* `validCredential` must be a credential the adapter verifies successfully,
|
|
204
|
+
* and `expected` the claims it should resolve to.
|
|
205
|
+
*/
|
|
206
|
+
declare function identityPortChecks(fixtures: {
|
|
207
|
+
validCredential: string;
|
|
208
|
+
expected: Pick<ActorClaims, "subject" | "tenantId">;
|
|
209
|
+
expiredCredential?: string;
|
|
210
|
+
}): PortCheck<IdentityPort>[];
|
|
119
211
|
declare function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[];
|
|
120
212
|
/** Runs a contract suite and returns every failure, rather than stopping at the first. */
|
|
121
213
|
declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{
|
|
@@ -126,4 +218,4 @@ declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<
|
|
|
126
218
|
}>;
|
|
127
219
|
}>;
|
|
128
220
|
|
|
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 };
|
|
221
|
+
export { type ActorClaims, ActorScopeError, type ActorScopeRejection, type ActorSpaceCoverage, type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, IDENTITY_ACTOR_TYPES, type IdentityActorType, type IdentityPort, type PortCheck, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, actorContextFromClaims, assertActorClaimsCoverScope, assertPortRequirementsSatisfied, designTokensPortChecks, flagsPortChecks, identityPortChecks, resolveDesignTokens, runPortContract, validatePortRequirements };
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,79 @@ function resolveDesignTokens(document) {
|
|
|
52
52
|
};
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
+
var IDENTITY_ACTOR_TYPES = [
|
|
56
|
+
"natural_person",
|
|
57
|
+
"agent",
|
|
58
|
+
"system",
|
|
59
|
+
"service_account",
|
|
60
|
+
"external_system",
|
|
61
|
+
"integration"
|
|
62
|
+
];
|
|
63
|
+
var ActorScopeError = class extends Error {
|
|
64
|
+
constructor(rejection, message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.rejection = rejection;
|
|
67
|
+
}
|
|
68
|
+
rejection;
|
|
69
|
+
name = "ActorScopeError";
|
|
70
|
+
};
|
|
71
|
+
function assertActorClaimsCoverScope(claims, scope, now = /* @__PURE__ */ new Date()) {
|
|
72
|
+
const currentTime = now.getTime();
|
|
73
|
+
if (Number.isNaN(currentTime)) {
|
|
74
|
+
throw new ActorScopeError("malformed_claims", "Actor scope was evaluated against an invalid clock.");
|
|
75
|
+
}
|
|
76
|
+
if (typeof claims.actorType !== "string" || !IDENTITY_ACTOR_TYPES.includes(claims.actorType)) {
|
|
77
|
+
throw new ActorScopeError("unknown_actor_type", `Actor type "${String(claims.actorType)}" is not a known actor kind.`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof claims.subject !== "string" || claims.subject.length === 0) {
|
|
80
|
+
throw new ActorScopeError("malformed_claims", "Actor claims carry no subject.");
|
|
81
|
+
}
|
|
82
|
+
if (typeof claims.tenantId !== "string" || claims.tenantId.length === 0) {
|
|
83
|
+
throw new ActorScopeError("malformed_claims", "Actor claims carry no tenant.");
|
|
84
|
+
}
|
|
85
|
+
const issuedAt = parseRfc3339(claims.issuedAt);
|
|
86
|
+
const expiresAt = parseRfc3339(claims.expiresAt);
|
|
87
|
+
if (issuedAt === void 0 || expiresAt === void 0) {
|
|
88
|
+
throw new ActorScopeError("malformed_claims", "Actor claims carry an issuedAt or expiresAt that is not an RFC 3339 timestamp with an explicit offset.");
|
|
89
|
+
}
|
|
90
|
+
if (currentTime >= expiresAt) {
|
|
91
|
+
throw new ActorScopeError("expired", `Actor claims expired at ${claims.expiresAt}.`);
|
|
92
|
+
}
|
|
93
|
+
if (currentTime < issuedAt) {
|
|
94
|
+
throw new ActorScopeError("not_yet_valid", `Actor claims are not valid until ${claims.issuedAt}.`);
|
|
95
|
+
}
|
|
96
|
+
if (claims.tenantId !== scope.tenantId) {
|
|
97
|
+
throw new ActorScopeError(
|
|
98
|
+
"tenant_mismatch",
|
|
99
|
+
`Actor claims cover tenant "${claims.tenantId}" but the request targets "${scope.tenantId}".`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (claims.spaceIds === "tenant-wide") return;
|
|
103
|
+
if (!Array.isArray(claims.spaceIds) || !claims.spaceIds.every((space) => typeof space === "string")) {
|
|
104
|
+
throw new ActorScopeError("malformed_claims", 'Actor claims spaceIds must be an array of strings or the literal "tenant-wide".');
|
|
105
|
+
}
|
|
106
|
+
if (!claims.spaceIds.includes(scope.spaceId)) {
|
|
107
|
+
throw new ActorScopeError(
|
|
108
|
+
"space_not_covered",
|
|
109
|
+
`Actor claims do not cover space "${scope.spaceId}".`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
114
|
+
function parseRfc3339(value) {
|
|
115
|
+
if (typeof value !== "string" || !RFC3339.test(value)) return void 0;
|
|
116
|
+
const parsed = Date.parse(value);
|
|
117
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
118
|
+
}
|
|
119
|
+
function actorContextFromClaims(claims, scope, now = /* @__PURE__ */ new Date()) {
|
|
120
|
+
assertActorClaimsCoverScope(claims, scope, now);
|
|
121
|
+
return {
|
|
122
|
+
actorId: claims.subject,
|
|
123
|
+
actorType: claims.actorType,
|
|
124
|
+
tenantId: claims.tenantId,
|
|
125
|
+
spaceId: scope.spaceId
|
|
126
|
+
};
|
|
127
|
+
}
|
|
55
128
|
function validatePortRequirements(input) {
|
|
56
129
|
const findings = [];
|
|
57
130
|
for (const requirement of input.capability.requirements?.ports ?? []) {
|
|
@@ -142,6 +215,85 @@ function flagsPortChecks() {
|
|
|
142
215
|
}
|
|
143
216
|
];
|
|
144
217
|
}
|
|
218
|
+
function identityPortChecks(fixtures) {
|
|
219
|
+
const checks = [
|
|
220
|
+
{
|
|
221
|
+
id: "identity.rejects-garbage",
|
|
222
|
+
title: "an unverifiable credential resolves to null rather than partial claims",
|
|
223
|
+
async run(port) {
|
|
224
|
+
const result = await port.verify("not-a-credential");
|
|
225
|
+
expect(result === null, "an unverifiable credential resolved to claims instead of null");
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "identity.rejects-empty",
|
|
230
|
+
title: "an empty credential resolves to null",
|
|
231
|
+
async run(port) {
|
|
232
|
+
expect(await port.verify("") === null, "an empty credential resolved to claims");
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "identity.resolves-valid",
|
|
237
|
+
title: "a valid credential resolves to the expected subject and tenant",
|
|
238
|
+
async run(port) {
|
|
239
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
240
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
241
|
+
expect(claims?.subject === fixtures.expected.subject, `subject was "${claims?.subject}"`);
|
|
242
|
+
expect(claims?.tenantId === fixtures.expected.tenantId, `tenantId was "${claims?.tenantId}"`);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "identity.claims-are-complete",
|
|
247
|
+
title: "resolved claims carry every field the platform derives actor context from",
|
|
248
|
+
async run(port) {
|
|
249
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
250
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
251
|
+
if (!claims) return;
|
|
252
|
+
expect(IDENTITY_ACTOR_TYPES.includes(claims.actorType), `actorType "${claims.actorType}" is not a known actor kind`);
|
|
253
|
+
expect(typeof claims.issuer === "string" && claims.issuer.length > 0, "claims carry no issuer");
|
|
254
|
+
expect(!Number.isNaN(Date.parse(claims.issuedAt)), "issuedAt is not a parseable timestamp");
|
|
255
|
+
expect(!Number.isNaN(Date.parse(claims.expiresAt)), "expiresAt is not a parseable timestamp");
|
|
256
|
+
expect(
|
|
257
|
+
claims.spaceIds === "tenant-wide" || Array.isArray(claims.spaceIds),
|
|
258
|
+
'spaceIds must be an explicit list or the literal "tenant-wide"'
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "identity.carries-no-credential-material",
|
|
264
|
+
title: "resolved claims never echo the credential back",
|
|
265
|
+
async run(port) {
|
|
266
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
267
|
+
if (!claims) return;
|
|
268
|
+
const serialized = JSON.stringify(claims);
|
|
269
|
+
expect(
|
|
270
|
+
!serialized.includes(fixtures.validCredential),
|
|
271
|
+
"resolved claims contain the presented credential; claims must carry an opaque reference instead"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "identity.verification-is-stable",
|
|
277
|
+
title: "the same credential resolves the same subject twice",
|
|
278
|
+
async run(port) {
|
|
279
|
+
const first = await port.verify(fixtures.validCredential);
|
|
280
|
+
const second = await port.verify(fixtures.validCredential);
|
|
281
|
+
expect(first?.subject === second?.subject, "the same credential resolved to two different subjects");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
];
|
|
285
|
+
if (fixtures.expiredCredential !== void 0) {
|
|
286
|
+
checks.push({
|
|
287
|
+
id: "identity.rejects-expired",
|
|
288
|
+
title: "an expired credential resolves to null rather than stale claims",
|
|
289
|
+
async run(port) {
|
|
290
|
+
const expiredCredential = fixtures.expiredCredential;
|
|
291
|
+
expect(await port.verify(expiredCredential) === null, "an expired credential still resolved to claims");
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return checks;
|
|
296
|
+
}
|
|
145
297
|
function designTokensPortChecks(theme) {
|
|
146
298
|
return [
|
|
147
299
|
{
|
|
@@ -186,9 +338,14 @@ async function runPortContract(port, checks) {
|
|
|
186
338
|
return { passed: failures.length === 0, failures };
|
|
187
339
|
}
|
|
188
340
|
export {
|
|
341
|
+
ActorScopeError,
|
|
342
|
+
IDENTITY_ACTOR_TYPES,
|
|
343
|
+
actorContextFromClaims,
|
|
344
|
+
assertActorClaimsCoverScope,
|
|
189
345
|
assertPortRequirementsSatisfied,
|
|
190
346
|
designTokensPortChecks,
|
|
191
347
|
flagsPortChecks,
|
|
348
|
+
identityPortChecks,
|
|
192
349
|
resolveDesignTokens,
|
|
193
350
|
runPortContract,
|
|
194
351
|
validatePortRequirements
|
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// ── 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// ── 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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabricorg/ports",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"@types/node": "^22.10.0",
|
|
30
30
|
"tsup": "^8.5.0",
|
|
31
31
|
"typescript": "^5.7.3",
|
|
32
|
-
"vitest": "^4.1.5"
|
|
32
|
+
"vitest": "^4.1.5",
|
|
33
|
+
"@fabricorg/platform": "1.2.0"
|
|
33
34
|
},
|
|
34
35
|
"publishConfig": {
|
|
35
36
|
"access": "public"
|