@phis/contracts 0.4.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.
@@ -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
+ };
@@ -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 {};
@@ -0,0 +1,8 @@
1
+ export declare function isPhiRecord(value: unknown): value is Record<string, unknown>;
2
+ /**
3
+ * The value a dotted path names inside a record: `"author.name"` reads `input.author.name`.
4
+ *
5
+ * Empty segments are skipped, so `""` names the input itself, and only records are walked -- an array
6
+ * or a primitive on the way ends the walk with `undefined` rather than being indexed into.
7
+ */
8
+ export declare function readPhiDotPath(input: unknown, path: string): unknown;
package/dist/record.js ADDED
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Shared by the parsers of this package and not exported from it: each consumer keeps its own, and
3
+ * the one question they all answer is settled the same way -- an array is not a keyed record.
4
+ */
5
+ export function isPhiRecord(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ /**
9
+ * The value a dotted path names inside a record: `"author.name"` reads `input.author.name`.
10
+ *
11
+ * Empty segments are skipped, so `""` names the input itself, and only records are walked -- an array
12
+ * or a primitive on the way ends the walk with `undefined` rather than being indexed into.
13
+ */
14
+ export function readPhiDotPath(input, path) {
15
+ let current = input;
16
+ for (const segment of path.split(".")) {
17
+ if (!segment)
18
+ continue;
19
+ if (!isPhiRecord(current))
20
+ return undefined;
21
+ current = current[segment];
22
+ }
23
+ return current;
24
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * What one side of the house means when it points at a Page: `@phis/server/references:v1`.
3
+ *
4
+ * The codec stood in two copies, in phis-ui and in phi-server, for the same reason the CMS instance id
5
+ * did before it moved here -- and with the same risk, which is that two sides agreeing byte for byte is
6
+ * a thing nobody notices stopping. It is one file now.
7
+ *
8
+ * **A reference is short because a Page already has a name.** It used to be base64-encoded JSON carrying
9
+ * the whole identity, which for a Module Page ran to 98 characters in every Markdown body that linked to
10
+ * one. The envelope was never protection -- it is encoding, not a signature, and the server revalidates
11
+ * Site and Area regardless -- so what it actually bought was a payload the server could unpack. It no
12
+ * longer needs to: a Module Page is named by the CMS Page id derived from `(ownerModuleId, presetKey)`,
13
+ * which is recomputable rather than allocated and therefore stands before any row does.
14
+ *
15
+ * The two variants are shaped differently on purpose. A Site Page carries its Page Scope id as a plain
16
+ * number because the server writes that number into a foreign key with `ON DELETE RESTRICT`: the
17
+ * database itself refuses to delete a Page something still points at, and nothing derived could take
18
+ * that column's place. A Module Page has no row to point a foreign key at -- the Site may never have
19
+ * touched it -- so it carries the derived id, which the server passes on without ever unpacking.
20
+ */
21
+ import { type PhiCmsAreaKey, type PhiCmsInstanceId } from "./cms.js";
22
+ export declare const PHIS_INTERNAL_PAGE_SCHEME: "phis:page/";
23
+ export declare const PHIS_INTERNAL_ASSET_SCHEME: "phis:asset/";
24
+ /**
25
+ * A Module Page's identity, hashed to one token both sides can recompute.
26
+ *
27
+ * Recomputable rather than allocated, so no row has to exist before the Page can be addressed, and it
28
+ * never mentions the path: reassigning where a Page answers leaves everything pointing at it alone.
29
+ */
30
+ export declare function createPhiPresetCmsPageId(identity: {
31
+ ownerModuleId: string;
32
+ presetKey: string;
33
+ }): PhiCmsInstanceId;
34
+ /** What a reference says, once read. A Module Page answers with its id, never with what was hashed. */
35
+ export type PhiPageTarget = {
36
+ kind: "site";
37
+ pageScopeId: number;
38
+ } | {
39
+ kind: "module";
40
+ pageId: PhiCmsInstanceId;
41
+ };
42
+ /** What a reference is made from. The Module variant differs: the identity goes in, the id comes out. */
43
+ export type PhiPageTargetInput = {
44
+ kind: "site";
45
+ pageScopeId: number;
46
+ } | {
47
+ kind: "module";
48
+ ownerModuleId: string;
49
+ presetKey: string;
50
+ };
51
+ export type PhiPageReference = string & {
52
+ readonly __phiPageReference: unique symbol;
53
+ };
54
+ export type PhiInternalReference = {
55
+ kind: "page";
56
+ reference: PhiPageReference;
57
+ target: PhiPageTarget;
58
+ fragment: string | null;
59
+ } | {
60
+ kind: "asset";
61
+ assetId: number;
62
+ };
63
+ export declare function createPhiPageReference(target: PhiPageTargetInput): PhiPageReference;
64
+ export declare function readPhiPageReference(value: unknown): {
65
+ reference: PhiPageReference;
66
+ target: PhiPageTarget;
67
+ } | null;
68
+ export declare function createPhiPageUri(reference: PhiPageReference, fragment?: string | null): string;
69
+ export declare function createPhiAssetUri(assetId: number): string;
70
+ export declare function readPhiInternalReference(value: unknown): PhiInternalReference | null;
71
+ /**
72
+ * Where a Control that offers one link leads.
73
+ *
74
+ * Two kinds and no third. A Page is named by its reference, which is identity and outlives the path
75
+ * moving under it; everything else is a literal URL somebody typed. What this refuses is the middle --
76
+ * the root-relative string that reads as external and means internal -- because that is the shape that
77
+ * rots in silence: the Page moves, the string does not, and nothing in the system ever knew the two were
78
+ * related.
79
+ *
80
+ * `newTab` sits inside the target rather than beside it. A Widget that offers two links -- a Card with a
81
+ * heading and an action -- otherwise grows one flag per link and names them in parallel (`newTab`,
82
+ * `actionNewTab`, and a third the day a third link lands). That is one decision written twice, and it
83
+ * drifts the first time only one of the two is read.
84
+ */
85
+ export type PhiLinkTarget = {
86
+ kind: "page";
87
+ reference: PhiPageReference;
88
+ /**
89
+ * Which Area to resolve the reference in, where it is not the one asking.
90
+ *
91
+ * A reference names a Page and not where to look for it: the Area is resolution context, supplied
92
+ * by whoever asks. Every asker supplied its own, so a link out of one Area into another resolved
93
+ * to nothing -- the Navigation source tree let an author pick a Page from another Area, the drop
94
+ * kept it, and the table then showed it as unresolvable.
95
+ *
96
+ * Absent means the asking Area, which is nearly every link, so nothing is stored for the ordinary
97
+ * case. Present, it does not make the reference mean something else; it says where the question
98
+ * goes. Whether the reader may follow it is still the target Area's own answer.
99
+ */
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>>;
110
+ fragment?: string | null;
111
+ newTab?: boolean;
112
+ } | {
113
+ kind: "external";
114
+ href: string;
115
+ newTab?: boolean;
116
+ };
117
+ /**
118
+ * The field names a stored link target may stand under, and nothing else identifies one in persisted
119
+ * config.
120
+ *
121
+ * The Asset rule is the precedent and the reason: the server's reference collector reads persisted JSON,
122
+ * not the Widget catalogue, so it cannot ask a plugin which of its fields hold targets. A Widget that
123
+ * keeps one under another name authors a reference the delete guard cannot see, and the Page it points
124
+ * at can be removed while the Widget still draws a link to it.
125
+ *
126
+ * One plain name for the single-link case and a suffix for the rest, so a Card's second link is
127
+ * `actionLinkTarget` and is found by the same rule that finds the first.
128
+ */
129
+ export declare const PHI_LINK_TARGET_CONFIG_KEY = "linkTarget";
130
+ export declare function isPhiLinkTargetConfigKey(key: string): boolean;
131
+ export declare function isPhiStorableExternalHref(value: string): boolean;
132
+ /**
133
+ * A route pattern with its parameters filled, or `null` while one is still open.
134
+ *
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.
137
+ */
138
+ export declare function fillPhiRouteParams(pattern: string, params: Readonly<Record<string, string>> | null | undefined): string | null;
139
+ export declare function readPhiLinkTarget(value: unknown): PhiLinkTarget | null;