initiative-plugin-sdk 2.0.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +656 -0
  3. package/dist/build.d.ts +52 -0
  4. package/dist/build.js +229 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/cli.d.ts +24 -0
  7. package/dist/cli.js +160 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/client.d.ts +288 -0
  10. package/dist/client.js +539 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/contract.d.ts +1247 -0
  13. package/dist/contract.js +167 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/define.d.ts +336 -0
  16. package/dist/define.js +120 -0
  17. package/dist/define.js.map +1 -0
  18. package/dist/dev.d.ts +28 -0
  19. package/dist/dev.js +147 -0
  20. package/dist/dev.js.map +1 -0
  21. package/dist/expression.d.ts +28 -0
  22. package/dist/expression.js +67 -0
  23. package/dist/expression.js.map +1 -0
  24. package/dist/init.d.ts +14 -0
  25. package/dist/init.js +52 -0
  26. package/dist/init.js.map +1 -0
  27. package/dist/keys.d.ts +85 -0
  28. package/dist/keys.js +109 -0
  29. package/dist/keys.js.map +1 -0
  30. package/dist/manifest.d.ts +8 -0
  31. package/dist/manifest.js +8 -0
  32. package/dist/manifest.js.map +1 -0
  33. package/dist/pack.d.ts +35 -0
  34. package/dist/pack.js +84 -0
  35. package/dist/pack.js.map +1 -0
  36. package/dist/plugin-api.generated.d.ts +4263 -0
  37. package/dist/plugin-api.generated.js +1019 -0
  38. package/dist/plugin-api.generated.js.map +1 -0
  39. package/dist/server.d.ts +75 -0
  40. package/dist/server.js +408 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/testing.d.ts +116 -0
  43. package/dist/testing.js +386 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/tokens.d.ts +161 -0
  46. package/dist/tokens.js +215 -0
  47. package/dist/tokens.js.map +1 -0
  48. package/dist/validate.d.ts +86 -0
  49. package/dist/validate.js +997 -0
  50. package/dist/validate.js.map +1 -0
  51. package/dist/widget.d.ts +235 -0
  52. package/dist/widget.js +26 -0
  53. package/dist/widget.js.map +1 -0
  54. package/examples/minimal/README.md +48 -0
  55. package/examples/minimal/assets/avatar.png +0 -0
  56. package/examples/minimal/package.json +22 -0
  57. package/examples/minimal/src/plugin.ts +77 -0
  58. package/examples/minimal/src/widgets/open-issues.ts +11 -0
  59. package/examples/minimal/test/fixtures/issues-statistics.json +9 -0
  60. package/examples/minimal/test/issue-counts.test.ts +48 -0
  61. package/examples/minimal/tsconfig.json +14 -0
  62. package/manifest.contract.json +1886 -0
  63. package/package.json +92 -0
  64. package/schemas/plugin-manifest.json +1747 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Initiative, as a plug-in reaches it.
