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.
- package/LICENSE +21 -0
- package/README.md +656 -0
- package/dist/build.d.ts +52 -0
- package/dist/build.js +229 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +160 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +288 -0
- package/dist/client.js +539 -0
- package/dist/client.js.map +1 -0
- package/dist/contract.d.ts +1247 -0
- package/dist/contract.js +167 -0
- package/dist/contract.js.map +1 -0
- package/dist/define.d.ts +336 -0
- package/dist/define.js +120 -0
- package/dist/define.js.map +1 -0
- package/dist/dev.d.ts +28 -0
- package/dist/dev.js +147 -0
- package/dist/dev.js.map +1 -0
- package/dist/expression.d.ts +28 -0
- package/dist/expression.js +67 -0
- package/dist/expression.js.map +1 -0
- package/dist/init.d.ts +14 -0
- package/dist/init.js +52 -0
- package/dist/init.js.map +1 -0
- package/dist/keys.d.ts +85 -0
- package/dist/keys.js +109 -0
- package/dist/keys.js.map +1 -0
- package/dist/manifest.d.ts +8 -0
- package/dist/manifest.js +8 -0
- package/dist/manifest.js.map +1 -0
- package/dist/pack.d.ts +35 -0
- package/dist/pack.js +84 -0
- package/dist/pack.js.map +1 -0
- package/dist/plugin-api.generated.d.ts +4263 -0
- package/dist/plugin-api.generated.js +1019 -0
- package/dist/plugin-api.generated.js.map +1 -0
- package/dist/server.d.ts +75 -0
- package/dist/server.js +408 -0
- package/dist/server.js.map +1 -0
- package/dist/testing.d.ts +116 -0
- package/dist/testing.js +386 -0
- package/dist/testing.js.map +1 -0
- package/dist/tokens.d.ts +161 -0
- package/dist/tokens.js +215 -0
- package/dist/tokens.js.map +1 -0
- package/dist/validate.d.ts +86 -0
- package/dist/validate.js +997 -0
- package/dist/validate.js.map +1 -0
- package/dist/widget.d.ts +235 -0
- package/dist/widget.js +26 -0
- package/dist/widget.js.map +1 -0
- package/examples/minimal/README.md +48 -0
- package/examples/minimal/assets/avatar.png +0 -0
- package/examples/minimal/package.json +22 -0
- package/examples/minimal/src/plugin.ts +77 -0
- package/examples/minimal/src/widgets/open-issues.ts +11 -0
- package/examples/minimal/test/fixtures/issues-statistics.json +9 -0
- package/examples/minimal/test/issue-counts.test.ts +48 -0
- package/examples/minimal/tsconfig.json +14 -0
- package/manifest.contract.json +1886 -0
- package/package.json +92 -0
- package/schemas/plugin-manifest.json +1747 -0
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying the tokens Initiative signs when it reaches the plug-in.
|
|
3
|
+
*
|
|
4
|
+
* Both kinds are RS256 JWTs signed with the deployment's key and published in
|
|
5
|
+
* its JWKS at `/api/v1/plugin-platform/jwks.json`:
|
|
6
|
+
*
|
|
7
|
+
* - **Context token**, on every call to the plug-in (`Authorization: Bearer …`).
|
|
8
|
+
* Scope `endpoint` is a call to one endpoint, named by `endpoint_id`; scope
|
|
9
|
+
* `lifecycle` is a call to one hook, named by `hook`. When another plug-in made
|
|
10
|
+
* the call through Initiative it also carries `act` (that plug-in), `actor`,
|
|
11
|
+
* `member` and, when the caller was confined to one, `initiative_id`.
|
|
12
|
+
* - **Handoff token**, when a member opens one of the plug-in's surfaces. It names
|
|
13
|
+
* the member (`sub`), the surface, and the initiative it was opened in. It is
|
|
14
|
+
* for one use.
|
|
15
|
+
*
|
|
16
|
+
* Each is checked the same way: the `kid` against the deployment's JWKS, the
|
|
17
|
+
* signature, `iss` = `initiative`, `aud` = `initiative-plugin:<public id>`, and
|
|
18
|
+
* `exp`/`iat` against the clock with a small leeway.
|
|
19
|
+
*/
|
|
20
|
+
import { type KeyObject } from "node:crypto";
|
|
21
|
+
import { type ActorKind } from "./contract.js";
|
|
22
|
+
/**
|
|
23
|
+
* What a context token authorizes.
|
|
24
|
+
*
|
|
25
|
+
* `endpoint` covers every call to an endpoint your plug-in declares; the id says
|
|
26
|
+
* which. `lifecycle` is Initiative calling one of your hooks about an
|
|
27
|
+
* installation; `hook` says which.
|
|
28
|
+
*/
|
|
29
|
+
export type ContextScope = "endpoint" | "lifecycle";
|
|
30
|
+
/** Where the deployment publishes its verification keys. */
|
|
31
|
+
export declare const JWKS_PATH = "/api/v1/plugin-platform/jwks.json";
|
|
32
|
+
/** How long a fetched key set is reused before a refetch is considered. */
|
|
33
|
+
export declare const JWKS_CACHE_SECONDS = 300;
|
|
34
|
+
/** The `iss` every token from Initiative carries. */
|
|
35
|
+
export declare const INITIATIVE_ISSUER = "initiative";
|
|
36
|
+
/** `typ` on a call to an endpoint or a hook (RFC 8725 §3.11). */
|
|
37
|
+
export declare const CONTEXT_TOKEN_TYPE = "initiative-context+jwt";
|
|
38
|
+
/** `typ` on a page handoff. */
|
|
39
|
+
export declare const HANDOFF_TOKEN_TYPE = "initiative-handoff+jwt";
|
|
40
|
+
/** Claims every token from Initiative carries. */
|
|
41
|
+
export interface InitiativeTokenClaims {
|
|
42
|
+
jti: string;
|
|
43
|
+
iss: string;
|
|
44
|
+
aud: string;
|
|
45
|
+
iat: number;
|
|
46
|
+
exp: number;
|
|
47
|
+
/**
|
|
48
|
+
* The community, by the reference your installation knows it by. Stable for
|
|
49
|
+
* your installation, so it is what your own rows key on.
|
|
50
|
+
*/
|
|
51
|
+
community_ref: string;
|
|
52
|
+
/** The installation within that community. */
|
|
53
|
+
plugin_install_id: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ContextClaims extends InitiativeTokenClaims {
|
|
56
|
+
scope: ContextScope;
|
|
57
|
+
/** Which endpoint this call is for. Present when the scope is `endpoint`. */
|
|
58
|
+
endpoint_id?: string;
|
|
59
|
+
/** Which hook this call is for. Present when the scope is `lifecycle`. */
|
|
60
|
+
hook?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Connection id → the opaque handle you ask Initiative for an access token
|
|
63
|
+
* with. Present only where the call depends on a connection. On a call from
|
|
64
|
+
* another plug-in it holds only what the actor may use: the member's own
|
|
65
|
+
* connections for a `member` call, the community's for an `installation`
|
|
66
|
+
* one.
|
|
67
|
+
*/
|
|
68
|
+
connection_refs?: Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Present when another plug-in made this call through Initiative: that plug-in's
|
|
71
|
+
* public id, as `act.sub` (RFC 8693 §4.1). Absent when Initiative itself
|
|
72
|
+
* called, for a widget.
|
|
73
|
+
*/
|
|
74
|
+
act?: {
|
|
75
|
+
sub: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* On a call from another plug-in: whose behalf it is on. `installation` is the
|
|
79
|
+
* community; `member` is the member named in {@link ContextClaims.member}.
|
|
80
|
+
*/
|
|
81
|
+
actor?: ActorKind;
|
|
82
|
+
/**
|
|
83
|
+
* On a `member` call: the member, by the reference your installation knows
|
|
84
|
+
* them by. The calling plug-in never sees it.
|
|
85
|
+
*/
|
|
86
|
+
member?: string;
|
|
87
|
+
/**
|
|
88
|
+
* On a call from another plug-in whose token is confined to one initiative: that
|
|
89
|
+
* initiative. Your plug-in is placed there too.
|
|
90
|
+
*/
|
|
91
|
+
initiative_id?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface HandoffClaims extends InitiativeTokenClaims {
|
|
94
|
+
/** The member, by their reference for your installation. */
|
|
95
|
+
sub: string;
|
|
96
|
+
/** Which of your surfaces was opened. */
|
|
97
|
+
surface_id: string;
|
|
98
|
+
/** The initiative it was opened in. Absent when opened for the whole community. */
|
|
99
|
+
initiative_id?: number;
|
|
100
|
+
/** Whether the member administers the community. */
|
|
101
|
+
community_admin?: boolean;
|
|
102
|
+
}
|
|
103
|
+
export declare class ContextTokenError extends Error {
|
|
104
|
+
}
|
|
105
|
+
/** The audience a token for `publicId` names. */
|
|
106
|
+
export declare function audienceFor(publicId: string): string;
|
|
107
|
+
/**
|
|
108
|
+
* Fetches and caches a deployment's published verification keys, by `kid`.
|
|
109
|
+
*
|
|
110
|
+
* An unknown `kid` refetches the set once, so a key rotation (which publishes
|
|
111
|
+
* both keys for a while) resolves without a restart. A second miss inside the
|
|
112
|
+
* same window does not refetch again.
|
|
113
|
+
*/
|
|
114
|
+
export declare class JwksCache {
|
|
115
|
+
private readonly options;
|
|
116
|
+
private readonly cache;
|
|
117
|
+
constructor(options?: {
|
|
118
|
+
/** Injectable for tests and for a runtime with its own fetch. */
|
|
119
|
+
fetchImpl?: typeof fetch;
|
|
120
|
+
/** Milliseconds since the epoch. */
|
|
121
|
+
now?: () => number;
|
|
122
|
+
cacheSeconds?: number;
|
|
123
|
+
/** Which document to read. Defaults to {@link JWKS_PATH}. */
|
|
124
|
+
path?: string;
|
|
125
|
+
});
|
|
126
|
+
/** The key for `kid`, fetching the set if it is unknown or stale. */
|
|
127
|
+
keyFor(baseUrl: string, kid: string): Promise<KeyObject>;
|
|
128
|
+
private load;
|
|
129
|
+
}
|
|
130
|
+
export interface VerifyOptions {
|
|
131
|
+
/** Your plug-in's public id. The audience must name it. */
|
|
132
|
+
publicId: string;
|
|
133
|
+
/** The deployment, for key lookup: its origin or its API base. */
|
|
134
|
+
baseUrl: string;
|
|
135
|
+
jwks: JwksCache;
|
|
136
|
+
/** Defaults to {@link INITIATIVE_ISSUER}. */
|
|
137
|
+
issuer?: string;
|
|
138
|
+
/** Milliseconds since the epoch. */
|
|
139
|
+
now?: () => number;
|
|
140
|
+
/** Tolerance for clock skew, in seconds. Default 30. */
|
|
141
|
+
leewaySeconds?: number;
|
|
142
|
+
}
|
|
143
|
+
/** Verify a context token and return its claims. */
|
|
144
|
+
export declare function verifyContextToken(token: string, options: VerifyOptions): Promise<ContextClaims>;
|
|
145
|
+
/**
|
|
146
|
+
* Verify a handoff token and return its claims.
|
|
147
|
+
*
|
|
148
|
+
* Single use is yours to enforce: record `jti` until `exp` and refuse a token
|
|
149
|
+
* whose `jti` you have already seen.
|
|
150
|
+
*/
|
|
151
|
+
export declare function verifyHandoffToken(token: string, options: VerifyOptions): Promise<HandoffClaims>;
|
|
152
|
+
/**
|
|
153
|
+
* Verify the token on a call to one of your hooks, and return its claims.
|
|
154
|
+
*
|
|
155
|
+
* Refuses anything but a `lifecycle` token, and, when `hook` is given, one
|
|
156
|
+
* minted for another hook: a token for `revoke` cannot be spent on
|
|
157
|
+
* `after_connect`.
|
|
158
|
+
*/
|
|
159
|
+
export declare function verifyLifecycleToken(token: string, options: VerifyOptions & {
|
|
160
|
+
hook?: string;
|
|
161
|
+
}): Promise<ContextClaims>;
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying the tokens Initiative signs when it reaches the plug-in.
|
|
3
|
+
*
|
|
4
|
+
* Both kinds are RS256 JWTs signed with the deployment's key and published in
|
|
5
|
+
* its JWKS at `/api/v1/plugin-platform/jwks.json`:
|
|
6
|
+
*
|
|
7
|
+
* - **Context token**, on every call to the plug-in (`Authorization: Bearer …`).
|
|
8
|
+
* Scope `endpoint` is a call to one endpoint, named by `endpoint_id`; scope
|
|
9
|
+
* `lifecycle` is a call to one hook, named by `hook`. When another plug-in made
|
|
10
|
+
* the call through Initiative it also carries `act` (that plug-in), `actor`,
|
|
11
|
+
* `member` and, when the caller was confined to one, `initiative_id`.
|
|
12
|
+
* - **Handoff token**, when a member opens one of the plug-in's surfaces. It names
|
|
13
|
+
* the member (`sub`), the surface, and the initiative it was opened in. It is
|
|
14
|
+
* for one use.
|
|
15
|
+
*
|
|
16
|
+
* Each is checked the same way: the `kid` against the deployment's JWKS, the
|
|
17
|
+
* signature, `iss` = `initiative`, `aud` = `initiative-plugin:<public id>`, and
|
|
18
|
+
* `exp`/`iat` against the clock with a small leeway.
|
|
19
|
+
*/
|
|
20
|
+
import { createPublicKey, createVerify } from "node:crypto";
|
|
21
|
+
import { ACTOR_KINDS } from "./contract.js";
|
|
22
|
+
/** Where the deployment publishes its verification keys. */
|
|
23
|
+
export const JWKS_PATH = "/api/v1/plugin-platform/jwks.json";
|
|
24
|
+
/** How long a fetched key set is reused before a refetch is considered. */
|
|
25
|
+
export const JWKS_CACHE_SECONDS = 300;
|
|
26
|
+
/** The `iss` every token from Initiative carries. */
|
|
27
|
+
export const INITIATIVE_ISSUER = "initiative";
|
|
28
|
+
/** `typ` on a call to an endpoint or a hook (RFC 8725 §3.11). */
|
|
29
|
+
export const CONTEXT_TOKEN_TYPE = "initiative-context+jwt";
|
|
30
|
+
/** `typ` on a page handoff. */
|
|
31
|
+
export const HANDOFF_TOKEN_TYPE = "initiative-handoff+jwt";
|
|
32
|
+
export class ContextTokenError extends Error {
|
|
33
|
+
}
|
|
34
|
+
/** The audience a token for `publicId` names. */
|
|
35
|
+
export function audienceFor(publicId) {
|
|
36
|
+
return `initiative-plugin:${publicId}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetches and caches a deployment's published verification keys, by `kid`.
|
|
40
|
+
*
|
|
41
|
+
* An unknown `kid` refetches the set once, so a key rotation (which publishes
|
|
42
|
+
* both keys for a while) resolves without a restart. A second miss inside the
|
|
43
|
+
* same window does not refetch again.
|
|
44
|
+
*/
|
|
45
|
+
export class JwksCache {
|
|
46
|
+
options;
|
|
47
|
+
cache = new Map();
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
}
|
|
51
|
+
/** The key for `kid`, fetching the set if it is unknown or stale. */
|
|
52
|
+
async keyFor(baseUrl, kid) {
|
|
53
|
+
const document = new URL(this.options.path ?? JWKS_PATH, baseUrl).toString();
|
|
54
|
+
const entry = this.cache.get(document);
|
|
55
|
+
const now = this.options.now?.() ?? Date.now();
|
|
56
|
+
const ttl = (this.options.cacheSeconds ?? JWKS_CACHE_SECONDS) * 1000;
|
|
57
|
+
const fresh = entry !== undefined && now - entry.fetchedAt < ttl;
|
|
58
|
+
if (fresh) {
|
|
59
|
+
const cached = entry.keys.get(kid);
|
|
60
|
+
if (cached)
|
|
61
|
+
return cached;
|
|
62
|
+
if (entry.refetched)
|
|
63
|
+
return missing(document, entry.keys, kid);
|
|
64
|
+
}
|
|
65
|
+
const keys = await this.load(document);
|
|
66
|
+
this.cache.set(document, { keys, fetchedAt: now, refetched: fresh });
|
|
67
|
+
const found = keys.get(kid);
|
|
68
|
+
if (!found)
|
|
69
|
+
return missing(document, keys, kid);
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
async load(document) {
|
|
73
|
+
const doFetch = this.options.fetchImpl ?? fetch;
|
|
74
|
+
const response = await doFetch(document);
|
|
75
|
+
if (response.status === 404)
|
|
76
|
+
return new Map();
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new ContextTokenError(`jwks fetch failed with ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
const parsed = (await response.json());
|
|
81
|
+
const keys = new Map();
|
|
82
|
+
for (const jwk of parsed.keys ?? []) {
|
|
83
|
+
if (jwk.kty !== "RSA" || !jwk.kid || !jwk.n || !jwk.e)
|
|
84
|
+
continue;
|
|
85
|
+
keys.set(jwk.kid, createPublicKey({ key: jwk, format: "jwk" }));
|
|
86
|
+
}
|
|
87
|
+
return keys;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The same refusal for "no keys published" and "no such key". */
|
|
91
|
+
function missing(document, keys, kid) {
|
|
92
|
+
throw new ContextTokenError(keys.size === 0
|
|
93
|
+
? `${document} published no keys`
|
|
94
|
+
: `no verification key published for kid ${kid}`);
|
|
95
|
+
}
|
|
96
|
+
/** Verify a context token and return its claims. */
|
|
97
|
+
export async function verifyContextToken(token, options) {
|
|
98
|
+
const claims = (await verifyInitiativeToken(token, options, CONTEXT_TOKEN_TYPE));
|
|
99
|
+
if (claims.scope !== "endpoint" && claims.scope !== "lifecycle") {
|
|
100
|
+
throw new ContextTokenError(`not a context token (scope ${String(claims.scope)})`);
|
|
101
|
+
}
|
|
102
|
+
checkCaller(claims);
|
|
103
|
+
return claims;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The claims naming another plug-in's call, checked for shape: an `act` names the
|
|
107
|
+
* caller, an `actor` is one of the two kinds, and a `member` call names its
|
|
108
|
+
* member.
|
|
109
|
+
*/
|
|
110
|
+
function checkCaller(claims) {
|
|
111
|
+
if (claims.act !== undefined) {
|
|
112
|
+
const act = claims.act;
|
|
113
|
+
if (typeof act !== "object" ||
|
|
114
|
+
act === null ||
|
|
115
|
+
typeof act.sub !== "string" ||
|
|
116
|
+
!act.sub) {
|
|
117
|
+
throw new ContextTokenError("act names no calling plug-in");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (claims.actor !== undefined && !ACTOR_KINDS.includes(claims.actor)) {
|
|
121
|
+
throw new ContextTokenError(`unknown actor ${String(claims.actor)}`);
|
|
122
|
+
}
|
|
123
|
+
if (claims.actor === "member" && (typeof claims.member !== "string" || !claims.member)) {
|
|
124
|
+
throw new ContextTokenError("a member call names no member");
|
|
125
|
+
}
|
|
126
|
+
if (claims.initiative_id !== undefined &&
|
|
127
|
+
(!Number.isInteger(claims.initiative_id) || claims.initiative_id <= 0)) {
|
|
128
|
+
throw new ContextTokenError("initiative_id is not an initiative");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Verify a handoff token and return its claims.
|
|
133
|
+
*
|
|
134
|
+
* Single use is yours to enforce: record `jti` until `exp` and refuse a token
|
|
135
|
+
* whose `jti` you have already seen.
|
|
136
|
+
*/
|
|
137
|
+
export async function verifyHandoffToken(token, options) {
|
|
138
|
+
const claims = (await verifyInitiativeToken(token, options, HANDOFF_TOKEN_TYPE));
|
|
139
|
+
if (typeof claims.sub !== "string" || !claims.sub) {
|
|
140
|
+
throw new ContextTokenError("handoff token names no member");
|
|
141
|
+
}
|
|
142
|
+
if (typeof claims.surface_id !== "string" || !claims.surface_id) {
|
|
143
|
+
throw new ContextTokenError("handoff token names no surface");
|
|
144
|
+
}
|
|
145
|
+
return claims;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Verify the token on a call to one of your hooks, and return its claims.
|
|
149
|
+
*
|
|
150
|
+
* Refuses anything but a `lifecycle` token, and, when `hook` is given, one
|
|
151
|
+
* minted for another hook: a token for `revoke` cannot be spent on
|
|
152
|
+
* `after_connect`.
|
|
153
|
+
*/
|
|
154
|
+
export async function verifyLifecycleToken(token, options) {
|
|
155
|
+
const claims = await verifyContextToken(token, options);
|
|
156
|
+
if (claims.scope !== "lifecycle") {
|
|
157
|
+
throw new ContextTokenError(`not a lifecycle token (scope ${String(claims.scope)})`);
|
|
158
|
+
}
|
|
159
|
+
if (options.hook !== undefined && claims.hook !== options.hook) {
|
|
160
|
+
throw new ContextTokenError(`token is for hook ${String(claims.hook)}, not ${options.hook}`);
|
|
161
|
+
}
|
|
162
|
+
return claims;
|
|
163
|
+
}
|
|
164
|
+
/** Type, signature, issuer, audience and time: everything the kinds share. */
|
|
165
|
+
async function verifyInitiativeToken(token, options, typ) {
|
|
166
|
+
const parts = token.split(".");
|
|
167
|
+
if (parts.length !== 3) {
|
|
168
|
+
throw new ContextTokenError("not a JWT");
|
|
169
|
+
}
|
|
170
|
+
const [rawHeader, rawPayload, rawSignature] = parts;
|
|
171
|
+
const header = decodeJson(rawHeader);
|
|
172
|
+
if (header.typ !== typ) {
|
|
173
|
+
throw new ContextTokenError(`token is typed ${String(header.typ)}, not ${typ}`);
|
|
174
|
+
}
|
|
175
|
+
if (header.alg !== "RS256") {
|
|
176
|
+
throw new ContextTokenError(`unexpected algorithm ${header.alg}`);
|
|
177
|
+
}
|
|
178
|
+
if (!header.kid) {
|
|
179
|
+
throw new ContextTokenError("token names no key");
|
|
180
|
+
}
|
|
181
|
+
const key = await options.jwks.keyFor(options.baseUrl, header.kid);
|
|
182
|
+
const verifier = createVerify("RSA-SHA256");
|
|
183
|
+
verifier.update(`${rawHeader}.${rawPayload}`);
|
|
184
|
+
verifier.end();
|
|
185
|
+
if (!verifier.verify(key, Buffer.from(rawSignature, "base64url"))) {
|
|
186
|
+
throw new ContextTokenError("signature did not verify");
|
|
187
|
+
}
|
|
188
|
+
const claims = decodeJson(rawPayload);
|
|
189
|
+
const expected = audienceFor(options.publicId);
|
|
190
|
+
if (claims.aud !== expected) {
|
|
191
|
+
throw new ContextTokenError(`token is for ${claims.aud}, not ${expected}`);
|
|
192
|
+
}
|
|
193
|
+
const issuer = options.issuer ?? INITIATIVE_ISSUER;
|
|
194
|
+
if (claims.iss !== issuer) {
|
|
195
|
+
throw new ContextTokenError(`token is from ${claims.iss}, not ${issuer}`);
|
|
196
|
+
}
|
|
197
|
+
const seconds = Math.floor((options.now?.() ?? Date.now()) / 1000);
|
|
198
|
+
const leeway = options.leewaySeconds ?? 30;
|
|
199
|
+
if (typeof claims.exp !== "number" || claims.exp + leeway < seconds) {
|
|
200
|
+
throw new ContextTokenError("token has expired");
|
|
201
|
+
}
|
|
202
|
+
if (typeof claims.iat === "number" && claims.iat - leeway > seconds) {
|
|
203
|
+
throw new ContextTokenError("token is not valid yet");
|
|
204
|
+
}
|
|
205
|
+
return claims;
|
|
206
|
+
}
|
|
207
|
+
function decodeJson(segment) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(Buffer.from(segment, "base64url").toString("utf-8"));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
throw new ContextTokenError("not a JWT");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking a manifest before a deployment does.
|
|
3
|
+
*
|
|
4
|
+
* **Schema-valid is necessary, not sufficient.** Some rules are not expressible
|
|
5
|
+
* in JSON Schema and are checked by the platform on publish: cross-references
|
|
6
|
+
* (the endpoint a widget binds, a `requires` term's connection, an endpoint's
|
|
7
|
+
* service prefix), the features/blocks cross-check in both directions, UTF-8
|
|
8
|
+
* byte-size caps, the rules tying a connection's `flow` and `token` to its
|
|
9
|
+
* scope and fields, what a vendor `setup` writes to, and the bounds and unique
|
|
10
|
+
* ids of `schedules`.
|
|
11
|
+
*
|
|
12
|
+
* {@link validateManifest} runs the schema and then every one of those except
|
|
13
|
+
* the byte caps. It also reports every term the contract does not declare: a
|
|
14
|
+
* deployment drops such a term rather than refusing the manifest, so a
|
|
15
|
+
* misspelt or retired field would otherwise do nothing without saying so. And
|
|
16
|
+
* it adds one rule the platform does not check: an endpoint's `identity` must
|
|
17
|
+
* name returns that endpoint actually sends.
|
|
18
|
+
*
|
|
19
|
+
* For a declarative plug-in it also parses every JSONata expression, and checks
|
|
20
|
+
* that a plug-in is one kind or the other, what its requests and steps name, and
|
|
21
|
+
* the hosts it calls.
|
|
22
|
+
*/
|
|
23
|
+
import { type Manifest } from "./contract.js";
|
|
24
|
+
/** Where a plug-in serves its manifest document. */
|
|
25
|
+
export declare const MANIFEST_PATH = "/.well-known/initiative-plugin.json";
|
|
26
|
+
/** The wire protocol this SDK speaks. */
|
|
27
|
+
export declare const PLUGIN_PROTOCOL_VERSION = 1;
|
|
28
|
+
/**
|
|
29
|
+
* The document served at {@link MANIFEST_PATH}, of which {@link Manifest} is one
|
|
30
|
+
* field: the plug-in's identity beside what it declares it can do. A registrar
|
|
31
|
+
* refuses anything without a `protocol_version`, a `public_id`, a `kind` and a
|
|
32
|
+
* `definition`.
|
|
33
|
+
*/
|
|
34
|
+
export interface PluginDocument {
|
|
35
|
+
protocol_version: number;
|
|
36
|
+
/** `<publisher>.<slug>`, the same id `definition.service.public_id` carries. */
|
|
37
|
+
public_id: string;
|
|
38
|
+
kind: "plugin";
|
|
39
|
+
/** The catalog id: publisher-assigned, immutable, never reused. */
|
|
40
|
+
uid?: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
definition: Manifest;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The document to serve at {@link MANIFEST_PATH}. Serve the same bytes every
|
|
46
|
+
* time: a deployment hashes what it fetches and re-checks it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pluginDocument(manifest: Manifest, options?: {
|
|
49
|
+
uid?: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
}): PluginDocument;
|
|
52
|
+
/**
|
|
53
|
+
* Check a whole served document — the envelope, then the manifest inside it.
|
|
54
|
+
*
|
|
55
|
+
* {@link validateManifest} checks what a plug-in declares; this checks what a
|
|
56
|
+
* registrar will actually fetch. Use it on the bytes you serve.
|
|
57
|
+
*/
|
|
58
|
+
export declare function validateDocument(document: unknown): ValidationProblem[];
|
|
59
|
+
/** The generated schema, read from disk once. */
|
|
60
|
+
export declare function manifestSchema(): Record<string, unknown>;
|
|
61
|
+
export interface ValidationProblem {
|
|
62
|
+
/** A JSON Pointer-ish path into the manifest. */
|
|
63
|
+
where: string;
|
|
64
|
+
message: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Everything this side can check: the schema, then the two cross-cutting rules
|
|
68
|
+
* it cannot express.
|
|
69
|
+
*
|
|
70
|
+
* The schema runs first and short-circuits. A manifest whose shape is wrong
|
|
71
|
+
* produces cascading nonsense from the reference checks — "binds 'undefined',
|
|
72
|
+
* which is not a declared read endpoint" when the real answer is "endpoints
|
|
73
|
+
* must be an array" — so the structural answer is worth giving alone.
|
|
74
|
+
*
|
|
75
|
+
* An empty array does not promise the platform will accept it — see the module
|
|
76
|
+
* note — but a non-empty one is a definite refusal, so this is worth running in
|
|
77
|
+
* CI and before a publish.
|
|
78
|
+
*
|
|
79
|
+
* A declarative plug-in's manifest has no `service` block to name the plug-in, so its
|
|
80
|
+
* endpoint ids are checked against `publicId` when one is given.
|
|
81
|
+
*/
|
|
82
|
+
export declare function validateManifest(manifest: unknown, options?: {
|
|
83
|
+
publicId?: string;
|
|
84
|
+
}): ValidationProblem[];
|
|
85
|
+
/** Every `{name}` in a template, in order. */
|
|
86
|
+
export declare function templateNames(value: string): string[];
|