@fabricorg/ports 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -133,6 +133,191 @@ declare function actorContextFromClaims(claims: ActorClaims, scope: {
133
133
  tenantId: string;
134
134
  spaceId: string;
135
135
  };
136
+ /**
137
+ * A resolved piece of authored content.
138
+ *
139
+ * Content is copy and layout, never permission. A content system deciding what
140
+ * a screen says is expected; a content system deciding what a screen may reach
141
+ * is the failure this whole boundary exists to prevent, which is why nothing
142
+ * here carries a capability reference.
143
+ */
144
+ interface ContentEntry {
145
+ key: string;
146
+ locale: string;
147
+ /** Opaque revision, stable for identical content. Use it for cache keys. */
148
+ revision: string;
149
+ value: JsonLike;
150
+ }
151
+ type JsonLike = string | number | boolean | null | JsonLike[] | {
152
+ [key: string]: JsonLike;
153
+ };
154
+ interface ContentQuery {
155
+ key: string;
156
+ locale: string;
157
+ /** Optional variant, for an experiment arm already enumerated in a release. */
158
+ variant?: string;
159
+ }
160
+ interface ContentPort {
161
+ /** Resolves to `null` when the key is not authored, rather than inventing a default. */
162
+ resolve(query: ContentQuery): Promise<ContentEntry | null>;
163
+ }
164
+ /**
165
+ * Money as an integer in the currency's minor unit.
166
+ *
167
+ * Never a float. A payment expressed as 10.1 is a payment that will eventually
168
+ * be off by a cent, and reconciling that costs more than the type ever did.
169
+ */
170
+ interface MinorUnitAmount {
171
+ /** Integer in the minor unit: 1050 is USD 10.50. */
172
+ amount: number;
173
+ /** ISO 4217 alphabetic code. */
174
+ currency: string;
175
+ }
176
+ /**
177
+ * The outcome of asking a provider to move money.
178
+ *
179
+ * `ambiguous` is the one that matters and the one most interfaces omit. A
180
+ * timeout tells you nothing about whether the charge landed, and guessing is
181
+ * how a customer gets billed twice. It maps onto the platform's own
182
+ * `AdapterOutcomeKind`, so an ambiguous payment routes to reconciliation with
183
+ * durable evidence instead of being retried.
184
+ */
185
+ type PaymentOutcome = "succeeded" | "failed" | "ambiguous";
186
+ interface PaymentResult {
187
+ outcome: PaymentOutcome;
188
+ /** The provider's own identifier, for reconciliation. Required when known. */
189
+ providerReference?: string;
190
+ /** Provider-supplied reason, for evidence rather than for branching. */
191
+ reason?: string;
192
+ }
193
+ interface PaymentRequest {
194
+ /** Caller-stable key. The same key must never move money twice. */
195
+ idempotencyKey: string;
196
+ amount: MinorUnitAmount;
197
+ }
198
+ interface PaymentsPort {
199
+ charge(request: PaymentRequest): Promise<PaymentResult>;
200
+ }
201
+ /**
202
+ * A latency-critical read path, deliberately outside the governed pipeline.
203
+ *
204
+ * Search results are pointers, not authority. An identifier appearing in a
205
+ * result set says the index knows about it, never that this actor may read it,
206
+ * so anything the viewer then opens still goes through `ProjectionHost`.
207
+ */
208
+ interface SearchHit {
209
+ /** Identifier a governed read can resolve. Never the record itself. */
210
+ id: string;
211
+ score: number;
212
+ }
213
+ interface SearchQuery {
214
+ text: string;
215
+ limit: number;
216
+ /** Narrowing the index can apply cheaply. Never a substitute for authorization. */
217
+ filters?: Record<string, string>;
218
+ }
219
+ interface SearchPort {
220
+ query(query: SearchQuery): Promise<{
221
+ hits: SearchHit[];
222
+ total: number;
223
+ }>;
224
+ }
225
+ /**
226
+ * An outbound record of something that already happened.
227
+ *
228
+ * A CRM is downstream of the governed pipeline, never upstream of it. It is
229
+ * told; it does not decide. Keeping this one-way is what stops a marketing
230
+ * system becoming a source of business truth.
231
+ */
232
+ interface CustomerEvent {
233
+ /** Stable key so redelivery does not duplicate the record. */
234
+ eventId: string;
235
+ subjectId: string;
236
+ type: string;
237
+ occurredAt: string;
238
+ attributes?: Record<string, JsonLike>;
239
+ }
240
+ interface CustomerRecordPort {
241
+ record(event: CustomerEvent): Promise<void>;
242
+ }
243
+ /**
244
+ * A port, as a first-class thing rather than a name in a string.
245
+ *
246
+ * Fabric ships a handful of port definitions because they speak open standards
247
+ * and everyone needs them. It cannot ship the rest: an enterprise integrates
248
+ * hundreds of external systems, and a framework that requires a pull request
249
+ * for each one is not a framework. A vertical, a partner or a vendor defines
250
+ * its own port with {@link definePort} and every mechanism here applies to it
251
+ * unchanged — Fabric never learns what the system behind it is.
252
+ *
253
+ * The definition binds an id to the suite that certifies an adapter for it.
254
+ * That binding is the point: without it, "this port is satisfied" only ever
255
+ * meant "somebody registered something claiming to be it".
256
+ */
257
+ interface PortDefinition<TPort, TFixtures = void> {
258
+ /**
259
+ * Namespaced identifier, for example `fabric.flags` or `acme.docusign`.
260
+ * Namespacing is required so a port defined outside this repository cannot
261
+ * collide with one defined inside it.
262
+ */
263
+ id: string;
264
+ /**
265
+ * Version of the port contract itself, not of any adapter.
266
+ *
267
+ * Bump it when the interface changes shape. Certification is recorded
268
+ * against a version, so raising it correctly invalidates every adapter
269
+ * certified against the old contract instead of silently carrying them
270
+ * forward.
271
+ */
272
+ version: string;
273
+ /** The open standard this port speaks, where one exists. */
274
+ standard?: {
275
+ name: string;
276
+ version?: string;
277
+ };
278
+ /** One-line statement of what an adapter behind this port is responsible for. */
279
+ description: string;
280
+ /** The suite every adapter must pass. */
281
+ checks(fixtures: TFixtures): readonly PortCheck<TPort>[];
282
+ }
283
+ /**
284
+ * Declare a port. Validates the definition itself, because a malformed port
285
+ * definition produces adapters that certify against nothing.
286
+ */
287
+ declare function definePort<TPort, TFixtures = void>(definition: PortDefinition<TPort, TFixtures>): PortDefinition<TPort, TFixtures>;
288
+ /**
289
+ * Confirm a port's suite can actually fail, and that its check ids are unique.
290
+ *
291
+ * A suite whose checks all pass against a deliberately broken adapter certifies
292
+ * nothing while looking rigorous, which is worse than having no suite at all.
293
+ * Run this against a stub that does the wrong thing when you author a port.
294
+ */
295
+ declare function assertPortSuiteHasTeeth<TPort, TFixtures>(definition: PortDefinition<TPort, TFixtures>, fixtures: TFixtures, brokenAdapters: TPort | readonly TPort[]): Promise<void>;
296
+ /** Evidence that an adapter passed a port's suite, and which contract it passed. */
297
+ interface AdapterCertification {
298
+ portId: string;
299
+ /** Port contract version the adapter was certified against. */
300
+ portVersion: string;
301
+ checks: string[];
302
+ certifiedAt: string;
303
+ }
304
+ interface CertifiedAdapter extends RegisteredAdapter {
305
+ certification: AdapterCertification;
306
+ }
307
+ /**
308
+ * Run a port's suite against an adapter and record what it passed.
309
+ *
310
+ * Registration is a claim; certification is evidence. Deployment gates can then
311
+ * require the second rather than accepting the first.
312
+ */
313
+ declare function certifyAdapter<TPort, TFixtures>(input: {
314
+ definition: PortDefinition<TPort, TFixtures>;
315
+ fixtures: TFixtures;
316
+ adapter: TPort;
317
+ vendor: string;
318
+ version?: string;
319
+ now?: Date;
320
+ }): Promise<CertifiedAdapter>;
136
321
  interface PortRequirement {
137
322
  name: string;
138
323
  version?: string;
@@ -158,7 +343,7 @@ interface RegisteredAdapter {
158
343
  version?: string;
159
344
  };
160
345
  }
161
- type PortFindingCode = "unsatisfied_port" | "standard_not_spoken" | "version_mismatch";
346
+ type PortFindingCode = "unsatisfied_port" | "standard_not_spoken" | "version_mismatch" | "uncertified_adapter" | "stale_certification";
162
347
  interface PortFinding {
163
348
  code: PortFindingCode;
164
349
  port: string;
@@ -179,10 +364,23 @@ interface PortValidationResult {
179
364
  declare function validatePortRequirements(input: {
180
365
  capability: PortRequiringCapability;
181
366
  adapters: readonly RegisteredAdapter[];
367
+ /**
368
+ * Port definitions in force. Supplying them checks that whatever
369
+ * certification is present was issued against the current contract version.
370
+ *
371
+ * They do not by themselves require certification to exist: pass
372
+ * `requireCertification` for that. Definitions alone catch a stale
373
+ * certification, not a missing one, and an adapter carrying none passes.
374
+ */
375
+ definitions?: readonly PortDefinition<never, never>[];
376
+ /** Require certification for every required port. Defaults to false. */
377
+ requireCertification?: boolean;
182
378
  }): PortValidationResult;
183
379
  declare function assertPortRequirementsSatisfied(input: {
184
380
  capability: PortRequiringCapability;
185
381
  adapters: readonly RegisteredAdapter[];
382
+ definitions?: readonly PortDefinition<never, never>[];
383
+ requireCertification?: boolean;
186
384
  }): void;
187
385
  interface PortCheck<TPort> {
188
386
  id: string;
@@ -208,6 +406,23 @@ declare function identityPortChecks(fixtures: {
208
406
  expected: Pick<ActorClaims, "subject" | "tenantId">;
209
407
  expiredCredential?: string;
210
408
  }): PortCheck<IdentityPort>[];
409
+ /** The suite a content adapter must pass. */
410
+ declare function contentPortChecks(fixtures: {
411
+ present: ContentQuery;
412
+ absentKey: string;
413
+ }): PortCheck<ContentPort>[];
414
+ /** The suite a payments adapter must pass. */
415
+ declare function paymentsPortChecks(fixtures: {
416
+ request: PaymentRequest;
417
+ }): PortCheck<PaymentsPort>[];
418
+ /** The suite a search adapter must pass. */
419
+ declare function searchPortChecks(fixtures: {
420
+ query: SearchQuery;
421
+ }): PortCheck<SearchPort>[];
422
+ /** The suite a customer-record adapter must pass. */
423
+ declare function customerRecordPortChecks(fixtures: {
424
+ event: CustomerEvent;
425
+ }): PortCheck<CustomerRecordPort>[];
211
426
  declare function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[];
212
427
  /** Runs a contract suite and returns every failure, rather than stopping at the first. */
213
428
  declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{
@@ -218,4 +433,4 @@ declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<
218
433
  }>;
219
434
  }>;
220
435
 
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 };
436
+ export { type ActorClaims, ActorScopeError, type ActorScopeRejection, type ActorSpaceCoverage, type AdapterCertification, type CertifiedAdapter, type ContentEntry, type ContentPort, type ContentQuery, type CustomerEvent, type CustomerRecordPort, type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, IDENTITY_ACTOR_TYPES, type IdentityActorType, type IdentityPort, type JsonLike, type MinorUnitAmount, type PaymentOutcome, type PaymentRequest, type PaymentResult, type PaymentsPort, type PortCheck, type PortDefinition, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, type SearchHit, type SearchPort, type SearchQuery, actorContextFromClaims, assertActorClaimsCoverScope, assertPortRequirementsSatisfied, assertPortSuiteHasTeeth, certifyAdapter, contentPortChecks, customerRecordPortChecks, definePort, designTokensPortChecks, flagsPortChecks, identityPortChecks, paymentsPortChecks, resolveDesignTokens, runPortContract, searchPortChecks, validatePortRequirements };
package/dist/index.js CHANGED
@@ -1,353 +1,41 @@
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
- function validatePortRequirements(input) {
129
- const findings = [];
130
- for (const requirement of input.capability.requirements?.ports ?? []) {
131
- const candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);
132
- if (candidates.length === 0) {
133
- findings.push({
134
- code: "unsatisfied_port",
135
- port: requirement.name,
136
- message: `capability "${input.capability.namespace}" requires port "${requirement.name}" and no adapter is registered for it`
137
- });
138
- continue;
139
- }
140
- if (requirement.standard) {
141
- const speaking = candidates.filter((adapter) => adapter.standard?.name === requirement.standard.name);
142
- if (speaking.length === 0) {
143
- findings.push({
144
- code: "standard_not_spoken",
145
- port: requirement.name,
146
- message: `port "${requirement.name}" must speak "${requirement.standard.name}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? "no standard"})`).join(", ")}`
147
- });
148
- continue;
149
- }
150
- }
151
- if (requirement.version && !candidates.some((adapter) => adapter.version === requirement.version)) {
152
- findings.push({
153
- code: "version_mismatch",
154
- port: requirement.name,
155
- message: `port "${requirement.name}" requires version "${requirement.version}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}@${adapter.version ?? "unversioned"}`).join(", ")}`
156
- });
157
- }
158
- }
159
- return { valid: findings.length === 0, findings };
160
- }
161
- function assertPortRequirementsSatisfied(input) {
162
- const result = validatePortRequirements(input);
163
- if (result.valid) return;
164
- throw new Error([
165
- `Port requirements for "${input.capability.namespace}" are not satisfied:`,
166
- ...result.findings.map((finding) => ` - ${finding.message}`)
167
- ].join("\n"));
168
- }
169
- var PortContractFailure = class extends Error {
170
- name = "PortContractFailure";
171
- };
172
- function expect(condition, message) {
173
- if (!condition) throw new PortContractFailure(message);
174
- }
175
- function flagsPortChecks() {
176
- const unknownKey = "fabric.contract.definitely-not-configured";
177
- return [
178
- {
179
- id: "flags.default-on-unknown-key",
180
- title: "an unknown flag resolves to the supplied default rather than throwing",
181
- async run(port) {
182
- const result = await port.resolveBoolean(unknownKey, true);
183
- expect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);
184
- expect(
185
- result.reason === "DEFAULT" || result.reason === "ERROR",
186
- `an unknown flag resolved with reason "${result.reason}"; a default or error was expected`
187
- );
188
- }
189
- },
190
- {
191
- id: "flags.default-is-typed",
192
- title: "each typed resolver returns its own type",
193
- async run(port) {
194
- expect(typeof (await port.resolveBoolean(unknownKey, false)).value === "boolean", "resolveBoolean did not return a boolean");
195
- expect(typeof (await port.resolveString(unknownKey, "fallback")).value === "string", "resolveString did not return a string");
196
- expect(typeof (await port.resolveNumber(unknownKey, 42)).value === "number", "resolveNumber did not return a number");
197
- }
198
- },
199
- {
200
- id: "flags.evaluation-is-pure",
201
- title: "the same key and context resolve the same way twice",
202
- async run(port) {
203
- const context = { targetingKey: "fabric-contract-subject" };
204
- const first = await port.resolveString(unknownKey, "fallback", context);
205
- const second = await port.resolveString(unknownKey, "fallback", context);
206
- expect(first.value === second.value, `the same evaluation returned "${first.value}" then "${second.value}"`);
207
- }
208
- },
209
- {
210
- id: "flags.tolerates-absent-context",
211
- title: "evaluation without a context does not throw",
212
- async run(port) {
213
- await port.resolveBoolean(unknownKey, false);
214
- }
215
- }
216
- ];
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
- }
297
- function designTokensPortChecks(theme) {
298
- return [
299
- {
300
- id: "tokens.theme-resolves",
301
- title: "a known theme resolves to at least one token",
302
- async run(port) {
303
- const tokens = await port.resolve(theme);
304
- expect(Array.isArray(tokens) && tokens.length > 0, `theme "${theme}" resolved to no tokens`);
305
- }
306
- },
307
- {
308
- id: "tokens.no-unresolved-aliases",
309
- title: "no resolved token still carries an alias",
310
- async run(port) {
311
- for (const token of await port.resolve(theme)) {
312
- expect(
313
- typeof token.value !== "string" || !ALIAS.test(token.value.trim()),
314
- `token "${token.name}" resolved to the unfollowed alias ${String(token.value)}`
315
- );
316
- }
317
- }
318
- },
319
- {
320
- id: "tokens.names-are-unique",
321
- title: "token names are unique within a theme",
322
- async run(port) {
323
- const names = (await port.resolve(theme)).map((token) => token.name);
324
- expect(new Set(names).size === names.length, "the theme resolved duplicate token names");
325
- }
326
- }
327
- ];
328
- }
329
- async function runPortContract(port, checks) {
330
- const failures = [];
331
- for (const check of checks) {
332
- try {
333
- await check.run(port);
334
- } catch (error) {
335
- failures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });
336
- }
337
- }
338
- return { passed: failures.length === 0, failures };
339
- }
1
+ import {
2
+ ActorScopeError,
3
+ IDENTITY_ACTOR_TYPES,
4
+ actorContextFromClaims,
5
+ assertActorClaimsCoverScope,
6
+ assertPortRequirementsSatisfied,
7
+ assertPortSuiteHasTeeth,
8
+ certifyAdapter,
9
+ contentPortChecks,
10
+ customerRecordPortChecks,
11
+ definePort,
12
+ designTokensPortChecks,
13
+ flagsPortChecks,
14
+ identityPortChecks,
15
+ paymentsPortChecks,
16
+ resolveDesignTokens,
17
+ runPortContract,
18
+ searchPortChecks,
19
+ validatePortRequirements
20
+ } from "./chunk-HQF57NML.js";
340
21
  export {
341
22
  ActorScopeError,
342
23
  IDENTITY_ACTOR_TYPES,
343
24
  actorContextFromClaims,
344
25
  assertActorClaimsCoverScope,
345
26
  assertPortRequirementsSatisfied,
27
+ assertPortSuiteHasTeeth,
28
+ certifyAdapter,
29
+ contentPortChecks,
30
+ customerRecordPortChecks,
31
+ definePort,
346
32
  designTokensPortChecks,
347
33
  flagsPortChecks,
348
34
  identityPortChecks,
35
+ paymentsPortChecks,
349
36
  resolveDesignTokens,
350
37
  runPortContract,
38
+ searchPortChecks,
351
39
  validatePortRequirements
352
40
  };
353
41
  //# sourceMappingURL=index.js.map