@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
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
var ALIAS = /^\{([^}]+)\}$/;
|
|
3
|
+
var isToken = (value) => !!value && typeof value === "object" && !Array.isArray(value) && "$value" in value;
|
|
4
|
+
var isGroup = (value) => !!value && typeof value === "object" && !Array.isArray(value) && !("$value" in value);
|
|
5
|
+
function resolveDesignTokens(document) {
|
|
6
|
+
const flat = /* @__PURE__ */ new Map();
|
|
7
|
+
const walk = (node, path, inheritedType) => {
|
|
8
|
+
const groupType = typeof node.$type === "string" ? node.$type : inheritedType;
|
|
9
|
+
for (const [key, member] of Object.entries(node)) {
|
|
10
|
+
if (key.startsWith("$")) continue;
|
|
11
|
+
const next = [...path, key];
|
|
12
|
+
if (isToken(member)) {
|
|
13
|
+
flat.set(next.join("."), { token: member, type: member.$type ?? groupType });
|
|
14
|
+
} else if (isGroup(member)) {
|
|
15
|
+
walk(member, next, groupType);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
walk(document, []);
|
|
20
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
21
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
22
|
+
const valueOf = (name) => {
|
|
23
|
+
if (resolved.has(name)) return resolved.get(name);
|
|
24
|
+
const entry = flat.get(name);
|
|
25
|
+
if (!entry) throw new Error(`Design token alias "{${name}}" does not resolve to a declared token.`);
|
|
26
|
+
if (resolving.has(name)) {
|
|
27
|
+
throw new Error(`Design token alias cycle: ${[...resolving, name].join(" -> ")}.`);
|
|
28
|
+
}
|
|
29
|
+
const raw = entry.token.$value;
|
|
30
|
+
if (typeof raw !== "string") {
|
|
31
|
+
resolved.set(name, raw);
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
34
|
+
const alias = ALIAS.exec(raw.trim());
|
|
35
|
+
if (!alias) {
|
|
36
|
+
resolved.set(name, raw);
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
resolving.add(name);
|
|
40
|
+
const target = valueOf(alias[1]);
|
|
41
|
+
resolving.delete(name);
|
|
42
|
+
resolved.set(name, target);
|
|
43
|
+
return target;
|
|
44
|
+
};
|
|
45
|
+
return [...flat.entries()].map(([name, entry]) => {
|
|
46
|
+
const value = valueOf(name);
|
|
47
|
+
return {
|
|
48
|
+
name,
|
|
49
|
+
value,
|
|
50
|
+
...entry.type === void 0 ? {} : { type: entry.type },
|
|
51
|
+
...entry.token.$description === void 0 ? {} : { description: entry.token.$description }
|
|
52
|
+
};
|
|
53
|
+
});
|
|
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
|
+
}
|
|
128
|
+
var PORT_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
|
|
129
|
+
function definePort(definition) {
|
|
130
|
+
if (!PORT_ID.test(definition.id)) {
|
|
131
|
+
throw new Error(`Port id "${definition.id}" must be namespaced lowercase, for example "acme.docusign".`);
|
|
132
|
+
}
|
|
133
|
+
if (!/^\d+\.\d+\.\d+$/.test(definition.version)) {
|
|
134
|
+
throw new Error(`Port "${definition.id}" version must be a semver version.`);
|
|
135
|
+
}
|
|
136
|
+
if (!definition.description.trim()) {
|
|
137
|
+
throw new Error(`Port "${definition.id}" must describe what an adapter behind it is responsible for.`);
|
|
138
|
+
}
|
|
139
|
+
if (typeof definition.checks !== "function") {
|
|
140
|
+
throw new Error(`Port "${definition.id}" must supply a conformance suite.`);
|
|
141
|
+
}
|
|
142
|
+
return definition;
|
|
143
|
+
}
|
|
144
|
+
async function assertPortSuiteHasTeeth(definition, fixtures, brokenAdapters) {
|
|
145
|
+
const checks = definition.checks(fixtures);
|
|
146
|
+
if (checks.length === 0) {
|
|
147
|
+
throw new Error(`Port "${definition.id}" declares no checks, so no adapter can be certified against it.`);
|
|
148
|
+
}
|
|
149
|
+
const ids = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const check of checks) {
|
|
151
|
+
if (ids.has(check.id)) throw new Error(`Port "${definition.id}" declares check "${check.id}" twice.`);
|
|
152
|
+
ids.add(check.id);
|
|
153
|
+
}
|
|
154
|
+
const adapters = Array.isArray(brokenAdapters) ? brokenAdapters : [brokenAdapters];
|
|
155
|
+
if (adapters.length === 0) {
|
|
156
|
+
throw new Error(`Port "${definition.id}" needs at least one deliberately broken adapter to prove its suite bites.`);
|
|
157
|
+
}
|
|
158
|
+
const uncaught = new Set(checks.map((check) => check.id));
|
|
159
|
+
for (const adapter of adapters) {
|
|
160
|
+
for (const check of checks) {
|
|
161
|
+
if (!uncaught.has(check.id)) continue;
|
|
162
|
+
try {
|
|
163
|
+
await check.run(adapter);
|
|
164
|
+
} catch {
|
|
165
|
+
uncaught.delete(check.id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (uncaught.size > 0) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Port "${definition.id}" has ${uncaught.size} check(s) that no supplied broken adapter trips, so they prove nothing: ${[...uncaught].join(", ")}.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function certifyAdapter(input) {
|
|
176
|
+
definePort(input.definition);
|
|
177
|
+
const checks = input.definition.checks(input.fixtures);
|
|
178
|
+
if (checks.length === 0) {
|
|
179
|
+
throw new Error(`Port "${input.definition.id}" declares no checks, so no adapter can be certified against it.`);
|
|
180
|
+
}
|
|
181
|
+
const passed = [];
|
|
182
|
+
for (const check of checks) {
|
|
183
|
+
try {
|
|
184
|
+
await check.run(input.adapter);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Adapter "${input.vendor}" failed port "${input.definition.id}" check "${check.id}": ${error instanceof Error ? error.message : String(error)}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
passed.push(check.id);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
port: input.definition.id,
|
|
194
|
+
vendor: input.vendor,
|
|
195
|
+
...input.version === void 0 ? {} : { version: input.version },
|
|
196
|
+
...input.definition.standard ? { standard: input.definition.standard } : {},
|
|
197
|
+
certification: {
|
|
198
|
+
portId: input.definition.id,
|
|
199
|
+
portVersion: input.definition.version,
|
|
200
|
+
checks: passed,
|
|
201
|
+
certifiedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function isCertifiedFor(adapter, portName) {
|
|
206
|
+
const certification = adapter.certification;
|
|
207
|
+
if (!certification || typeof certification !== "object") return false;
|
|
208
|
+
if (certification.portId !== portName) return false;
|
|
209
|
+
if (certification.portId !== adapter.port) return false;
|
|
210
|
+
if (!/^\d+\.\d+\.\d+$/.test(certification.portVersion ?? "")) return false;
|
|
211
|
+
if (!Array.isArray(certification.checks) || certification.checks.length === 0) return false;
|
|
212
|
+
if (Number.isNaN(Date.parse(certification.certifiedAt ?? ""))) return false;
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
function validatePortRequirements(input) {
|
|
216
|
+
const definitions = new Map((input.definitions ?? []).map((definition) => [definition.id, definition]));
|
|
217
|
+
const findings = [];
|
|
218
|
+
for (const requirement of input.capability.requirements?.ports ?? []) {
|
|
219
|
+
const candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);
|
|
220
|
+
if (candidates.length === 0) {
|
|
221
|
+
findings.push({
|
|
222
|
+
code: "unsatisfied_port",
|
|
223
|
+
port: requirement.name,
|
|
224
|
+
message: `capability "${input.capability.namespace}" requires port "${requirement.name}" and no adapter is registered for it`
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
let eligible = candidates;
|
|
229
|
+
if (requirement.standard) {
|
|
230
|
+
eligible = eligible.filter((adapter) => adapter.standard?.name === requirement.standard.name);
|
|
231
|
+
if (eligible.length === 0) {
|
|
232
|
+
findings.push({
|
|
233
|
+
code: "standard_not_spoken",
|
|
234
|
+
port: requirement.name,
|
|
235
|
+
message: `port "${requirement.name}" must speak "${requirement.standard.name}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? "no standard"})`).join(", ")}`
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (requirement.standard.version) {
|
|
240
|
+
const atVersion = eligible.filter((adapter) => adapter.standard?.version === requirement.standard.version);
|
|
241
|
+
if (atVersion.length === 0) {
|
|
242
|
+
findings.push({
|
|
243
|
+
code: "standard_not_spoken",
|
|
244
|
+
port: requirement.name,
|
|
245
|
+
message: `port "${requirement.name}" must speak "${requirement.standard.name}" version "${requirement.standard.version}"; registered adapters speak ${eligible.map((adapter) => adapter.standard?.version ?? "an unstated version").join(", ")}`
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
eligible = atVersion;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (requirement.version) {
|
|
253
|
+
const atVersion = eligible.filter((adapter) => adapter.version === requirement.version);
|
|
254
|
+
if (atVersion.length === 0) {
|
|
255
|
+
findings.push({
|
|
256
|
+
code: "version_mismatch",
|
|
257
|
+
port: requirement.name,
|
|
258
|
+
message: `port "${requirement.name}" requires version "${requirement.version}"; eligible adapters are ${eligible.map((adapter) => `${adapter.vendor}@${adapter.version ?? "unversioned"}`).join(", ")}`
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
eligible = atVersion;
|
|
263
|
+
}
|
|
264
|
+
const definition = definitions.get(requirement.name);
|
|
265
|
+
if (input.requireCertification || definition) {
|
|
266
|
+
const certified = eligible.filter((adapter) => isCertifiedFor(adapter, requirement.name));
|
|
267
|
+
if (certified.length === 0) {
|
|
268
|
+
if (input.requireCertification) {
|
|
269
|
+
findings.push({
|
|
270
|
+
code: "uncertified_adapter",
|
|
271
|
+
port: requirement.name,
|
|
272
|
+
message: `port "${requirement.name}" has eligible adapters (${eligible.map((adapter) => adapter.vendor).join(", ")}) but none carries certification evidence`
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (definition && !certified.some((adapter) => adapter.certification.portVersion === definition.version)) {
|
|
278
|
+
findings.push({
|
|
279
|
+
code: "stale_certification",
|
|
280
|
+
port: requirement.name,
|
|
281
|
+
message: `port "${requirement.name}" is at contract version ${definition.version}; adapters are certified against ${certified.map((adapter) => `${adapter.vendor}@${adapter.certification.portVersion}`).join(", ")}`
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { valid: findings.length === 0, findings };
|
|
287
|
+
}
|
|
288
|
+
function assertPortRequirementsSatisfied(input) {
|
|
289
|
+
const result = validatePortRequirements(input);
|
|
290
|
+
if (result.valid) return;
|
|
291
|
+
throw new Error([
|
|
292
|
+
`Port requirements for "${input.capability.namespace}" are not satisfied:`,
|
|
293
|
+
...result.findings.map((finding) => ` - ${finding.message}`)
|
|
294
|
+
].join("\n"));
|
|
295
|
+
}
|
|
296
|
+
var PortContractFailure = class extends Error {
|
|
297
|
+
name = "PortContractFailure";
|
|
298
|
+
};
|
|
299
|
+
function expect(condition, message) {
|
|
300
|
+
if (!condition) throw new PortContractFailure(message);
|
|
301
|
+
}
|
|
302
|
+
function flagsPortChecks() {
|
|
303
|
+
const unknownKey = "fabric.contract.definitely-not-configured";
|
|
304
|
+
return [
|
|
305
|
+
{
|
|
306
|
+
id: "flags.default-on-unknown-key",
|
|
307
|
+
title: "an unknown flag resolves to the supplied default rather than throwing",
|
|
308
|
+
async run(port) {
|
|
309
|
+
const result = await port.resolveBoolean(unknownKey, true);
|
|
310
|
+
expect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);
|
|
311
|
+
expect(
|
|
312
|
+
result.reason === "DEFAULT" || result.reason === "ERROR",
|
|
313
|
+
`an unknown flag resolved with reason "${result.reason}"; a default or error was expected`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "flags.default-is-typed",
|
|
319
|
+
title: "each typed resolver returns its own type",
|
|
320
|
+
async run(port) {
|
|
321
|
+
expect(typeof (await port.resolveBoolean(unknownKey, false)).value === "boolean", "resolveBoolean did not return a boolean");
|
|
322
|
+
expect(typeof (await port.resolveString(unknownKey, "fallback")).value === "string", "resolveString did not return a string");
|
|
323
|
+
expect(typeof (await port.resolveNumber(unknownKey, 42)).value === "number", "resolveNumber did not return a number");
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: "flags.evaluation-is-pure",
|
|
328
|
+
title: "the same key and context resolve the same way twice",
|
|
329
|
+
async run(port) {
|
|
330
|
+
const context = { targetingKey: "fabric-contract-subject" };
|
|
331
|
+
const first = await port.resolveString(unknownKey, "fallback", context);
|
|
332
|
+
const second = await port.resolveString(unknownKey, "fallback", context);
|
|
333
|
+
expect(first.value === second.value, `the same evaluation returned "${first.value}" then "${second.value}"`);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "flags.tolerates-absent-context",
|
|
338
|
+
title: "evaluation without a context does not throw",
|
|
339
|
+
async run(port) {
|
|
340
|
+
await port.resolveBoolean(unknownKey, false);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
function identityPortChecks(fixtures) {
|
|
346
|
+
const checks = [
|
|
347
|
+
{
|
|
348
|
+
id: "identity.rejects-garbage",
|
|
349
|
+
title: "an unverifiable credential resolves to null rather than partial claims",
|
|
350
|
+
async run(port) {
|
|
351
|
+
const result = await port.verify("not-a-credential");
|
|
352
|
+
expect(result === null, "an unverifiable credential resolved to claims instead of null");
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "identity.rejects-empty",
|
|
357
|
+
title: "an empty credential resolves to null",
|
|
358
|
+
async run(port) {
|
|
359
|
+
expect(await port.verify("") === null, "an empty credential resolved to claims");
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: "identity.resolves-valid",
|
|
364
|
+
title: "a valid credential resolves to the expected subject and tenant",
|
|
365
|
+
async run(port) {
|
|
366
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
367
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
368
|
+
expect(claims?.subject === fixtures.expected.subject, `subject was "${claims?.subject}"`);
|
|
369
|
+
expect(claims?.tenantId === fixtures.expected.tenantId, `tenantId was "${claims?.tenantId}"`);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: "identity.claims-are-complete",
|
|
374
|
+
title: "resolved claims carry every field the platform derives actor context from",
|
|
375
|
+
async run(port) {
|
|
376
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
377
|
+
expect(claims !== null, "a valid credential failed to verify");
|
|
378
|
+
if (!claims) return;
|
|
379
|
+
expect(IDENTITY_ACTOR_TYPES.includes(claims.actorType), `actorType "${claims.actorType}" is not a known actor kind`);
|
|
380
|
+
expect(typeof claims.issuer === "string" && claims.issuer.length > 0, "claims carry no issuer");
|
|
381
|
+
expect(RFC3339.test(claims.issuedAt), `issuedAt "${claims.issuedAt}" is not RFC 3339 with an explicit offset`);
|
|
382
|
+
expect(RFC3339.test(claims.expiresAt), `expiresAt "${claims.expiresAt}" is not RFC 3339 with an explicit offset`);
|
|
383
|
+
expect(typeof claims.tenantId === "string" && claims.tenantId.length > 0, "claims carry no tenant");
|
|
384
|
+
expect(
|
|
385
|
+
claims.spaceIds === "tenant-wide" || Array.isArray(claims.spaceIds),
|
|
386
|
+
'spaceIds must be an explicit list or the literal "tenant-wide"'
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "identity.carries-no-credential-material",
|
|
392
|
+
title: "resolved claims never echo the credential back",
|
|
393
|
+
async run(port) {
|
|
394
|
+
const claims = await port.verify(fixtures.validCredential);
|
|
395
|
+
expect(claims !== null, "a valid credential failed to verify, so this check proved nothing");
|
|
396
|
+
if (!claims) return;
|
|
397
|
+
const serialized = JSON.stringify(claims);
|
|
398
|
+
expect(
|
|
399
|
+
!serialized.includes(fixtures.validCredential),
|
|
400
|
+
"resolved claims contain the presented credential; claims must carry an opaque reference instead"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: "identity.verification-is-stable",
|
|
406
|
+
title: "the same credential resolves the same subject twice",
|
|
407
|
+
async run(port) {
|
|
408
|
+
const first = await port.verify(fixtures.validCredential);
|
|
409
|
+
const second = await port.verify(fixtures.validCredential);
|
|
410
|
+
expect(first?.subject === second?.subject, "the same credential resolved to two different subjects");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
];
|
|
414
|
+
if (fixtures.expiredCredential !== void 0) {
|
|
415
|
+
checks.push({
|
|
416
|
+
id: "identity.rejects-expired",
|
|
417
|
+
title: "an expired credential resolves to null rather than stale claims",
|
|
418
|
+
async run(port) {
|
|
419
|
+
const expiredCredential = fixtures.expiredCredential;
|
|
420
|
+
expect(await port.verify(expiredCredential) === null, "an expired credential still resolved to claims");
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return checks;
|
|
425
|
+
}
|
|
426
|
+
function contentPortChecks(fixtures) {
|
|
427
|
+
return [
|
|
428
|
+
{
|
|
429
|
+
id: "content.missing-resolves-null",
|
|
430
|
+
title: "an unauthored key resolves to null rather than an invented default",
|
|
431
|
+
async run(port) {
|
|
432
|
+
const result = await port.resolve({ ...fixtures.present, key: fixtures.absentKey });
|
|
433
|
+
expect(result === null, "an unauthored key resolved to content instead of null");
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: "content.revision-is-stable",
|
|
438
|
+
title: "identical content resolves the same revision twice",
|
|
439
|
+
async run(port) {
|
|
440
|
+
const first = await port.resolve(fixtures.present);
|
|
441
|
+
const second = await port.resolve(fixtures.present);
|
|
442
|
+
expect(first !== null && second !== null, "the fixture key is not authored");
|
|
443
|
+
expect(first?.revision === second?.revision, "the same content reported two revisions, so it cannot be cached");
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: "content.carries-no-capability-reference",
|
|
448
|
+
title: "authored content never carries a capability reference",
|
|
449
|
+
async run(port) {
|
|
450
|
+
const result = await port.resolve(fixtures.present);
|
|
451
|
+
expect(
|
|
452
|
+
!JSON.stringify(result ?? {}).includes("capability://"),
|
|
453
|
+
"content carried a capability reference; what a screen may reach is decided by a release, not by an author"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
function paymentsPortChecks(fixtures) {
|
|
460
|
+
return [
|
|
461
|
+
{
|
|
462
|
+
id: "payments.rejects-non-integer-amount",
|
|
463
|
+
title: "a fractional minor-unit amount is refused rather than rounded",
|
|
464
|
+
async run(port) {
|
|
465
|
+
let refused = false;
|
|
466
|
+
try {
|
|
467
|
+
await port.charge({ ...fixtures.request, amount: { ...fixtures.request.amount, amount: 10.5 } });
|
|
468
|
+
} catch {
|
|
469
|
+
refused = true;
|
|
470
|
+
}
|
|
471
|
+
expect(refused, "a fractional minor-unit amount was accepted; money is an integer or it drifts");
|
|
472
|
+
const control = await port.charge({
|
|
473
|
+
...fixtures.request,
|
|
474
|
+
idempotencyKey: `${fixtures.request.idempotencyKey}:integer-probe`
|
|
475
|
+
});
|
|
476
|
+
expect(
|
|
477
|
+
control.outcome === "succeeded",
|
|
478
|
+
`the fixture charge resolved "${control.outcome}"; a suite run against an adapter that never succeeds certifies nothing`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "payments.requires-idempotency-key",
|
|
484
|
+
title: "a charge without a stable key is refused",
|
|
485
|
+
async run(port) {
|
|
486
|
+
let refused = false;
|
|
487
|
+
try {
|
|
488
|
+
await port.charge({ ...fixtures.request, idempotencyKey: "" });
|
|
489
|
+
} catch {
|
|
490
|
+
refused = true;
|
|
491
|
+
}
|
|
492
|
+
expect(refused, "a charge with no idempotency key was accepted; a retry would move money twice");
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "payments.same-key-moves-money-once",
|
|
497
|
+
title: "the same idempotency key resolves to the same provider reference",
|
|
498
|
+
async run(port) {
|
|
499
|
+
const first = await port.charge(fixtures.request);
|
|
500
|
+
const second = await port.charge(fixtures.request);
|
|
501
|
+
expect(
|
|
502
|
+
first.outcome === second.outcome,
|
|
503
|
+
`the same key produced "${first.outcome}" then "${second.outcome}"`
|
|
504
|
+
);
|
|
505
|
+
expect(
|
|
506
|
+
first.outcome !== "succeeded" || first.providerReference !== void 0,
|
|
507
|
+
"a succeeded charge carried no provider reference, so a retry cannot be told from a second charge"
|
|
508
|
+
);
|
|
509
|
+
expect(
|
|
510
|
+
first.providerReference === second.providerReference,
|
|
511
|
+
"the same key produced two provider references, so a retry charged twice"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "payments.outcome-is-declared",
|
|
517
|
+
title: "an outcome is one of the three the platform can act on",
|
|
518
|
+
async run(port) {
|
|
519
|
+
const result = await port.charge(fixtures.request);
|
|
520
|
+
expect(
|
|
521
|
+
result.outcome === "succeeded" || result.outcome === "failed" || result.outcome === "ambiguous",
|
|
522
|
+
`outcome "${result.outcome}" is not one the platform can reconcile`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
function searchPortChecks(fixtures) {
|
|
529
|
+
return [
|
|
530
|
+
{
|
|
531
|
+
id: "search.fixture-returns-hits",
|
|
532
|
+
title: "the fixture query matches something, so the checks below are not vacuous",
|
|
533
|
+
async run(port) {
|
|
534
|
+
const result = await port.query(fixtures.query);
|
|
535
|
+
expect(
|
|
536
|
+
result.hits.length > 0,
|
|
537
|
+
"the fixture query returned nothing, so every check below passes without exercising the adapter"
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "search.respects-limit",
|
|
543
|
+
title: "a result set never exceeds the requested limit",
|
|
544
|
+
async run(port) {
|
|
545
|
+
const result = await port.query({ ...fixtures.query, limit: 1 });
|
|
546
|
+
expect(result.hits.length <= 1, `returned ${result.hits.length} hits for a limit of 1`);
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: "search.returns-identifiers-not-records",
|
|
551
|
+
title: "a hit carries an identifier a governed read can resolve, not the record",
|
|
552
|
+
async run(port) {
|
|
553
|
+
const result = await port.query(fixtures.query);
|
|
554
|
+
for (const hit of result.hits) {
|
|
555
|
+
expect(typeof hit.id === "string" && hit.id.length > 0, "a hit carried no identifier");
|
|
556
|
+
expect(
|
|
557
|
+
Object.keys(hit).every((key) => key === "id" || key === "score"),
|
|
558
|
+
"a hit carried record fields; search results are pointers, and reading one still goes through ProjectionHost"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
function customerRecordPortChecks(fixtures) {
|
|
566
|
+
return [
|
|
567
|
+
{
|
|
568
|
+
// This proves redelivery does not *fail*. Whether it duplicates is not
|
|
569
|
+
// observable through this port, which has no read side by design, so
|
|
570
|
+
// the suite says what it can and does not imply more.
|
|
571
|
+
id: "crm.redelivery-does-not-fail",
|
|
572
|
+
title: "recording the same event twice is not an error",
|
|
573
|
+
async run(port) {
|
|
574
|
+
await port.record(fixtures.event);
|
|
575
|
+
await port.record(fixtures.event);
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: "crm.rejects-unidentified-event",
|
|
580
|
+
title: "an event with no stable id is refused",
|
|
581
|
+
async run(port) {
|
|
582
|
+
let refused = false;
|
|
583
|
+
try {
|
|
584
|
+
await port.record({ ...fixtures.event, eventId: "" });
|
|
585
|
+
} catch {
|
|
586
|
+
refused = true;
|
|
587
|
+
}
|
|
588
|
+
expect(refused, "an event with no id was accepted; redelivery would duplicate the record");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
];
|
|
592
|
+
}
|
|
593
|
+
function designTokensPortChecks(theme) {
|
|
594
|
+
return [
|
|
595
|
+
{
|
|
596
|
+
id: "tokens.theme-resolves",
|
|
597
|
+
title: "a known theme resolves to at least one token",
|
|
598
|
+
async run(port) {
|
|
599
|
+
const tokens = await port.resolve(theme);
|
|
600
|
+
expect(Array.isArray(tokens) && tokens.length > 0, `theme "${theme}" resolved to no tokens`);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: "tokens.no-unresolved-aliases",
|
|
605
|
+
title: "no resolved token still carries an alias",
|
|
606
|
+
async run(port) {
|
|
607
|
+
for (const token of await port.resolve(theme)) {
|
|
608
|
+
expect(
|
|
609
|
+
typeof token.value !== "string" || !ALIAS.test(token.value.trim()),
|
|
610
|
+
`token "${token.name}" resolved to the unfollowed alias ${String(token.value)}`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
id: "tokens.names-are-unique",
|
|
617
|
+
title: "token names are unique within a theme",
|
|
618
|
+
async run(port) {
|
|
619
|
+
const names = (await port.resolve(theme)).map((token) => token.name);
|
|
620
|
+
expect(new Set(names).size === names.length, "the theme resolved duplicate token names");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
async function runPortContract(port, checks) {
|
|
626
|
+
const failures = [];
|
|
627
|
+
for (const check of checks) {
|
|
628
|
+
try {
|
|
629
|
+
await check.run(port);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
failures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return { passed: failures.length === 0, failures };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export {
|
|
638
|
+
resolveDesignTokens,
|
|
639
|
+
IDENTITY_ACTOR_TYPES,
|
|
640
|
+
ActorScopeError,
|
|
641
|
+
assertActorClaimsCoverScope,
|
|
642
|
+
actorContextFromClaims,
|
|
643
|
+
definePort,
|
|
644
|
+
assertPortSuiteHasTeeth,
|
|
645
|
+
certifyAdapter,
|
|
646
|
+
validatePortRequirements,
|
|
647
|
+
assertPortRequirementsSatisfied,
|
|
648
|
+
flagsPortChecks,
|
|
649
|
+
identityPortChecks,
|
|
650
|
+
contentPortChecks,
|
|
651
|
+
paymentsPortChecks,
|
|
652
|
+
searchPortChecks,
|
|
653
|
+
customerRecordPortChecks,
|
|
654
|
+
designTokensPortChecks,
|
|
655
|
+
runPortContract
|
|
656
|
+
};
|
|
657
|
+
//# sourceMappingURL=chunk-FH6OL7JX.js.map
|