@picked/app-kit 0.0.0-stage → 0.0.117-next.2
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 +1 -0
- package/README.md +186 -2
- package/bin/copy-frame.mjs +23 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cjs/frame.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/node.js +1 -0
- package/dist/cjs/oauth.js +56 -0
- package/dist/cjs/server.js +1 -0
- package/dist/cjs/testing.js +1 -0
- package/dist/esm/frame.js +132 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/node.js +40 -0
- package/dist/esm/oauth.js +202 -0
- package/dist/esm/server.js +279 -0
- package/dist/esm/testing.js +197 -0
- package/dist/types/client.d.ts +145 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/frame.d.ts +124 -0
- package/dist/types/frame.d.ts.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/link.d.ts +41 -0
- package/dist/types/link.d.ts.map +1 -0
- package/dist/types/node.d.ts +19 -0
- package/dist/types/node.d.ts.map +1 -0
- package/dist/types/oauth.d.ts +128 -0
- package/dist/types/oauth.d.ts.map +1 -0
- package/dist/types/server.d.ts +167 -0
- package/dist/types/server.d.ts.map +1 -0
- package/dist/types/testing.d.ts +84 -0
- package/dist/types/testing.d.ts.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The handshake between an app's iframe and the dashboard hosting it.
|
|
3
|
+
*
|
|
4
|
+
* The frame posts `picked:ready`; the dashboard answers `picked:context` with where the
|
|
5
|
+
* frame renders and a session token the frame forwards to its own server. The frame can
|
|
6
|
+
* post `picked:resize` to set its height. Each `picked:ready` gets a context with a new session
|
|
7
|
+
* token, which `keepConnected` uses to renew the token before it expires.
|
|
8
|
+
*/
|
|
9
|
+
import type { AppSlotLocation } from '@picked/app-protocol';
|
|
10
|
+
/** What the dashboard tells a frame. */
|
|
11
|
+
export interface FrameContext {
|
|
12
|
+
installationId: string;
|
|
13
|
+
organizationId: string;
|
|
14
|
+
organizationSlug: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
/** The slot the frame renders in. */
|
|
17
|
+
location: AppSlotLocation;
|
|
18
|
+
/** The installation's pinned app version. */
|
|
19
|
+
appVersion: number;
|
|
20
|
+
/** A short-lived token asserting the user and installation; verify it with `readSession`. */
|
|
21
|
+
sessionToken: string;
|
|
22
|
+
/** The dashboard's colour scheme. */
|
|
23
|
+
theme: 'light' | 'dark';
|
|
24
|
+
}
|
|
25
|
+
/** A message the frame posts to the dashboard. */
|
|
26
|
+
export type FrameToHostMessage = {
|
|
27
|
+
type: 'picked:ready';
|
|
28
|
+
} | {
|
|
29
|
+
type: 'picked:resize';
|
|
30
|
+
height: number;
|
|
31
|
+
};
|
|
32
|
+
/** A message the dashboard posts to the frame. */
|
|
33
|
+
export interface HostToFrameMessage {
|
|
34
|
+
type: 'picked:context';
|
|
35
|
+
context: FrameContext;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Connects to the dashboard and resolves with the frame's context. Call once on load. It
|
|
39
|
+
* works only in a page the Picked dashboard frames: it rejects in a top-level window, and
|
|
40
|
+
* accepts the context only from the frame's parent window on the dashboard's origin.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} [options] - Connection options.
|
|
43
|
+
* @param {string} [options.dashboardOrigin] - The dashboard origin to accept and post to; the referrer's when absent.
|
|
44
|
+
* @returns {Promise<FrameContext>} Where the frame renders and who is looking.
|
|
45
|
+
* @example const context = await connect()
|
|
46
|
+
*/
|
|
47
|
+
export declare function connect(options?: {
|
|
48
|
+
dashboardOrigin?: string;
|
|
49
|
+
}): Promise<FrameContext>;
|
|
50
|
+
/** A connection to the dashboard that renews its session token before the token expires. */
|
|
51
|
+
export interface FrameConnection {
|
|
52
|
+
/**
|
|
53
|
+
* Returns the newest context the dashboard sent. Read it for each request and each
|
|
54
|
+
* `authorizeInPopup`, so they carry a session token that has not expired.
|
|
55
|
+
*/
|
|
56
|
+
context: () => FrameContext;
|
|
57
|
+
/** Stops renewing the session token. */
|
|
58
|
+
stop: () => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Connects to the dashboard as `connect` does, then asks the dashboard for a new context two
|
|
62
|
+
* minutes before each session token expires and at most four minutes after the last one. It
|
|
63
|
+
* checks the clock every 15 seconds and when the frame regains focus or visibility, and asks
|
|
64
|
+
* again 15 seconds after a renewal the dashboard leaves unanswered for 10 seconds. Call once
|
|
65
|
+
* on load, in place of `connect`.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} [options] - Connection options.
|
|
68
|
+
* @param {string} [options.dashboardOrigin] - The dashboard origin to accept and post to; the referrer's when absent.
|
|
69
|
+
* @returns {Promise<FrameConnection>} The connection, once the dashboard sends the first context.
|
|
70
|
+
* @example const connection = await keepConnected()
|
|
71
|
+
*/
|
|
72
|
+
export declare function keepConnected(options?: {
|
|
73
|
+
dashboardOrigin?: string;
|
|
74
|
+
}): Promise<FrameConnection>;
|
|
75
|
+
/**
|
|
76
|
+
* Sets the frame's height in the dashboard.
|
|
77
|
+
*
|
|
78
|
+
* @param {number} height - The height in pixels.
|
|
79
|
+
* @returns {void}
|
|
80
|
+
* @example resize(document.body.scrollHeight)
|
|
81
|
+
*/
|
|
82
|
+
export declare function resize(height: number): void;
|
|
83
|
+
/**
|
|
84
|
+
* Keeps the frame's height matched to its content.
|
|
85
|
+
*
|
|
86
|
+
* @returns {() => void} Stops observing.
|
|
87
|
+
* @example const stop = autoResize()
|
|
88
|
+
*/
|
|
89
|
+
export declare function autoResize(): () => void;
|
|
90
|
+
/** How an authorization the frame opened in a popup ended. */
|
|
91
|
+
export interface AuthorizationResult {
|
|
92
|
+
ok: boolean;
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
/** What the frame starts an authorization with. */
|
|
96
|
+
export interface AuthorizationRequest {
|
|
97
|
+
/** The session token from the frame's context, which the popup's start request carries. */
|
|
98
|
+
sessionToken: string;
|
|
99
|
+
/** What the app's `/oauth/start` hands its `authorize` as `params`, such as a data centre. */
|
|
100
|
+
params?: unknown;
|
|
101
|
+
/**
|
|
102
|
+
* Reads whether the app now holds the connection, answering the message to show when it
|
|
103
|
+
* does and null when it does not. Once the popup reads as closed without reporting back,
|
|
104
|
+
* the frame asks it every second for thirty seconds and settles on the first message.
|
|
105
|
+
*/
|
|
106
|
+
connected?: () => Promise<string | null>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Runs an OAuth authorization in a popup. Call it from a click handler: the popup opens at
|
|
110
|
+
* once on the app's `/oauth/start`, which asks for the session, and this frame answers that
|
|
111
|
+
* popup alone with the session token and params. It accepts messages only from that popup, on
|
|
112
|
+
* the app's own origin. The start binds the authorization to the browser with a cookie and
|
|
113
|
+
* goes on to the provider. The promise settles when the app's
|
|
114
|
+
* callback page reports back, or once the popup reads as closed and this frame has focus. A
|
|
115
|
+
* popup a provider's `Cross-Origin-Opener-Policy` separates from this frame reads as closed
|
|
116
|
+
* while the user may still be connecting in it, so the frame waits for the user to come back,
|
|
117
|
+
* then asks `connected` until the app holds the connection the popup saves.
|
|
118
|
+
*
|
|
119
|
+
* @param {AuthorizationRequest} request - The session token and the start's params.
|
|
120
|
+
* @returns {Promise<AuthorizationResult>} How the authorization ended.
|
|
121
|
+
* @example button.onclick = async () => show(await authorizeInPopup({ sessionToken: context.sessionToken }))
|
|
122
|
+
*/
|
|
123
|
+
export declare function authorizeInPopup(request: AuthorizationRequest): Promise<AuthorizationResult>;
|
|
124
|
+
//# sourceMappingURL=frame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../../src/frame.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,qCAAqC;IACrC,QAAQ,EAAE,eAAe,CAAA;IACzB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,6FAA6F;IAC7F,YAAY,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;CACxB;AAED,kDAAkD;AAClD,MAAM,MAAM,kBAAkB,GAC5B;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtE,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,EAAE,YAAY,CAAA;CACtB;AAqFD;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAKzF;AAsED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,EAAE,MAAM,YAAY,CAAA;IAC3B,wCAAwC;IACxC,IAAI,EAAE,MAAM,IAAI,CAAA;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAA;CAAO,GACzC,OAAO,CAAC,eAAe,CAAC,CAoC1B;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,IAAI,CAKvC;AAED,8DAA8D;AAC9D,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,mDAAmD;AACnD,MAAM,WAAW,oBAAoB;IACnC,2FAA2F;IAC3F,YAAY,EAAE,MAAM,CAAA;IACpB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACzC;AAoCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6D5F"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `@picked/app-kit`: the app manifest and the protocol Picked speaks to apps, re-exported
|
|
3
|
+
* from `@picked/app-protocol`.
|
|
4
|
+
*
|
|
5
|
+
* `@picked/app-kit/server` builds an app's endpoint, `@picked/app-kit/node` serves it, and
|
|
6
|
+
* `@picked/app-kit/frame` runs inside a dashboard slot's iframe.
|
|
7
|
+
*/
|
|
8
|
+
export * from '@picked/app-protocol';
|
|
9
|
+
export type { FrameContext, FrameToHostMessage, HostToFrameMessage } from './frame';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,sBAAsB,CAAA;AACpC,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The dashboard link that opens the installation a label names, for an app to send a
|
|
3
|
+
* person from its own site back to the right project in Picked.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Tells whether a value has the shape of an app ID.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} value - The value.
|
|
9
|
+
* @returns {boolean} Whether it is `app_` and lowercase letters and digits.
|
|
10
|
+
* @example isAppId('app_k3v9x2m1q8zt') // true
|
|
11
|
+
*/
|
|
12
|
+
export declare function isAppId(value: string): boolean;
|
|
13
|
+
/** What a link to an installation names. */
|
|
14
|
+
export interface InstallationLink {
|
|
15
|
+
/** The dashboard's origin, such as `https://app.picked.app`. A path on it is ignored. */
|
|
16
|
+
dashboardOrigin: string;
|
|
17
|
+
/**
|
|
18
|
+
* The app's ID on that dashboard's Picked, such as `app_k3v9x2m1q8zt`, which
|
|
19
|
+
* `picked app deploy` prints. Each Picked environment gives the app its own ID.
|
|
20
|
+
*/
|
|
21
|
+
appId: string;
|
|
22
|
+
/** The namespace of a plain label the app keeps on the installation. */
|
|
23
|
+
namespace: string;
|
|
24
|
+
/** The label's key within its namespace. */
|
|
25
|
+
key: string;
|
|
26
|
+
/** The value the installation's label holds. */
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds the dashboard URL that opens the installation of an app holding a plain label with a
|
|
31
|
+
* value: `/apps/<app ID>/open?label=<namespace>.<key>&value=<value>`. The dashboard opens the
|
|
32
|
+
* one installation the signed-in person can see, or offers a choice of several. A secret label
|
|
33
|
+
* never matches.
|
|
34
|
+
*
|
|
35
|
+
* @param {InstallationLink} link - The dashboard, app, label and value.
|
|
36
|
+
* @returns {string} The URL.
|
|
37
|
+
* @throws {TypeError} When the app ID, namespace or key is not one Picked holds, or the origin is not a URL.
|
|
38
|
+
* @example installationLinkUrl({ dashboardOrigin: 'https://app.picked.app', appId: 'app_k3v9x2m1q8zt', namespace: 'vercel', key: 'configuration-id', value: 'icfg_123' })
|
|
39
|
+
*/
|
|
40
|
+
export declare function installationLinkUrl(link: InstallationLink): string;
|
|
41
|
+
//# sourceMappingURL=link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/link.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C;AAKD,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,eAAe,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAUlE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Serves a handler on a port, logging each request's method, path and status.
|
|
4
|
+
*
|
|
5
|
+
* @param {(request: Request) => Promise<Response>} handler - The handler.
|
|
6
|
+
* @param {number} port - The port.
|
|
7
|
+
* @returns {Server} The listening server.
|
|
8
|
+
* @example serve(createAppHandler(options), 4100)
|
|
9
|
+
*/
|
|
10
|
+
export declare function serve(handler: (request: Request) => Promise<Response>, port: number): Server;
|
|
11
|
+
/**
|
|
12
|
+
* Answers with the browser build of `@picked/app-kit/frame`, for an app's pages to import as
|
|
13
|
+
* `/app-kit/frame.js`.
|
|
14
|
+
*
|
|
15
|
+
* @returns {Promise<Response>} The script.
|
|
16
|
+
* @example if (path === '/app-kit/frame.js') return frameScript()
|
|
17
|
+
*/
|
|
18
|
+
export declare function frameScript(): Promise<Response>;
|
|
19
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/node.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAmB,MAAM,EAAkB,MAAM,WAAW,CAAA;AA0DxE;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAgB5F;AAGD;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,QAAQ,CAAC,CAGrD"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OAuth authorization-code helpers for apps that connect an installation to a third
|
|
3
|
+
* party. The popup starts at the app's `/oauth/start`, which takes the dashboard session from
|
|
4
|
+
* the frame that opened it, sets a cookie holding a nonce, and sends the browser to the
|
|
5
|
+
* provider with a sealed `state` carrying the installation and that nonce. The provider's
|
|
6
|
+
* callback completes only in the browser whose cookie holds the state's nonce, and its page
|
|
7
|
+
* tells the frame how it went.
|
|
8
|
+
*/
|
|
9
|
+
import { type PublicKeySet, type SessionClaims } from '@picked/app-protocol';
|
|
10
|
+
/** The message the callback page posts to the frame that opened it. */
|
|
11
|
+
export declare const OAUTH_RESULT_MESSAGE = "picked-app:oauth-result";
|
|
12
|
+
/** The message the start page posts to its opener once it can take the session. */
|
|
13
|
+
export declare const OAUTH_START_READY_MESSAGE = "picked-app:oauth-start-ready";
|
|
14
|
+
/** The message the opener answers the start page with: the session token and the start's params. */
|
|
15
|
+
export declare const OAUTH_START_MESSAGE = "picked-app:oauth-start";
|
|
16
|
+
/** The path an app serves the popup's start page (`GET`) and its start request (`POST`) at. */
|
|
17
|
+
export declare const OAUTH_START_PATH = "/oauth/start";
|
|
18
|
+
/** What the `state` parameter carries through the provider. */
|
|
19
|
+
export interface OAuthState {
|
|
20
|
+
/** The installation being connected. */
|
|
21
|
+
installationId: string;
|
|
22
|
+
/** The Picked API the installation calls back. */
|
|
23
|
+
apiUrl: string;
|
|
24
|
+
/** Which connection this is, for an app with more than one provider or data centre. */
|
|
25
|
+
connection: string;
|
|
26
|
+
/**
|
|
27
|
+
* The session token of the dashboard user who started the connection, with which the
|
|
28
|
+
* callback mints a token to save it.
|
|
29
|
+
*/
|
|
30
|
+
sessionToken: string;
|
|
31
|
+
/** Expiry, in unix seconds. */
|
|
32
|
+
exp: number;
|
|
33
|
+
/** The value of the cookie the start set in the browser that began the authorization. */
|
|
34
|
+
nonce: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders the page a provider's callback answers with. It posts the result to the frame that
|
|
38
|
+
* opened the popup, on the app's own origin, and closes. A popup whose opener is gone, as a
|
|
39
|
+
* provider's `Cross-Origin-Opener-Policy` can leave it, stays open showing the result.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} result - How the authorization went.
|
|
42
|
+
* @param {boolean} result.ok - Whether the connection was saved.
|
|
43
|
+
* @param {string} result.message - What to tell the user.
|
|
44
|
+
* @returns {string} The page's HTML.
|
|
45
|
+
* @example oauthCallbackPage({ ok: true, message: 'Connected' })
|
|
46
|
+
*/
|
|
47
|
+
export declare function oauthCallbackPage(result: {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
message: string;
|
|
50
|
+
}): string;
|
|
51
|
+
/**
|
|
52
|
+
* Renders the page the popup opens at. It tells the frame that opened it that it is ready,
|
|
53
|
+
* takes the session token and params that frame answers with, from that frame alone, and
|
|
54
|
+
* posts them to its own path. That answer sets the binding cookie and names the provider's
|
|
55
|
+
* URL, which the page goes to; a refusal goes to the frame as the result.
|
|
56
|
+
*
|
|
57
|
+
* @returns {string} The page's HTML.
|
|
58
|
+
* @example oauthStartPage()
|
|
59
|
+
*/
|
|
60
|
+
export declare function oauthStartPage(): string;
|
|
61
|
+
/** An authorization the popup asked to start. */
|
|
62
|
+
export interface AuthorizationStart {
|
|
63
|
+
/** The verified session of the dashboard user starting it. */
|
|
64
|
+
session: SessionClaims;
|
|
65
|
+
/** The raw session token, which an installation client passes on as `sessionToken`. */
|
|
66
|
+
sessionToken: string;
|
|
67
|
+
/** What the app's page passed `authorizeInPopup` as `params`, or null. */
|
|
68
|
+
params: unknown;
|
|
69
|
+
/**
|
|
70
|
+
* Seals the `state` for a connection of the session's installation, carrying the session
|
|
71
|
+
* token and the nonce of the cookie this start sets.
|
|
72
|
+
*/
|
|
73
|
+
state: (connection: string) => Promise<string>;
|
|
74
|
+
}
|
|
75
|
+
/** What `startAuthorization` is given. */
|
|
76
|
+
export interface StartAuthorizationOptions {
|
|
77
|
+
/** The app's private key, from which the state's key derives. */
|
|
78
|
+
privateKey: string;
|
|
79
|
+
/** Picked's public keys, which sign session tokens. */
|
|
80
|
+
pickedKeys: PublicKeySet;
|
|
81
|
+
/**
|
|
82
|
+
* Builds the provider's authorization URL with a `state` from `start.state`, or answers a
|
|
83
|
+
* refusal, which goes back to the frame and sets no cookie.
|
|
84
|
+
*/
|
|
85
|
+
authorize: (start: AuthorizationStart) => Promise<string | Response>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Answers the app's `/oauth/start`. `GET` serves the popup's page. `POST`, which that page
|
|
89
|
+
* sends with the session token as `Authorization: Bearer <token>` and the params as its JSON
|
|
90
|
+
* body, verifies the session, sets the binding cookie to a fresh nonce, and answers
|
|
91
|
+
* `{ url }`: the provider's URL `authorize` builds.
|
|
92
|
+
*
|
|
93
|
+
* @param {Request} request - The request to `/oauth/start`.
|
|
94
|
+
* @param {StartAuthorizationOptions} options - The app's keys and its provider's URL.
|
|
95
|
+
* @returns {Promise<Response>} The page, the provider's URL, or a refusal.
|
|
96
|
+
* @example if (pathname === OAUTH_START_PATH) return startAuthorization(request, { privateKey, pickedKeys, authorize })
|
|
97
|
+
*/
|
|
98
|
+
export declare function startAuthorization(request: Request, options: StartAuthorizationOptions): Promise<Response>;
|
|
99
|
+
/** A provider's callback in the browser that started the authorization. */
|
|
100
|
+
export interface OAuthCallback {
|
|
101
|
+
/** The state the start sealed. */
|
|
102
|
+
state: OAuthState;
|
|
103
|
+
/** The callback URL, carrying the provider's `code` or `error`. */
|
|
104
|
+
url: URL;
|
|
105
|
+
}
|
|
106
|
+
/** What `finishAuthorization` is given. */
|
|
107
|
+
export interface FinishAuthorizationOptions {
|
|
108
|
+
/** The app's private key, from which the state's key derives. */
|
|
109
|
+
privateKey: string;
|
|
110
|
+
/** Exchanges the code and saves the connection, answering what the popup reports. */
|
|
111
|
+
complete: (callback: OAuthCallback) => Promise<{
|
|
112
|
+
ok: boolean;
|
|
113
|
+
message: string;
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Answers a provider's callback. It refuses a forged, changed or expired state, and a state
|
|
118
|
+
* whose nonce differs from the binding cookie the request carries, leaving that cookie in
|
|
119
|
+
* place. Otherwise it clears the cookie, so the callback completes once, and answers the page
|
|
120
|
+
* reporting what `complete` returns.
|
|
121
|
+
*
|
|
122
|
+
* @param {Request} request - The callback request.
|
|
123
|
+
* @param {FinishAuthorizationOptions} options - The app's key and its completion.
|
|
124
|
+
* @returns {Promise<Response>} The callback page.
|
|
125
|
+
* @example if (pathname === '/oauth/callback') return finishAuthorization(request, { privateKey, complete })
|
|
126
|
+
*/
|
|
127
|
+
export declare function finishAuthorization(request: Request, options: FinishAuthorizationOptions): Promise<Response>;
|
|
128
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAKL,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAA;AAK7B,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,4BAA4B,CAAA;AAE7D,mFAAmF;AACnF,eAAO,MAAM,yBAAyB,iCAAiC,CAAA;AAEvE,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,2BAA2B,CAAA;AAE3D,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,iBAAiB,CAAA;AAE9C,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,cAAc,EAAE,MAAM,CAAA;IACtB,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAA;IACd,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;CACd;AAwHD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAkBlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,MAAM,CA0CvC;AA4GD,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,OAAO,EAAE,aAAa,CAAA;IACtB,uFAAuF;IACvF,YAAY,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,MAAM,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/C;AAED,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACxC,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,UAAU,EAAE,YAAY,CAAA;IACxB;;;OAGG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAA;CACrE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAiCnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,KAAK,EAAE,UAAU,CAAA;IACjB,mEAAmE;IACnE,GAAG,EAAE,GAAG,CAAA;CACT;AAED,2CAA2C;AAC3C,MAAM,WAAW,0BAA0B;IACzC,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,qFAAqF;IACrF,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,QAAQ,CAAC,CAqBnB"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The app server: a fetch-style handler that checks Picked's signature and routes
|
|
3
|
+
* lifecycle events, app events, tool calls and MCP requests, plus the responses its pages
|
|
4
|
+
* and APIs answer with.
|
|
5
|
+
*/
|
|
6
|
+
import { type AppEvent, type AppManifest, type InstallationCreatedEvent, type InstallationDeletedEvent, type InstallationRef, type InstallationUpgradedEvent, type PublicKeySet, type SessionClaims, type ToolCall } from '@picked/app-protocol';
|
|
7
|
+
export * from '@picked/app-protocol';
|
|
8
|
+
export * from './client';
|
|
9
|
+
export * from './link';
|
|
10
|
+
/** A tool's body: given the call, returns what the manifest's `output` declares. */
|
|
11
|
+
export type ToolHandler = (call: ToolCall) => Promise<Record<string, unknown>>;
|
|
12
|
+
/** What an app server handles. */
|
|
13
|
+
export interface AppHandlerOptions {
|
|
14
|
+
/** Picked's public keys, which every request Picked sends must be signed by. */
|
|
15
|
+
pickedKeys: PublicKeySet;
|
|
16
|
+
/** The path the manifest's `endpoint` points at, such as `/picked`. Empty by default. */
|
|
17
|
+
basePath?: string;
|
|
18
|
+
/** Called when the app is installed on a project. */
|
|
19
|
+
onInstall?: (event: InstallationCreatedEvent) => Promise<void> | void;
|
|
20
|
+
/** Called when an installation moves to another version. */
|
|
21
|
+
onUpgrade?: (event: InstallationUpgradedEvent) => Promise<void> | void;
|
|
22
|
+
/** Called when the app is removed from a project. */
|
|
23
|
+
onUninstall?: (event: InstallationDeletedEvent) => Promise<void> | void;
|
|
24
|
+
/** Called with each event the manifest's `events` subscribes to. */
|
|
25
|
+
onEvent?: (event: AppEvent) => Promise<void> | void;
|
|
26
|
+
/** The tool bodies, by the names the manifest declares. */
|
|
27
|
+
tools?: Record<string, ToolHandler>;
|
|
28
|
+
/** The MCP server at the manifest's `mcp.url`, and the handler for the signed requests to it. */
|
|
29
|
+
mcp?: {
|
|
30
|
+
/** The pathname of the manifest's `mcp.url`, such as `/mcp`, matched against the full path. */
|
|
31
|
+
path: string;
|
|
32
|
+
handle: (request: Request, installation: InstallationRef) => Promise<Response>;
|
|
33
|
+
};
|
|
34
|
+
/** The app's manifest, which the handler serves at `GET /manifest.json`. */
|
|
35
|
+
manifest?: AppManifest;
|
|
36
|
+
/** Everything else the app serves, such as its frame pages. */
|
|
37
|
+
fallback?: (request: Request) => Promise<Response> | Response;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Answers JSON.
|
|
41
|
+
*
|
|
42
|
+
* @param {unknown} body - The body.
|
|
43
|
+
* @param {number} [status] - The status; 200 by default.
|
|
44
|
+
* @returns {Response} The response.
|
|
45
|
+
* @example json({ ok: true })
|
|
46
|
+
*/
|
|
47
|
+
export declare function json(body: unknown, status?: number): Response;
|
|
48
|
+
/**
|
|
49
|
+
* Answers an HTML page the dashboard may frame and no other site may.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} html - The page.
|
|
52
|
+
* @param {object} options - Who may frame it.
|
|
53
|
+
* @param {string[]} options.frameAncestors - The dashboard origins, such as `https://app.picked.app`.
|
|
54
|
+
* @returns {Response} The response.
|
|
55
|
+
* @example page(settingsPage(), { frameAncestors: ['https://app.picked.app'] })
|
|
56
|
+
*/
|
|
57
|
+
export declare function page(html: string, options: {
|
|
58
|
+
frameAncestors: string[];
|
|
59
|
+
}): Response;
|
|
60
|
+
/**
|
|
61
|
+
* Builds the handler for an app's endpoint.
|
|
62
|
+
*
|
|
63
|
+
* @param {AppHandlerOptions} options - What the app handles.
|
|
64
|
+
* @returns {(request: Request) => Promise<Response>} A fetch-style handler.
|
|
65
|
+
* @example serve(createAppHandler({ pickedKeys: environment.pickedKeys, tools: { ping: async () => ({ ok: true }) } }))
|
|
66
|
+
*/
|
|
67
|
+
export declare function createAppHandler(options: AppHandlerOptions): (request: Request) => Promise<Response>;
|
|
68
|
+
/**
|
|
69
|
+
* Reads the raw session token a frame sends as `Authorization: Bearer <token>`, which an
|
|
70
|
+
* installation client passes on as `sessionToken` to mint a token for that user's request.
|
|
71
|
+
*
|
|
72
|
+
* @param {Request} request - The frame's request to the app's own server.
|
|
73
|
+
* @returns {string | null} The token, or null when the request carries none.
|
|
74
|
+
* @example createInstallationClient({ ...options, sessionToken: sessionTokenOf(request) ?? undefined })
|
|
75
|
+
*/
|
|
76
|
+
export declare function sessionTokenOf(request: Request): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Reads the session token a frame sends as `Authorization: Bearer <token>`.
|
|
79
|
+
*
|
|
80
|
+
* @param {Request} request - The frame's request to the app's own server.
|
|
81
|
+
* @param {PublicKeySet} pickedKeys - Picked's public keys, which sign session tokens.
|
|
82
|
+
* @returns {Promise<SessionClaims | null>} Who is asking, or null without a valid token.
|
|
83
|
+
* @example const session = await readSession(request, environment.pickedKeys)
|
|
84
|
+
*/
|
|
85
|
+
export declare function readSession(request: Request, pickedKeys: PublicKeySet): Promise<SessionClaims | null>;
|
|
86
|
+
/** What every app server reads from its environment. */
|
|
87
|
+
export interface AppEnvironment {
|
|
88
|
+
/** `PICKED_APP_PRIVATE_KEY`: the app's Ed25519 private key, as the base64 of its PKCS#8 DER or a PKCS#8 PEM. */
|
|
89
|
+
privateKey: string;
|
|
90
|
+
/** `PICKED_API_URL`: the Picked API the app trusts, whose public keys verify Picked's requests. */
|
|
91
|
+
pickedUrl: string;
|
|
92
|
+
/** Picked's public keys, read from `pickedUrl`. */
|
|
93
|
+
pickedKeys: PublicKeySet;
|
|
94
|
+
/** `APP_PUBLIC_URL`: the origin the app is served at, which its manifest's endpoint names. */
|
|
95
|
+
publicUrl: string;
|
|
96
|
+
/**
|
|
97
|
+
* The dashboard origins the environment names: `PICKED_DASHBOARD_ORIGINS`, or `DASHBOARD_PORT`
|
|
98
|
+
* on localhost; empty when it names none.
|
|
99
|
+
*/
|
|
100
|
+
frameAncestors: string[];
|
|
101
|
+
/**
|
|
102
|
+
* The dashboard origins that may frame the app's pages, the dashboard's own first, which an
|
|
103
|
+
* app links people to as `installationLinkUrl`'s `dashboardOrigin`: `frameAncestors`, or the
|
|
104
|
+
* ones Picked's app config at `pickedUrl` lists when the environment names none.
|
|
105
|
+
*/
|
|
106
|
+
dashboardOrigins: () => Promise<string[]>;
|
|
107
|
+
/**
|
|
108
|
+
* `PICKED_APP_ID`: the app's ID on the Picked it trusts, which `picked app deploy` prints, or
|
|
109
|
+
* null when it is unset or not an app ID.
|
|
110
|
+
*/
|
|
111
|
+
appId: string | null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The dashboard origins Picked's app config lists. It fetches the config on first use, on every
|
|
115
|
+
* use until a fetch succeeds, and again on a use once `refreshAfterMs` has passed since the last
|
|
116
|
+
* success. A fetch that takes longer than `timeoutMs` is abandoned. It keeps the origins it last
|
|
117
|
+
* read when a fetch fails, and holds none before any fetch succeeds.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} options - Where the config lives.
|
|
120
|
+
* @param {string} options.url - The config's URL.
|
|
121
|
+
* @param {typeof fetch} [options.fetch] - The fetch to read it with.
|
|
122
|
+
* @param {number} [options.refreshAfterMs] - The least time between fetches; five minutes by default.
|
|
123
|
+
* @param {number} [options.timeoutMs] - The longest a fetch may take; five seconds by default.
|
|
124
|
+
* @returns {() => Promise<string[]>} Reads the origins.
|
|
125
|
+
* @example remoteDashboardOrigins({ url: 'https://api.picked.app/.well-known/picked-app-config.json' })
|
|
126
|
+
*/
|
|
127
|
+
export declare function remoteDashboardOrigins(options: {
|
|
128
|
+
url: string;
|
|
129
|
+
fetch?: typeof fetch;
|
|
130
|
+
refreshAfterMs?: number;
|
|
131
|
+
timeoutMs?: number;
|
|
132
|
+
}): () => Promise<string[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Reads an app server's environment. An origin the environment leaves unset comes from
|
|
135
|
+
* Vercel when the app runs there: Picked's API from the related project `picked-api`, and the
|
|
136
|
+
* public URL from `VERCEL_PROJECT_PRODUCTION_URL` in production and `VERCEL_BRANCH_URL`
|
|
137
|
+
* elsewhere, so a preview targets the API deployed from its own branch. Locally, Picked's API
|
|
138
|
+
* defaults to `API_PORT` on localhost, the public URL to the app's port and the frame
|
|
139
|
+
* ancestors to the dashboard's port. The dashboard origins are the frame ancestors, or, when the
|
|
140
|
+
* environment names none, the ones in the app config Picked's API publishes: the dashboard's
|
|
141
|
+
* own origin first, then every tenant host.
|
|
142
|
+
*
|
|
143
|
+
* @param {Record<string, string | undefined>} env - The environment.
|
|
144
|
+
* @param {string} port - The port the app listens on locally.
|
|
145
|
+
* @param {object} [options] - How Picked's documents are read.
|
|
146
|
+
* @param {typeof fetch} [options.fetch] - The fetch Picked's keys and app config are read with.
|
|
147
|
+
* @param {number} [options.refreshAfterMs] - The least time between reads of Picked's app config.
|
|
148
|
+
* @param {number} [options.timeoutMs] - The longest a read of Picked's app config may take.
|
|
149
|
+
* @returns {AppEnvironment} The environment.
|
|
150
|
+
* @throws {Error} When `PICKED_APP_PRIVATE_KEY` or `PICKED_API_URL` is unset.
|
|
151
|
+
* @example readAppEnvironment(process.env, process.env.APP_PORT ?? '4101')
|
|
152
|
+
*/
|
|
153
|
+
export declare function readAppEnvironment(env: Record<string, string | undefined>, port: string, options?: {
|
|
154
|
+
fetch?: typeof fetch;
|
|
155
|
+
refreshAfterMs?: number;
|
|
156
|
+
timeoutMs?: number;
|
|
157
|
+
}): AppEnvironment;
|
|
158
|
+
/**
|
|
159
|
+
* Serves a handler behind a Vercel rewrite of every path to `/api?__path=/:path*`, restoring
|
|
160
|
+
* the path the request was sent to.
|
|
161
|
+
*
|
|
162
|
+
* @param {(request: Request) => Promise<Response>} handler - The app's handler.
|
|
163
|
+
* @returns {(request: Request) => Promise<Response>} The handler for `api/index.ts`.
|
|
164
|
+
* @example export default { fetch: withRewrittenPath(handler) }
|
|
165
|
+
*/
|
|
166
|
+
export declare function withRewrittenPath(handler: (request: Request) => Promise<Response>): (request: Request) => Promise<Response>;
|
|
167
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Test doubles for app servers: a fake Picked API that answers the GraphQL operations the
|
|
3
|
+
* installation client sends, mints tokens for app assertions signed with the app's key (for an
|
|
4
|
+
* installation without offline access, only alongside a session token it signed), keeps
|
|
5
|
+
* installation labels and publishes Picked's public keys; and helpers that build the requests
|
|
6
|
+
* Picked and the dashboard send an app, signed with the fake's Picked key.
|
|
7
|
+
*/
|
|
8
|
+
import { type InstallationRef, type PublicKeySet, type ToolCall } from '@picked/app-protocol';
|
|
9
|
+
import type { AppLabel } from './client';
|
|
10
|
+
/** A GraphQL operation the fake received. */
|
|
11
|
+
export interface ReceivedOperation {
|
|
12
|
+
operationName: string;
|
|
13
|
+
variables: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** A fake Picked API for one app. */
|
|
16
|
+
export interface FakePicked {
|
|
17
|
+
/** The fake's API origin. */
|
|
18
|
+
apiUrl: string;
|
|
19
|
+
/** The fetch to hand the app as its Picked sender. */
|
|
20
|
+
fetch: typeof fetch;
|
|
21
|
+
/** Picked's public keys, as an app verifies Picked's requests with them. */
|
|
22
|
+
pickedKeys: PublicKeySet;
|
|
23
|
+
/** Each installation's labels, by installation id, then by `<namespace>.<key>`. */
|
|
24
|
+
labels: Map<string, Map<string, AppLabel>>;
|
|
25
|
+
/** Every GraphQL operation received, in order. */
|
|
26
|
+
received: ReceivedOperation[];
|
|
27
|
+
/** Mints a token for an installation, as Picked sends one with each request. */
|
|
28
|
+
tokenFor: (installationId: string) => Promise<{
|
|
29
|
+
token: string;
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Names an installation as a request from Picked does: on the fake's API, with a fresh token,
|
|
34
|
+
* in organisation `org` and project `proj_1` unless the overrides say otherwise.
|
|
35
|
+
*/
|
|
36
|
+
installationRef: (installationId: string, overrides?: Partial<Pick<InstallationRef, 'organizationId' | 'projectId'>>) => Promise<InstallationRef>;
|
|
37
|
+
/** Builds a request Picked sends the app, signed with Picked's key. */
|
|
38
|
+
signedRequest: (url: string, payload: unknown) => Promise<Request>;
|
|
39
|
+
/** Builds an MCP request Picked forwards to the app, signed with Picked's key. */
|
|
40
|
+
mcpRequest: (url: string, ref: InstallationRef, payload: unknown) => Promise<Request>;
|
|
41
|
+
/** Builds a request from a dashboard user viewing an installation's frame. */
|
|
42
|
+
sessionRequest: (params: {
|
|
43
|
+
ref: InstallationRef;
|
|
44
|
+
url: string;
|
|
45
|
+
method?: string;
|
|
46
|
+
body?: unknown;
|
|
47
|
+
}) => Promise<Request>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds a fake Picked API that trusts one app's key.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} options - The app.
|
|
53
|
+
* @param {string} options.appPrivateKey - The app's private key, whose public half the fake trusts.
|
|
54
|
+
* @param {string} [options.apiUrl] - The fake's API origin.
|
|
55
|
+
* @param {boolean} [options.offlineAccess] - Whether every installation holds offline access.
|
|
56
|
+
* @returns {Promise<FakePicked>} The fake.
|
|
57
|
+
* @example const picked = await createFakePicked({ appPrivateKey: privateKey })
|
|
58
|
+
*/
|
|
59
|
+
export declare function createFakePicked(options: {
|
|
60
|
+
appPrivateKey: string;
|
|
61
|
+
apiUrl?: string;
|
|
62
|
+
offlineAccess?: boolean;
|
|
63
|
+
}): Promise<FakePicked>;
|
|
64
|
+
/**
|
|
65
|
+
* Builds a workflow tool call to an installation.
|
|
66
|
+
*
|
|
67
|
+
* @param {InstallationRef} ref - The installation.
|
|
68
|
+
* @param {string} tool - The tool.
|
|
69
|
+
* @param {Record<string, unknown>} args - Its arguments.
|
|
70
|
+
* @param {boolean} [test] - Whether the call belongs to a test run.
|
|
71
|
+
* @returns {ToolCall} The call.
|
|
72
|
+
* @example toolCall(ref, 'create_lead', { lastName: 'Lovelace' })
|
|
73
|
+
*/
|
|
74
|
+
export declare function toolCall(ref: InstallationRef, tool: string, args: Record<string, unknown>, test?: boolean): ToolCall;
|
|
75
|
+
/**
|
|
76
|
+
* Reads the cookie an OAuth start set, as the `cookie` header the browser sends with the
|
|
77
|
+
* provider's callback.
|
|
78
|
+
*
|
|
79
|
+
* @param {Response} response - The answer to `POST /oauth/start`.
|
|
80
|
+
* @returns {string} The `<name>=<value>` pair, or an empty string when it set none.
|
|
81
|
+
* @example new Request(callbackUrl, { headers: { cookie: oauthCookie(started) } })
|
|
82
|
+
*/
|
|
83
|
+
export declare function oauthCookie(response: Response): string;
|
|
84
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAaL,KAAK,eAAe,EACpB,KAAK,YAAY,EAEjB,KAAK,QAAQ,EACd,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,UAAU,CAAA;AAErE,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,4EAA4E;IAC5E,UAAU,EAAE,YAAY,CAAA;IACxB,mFAAmF;IACnF,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC1C,kDAAkD;IAClD,QAAQ,EAAE,iBAAiB,EAAE,CAAA;IAC7B,gFAAgF;IAChF,QAAQ,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACnF;;;OAGG;IACH,eAAe,EAAE,CACf,cAAc,EAAE,MAAM,EACtB,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,gBAAgB,GAAG,WAAW,CAAC,CAAC,KACvE,OAAO,CAAC,eAAe,CAAC,CAAA;IAC7B,uEAAuE;IACvE,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAClE,kFAAkF;IAClF,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACrF,8EAA8E;IAC9E,cAAc,EAAE,CAAC,MAAM,EAAE;QACvB,GAAG,EAAE,eAAe,CAAA;QACpB,GAAG,EAAE,MAAM,CAAA;QACX,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACvB;AAuKD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgItB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,UAAQ,GACX,QAAQ,CASV;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEtD"}
|