3
+ *
4
+ * A plug-in authenticates with its own key (`private_key_jwt`, RFC 7523) at
5
+ * `POST {baseUrl}/plugin-platform/oauth/token` and acts with one of two tokens:
6
+ *
7
+ * - an **installation token** (`client_credentials` + `installation`), acting as
8
+ * the community that installed the plug-in, with the scopes it granted;
9
+ * - a **member token** (the JWT-bearer grant, RFC 7523 §2.1), acting for one
10
+ * member, within what that member consented to.
11
+ *
12
+ * Either can be narrowed to one initiative the plug-in is placed in (RFC 8707) and
13
+ * to fewer scopes (RFC 6749 §3.3). {@link Initiative.asInstallation} and
14
+ * {@link Initiative.asMember} each give a {@link Client} acting that way; a
15
+ * handler is handed one already acting for its call. {@link Client.api} holds
16
+ * a typed method for every route Initiative's plug-in API describes, generated
17
+ * from that description.
18
+ *
19
+ * Tokens are opaque and never read. The token response says how long each
20
+ * lives and which scopes it holds: a call needing a scope the token does not
21
+ * hold fails before it is sent, naming the scope. Tokens are cached until 30
22
+ * seconds before they expire, concurrent callers share one request, and a
23
+ * call answered 401 is sent once more on a fresh token.
24
+ */
25
+ import { PluginApi } from "./plugin-api.generated.js";
26
+ import type { ActorKind, PluginScope, Scope } from "./contract.js";
27
+ import type { Actor } from "./define.js";
28
+ import { type PluginSigningKey } from "./keys.js";
29
+ export { generatePluginKeys, jwkThumbprint, loadPrivateKey, publicJwks, type PluginKeyAlgorithm, type PluginSigningKey, type GeneratedPluginKeys, type Jwks, type PublicJwk, } from "./keys.js";
30
+ export type { PluginApi, PluginApiSchemas } from "./plugin-api.generated.js";
31
+ export interface InitiativeOptions {
32
+ /** The deployment's API base, such as `https://initiative.example.com/api/v1`. */
33
+ baseUrl: string;
34
+ /** The plug-in's public id: its OAuth client id. */
35
+ publicId: string;
36
+ /** The plug-in's key, as `loadPrivateKey` gives it. */
37
+ key: PluginSigningKey;
38
+ fetch?: typeof fetch;
39
+ /** Milliseconds since the epoch. */
40
+ now?: () => number;
41
+ }
42
+ /** Narrowing for a token: fewer scopes than were granted, and one initiative. */
43
+ export interface Narrowing {
44
+ scopes?: ReadonlyArray<Scope | PluginScope>;
45
+ initiative?: number;
46
+ }
47
+ /**
48
+ * A standing beyond the plug-in's own reach, which a community grants with its
49
+ * scope: `moderator` (`initiatives:moderate`) in the one initiative the token
50
+ * is narrowed to, or `community_admin` (`community:admin`) across the community.
51
+ */
52
+ export type InstallLevel = "moderator" | "community_admin";
53
+ export interface InstallationOptions extends Narrowing {
54
+ /** Absent: the plug-in's own reach. */
55
+ level?: InstallLevel;
56
+ }
57
+ export interface MemberOptions extends Narrowing {
58
+ /** The purpose the member consented to. Absent: consent to the whole plug-in. */
59
+ purpose?: string;
60
+ }
61
+ /** One community that has installed the plug-in. */
62
+ export interface Installation {
63
+ installation: string;
64
+ /** False while it is switched off or its community is on hold: keep what you hold for it. */
65
+ active: boolean;
66
+ }
67
+ /** What another plug-in answered, through Initiative. */
68
+ export interface InvokeOutcome {
69
+ endpoint: string;
70
+ actor: ActorKind;
71
+ result: Record<string, unknown>;
72
+ }
73
+ /** One member's stored values, by the handle the plug-in knows. */
74
+ export interface MemberConnectionConfig {
75
+ connectionId: string;
76
+ connectionRef: string;
77
+ status: string;
78
+ values: Record<string, unknown>;
79
+ }
80
+ /**
81
+ * The installation's configuration: each community-wide connection's values
82
+ * and the managed values of each member's. Vendor tokens are never in it; ask
83
+ * for one with {@link Client.connectionToken}. Hold it in memory only.
84
+ */
85
+ export interface InstallationConfig {
86
+ communityRef: string;
87
+ installId: number;
88
+ listingUid: string;
89
+ listingVersion: string;
90
+ enabled: boolean;
91
+ /** The plug-in's last verdict on this configuration: `unverified`, `ok` or `invalid`. */
92
+ configState: string;
93
+ configStateDetail: string | null;
94
+ /** Whether an admin still has a community-wide connection to fill in. */
95
+ needsConfig: boolean;
96
+ /** Connection id → field key → value. */
97
+ connections: Record<string, Record<string, unknown>>;
98
+ /** Connection id → the handle of each community-wide connection that has one. */
99
+ connectionRefs: Record<string, string>;
100
+ memberConnections: MemberConnectionConfig[];
101
+ }
102
+ /** One member's connection, with its state and no values. */
103
+ export interface InstallationConnection {
104
+ connectionId: string;
105
+ connectionRef: string;
106
+ status: string;
107
+ blocked: boolean;
108
+ accountLabel: string | null;
109
+ createdAt: string;
110
+ updatedAt: string;
111
+ }
112
+ /** A vendor access token for one connection. Send it to the vendor; never store it. */
113
+ export interface ConnectionAccessToken {
114
+ accessToken: string;
115
+ /** Milliseconds since the epoch, or null when the vendor did not say. */
116
+ expiresAt: number | null;
117
+ }
118
+ export interface ConfigStatusReport {
119
+ state: "ok" | "invalid";
120
+ /** A short code shown beside `invalid`, such as `missing_scope`. */
121
+ detail?: string;
122
+ }
123
+ export interface ConfigStatus {
124
+ communityRef: string;
125
+ installId: number;
126
+ configState: string;
127
+ configStateDetail: string | null;
128
+ }
129
+ export interface InstallationEvent {
130
+ /** An `emit` endpoint the plug-in declares, by its manifest id. */
131
+ eventType: string;
132
+ /** At most 8 KiB as JSON. */
133
+ payload?: Record<string, unknown>;
134
+ /** The initiative the event is about, when it is about one. */
135
+ initiativeId?: number;
136
+ }
137
+ export interface ConsentRequest {
138
+ member: string;
139
+ /** The plug-in's own id for what the member is consenting to. Absent: the whole plug-in. */
140
+ purpose?: string;
141
+ /** Shown to the member as the plug-in's own words. */
142
+ label: string;
143
+ initiativeId?: number;
144
+ /** What the plug-in asks for. The member may grant less. */
145
+ access: "read" | "read_write";
146
+ }
147
+ /** The token endpoint refused (RFC 6749 §5.2). */
148
+ export declare class InitiativeAuthError extends Error {
149
+ /** `invalid_client`, `invalid_grant`, `invalid_scope`, … */
150
+ readonly error: string;
151
+ readonly errorDescription: string | undefined;
152
+ readonly status: number;
153
+ constructor(
154
+ /** `invalid_client`, `invalid_grant`, `invalid_scope`, … */
155
+ error: string, errorDescription: string | undefined, status: number);
156
+ }
157
+ /** The member has not consented to this purpose, or no longer can be acted for. */
158
+ export declare class ConsentRequiredError extends InitiativeAuthError {
159
+ constructor(errorDescription: string | undefined, status: number);
160
+ }
161
+ /** A call to Initiative answered with an error status. */
162
+ export declare class InitiativeApiError extends Error {
163
+ readonly status: number;
164
+ /** Initiative's machine-readable `detail`, when the body carried one. */
165
+ readonly detail: unknown;
166
+ constructor(status: number,
167
+ /** Initiative's machine-readable `detail`, when the body carried one. */
168
+ detail: unknown);
169
+ }
170
+ /**
171
+ * A call needs a scope the token does not hold. Nothing was sent. When any one
172
+ * of several would do, `scope` lists them, separated by spaces.
173
+ */
174
+ export declare class MissingScopeError extends Error {
175
+ readonly scope: string;
176
+ constructor(scope: string);
177
+ }
178
+ /** Whether `held` covers `scope`. Writing implies reading. */
179
+ export declare function grants(held: readonly string[], scope: string): boolean;
180
+ interface Grant {
181
+ installation: string;
182
+ member?: string;
183
+ purpose?: string;
184
+ scopes: string[];
185
+ initiative?: number;
186
+ level?: InstallLevel;
187
+ }
188
+ interface AccessToken {
189
+ token: string;
190
+ scopes: string[];
191
+ expiresAt: number;
192
+ }
193
+ /** Issues, caches and spends the plug-in's tokens. */
194
+ declare class Tokens {
195
+ private readonly options;
196
+ readonly baseUrl: string;
197
+ readonly endpoint: string;
198
+ private readonly cache;
199
+ private readonly inflight;
200
+ readonly fetch: typeof fetch;
201
+ readonly now: () => number;
202
+ constructor(options: InitiativeOptions);
203
+ /** A token for the grant, from the cache while it is fresh. */
204
+ token(grant: Grant | null): Promise<AccessToken>;
205
+ /**
206
+ * `fetch` against the API on the grant's token. A 401 drops the token and the
207
+ * call is sent once more on a fresh one.
208
+ */
209
+ send(grant: Grant | null, url: string, init: RequestInit): Promise<Response>;
210
+ private issue;
211
+ /** A signed assertion addressed to the token endpoint. */
212
+ private assertion;
213
+ }
214
+ /** Initiative, for one plug-in on one deployment. */
215
+ export declare class Initiative {
216
+ private readonly tokens;
217
+ constructor(options: InitiativeOptions);
218
+ /**
219
+ * Every community that has installed the plug-in, following Initiative's
220
+ * `Link` pages to the end. One that is gone is not listed.
221
+ */
222
+ installations(): Promise<Installation[]>;
223
+ /** The plug-in acting as the community that installed it. */
224
+ asInstallation(installation: string, options?: InstallationOptions): Client;
225
+ /** The plug-in acting for one member, within what they consented to. */
226
+ asMember(installation: string, member: string, options?: MemberOptions): Client;
227
+ }
228
+ /**
229
+ * Initiative, acting as one actor in one installation.
230
+ *
231
+ * Content calls ({@link Client.request}, {@link Client.callPlugin}) go on the
232
+ * actor's own token. The installation's own configuration, connections, status
233
+ * and events go on the installation's token, narrowed to the same initiative.
234
+ */
235
+ export declare class Client {
236
+ private readonly tokens;
237
+ private readonly grant;
238
+ private pluginApi?;
239
+ constructor(tokens: Tokens, grant: Grant);
240
+ /**
241
+ * Every route Initiative's plug-in API describes, as a typed method on the
242
+ * actor's token: `client.api.updateTask({ path: { task_id: 7 }, body })`.
243
+ * Each is checked against the token's scopes before it is sent, like
244
+ * {@link Client.request}.
245
+ */
246
+ get api(): PluginApi;
247
+ get installation(): string;
248
+ get actor(): Actor;
249
+ get initiative(): number | null;
250
+ /** The scopes the actor's token holds. */
251
+ scopes(): Promise<string[]>;
252
+ /**
253
+ * One call to a community route, the path after `/c/{community}`, on the actor's
254
+ * token. It is not sent unless the token holds `scope`. Answers the parsed
255
+ * JSON body (a `Blob` when the answer is not JSON), or throws
256
+ * {@link InitiativeApiError}.
257
+ */
258
+ request<T = unknown>(method: string, path: string, options: {
259
+ scope: Scope | PluginScope;
260
+ body?: unknown;
261
+ }): Promise<T>;
262
+ /**
263
+ * Call another plug-in's public endpoint through Initiative, as this actor. Needs
264
+ * `plugins:<publicId>`. A write is sent once and never retried by Initiative.
265
+ */
266
+ callPlugin(publicId: string, endpointId: string, params?: Record<string, unknown>): Promise<InvokeOutcome>;
267
+ /** The installation's configuration. */
268
+ config(): Promise<InstallationConfig>;
269
+ /** The installation's member connections: which handles are live, with no values. */
270
+ connections(): Promise<InstallationConnection[]>;
271
+ /**
272
+ * A usable vendor access token for one connection, by its handle. Initiative
273
+ * refreshes it, or mints it for a `jwt_bearer` connection. Ask each time.
274
+ */
275
+ connectionToken(connectionRef: string): Promise<ConnectionAccessToken>;
276
+ /** Tell Initiative whether the configuration the plug-in was handed works. */
277
+ reportConfigStatus(report: ConfigStatusReport): Promise<ConfigStatus>;
278
+ /** Emit one of the plug-in's declared events, for Initiative to keep and deliver. */
279
+ emitEvent(event: InstallationEvent): Promise<void>;
280
+ /** Ask a member to let the plug-in act for them, on Initiative's own consent screen. */
281
+ requestConsent(request: ConsentRequest): Promise<Record<string, unknown>>;
282
+ /** One operation of the plug-in API, its scope resolved from the operations table. */
283
+ private operation;
284
+ private installationGrant;
285
+ private installationCall;
286
+ private call;
287
+ private send;
288
+ }