@phis/contracts 0.5.0 → 0.6.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/README.md +23 -5
- package/dist/addon-index.d.ts +1 -0
- package/dist/addon-index.js +1 -0
- package/dist/addon.d.ts +3 -1
- package/dist/capabilities.d.ts +30 -13
- package/dist/cms.d.ts +23 -1
- package/dist/cms.js +32 -1
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +1 -0
- package/dist/core.d.ts +17 -1
- package/dist/core.js +17 -0
- package/dist/payments.d.ts +274 -0
- package/dist/payments.js +8 -0
- package/dist/references.d.ts +13 -4
- package/dist/references.js +33 -0
- package/dist/resource-links.d.ts +13 -7
- package/dist/schema.d.ts +9 -0
- package/dist/support.d.ts +184 -0
- package/dist/support.js +11 -0
- package/dist/threads.d.ts +29 -0
- package/dist/timeline.d.ts +39 -0
- package/dist/timeline.js +53 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ and change at different moments.
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
@phis/contracts/addon what phis and a separately shipped Add-on promise each other
|
|
9
|
+
@phis/contracts/payments the approved Payment consumer, Provider and merchant-profile types
|
|
9
10
|
@phis/contracts/access the authorization vocabulary phis and @phis/ui both evaluate
|
|
10
11
|
@phis/contracts/signals the signal vocabulary and address grammar the UI declares in and phis validates
|
|
11
12
|
@phis/contracts/catalog the Module categories an Add-on declares, the UI groups by and a market filters on
|
|
@@ -45,7 +46,7 @@ The types and constants an Add-on declares or implements. Types and constants on
|
|
|
45
46
|
`server-only`, no React, no Next.
|
|
46
47
|
|
|
47
48
|
An Add-on is compiled to a single ESM artifact and installed into a running phis through the `phis` CLI;
|
|
48
|
-
it is never built into the server. This subpath is the
|
|
49
|
+
it is never built into the server. This subpath is the shared Add-on entrypoint it compiles against, and it must
|
|
49
50
|
not depend on phis itself. For an Add-on author it is a build-time dependency: everything it exports is
|
|
50
51
|
erased at compile time apart from a handful of constants.
|
|
51
52
|
|
|
@@ -61,10 +62,10 @@ What it covers:
|
|
|
61
62
|
(`PhisMediaStorageAdapter`, upload plans, object I/O) and Directory. `PHIS_SERVICE_KINDS` names them
|
|
62
63
|
and `PHIS_SERVICE_INTERFACE_DIGESTS` holds the digest of each kind's current interface; Core refuses an
|
|
63
64
|
Add-on built against a different digest at install.
|
|
64
|
-
- **Core capabilities** -- `PHIS_CORE_CAPABILITIES`, the
|
|
65
|
-
against these constants rather than spelling the ids out.
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
- **Core capabilities** -- `PHIS_CORE_CAPABILITIES`, the catalogue ids an Add-on may require. Declare
|
|
66
|
+
against these constants rather than spelling the ids out. Core separately validates delivery to
|
|
67
|
+
Add-ons; a catalogue id such as `authentication` may serve Site Modules without a corresponding
|
|
68
|
+
Add-on context field. Payment ids accompany the implemented Core capability/service/event catalogue.
|
|
68
69
|
|
|
69
70
|
`PHIS_ADDON_ABI_VERSION` is the single number phis checks an Add-on against.
|
|
70
71
|
|
|
@@ -72,6 +73,23 @@ How an Add-on is laid out, compiled, installed, activated and upgraded is descri
|
|
|
72
73
|
[`ADDON_HOWTO.md`](https://github.com/phis-dev/phis-server/blob/main/ADDON_HOWTO.md); the rules behind it
|
|
73
74
|
are in [`SERVER_ADDONS.md`](https://github.com/phis-dev/phis-server/blob/main/SERVER_ADDONS.md).
|
|
74
75
|
|
|
76
|
+
## `/payments`
|
|
77
|
+
|
|
78
|
+
The approved v1 consumer capability, Provider adapter, checkout plans, normalized money,
|
|
79
|
+
payment/operation/refund records, merchant profile/grant types and owning-consumer event facts.
|
|
80
|
+
All exports are types; there is no secret accessor implementation, checkout SDK, exchange-rate
|
|
81
|
+
conversion or Payment engine here. The same types are re-exported by `/addon`.
|
|
82
|
+
|
|
83
|
+
Consumers own their prices in CHF, EUR, USD or another supported currency. Every payment uses one
|
|
84
|
+
currency; capture and refunds inherit it. Core never infers conversion or settlement currency.
|
|
85
|
+
|
|
86
|
+
Core implements Payment execution separately. Its catalogue exports `payments`, `paymentNotifications`,
|
|
87
|
+
the Payment service kind/interface digest and `payment.changed`. Notification ingress is available
|
|
88
|
+
only in a bound hook context. The Add-on event context discriminates thread and Payment subjects;
|
|
89
|
+
thread producers remain explicitly typed as thread events.
|
|
90
|
+
See the normative [Payment contract](https://github.com/phis-dev/phis-server/blob/main/PAYMENTS.md)
|
|
91
|
+
and [Stripe/profile contract](https://github.com/phis-dev/phis-server/blob/main/PAYMENTS_STRIPE.md).
|
|
92
|
+
|
|
75
93
|
## `/access`
|
|
76
94
|
|
|
77
95
|
Claim shapes, policy shapes, and the evaluator that decides them. Unlike `/addon`, this is runtime code
|
package/dist/addon-index.d.ts
CHANGED
package/dist/addon-index.js
CHANGED
package/dist/addon.d.ts
CHANGED
|
@@ -65,7 +65,9 @@ export type PhisAddonHookDescriptor = {
|
|
|
65
65
|
*
|
|
66
66
|
* Absent is not a lesser variant of present: an unverified hook writes nothing at all -- not through
|
|
67
67
|
* `data:v1`, not by opening a thread -- because nothing distinguishes its caller from anybody who
|
|
68
|
-
* found the address. A hook that needs
|
|
68
|
+
* found the address. A hook that needs ambient write authority declares how it can be believed.
|
|
69
|
+
* Target v1 Payment notification ingress instead verifies through its bound Core capability and
|
|
70
|
+
* reconciles Core-owned payment state without granting ambient hook write authority.
|
|
69
71
|
*/
|
|
70
72
|
verification?: PhisAddonHookVerificationDescriptor;
|
|
71
73
|
/**
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* at the point where it made the mistake.
|
|
13
13
|
*/
|
|
14
14
|
import type { PhisAssetsCapabilityV1 } from "./assets.js";
|
|
15
|
-
import type {
|
|
15
|
+
import type { PhisAddonThreadEventId } from "./core.js";
|
|
16
|
+
import type { PhisPaymentChangedEventFacts, PhisPaymentNotificationsCapabilityV1, PhisPaymentsCapabilityV1 } from "./payments.js";
|
|
16
17
|
import type { PhisDataCapabilityV1 } from "./queries.js";
|
|
17
18
|
import type { PhisResourceLinksCapabilityV1 } from "./resource-links.js";
|
|
18
19
|
import type { PhisSupportCapabilityV1 } from "./support.js";
|
|
@@ -102,6 +103,10 @@ export type PhisAddonCapabilities = {
|
|
|
102
103
|
support?: PhisSupportCapabilityV1;
|
|
103
104
|
settings?: PhisSettingsCapabilityV1;
|
|
104
105
|
roles?: PhisRolesCapabilityV1;
|
|
106
|
+
/** Bound to the declaring consumer and Site when required. */
|
|
107
|
+
payments?: PhisPaymentsCapabilityV1;
|
|
108
|
+
/** Supplied only to an original, bound Payment Provider hook request. */
|
|
109
|
+
paymentNotifications?: PhisPaymentNotificationsCapabilityV1;
|
|
105
110
|
};
|
|
106
111
|
/**
|
|
107
112
|
* The roles capability: `@phis/server/roles:v1`.
|
|
@@ -206,9 +211,19 @@ export type PhisStorageCapabilityV1 = {
|
|
|
206
211
|
* own would be a package that could plant one.
|
|
207
212
|
*/
|
|
208
213
|
export type PhisSecretsCapabilityV1 = {
|
|
209
|
-
/**
|
|
210
|
-
|
|
214
|
+
/**
|
|
215
|
+
* The value, or null when the operator has not configured it.
|
|
216
|
+
*
|
|
217
|
+
* `scope` says which of the two an operator can set: `"site"` (the default) is the one set with
|
|
218
|
+
* `--site` for the Site this call runs on; `"addon"` is the one set without `--site`, shared by every
|
|
219
|
+
* Site the Add-on runs on -- one credential for one external account, such as a single app
|
|
220
|
+
* registration. The two are never substituted for each other: a Site secret that is missing is
|
|
221
|
+
* `null`, not the shared one.
|
|
222
|
+
*/
|
|
223
|
+
read(name: string, scope?: PhisSecretScope): Promise<string | null>;
|
|
211
224
|
};
|
|
225
|
+
/** Which secret `PhisSecretsCapabilityV1.read` names: the Site's own, or the one shared by all Sites. */
|
|
226
|
+
export type PhisSecretScope = "site" | "addon";
|
|
212
227
|
/**
|
|
213
228
|
* What an Add-on may learn about a person.
|
|
214
229
|
*
|
|
@@ -357,24 +372,26 @@ export type PhisAddonJobContext = {
|
|
|
357
372
|
* this Site something, an event is this Site telling an Add-on something, and both run as the Add-on
|
|
358
373
|
* rather than as a person.
|
|
359
374
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
375
|
+
* Thread delivery is bounded by a synced link. The Payment event reaches only its stored
|
|
376
|
+
* consuming Add-on. The subject kind and event discriminate those two ownership models; approved
|
|
377
|
+
* Payment facts are delivered through Core's durable owner-directed outbox.
|
|
363
378
|
*/
|
|
364
|
-
export type
|
|
379
|
+
export type PhisAddonEventFacts = {
|
|
380
|
+
event: PhisAddonThreadEventId;
|
|
381
|
+
subject: {
|
|
382
|
+
kind: "thread";
|
|
383
|
+
threadId: number;
|
|
384
|
+
messageId: number | null;
|
|
385
|
+
};
|
|
386
|
+
} | PhisPaymentChangedEventFacts;
|
|
387
|
+
export type PhisAddonEventContext = PhisAddonEventFacts & {
|
|
365
388
|
addonId: string;
|
|
366
|
-
event: PhisAddonEventId;
|
|
367
389
|
/** This delivery, stable across retries, so a handler can recognise one it already settled. */
|
|
368
390
|
deliveryId: string;
|
|
369
391
|
site: {
|
|
370
392
|
id: number;
|
|
371
393
|
key: string;
|
|
372
394
|
};
|
|
373
|
-
/** The thread the event happened in, and the message where the event is about one. */
|
|
374
|
-
subject: {
|
|
375
|
-
threadId: number;
|
|
376
|
-
messageId: number | null;
|
|
377
|
-
};
|
|
378
395
|
capabilities: PhisAddonCapabilities;
|
|
379
396
|
signal: AbortSignal;
|
|
380
397
|
};
|
package/dist/cms.d.ts
CHANGED
|
@@ -73,7 +73,9 @@ export declare const PhiCmsVisibilityContext: {
|
|
|
73
73
|
* The flags a CMS row carries, in the one numbering both sides read them from.
|
|
74
74
|
*
|
|
75
75
|
* Not every flag reaches every kind of row -- `Collapsed` is a Region's answer and `MobileOnly` a
|
|
76
|
-
* renderable's -- but they share one space, because a row is stored as a row whatever it holds.
|
|
76
|
+
* renderable's -- but they share one space, because a row is stored as a row whatever it holds. The
|
|
77
|
+
* same numbering serves a config's own `flags`, where a Region's and an Overlay's behaviour live
|
|
78
|
+
* (`Sticky`, `NoCloseButton`, ...): a yes-or-no answer in a stored record is a bit, never a boolean.
|
|
77
79
|
*
|
|
78
80
|
* `NoIndex` is a Page's, and it is the Site's answer rather than the preset's: a Module states what a
|
|
79
81
|
* Page it ships starts out as, and the Builder's switch is what decides from then on. That is why it
|
|
@@ -88,6 +90,26 @@ export declare const PhiCmsFlags: {
|
|
|
88
90
|
readonly Collapsed: number;
|
|
89
91
|
readonly NoTranslate: number;
|
|
90
92
|
readonly NoIndex: number;
|
|
93
|
+
/** Stays in view while the page scrolls (Header and Sider families). */
|
|
94
|
+
readonly Sticky: number;
|
|
95
|
+
/** A Sider that takes the viewport height below its offset. */
|
|
96
|
+
readonly FullHeight: number;
|
|
97
|
+
/** A Sider the viewer can fold to its collapsed width. */
|
|
98
|
+
readonly Collapsible: number;
|
|
99
|
+
/** The Header chrome renders no close button. */
|
|
100
|
+
readonly NoCloseButton: number;
|
|
101
|
+
/** Escape does not request dismissal. */
|
|
102
|
+
readonly NoEscapeClose: number;
|
|
103
|
+
/** A Modal centred in the viewport. */
|
|
104
|
+
readonly Centered: number;
|
|
105
|
+
/** A Drawer the viewer can resize. */
|
|
106
|
+
readonly Resizable: number;
|
|
107
|
+
/** A Drawer that pushes a nested Drawer aside (`pushDistance` states how far). */
|
|
108
|
+
readonly Push: number;
|
|
109
|
+
/** Pointer input reaches the page behind the Overlay. */
|
|
110
|
+
readonly MaskPassesPointer: number;
|
|
111
|
+
/** An outside pointer action does not request dismissal. */
|
|
112
|
+
readonly MaskKeepsOpen: number;
|
|
91
113
|
};
|
|
92
114
|
export declare const PhiCmsRevisionFlags: {
|
|
93
115
|
readonly Draft: number;
|
package/dist/cms.js
CHANGED
|
@@ -81,7 +81,9 @@ export const PhiCmsVisibilityContext = {
|
|
|
81
81
|
* The flags a CMS row carries, in the one numbering both sides read them from.
|
|
82
82
|
*
|
|
83
83
|
* Not every flag reaches every kind of row -- `Collapsed` is a Region's answer and `MobileOnly` a
|
|
84
|
-
* renderable's -- but they share one space, because a row is stored as a row whatever it holds.
|
|
84
|
+
* renderable's -- but they share one space, because a row is stored as a row whatever it holds. The
|
|
85
|
+
* same numbering serves a config's own `flags`, where a Region's and an Overlay's behaviour live
|
|
86
|
+
* (`Sticky`, `NoCloseButton`, ...): a yes-or-no answer in a stored record is a bit, never a boolean.
|
|
85
87
|
*
|
|
86
88
|
* `NoIndex` is a Page's, and it is the Site's answer rather than the preset's: a Module states what a
|
|
87
89
|
* Page it ships starts out as, and the Builder's switch is what decides from then on. That is why it
|
|
@@ -96,6 +98,35 @@ export const PhiCmsFlags = {
|
|
|
96
98
|
Collapsed: 1 << 4,
|
|
97
99
|
NoTranslate: 1 << 5,
|
|
98
100
|
NoIndex: 1 << 6,
|
|
101
|
+
/*
|
|
102
|
+
* A Region's behaviour, carried in its config's `flags`: a Region stored on a Page is a config under
|
|
103
|
+
* the Page's `layoutConfig` and has no row of its own, so the config is the one place both kinds have.
|
|
104
|
+
*/
|
|
105
|
+
/** Stays in view while the page scrolls (Header and Sider families). */
|
|
106
|
+
Sticky: 1 << 7,
|
|
107
|
+
/** A Sider that takes the viewport height below its offset. */
|
|
108
|
+
FullHeight: 1 << 8,
|
|
109
|
+
/** A Sider the viewer can fold to its collapsed width. */
|
|
110
|
+
Collapsible: 1 << 9,
|
|
111
|
+
/*
|
|
112
|
+
* An Overlay's behaviour, carried in its config's `flags` the same way. The bits name the departure
|
|
113
|
+
* from the Overlay's defaults, so an unset flag set is the plain Overlay: a close button, Escape
|
|
114
|
+
* closes, the mask stops the pointer and closes on a click.
|
|
115
|
+
*/
|
|
116
|
+
/** The Header chrome renders no close button. */
|
|
117
|
+
NoCloseButton: 1 << 10,
|
|
118
|
+
/** Escape does not request dismissal. */
|
|
119
|
+
NoEscapeClose: 1 << 11,
|
|
120
|
+
/** A Modal centred in the viewport. */
|
|
121
|
+
Centered: 1 << 12,
|
|
122
|
+
/** A Drawer the viewer can resize. */
|
|
123
|
+
Resizable: 1 << 13,
|
|
124
|
+
/** A Drawer that pushes a nested Drawer aside (`pushDistance` states how far). */
|
|
125
|
+
Push: 1 << 14,
|
|
126
|
+
/** Pointer input reaches the page behind the Overlay. */
|
|
127
|
+
MaskPassesPointer: 1 << 15,
|
|
128
|
+
/** An outside pointer action does not request dismissal. */
|
|
129
|
+
MaskKeepsOpen: 1 << 16,
|
|
99
130
|
};
|
|
100
131
|
export const PhiCmsRevisionFlags = {
|
|
101
132
|
Draft: 1 << 0,
|
package/dist/controls.d.ts
CHANGED
|
@@ -26,7 +26,15 @@ export type PhiControlOption<TValue extends string | number = string> = {
|
|
|
26
26
|
* come, so the order has to make sense without the headings.
|
|
27
27
|
*/
|
|
28
28
|
group?: string;
|
|
29
|
+
/**
|
|
30
|
+
* A line drawn between this option and the one before or after it, where the Control can draw one --
|
|
31
|
+
* a Select does, in its menu. It sets an option apart from the rest without a heading to name it: the
|
|
32
|
+
* Button's "Custom" above the actions it is not one of. A Control that cannot draw it lists the
|
|
33
|
+
* options as they come.
|
|
34
|
+
*/
|
|
35
|
+
separator?: PhiControlOptionSeparator;
|
|
29
36
|
};
|
|
37
|
+
export type PhiControlOptionSeparator = "before" | "after";
|
|
30
38
|
export type PhiControlOptionPreview = PhiControlOptionBackgroundPreview | PhiControlOptionFontPreview;
|
|
31
39
|
/**
|
|
32
40
|
* The option's label set in the typeface it names, so a list of fonts shows the fonts.
|
package/dist/controls.js
CHANGED
|
@@ -125,6 +125,7 @@ export function readPhiControlOptions(value) {
|
|
|
125
125
|
icon: readString(record.icon),
|
|
126
126
|
preview: readOptionPreview(record.preview),
|
|
127
127
|
group: readString(record.group),
|
|
128
|
+
separator: record.separator === "before" || record.separator === "after" ? record.separator : undefined,
|
|
128
129
|
};
|
|
129
130
|
})
|
|
130
131
|
.filter((item) => item != null);
|
package/dist/core.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PhisPaymentChangedEventFacts } from "./payments.js";
|
|
1
2
|
/**
|
|
2
3
|
* What Core offers, named once so both sides can spell it the same way.
|
|
3
4
|
*
|
|
@@ -45,6 +46,8 @@ export declare const PHIS_CORE_CAPABILITIES: {
|
|
|
45
46
|
readonly settings: "@phis/server/settings:v1";
|
|
46
47
|
/** Whether the acting user holds one of this Add-on's own declared roles. */
|
|
47
48
|
readonly roles: "@phis/server/roles:v1";
|
|
49
|
+
readonly payments: "@phis/server/payments:v1";
|
|
50
|
+
readonly paymentNotifications: "@phis/server/payment-notifications:v1";
|
|
48
51
|
};
|
|
49
52
|
export type PhisCoreCapabilityId = (typeof PHIS_CORE_CAPABILITIES)[keyof typeof PHIS_CORE_CAPABILITIES];
|
|
50
53
|
/**
|
|
@@ -67,8 +70,11 @@ export declare const PHIS_ADDON_EVENTS: {
|
|
|
67
70
|
readonly threadStatusChanged: "thread.status.changed";
|
|
68
71
|
/** The link itself is gone; what follows from that is the Add-on's own business. */
|
|
69
72
|
readonly threadLinkRemoved: "thread.link.removed";
|
|
73
|
+
readonly paymentChanged: "payment.changed";
|
|
70
74
|
};
|
|
71
|
-
export type
|
|
75
|
+
export type PhisAddonThreadEventId = Exclude<(typeof PHIS_ADDON_EVENTS)[keyof typeof PHIS_ADDON_EVENTS], "payment.changed">;
|
|
76
|
+
/** Delivered v1 event vocabulary, discriminated by its owned subject. */
|
|
77
|
+
export type PhisAddonEventId = PhisAddonThreadEventId | PhisPaymentChangedEventFacts["event"];
|
|
72
78
|
/**
|
|
73
79
|
* The Core-owned service kinds an Add-on may implement.
|
|
74
80
|
*
|
|
@@ -78,6 +84,7 @@ export type PhisAddonEventId = (typeof PHIS_ADDON_EVENTS)[keyof typeof PHIS_ADDO
|
|
|
78
84
|
*/
|
|
79
85
|
export declare const PHIS_SERVICE_KINDS: {
|
|
80
86
|
readonly mediaStorage: "@phis/server/service/media-storage";
|
|
87
|
+
readonly payments: "@phis/server/service/payments";
|
|
81
88
|
readonly directory: "@phis/server/service/directory";
|
|
82
89
|
/**
|
|
83
90
|
* Turning text in one language into text in another.
|
|
@@ -88,6 +95,15 @@ export declare const PHIS_SERVICE_KINDS: {
|
|
|
88
95
|
* operator selects which one an installation uses.
|
|
89
96
|
*/
|
|
90
97
|
readonly translation: "@phis/server/service/translation";
|
|
98
|
+
/**
|
|
99
|
+
* Handing a Support ticket's work to a system outside the Site -- an issue tracker, a ticket desk.
|
|
100
|
+
*
|
|
101
|
+
* A kind because Core owns the question: which ticket, which task, what may leave and what comes back.
|
|
102
|
+
* What the implementation talks to is its own business, and several may be installed at once; a
|
|
103
|
+
* Supporter chooses one per forward. The interface is `PhisSupportForwardingAdapterV1` in
|
|
104
|
+
* `@phis/contracts/support`.
|
|
105
|
+
*/
|
|
106
|
+
readonly supportForwarding: "@phis/server/service/support-forwarding";
|
|
91
107
|
};
|
|
92
108
|
export type PhisServiceKind = (typeof PHIS_SERVICE_KINDS)[keyof typeof PHIS_SERVICE_KINDS];
|
|
93
109
|
/**
|
package/dist/core.js
CHANGED
|
@@ -45,6 +45,8 @@ export const PHIS_CORE_CAPABILITIES = {
|
|
|
45
45
|
settings: "@phis/server/settings:v1",
|
|
46
46
|
/** Whether the acting user holds one of this Add-on's own declared roles. */
|
|
47
47
|
roles: "@phis/server/roles:v1",
|
|
48
|
+
payments: "@phis/server/payments:v1",
|
|
49
|
+
paymentNotifications: "@phis/server/payment-notifications:v1",
|
|
48
50
|
};
|
|
49
51
|
/**
|
|
50
52
|
* What Core tells an Add-on about after the fact.
|
|
@@ -66,6 +68,7 @@ export const PHIS_ADDON_EVENTS = {
|
|
|
66
68
|
threadStatusChanged: "thread.status.changed",
|
|
67
69
|
/** The link itself is gone; what follows from that is the Add-on's own business. */
|
|
68
70
|
threadLinkRemoved: "thread.link.removed",
|
|
71
|
+
paymentChanged: "payment.changed",
|
|
69
72
|
};
|
|
70
73
|
/**
|
|
71
74
|
* The Core-owned service kinds an Add-on may implement.
|
|
@@ -76,6 +79,7 @@ export const PHIS_ADDON_EVENTS = {
|
|
|
76
79
|
*/
|
|
77
80
|
export const PHIS_SERVICE_KINDS = {
|
|
78
81
|
mediaStorage: "@phis/server/service/media-storage",
|
|
82
|
+
payments: "@phis/server/service/payments",
|
|
79
83
|
directory: "@phis/server/service/directory",
|
|
80
84
|
/**
|
|
81
85
|
* Turning text in one language into text in another.
|
|
@@ -86,6 +90,15 @@ export const PHIS_SERVICE_KINDS = {
|
|
|
86
90
|
* operator selects which one an installation uses.
|
|
87
91
|
*/
|
|
88
92
|
translation: "@phis/server/service/translation",
|
|
93
|
+
/**
|
|
94
|
+
* Handing a Support ticket's work to a system outside the Site -- an issue tracker, a ticket desk.
|
|
95
|
+
*
|
|
96
|
+
* A kind because Core owns the question: which ticket, which task, what may leave and what comes back.
|
|
97
|
+
* What the implementation talks to is its own business, and several may be installed at once; a
|
|
98
|
+
* Supporter chooses one per forward. The interface is `PhisSupportForwardingAdapterV1` in
|
|
99
|
+
* `@phis/contracts/support`.
|
|
100
|
+
*/
|
|
101
|
+
supportForwarding: "@phis/server/service/support-forwarding",
|
|
89
102
|
};
|
|
90
103
|
/**
|
|
91
104
|
* The digest of the interface each service kind currently has.
|
|
@@ -107,11 +120,15 @@ export const PHIS_SERVICE_KINDS = {
|
|
|
107
120
|
*/
|
|
108
121
|
export const PHIS_SERVICE_INTERFACE_VERSIONS = {
|
|
109
122
|
[PHIS_SERVICE_KINDS.mediaStorage]: 1,
|
|
123
|
+
[PHIS_SERVICE_KINDS.payments]: 1,
|
|
110
124
|
[PHIS_SERVICE_KINDS.directory]: 1,
|
|
111
125
|
[PHIS_SERVICE_KINDS.translation]: 1,
|
|
126
|
+
[PHIS_SERVICE_KINDS.supportForwarding]: 1,
|
|
112
127
|
};
|
|
113
128
|
export const PHIS_SERVICE_INTERFACE_DIGESTS = {
|
|
129
|
+
[PHIS_SERVICE_KINDS.payments]: "6b106818d5edbfeddd484e9f571509695a1a8dbb5b82435338a094216410fcf7",
|
|
114
130
|
[PHIS_SERVICE_KINDS.mediaStorage]: "45a871522511f7d77841ca8dd0113b423b3cfd4cb921f143cd1d3d116cab6952",
|
|
115
131
|
[PHIS_SERVICE_KINDS.directory]: "2cac7397c7beb1b8e71fc67f5cae3be89182dddf096895570a15deafa1adbfc9",
|
|
116
132
|
[PHIS_SERVICE_KINDS.translation]: "9bae2e7b92ded5cdde9321939a3bb8cd589f6e1ef8bca7bc5f65608313b3207d",
|
|
133
|
+
[PHIS_SERVICE_KINDS.supportForwarding]: "e9d01711a5ebdfa37ea4c6828952c87935b6e755b98ab8e69cc397933dfbed6f",
|
|
117
134
|
};
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
export type PhisPaymentId = string;
|
|
9
|
+
export type PhisPaymentOperationId = string;
|
|
10
|
+
export type PhisPaymentRefundId = string;
|
|
11
|
+
export type PhisPaymentMoney = {
|
|
12
|
+
/** Uppercase ISO currency code; one agreed currency for the whole payment. */
|
|
13
|
+
currency: string;
|
|
14
|
+
/** Canonical non-negative integer string of minor units, never floating-point money. */
|
|
15
|
+
minor: string;
|
|
16
|
+
};
|
|
17
|
+
export type PhisPaymentCaptureMode = "automatic" | "manual";
|
|
18
|
+
export type PhisPaymentCheckoutKind = "redirect" | "provider-ui";
|
|
19
|
+
export type PhisPaymentFeatures = {
|
|
20
|
+
captureModes: readonly PhisPaymentCaptureMode[];
|
|
21
|
+
checkoutKinds: readonly PhisPaymentCheckoutKind[];
|
|
22
|
+
canPartialCapture: boolean;
|
|
23
|
+
canCancel: boolean;
|
|
24
|
+
canRefund: boolean;
|
|
25
|
+
canPartialRefund: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type PhisPaymentMethod = {
|
|
28
|
+
profileKey: string;
|
|
29
|
+
methodKey: string | null;
|
|
30
|
+
title: string;
|
|
31
|
+
features: PhisPaymentFeatures;
|
|
32
|
+
};
|
|
33
|
+
export type PhisPaymentCheckoutPlan = {
|
|
34
|
+
kind: "redirect";
|
|
35
|
+
url: string;
|
|
36
|
+
expiresAt: string | null;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "provider-ui";
|
|
39
|
+
moduleId: string;
|
|
40
|
+
adapterKey: string;
|
|
41
|
+
parameters: Readonly<Record<string, string>>;
|
|
42
|
+
expiresAt: string | null;
|
|
43
|
+
};
|
|
44
|
+
export type PhisPaymentStatus = "created" | "awaiting_customer" | "processing" | "authorized" | "captured" | "canceled" | "expired" | "failed";
|
|
45
|
+
export type PhisPayment = {
|
|
46
|
+
id: PhisPaymentId;
|
|
47
|
+
reference: string;
|
|
48
|
+
profileKey: string;
|
|
49
|
+
mode: "test" | "live";
|
|
50
|
+
amount: PhisPaymentMoney;
|
|
51
|
+
captureMode: PhisPaymentCaptureMode;
|
|
52
|
+
status: PhisPaymentStatus;
|
|
53
|
+
authorizedMinor: string;
|
|
54
|
+
capturedMinor: string;
|
|
55
|
+
refundedMinor: string;
|
|
56
|
+
authorizationExpiresAt: string | null;
|
|
57
|
+
checkout: PhisPaymentCheckoutPlan | null;
|
|
58
|
+
reconciliationRequired: boolean;
|
|
59
|
+
version: number;
|
|
60
|
+
lastObservedAt: string | null;
|
|
61
|
+
};
|
|
62
|
+
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";
|
|
63
|
+
export type PhisPaymentOperation = {
|
|
64
|
+
id: PhisPaymentOperationId;
|
|
65
|
+
paymentId: PhisPaymentId;
|
|
66
|
+
action: "create" | "complete" | "capture" | "cancel" | "refund";
|
|
67
|
+
state: "reserved" | "pending" | "succeeded" | "failed" | "indeterminate";
|
|
68
|
+
errorCode: PhisPaymentErrorCode | null;
|
|
69
|
+
};
|
|
70
|
+
export type PhisPaymentRefund = {
|
|
71
|
+
id: PhisPaymentRefundId;
|
|
72
|
+
paymentId: PhisPaymentId;
|
|
73
|
+
amount: PhisPaymentMoney;
|
|
74
|
+
status: "pending" | "succeeded" | "failed" | "canceled";
|
|
75
|
+
};
|
|
76
|
+
export type PhisPaymentCommandResult = {
|
|
77
|
+
operation: PhisPaymentOperation;
|
|
78
|
+
payment: PhisPayment;
|
|
79
|
+
refund: PhisPaymentRefund | null;
|
|
80
|
+
};
|
|
81
|
+
export type PhisPaymentCreateInput = {
|
|
82
|
+
profileKey: string;
|
|
83
|
+
reference: string;
|
|
84
|
+
idempotencyKey: string;
|
|
85
|
+
amount: PhisPaymentMoney;
|
|
86
|
+
description: string;
|
|
87
|
+
captureMode: PhisPaymentCaptureMode;
|
|
88
|
+
methodKey: string | null;
|
|
89
|
+
checkoutKind: PhisPaymentCheckoutKind;
|
|
90
|
+
returnPath: string;
|
|
91
|
+
};
|
|
92
|
+
export type PhisPaymentCommandInput = {
|
|
93
|
+
paymentId: PhisPaymentId;
|
|
94
|
+
idempotencyKey: string;
|
|
95
|
+
};
|
|
96
|
+
export interface PhisPaymentsCapabilityV1 {
|
|
97
|
+
listMethods(input: {
|
|
98
|
+
profileKey: string;
|
|
99
|
+
amount: PhisPaymentMoney;
|
|
100
|
+
captureMode: PhisPaymentCaptureMode;
|
|
101
|
+
checkoutKind: PhisPaymentCheckoutKind;
|
|
102
|
+
}): Promise<readonly PhisPaymentMethod[]>;
|
|
103
|
+
/** Core binds Site/consumer/profile; the consumer checks access to its business resource. */
|
|
104
|
+
create(input: PhisPaymentCreateInput): Promise<PhisPaymentCommandResult>;
|
|
105
|
+
get(paymentId: PhisPaymentId): Promise<PhisPayment | null>;
|
|
106
|
+
getOperation(operationId: PhisPaymentOperationId): Promise<PhisPaymentOperation | null>;
|
|
107
|
+
getRefund(refundId: PhisPaymentRefundId): Promise<PhisPaymentRefund | null>;
|
|
108
|
+
/** Customer continuation; never captures a manually authorized payment. */
|
|
109
|
+
complete(input: PhisPaymentCommandInput): Promise<PhisPaymentCommandResult>;
|
|
110
|
+
/** Explicit bounded reconciliation that may update state and enqueue a consumer event. */
|
|
111
|
+
refresh(paymentId: PhisPaymentId): Promise<PhisPayment | null>;
|
|
112
|
+
/** One final merchant-authorized capture; minor units inherit the stored currency. */
|
|
113
|
+
capture(input: PhisPaymentCommandInput & {
|
|
114
|
+
minor: string;
|
|
115
|
+
}): Promise<PhisPaymentCommandResult>;
|
|
116
|
+
cancel(input: PhisPaymentCommandInput): Promise<PhisPaymentCommandResult>;
|
|
117
|
+
/** Merchant-authorized refund; pending/unknown amounts reserve the captured balance. */
|
|
118
|
+
refund(input: PhisPaymentCommandInput & {
|
|
119
|
+
minor: string;
|
|
120
|
+
reason: string;
|
|
121
|
+
}): Promise<PhisPaymentCommandResult>;
|
|
122
|
+
}
|
|
123
|
+
export type PhisPaymentProviderConfigV1 = {
|
|
124
|
+
profileKey: string;
|
|
125
|
+
mode: "test" | "live";
|
|
126
|
+
merchantIdentity: string | null;
|
|
127
|
+
};
|
|
128
|
+
export type PhisPaymentProviderContextV1 = {
|
|
129
|
+
providerKey: string;
|
|
130
|
+
readCredentials(): Promise<Readonly<Record<string, string>> | null>;
|
|
131
|
+
};
|
|
132
|
+
export type PhisPaymentProviderBinding = {
|
|
133
|
+
reference: string;
|
|
134
|
+
data: Readonly<Record<string, string>>;
|
|
135
|
+
};
|
|
136
|
+
export type PhisPaymentProviderProbe = {
|
|
137
|
+
merchantIdentity: string;
|
|
138
|
+
mode: "test" | "live";
|
|
139
|
+
methods: readonly Omit<PhisPaymentMethod, "profileKey">[];
|
|
140
|
+
};
|
|
141
|
+
export type PhisPaymentProviderObservation = {
|
|
142
|
+
binding: PhisPaymentProviderBinding;
|
|
143
|
+
merchantIdentity: string;
|
|
144
|
+
mode: "test" | "live";
|
|
145
|
+
status: PhisPaymentStatus;
|
|
146
|
+
amount: PhisPaymentMoney;
|
|
147
|
+
authorizedMinor: string;
|
|
148
|
+
capturedMinor: string;
|
|
149
|
+
authorizationExpiresAt: string | null;
|
|
150
|
+
checkout: PhisPaymentCheckoutPlan | null;
|
|
151
|
+
/** Complete refund observations, including remote refunds created outside Core. */
|
|
152
|
+
refunds: readonly {
|
|
153
|
+
operationId: PhisPaymentOperationId | null;
|
|
154
|
+
binding: PhisPaymentProviderBinding;
|
|
155
|
+
minor: string;
|
|
156
|
+
status: PhisPaymentRefund["status"];
|
|
157
|
+
}[];
|
|
158
|
+
};
|
|
159
|
+
export type PhisPaymentProviderCommand = {
|
|
160
|
+
operationId: PhisPaymentOperationId;
|
|
161
|
+
paymentId: PhisPaymentId;
|
|
162
|
+
providerIdempotencyKey: string;
|
|
163
|
+
signal: AbortSignal;
|
|
164
|
+
} & ({
|
|
165
|
+
action: "create";
|
|
166
|
+
amount: PhisPaymentMoney;
|
|
167
|
+
description: string;
|
|
168
|
+
captureMode: PhisPaymentCaptureMode;
|
|
169
|
+
methodKey: string | null;
|
|
170
|
+
checkoutKind: PhisPaymentCheckoutKind;
|
|
171
|
+
returnUrl: string;
|
|
172
|
+
notificationUrl: string;
|
|
173
|
+
} | {
|
|
174
|
+
action: "complete" | "cancel";
|
|
175
|
+
binding: PhisPaymentProviderBinding;
|
|
176
|
+
} | {
|
|
177
|
+
action: "capture";
|
|
178
|
+
binding: PhisPaymentProviderBinding;
|
|
179
|
+
minor: string;
|
|
180
|
+
} | {
|
|
181
|
+
action: "refund";
|
|
182
|
+
binding: PhisPaymentProviderBinding;
|
|
183
|
+
minor: string;
|
|
184
|
+
reason: string;
|
|
185
|
+
});
|
|
186
|
+
export type PhisPaymentProviderExecution = {
|
|
187
|
+
outcome: "applied" | "pending";
|
|
188
|
+
observation: PhisPaymentProviderObservation;
|
|
189
|
+
} | {
|
|
190
|
+
outcome: "rejected";
|
|
191
|
+
code: PhisPaymentErrorCode;
|
|
192
|
+
} | {
|
|
193
|
+
outcome: "unknown";
|
|
194
|
+
};
|
|
195
|
+
export type PhisPaymentProviderNotification = {
|
|
196
|
+
outcome: "verified";
|
|
197
|
+
deliveryId: string;
|
|
198
|
+
reference: string;
|
|
199
|
+
} | {
|
|
200
|
+
outcome: "lookup-required";
|
|
201
|
+
reference: string;
|
|
202
|
+
} | {
|
|
203
|
+
outcome: "irrelevant";
|
|
204
|
+
} | {
|
|
205
|
+
outcome: "rejected";
|
|
206
|
+
} | {
|
|
207
|
+
outcome: "unavailable";
|
|
208
|
+
};
|
|
209
|
+
export interface PhisPaymentProviderV1 {
|
|
210
|
+
probe(input: {
|
|
211
|
+
amount: PhisPaymentMoney;
|
|
212
|
+
captureMode: PhisPaymentCaptureMode;
|
|
213
|
+
checkoutKind: PhisPaymentCheckoutKind;
|
|
214
|
+
signal: AbortSignal;
|
|
215
|
+
}): Promise<PhisPaymentProviderProbe>;
|
|
216
|
+
execute(input: PhisPaymentProviderCommand): Promise<PhisPaymentProviderExecution>;
|
|
217
|
+
observe(input: {
|
|
218
|
+
binding: PhisPaymentProviderBinding;
|
|
219
|
+
signal: AbortSignal;
|
|
220
|
+
}): Promise<PhisPaymentProviderObservation>;
|
|
221
|
+
/** Recover the same command; never replace its key, merchant, mode or Provider. */
|
|
222
|
+
recover(input: {
|
|
223
|
+
command: PhisPaymentProviderCommand;
|
|
224
|
+
binding: PhisPaymentProviderBinding | null;
|
|
225
|
+
}): Promise<PhisPaymentProviderExecution>;
|
|
226
|
+
verifyNotification(input: {
|
|
227
|
+
method: string;
|
|
228
|
+
headers: Readonly<Record<string, string>>;
|
|
229
|
+
body: Uint8Array;
|
|
230
|
+
signal: AbortSignal;
|
|
231
|
+
}): Promise<PhisPaymentProviderNotification>;
|
|
232
|
+
}
|
|
233
|
+
export interface PhisPaymentNotificationsCapabilityV1 {
|
|
234
|
+
receive(input: {
|
|
235
|
+
profileKey: string;
|
|
236
|
+
}): Promise<{
|
|
237
|
+
outcome: "processed" | "duplicate" | "ignored";
|
|
238
|
+
}>;
|
|
239
|
+
}
|
|
240
|
+
export type PhisPaymentGrantedOperation = "read" | "create" | "complete" | "refresh" | "capture" | "cancel" | "refund";
|
|
241
|
+
export type PhisPaymentMerchantActionPolicy = {
|
|
242
|
+
roles: readonly string[];
|
|
243
|
+
jobs: readonly string[];
|
|
244
|
+
events: readonly string[];
|
|
245
|
+
};
|
|
246
|
+
export type PhisPaymentProfileGrant = {
|
|
247
|
+
consumerAddonId: string;
|
|
248
|
+
operations: readonly PhisPaymentGrantedOperation[];
|
|
249
|
+
merchantActions: {
|
|
250
|
+
capture: PhisPaymentMerchantActionPolicy;
|
|
251
|
+
cancel: PhisPaymentMerchantActionPolicy;
|
|
252
|
+
refund: PhisPaymentMerchantActionPolicy;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
/** Operator-controlled references and grants; contains no credential values. */
|
|
256
|
+
export type PhisPaymentProfileDraft = {
|
|
257
|
+
key: string;
|
|
258
|
+
providerKey: string;
|
|
259
|
+
mode: "test" | "live";
|
|
260
|
+
credentialReferences: Readonly<Record<string, string>>;
|
|
261
|
+
grants: readonly PhisPaymentProfileGrant[];
|
|
262
|
+
};
|
|
263
|
+
/** A payment event reaches only the consuming Add-on stored on that payment. */
|
|
264
|
+
export type PhisPaymentChangedEventFacts = {
|
|
265
|
+
event: "payment.changed";
|
|
266
|
+
subject: {
|
|
267
|
+
kind: "payment";
|
|
268
|
+
paymentId: PhisPaymentId;
|
|
269
|
+
reference: string;
|
|
270
|
+
version: number;
|
|
271
|
+
operationId: PhisPaymentOperationId | null;
|
|
272
|
+
refundId: PhisPaymentRefundId | null;
|
|
273
|
+
};
|
|
274
|
+
};
|
package/dist/payments.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
package/dist/references.d.ts
CHANGED
|
@@ -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
|
|
133
|
+
* A route pattern with its parameters filled, or `null` while one is still open.
|
|
125
134
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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;
|
package/dist/references.js
CHANGED
|
@@ -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
|
};
|
package/dist/resource-links.d.ts
CHANGED
|
@@ -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
|
|
63
|
-
*
|
|
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
|
|
72
|
-
* within this Add-on and this Site, so a second call answers with the first
|
|
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
|
-
/**
|
|
90
|
-
|
|
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
|
|
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;
|
package/dist/support.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PhisSecretsCapabilityV1, PhisSettingsCapabilityV1 } from "./capabilities.js";
|
|
1
2
|
/**
|
|
2
3
|
* The Support vocabulary Core and the Support Module must spell the same way.
|
|
3
4
|
*
|
|
@@ -44,6 +45,17 @@ export declare const PhisSupportTicketTypeFlag: {
|
|
|
44
45
|
*/
|
|
45
46
|
readonly Requestable: number;
|
|
46
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* What is true of one ticket, as bit flags (`support_tickets.flags`).
|
|
50
|
+
*
|
|
51
|
+
* `AwaitingDelivery`: the work is resolved and the fix is not in the requester's hands yet. An integration
|
|
52
|
+
* that tracks releases sets it when the merge resolves the ticket, and the requester window does not start
|
|
53
|
+
* until the release says the fix is out (`delivered_at`): a window that ran out before anybody could have
|
|
54
|
+
* the fix would close a ticket nobody had a chance to object to.
|
|
55
|
+
*/
|
|
56
|
+
export declare const PhisSupportTicketFlag: {
|
|
57
|
+
readonly AwaitingDelivery: number;
|
|
58
|
+
};
|
|
47
59
|
/**
|
|
48
60
|
* What is true of a Support queue, as bit flags.
|
|
49
61
|
*
|
|
@@ -327,6 +339,13 @@ export type PhisSupportCapabilityV1 = {
|
|
|
327
339
|
}): Promise<PhisSupportTicket>;
|
|
328
340
|
/** The work on one ticket, in the order the Site keeps it. */
|
|
329
341
|
listTasks(ticketId: number): Promise<PhisSupportTicketTask[]>;
|
|
342
|
+
/**
|
|
343
|
+
* One task, or null where its ticket is out of reach or it is absent.
|
|
344
|
+
*
|
|
345
|
+
* The way back from a link: an integration that hears about its external record finds the link, the
|
|
346
|
+
* link names a task, and the task names its ticket -- whose conversation a comment belongs in.
|
|
347
|
+
*/
|
|
348
|
+
getTask(taskId: number): Promise<PhisSupportTicketTask | null>;
|
|
330
349
|
/** Records a piece of work, which is what a linked issue hangs on. */
|
|
331
350
|
addTask(input: {
|
|
332
351
|
ticketId: number;
|
|
@@ -339,6 +358,19 @@ export type PhisSupportCapabilityV1 = {
|
|
|
339
358
|
taskId: number;
|
|
340
359
|
status: number;
|
|
341
360
|
}): Promise<PhisSupportTicketTask>;
|
|
361
|
+
/**
|
|
362
|
+
* Holds a resolved ticket's requester window until the fix is delivered.
|
|
363
|
+
*
|
|
364
|
+
* For an integration that tracks releases: the merge resolved the ticket, and the requester cannot have
|
|
365
|
+
* the fix until a release carries it. False where the ticket is not resolved -- there is no window to
|
|
366
|
+
* hold. Reopening or resolving again lets go of it.
|
|
367
|
+
*/
|
|
368
|
+
awaitDelivery(ticketId: number): Promise<boolean>;
|
|
369
|
+
/**
|
|
370
|
+
* Says the fix is out: the requester window of a resolved ticket starts now. False where the ticket is
|
|
371
|
+
* not resolved or was already delivered.
|
|
372
|
+
*/
|
|
373
|
+
markDelivered(ticketId: number): Promise<boolean>;
|
|
342
374
|
/**
|
|
343
375
|
* What one person may still ask for, of one type.
|
|
344
376
|
*
|
|
@@ -388,3 +420,155 @@ export type PhisSupportCapabilityV1 = {
|
|
|
388
420
|
reference: string;
|
|
389
421
|
}): Promise<PhisSupportAllowanceGrant | null>;
|
|
390
422
|
};
|
|
423
|
+
/**
|
|
424
|
+
* What Core hands an implementation of `@phis/server/service/support-forwarding` when it builds one.
|
|
425
|
+
*
|
|
426
|
+
* Built per Site and per forward, so the context names the Site; what the implementation talks to and how
|
|
427
|
+
* it signs in is its own, which is why what it reads from Core is its own configuration and nothing else.
|
|
428
|
+
*/
|
|
429
|
+
export type PhisSupportForwardingServiceContext = {
|
|
430
|
+
/** The Add-on implementing the kind. */
|
|
431
|
+
providerId: string;
|
|
432
|
+
siteKey: string;
|
|
433
|
+
/**
|
|
434
|
+
* The implementation's own Site secrets and settings, the same two capabilities its hooks and jobs
|
|
435
|
+
* have: where the work goes (a repository, an installation) is per-Site configuration, and so are the
|
|
436
|
+
* credentials it goes with.
|
|
437
|
+
*/
|
|
438
|
+
secrets: PhisSecretsCapabilityV1;
|
|
439
|
+
settings: PhisSettingsCapabilityV1;
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* One place an implementation can send work to: a repository, a project, a desk.
|
|
443
|
+
*
|
|
444
|
+
* `key` is the implementation's own name for it and comes back unchanged in `PhisSupportForwardRequest`;
|
|
445
|
+
* `label` is shown to a Supporter as it is ("GitHub acme/app").
|
|
446
|
+
*/
|
|
447
|
+
export type PhisSupportForwardingTarget = {
|
|
448
|
+
key: string;
|
|
449
|
+
label: string;
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* Whether this implementation can take work on this Site right now, and where.
|
|
453
|
+
*
|
|
454
|
+
* Asked before a Supporter is offered anything. One implementation may offer several targets -- every
|
|
455
|
+
* repository its installation reaches, say -- and a Supporter chooses one per forward. `findings` say in an
|
|
456
|
+
* operator's terms what does not answer when `usable` is false or a target is left out.
|
|
457
|
+
*/
|
|
458
|
+
export type PhisSupportForwardingProbe = {
|
|
459
|
+
usable: boolean;
|
|
460
|
+
targets: PhisSupportForwardingTarget[];
|
|
461
|
+
findings: string[];
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* One ticket's work, handed over.
|
|
465
|
+
*
|
|
466
|
+
* `mirror` is the ticket type's `External`, decided by Core and not by the implementation: where it is
|
|
467
|
+
* false the external record is opened from the title alone and nothing of the conversation follows it.
|
|
468
|
+
* Where it is true, `body` is what opens it -- the ticket's first message, and only if the person who
|
|
469
|
+
* wrote it agreed to it leaving the Site. What follows afterwards reaches the implementation as thread
|
|
470
|
+
* events, through the synced link Core writes for it, and Core withholds there what may not leave.
|
|
471
|
+
*/
|
|
472
|
+
export type PhisSupportForwardRequest = {
|
|
473
|
+
/** The target the Supporter chose, by the key the probe gave it. */
|
|
474
|
+
target: string;
|
|
475
|
+
ticket: {
|
|
476
|
+
id: number;
|
|
477
|
+
threadId: number;
|
|
478
|
+
subject: string;
|
|
479
|
+
typeKey: string;
|
|
480
|
+
};
|
|
481
|
+
task: {
|
|
482
|
+
title: string;
|
|
483
|
+
};
|
|
484
|
+
mirror: boolean;
|
|
485
|
+
body: string | null;
|
|
486
|
+
/** Who forwarded it, for the external record's attribution. */
|
|
487
|
+
actor: {
|
|
488
|
+
userId: number;
|
|
489
|
+
displayName: string | null;
|
|
490
|
+
};
|
|
491
|
+
signal: AbortSignal;
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* The external record the work now lives in.
|
|
495
|
+
*
|
|
496
|
+
* `type` and `state` are the implementation's own vocabulary -- an issue, a pull request, open -- and Core
|
|
497
|
+
* stores them on the resource link it writes for the forward without interpreting either, as it does for
|
|
498
|
+
* every link.
|
|
499
|
+
*/
|
|
500
|
+
export type PhisSupportForwardResult = {
|
|
501
|
+
type: number;
|
|
502
|
+
externalId: string;
|
|
503
|
+
url: string | null;
|
|
504
|
+
state: number | null;
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* A record that already exists in the other system, named by a Supporter: what they pasted, as it is.
|
|
508
|
+
*
|
|
509
|
+
* `reference` is whatever the implementation's own people call one -- a URL, `owner/name#12` -- and each
|
|
510
|
+
* implementation reads only its own. Looking one up changes nothing anywhere: whether the link is made is
|
|
511
|
+
* Core's to decide afterwards, and the record hears of it only once it stands (`linked`).
|
|
512
|
+
*/
|
|
513
|
+
export type PhisSupportLinkLookup = {
|
|
514
|
+
reference: string;
|
|
515
|
+
signal: AbortSignal;
|
|
516
|
+
};
|
|
517
|
+
/**
|
|
518
|
+
* What an implementation answers about a reference.
|
|
519
|
+
*
|
|
520
|
+
* - `unknown`: not one of its own; Core asks the next implementation.
|
|
521
|
+
* - `refused`: its own, but no work can go there -- closed, out of the Site's reach, gone. `reason` is
|
|
522
|
+
* shown to the Supporter as it is.
|
|
523
|
+
* - `linkable`: the record, as `forward` would have answered it, and what it is called there, which
|
|
524
|
+
* becomes the engineering task's title.
|
|
525
|
+
*/
|
|
526
|
+
export type PhisSupportLinkResult = {
|
|
527
|
+
status: "unknown";
|
|
528
|
+
} | {
|
|
529
|
+
status: "refused";
|
|
530
|
+
reason: string;
|
|
531
|
+
} | {
|
|
532
|
+
status: "linkable";
|
|
533
|
+
record: PhisSupportForwardResult;
|
|
534
|
+
title: string;
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* A link that now stands, so the record can say where it is linked from, as a forwarded one does in its
|
|
538
|
+
* body. Nothing of the conversation travels with it: a link hands nothing over by itself.
|
|
539
|
+
*/
|
|
540
|
+
export type PhisSupportLinkedNotice = {
|
|
541
|
+
record: PhisSupportForwardResult;
|
|
542
|
+
ticket: {
|
|
543
|
+
id: number;
|
|
544
|
+
threadId: number;
|
|
545
|
+
subject: string;
|
|
546
|
+
typeKey: string;
|
|
547
|
+
};
|
|
548
|
+
/** Who linked it, for the record's attribution. */
|
|
549
|
+
actor: {
|
|
550
|
+
userId: number;
|
|
551
|
+
displayName: string | null;
|
|
552
|
+
};
|
|
553
|
+
signal: AbortSignal;
|
|
554
|
+
};
|
|
555
|
+
/**
|
|
556
|
+
* The interface of `@phis/server/service/support-forwarding`, version 1.
|
|
557
|
+
*
|
|
558
|
+
* Whether it can take work, taking it, and linking work that is already there -- an issue somebody opened
|
|
559
|
+
* in the other system before or beside the ticket: `lookup` says what a reference is, Core records the link,
|
|
560
|
+
* and `linked` tells the record. A linked record is followed exactly as a forwarded one is; only how it
|
|
561
|
+
* came to exist differs, and a `linked` that fails leaves the link standing.
|
|
562
|
+
*
|
|
563
|
+
* Everything after the hand-over is the capabilities every Add-on has -- `support:v1` to finish the engineering task the forward created,
|
|
564
|
+
* `threads:v1` to answer in the conversation, `resource-links:v1` to move the link's state -- so that what
|
|
565
|
+
* happens to a ticket stays Core's to decide.
|
|
566
|
+
*/
|
|
567
|
+
export interface PhisSupportForwardingAdapterV1 {
|
|
568
|
+
probe(input: {
|
|
569
|
+
signal: AbortSignal;
|
|
570
|
+
}): Promise<PhisSupportForwardingProbe>;
|
|
571
|
+
forward(input: PhisSupportForwardRequest): Promise<PhisSupportForwardResult>;
|
|
572
|
+
lookup(input: PhisSupportLinkLookup): Promise<PhisSupportLinkResult>;
|
|
573
|
+
linked(input: PhisSupportLinkedNotice): Promise<void>;
|
|
574
|
+
}
|
package/dist/support.js
CHANGED
|
@@ -44,6 +44,17 @@ export const PhisSupportTicketTypeFlag = {
|
|
|
44
44
|
*/
|
|
45
45
|
Requestable: 1 << 2,
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* What is true of one ticket, as bit flags (`support_tickets.flags`).
|
|
49
|
+
*
|
|
50
|
+
* `AwaitingDelivery`: the work is resolved and the fix is not in the requester's hands yet. An integration
|
|
51
|
+
* that tracks releases sets it when the merge resolves the ticket, and the requester window does not start
|
|
52
|
+
* until the release says the fix is out (`delivered_at`): a window that ran out before anybody could have
|
|
53
|
+
* the fix would close a ticket nobody had a chance to object to.
|
|
54
|
+
*/
|
|
55
|
+
export const PhisSupportTicketFlag = {
|
|
56
|
+
AwaitingDelivery: 1 << 0,
|
|
57
|
+
};
|
|
47
58
|
/**
|
|
48
59
|
* What is true of a Support queue, as bit flags.
|
|
49
60
|
*
|
package/dist/threads.d.ts
CHANGED
|
@@ -278,6 +278,11 @@ export type PhisThreadsCapabilityV1 = {
|
|
|
278
278
|
*
|
|
279
279
|
* `messageFlags` is normalized rather than trusted: `Internal` requires an actor who is Staff, and
|
|
280
280
|
* `Imported` is Core's to set on what arrives through an integration.
|
|
281
|
+
*
|
|
282
|
+
* One exception, and only in the narrowing direction: without an actor, an Add-on may mark a message it
|
|
283
|
+
* imports -- one carrying `externalRef` -- as `Internal`. What it brings in from outside is its own to
|
|
284
|
+
* keep from the requester, as a maintainer's comment on an issue is; nothing it writes can be made
|
|
285
|
+
* readable to more people this way, only to fewer.
|
|
281
286
|
*/
|
|
282
287
|
create(input: {
|
|
283
288
|
kind: PhisThreadKindValue;
|
|
@@ -321,6 +326,7 @@ export type PhisThreadsCapabilityV1 = {
|
|
|
321
326
|
*
|
|
322
327
|
* Idempotent on `externalRef` where one is given: the same reference answers with the thread as it
|
|
323
328
|
* stands instead of writing a second message, which is what a webhook delivered twice needs.
|
|
329
|
+
* `messageFlags` follow the terms `create` states, its exception for an imported `Internal` included.
|
|
324
330
|
*/
|
|
325
331
|
reply(input: {
|
|
326
332
|
threadId: number;
|
|
@@ -360,4 +366,27 @@ export type PhisThreadsCapabilityV1 = {
|
|
|
360
366
|
* or `reply`, so it finds its way back to a conversation it already knows about and to no other.
|
|
361
367
|
*/
|
|
362
368
|
findByExternalRef(externalRef: string): Promise<PhisThreadDetail | null>;
|
|
369
|
+
/**
|
|
370
|
+
* Records that a message of a reachable thread was written out to the other system, under that
|
|
371
|
+
* system's id for it. True when recorded now, false when it already was -- an event delivered twice
|
|
372
|
+
* finds its first export and posts nothing.
|
|
373
|
+
*
|
|
374
|
+
* An exported message is never imported back: what this Add-on wrote out and then hears about again is
|
|
375
|
+
* recognised by the reference and left alone.
|
|
376
|
+
*/
|
|
377
|
+
recordExport(input: {
|
|
378
|
+
messageId: number;
|
|
379
|
+
externalRef: string;
|
|
380
|
+
}): Promise<boolean>;
|
|
381
|
+
/** The other system's id for a message this Add-on exported, or null. */
|
|
382
|
+
findExport(messageId: number): Promise<string | null>;
|
|
383
|
+
/**
|
|
384
|
+
* Changes a message this Add-on imported, as its source changed it: new text marks it `Edited`, `null`
|
|
385
|
+
* withdraws it -- the text is gone and the message stays, `Redacted`, so the conversation still reads.
|
|
386
|
+
* False where this Add-on imported nothing under that reference.
|
|
387
|
+
*/
|
|
388
|
+
amendImported(input: {
|
|
389
|
+
externalRef: string;
|
|
390
|
+
message: string | null;
|
|
391
|
+
}): Promise<boolean>;
|
|
363
392
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Timeline shows, the same for a Widget's own entries and for a Timeline Provider's answer.
|
|
3
|
+
*
|
|
4
|
+
* A Provider may run in a Module of ours, in an outside Module, or answer from an Add-on behind a Site
|
|
5
|
+
* route, so the entry is spelled here rather than in the UI package: every end that hands entries over
|
|
6
|
+
* reads and writes this shape.
|
|
7
|
+
*/
|
|
8
|
+
/** What an entry's mark says about it: a word about meaning, so the Theme decides the colour. */
|
|
9
|
+
export declare const PHI_TIMELINE_TONES: readonly ["secondary", "success", "warning", "danger"];
|
|
10
|
+
export type PhiTimelineTone = (typeof PHI_TIMELINE_TONES)[number];
|
|
11
|
+
export type PhiTimelineEntry = {
|
|
12
|
+
/** Stable within the list; the entry's identity when the list is drawn again. */
|
|
13
|
+
key: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
content: string;
|
|
16
|
+
/**
|
|
17
|
+
* When it happened, as an ISO 8601 date or date-time. The Widget formats it for the reader's locale.
|
|
18
|
+
* An entry that states `time` as well shows `time`.
|
|
19
|
+
*/
|
|
20
|
+
at?: string;
|
|
21
|
+
/**
|
|
22
|
+
* When it happened, already in words: "Spring 2026", "v1.2", "Yesterday". Shown as written, and
|
|
23
|
+
* preferred over `at` where an entry states both.
|
|
24
|
+
*/
|
|
25
|
+
time?: string;
|
|
26
|
+
/** A Phi icon name, drawn in place of the mark. */
|
|
27
|
+
icon?: string;
|
|
28
|
+
tone?: PhiTimelineTone;
|
|
29
|
+
/** Still under way: the mark spins. */
|
|
30
|
+
pending?: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** A Timeline Provider's answer. */
|
|
33
|
+
export type PhiTimelineProviderResult = {
|
|
34
|
+
entries: PhiTimelineEntry[];
|
|
35
|
+
};
|
|
36
|
+
export declare function isPhiTimelineTone(value: unknown): value is PhiTimelineTone;
|
|
37
|
+
/** One entry, or `null` for anything that is not one. An entry without a key or text is not one. */
|
|
38
|
+
export declare function readPhiTimelineEntry(value: unknown): PhiTimelineEntry | null;
|
|
39
|
+
export declare function readPhiTimelineEntries(value: unknown): PhiTimelineEntry[];
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Timeline shows, the same for a Widget's own entries and for a Timeline Provider's answer.
|
|
3
|
+
*
|
|
4
|
+
* A Provider may run in a Module of ours, in an outside Module, or answer from an Add-on behind a Site
|
|
5
|
+
* route, so the entry is spelled here rather than in the UI package: every end that hands entries over
|
|
6
|
+
* reads and writes this shape.
|
|
7
|
+
*/
|
|
8
|
+
/** What an entry's mark says about it: a word about meaning, so the Theme decides the colour. */
|
|
9
|
+
export const PHI_TIMELINE_TONES = ["secondary", "success", "warning", "danger"];
|
|
10
|
+
function readString(value) {
|
|
11
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
12
|
+
}
|
|
13
|
+
export function isPhiTimelineTone(value) {
|
|
14
|
+
return typeof value === "string" && PHI_TIMELINE_TONES.includes(value);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The `at` of an entry, when it is a date this reader can place. A string that is not one is dropped
|
|
18
|
+
* rather than shown: an unreadable instant would print as "Invalid Date" in the reader's language.
|
|
19
|
+
*/
|
|
20
|
+
function readInstant(value) {
|
|
21
|
+
const text = readString(value);
|
|
22
|
+
return text && !Number.isNaN(Date.parse(text)) ? text : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** One entry, or `null` for anything that is not one. An entry without a key or text is not one. */
|
|
25
|
+
export function readPhiTimelineEntry(value) {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const record = value;
|
|
30
|
+
const key = readString(record.key);
|
|
31
|
+
if (!key) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const title = readString(record.title);
|
|
35
|
+
const at = readInstant(record.at);
|
|
36
|
+
const time = readString(record.time);
|
|
37
|
+
const icon = readString(record.icon);
|
|
38
|
+
return {
|
|
39
|
+
key,
|
|
40
|
+
content: typeof record.content === "string" ? record.content : "",
|
|
41
|
+
...(title ? { title } : {}),
|
|
42
|
+
...(at ? { at } : {}),
|
|
43
|
+
...(time ? { time } : {}),
|
|
44
|
+
...(icon ? { icon } : {}),
|
|
45
|
+
...(isPhiTimelineTone(record.tone) ? { tone: record.tone } : {}),
|
|
46
|
+
...(record.pending === true ? { pending: true } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function readPhiTimelineEntries(value) {
|
|
50
|
+
return Array.isArray(value)
|
|
51
|
+
? value.map(readPhiTimelineEntry).filter((entry) => entry != null)
|
|
52
|
+
: [];
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phis/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"prepack": "npm run build"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
+
"./payments": {
|
|
19
|
+
"types": "./dist/payments.d.ts",
|
|
20
|
+
"import": "./dist/payments.js",
|
|
21
|
+
"default": "./dist/payments.js"
|
|
22
|
+
},
|
|
18
23
|
"./references": {
|
|
19
24
|
"types": "./dist/references.d.ts",
|
|
20
25
|
"import": "./dist/references.js",
|
|
@@ -114,6 +119,11 @@
|
|
|
114
119
|
"types": "./dist/layout.d.ts",
|
|
115
120
|
"import": "./dist/layout.js",
|
|
116
121
|
"default": "./dist/layout.js"
|
|
122
|
+
},
|
|
123
|
+
"./timeline": {
|
|
124
|
+
"types": "./dist/timeline.d.ts",
|
|
125
|
+
"import": "./dist/timeline.js",
|
|
126
|
+
"default": "./dist/timeline.js"
|
|
117
127
|
}
|
|
118
128
|
},
|
|
119
129
|
"files": [
|