@phis/contracts 0.5.0 → 0.7.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.
@@ -0,0 +1,385 @@
1
+ /**
2
+ * The approved v1 Payment agreement. Core owns execution; consumers own prices and commerce.
3
+ *
4
+ * Execution belongs to Core; its implemented capability, service and event identities live in core.ts.
5
+ * These declarations contain no SDK, credentials, persistence or exchange-rate conversion. See phis-server's
6
+ * PAYMENTS.md and PAYMENTS_STRIPE.md for authority, durability and Provider mapping requirements.
7
+ *
8
+ * `amount` is always the consumer's base amount. Under a pricing policy other than `final`/`none` the
9
+ * Provider adds tax, shipping and discounts on top of it and reports the customer's total with its parts
10
+ * (PAYMENTS.md section 12); authorized, captured and refunded funds are measured against that total.
11
+ */
12
+ export type PhisPaymentId = string;
13
+ export type PhisPaymentOperationId = string;
14
+ export type PhisPaymentRefundId = string;
15
+ export type PhisPaymentMoney = {
16
+ /** Uppercase ISO currency code; one agreed currency for the whole payment. */
17
+ currency: string;
18
+ /** Canonical non-negative integer string of minor units, never floating-point money. */
19
+ minor: string;
20
+ };
21
+ export type PhisPaymentCaptureMode = "automatic" | "manual";
22
+ export type PhisPaymentCheckoutKind = "redirect" | "provider-ui";
23
+ export type PhisPaymentFeatures = {
24
+ captureModes: readonly PhisPaymentCaptureMode[];
25
+ checkoutKinds: readonly PhisPaymentCheckoutKind[];
26
+ canPartialCapture: boolean;
27
+ canCancel: boolean;
28
+ canRefund: boolean;
29
+ canPartialRefund: boolean;
30
+ pricing: PhisPaymentPricingFeatures;
31
+ };
32
+ export type PhisPaymentMethod = {
33
+ profileKey: string;
34
+ methodKey: string | null;
35
+ title: string;
36
+ features: PhisPaymentFeatures;
37
+ };
38
+ export type PhisPaymentCheckoutPlan = {
39
+ kind: "redirect";
40
+ url: string;
41
+ expiresAt: string | null;
42
+ } | {
43
+ kind: "provider-ui";
44
+ moduleId: string;
45
+ adapterKey: string;
46
+ parameters: Readonly<Record<string, string>>;
47
+ expiresAt: string | null;
48
+ };
49
+ export type PhisPaymentStatus = "created" | "awaiting_customer" | "processing" | "authorized" | "captured" | "canceled" | "expired" | "failed";
50
+ export type PhisPayment = {
51
+ id: PhisPaymentId;
52
+ reference: string;
53
+ profileKey: string;
54
+ mode: "test" | "live";
55
+ /** The consumer's base amount; the customer pays `totals.total` once the Provider reports it. */
56
+ amount: PhisPaymentMoney;
57
+ pricing: PhisPaymentPricing;
58
+ totals: PhisPaymentTotals | null;
59
+ captureMode: PhisPaymentCaptureMode;
60
+ status: PhisPaymentStatus;
61
+ authorizedMinor: string;
62
+ capturedMinor: string;
63
+ refundedMinor: string;
64
+ authorizationExpiresAt: string | null;
65
+ checkout: PhisPaymentCheckoutPlan | null;
66
+ reconciliationRequired: boolean;
67
+ version: number;
68
+ lastObservedAt: string | null;
69
+ };
70
+ export type PhisPaymentErrorCode = "invalid_input" | "not_found" | "forbidden" | "profile_unavailable" | "method_unavailable" | "unsupported_operation" | "invalid_state" | "active_attempt_exists" | "idempotency_conflict" | "payment_declined" | "provider_rejected" | "provider_unavailable" | "reconciliation_required";
71
+ export type PhisPaymentOperation = {
72
+ id: PhisPaymentOperationId;
73
+ paymentId: PhisPaymentId;
74
+ action: "create" | "complete" | "capture" | "cancel" | "refund";
75
+ state: "reserved" | "pending" | "succeeded" | "failed" | "indeterminate";
76
+ errorCode: PhisPaymentErrorCode | null;
77
+ };
78
+ export type PhisPaymentRefund = {
79
+ id: PhisPaymentRefundId;
80
+ paymentId: PhisPaymentId;
81
+ amount: PhisPaymentMoney;
82
+ /** The tax or discount share of this refund when the Provider reports one; informational. */
83
+ additions: readonly PhisPaymentAddition[] | null;
84
+ status: "pending" | "succeeded" | "failed" | "canceled";
85
+ };
86
+ export type PhisPaymentCommandResult = {
87
+ operation: PhisPaymentOperation;
88
+ payment: PhisPayment;
89
+ refund: PhisPaymentRefund | null;
90
+ };
91
+ export type PhisPaymentCreateInput = {
92
+ profileKey: string;
93
+ reference: string;
94
+ idempotencyKey: string;
95
+ amount: PhisPaymentMoney;
96
+ pricing: PhisPaymentPricing;
97
+ description: string;
98
+ captureMode: PhisPaymentCaptureMode;
99
+ methodKey: string | null;
100
+ checkoutKind: PhisPaymentCheckoutKind;
101
+ returnPath: string;
102
+ };
103
+ export type PhisPaymentCommandInput = {
104
+ paymentId: PhisPaymentId;
105
+ idempotencyKey: string;
106
+ };
107
+ export interface PhisPaymentsCapabilityV1 {
108
+ listMethods(input: {
109
+ profileKey: string;
110
+ amount: PhisPaymentMoney;
111
+ captureMode: PhisPaymentCaptureMode;
112
+ checkoutKind: PhisPaymentCheckoutKind;
113
+ pricing: PhisPaymentPricingSelector;
114
+ }): Promise<readonly PhisPaymentMethod[]>;
115
+ /** Core binds Site/consumer/profile; the consumer checks access to its business resource. */
116
+ create(input: PhisPaymentCreateInput): Promise<PhisPaymentCommandResult>;
117
+ get(paymentId: PhisPaymentId): Promise<PhisPayment | null>;
118
+ getOperation(operationId: PhisPaymentOperationId): Promise<PhisPaymentOperation | null>;
119
+ getRefund(refundId: PhisPaymentRefundId): Promise<PhisPaymentRefund | null>;
120
+ /** Customer continuation; never captures a manually authorized payment. */
121
+ complete(input: PhisPaymentCommandInput): Promise<PhisPaymentCommandResult>;
122
+ /** Explicit bounded reconciliation that may update state and enqueue a consumer event. */
123
+ refresh(paymentId: PhisPaymentId): Promise<PhisPayment | null>;
124
+ /** One final merchant-authorized capture; minor units inherit the stored currency. */
125
+ capture(input: PhisPaymentCommandInput & {
126
+ minor: string;
127
+ }): Promise<PhisPaymentCommandResult>;
128
+ cancel(input: PhisPaymentCommandInput): Promise<PhisPaymentCommandResult>;
129
+ /** Merchant-authorized refund; pending/unknown amounts reserve the captured balance. */
130
+ refund(input: PhisPaymentCommandInput & {
131
+ minor: string;
132
+ reason: string;
133
+ }): Promise<PhisPaymentCommandResult>;
134
+ }
135
+ export type PhisPaymentProviderConfigV1 = {
136
+ profileKey: string;
137
+ mode: "test" | "live";
138
+ merchantIdentity: string | null;
139
+ };
140
+ export type PhisPaymentProviderContextV1 = {
141
+ providerKey: string;
142
+ readCredentials(): Promise<Readonly<Record<string, string>> | null>;
143
+ };
144
+ export type PhisPaymentProviderBinding = {
145
+ reference: string;
146
+ data: Readonly<Record<string, string>>;
147
+ };
148
+ export type PhisPaymentProviderProbe = {
149
+ merchantIdentity: string;
150
+ mode: "test" | "live";
151
+ methods: readonly Omit<PhisPaymentMethod, "profileKey">[];
152
+ };
153
+ export type PhisPaymentProviderObservation = {
154
+ binding: PhisPaymentProviderBinding;
155
+ merchantIdentity: string;
156
+ mode: "test" | "live";
157
+ status: PhisPaymentStatus;
158
+ /** The base amount as created; a Provider never changes it. */
159
+ amount: PhisPaymentMoney;
160
+ /** Null only while the Provider cannot yet know the total; required from `authorized` on. */
161
+ totals: PhisPaymentObservedTotals | null;
162
+ authorizedMinor: string;
163
+ capturedMinor: string;
164
+ authorizationExpiresAt: string | null;
165
+ checkout: PhisPaymentCheckoutPlan | null;
166
+ /** Complete refund observations, including remote refunds created outside Core. */
167
+ refunds: readonly {
168
+ operationId: PhisPaymentOperationId | null;
169
+ binding: PhisPaymentProviderBinding;
170
+ minor: string;
171
+ status: PhisPaymentRefund["status"];
172
+ additions: readonly PhisPaymentAddition[] | null;
173
+ }[];
174
+ };
175
+ export type PhisPaymentProviderCommand = {
176
+ operationId: PhisPaymentOperationId;
177
+ paymentId: PhisPaymentId;
178
+ providerIdempotencyKey: string;
179
+ signal: AbortSignal;
180
+ } & ({
181
+ action: "create";
182
+ amount: PhisPaymentMoney;
183
+ pricing: PhisPaymentPricing;
184
+ description: string;
185
+ captureMode: PhisPaymentCaptureMode;
186
+ methodKey: string | null;
187
+ checkoutKind: PhisPaymentCheckoutKind;
188
+ returnUrl: string;
189
+ notificationUrl: string;
190
+ } | {
191
+ action: "complete" | "cancel";
192
+ binding: PhisPaymentProviderBinding;
193
+ } | {
194
+ action: "capture";
195
+ binding: PhisPaymentProviderBinding;
196
+ minor: string;
197
+ } | {
198
+ action: "refund";
199
+ binding: PhisPaymentProviderBinding;
200
+ minor: string;
201
+ reason: string;
202
+ });
203
+ export type PhisPaymentProviderExecution = {
204
+ outcome: "applied" | "pending";
205
+ observation: PhisPaymentProviderObservation;
206
+ } | {
207
+ outcome: "rejected";
208
+ code: PhisPaymentErrorCode;
209
+ } | {
210
+ outcome: "unknown";
211
+ };
212
+ export type PhisPaymentProviderNotification = {
213
+ outcome: "verified";
214
+ deliveryId: string;
215
+ reference: string;
216
+ } | {
217
+ outcome: "lookup-required";
218
+ reference: string;
219
+ } | {
220
+ outcome: "irrelevant";
221
+ } | {
222
+ outcome: "rejected";
223
+ } | {
224
+ outcome: "unavailable";
225
+ };
226
+ export interface PhisPaymentProviderV1 {
227
+ probe(input: {
228
+ amount: PhisPaymentMoney;
229
+ captureMode: PhisPaymentCaptureMode;
230
+ checkoutKind: PhisPaymentCheckoutKind;
231
+ pricing: PhisPaymentPricingSelector;
232
+ signal: AbortSignal;
233
+ }): Promise<PhisPaymentProviderProbe>;
234
+ execute(input: PhisPaymentProviderCommand): Promise<PhisPaymentProviderExecution>;
235
+ observe(input: {
236
+ binding: PhisPaymentProviderBinding;
237
+ signal: AbortSignal;
238
+ }): Promise<PhisPaymentProviderObservation>;
239
+ /** Recover the same command; never replace its key, merchant, mode or Provider. */
240
+ recover(input: {
241
+ command: PhisPaymentProviderCommand;
242
+ binding: PhisPaymentProviderBinding | null;
243
+ }): Promise<PhisPaymentProviderExecution>;
244
+ verifyNotification(input: {
245
+ method: string;
246
+ headers: Readonly<Record<string, string>>;
247
+ body: Uint8Array;
248
+ signal: AbortSignal;
249
+ }): Promise<PhisPaymentProviderNotification>;
250
+ }
251
+ export interface PhisPaymentNotificationsCapabilityV1 {
252
+ receive(input: {
253
+ profileKey: string;
254
+ }): Promise<{
255
+ outcome: "processed" | "duplicate" | "ignored";
256
+ }>;
257
+ }
258
+ export type PhisPaymentGrantedOperation = "read" | "create" | "complete" | "refresh" | "capture" | "cancel" | "refund";
259
+ export type PhisPaymentMerchantActionPolicy = {
260
+ roles: readonly string[];
261
+ jobs: readonly string[];
262
+ events: readonly string[];
263
+ };
264
+ export type PhisPaymentProfileGrant = {
265
+ consumerAddonId: string;
266
+ operations: readonly PhisPaymentGrantedOperation[];
267
+ merchantActions: {
268
+ capture: PhisPaymentMerchantActionPolicy;
269
+ cancel: PhisPaymentMerchantActionPolicy;
270
+ refund: PhisPaymentMerchantActionPolicy;
271
+ };
272
+ };
273
+ /** Operator-controlled references and grants; contains no credential values. */
274
+ export type PhisPaymentProfileDraft = {
275
+ key: string;
276
+ providerKey: string;
277
+ mode: "test" | "live";
278
+ credentialReferences: Readonly<Record<string, string>>;
279
+ grants: readonly PhisPaymentProfileGrant[];
280
+ };
281
+ /** A payment event reaches only the consuming Add-on stored on that payment. */
282
+ export type PhisPaymentChangedEventFacts = {
283
+ event: "payment.changed";
284
+ subject: {
285
+ kind: "payment";
286
+ paymentId: PhisPaymentId;
287
+ reference: string;
288
+ version: number;
289
+ operationId: PhisPaymentOperationId | null;
290
+ refundId: PhisPaymentRefundId | null;
291
+ };
292
+ };
293
+ export type PhisPaymentPricingBasis = "final" | "net";
294
+ export type PhisPaymentTaxHandling = "none" | "provider" | "merchant-of-record";
295
+ export type PhisPaymentPromotionCodes = "none" | "provider";
296
+ /** What is sold, never a rate; each Provider maps the closed vocabulary to its own codes. */
297
+ export type PhisPaymentTaxCategory = "general" | "reduced-goods" | "digital-goods" | "digital-service" | "service" | "shipping" | "exempt";
298
+ export type PhisPaymentLineItem = {
299
+ reference: string;
300
+ description: string;
301
+ quantity: number;
302
+ unitMinor: string;
303
+ taxCategory: PhisPaymentTaxCategory;
304
+ /** Provider-specific code overriding the category mapping, keyed by provider key. */
305
+ providerTaxCodes: Readonly<Record<string, string>> | null;
306
+ };
307
+ /** A reduction the consumer decided; the Provider shows it and taxes the reduced amount. */
308
+ export type PhisPaymentDiscount = {
309
+ reference: string;
310
+ label: string;
311
+ minor: string;
312
+ };
313
+ export type PhisPaymentCustomerDetails = {
314
+ country: string;
315
+ postalCode: string | null;
316
+ region: string | null;
317
+ addressSource: "billing" | "shipping";
318
+ taxIds: readonly {
319
+ type: string;
320
+ value: string;
321
+ }[];
322
+ };
323
+ /**
324
+ * `final`/`none` is v1: the base amount is what the customer pays and the Provider adds nothing. Every
325
+ * other policy carries line items; the base amount then equals their sum plus shipping minus discounts.
326
+ */
327
+ export type PhisPaymentPricing = {
328
+ basis: "final";
329
+ tax: "none";
330
+ } | {
331
+ basis: PhisPaymentPricingBasis;
332
+ tax: PhisPaymentTaxHandling;
333
+ lineItems: readonly PhisPaymentLineItem[];
334
+ shipping: {
335
+ minor: string;
336
+ taxCategory: "shipping";
337
+ } | null;
338
+ discounts: readonly PhisPaymentDiscount[];
339
+ promotionCodes: PhisPaymentPromotionCodes;
340
+ customer: PhisPaymentCustomerDetails | null;
341
+ };
342
+ /** The facts of a policy a method must support; `discounts` is the number of stated discounts. */
343
+ export type PhisPaymentPricingSelector = {
344
+ basis: PhisPaymentPricingBasis;
345
+ tax: PhisPaymentTaxHandling;
346
+ discounts: number;
347
+ promotionCodes: PhisPaymentPromotionCodes;
348
+ };
349
+ export type PhisPaymentPricingFeatures = {
350
+ /** Always includes `final`. */
351
+ bases: readonly PhisPaymentPricingBasis[];
352
+ /** Always includes `none`. */
353
+ tax: readonly PhisPaymentTaxHandling[];
354
+ /** How many stated discounts one payment may carry; 0 when the Provider shows none. */
355
+ maxDiscounts: number;
356
+ /** Whether a customer may enter a code on the Provider's page, and whether next to stated discounts. */
357
+ promotionCodes: "none" | "alone" | "with-discounts";
358
+ collectsCustomerDetails: boolean;
359
+ collectsTaxIds: boolean;
360
+ };
361
+ export type PhisPaymentAdditionKind = "tax" | "shipping" | "discount";
362
+ export type PhisPaymentAddition = {
363
+ kind: PhisPaymentAdditionKind;
364
+ minor: string;
365
+ label: string;
366
+ /** Contained in the base amount rather than added to it; a `discount` is never inclusive. */
367
+ inclusive: boolean;
368
+ /** Display-only decimal string such as "8.1"; never enters Core arithmetic. */
369
+ rate: string | null;
370
+ /** ISO country code, optionally followed by "-" and a region. */
371
+ jurisdiction: string | null;
372
+ /** Who owes a tax addition to the authority: the merchant, or the Provider as Merchant of Record. */
373
+ collectedBy: "merchant" | "provider";
374
+ /** The promotion code or stated discount reference behind a `discount`; null for every other kind. */
375
+ code: string | null;
376
+ };
377
+ export type PhisPaymentObservedTotals = {
378
+ total: PhisPaymentMoney;
379
+ additions: readonly PhisPaymentAddition[];
380
+ customer: PhisPaymentCustomerDetails | null;
381
+ };
382
+ export type PhisPaymentTotals = PhisPaymentObservedTotals & {
383
+ /** Set by Core when the total became binding; a differing total afterwards is an inconsistency. */
384
+ frozenAt: string | null;
385
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The approved v1 Payment agreement. Core owns execution; consumers own prices and commerce.
3
+ *
4
+ * Execution belongs to Core; its implemented capability, service and event identities live in core.ts.
5
+ * These declarations contain no SDK, credentials, persistence or exchange-rate conversion. See phis-server's
6
+ * PAYMENTS.md and PAYMENTS_STRIPE.md for authority, durability and Provider mapping requirements.
7
+ *
8
+ * `amount` is always the consumer's base amount. Under a pricing policy other than `final`/`none` the
9
+ * Provider adds tax, shipping and discounts on top of it and reports the customer's total with its parts
10
+ * (PAYMENTS.md section 12); authorized, captured and refunded funds are measured against that total.
11
+ */
12
+ export {};
@@ -98,6 +98,15 @@ export type PhiLinkTarget = {
98
98
  * goes. Whether the reader may follow it is still the target Area's own answer.
99
99
  */
100
100
  area?: PhiCmsAreaKey;
101
+ /**
102
+ * The values of the Page's route parameters, where its route has any (`/news/:slug`).
103
+ *
104
+ * A reference names the route and not one address of it, so a link to one entry of a family of
105
+ * Pages states which: `{ slug: "launch" }`. The resolver answers the route's pattern, and the link
106
+ * fills it from here and from what its caller adds -- a list that links each of its entries states
107
+ * the route once and the value per entry. A parameter nobody fills leaves no link at all.
108
+ */
109
+ params?: Readonly<Record<string, string>>;
101
110
  fragment?: string | null;
102
111
  newTab?: boolean;
103
112
  } | {
@@ -121,10 +130,10 @@ export declare const PHI_LINK_TARGET_CONFIG_KEY = "linkTarget";
121
130
  export declare function isPhiLinkTargetConfigKey(key: string): boolean;
122
131
  export declare function isPhiStorableExternalHref(value: string): boolean;
123
132
  /**
124
- * A stored link target, read back under the contract rather than trusted.
133
+ * A route pattern with its parameters filled, or `null` while one is still open.
125
134
  *
126
- * Anything that is not one of the two shapes answers `null`, and a Control that gets `null` draws no
127
- * link at all. That is the same answer the contract gives for a Page reference that no longer resolves:
128
- * non-interactive text beats a link that goes somewhere nobody chose.
135
+ * Each value is one path segment, encoded as one: a slug with a slash in it cannot step into another
136
+ * route. A pattern without parameters comes back as it is.
129
137
  */
138
+ export declare function fillPhiRouteParams(pattern: string, params: Readonly<Record<string, string>> | null | undefined): string | null;
130
139
  export declare function readPhiLinkTarget(value: unknown): PhiLinkTarget | null;
@@ -174,6 +174,37 @@ export function isPhiStorableExternalHref(value) {
174
174
  * link at all. That is the same answer the contract gives for a Page reference that no longer resolves:
175
175
  * non-interactive text beats a link that goes somewhere nobody chose.
176
176
  */
177
+ const PHI_ROUTE_PARAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/u;
178
+ /** Route parameter values as a target stores them: names a route can declare, non-empty string values. */
179
+ function readPhiRouteParams(value) {
180
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
181
+ return null;
182
+ }
183
+ const entries = Object.entries(value).flatMap(([name, entry]) => PHI_ROUTE_PARAM_NAME_PATTERN.test(name) && typeof entry === "string" && entry.trim()
184
+ ? [[name, entry.trim()]]
185
+ : []);
186
+ return entries.length > 0 ? Object.fromEntries(entries) : null;
187
+ }
188
+ /**
189
+ * A route pattern with its parameters filled, or `null` while one is still open.
190
+ *
191
+ * Each value is one path segment, encoded as one: a slug with a slash in it cannot step into another
192
+ * route. A pattern without parameters comes back as it is.
193
+ */
194
+ export function fillPhiRouteParams(pattern, params) {
195
+ let open = false;
196
+ const filled = pattern.split("/").map((segment) => {
197
+ if (!segment.startsWith(":"))
198
+ return segment;
199
+ const value = params?.[segment.slice(1)];
200
+ if (!value) {
201
+ open = true;
202
+ return segment;
203
+ }
204
+ return encodeURIComponent(value);
205
+ }).join("/");
206
+ return open ? null : filled;
207
+ }
177
208
  export function readPhiLinkTarget(value) {
178
209
  if (!value || typeof value !== "object" || Array.isArray(value)) {
179
210
  return null;
@@ -192,10 +223,12 @@ export function readPhiLinkTarget(value) {
192
223
  * the asking Area is the behaviour of every link that names none, which is the honest default.
193
224
  */
194
225
  const area = isPhiCmsAreaKey(record.area) ? { area: record.area } : {};
226
+ const params = readPhiRouteParams(record.params);
195
227
  return {
196
228
  kind: "page",
197
229
  reference: parsed.reference,
198
230
  ...area,
231
+ ...(params ? { params } : {}),
199
232
  ...(fragment ? { fragment } : {}),
200
233
  ...newTab,
201
234
  };
@@ -59,8 +59,9 @@ export type PhisResourceLink = {
59
59
  *
60
60
  * Bounded twice: to this Add-on's own links, and to subjects the acting user may reach. Writing needs
61
61
  * an actor -- a link is a person saying these two things belong together, and it is what later lets the
62
- * Add-on act with no person there. A hook or a job may read the links it wrote and may not make new
63
- * ones.
62
+ * Add-on act with no person there. A hook or a job may read the links it wrote and may make new ones only
63
+ * on what it opened itself: a thread it owns -- or a ticket or task whose thread it owns -- it reaches
64
+ * already, so a link there grants nothing that needed anybody's consent.
64
65
  */
65
66
  export type PhisResourceLinksCapabilityV1 = {
66
67
  /** This Add-on's links on one subject, or none where the subject is out of reach. */
@@ -68,8 +69,10 @@ export type PhisResourceLinksCapabilityV1 = {
68
69
  /**
69
70
  * Records a reference, or hands back the one that is already there.
70
71
  *
71
- * Asking twice is how an id is learned, the way a declared group is: `(type, externalId)` is unique
72
- * within this Add-on and this Site, so a second call answers with the first link instead of failing.
72
+ * Asking twice is how an id is learned, the way a declared group is: `(subject, type, externalId)` is
73
+ * unique within this Add-on and this Site, so a second call for the same subject answers with the first
74
+ * link instead of failing. One external record may be linked from several subjects -- one issue that
75
+ * several tickets wait on -- and each of those links is its own.
73
76
  */
74
77
  link(input: {
75
78
  subject: PhisResourceLinkSubject;
@@ -86,9 +89,12 @@ export type PhisResourceLinksCapabilityV1 = {
86
89
  }): Promise<boolean>;
87
90
  /** Removes a link, and with it whatever actorless reach it granted. */
88
91
  unlink(linkId: number): Promise<boolean>;
89
- /** One link of this Add-on by the external id it recorded, or null. */
90
- findByExternalId(input: {
92
+ /**
93
+ * Every link of this Add-on to one external record, oldest first: what an event about that record has
94
+ * to reach. Empty where none is recorded.
95
+ */
96
+ listByExternalId(input: {
91
97
  type: number;
92
98
  externalId: string;
93
- }): Promise<PhisResourceLink | null>;
99
+ }): Promise<PhisResourceLink[]>;
94
100
  };
package/dist/schema.d.ts CHANGED
@@ -235,6 +235,15 @@ export type PhisAddonAssetSlotDescriptor = {
235
235
  * newest of several would be a fact nobody asked for.
236
236
  */
237
237
  mirrorColumn?: string;
238
+ /**
239
+ * A role this Add-on declares, which the actor must hold to put a file in this slot, take one out,
240
+ * or list what it holds -- the same guard `requiresRole` is on a declared query, refused loudly.
241
+ *
242
+ * On an owner- or group-scoped table it narrows: the row's own authority still has to agree. On a
243
+ * table with neither, Core has no row to judge, so a slot there must state one, and the role is then
244
+ * the whole authority -- which is what lets a category icon or a shop logo carry a file at all.
245
+ */
246
+ requiresRole?: string;
238
247
  };
239
248
  export type PhisAddonTableDescriptor = {
240
249
  name: string;