flare-plugin-sdk 0.1.0-beta.5 → 0.1.0-beta.8
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/README.md +69 -19
- package/bin/cli.mjs +9 -3
- package/package.json +3 -1
- package/skill/flare-plugin/README.md +9 -5
- package/skill/flare-plugin/SKILL.md +7 -2
- package/skill/flare-plugin/assets/github-template/README.md +42 -18
- package/skill/flare-plugin/assets/github-template/_gitignore +5 -0
- package/skill/flare-plugin/assets/github-template/build.mjs +76 -0
- package/skill/flare-plugin/assets/github-template/manifest.json +1 -0
- package/skill/flare-plugin/assets/github-template/package.json +16 -0
- package/skill/flare-plugin/assets/github-template/plugin.js +109 -103
- package/skill/flare-plugin/assets/github-template/src/plugin.ts +118 -0
- package/skill/flare-plugin/assets/github-template/tsconfig.json +14 -0
- package/skill/flare-plugin/examples/pixelfed/manifest.json +1 -0
- package/skill/flare-plugin/manifest.schema.json +1856 -0
- package/skill/flare-plugin/references/manifest.md +42 -0
- package/skill/flare-plugin/references/publishing.md +17 -3
- package/skill/flare-plugin/references/testing.md +17 -10
|
@@ -1,108 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
capabilities: {
|
|
23
|
-
timeline: {
|
|
24
|
-
async page(request, context) {
|
|
25
|
-
if (request.timelineId !== "home") fail("Unsupported", "Unknown timeline");
|
|
26
|
-
const credential = await flare.credential.read();
|
|
27
|
-
if (!credential || typeof credential !== "object" || Array.isArray(credential) ||
|
|
28
|
-
typeof credential.accessToken !== "string" || !credential.accessToken) {
|
|
29
|
-
fail("AuthenticationRequired", "Import your access token again");
|
|
30
|
-
}
|
|
31
|
-
let url = context.origin + "/api/posts?limit=" + request.page.limit;
|
|
32
|
-
if (request.page.direction !== "refresh" && request.page.cursor) {
|
|
33
|
-
url += "&cursor=" + encodeURIComponent(request.page.cursor);
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// src/plugin.ts
|
|
4
|
+
definePlugin({
|
|
5
|
+
login: {
|
|
6
|
+
token: {
|
|
7
|
+
async begin(request, context) {
|
|
8
|
+
const token = request.values.token?.trim();
|
|
9
|
+
if (!token) fail("Validation", "Enter an access token");
|
|
10
|
+
const profile = mapProfile(await requestJson(context.origin + "/api/me", token), context);
|
|
11
|
+
return {
|
|
12
|
+
type: "success",
|
|
13
|
+
value: {
|
|
14
|
+
accountId: profile.key.id,
|
|
15
|
+
origin: context.origin,
|
|
16
|
+
credential: { accessToken: token },
|
|
17
|
+
profile,
|
|
18
|
+
capabilities: { "flare.datasource.timeline/v1": ["page"] }
|
|
19
|
+
}
|
|
20
|
+
};
|
|
34
21
|
}
|
|
35
|
-
|
|
36
|
-
if (!Array.isArray(data.items)) fail("InvalidResponse", "Expected a post array");
|
|
37
|
-
if (data.next_cursor != null && (typeof data.next_cursor !== "string" || !data.next_cursor)) {
|
|
38
|
-
fail("InvalidResponse", "Expected a non-empty cursor or null");
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
items: data.items.map(post => ({
|
|
42
|
-
key: { id: requiredString(post && post.id), host: accountHost(context) },
|
|
43
|
-
author: mapProfile(post.author, context),
|
|
44
|
-
createdAt: requiredString(post.created_at),
|
|
45
|
-
content: { format: "plain", value: requiredString(post.text) },
|
|
46
|
-
detailType: "link",
|
|
47
|
-
url: requiredString(post.url),
|
|
48
|
-
})),
|
|
49
|
-
olderCursor: data.next_cursor,
|
|
50
|
-
endReached: data.next_cursor == null,
|
|
51
|
-
};
|
|
52
|
-
},
|
|
22
|
+
}
|
|
53
23
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
24
|
+
capabilities: {
|
|
25
|
+
timeline: {
|
|
26
|
+
async page(request, context) {
|
|
27
|
+
if (request.timelineId !== "home") fail("Unsupported", "Unknown timeline");
|
|
28
|
+
const credential = await flare.credential.read();
|
|
29
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential) || typeof credential.accessToken !== "string" || !credential.accessToken) {
|
|
30
|
+
fail("AuthenticationRequired", "Import your access token again");
|
|
31
|
+
}
|
|
32
|
+
let url = context.origin + "/api/posts?limit=" + request.page.limit;
|
|
33
|
+
if (request.page.direction !== "refresh" && request.page.cursor) {
|
|
34
|
+
url += "&cursor=" + encodeURIComponent(request.page.cursor);
|
|
35
|
+
}
|
|
36
|
+
const data = await requestJson(url, credential.accessToken);
|
|
37
|
+
if (!Array.isArray(data.items)) fail("InvalidResponse", "Expected a post array");
|
|
38
|
+
const cursor = data.next_cursor;
|
|
39
|
+
if (cursor != null && (typeof cursor !== "string" || !cursor)) {
|
|
40
|
+
fail("InvalidResponse", "Expected a non-empty cursor or null");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
items: data.items.map((value) => {
|
|
44
|
+
const post = requiredObject(value);
|
|
45
|
+
return {
|
|
46
|
+
key: { id: requiredString(post.id), host: accountHost(context) },
|
|
47
|
+
author: mapProfile(post.author, context),
|
|
48
|
+
createdAt: requiredString(post.created_at),
|
|
49
|
+
content: { format: "plain", value: requiredString(post.text) },
|
|
50
|
+
detailType: "link",
|
|
51
|
+
url: requiredString(post.url)
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
olderCursor: cursor ?? void 0,
|
|
55
|
+
endReached: cursor == null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
function accountHost(context) {
|
|
62
|
+
return context.origin.slice("https://".length);
|
|
63
|
+
}
|
|
64
|
+
function mapProfile(value, context) {
|
|
65
|
+
const profile = requiredObject(value);
|
|
66
|
+
return {
|
|
67
|
+
key: { id: requiredString(profile.id), host: accountHost(context) },
|
|
68
|
+
handle: requiredString(profile.username),
|
|
69
|
+
displayName: requiredString(profile.name)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function requiredString(value) {
|
|
73
|
+
if (typeof value !== "string" || !value.trim()) fail("InvalidResponse", "Missing required text field");
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function requiredObject(value) {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("InvalidResponse", "Expected a JSON object");
|
|
78
|
+
return value;
|
|
81
79
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
async function requestJson(url, token) {
|
|
81
|
+
let response;
|
|
82
|
+
try {
|
|
83
|
+
response = await flare.http.request({ url, authorization: { type: "bearer", token } });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error && typeof error === "object" && "code" in error && error.code === "host.unavailable") {
|
|
86
|
+
fail("Network", "The request could not be completed");
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (response.status === 401) fail("AuthenticationRequired", "The access token was rejected");
|
|
91
|
+
if (response.status === 404) fail("NotFound", "The requested resource was not found");
|
|
92
|
+
if (response.status === 429) {
|
|
93
|
+
const entry = Object.entries(response.headers).find(([name]) => name.toLowerCase() === "retry-after");
|
|
94
|
+
const value = entry?.[1][0];
|
|
95
|
+
const seconds = value && /^\d+$/.test(value) ? Number(value) : void 0;
|
|
96
|
+
throw flare.error({
|
|
97
|
+
code: "RateLimited",
|
|
98
|
+
message: { value: "Try again later" },
|
|
99
|
+
retryAfterSeconds: typeof seconds === "number" && Number.isInteger(seconds) && seconds <= 604800 ? seconds : void 0
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (response.status < 200 || response.status >= 300) fail("Remote", "The website returned an error");
|
|
103
|
+
let data;
|
|
104
|
+
try {
|
|
105
|
+
data = JSON.parse(response.body);
|
|
106
|
+
} catch {
|
|
107
|
+
fail("InvalidResponse", "The website did not return JSON");
|
|
108
|
+
}
|
|
109
|
+
return requiredObject(data);
|
|
93
110
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
data = JSON.parse(response.body);
|
|
98
|
-
} catch (_) {
|
|
99
|
-
fail("InvalidResponse", "The website did not return JSON");
|
|
111
|
+
function fail(code, message) {
|
|
112
|
+
throw flare.error({ code, message: { value: message } });
|
|
100
113
|
}
|
|
101
|
-
|
|
102
|
-
return data;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** @param {PluginErrorCodeV1} code @param {string} message @returns {never} */
|
|
106
|
-
function fail(code, message) {
|
|
107
|
-
throw flare.error({ code, message: { value: message } });
|
|
108
|
-
}
|
|
114
|
+
})();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Adapt the illustrative website contract in README.md before publication.
|
|
2
|
+
definePlugin({
|
|
3
|
+
login: {
|
|
4
|
+
token: {
|
|
5
|
+
async begin(request, context) {
|
|
6
|
+
const token = request.values.token?.trim();
|
|
7
|
+
if (!token) fail("Validation", "Enter an access token");
|
|
8
|
+
const profile = mapProfile(await requestJson(context.origin + "/api/me", token), context);
|
|
9
|
+
return {
|
|
10
|
+
type: "success",
|
|
11
|
+
value: {
|
|
12
|
+
accountId: profile.key.id,
|
|
13
|
+
origin: context.origin,
|
|
14
|
+
credential: { accessToken: token },
|
|
15
|
+
profile,
|
|
16
|
+
capabilities: { "flare.datasource.timeline/v1": ["page"] },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
capabilities: {
|
|
23
|
+
timeline: {
|
|
24
|
+
async page(request, context) {
|
|
25
|
+
if (request.timelineId !== "home") fail("Unsupported", "Unknown timeline");
|
|
26
|
+
const credential = await flare.credential.read();
|
|
27
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential) ||
|
|
28
|
+
typeof credential.accessToken !== "string" || !credential.accessToken) {
|
|
29
|
+
fail("AuthenticationRequired", "Import your access token again");
|
|
30
|
+
}
|
|
31
|
+
let url = context.origin + "/api/posts?limit=" + request.page.limit;
|
|
32
|
+
if (request.page.direction !== "refresh" && request.page.cursor) {
|
|
33
|
+
url += "&cursor=" + encodeURIComponent(request.page.cursor);
|
|
34
|
+
}
|
|
35
|
+
const data = await requestJson(url, credential.accessToken);
|
|
36
|
+
if (!Array.isArray(data.items)) fail("InvalidResponse", "Expected a post array");
|
|
37
|
+
const cursor = data.next_cursor;
|
|
38
|
+
if (cursor != null && (typeof cursor !== "string" || !cursor)) {
|
|
39
|
+
fail("InvalidResponse", "Expected a non-empty cursor or null");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
items: data.items.map((value: unknown): PostV1 => {
|
|
43
|
+
const post = requiredObject(value);
|
|
44
|
+
return {
|
|
45
|
+
key: { id: requiredString(post.id), host: accountHost(context) },
|
|
46
|
+
author: mapProfile(post.author, context),
|
|
47
|
+
createdAt: requiredString(post.created_at),
|
|
48
|
+
content: { format: "plain", value: requiredString(post.text) },
|
|
49
|
+
detailType: "link",
|
|
50
|
+
url: requiredString(post.url),
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
olderCursor: cursor ?? undefined,
|
|
54
|
+
endReached: cursor == null,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function accountHost(context: PluginInvocationContextV1): string {
|
|
62
|
+
return context.origin.slice("https://".length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mapProfile(value: unknown, context: PluginInvocationContextV1): ProfileV1 {
|
|
66
|
+
const profile = requiredObject(value);
|
|
67
|
+
return {
|
|
68
|
+
key: { id: requiredString(profile.id), host: accountHost(context) },
|
|
69
|
+
handle: requiredString(profile.username),
|
|
70
|
+
displayName: requiredString(profile.name),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requiredString(value: unknown): string {
|
|
75
|
+
if (typeof value !== "string" || !value.trim()) fail("InvalidResponse", "Missing required text field");
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requiredObject(value: unknown): Record<string, unknown> {
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("InvalidResponse", "Expected a JSON object");
|
|
81
|
+
return value as Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function requestJson(url: string, token: string): Promise<Record<string, unknown>> {
|
|
85
|
+
let response: HttpResponseV1;
|
|
86
|
+
try {
|
|
87
|
+
response = await flare.http.request({ url, authorization: { type: "bearer", token } });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error && typeof error === "object" && "code" in error && error.code === "host.unavailable") {
|
|
90
|
+
fail("Network", "The request could not be completed");
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
if (response.status === 401) fail("AuthenticationRequired", "The access token was rejected");
|
|
95
|
+
if (response.status === 404) fail("NotFound", "The requested resource was not found");
|
|
96
|
+
if (response.status === 429) {
|
|
97
|
+
const entry = Object.entries(response.headers).find(([name]) => name.toLowerCase() === "retry-after");
|
|
98
|
+
const value = entry?.[1][0];
|
|
99
|
+
const seconds = value && /^\d+$/.test(value) ? Number(value) : undefined;
|
|
100
|
+
throw flare.error({
|
|
101
|
+
code: "RateLimited",
|
|
102
|
+
message: { value: "Try again later" },
|
|
103
|
+
retryAfterSeconds: typeof seconds === "number" && Number.isInteger(seconds) && seconds <= 604800 ? seconds : undefined,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (response.status < 200 || response.status >= 300) fail("Remote", "The website returned an error");
|
|
107
|
+
let data: unknown;
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(response.body);
|
|
110
|
+
} catch {
|
|
111
|
+
fail("InvalidResponse", "The website did not return JSON");
|
|
112
|
+
}
|
|
113
|
+
return requiredObject(data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fail(code: PluginErrorCodeV1, message: string): never {
|
|
117
|
+
throw flare.error({ code, message: { value: message } });
|
|
118
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"target": "ES2020",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"lib": ["ES2020"],
|
|
10
|
+
"types": ["flare-plugin-sdk"],
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|