@fabricorg/ports 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +166 -0
- package/README.md +17 -1
- package/dist/catalog.cjs +457 -0
- package/dist/catalog.cjs.map +1 -0
- package/dist/catalog.d.cts +53 -0
- package/dist/catalog.d.ts +53 -0
- package/dist/catalog.js +77 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunk-FH6OL7JX.js +657 -0
- package/dist/chunk-FH6OL7JX.js.map +1 -0
- package/dist/index.cjs +480 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +309 -2
- package/dist/index.d.ts +309 -2
- package/dist/index.js +32 -187
- package/dist/index.js.map +1 -1
- package/package.json +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,171 @@
|
|
|
1
1
|
# @fabricorg/ports
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8ce5e06: Derive release grants from what an actor holds, and complete the port catalog.
|
|
8
|
+
|
|
9
|
+
`deriveAuthorizationGrants` turns usage contracts plus an actor's permissions, entitlements and
|
|
10
|
+
feature flags into an `SduiAuthorizationGrants`. A shell that answers "what may this actor see" from
|
|
11
|
+
its own configuration, while grants are hand-maintained elsewhere and enforcement happens in a third
|
|
12
|
+
place, produces three sources that must agree with nothing forcing them to. Deriving them makes
|
|
13
|
+
navigation a projection of the same declarations. It is navigation-shaped rather than enforcement:
|
|
14
|
+
the Host still re-checks authority at execution and `ProjectionHost` still authorizes every query.
|
|
15
|
+
Withheld capabilities are returned with the requirement that was missing, so an absent entry is
|
|
16
|
+
explainable.
|
|
17
|
+
|
|
18
|
+
`ContentPort`, `PaymentsPort`, `SearchPort` and `CustomerRecordPort` join flags, design tokens and
|
|
19
|
+
identity, each with the suite an adapter must pass. The suites encode what each port class actually
|
|
20
|
+
gets wrong: content inventing a default for an unauthored key or carrying a capability reference,
|
|
21
|
+
money expressed as a float or an idempotency key that moves it twice, search hits mistaken for
|
|
22
|
+
authorization rather than pointers a governed read still resolves, and redelivery duplicating a
|
|
23
|
+
customer record. `PaymentOutcome` includes `ambiguous`, which maps onto the platform's existing
|
|
24
|
+
adapter outcomes so an uncertain charge reconciles instead of being retried.
|
|
25
|
+
|
|
26
|
+
- 9a5b18a: Close what a fifth review found, including a regression the previous fix introduced.
|
|
27
|
+
|
|
28
|
+
**Ambiguity fired on legal documents.** Counting every declared view made publishing a view at two
|
|
29
|
+
versions a hard failure, and view versioning is a first-class pattern: the platform and the usage
|
|
30
|
+
contract both key a view on `name@version`. It also failed a release over a capability that release
|
|
31
|
+
never touched. Ambiguity is now reported for _granted_ references only, where a versionless reference
|
|
32
|
+
genuinely cannot pick between two versions, and a contract supplied twice gets its own finding
|
|
33
|
+
instead of making every one of its views look ambiguous.
|
|
34
|
+
|
|
35
|
+
**A failed replay is `unknown`, not `refused`.** `failed` is reachable after adapter steps succeeded
|
|
36
|
+
and the domain transaction committed, so telling an operator the action was definitely refused could
|
|
37
|
+
be as wrong as telling them it was applied. Only policy blocks and validation failures are decided
|
|
38
|
+
before any effect. A replay adapter that returns nothing is surfaced rather than counted as applied,
|
|
39
|
+
which was the one line the previous rewrite left untouched.
|
|
40
|
+
|
|
41
|
+
**`parked` and `expired` carry their records**, and reconciliation carries the invocation id. Bare
|
|
42
|
+
counts left nobody able to say which action needs approving or whose work was dropped, and external
|
|
43
|
+
reconciliation needs the id the type exists to enable.
|
|
44
|
+
|
|
45
|
+
Settled-failure classification gained the superseded-action-version case and dropped two it had
|
|
46
|
+
wrong: a future-dated capture comes good as clocks correct, and a gateway missing its replay window
|
|
47
|
+
is a deployment mistake somebody will fix. Discarding either would destroy work.
|
|
48
|
+
|
|
49
|
+
**`assertPortSuiteHasTeeth` takes a set of broken adapters.** Requiring one stub to trip every check
|
|
50
|
+
was unsatisfiable for any suite with a fixture-liveness guard, since the guard bites on the
|
|
51
|
+
complement of what it guards, and two reference ports could not pass their own suites. Port ids now
|
|
52
|
+
permit a hyphen in the first segment, so a port can be defined for every name a manifest can declare,
|
|
53
|
+
and `certifyAdapter` validates its definition rather than issuing evidence the gate reports as
|
|
54
|
+
missing.
|
|
55
|
+
|
|
56
|
+
- 9b2e55b: Close what a fourth review found: each earlier fix closed one instance of a class and left its
|
|
57
|
+
siblings.
|
|
58
|
+
|
|
59
|
+
**Replay disposition is now exhaustive over `ActionStatus`.** The previous fix enumerated the two
|
|
60
|
+
statuses that looked like refusals, so `failed`, `validation_failed` and `waiting_for_approval` fell
|
|
61
|
+
through and were deleted from the queue and reported to the operator as applied — the same bug the
|
|
62
|
+
fix set out to kill, one layer in. Any returned outcome now means the platform holds the invocation,
|
|
63
|
+
so the capture leaves the queue either way; what differs is whether a person needs to hear about it.
|
|
64
|
+
An undeterminable adapter outcome is reported as `unknown` rather than `refused`, because the effect
|
|
65
|
+
may well have happened. `isSettledReplayFailure` learned the refusals the sibling commit introduced,
|
|
66
|
+
including a capture rejected for age, which can never come good.
|
|
67
|
+
|
|
68
|
+
**A replay without `capturedAt` is refused.** It was reachable only through an untyped cast, so
|
|
69
|
+
omitting it skipped the age check entirely and stamped receipt time into durable capture evidence.
|
|
70
|
+
|
|
71
|
+
**A capability can name a namespaced port.** `definePort` required a dot and the manifest validator
|
|
72
|
+
forbade one, so no real capability could name a defined port and the certification gate was
|
|
73
|
+
unreachable. Certification is also checked structurally rather than by the presence of a property, so
|
|
74
|
+
a hand-written claim no longer satisfies the gate that exists to demand evidence, and a port
|
|
75
|
+
requirement narrows its candidates as each constraint is applied.
|
|
76
|
+
|
|
77
|
+
**`assertPortSuiteHasTeeth` names every vacuous check** rather than returning on the first one that
|
|
78
|
+
bites, and a reference port is actually run through it. The payments control asserts its result, so
|
|
79
|
+
an adapter that never charges no longer certifies. The capability-reference grammar matches the
|
|
80
|
+
platform's own view-name rule instead of being stricter than it, and ambiguity is counted by
|
|
81
|
+
occurrence across views and intents rather than by comparing names within one contract.
|
|
82
|
+
|
|
83
|
+
- 0d1f6f6: Close what a third independent review found, including two defects introduced by the previous fix.
|
|
84
|
+
|
|
85
|
+
**Offline replay counted refusals as successes.** `PlatformHost` reports a refused capture by
|
|
86
|
+
_returning_ `reconciliation_required` with the reason, not by throwing, so the queue took its success
|
|
87
|
+
branch, deleted the capture and reported it applied. The reconciliation path was unreachable. Replay
|
|
88
|
+
now reads the returned outcome. The already-applied branch is removed entirely: a repeated key
|
|
89
|
+
returns the original invocation without complaint, and an idempotency _conflict_ means the key was
|
|
90
|
+
reused for a materially different command, so that capture was never applied and is kept rather than
|
|
91
|
+
discarded.
|
|
92
|
+
|
|
93
|
+
**`deriveAuthorizationGrants` defaulted open and reached the signed artifact.** Requirements are
|
|
94
|
+
optional metadata, so a capability declaring none granted everything to an actor holding nothing.
|
|
95
|
+
Grants are also carried onto a release unexamined and promotion signs a route for each, so a surplus
|
|
96
|
+
grant became a signed route to a capability the screen never renders. Undeclared requirements now
|
|
97
|
+
withhold by default, and `document` scopes the result to references the screen actually binds.
|
|
98
|
+
|
|
99
|
+
Several conformance checks could not fail: a search suite passed an adapter returning nothing, a
|
|
100
|
+
payments suite passed a double-charger that omitted a provider reference, an identity suite certified
|
|
101
|
+
timestamps the scope check then rejected on every request, an MFE probe could be refused for an
|
|
102
|
+
unrelated reason, and a channel check accepted any exception as a refusal. Each now proves what it
|
|
103
|
+
claims. Promotion configuration rejects anything reading like a key rather than two spellings,
|
|
104
|
+
rejects unknown fields, and constrains the application name that decides the default output path.
|
|
105
|
+
|
|
106
|
+
### Patch Changes
|
|
107
|
+
|
|
108
|
+
- a598fb9: Close the remaining review findings.
|
|
109
|
+
|
|
110
|
+
**One capability-reference grammar.** `sdui-release` accepted any name after the namespace while
|
|
111
|
+
`sdui-composer` rejected `/`, so a view named `orders/reports/daily` produced a release that validated
|
|
112
|
+
and then died at promotion with nothing wrong upstream to point at. Both now share
|
|
113
|
+
`parseCapabilityRef`, tightened to lowercase path segments matching what the platform permits for a
|
|
114
|
+
view name. Two views in one contract that reduce to the same reference are reported as
|
|
115
|
+
`ambiguous_capability_reference` at validation rather than surfacing as an ambiguous route later.
|
|
116
|
+
|
|
117
|
+
**A bounded replay window.** A client-supplied `capturedAt` had no lower bound. Deferring it to the
|
|
118
|
+
Host looked safe and was not: the Host checks staleness only when the capture-authority adapter
|
|
119
|
+
supplies an `expiresAt`, and skips the execution-time expiry check entirely for actions governed at
|
|
120
|
+
capture. A gateway that accepts replay must now declare `maxOfflineReplayAgeMs`, and the instant must
|
|
121
|
+
be RFC 3339 with an explicit offset.
|
|
122
|
+
|
|
123
|
+
**One token-set grammar.** `document.tokenSet` accepted names and versions that `supportedTokenSets`
|
|
124
|
+
rejected, while validation treats the two fields as interchangeable.
|
|
125
|
+
|
|
126
|
+
**Port requirements narrow as they are checked.** A requirement for a standard _and_ a version could
|
|
127
|
+
be satisfied by two different adapters covering one constraint each. `requirement.standard.version`
|
|
128
|
+
was declared and never read.
|
|
129
|
+
|
|
130
|
+
## 0.3.0
|
|
131
|
+
|
|
132
|
+
### Minor Changes
|
|
133
|
+
|
|
134
|
+
- 89982ba: Add `IdentityPort`, the OAuth2/OIDC-shaped seam a gateway implements to turn a presented credential
|
|
135
|
+
into verified `ActorClaims`. Every governed action, projection decision, grant and audit record
|
|
136
|
+
derives from actor context, which until now the platform accepted on trust from its caller.
|
|
137
|
+
|
|
138
|
+
`assertActorClaimsCoverScope` enforces the half that verification does not: a credential proves who
|
|
139
|
+
the caller is, never that they may act in the tenant and space a request names. It refuses expired,
|
|
140
|
+
not-yet-valid, unparseable, cross-tenant and out-of-space claims, and `actorContextFromClaims`
|
|
141
|
+
derives submission fields only from claims that passed. Space coverage is spelled `"tenant-wide"`
|
|
142
|
+
rather than left absent, so claims that simply omit coverage can never be read as covering
|
|
143
|
+
everything. `identityPortChecks` is the suite an adapter must pass, including that resolved claims
|
|
144
|
+
never echo credential material back.
|
|
145
|
+
|
|
146
|
+
### Patch Changes
|
|
147
|
+
|
|
148
|
+
- 6f5471b: Close the gaps an independent review found in the grant, remote and identity work.
|
|
149
|
+
|
|
150
|
+
A capability reference object was treated as a leaf, so a granted reference could shield ungranted
|
|
151
|
+
ones parked beside it on the same object; sibling keys are now walked. Prop recursion is depth
|
|
152
|
+
bounded, so cyclic programmatic input reports a finding instead of exhausting the stack. Federated
|
|
153
|
+
remotes now compare their exposed-module mapping, not only entry and integrity, and the locked remote
|
|
154
|
+
is carried onto the promoted release so two releases differing only in their remote cannot share a
|
|
155
|
+
digest. Pack lookup matches full pack identity rather than namespace and name alone, and a stored
|
|
156
|
+
lockfile that declares one pack or capability twice is rejected rather than resolved by first match.
|
|
157
|
+
|
|
158
|
+
Grant digests omit `variants` and `fragments` when absent, so a release that declares neither digests
|
|
159
|
+
exactly as it did before those fields existed and previously promoted releases keep verifying.
|
|
160
|
+
Variants are hashed in id order, so reordering the same set no longer churns the digest. Fragment
|
|
161
|
+
grants written as a `{ __proto__: … }` object literal are rejected, because that sets the prototype
|
|
162
|
+
rather than creating the entry and the narrowing would silently disappear.
|
|
163
|
+
|
|
164
|
+
`assertActorClaimsCoverScope` no longer fails open: an invalid clock is rejected instead of making
|
|
165
|
+
every temporal comparison false, timestamps must be RFC 3339 with an explicit offset so a credential
|
|
166
|
+
cannot expire at different instants on different hosts, and `spaceIds` must be an array of strings so
|
|
167
|
+
a bare string cannot reach substring matching where `"space_10"` would cover `"space_1"`.
|
|
168
|
+
|
|
3
169
|
## 0.2.0
|
|
4
170
|
|
|
5
171
|
### 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/catalog.cjs
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// catalog.ts
|
|
21
|
+
var catalog_exports = {};
|
|
22
|
+
__export(catalog_exports, {
|
|
23
|
+
CONTENT_PORT: () => CONTENT_PORT,
|
|
24
|
+
CUSTOMER_RECORD_PORT: () => CUSTOMER_RECORD_PORT,
|
|
25
|
+
DESIGN_TOKENS_PORT: () => DESIGN_TOKENS_PORT,
|
|
26
|
+
FLAGS_PORT: () => FLAGS_PORT,
|
|
27
|
+
IDENTITY_PORT: () => IDENTITY_PORT,
|
|
28
|
+
PAYMENTS_PORT: () => PAYMENTS_PORT,
|
|
29
|
+
REFERENCE_PORTS: () => REFERENCE_PORTS,
|
|
30
|
+
SEARCH_PORT: () => SEARCH_PORT
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(catalog_exports);
|
|
33
|
+
|
|
34
|
+
// index.ts
|
|
35
|
+
var ALIAS = /^\{([^}]+)\}$/;
|
|
36
|
+
var IDENTITY_ACTOR_TYPES = [
|
|
37
|
+
"natural_person",
|
|
38
|
+
"agent",
|
|
39
|
+
"system",
|
|
40
|
+
"service_account",
|
|
41
|
+
"external_system",
|
|
42
|
+
"integration"
|
|
43
|
+
];
|
|
44
|
+
var RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
45
|
+
var PORT_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
|
|
46
|
+
function definePort(definition) {
|
|
47
|
+
if (!PORT_ID.test(definition.id)) {
|
|
48
|
+
throw new Error(`Port id "${definition.id}" must be namespaced lowercase, for example "acme.docusign".`);
|
|
49
|
+
}
|
|
50
|
+
if (!/^\d+\.\d+\.\d+$/.test(definition.version)) {
|
|
51
|
+
throw new Error(`Port "${definition.id}" version must be a semver version.`);
|
|
52
|
+
}
|
|
53
|
+
if (!definition.description.trim()) {
|
|
54
|
+
throw new Error(`Port "${definition.id}" must describe what an adapter behind it is responsible for.`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof definition.checks !== "function") {
|
|
57
|
+
throw new Error(`Port "${definition.id}" must supply a conformance suite.`);
|
|
58
|
+
}
|
|
59
|
+
return definition;
|
|
60
|
+
}
|
|
61
|
+
var PortContractFailure = class extends Error {
|
|
62
|
+
name = "PortContractFailure";
|
|
63
|
+
};
|
|
64
|
+
function expect(condition, message) {
|
|
65
|
+
if (!condition) throw new PortContractFailure(message);
|
|
66
|
+
}
|
|
67
|
+
function flagsPortChecks() {
|
|
68
|
+
const unknownKey = "fabric.contract.definitely-not-configured";
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
id: "flags.default-on-unknown-key",
|
|
72
|
+
title: "an unknown flag resolves to the supplied default rather than throwing",
|
|
73
|
+
async run(port) {
|
|
74
|
+
const result = await port.resolveBoolean(unknownKey, true);
|
|
75
|
+
expect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);
|
|
76
|
+
expect(
|
|
77
|
+
result.reason === "DEFAULT" || result.reason === "ERROR",
|
|
78
|
+
`an unknown flag resolved with reason "${result.reason}"; a default or error was expected`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "flags.default-is-typed",
|
|
84
|
+
title: "each typed resolver returns its own type",
|
|
85
|
+
async run(port) {
|
|
86
|
+
expect(typeof (await port.resolveBoolean(unknownKey, false)).value === "boolean", "resolveBoolean did not return a boolean");
|
|
87
|
+
expect(typeof (await port.resolveString(unknownKey, "fallback")).value === "string", "resolveString did not return a string");
|
|
88
|
+
expect(typeof (await port.resolveNumber(unknownKey, 42)).value === "number", "resolveNumber did not return a number");
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "flags.evaluation-is-pure",
|
|
93
|
+
title: "the same key and context resolve the same way twice",
|
|
94
|
+
async run(port) {
|
|
95
|
+
const context = { targetingKey: "fabric-contract-subject" };
|
|
96
|
+
const first = await port.resolveString(unknownKey, "fallback", context);
|
|
97
|
+
const second = await port.resolveString(unknownKey, "fallback", context);
|
|
98
|
+
expect(first.value === second.value, `the same evaluation returned "${first.value}" then "${second.value}"`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "flags.tolerates-absent-context",
|
|
103
|
+
title: "evaluation without a context does not throw",
|
|
104
|
+
async run(port) {
|
|
105
|
+
await port.resolveBoolean(unknownKey, false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
function identityPortChecks(fixtures) {
|
|
111
|
+
const checks = [
|
|
112
|
+
{
|
|
113
|
+
id: "identity.rejects-garbage",
|
|
114
|
+
title: "an unverifiable credential resolves to null rather than partial claims",
|
|
115
|
+
async run(port) {
|
|
116
|
+
const result = await port.verify("not-a-credential");
|
|
117
|
+
expect(result === null, "an unverifiable credential resolved to claims instead of null");
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "identity.rejects-empty",
|
|
122
|
+
title: "an empty credential resolves to null",
|
|
123
|
+
async run(port) {
|
|
124
|
+
expect(await port.verify("") === null, "an empty credential resolved to claims");
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "identity.resolves-valid",
|
|
129
|
+
title: "a valid credential resolves to the expected subject and tenant",
|
|
130
|
+
async run(port) {
|
|
131
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
132
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
133
|
+
expect(claims?.subject === fixtures.expected.subject, `subject was "${claims?.subject}"`);
|
|
134
|
+
expect(claims?.tenantId === fixtures.expected.tenantId, `tenantId was "${claims?.tenantId}"`);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "identity.claims-are-complete",
|
|
139
|
+
title: "resolved claims carry every field the platform derives actor context from",
|
|
140
|
+
async run(port) {
|
|
141
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
142
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
143
|
+
if (!claims) return;
|
|
144
|
+
expect(IDENTITY_ACTOR_TYPES.includes(claims.actorType), `actorType "${claims.actorType}" is not a known actor kind`);
|
|
145
|
+
expect(typeof claims.issuer === "string" && claims.issuer.length > 0, "claims carry no issuer");
|
|
146
|
+
expect(RFC3339.test(claims.issuedAt), `issuedAt "${claims.issuedAt}" is not RFC 3339 with an explicit offset`);
|
|
147
|
+
expect(RFC3339.test(claims.expiresAt), `expiresAt "${claims.expiresAt}" is not RFC 3339 with an explicit offset`);
|
|
148
|
+
expect(typeof claims.tenantId === "string" && claims.tenantId.length > 0, "claims carry no tenant");
|
|
149
|
+
expect(
|
|
150
|
+
claims.spaceIds === "tenant-wide" || Array.isArray(claims.spaceIds),
|
|
151
|
+
'spaceIds must be an explicit list or the literal "tenant-wide"'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "identity.carries-no-credential-material",
|
|
157
|
+
title: "resolved claims never echo the credential back",
|
|
158
|
+
async run(port) {
|
|
159
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
160
|
+
expect(claims !== null, "a valid credential failed to verify, so this check proved nothing");
|
|
161
|
+
if (!claims) return;
|
|
162
|
+
const serialized = JSON.stringify(claims);
|
|
163
|
+
expect(
|
|
164
|
+
!serialized.includes(fixtures.validCredential),
|
|
165
|
+
"resolved claims contain the presented credential; claims must carry an opaque reference instead"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "identity.verification-is-stable",
|
|
171
|
+
title: "the same credential resolves the same subject twice",
|
|
172
|
+
async run(port) {
|
|
173
|
+
const first = await port.verify(fixtures.validCredential);
|
|
174
|
+
const second = await port.verify(fixtures.validCredential);
|
|
175
|
+
expect(first?.subject === second?.subject, "the same credential resolved to two different subjects");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
];
|
|
179
|
+
if (fixtures.expiredCredential !== void 0) {
|
|
180
|
+
checks.push({
|
|
181
|
+
id: "identity.rejects-expired",
|
|
182
|
+
title: "an expired credential resolves to null rather than stale claims",
|
|
183
|
+
async run(port) {
|
|
184
|
+
const expiredCredential = fixtures.expiredCredential;
|
|
185
|
+
expect(await port.verify(expiredCredential) === null, "an expired credential still resolved to claims");
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return checks;
|
|
190
|
+
}
|
|
191
|
+
function contentPortChecks(fixtures) {
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
id: "content.missing-resolves-null",
|
|
195
|
+
title: "an unauthored key resolves to null rather than an invented default",
|
|
196
|
+
async run(port) {
|
|
197
|
+
const result = await port.resolve({ ...fixtures.present, key: fixtures.absentKey });
|
|
198
|
+
expect(result === null, "an unauthored key resolved to content instead of null");
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "content.revision-is-stable",
|
|
203
|
+
title: "identical content resolves the same revision twice",
|
|
204
|
+
async run(port) {
|
|
205
|
+
const first = await port.resolve(fixtures.present);
|
|
206
|
+
const second = await port.resolve(fixtures.present);
|
|
207
|
+
expect(first !== null && second !== null, "the fixture key is not authored");
|
|
208
|
+
expect(first?.revision === second?.revision, "the same content reported two revisions, so it cannot be cached");
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "content.carries-no-capability-reference",
|
|
213
|
+
title: "authored content never carries a capability reference",
|
|
214
|
+
async run(port) {
|
|
215
|
+
const result = await port.resolve(fixtures.present);
|
|
216
|
+
expect(
|
|
217
|
+
!JSON.stringify(result ?? {}).includes("capability://"),
|
|
218
|
+
"content carried a capability reference; what a screen may reach is decided by a release, not by an author"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
function paymentsPortChecks(fixtures) {
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
id: "payments.rejects-non-integer-amount",
|
|
228
|
+
title: "a fractional minor-unit amount is refused rather than rounded",
|
|
229
|
+
async run(port) {
|
|
230
|
+
let refused = false;
|
|
231
|
+
try {
|
|
232
|
+
await port.charge({ ...fixtures.request, amount: { ...fixtures.request.amount, amount: 10.5 } });
|
|
233
|
+
} catch {
|
|
234
|
+
refused = true;
|
|
235
|
+
}
|
|
236
|
+
expect(refused, "a fractional minor-unit amount was accepted; money is an integer or it drifts");
|
|
237
|
+
const control = await port.charge({
|
|
238
|
+
...fixtures.request,
|
|
239
|
+
idempotencyKey: `${fixtures.request.idempotencyKey}:integer-probe`
|
|
240
|
+
});
|
|
241
|
+
expect(
|
|
242
|
+
control.outcome === "succeeded",
|
|
243
|
+
`the fixture charge resolved "${control.outcome}"; a suite run against an adapter that never succeeds certifies nothing`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "payments.requires-idempotency-key",
|
|
249
|
+
title: "a charge without a stable key is refused",
|
|
250
|
+
async run(port) {
|
|
251
|
+
let refused = false;
|
|
252
|
+
try {
|
|
253
|
+
await port.charge({ ...fixtures.request, idempotencyKey: "" });
|
|
254
|
+
} catch {
|
|
255
|
+
refused = true;
|
|
256
|
+
}
|
|
257
|
+
expect(refused, "a charge with no idempotency key was accepted; a retry would move money twice");
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "payments.same-key-moves-money-once",
|
|
262
|
+
title: "the same idempotency key resolves to the same provider reference",
|
|
263
|
+
async run(port) {
|
|
264
|
+
const first = await port.charge(fixtures.request);
|
|
265
|
+
const second = await port.charge(fixtures.request);
|
|
266
|
+
expect(
|
|
267
|
+
first.outcome === second.outcome,
|
|
268
|
+
`the same key produced "${first.outcome}" then "${second.outcome}"`
|
|
269
|
+
);
|
|
270
|
+
expect(
|
|
271
|
+
first.outcome !== "succeeded" || first.providerReference !== void 0,
|
|
272
|
+
"a succeeded charge carried no provider reference, so a retry cannot be told from a second charge"
|
|
273
|
+
);
|
|
274
|
+
expect(
|
|
275
|
+
first.providerReference === second.providerReference,
|
|
276
|
+
"the same key produced two provider references, so a retry charged twice"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "payments.outcome-is-declared",
|
|
282
|
+
title: "an outcome is one of the three the platform can act on",
|
|
283
|
+
async run(port) {
|
|
284
|
+
const result = await port.charge(fixtures.request);
|
|
285
|
+
expect(
|
|
286
|
+
result.outcome === "succeeded" || result.outcome === "failed" || result.outcome === "ambiguous",
|
|
287
|
+
`outcome "${result.outcome}" is not one the platform can reconcile`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
function searchPortChecks(fixtures) {
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
id: "search.fixture-returns-hits",
|
|
297
|
+
title: "the fixture query matches something, so the checks below are not vacuous",
|
|
298
|
+
async run(port) {
|
|
299
|
+
const result = await port.query(fixtures.query);
|
|
300
|
+
expect(
|
|
301
|
+
result.hits.length > 0,
|
|
302
|
+
"the fixture query returned nothing, so every check below passes without exercising the adapter"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "search.respects-limit",
|
|
308
|
+
title: "a result set never exceeds the requested limit",
|
|
309
|
+
async run(port) {
|
|
310
|
+
const result = await port.query({ ...fixtures.query, limit: 1 });
|
|
311
|
+
expect(result.hits.length <= 1, `returned ${result.hits.length} hits for a limit of 1`);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "search.returns-identifiers-not-records",
|
|
316
|
+
title: "a hit carries an identifier a governed read can resolve, not the record",
|
|
317
|
+
async run(port) {
|
|
318
|
+
const result = await port.query(fixtures.query);
|
|
319
|
+
for (const hit of result.hits) {
|
|
320
|
+
expect(typeof hit.id === "string" && hit.id.length > 0, "a hit carried no identifier");
|
|
321
|
+
expect(
|
|
322
|
+
Object.keys(hit).every((key) => key === "id" || key === "score"),
|
|
323
|
+
"a hit carried record fields; search results are pointers, and reading one still goes through ProjectionHost"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
function customerRecordPortChecks(fixtures) {
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
// This proves redelivery does not *fail*. Whether it duplicates is not
|
|
334
|
+
// observable through this port, which has no read side by design, so
|
|
335
|
+
// the suite says what it can and does not imply more.
|
|
336
|
+
id: "crm.redelivery-does-not-fail",
|
|
337
|
+
title: "recording the same event twice is not an error",
|
|
338
|
+
async run(port) {
|
|
339
|
+
await port.record(fixtures.event);
|
|
340
|
+
await port.record(fixtures.event);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "crm.rejects-unidentified-event",
|
|
345
|
+
title: "an event with no stable id is refused",
|
|
346
|
+
async run(port) {
|
|
347
|
+
let refused = false;
|
|
348
|
+
try {
|
|
349
|
+
await port.record({ ...fixtures.event, eventId: "" });
|
|
350
|
+
} catch {
|
|
351
|
+
refused = true;
|
|
352
|
+
}
|
|
353
|
+
expect(refused, "an event with no id was accepted; redelivery would duplicate the record");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
function designTokensPortChecks(theme) {
|
|
359
|
+
return [
|
|
360
|
+
{
|
|
361
|
+
id: "tokens.theme-resolves",
|
|
362
|
+
title: "a known theme resolves to at least one token",
|
|
363
|
+
async run(port) {
|
|
364
|
+
const tokens = await port.resolve(theme);
|
|
365
|
+
expect(Array.isArray(tokens) && tokens.length > 0, `theme "${theme}" resolved to no tokens`);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: "tokens.no-unresolved-aliases",
|
|
370
|
+
title: "no resolved token still carries an alias",
|
|
371
|
+
async run(port) {
|
|
372
|
+
for (const token of await port.resolve(theme)) {
|
|
373
|
+
expect(
|
|
374
|
+
typeof token.value !== "string" || !ALIAS.test(token.value.trim()),
|
|
375
|
+
`token "${token.name}" resolved to the unfollowed alias ${String(token.value)}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "tokens.names-are-unique",
|
|
382
|
+
title: "token names are unique within a theme",
|
|
383
|
+
async run(port) {
|
|
384
|
+
const names = (await port.resolve(theme)).map((token) => token.name);
|
|
385
|
+
expect(new Set(names).size === names.length, "the theme resolved duplicate token names");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// catalog.ts
|
|
392
|
+
var FLAGS_PORT = definePort({
|
|
393
|
+
id: "fabric.flags",
|
|
394
|
+
version: "1.0.0",
|
|
395
|
+
standard: { name: "OpenFeature" },
|
|
396
|
+
description: "Resolves a typed flag for an evaluation context, defaulting rather than throwing.",
|
|
397
|
+
checks: () => flagsPortChecks()
|
|
398
|
+
});
|
|
399
|
+
var DESIGN_TOKENS_PORT = definePort({
|
|
400
|
+
id: "fabric.design-tokens",
|
|
401
|
+
version: "1.0.0",
|
|
402
|
+
standard: { name: "W3C DTCG" },
|
|
403
|
+
description: "Resolves a DTCG token document into flat, renderer-neutral values.",
|
|
404
|
+
checks: (fixtures) => designTokensPortChecks(fixtures)
|
|
405
|
+
});
|
|
406
|
+
var IDENTITY_PORT = definePort({
|
|
407
|
+
id: "fabric.identity",
|
|
408
|
+
version: "1.0.0",
|
|
409
|
+
standard: { name: "OAuth 2.0 / OIDC" },
|
|
410
|
+
description: "Verifies a presented credential into actor claims, or resolves it to null.",
|
|
411
|
+
checks: (fixtures) => identityPortChecks(fixtures)
|
|
412
|
+
});
|
|
413
|
+
var CONTENT_PORT = definePort({
|
|
414
|
+
id: "fabric.content",
|
|
415
|
+
version: "1.0.0",
|
|
416
|
+
description: "Resolves authored copy and layout, never authority.",
|
|
417
|
+
checks: (fixtures) => contentPortChecks(fixtures)
|
|
418
|
+
});
|
|
419
|
+
var PAYMENTS_PORT = definePort({
|
|
420
|
+
id: "fabric.payments",
|
|
421
|
+
version: "1.0.0",
|
|
422
|
+
description: "Moves money once per idempotency key, and admits when the outcome is unknown.",
|
|
423
|
+
checks: (fixtures) => paymentsPortChecks(fixtures)
|
|
424
|
+
});
|
|
425
|
+
var SEARCH_PORT = definePort({
|
|
426
|
+
id: "fabric.search",
|
|
427
|
+
version: "1.0.0",
|
|
428
|
+
description: "Returns identifiers a governed read can resolve, never records.",
|
|
429
|
+
checks: (fixtures) => searchPortChecks(fixtures)
|
|
430
|
+
});
|
|
431
|
+
var CUSTOMER_RECORD_PORT = definePort({
|
|
432
|
+
id: "fabric.customer-record",
|
|
433
|
+
version: "1.0.0",
|
|
434
|
+
description: "Records outbound events idempotently. Told, never asked.",
|
|
435
|
+
checks: (fixtures) => customerRecordPortChecks(fixtures)
|
|
436
|
+
});
|
|
437
|
+
var REFERENCE_PORTS = [
|
|
438
|
+
FLAGS_PORT,
|
|
439
|
+
DESIGN_TOKENS_PORT,
|
|
440
|
+
IDENTITY_PORT,
|
|
441
|
+
CONTENT_PORT,
|
|
442
|
+
PAYMENTS_PORT,
|
|
443
|
+
SEARCH_PORT,
|
|
444
|
+
CUSTOMER_RECORD_PORT
|
|
445
|
+
];
|
|
446
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
447
|
+
0 && (module.exports = {
|
|
448
|
+
CONTENT_PORT,
|
|
449
|
+
CUSTOMER_RECORD_PORT,
|
|
450
|
+
DESIGN_TOKENS_PORT,
|
|
451
|
+
FLAGS_PORT,
|
|
452
|
+
IDENTITY_PORT,
|
|
453
|
+
PAYMENTS_PORT,
|
|
454
|
+
REFERENCE_PORTS,
|
|
455
|
+
SEARCH_PORT
|
|
456
|
+
});
|
|
457
|
+
//# sourceMappingURL=catalog.cjs.map
|