@waler/sdk 0.1.1
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/bridge/iframe.d.ts +23 -0
- package/dist/bridge/iframe.js +103 -0
- package/dist/bridge/null.d.ts +6 -0
- package/dist/bridge/null.js +8 -0
- package/dist/bridge/rejecting.d.ts +11 -0
- package/dist/bridge/rejecting.js +36 -0
- package/dist/bridge/select.d.ts +8 -0
- package/dist/bridge/select.js +16 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.js +21 -0
- package/dist/client/capabilities.d.ts +10 -0
- package/dist/client/capabilities.js +16 -0
- package/dist/client/events.d.ts +6 -0
- package/dist/client/events.js +36 -0
- package/dist/client/ready.d.ts +11 -0
- package/dist/client/ready.js +50 -0
- package/dist/client/rpc.d.ts +20 -0
- package/dist/client/rpc.js +94 -0
- package/dist/client/sdk.d.ts +7 -0
- package/dist/client/sdk.js +100 -0
- package/dist/client/timeouts.d.ts +12 -0
- package/dist/client/timeouts.js +17 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +38 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +21 -0
- package/dist/protocol/ids.d.ts +1 -0
- package/dist/protocol/ids.js +15 -0
- package/dist/protocol/wire.d.ts +22 -0
- package/dist/protocol/wire.js +69 -0
- package/dist/types.d.ts +324 -0
- package/dist/types.js +8 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +68 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of `@waler/sdk`.
|
|
3
|
+
*
|
|
4
|
+
* These declarations are additive: fields are added, never repurposed. An app
|
|
5
|
+
* built against an older version of this package keeps working against a newer
|
|
6
|
+
* shell, which is the guarantee the whole design rests on.
|
|
7
|
+
*/
|
|
8
|
+
/** Where the app is running. `standalone` means it is outside a shell. */
|
|
9
|
+
export type Platform = 'web' | 'ios' | 'android' | 'standalone';
|
|
10
|
+
/**
|
|
11
|
+
* Which population a user belongs to. One shell serves all of them, and a
|
|
12
|
+
* surface declares which ones it is for.
|
|
13
|
+
*
|
|
14
|
+
* It is derived from what the user actually has access to, never assigned
|
|
15
|
+
* directly, so an app can read it but never change it.
|
|
16
|
+
*/
|
|
17
|
+
export type Audience = 'buyer' | 'staff' | 'broker';
|
|
18
|
+
/**
|
|
19
|
+
* The context the shell resolved for this surface.
|
|
20
|
+
*
|
|
21
|
+
* `tenantId`, `userId`, `audiences` and `activeAudience` are always present.
|
|
22
|
+
* Every other field is present only when the surface declared that it needs it,
|
|
23
|
+
* so check before use.
|
|
24
|
+
*/
|
|
25
|
+
export interface ShellContext {
|
|
26
|
+
tenantId: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
/**
|
|
29
|
+
* Every population this user belongs to. It can hold more than one, so never
|
|
30
|
+
* assume a single element and never read `audiences[0]` as "the" audience.
|
|
31
|
+
*/
|
|
32
|
+
audiences: readonly Audience[];
|
|
33
|
+
/**
|
|
34
|
+
* The population the shell is rendering under right now, and the one to branch
|
|
35
|
+
* on. `audiences` says what the user could switch to; this says what they are
|
|
36
|
+
* looking at. It always belongs to `audiences`, and it can change without a
|
|
37
|
+
* reload — listen for `audiencechange`.
|
|
38
|
+
*/
|
|
39
|
+
activeAudience: Audience;
|
|
40
|
+
ventureId?: string;
|
|
41
|
+
buildingId?: string;
|
|
42
|
+
unitId?: string;
|
|
43
|
+
salesContractId?: string;
|
|
44
|
+
partyId?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Theme tokens for the current tenant, so an app can match the surrounding UI. */
|
|
47
|
+
export interface ThemeTokens {
|
|
48
|
+
colorScheme: 'light' | 'dark';
|
|
49
|
+
primary: string;
|
|
50
|
+
onPrimary: string;
|
|
51
|
+
surface: string;
|
|
52
|
+
onSurface: string;
|
|
53
|
+
border: string;
|
|
54
|
+
danger: string;
|
|
55
|
+
radius: string;
|
|
56
|
+
fontFamily: string;
|
|
57
|
+
/** Extra tokens a tenant may define. Always treat them as optional. */
|
|
58
|
+
[token: string]: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Capabilities a shell may offer. The list grows over time, so `Capability`
|
|
62
|
+
* accepts any string: never break on seeing a name you do not recognise.
|
|
63
|
+
*/
|
|
64
|
+
export type KnownCapability = 'download' | 'notify' | 'share' | 'camera' | 'scanBarcode' | 'geolocation' | 'biometrics' | 'haptics' | 'clipboard' | 'openExternal' | 'requestReview';
|
|
65
|
+
export type Capability = KnownCapability | (string & {});
|
|
66
|
+
export interface CapabilitySet {
|
|
67
|
+
/** Check support with this, never by comparing versions. */
|
|
68
|
+
has(capability: Capability): boolean;
|
|
69
|
+
list(): readonly Capability[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Where to reach the platform API for this tenant and environment.
|
|
73
|
+
*
|
|
74
|
+
* Never hardcode these. One build of an app runs inside many shells on different
|
|
75
|
+
* domains and across environments; only the shell knows which one this is.
|
|
76
|
+
*/
|
|
77
|
+
export interface PlatformEndpoints {
|
|
78
|
+
/** Root of the platform API. Build request URLs from this. */
|
|
79
|
+
baseUrl: string;
|
|
80
|
+
/**
|
|
81
|
+
* Issuer of the tokens `getToken()` returns. Your backend validates them by
|
|
82
|
+
* discovering the JWKS from here, instead of shipping a key in its code.
|
|
83
|
+
*/
|
|
84
|
+
issuer: string;
|
|
85
|
+
}
|
|
86
|
+
/** Resolved when the handshake with the shell finishes. */
|
|
87
|
+
export interface ReadyPayload {
|
|
88
|
+
/** Version of the host shell. For telemetry only — gate on `capabilities`. */
|
|
89
|
+
shellVersion: string;
|
|
90
|
+
sdkVersion: string;
|
|
91
|
+
platform: Platform;
|
|
92
|
+
/** Where to reach the platform API. Never hardcode it. */
|
|
93
|
+
api: PlatformEndpoints;
|
|
94
|
+
capabilities: readonly Capability[];
|
|
95
|
+
locale: string;
|
|
96
|
+
timeZone: string;
|
|
97
|
+
theme: ThemeTokens;
|
|
98
|
+
context: ShellContext;
|
|
99
|
+
}
|
|
100
|
+
export type KnownSdkErrorCode =
|
|
101
|
+
/** The shell does not offer this method or capability. Take your fallback path. */
|
|
102
|
+
'UNSUPPORTED'
|
|
103
|
+
/** Not granted to this app, or denied for this user. */
|
|
104
|
+
| 'FORBIDDEN'
|
|
105
|
+
/** The user backed out, or denied a permission. */
|
|
106
|
+
| 'CANCELLED' | 'TIMEOUT' | 'INVALID_ARGUMENT'
|
|
107
|
+
/** The app is running outside a shell. */
|
|
108
|
+
| 'NOT_IN_SHELL' | 'INTERNAL';
|
|
109
|
+
/**
|
|
110
|
+
* An OPEN union, for the same reason `Capability` is open: the shell moves ahead
|
|
111
|
+
* of the SDK. A shell that learns a new code
|
|
112
|
+
* would have it collapsed into `INTERNAL` by a closed union, and the app would
|
|
113
|
+
* lose the one piece of information that could have told it what happened —
|
|
114
|
+
* silently, and only in the tenants that had already updated.
|
|
115
|
+
*
|
|
116
|
+
* `if (e.code === 'UNSUPPORTED')` keeps working unchanged. What this costs is
|
|
117
|
+
* that an exhaustive `switch` now needs a `default`, which it needed anyway.
|
|
118
|
+
*/
|
|
119
|
+
export type SdkErrorCode = KnownSdkErrorCode | (string & {});
|
|
120
|
+
export interface SdkError extends Error {
|
|
121
|
+
readonly code: SdkErrorCode;
|
|
122
|
+
readonly method?: string;
|
|
123
|
+
readonly details?: unknown;
|
|
124
|
+
}
|
|
125
|
+
export interface TokenRequest {
|
|
126
|
+
/** Who the token is for. Omit it for your own backend. */
|
|
127
|
+
audience?: string;
|
|
128
|
+
scopes?: readonly string[];
|
|
129
|
+
/** Skip the cache and request a freshly issued token. */
|
|
130
|
+
forceRefresh?: boolean;
|
|
131
|
+
}
|
|
132
|
+
export interface TokenResult {
|
|
133
|
+
token: string;
|
|
134
|
+
expiresAt: string;
|
|
135
|
+
scopes: readonly string[];
|
|
136
|
+
}
|
|
137
|
+
export interface SdkEventMap {
|
|
138
|
+
/** The shell resolved a different context, without reloading the surface. */
|
|
139
|
+
contextchange: ShellContext;
|
|
140
|
+
/** The user switched populations. Only fires for users who have more than one. */
|
|
141
|
+
audiencechange: {
|
|
142
|
+
activeAudience: Audience;
|
|
143
|
+
audiences: readonly Audience[];
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* The result of a write this app started, which completes asynchronously.
|
|
147
|
+
*
|
|
148
|
+
* Assume both: it MAY NOT ARRIVE, so rebuild state when your view opens; and it
|
|
149
|
+
* MAY ARRIVE TWICE, so handle it idempotently by `commandId`.
|
|
150
|
+
*/
|
|
151
|
+
commandupdate: {
|
|
152
|
+
commandId: string;
|
|
153
|
+
status: 'dispatched' | 'confirmed' | 'failed' | 'compensated';
|
|
154
|
+
error?: {
|
|
155
|
+
code: string;
|
|
156
|
+
message: string;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
themechange: ThemeTokens;
|
|
160
|
+
localechange: {
|
|
161
|
+
locale: string;
|
|
162
|
+
timeZone: string;
|
|
163
|
+
};
|
|
164
|
+
/** The surface left or came back to the screen. Pause background work when hidden. */
|
|
165
|
+
visibilitychange: {
|
|
166
|
+
visible: boolean;
|
|
167
|
+
};
|
|
168
|
+
/** Early warning that the current token is about to expire. Call `getToken()`. */
|
|
169
|
+
tokenexpiring: {
|
|
170
|
+
expiresAt: string;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export type Unsubscribe = () => void;
|
|
174
|
+
export interface NotifyOptions {
|
|
175
|
+
/** A notification channel this app declared. */
|
|
176
|
+
channel: string;
|
|
177
|
+
title: string;
|
|
178
|
+
body?: string;
|
|
179
|
+
/** Which surface to open when the notification is tapped. */
|
|
180
|
+
deepLink?: {
|
|
181
|
+
key: string;
|
|
182
|
+
params?: Record<string, string>;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export interface DownloadOptions {
|
|
186
|
+
url: string;
|
|
187
|
+
filename: string;
|
|
188
|
+
mimeType?: string;
|
|
189
|
+
}
|
|
190
|
+
export interface ShareOptions {
|
|
191
|
+
title?: string;
|
|
192
|
+
text?: string;
|
|
193
|
+
url?: string;
|
|
194
|
+
}
|
|
195
|
+
export interface ScanResult {
|
|
196
|
+
value: string;
|
|
197
|
+
format: string;
|
|
198
|
+
}
|
|
199
|
+
export interface GeolocationResult {
|
|
200
|
+
latitude: number;
|
|
201
|
+
longitude: number;
|
|
202
|
+
accuracy: number;
|
|
203
|
+
capturedAt: string;
|
|
204
|
+
}
|
|
205
|
+
export interface HeaderState {
|
|
206
|
+
title?: string;
|
|
207
|
+
/** Badge count shown on this app inside the shell. */
|
|
208
|
+
badge?: number;
|
|
209
|
+
/** Hide the shell's header for a full-bleed screen. */
|
|
210
|
+
hidden?: boolean;
|
|
211
|
+
}
|
|
212
|
+
export interface WalerSDK {
|
|
213
|
+
/**
|
|
214
|
+
* Resolves when the handshake with the shell completes, and only over a live
|
|
215
|
+
* bridge with a payload the SDK has already checked — so after `await ready`,
|
|
216
|
+
* `context` is filled and `capabilities` is populated.
|
|
217
|
+
*
|
|
218
|
+
* Rejects with `NOT_IN_SHELL` when there is no shell, and with `FORBIDDEN` when
|
|
219
|
+
* the shell refuses this surface. It never resolves half-way.
|
|
220
|
+
*/
|
|
221
|
+
readonly ready: Promise<ReadyPayload>;
|
|
222
|
+
/**
|
|
223
|
+
* Null until `ready` resolves, then kept in step with `contextchange` and
|
|
224
|
+
* `audiencechange`. Prefer `const { context } = await Waler.ready`, which has
|
|
225
|
+
* no null in it; this is for code that runs later and cannot await.
|
|
226
|
+
*/
|
|
227
|
+
readonly context: ShellContext | null;
|
|
228
|
+
readonly capabilities: CapabilitySet;
|
|
229
|
+
getToken(request?: TokenRequest): Promise<TokenResult>;
|
|
230
|
+
/**
|
|
231
|
+
* Opens another surface — this app's or another installed app's — and resolves
|
|
232
|
+
* with whatever that surface passed to `close(result)`. Allow for it taking as
|
|
233
|
+
* long as a person takes.
|
|
234
|
+
*/
|
|
235
|
+
openApp<T = void>(key: string, params?: Record<string, string>): Promise<T>;
|
|
236
|
+
/**
|
|
237
|
+
* Tells the shell your current route, for deep links and the back button.
|
|
238
|
+
*
|
|
239
|
+
* Call it yourself: the SDK does not watch `history.pushState` to infer it,
|
|
240
|
+
* because patching a global inside your app would fight whatever your router
|
|
241
|
+
* does to the same function.
|
|
242
|
+
*/
|
|
243
|
+
navigate(path: string): void;
|
|
244
|
+
/** Closes this surface. `result` resolves the `openApp()` that opened it. */
|
|
245
|
+
close(result?: unknown): void;
|
|
246
|
+
/** Sets the title, badge or visibility of the shell's header for this surface. */
|
|
247
|
+
setHeader(state: HeaderState): void;
|
|
248
|
+
notify(options: NotifyOptions): Promise<void>;
|
|
249
|
+
download(options: DownloadOptions): Promise<void>;
|
|
250
|
+
share(options: ShareOptions): Promise<void>;
|
|
251
|
+
scanBarcode(): Promise<ScanResult>;
|
|
252
|
+
getGeolocation(): Promise<GeolocationResult>;
|
|
253
|
+
on<K extends keyof SdkEventMap>(event: K, handler: (payload: SdkEventMap[K]) => void): Unsubscribe;
|
|
254
|
+
/**
|
|
255
|
+
* Typed escape hatch for methods newer than this package. Rejects with
|
|
256
|
+
* `UNSUPPORTED` if the shell does not know the method.
|
|
257
|
+
*/
|
|
258
|
+
invoke<T = unknown>(method: string, payload?: unknown): Promise<T>;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Options for `createSdk`.
|
|
262
|
+
*
|
|
263
|
+
* Nothing is installed on `window` and nothing happens on import; if you want a
|
|
264
|
+
* global, assign one yourself: `window.Waler = createSdk({ … })`.
|
|
265
|
+
*/
|
|
266
|
+
export interface WalerSdkOptions {
|
|
267
|
+
/** This app's registered id, sent in the handshake. */
|
|
268
|
+
readonly appId: string;
|
|
269
|
+
/**
|
|
270
|
+
* The shell origins this surface accepts, and the only ones it will ever post
|
|
271
|
+
* to. Anything that is not an absolute `http(s)` origin is dropped, so no
|
|
272
|
+
* wildcard can be expressed — `'*'` would deliver to whatever page embedded
|
|
273
|
+
* the app, permitted or not.
|
|
274
|
+
*
|
|
275
|
+
* If nothing usable is left, `ready` rejects with `INVALID_ARGUMENT` rather
|
|
276
|
+
* than silently reaching nobody.
|
|
277
|
+
*/
|
|
278
|
+
readonly allowedOrigins: readonly string[];
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The envelope every message travels in. `v` is the protocol's version — not
|
|
282
|
+
* the shell's and not this package's — and it only moves on a breaking change to
|
|
283
|
+
* the protocol itself.
|
|
284
|
+
*/
|
|
285
|
+
export interface BridgeEnvelope {
|
|
286
|
+
v: 1;
|
|
287
|
+
/** Pairs one request with its response. */
|
|
288
|
+
id: string;
|
|
289
|
+
/**
|
|
290
|
+
* Identifies a whole user action, across every hop it causes. Where `id` pairs
|
|
291
|
+
* two messages, this stays the same for everything one action sets off, so a
|
|
292
|
+
* single failure can be traced through every system that handled it.
|
|
293
|
+
*/
|
|
294
|
+
correlationId: string;
|
|
295
|
+
kind: 'request' | 'response' | 'error' | 'event';
|
|
296
|
+
method?: string;
|
|
297
|
+
/**
|
|
298
|
+
* A `string` rather than the closed set of known event names: a shell newer
|
|
299
|
+
* than this package can send names it cannot enumerate. Unknown names find no
|
|
300
|
+
* handler and are dropped.
|
|
301
|
+
*/
|
|
302
|
+
event?: string;
|
|
303
|
+
payload?: unknown;
|
|
304
|
+
error?: {
|
|
305
|
+
code: SdkErrorCode;
|
|
306
|
+
message: string;
|
|
307
|
+
details?: unknown;
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The contract every transport implements. The SDK picks one for the platform it
|
|
312
|
+
* finds itself on, and an app never sees the difference.
|
|
313
|
+
*
|
|
314
|
+
* Note what is NOT here: no origin, no target window, no serialisation. Those
|
|
315
|
+
* belong to whichever transport needs them, so this interface stays
|
|
316
|
+
* implementable everywhere the SDK runs.
|
|
317
|
+
*/
|
|
318
|
+
export interface BridgeTransport {
|
|
319
|
+
readonly platform: Platform;
|
|
320
|
+
send(envelope: BridgeEnvelope): void;
|
|
321
|
+
onMessage(handler: (envelope: BridgeEnvelope) => void): Unsubscribe;
|
|
322
|
+
/** `false` on the null transport — the app is outside the shell. */
|
|
323
|
+
readonly connected: boolean;
|
|
324
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of `@waler/sdk`.
|
|
3
|
+
*
|
|
4
|
+
* These declarations are additive: fields are added, never repurposed. An app
|
|
5
|
+
* built against an older version of this package keeps working against a newer
|
|
6
|
+
* shell, which is the guarantee the whole design rests on.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waler/sdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The surface a Waler app talks to the shell through.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"waler",
|
|
8
|
+
"sdk",
|
|
9
|
+
"micro-frontend",
|
|
10
|
+
"iframe",
|
|
11
|
+
"postmessage",
|
|
12
|
+
"bridge",
|
|
13
|
+
"superapp"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/waler-app/waler-sdk.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/waler-app/waler-sdk#readme",
|
|
20
|
+
"bugs": "https://github.com/waler-app/waler-sdk/issues",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
41
|
+
"@changesets/cli": "^3.0.1",
|
|
42
|
+
"@commitlint/cli": "^21.2.2",
|
|
43
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
44
|
+
"@eslint/js": "^10.0.1",
|
|
45
|
+
"@types/node": "^26.4.0",
|
|
46
|
+
"eslint": "^10.9.1",
|
|
47
|
+
"eslint-config-prettier": "^10.1.8",
|
|
48
|
+
"prettier": "^3.9.6",
|
|
49
|
+
"publint": "^0.3.24",
|
|
50
|
+
"typescript": "^6.0.3",
|
|
51
|
+
"typescript-eslint": "^8.68.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
55
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"lint:fix": "eslint . --fix",
|
|
58
|
+
"format": "prettier --write .",
|
|
59
|
+
"format:check": "prettier --check .",
|
|
60
|
+
"test": "node --test test/*.test.ts",
|
|
61
|
+
"check:origins": "pnpm build && node tools/origin-check/serve.ts",
|
|
62
|
+
"check:package": "pnpm build && publint && attw --pack . --profile esm-only",
|
|
63
|
+
"verify:tarball": "node tools/verify-tarball.ts",
|
|
64
|
+
"changeset": "changeset",
|
|
65
|
+
"version-packages": "changeset version && node tools/sync-version.ts && prettier --write package.json CHANGELOG.md src/version.ts",
|
|
66
|
+
"release": "pnpm check:package && changeset publish"
|
|
67
|
+
}
|
|
68
|
+
}
|