spfn 0.3.0-beta.13 → 0.3.0-beta.15

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,228 @@
1
+ /**
2
+ * What a surface manifest describes and a call sends, as the client sees it.
3
+ */
4
+ type SurfaceEffect = 'read' | 'write' | 'destructive';
5
+ interface SurfaceModuleDescriptor {
6
+ id: string;
7
+ source: string;
8
+ contractVersion: string;
9
+ summary: string;
10
+ }
11
+ interface SurfaceCommandDescriptor {
12
+ name: string;
13
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
14
+ path: string;
15
+ module?: string;
16
+ summary?: string;
17
+ effect?: SurfaceEffect;
18
+ scopes?: string[];
19
+ /**
20
+ * Set when the app announced module metadata that failed validation, so
21
+ * this command's effect is unknown rather than absent. Callers must treat
22
+ * it as needing confirmation.
23
+ */
24
+ metadataRejected?: boolean;
25
+ input: {
26
+ params?: Record<string, unknown>;
27
+ query?: Record<string, unknown>;
28
+ body?: Record<string, unknown>;
29
+ };
30
+ }
31
+ interface SurfaceManifest {
32
+ manifestVersion: 1;
33
+ modules?: SurfaceModuleDescriptor[];
34
+ commands: SurfaceCommandDescriptor[];
35
+ }
36
+ interface SurfaceCallInput {
37
+ params: Record<string, string>;
38
+ query: Record<string, string>;
39
+ body: unknown | undefined;
40
+ }
41
+ interface SurfaceResponse {
42
+ status: number;
43
+ body: unknown;
44
+ }
45
+
46
+ /**
47
+ * Manifest trust
48
+ *
49
+ * The manifest is the app's own description of itself, and the client turns
50
+ * it into requests carrying a credential. So it is read as untrusted input:
51
+ * every command is checked against the surface's namespace before it can
52
+ * become a request, and what cannot be used is dropped with a warning rather
53
+ * than costing the caller every other command on the surface.
54
+ *
55
+ * Warnings go to the caller's sink with a structured description beside the
56
+ * message, so a CLI can word them in its own terms; nothing here writes to a
57
+ * terminal.
58
+ */
59
+
60
+ /** What the client dropped from a manifest, and why. */
61
+ type SurfaceWarning = {
62
+ kind: 'command';
63
+ reason: string;
64
+ } | {
65
+ kind: 'metadata';
66
+ command: string;
67
+ reason: string;
68
+ } | {
69
+ kind: 'modules';
70
+ reason: string;
71
+ } | {
72
+ kind: 'module';
73
+ reason: string;
74
+ };
75
+ type SurfaceWarningSink = (message: string, warning: SurfaceWarning) => void;
76
+
77
+ /**
78
+ * HTTP client for an app's command surface
79
+ *
80
+ * Talks to the running (usually deployed) app: fetches
81
+ * `GET <namespace>/_manifest` for command discovery and invokes commands
82
+ * against their declared method and path. The server owns validation — the
83
+ * client sends what its caller gave it and relays the server's answer.
84
+ *
85
+ * `spfn ops` is this client on `/_ops`; any namespace a `defineSurface`
86
+ * serves is reached the same way. The client writes nothing to a terminal:
87
+ * what it drops from a manifest goes to `onWarning`, and what it refuses is
88
+ * thrown — an app URL that would carry the credential in the clear among it.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * import { createSurfaceClient } from 'spfn/surface';
93
+ *
94
+ * const client = createSurfaceClient({
95
+ * appUrl: 'https://app.example',
96
+ * namespace: '/_app',
97
+ * token: async () => issueToken(),
98
+ * });
99
+ * const answer = await client.call('pageAdd', { params: {}, query: {}, body: { title: 'Hello' } });
100
+ * ```
101
+ */
102
+
103
+ interface SurfaceClientOptions {
104
+ /** The app's URL, base path included. https, or http against loopback. */
105
+ appUrl: string;
106
+ /** The surface's namespace, e.g. `/_app`. Its manifest is `<namespace>/_manifest`. */
107
+ namespace: string;
108
+ /** The bearer credential, or a function resolving it before each request. */
109
+ token: string | (() => Promise<string>);
110
+ /** Told about every command or module the client drops. Default: silent. */
111
+ onWarning?: SurfaceWarningSink;
112
+ }
113
+ interface SurfaceClient {
114
+ /** Fetch the manifest, keeping only the commands this client can call. */
115
+ manifest(): Promise<SurfaceManifest>;
116
+ /** One command's usage text, from a manifest already fetched. */
117
+ describe(manifest: SurfaceManifest, name: string): string;
118
+ /** Fetch the manifest and invoke the named command. */
119
+ call(name: string, input: SurfaceCallInput): Promise<SurfaceResponse>;
120
+ /** Invoke a command taken from a manifest already fetched. */
121
+ invoke(command: SurfaceCommandDescriptor, input: SurfaceCallInput): Promise<SurfaceResponse>;
122
+ }
123
+ /** The manifest request answered something other than 200. */
124
+ declare class SurfaceManifestError extends Error {
125
+ readonly status: number;
126
+ constructor(message: string, status: number);
127
+ }
128
+ /**
129
+ * Append an absolute path to the app URL, keeping whatever base path the
130
+ * caller gave.
131
+ *
132
+ * Concatenation rather than `new URL(path, appUrl)`, because an absolute path
133
+ * resolved against a base replaces it: an app mounted at
134
+ * `https://example.com/api` would be called at `https://example.com/_ops/...`.
135
+ * Every request goes through here so the two halves of the ops CLI — the ops
136
+ * calls and the administrator sign-in — agree about where the app is.
137
+ */
138
+ declare function joinUrl(appUrl: string, path: string): string;
139
+ /**
140
+ * Create a client for one surface of one app. The app URL and namespace are
141
+ * checked here, so a client that exists can only send its credential where
142
+ * it is safe to.
143
+ */
144
+ declare function createSurfaceClient(options: SurfaceClientOptions): SurfaceClient;
145
+
146
+ /**
147
+ * Command paths: whether a manifest's path template is safe to request, and
148
+ * the request path a call builds from it.
149
+ */
150
+
151
+ /**
152
+ * Substitute `:name` segments from params. A missing parameter fails here,
153
+ * before any request leaves the machine.
154
+ *
155
+ * The template is checked for namespace escape, and each value for climbing
156
+ * out of its own segment. The substituted result is not re-checked: by then
157
+ * every value is encoded, and reading an operator's own `%` or `/` back as an
158
+ * attempted escape would refuse data the app accepts.
159
+ */
160
+ declare function buildCommandPath(command: SurfaceCommandDescriptor, params: Record<string, string>, query: Record<string, string>, namespace: string): string;
161
+
162
+ /**
163
+ * Terminal-safe rendering of app-supplied text
164
+ *
165
+ * Its own module because both the manifest client and the usage renderer need
166
+ * it, and the client must not import the renderer to get it.
167
+ */
168
+ /**
169
+ * Strip what a terminal would act on rather than show.
170
+ *
171
+ * Every string rendered here — a field's description, its pattern, a command's
172
+ * name — is the app's, and it is written straight to the operator's terminal.
173
+ * Escape sequences left in it can clear the screen or redraw the lines above,
174
+ * so what the operator reads is no longer what the CLI wrote. Control
175
+ * characters are replaced rather than dropped, so text that contained them
176
+ * still reads as suspicious instead of quietly shrinking.
177
+ */
178
+ declare function plain(value: string): string;
179
+
180
+ /**
181
+ * Render a surface command's input schemas as usage text.
182
+ *
183
+ * The manifest carries each command's TypeBox schemas as plain JSON Schema,
184
+ * so the terminal can tell an operator what a command takes without the app's
185
+ * source. This turns that JSON into the shape a `--help` reader expects:
186
+ * one line per field, with its type, whether it is required, and whatever
187
+ * constraints the schema states.
188
+ *
189
+ * The server remains the authority on what is valid — this only reports what
190
+ * the server said.
191
+ */
192
+
193
+ /**
194
+ * The whole usage block for one command: its method and path, then a section
195
+ * per input kind. A command taking no input says so rather than printing
196
+ * nothing.
197
+ *
198
+ * `invoke` is the command line that calls it — `spfn ops call` for the ops
199
+ * CLI — and adds an example invocation line; a library caller has none.
200
+ */
201
+ declare function renderCommandUsage(command: SurfaceCommandDescriptor, invoke?: string): string;
202
+
203
+ /**
204
+ * The transport rule every surface request obeys: https, or http against the
205
+ * machine itself.
206
+ *
207
+ * Every request a surface client makes carries a credential — an ops token, a
208
+ * signed token, an administrator's password on `spfn ops token issue`. A stale
209
+ * app URL left over from a local run, or a typo, is enough to put it on the
210
+ * wire unencrypted, and nothing about the answer would say so. The client
211
+ * refuses such a URL when it is created; `spfn ops` asks the same question
212
+ * first so it can render the refusal its own way.
213
+ */
214
+ /** Why an app URL may not carry a credential, or null when it may. */
215
+ type TransportRefusal = 'cleartext-http' | 'not-http';
216
+ /**
217
+ * Where `http` is not a mistake: a machine talking to itself, which has no
218
+ * certificate to present and no network for anyone to listen on.
219
+ *
220
+ * The scaffold's own API runs on `http://localhost:8790`, so local development
221
+ * has to keep working — but that is the whole of the exception.
222
+ */
223
+ declare function isLoopback(hostname: string): boolean;
224
+ declare function transportRefusalOf(url: URL): TransportRefusal | null;
225
+ /** Throw unless `appUrl` is an absolute https URL, or http against loopback. */
226
+ declare function assertTransportSafe(appUrl: string): void;
227
+
228
+ export { type SurfaceCallInput, type SurfaceClient, type SurfaceClientOptions, type SurfaceCommandDescriptor, type SurfaceEffect, type SurfaceManifest, SurfaceManifestError, type SurfaceModuleDescriptor, type SurfaceResponse, type SurfaceWarning, type SurfaceWarningSink, type TransportRefusal, assertTransportSafe, buildCommandPath, createSurfaceClient, isLoopback, joinUrl, plain, renderCommandUsage, transportRefusalOf };