@foir/sdk 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/app/client.d.ts +8 -0
- package/dist/app/errors.d.ts +20 -0
- package/dist/app/index.d.ts +2 -2
- package/dist/app.js +11 -2
- package/dist/placement/index.d.ts +29 -0
- package/dist/placement.js +15 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,47 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
|
|
|
108
108
|
|
|
109
109
|
Changes on `dev` that have not been published to npm.
|
|
110
110
|
|
|
111
|
+
## [0.17.0] — 2026-10-02
|
|
112
|
+
|
|
113
|
+
Published under npm's `next` tag from `dev`, because it reads a field the
|
|
114
|
+
platform serves from the next promotion; `latest` moves to it then.
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- **An app is told when a project cannot email its customers**
|
|
119
|
+
(`@foir/sdk/app`). A project with no App base URL used to accept a
|
|
120
|
+
notification's email and drop it. The platform now refuses a notification
|
|
121
|
+
that carries an email, and an opt-in to a category confirmed by email, with
|
|
122
|
+
the reason as data. `appCustomerEmailUnavailableOf(error)` reads it:
|
|
123
|
+
`{ reason: 'NO_APP_BASE_URL' }`, or `null` for any other failure.
|
|
124
|
+
- `install.config()` answers `customerEmailUnavailable`: the same reason ahead
|
|
125
|
+
of time, or `null` when the project can send. `AppCustomerEmailUnavailableReason`
|
|
126
|
+
is the type.
|
|
127
|
+
|
|
128
|
+
### Changed
|
|
129
|
+
|
|
130
|
+
- `install.config()` asks the platform for `customerEmailUnavailable`, a field
|
|
131
|
+
a platform older than this release does not have.
|
|
132
|
+
|
|
133
|
+
## [0.16.0] — 2026-10-02
|
|
134
|
+
|
|
135
|
+
Published under npm's `next` tag from `dev`, as 0.15.0 was; `latest` moves to
|
|
136
|
+
it at the next promotion.
|
|
137
|
+
|
|
138
|
+
### Added
|
|
139
|
+
|
|
140
|
+
- **An embedded page is told how the console looks** (`@foir/sdk/placement`).
|
|
141
|
+
The console's theme is the operator's own setting, which a framed page cannot
|
|
142
|
+
see, so a page that followed the system's preference could sit dark inside a
|
|
143
|
+
light console. `connectPlacement` takes `onAppearance`, called with
|
|
144
|
+
`{ theme: 'light' | 'dark' }` as soon as the console says (before the token
|
|
145
|
+
arrives) and again whenever the operator changes it; `session.appearance`
|
|
146
|
+
holds the current one. `PlacementAppearance` is the type, and
|
|
147
|
+
`PLACEMENT_MESSAGE.appearance` the message.
|
|
148
|
+
- A console older than this says nothing: `onAppearance` is not called and
|
|
149
|
+
`session.appearance` is `null`, so keep the system's preference as the
|
|
150
|
+
fallback.
|
|
151
|
+
|
|
111
152
|
## [0.15.0] — 2026-10-02
|
|
112
153
|
|
|
113
154
|
Published under npm's `next` tag from `dev`, because it calls parts of the
|
package/dist/app/client.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { FoirDispatchCredential } from '../data/transport';
|
|
30
30
|
import { type FoirRetryPolicy } from '../transport/retry';
|
|
31
|
+
import type { AppCustomerEmailUnavailableReason } from './errors';
|
|
31
32
|
type Json = null | boolean | number | string | Json[] | {
|
|
32
33
|
[key: string]: Json;
|
|
33
34
|
};
|
|
@@ -95,6 +96,13 @@ export interface AppInstallConfig {
|
|
|
95
96
|
settings: JsonObject;
|
|
96
97
|
sources: AppBinding[];
|
|
97
98
|
sinks: AppBinding[];
|
|
99
|
+
/**
|
|
100
|
+
* Why the project cannot send a customer an email at the level this install
|
|
101
|
+
* sits at, or null when it can. While it is set, a notification that carries
|
|
102
|
+
* an email and an opt-in to a category confirmed by email are refused
|
|
103
|
+
* (`appCustomerEmailUnavailableOf`), so say so before one is.
|
|
104
|
+
*/
|
|
105
|
+
customerEmailUnavailable: AppCustomerEmailUnavailableReason | null;
|
|
98
106
|
}
|
|
99
107
|
/**
|
|
100
108
|
* The status an install reports. Leaving out `subscriptionHealth`,
|
package/dist/app/errors.d.ts
CHANGED
|
@@ -34,6 +34,26 @@ export declare function appCustomerUnlinkedOf(error: unknown): {
|
|
|
34
34
|
model: string;
|
|
35
35
|
identityProvider: string;
|
|
36
36
|
} | null;
|
|
37
|
+
/**
|
|
38
|
+
* Why a project cannot send a customer an email. `NO_APP_BASE_URL`: no App base
|
|
39
|
+
* URL is set for the project, or for the customer's organization. A reason
|
|
40
|
+
* this build does not know arrives as the platform spelled it.
|
|
41
|
+
*/
|
|
42
|
+
export type AppCustomerEmailUnavailableReason = 'NO_APP_BASE_URL' | (string & {});
|
|
43
|
+
/** The reason as the platform sent it, or null when it sent none. */
|
|
44
|
+
export declare function customerEmailUnavailableReasonOf(value: unknown): AppCustomerEmailUnavailableReason | null;
|
|
45
|
+
/**
|
|
46
|
+
* A request refused because it would send a customer an email the project
|
|
47
|
+
* cannot send, or null for any other failure. Nothing was written and nothing
|
|
48
|
+
* was queued. An opt-in to a category confirmed by email is refused this way,
|
|
49
|
+
* and so is a notification that carries an email.
|
|
50
|
+
*
|
|
51
|
+
* It is about the project, not the customer: the project's operator fixes it,
|
|
52
|
+
* and the same request goes once they have.
|
|
53
|
+
*/
|
|
54
|
+
export declare function appCustomerEmailUnavailableOf(error: unknown): {
|
|
55
|
+
reason: AppCustomerEmailUnavailableReason;
|
|
56
|
+
} | null;
|
|
37
57
|
/**
|
|
38
58
|
* Whether a read failed because the token is not served the field it asked
|
|
39
59
|
* for: the field is not on the schema this token's capabilities reach. That is
|
package/dist/app/index.d.ts
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
export { createAppClient } from './client';
|
|
24
24
|
export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppCustomer, AppCustomerEraseResult, AppCustomerList, AppCustomerSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
|
|
25
|
-
export { appCustomerUnlinkedOf, appLookupConflictOf, isAppFieldWithheld } from './errors';
|
|
26
|
-
export type { AppLookupConflict } from './errors';
|
|
25
|
+
export { appCustomerEmailUnavailableOf, appCustomerUnlinkedOf, appLookupConflictOf, isAppFieldWithheld } from './errors';
|
|
26
|
+
export type { AppCustomerEmailUnavailableReason, AppLookupConflict } from './errors';
|
|
27
27
|
export { appModelFieldName, appModelTypeName, appSelectorEnumValue } from './naming';
|
|
28
28
|
export { RECORD_KEY_RULE, recordKeyRefusal } from './record-key';
|
|
29
29
|
export { readAppDispatch, readCredentialProbe } from './dispatch';
|
package/dist/app.js
CHANGED
|
@@ -251,6 +251,13 @@ function appCustomerUnlinkedOf(error) {
|
|
|
251
251
|
if (typeof model !== "string" || typeof identityProvider !== "string") return null;
|
|
252
252
|
return { model, identityProvider };
|
|
253
253
|
}
|
|
254
|
+
function customerEmailUnavailableReasonOf(value) {
|
|
255
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
256
|
+
}
|
|
257
|
+
function appCustomerEmailUnavailableOf(error) {
|
|
258
|
+
const reason = customerEmailUnavailableReasonOf(extensionOf(error, "customerEmailUnavailable")?.reason);
|
|
259
|
+
return reason ? { reason } : null;
|
|
260
|
+
}
|
|
254
261
|
function isAppFieldWithheld(error, field) {
|
|
255
262
|
if (!(error instanceof FoirRequestError)) return false;
|
|
256
263
|
return (error.prunedSelections ?? []).some((selection) => {
|
|
@@ -490,7 +497,7 @@ function createAppClient(config) {
|
|
|
490
497
|
"integrationConfig",
|
|
491
498
|
`query AppInstallConfig {
|
|
492
499
|
integrationConfig {
|
|
493
|
-
name enabled middlewareUrl targetModels webhookUrl config
|
|
500
|
+
name enabled middlewareUrl targetModels webhookUrl config customerEmailUnavailable
|
|
494
501
|
sources { key model naturalKey mode fields { appField modelField direction } }
|
|
495
502
|
sinks { key model naturalKey fields { appField modelField direction } }
|
|
496
503
|
}
|
|
@@ -506,7 +513,8 @@ function createAppClient(config) {
|
|
|
506
513
|
webhookUrl: got.webhookUrl ?? "",
|
|
507
514
|
settings: got.config ?? {},
|
|
508
515
|
sources: bindingsOf(got.sources),
|
|
509
|
-
sinks: bindingsOf(got.sinks)
|
|
516
|
+
sinks: bindingsOf(got.sinks),
|
|
517
|
+
customerEmailUnavailable: customerEmailUnavailableReasonOf(got.customerEmailUnavailable)
|
|
510
518
|
};
|
|
511
519
|
},
|
|
512
520
|
async credentials() {
|
|
@@ -1145,6 +1153,7 @@ export {
|
|
|
1145
1153
|
FoirRequestError,
|
|
1146
1154
|
MAX_APP_MANIFEST_BYTES,
|
|
1147
1155
|
RECORD_KEY_RULE,
|
|
1156
|
+
appCustomerEmailUnavailableOf,
|
|
1148
1157
|
appCustomerUnlinkedOf,
|
|
1149
1158
|
appLookupConflictOf,
|
|
1150
1159
|
appModelFieldName,
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* const data = await session.request('{ preorders { items { id } } }');
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
|
+
* The console also tells the page how it looks, so the page can match it: pass
|
|
23
|
+
* `onAppearance` and apply the theme it names. The operator's console theme is
|
|
24
|
+
* their own setting and need not be their system's.
|
|
25
|
+
*
|
|
22
26
|
* The browser shows the page blank, however well it is authenticated, unless
|
|
23
27
|
* the page allows the console as a frame ancestor. Hydrogen and many other
|
|
24
28
|
* frameworks send `frame-ancestors 'none'` by default. `placementFrameAncestors`
|
|
@@ -37,6 +41,8 @@ export declare const PLACEMENT_MESSAGE: {
|
|
|
37
41
|
readonly refused: "foir:placement:refused";
|
|
38
42
|
/** Console to page: the operator signed out. */
|
|
39
43
|
readonly signedOut: "foir:placement:signed-out";
|
|
44
|
+
/** Console to page: how the console looks, now and whenever it changes. */
|
|
45
|
+
readonly appearance: "foir:placement:appearance";
|
|
40
46
|
};
|
|
41
47
|
/** What the console hands an embedded page. */
|
|
42
48
|
export interface PlacementGrant {
|
|
@@ -54,6 +60,14 @@ export interface PlacementGrant {
|
|
|
54
60
|
reason: string;
|
|
55
61
|
}[];
|
|
56
62
|
}
|
|
63
|
+
/** How the console looks, for an embedded page to match. */
|
|
64
|
+
export interface PlacementAppearance {
|
|
65
|
+
/**
|
|
66
|
+
* The console's theme as the operator sees it: their own choice, or their
|
|
67
|
+
* system's when they left it to the system.
|
|
68
|
+
*/
|
|
69
|
+
theme: 'light' | 'dark';
|
|
70
|
+
}
|
|
57
71
|
/** A message the console sends an embedded page. */
|
|
58
72
|
export type PlacementHostMessage = {
|
|
59
73
|
type: typeof PLACEMENT_MESSAGE.token;
|
|
@@ -63,6 +77,9 @@ export type PlacementHostMessage = {
|
|
|
63
77
|
reason: string;
|
|
64
78
|
} | {
|
|
65
79
|
type: typeof PLACEMENT_MESSAGE.signedOut;
|
|
80
|
+
} | {
|
|
81
|
+
type: typeof PLACEMENT_MESSAGE.appearance;
|
|
82
|
+
appearance: PlacementAppearance;
|
|
66
83
|
};
|
|
67
84
|
/** A message an embedded page sends the console. */
|
|
68
85
|
export type PlacementPageMessage = {
|
|
@@ -100,6 +117,13 @@ export interface ConnectPlacementOptions {
|
|
|
100
117
|
timeoutMs?: number;
|
|
101
118
|
/** How long before expiry to ask for a fresh token. Default 60 seconds. */
|
|
102
119
|
refreshBeforeMs?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Called with how the console looks: once as soon as the console says, which
|
|
122
|
+
* is before the token arrives, and again whenever the operator changes it.
|
|
123
|
+
* Apply it instead of the system's preference, so the page matches the
|
|
124
|
+
* console it is shown in.
|
|
125
|
+
*/
|
|
126
|
+
onAppearance?: (appearance: PlacementAppearance) => void;
|
|
103
127
|
/** The page's window. Default the global one; tests pass their own. */
|
|
104
128
|
window?: Window;
|
|
105
129
|
/** The fetch the session's requests use. Default the global one. */
|
|
@@ -109,6 +133,11 @@ export interface ConnectPlacementOptions {
|
|
|
109
133
|
export interface PlacementSession {
|
|
110
134
|
/** The grant in force now. Replaced as the session refreshes. */
|
|
111
135
|
readonly grant: PlacementGrant;
|
|
136
|
+
/**
|
|
137
|
+
* How the console looks now, or null when the console has not said (a console
|
|
138
|
+
* older than this). Kept current as the operator changes it.
|
|
139
|
+
*/
|
|
140
|
+
readonly appearance: PlacementAppearance | null;
|
|
112
141
|
/** Whether the token carries this scope, to hide or grey out what the operator cannot do. */
|
|
113
142
|
can(scope: string): boolean;
|
|
114
143
|
/** A GraphQL request to Foir's API as the operator, through your app. */
|
package/dist/placement.js
CHANGED
|
@@ -9,7 +9,9 @@ var PLACEMENT_MESSAGE = {
|
|
|
9
9
|
/** Console to page: no token, and why. */
|
|
10
10
|
refused: "foir:placement:refused",
|
|
11
11
|
/** Console to page: the operator signed out. */
|
|
12
|
-
signedOut: "foir:placement:signed-out"
|
|
12
|
+
signedOut: "foir:placement:signed-out",
|
|
13
|
+
/** Console to page: how the console looks, now and whenever it changes. */
|
|
14
|
+
appearance: "foir:placement:appearance"
|
|
13
15
|
};
|
|
14
16
|
var FOIR_CONSOLE_ORIGINS = {
|
|
15
17
|
production: ["https://app.foir.io"],
|
|
@@ -41,6 +43,10 @@ var PlacementRefused = class extends Error {
|
|
|
41
43
|
function isHostMessage(data) {
|
|
42
44
|
if (typeof data !== "object" || data === null) return false;
|
|
43
45
|
const t = data.type;
|
|
46
|
+
if (t === PLACEMENT_MESSAGE.appearance) {
|
|
47
|
+
const theme = data.appearance?.theme;
|
|
48
|
+
return theme === "light" || theme === "dark";
|
|
49
|
+
}
|
|
44
50
|
return t === PLACEMENT_MESSAGE.token || t === PLACEMENT_MESSAGE.refused || t === PLACEMENT_MESSAGE.signedOut;
|
|
45
51
|
}
|
|
46
52
|
function connectPlacement(options) {
|
|
@@ -55,6 +61,7 @@ function connectPlacement(options) {
|
|
|
55
61
|
}
|
|
56
62
|
return new Promise((resolve, reject) => {
|
|
57
63
|
let grant = null;
|
|
64
|
+
let appearance = null;
|
|
58
65
|
let settled = false;
|
|
59
66
|
let refreshTimer;
|
|
60
67
|
const signedOut = /* @__PURE__ */ new Set();
|
|
@@ -77,6 +84,9 @@ function connectPlacement(options) {
|
|
|
77
84
|
if (!grant) throw new PlacementRefused("the operator has signed out");
|
|
78
85
|
return grant;
|
|
79
86
|
},
|
|
87
|
+
get appearance() {
|
|
88
|
+
return appearance;
|
|
89
|
+
},
|
|
80
90
|
can: (scope) => grant?.grantedScopes.includes(scope) ?? false,
|
|
81
91
|
async request(document, variables) {
|
|
82
92
|
if (!grant) throw new PlacementRefused("the operator has signed out");
|
|
@@ -123,6 +133,10 @@ function connectPlacement(options) {
|
|
|
123
133
|
close();
|
|
124
134
|
for (const l of signedOut) l();
|
|
125
135
|
return;
|
|
136
|
+
case PLACEMENT_MESSAGE.appearance:
|
|
137
|
+
appearance = { theme: msg.appearance.theme };
|
|
138
|
+
options.onAppearance?.(appearance);
|
|
139
|
+
return;
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
const timeout = setTimeout(() => {
|