runline 0.29.0 → 0.30.1
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/dist/auth/errors.d.ts +22 -0
- package/dist/auth/errors.js +25 -0
- package/dist/auth/oauth2.d.ts +21 -0
- package/dist/auth/oauth2.js +154 -0
- package/dist/auth/token.d.ts +11 -0
- package/dist/auth/token.js +231 -0
- package/dist/auth/types.d.ts +93 -0
- package/dist/auth/types.js +1 -0
- package/dist/commands/auth.js +26 -13
- package/dist/commands/connection.js +3 -6
- package/dist/commands/exec.js +7 -2
- package/dist/commands/init.js +9 -2
- package/dist/config/loader.d.ts +7 -13
- package/dist/config/loader.js +58 -88
- package/dist/config/store.d.ts +17 -0
- package/dist/config/store.js +85 -0
- package/dist/connections/file.d.ts +13 -0
- package/dist/connections/file.js +53 -0
- package/dist/connections/memory.d.ts +15 -0
- package/dist/connections/memory.js +70 -0
- package/dist/connections/types.d.ts +47 -0
- package/dist/connections/types.js +10 -0
- package/dist/core/engine.d.ts +5 -2
- package/dist/core/engine.js +48 -55
- package/dist/core/oauth.d.ts +20 -15
- package/dist/core/oauth.js +195 -98
- package/dist/credentials/http.d.ts +19 -0
- package/dist/credentials/http.js +109 -0
- package/dist/credentials/policy.d.ts +5 -0
- package/dist/credentials/policy.js +92 -0
- package/dist/credentials/registry.d.ts +31 -0
- package/dist/credentials/registry.js +171 -0
- package/dist/credentials/transport.d.ts +38 -0
- package/dist/credentials/transport.js +283 -0
- package/dist/credentials/types.d.ts +80 -0
- package/dist/credentials/types.js +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +9 -1
- package/dist/plugin/types.d.ts +30 -17
- package/dist/plugins/_shared/credentialAdapter.js +88 -0
- package/dist/plugins/_shared/googleAuth.js +89 -103
- package/dist/plugins/_shared/googleCredentials.js +160 -0
- package/dist/plugins/_shared/microsoftAuth.js +81 -74
- package/dist/plugins/_shared/microsoftCredentials.js +90 -0
- package/dist/plugins/_shared/tokenRefresh.js +39 -0
- package/dist/plugins/bitwarden/src/index.js +13 -31
- package/dist/plugins/gmail/src/index.js +8 -44
- package/dist/plugins/googleAppsScript/src/index.js +3 -25
- package/dist/plugins/googleCalendar/src/index.js +3 -41
- package/dist/plugins/googleContacts/src/index.js +3 -36
- package/dist/plugins/googleDocs/src/index.js +5 -0
- package/dist/plugins/googleDocs/src/shared.js +2 -30
- package/dist/plugins/googleDrive/src/index.js +52 -98
- package/dist/plugins/googleSheets/src/index.js +3 -40
- package/dist/plugins/googleSlides/src/index.js +5 -39
- package/dist/plugins/googleTasks/src/index.js +3 -30
- package/dist/plugins/halopsa/src/index.js +16 -31
- package/dist/plugins/microsoftCalendar/src/index.js +2 -1
- package/dist/plugins/microsoftFiles/src/index.js +10 -24
- package/dist/plugins/microsoftMail/src/index.js +2 -1
- package/dist/plugins/paypal/src/index.js +14 -29
- package/dist/plugins/plaud/src/index.js +210 -0
- package/dist/plugins/plaud/src/shared.js +221 -0
- package/dist/plugins/salesforce/src/shared.js +17 -32
- package/dist/sdk.d.ts +13 -5
- package/dist/sdk.js +35 -29
- package/dist/utils/observer.d.ts +2 -0
- package/dist/utils/observer.js +11 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { graphRequest,
|
|
1
|
+
import { graphRequest, graphResponse, microsoftDownload, microsoftDriveBase, microsoftSetupHelp, } from "../../_shared/microsoftAuth.js";
|
|
2
2
|
const NAME = "microsoftFiles";
|
|
3
3
|
const SCOPES = [
|
|
4
4
|
"https://graph.microsoft.com/Files.ReadWrite.All",
|
|
@@ -6,30 +6,16 @@ const SCOPES = [
|
|
|
6
6
|
];
|
|
7
7
|
/** Resolve the drive root path: explicit drive/site, else the user's default drive. */
|
|
8
8
|
function driveBase(ctx) {
|
|
9
|
-
|
|
10
|
-
if (cfg.driveId)
|
|
11
|
-
return `/drives/${cfg.driveId}`;
|
|
12
|
-
if (cfg.siteId)
|
|
13
|
-
return `/sites/${cfg.siteId}/drive`;
|
|
14
|
-
return `${userBase(ctx)}/drive`;
|
|
9
|
+
return microsoftDriveBase(ctx.connection.config);
|
|
15
10
|
}
|
|
16
11
|
async function binaryFetch(ctx, method, path, body) {
|
|
17
|
-
|
|
18
|
-
const init = {
|
|
19
|
-
method,
|
|
20
|
-
headers: {
|
|
21
|
-
Authorization: `Bearer ${token}`,
|
|
22
|
-
...(body ? { "Content-Type": "application/octet-stream" } : {}),
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
if (body)
|
|
26
|
-
init.body = body;
|
|
27
|
-
return fetch(`https://graph.microsoft.com/v1.0${path}`, init);
|
|
12
|
+
return graphResponse(ctx, NAME, SCOPES, method, path, body, body ? "application/octet-stream" : undefined);
|
|
28
13
|
}
|
|
29
14
|
export default function microsoftFiles(rl) {
|
|
30
15
|
rl.setName(NAME);
|
|
31
16
|
rl.setVersion("1.0.0");
|
|
32
17
|
rl.setConnectionSchema({
|
|
18
|
+
authMethod: { type: "string", required: false, description: "delegated or appOnly; legacy configs infer the method from existing credentials" },
|
|
33
19
|
tenantId: { type: "string", required: false, env: "MS_GRAPH_TENANT_ID", description: "Entra tenant id (app-only) or omit for OAuth /common" },
|
|
34
20
|
clientId: { type: "string", required: false, env: "MS_GRAPH_CLIENT_ID", description: "App (client) id" },
|
|
35
21
|
clientSecret: { type: "string", required: false, env: "MS_GRAPH_CLIENT_SECRET", description: "Client secret VALUE" },
|
|
@@ -69,7 +55,7 @@ export default function microsoftFiles(rl) {
|
|
|
69
55
|
top: { type: "number", required: false, default: 100 },
|
|
70
56
|
},
|
|
71
57
|
async execute(input, ctx) {
|
|
72
|
-
const where = input.folderId ? `/items/${input.folderId}/children` : "/root/children";
|
|
58
|
+
const where = input.folderId ? `/items/${encodeURIComponent(input.folderId)}/children` : "/root/children";
|
|
73
59
|
const qs = new URLSearchParams({
|
|
74
60
|
$top: String(input.top ?? 100),
|
|
75
61
|
$select: "id,name,size,lastModifiedDateTime,webUrl,folder,file",
|
|
@@ -83,10 +69,10 @@ export default function microsoftFiles(rl) {
|
|
|
83
69
|
description: "Download a file by id. Returns {id,name,size,contentType,base64}. base64 is the file bytes.",
|
|
84
70
|
inputSchema: { id: { type: "string", required: true } },
|
|
85
71
|
async execute(input, ctx) {
|
|
86
|
-
const meta = await graphRequest(ctx, NAME, SCOPES, "GET", `${driveBase(ctx)}/items/${input.id}`);
|
|
87
|
-
const res = await
|
|
72
|
+
const meta = await graphRequest(ctx, NAME, SCOPES, "GET", `${driveBase(ctx)}/items/${encodeURIComponent(input.id)}`);
|
|
73
|
+
const res = await microsoftDownload(meta["@microsoft.graph.downloadUrl"]);
|
|
88
74
|
if (!res.ok)
|
|
89
|
-
throw new Error(`${NAME}: download
|
|
75
|
+
throw new Error(`${NAME}: download failed (HTTP ${res.status})`);
|
|
90
76
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
91
77
|
return {
|
|
92
78
|
id: meta.id,
|
|
@@ -109,7 +95,7 @@ export default function microsoftFiles(rl) {
|
|
|
109
95
|
const p = input.path.split("/").map(encodeURIComponent).join("/");
|
|
110
96
|
const res = await binaryFetch(ctx, "PUT", `${driveBase(ctx)}/root:/${p}:/content`, bytes);
|
|
111
97
|
if (!res.ok)
|
|
112
|
-
throw new Error(`${NAME}: upload
|
|
98
|
+
throw new Error(`${NAME}: upload failed (HTTP ${res.status})`);
|
|
113
99
|
return JSON.parse(await res.text());
|
|
114
100
|
},
|
|
115
101
|
});
|
|
@@ -121,7 +107,7 @@ export default function microsoftFiles(rl) {
|
|
|
121
107
|
parentId: { type: "string", required: false, description: "Parent folder id; omit for root" },
|
|
122
108
|
},
|
|
123
109
|
async execute(input, ctx) {
|
|
124
|
-
const where = input.parentId ? `/items/${input.parentId}/children` : "/root/children";
|
|
110
|
+
const where = input.parentId ? `/items/${encodeURIComponent(input.parentId)}/children` : "/root/children";
|
|
125
111
|
return graphRequest(ctx, NAME, SCOPES, "POST", `${driveBase(ctx)}${where}`, {
|
|
126
112
|
name: input.name,
|
|
127
113
|
folder: {},
|
|
@@ -20,6 +20,7 @@ export default function microsoftMail(rl) {
|
|
|
20
20
|
rl.setName(NAME);
|
|
21
21
|
rl.setVersion("1.0.0");
|
|
22
22
|
rl.setConnectionSchema({
|
|
23
|
+
authMethod: { type: "string", required: false, description: "delegated or appOnly; legacy configs infer the method from existing credentials" },
|
|
23
24
|
tenantId: { type: "string", required: false, env: "MS_GRAPH_TENANT_ID", description: "Entra tenant id (app-only) or omit for OAuth /common" },
|
|
24
25
|
clientId: { type: "string", required: false, env: "MS_GRAPH_CLIENT_ID", description: "App (client) id" },
|
|
25
26
|
clientSecret: { type: "string", required: false, env: "MS_GRAPH_CLIENT_SECRET", description: "Client secret VALUE" },
|
|
@@ -89,7 +90,7 @@ export default function microsoftMail(rl) {
|
|
|
89
90
|
description: "Get one message with full body by id.",
|
|
90
91
|
inputSchema: { id: { type: "string", required: true } },
|
|
91
92
|
async execute(input, ctx) {
|
|
92
|
-
return graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/messages/${input.id}`);
|
|
93
|
+
return graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/messages/${encodeURIComponent(input.id)}`);
|
|
93
94
|
},
|
|
94
95
|
});
|
|
95
96
|
}
|
|
@@ -1,35 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
function getConn(
|
|
3
|
-
const c = ctx.connection.config;
|
|
1
|
+
import { coordinatedAccessToken, requestToken, } from "../../_shared/tokenRefresh.js";
|
|
2
|
+
function getConn(c) {
|
|
4
3
|
const env = c.env ?? "sandbox";
|
|
5
4
|
const base = env === "live"
|
|
6
5
|
? "https://api-m.paypal.com"
|
|
7
6
|
: "https://api-m.sandbox.paypal.com";
|
|
8
7
|
return { base, clientId: c.clientId, secret: c.secret };
|
|
9
8
|
}
|
|
10
|
-
async function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
18
|
-
},
|
|
19
|
-
body: "grant_type=client_credentials",
|
|
9
|
+
async function apiRequest(ctx, method, endpoint, body, qs) {
|
|
10
|
+
const token = await coordinatedAccessToken(ctx, (current) => {
|
|
11
|
+
const conn = getConn(current);
|
|
12
|
+
return requestToken({
|
|
13
|
+
url: `${conn.base}/v1/oauth2/token`,
|
|
14
|
+
clientAuthentication: "client_secret_basic",
|
|
15
|
+
}, { grant_type: "client_credentials" }, { clientId: conn.clientId, clientSecret: conn.secret });
|
|
20
16
|
});
|
|
21
|
-
|
|
22
|
-
throw new Error(`PayPal auth error ${res.status}: ${await res.text()}`);
|
|
23
|
-
const data = (await res.json());
|
|
24
|
-
cachedToken = {
|
|
25
|
-
token: data.access_token,
|
|
26
|
-
expiresAt: Date.now() + (data.expires_in - 60) * 1000,
|
|
27
|
-
};
|
|
28
|
-
return cachedToken.token;
|
|
29
|
-
}
|
|
30
|
-
async function apiRequest(conn, method, endpoint, body, qs) {
|
|
31
|
-
const token = await getAccessToken(conn);
|
|
32
|
-
const url = new URL(`${conn.base}/v1${endpoint}`);
|
|
17
|
+
const url = new URL(`${getConn(ctx.connection.config).base}/v1${endpoint}`);
|
|
33
18
|
if (qs) {
|
|
34
19
|
for (const [k, v] of Object.entries(qs)) {
|
|
35
20
|
if (v !== undefined && v !== null)
|
|
@@ -103,7 +88,7 @@ export default function paypal(rl) {
|
|
|
103
88
|
header.email_message = p.emailMessage;
|
|
104
89
|
if (p.note)
|
|
105
90
|
header.note = p.note;
|
|
106
|
-
return apiRequest(
|
|
91
|
+
return apiRequest(ctx, "POST", "/payments/payouts", {
|
|
107
92
|
sender_batch_header: header,
|
|
108
93
|
items: p.items,
|
|
109
94
|
});
|
|
@@ -125,7 +110,7 @@ export default function paypal(rl) {
|
|
|
125
110
|
const qs = {};
|
|
126
111
|
if (p.limit)
|
|
127
112
|
qs.page_size = p.limit;
|
|
128
|
-
const data = (await apiRequest(
|
|
113
|
+
const data = (await apiRequest(ctx, "GET", `/payments/payouts/${p.payoutBatchId}`, undefined, qs));
|
|
129
114
|
return data.items;
|
|
130
115
|
},
|
|
131
116
|
});
|
|
@@ -135,7 +120,7 @@ export default function paypal(rl) {
|
|
|
135
120
|
inputSchema: { payoutItemId: { type: "string", required: true } },
|
|
136
121
|
async execute(input, ctx) {
|
|
137
122
|
const { payoutItemId } = input;
|
|
138
|
-
return apiRequest(
|
|
123
|
+
return apiRequest(ctx, "GET", `/payments/payouts-item/${payoutItemId}`);
|
|
139
124
|
},
|
|
140
125
|
});
|
|
141
126
|
rl.registerAction("payoutItem.cancel", {
|
|
@@ -144,7 +129,7 @@ export default function paypal(rl) {
|
|
|
144
129
|
inputSchema: { payoutItemId: { type: "string", required: true } },
|
|
145
130
|
async execute(input, ctx) {
|
|
146
131
|
const { payoutItemId } = input;
|
|
147
|
-
return apiRequest(
|
|
132
|
+
return apiRequest(ctx, "POST", `/payments/payouts-item/${payoutItemId}/cancel`);
|
|
148
133
|
},
|
|
149
134
|
});
|
|
150
135
|
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { blockContent, blocks, dateBoundary, list, PLAUD_OAUTH, PLAUD_PUBLIC_CLIENT_ID, PLAUD_REDIRECT_URI, recording, request, scan, } from "./shared.js";
|
|
3
|
+
const strict = { additionalProperties: false };
|
|
4
|
+
const Id = t.String({ minLength: 1, maxLength: 512 });
|
|
5
|
+
const RecordingInput = t.Object({ id: Id }, strict);
|
|
6
|
+
const ScanFields = {
|
|
7
|
+
limit: t.Optional(t.Integer({
|
|
8
|
+
minimum: 1,
|
|
9
|
+
maximum: 1000,
|
|
10
|
+
description: "Maximum matching recordings (default 50)",
|
|
11
|
+
})),
|
|
12
|
+
maxPages: t.Optional(t.Integer({
|
|
13
|
+
minimum: 1,
|
|
14
|
+
maximum: 10,
|
|
15
|
+
description: "Bounded scan of 100 recordings per page (default 5)",
|
|
16
|
+
})),
|
|
17
|
+
};
|
|
18
|
+
const Day = t.String({
|
|
19
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
20
|
+
description: "UTC date, YYYY-MM-DD",
|
|
21
|
+
});
|
|
22
|
+
const ListInput = t.Object({
|
|
23
|
+
page: t.Optional(t.Integer({ minimum: 1, maximum: 1000 })),
|
|
24
|
+
pageSize: t.Optional(t.Integer({ minimum: 10, maximum: 100 })),
|
|
25
|
+
}, strict);
|
|
26
|
+
const SearchInput = t.Object({
|
|
27
|
+
query: t.String({ minLength: 1, pattern: "\\S" }),
|
|
28
|
+
from: t.Optional(Day),
|
|
29
|
+
to: t.Optional(Day),
|
|
30
|
+
...ScanFields,
|
|
31
|
+
}, strict);
|
|
32
|
+
const RecentInput = t.Object({ days: t.Optional(t.Integer({ minimum: 1, maximum: 365 })), ...ScanFields }, strict);
|
|
33
|
+
const TranscriptInput = t.Object({
|
|
34
|
+
id: Id,
|
|
35
|
+
block: t.Optional(t.Union([
|
|
36
|
+
t.Literal("transaction"),
|
|
37
|
+
t.Literal("transaction_polish"),
|
|
38
|
+
t.Literal("outline"),
|
|
39
|
+
t.Literal("mark_memo"),
|
|
40
|
+
])),
|
|
41
|
+
}, strict);
|
|
42
|
+
const SummaryInput = t.Object({ id: Id, index: t.Optional(t.Integer({ minimum: 0, maximum: 999 })) }, strict);
|
|
43
|
+
export default function plaud(rl) {
|
|
44
|
+
rl.setName("plaud");
|
|
45
|
+
rl.setVersion("0.1.0");
|
|
46
|
+
rl.setOAuth({
|
|
47
|
+
protocol: PLAUD_OAUTH,
|
|
48
|
+
scopes: [],
|
|
49
|
+
redirectUri: PLAUD_REDIRECT_URI,
|
|
50
|
+
publicClient: true,
|
|
51
|
+
defaultClientId: PLAUD_PUBLIC_CLIENT_ID,
|
|
52
|
+
setupHelp: [
|
|
53
|
+
"Plaud publishes a public OAuth client (PKCE, no secret) with the fixed callback {{redirectUri}}; the same flow as the official CLI.",
|
|
54
|
+
"Set PLAUD_CLIENT_ID to use a different client registered for that callback.",
|
|
55
|
+
"Plaud Embedded partner credentials are for a different API and are not interchangeable.",
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
rl.setConnectionSchema({
|
|
59
|
+
clientId: {
|
|
60
|
+
type: "string",
|
|
61
|
+
required: false,
|
|
62
|
+
env: "PLAUD_CLIENT_ID",
|
|
63
|
+
description: "Public third-party OAuth client ID used at login; defaults to Plaud's published client",
|
|
64
|
+
},
|
|
65
|
+
refreshToken: {
|
|
66
|
+
type: "string",
|
|
67
|
+
required: false,
|
|
68
|
+
env: "PLAUD_REFRESH_TOKEN",
|
|
69
|
+
description: "Third-party OAuth refresh token",
|
|
70
|
+
},
|
|
71
|
+
accessToken: {
|
|
72
|
+
type: "string",
|
|
73
|
+
required: false,
|
|
74
|
+
env: "PLAUD_ACCESS_TOKEN",
|
|
75
|
+
description: "Cached third-party OAuth access token",
|
|
76
|
+
},
|
|
77
|
+
accessTokenExpiresAt: {
|
|
78
|
+
type: "number",
|
|
79
|
+
required: false,
|
|
80
|
+
description: "Access-token expiry in epoch milliseconds; omit if unknown",
|
|
81
|
+
},
|
|
82
|
+
contentOrigins: {
|
|
83
|
+
type: "array",
|
|
84
|
+
required: false,
|
|
85
|
+
description: "Host-approved exact HTTPS origins for signed transcript/summary data_link downloads. Empty by default; never accepts action-supplied origins.",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
rl.registerAction("user.get", {
|
|
89
|
+
access: "read",
|
|
90
|
+
description: "Get the authenticated Plaud user.",
|
|
91
|
+
inputSchema: t.Object({}, strict),
|
|
92
|
+
execute: (_input, ctx) => request(ctx, "users/current"),
|
|
93
|
+
});
|
|
94
|
+
rl.registerAction("recording.list", {
|
|
95
|
+
access: "read",
|
|
96
|
+
description: "List one page of recordings; preserves Plaud's data and pagination fields. Durations are milliseconds.",
|
|
97
|
+
inputSchema: ListInput,
|
|
98
|
+
execute: (input, ctx) => {
|
|
99
|
+
const p = input;
|
|
100
|
+
return list(ctx, p.page ?? 1, p.pageSize ?? 20);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
rl.registerAction("recording.get", {
|
|
104
|
+
access: "read",
|
|
105
|
+
description: "Get recording metadata, source_list, note_list and any signed audio URL. Returned content is private application data.",
|
|
106
|
+
inputSchema: RecordingInput,
|
|
107
|
+
execute: (input, ctx) => recording(ctx, input.id),
|
|
108
|
+
});
|
|
109
|
+
rl.registerAction("recording.search", {
|
|
110
|
+
access: "read",
|
|
111
|
+
description: "Case-insensitive name search over a bounded number of pages, not transcript/full-text search. Returns scan counts and truncated; dates are inclusive UTC.",
|
|
112
|
+
inputSchema: SearchInput,
|
|
113
|
+
execute: (input, ctx) => {
|
|
114
|
+
const p = input;
|
|
115
|
+
return scan(ctx, {
|
|
116
|
+
query: p.query,
|
|
117
|
+
from: dateBoundary(p.from, false),
|
|
118
|
+
to: dateBoundary(p.to, true),
|
|
119
|
+
limit: p.limit ?? 50,
|
|
120
|
+
maxPages: p.maxPages ?? 5,
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
rl.registerAction("recording.recent", {
|
|
125
|
+
access: "read",
|
|
126
|
+
description: "Recordings created within the last N days (default 7). Scans bounded pages; truncated means the result may be incomplete.",
|
|
127
|
+
inputSchema: RecentInput,
|
|
128
|
+
execute: (input, ctx) => {
|
|
129
|
+
const p = input;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
return scan(ctx, {
|
|
132
|
+
from: now - (p.days ?? 7) * 86_400_000,
|
|
133
|
+
to: now,
|
|
134
|
+
limit: p.limit ?? 50,
|
|
135
|
+
maxPages: p.maxPages ?? 5,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
rl.registerAction("recording.audioUrl", {
|
|
140
|
+
access: "read",
|
|
141
|
+
description: "Return Plaud's temporary signed audio URL without downloading audio. Treat the URL as a secret; missing URLs may be transient.",
|
|
142
|
+
inputSchema: RecordingInput,
|
|
143
|
+
async execute(input, ctx) {
|
|
144
|
+
const { id } = input;
|
|
145
|
+
const file = await recording(ctx, id);
|
|
146
|
+
return {
|
|
147
|
+
recordingId: id,
|
|
148
|
+
url: typeof file.presigned_url === "string" && file.presigned_url
|
|
149
|
+
? file.presigned_url
|
|
150
|
+
: null,
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
rl.registerAction("transcript.get", {
|
|
155
|
+
access: "read",
|
|
156
|
+
description: "Get an original/polished transcript, outline, or highlights. Uses inline content first; signed links require host-approved contentOrigins. JSON content is decoded; segment times remain milliseconds.",
|
|
157
|
+
inputSchema: TranscriptInput,
|
|
158
|
+
async execute(input, ctx) {
|
|
159
|
+
const { id, block = "transaction" } = input;
|
|
160
|
+
const file = await recording(ctx, id);
|
|
161
|
+
const sources = blocks(file, "source_list");
|
|
162
|
+
const content = await blockContent(ctx, sources.find((source) => source.data_type === block));
|
|
163
|
+
let parsed = content.content;
|
|
164
|
+
if (content.content) {
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(content.content);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
/* Plain-text transcripts are valid. */
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
recordingId: id,
|
|
174
|
+
block,
|
|
175
|
+
...content,
|
|
176
|
+
content: parsed,
|
|
177
|
+
availableBlocks: sources
|
|
178
|
+
.map((source) => source.data_type)
|
|
179
|
+
.filter((type) => typeof type === "string"),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
rl.registerAction("summary.get", {
|
|
184
|
+
access: "read",
|
|
185
|
+
description: "Get an auto_sum_note summary (Markdown). index selects among multiple summaries, starting at 0. Linked content requires host-approved contentOrigins.",
|
|
186
|
+
inputSchema: SummaryInput,
|
|
187
|
+
async execute(input, ctx) {
|
|
188
|
+
const { id, index = 0 } = input;
|
|
189
|
+
const notes = blocks(await recording(ctx, id), "note_list").filter((note) => note.data_type === "auto_sum_note");
|
|
190
|
+
return {
|
|
191
|
+
recordingId: id,
|
|
192
|
+
index,
|
|
193
|
+
count: notes.length,
|
|
194
|
+
...(await blockContent(ctx, notes[index])),
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
rl.registerAction("note.list", {
|
|
199
|
+
access: "read",
|
|
200
|
+
description: "List every note block verbatim, including data_type, titles, inline data_content, and signed data_link. Does not download links.",
|
|
201
|
+
inputSchema: RecordingInput,
|
|
202
|
+
async execute(input, ctx) {
|
|
203
|
+
const { id } = input;
|
|
204
|
+
return {
|
|
205
|
+
recordingId: id,
|
|
206
|
+
notes: blocks(await recording(ctx, id), "note_list"),
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { AuthError, downloadResource, OAuthGrantSchema, } from "runline";
|
|
2
|
+
import * as t from "typebox";
|
|
3
|
+
import { credentialRuntime } from "../../_shared/credentialAdapter.js";
|
|
4
|
+
const BASE = "https://platform.plaud.ai/developer/api";
|
|
5
|
+
/** Public client identifier and fixed callback published in Plaud's official CLI. */
|
|
6
|
+
export const PLAUD_PUBLIC_CLIENT_ID = "client_f9e0b214-c11f-434b-8b95-c4497d1feb81";
|
|
7
|
+
export const PLAUD_REDIRECT_URI = "http://localhost:8199/auth/callback";
|
|
8
|
+
/** Third-party personal-recording API, NOT Plaud Embedded's partner-token API. */
|
|
9
|
+
export const PLAUD_OAUTH = {
|
|
10
|
+
id: "plaud.oauth2",
|
|
11
|
+
provider: "plaud",
|
|
12
|
+
authorization: { url: "https://web.plaud.ai/platform/oauth" },
|
|
13
|
+
exchange: {
|
|
14
|
+
url: `${BASE}/oauth/third-party/access-token`,
|
|
15
|
+
clientAuthentication: "client_id_basic",
|
|
16
|
+
grantType: null,
|
|
17
|
+
sendState: true,
|
|
18
|
+
requirePkce: true,
|
|
19
|
+
},
|
|
20
|
+
refresh: {
|
|
21
|
+
url: `${BASE}/oauth/third-party/access-token/refresh`,
|
|
22
|
+
clientAuthentication: "none",
|
|
23
|
+
grantType: null,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export const PLAUD_CREDENTIAL = {
|
|
27
|
+
id: "plaud",
|
|
28
|
+
methods: {
|
|
29
|
+
oauth2: {
|
|
30
|
+
schema: t.Object({ grant: t.Optional(OAuthGrantSchema) }, { additionalProperties: false }),
|
|
31
|
+
authentication: {
|
|
32
|
+
kind: "oauth2",
|
|
33
|
+
grantField: "grant",
|
|
34
|
+
definition: PLAUD_OAUTH,
|
|
35
|
+
renewal: "refresh",
|
|
36
|
+
},
|
|
37
|
+
targets: {
|
|
38
|
+
api: { baseUrl: `${BASE}/open/third-party/`, methods: ["GET"] },
|
|
39
|
+
},
|
|
40
|
+
probe: {
|
|
41
|
+
target: "api",
|
|
42
|
+
path: "users/current",
|
|
43
|
+
method: "GET",
|
|
44
|
+
acceptedStatuses: [200],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export function plaudRuntime(ctx) {
|
|
50
|
+
return credentialRuntime(ctx, PLAUD_CREDENTIAL, "oauth2", (config) => [
|
|
51
|
+
config.clientId,
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
function object(value) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
throw new AuthError("invalid_response");
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
export async function request(ctx, path) {
|
|
60
|
+
const { binding, transport } = plaudRuntime(ctx);
|
|
61
|
+
const response = await transport.request(binding, { target: "api", path });
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
throw new Error(`plaud: request failed (HTTP ${response.status})`);
|
|
64
|
+
try {
|
|
65
|
+
return object(await response.json());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new AuthError("invalid_response");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function recording(ctx, id) {
|
|
72
|
+
// Encoding protects separators/query injection; the transport also rejects traversal.
|
|
73
|
+
return request(ctx, `files/${encodeURIComponent(id)}`);
|
|
74
|
+
}
|
|
75
|
+
export async function list(ctx, page, pageSize) {
|
|
76
|
+
const result = await request(ctx, `files/?${new URLSearchParams({ page: String(page), page_size: String(pageSize) })}`);
|
|
77
|
+
if (!Array.isArray(result.data) || result.data.length > pageSize)
|
|
78
|
+
throw new AuthError("invalid_response");
|
|
79
|
+
const data = result.data.map(object);
|
|
80
|
+
return { ...result, data };
|
|
81
|
+
}
|
|
82
|
+
export function blocks(file, field) {
|
|
83
|
+
const value = file[field];
|
|
84
|
+
if (value === undefined || value === null)
|
|
85
|
+
return [];
|
|
86
|
+
if (!Array.isArray(value))
|
|
87
|
+
throw new AuthError("invalid_response");
|
|
88
|
+
return value.map(object);
|
|
89
|
+
}
|
|
90
|
+
/** Linked content is a separate, unauthenticated capability approved by host config. */
|
|
91
|
+
export async function blockContent(ctx, block) {
|
|
92
|
+
if (!block)
|
|
93
|
+
return { status: "unavailable", content: null };
|
|
94
|
+
if (typeof block.data_content === "string" && block.data_content.length)
|
|
95
|
+
return { status: "available", content: block.data_content };
|
|
96
|
+
const link = block.data_link;
|
|
97
|
+
if (typeof link !== "string" || !link)
|
|
98
|
+
return { status: "unavailable", content: null };
|
|
99
|
+
const origins = ctx.connection.config.contentOrigins ?? [];
|
|
100
|
+
if (!Array.isArray(origins) ||
|
|
101
|
+
origins.some((origin) => {
|
|
102
|
+
if (typeof origin !== "string")
|
|
103
|
+
return true;
|
|
104
|
+
try {
|
|
105
|
+
const url = new URL(origin);
|
|
106
|
+
return url.protocol !== "https:" || url.origin !== origin;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}))
|
|
112
|
+
throw new AuthError("invalid_credentials");
|
|
113
|
+
let origin;
|
|
114
|
+
try {
|
|
115
|
+
const url = new URL(link);
|
|
116
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash)
|
|
117
|
+
throw new Error();
|
|
118
|
+
origin = url.origin;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new AuthError("request_not_allowed");
|
|
122
|
+
}
|
|
123
|
+
if (!origins.includes(origin))
|
|
124
|
+
return {
|
|
125
|
+
status: "link_requires_approval",
|
|
126
|
+
content: null,
|
|
127
|
+
contentUrl: link,
|
|
128
|
+
};
|
|
129
|
+
const response = await downloadResource(link, {
|
|
130
|
+
allowedOrigins: [...origins],
|
|
131
|
+
fetch: globalThis.fetch,
|
|
132
|
+
maxResponseBytes: 8 * 1024 * 1024,
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok)
|
|
135
|
+
throw new Error(`plaud: content download failed (HTTP ${response.status})`);
|
|
136
|
+
const content = await response.text();
|
|
137
|
+
return {
|
|
138
|
+
status: content ? "available" : "unavailable",
|
|
139
|
+
content: content || null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function utcDay(value) {
|
|
143
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
144
|
+
return undefined;
|
|
145
|
+
const ms = Date.parse(`${value}T00:00:00.000Z`);
|
|
146
|
+
return Number.isFinite(ms) &&
|
|
147
|
+
new Date(ms).toISOString().slice(0, 10) === value
|
|
148
|
+
? ms
|
|
149
|
+
: undefined;
|
|
150
|
+
}
|
|
151
|
+
/** Timezone-less timestamps are UTC; invalid calendar days/times are not normalized. */
|
|
152
|
+
function timestamp(value) {
|
|
153
|
+
if (typeof value !== "string")
|
|
154
|
+
return undefined;
|
|
155
|
+
const match = /^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/i.exec(value.trim());
|
|
156
|
+
if (!match)
|
|
157
|
+
return undefined;
|
|
158
|
+
const [, day, hour, minute, second = "00", fraction = "", zone = "Z"] = match;
|
|
159
|
+
const midnight = utcDay(day);
|
|
160
|
+
if (midnight === undefined || hour === undefined)
|
|
161
|
+
return midnight;
|
|
162
|
+
if (Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59)
|
|
163
|
+
return undefined;
|
|
164
|
+
const result = Date.parse(`${day}T${hour}:${minute}:${second}${fraction}${zone}`);
|
|
165
|
+
return Number.isFinite(result) ? result : undefined;
|
|
166
|
+
}
|
|
167
|
+
export function dateBoundary(value, end) {
|
|
168
|
+
if (value === undefined)
|
|
169
|
+
return undefined;
|
|
170
|
+
const ms = utcDay(value);
|
|
171
|
+
if (ms === undefined)
|
|
172
|
+
throw new Error("plaud: invalid date; use YYYY-MM-DD");
|
|
173
|
+
return ms + (end ? 86_400_000 - 1 : 0);
|
|
174
|
+
}
|
|
175
|
+
export async function scan(ctx, options) {
|
|
176
|
+
if (options.from !== undefined &&
|
|
177
|
+
options.to !== undefined &&
|
|
178
|
+
options.from > options.to)
|
|
179
|
+
throw new Error("plaud: from must not be after to");
|
|
180
|
+
const recordings = [];
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const query = options.query?.toLowerCase();
|
|
183
|
+
let scanned = 0;
|
|
184
|
+
let pagesScanned = 0;
|
|
185
|
+
for (let page = 1; page <= options.maxPages; page++) {
|
|
186
|
+
const result = await list(ctx, page, 100);
|
|
187
|
+
pagesScanned++;
|
|
188
|
+
for (let index = 0; index < result.data.length; index++) {
|
|
189
|
+
const file = result.data[index];
|
|
190
|
+
scanned++;
|
|
191
|
+
if (typeof file.id !== "string" || !file.id)
|
|
192
|
+
throw new AuthError("invalid_response");
|
|
193
|
+
if (seen.has(file.id))
|
|
194
|
+
continue;
|
|
195
|
+
seen.add(file.id);
|
|
196
|
+
if (query &&
|
|
197
|
+
!(typeof file.name === "string" &&
|
|
198
|
+
file.name.toLowerCase().includes(query)))
|
|
199
|
+
continue;
|
|
200
|
+
if (options.from !== undefined || options.to !== undefined) {
|
|
201
|
+
const created = timestamp(file.created_at);
|
|
202
|
+
if (created === undefined ||
|
|
203
|
+
(options.from !== undefined && created < options.from) ||
|
|
204
|
+
(options.to !== undefined && created > options.to))
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
recordings.push(file);
|
|
208
|
+
if (recordings.length >= options.limit)
|
|
209
|
+
return {
|
|
210
|
+
recordings,
|
|
211
|
+
scanned,
|
|
212
|
+
pagesScanned,
|
|
213
|
+
truncated: index < result.data.length - 1 || result.data.length === 100,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (result.data.length < 100)
|
|
217
|
+
return { recordings, scanned, pagesScanned, truncated: false };
|
|
218
|
+
}
|
|
219
|
+
// Do not claim exhaustive results or assume provider ordering beyond the scanned pages.
|
|
220
|
+
return { recordings, scanned, pagesScanned, truncated: true };
|
|
221
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestOAuth2Token } from "runline";
|
|
1
2
|
export const DEFAULT_API_VERSION = "v59.0";
|
|
2
3
|
export function config(ctx) {
|
|
3
4
|
return ctx.connection.config;
|
|
@@ -37,40 +38,24 @@ export async function getSession(ctx) {
|
|
|
37
38
|
const loginUrl = validateInstanceUrl(requireString(c.loginUrl ?? c.instanceUrl, "loginUrl or instanceUrl"));
|
|
38
39
|
const clientId = requireString(c.clientId, "clientId");
|
|
39
40
|
const clientSecret = requireString(c.clientSecret, "clientSecret");
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
const tokens = await requestOAuth2Token({
|
|
42
|
+
url: `${loginUrl}/services/oauth2/token`,
|
|
43
|
+
clientAuthentication: "client_secret_post",
|
|
44
|
+
response: {
|
|
45
|
+
metadata: {
|
|
46
|
+
instanceUrl: "instance_url",
|
|
47
|
+
id: "id",
|
|
48
|
+
issuedAt: "issued_at",
|
|
49
|
+
},
|
|
49
50
|
},
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
const text = await res.text();
|
|
53
|
-
let data;
|
|
54
|
-
try {
|
|
55
|
-
data = text ? JSON.parse(text) : {};
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
data = { raw: text };
|
|
59
|
-
}
|
|
60
|
-
if (!res.ok) {
|
|
61
|
-
const err = typeof data.error === "string" ? data.error : res.status;
|
|
62
|
-
const description = typeof data.error_description === "string"
|
|
63
|
-
? `: ${data.error_description}`
|
|
64
|
-
: "";
|
|
65
|
-
throw new Error(`Salesforce token error ${err}${description}`);
|
|
66
|
-
}
|
|
51
|
+
}, { grant_type: "client_credentials" }, { clientId, clientSecret });
|
|
67
52
|
return {
|
|
68
|
-
instanceUrl: validateInstanceUrl(requireString(
|
|
69
|
-
accessToken:
|
|
70
|
-
tokenType:
|
|
71
|
-
scope:
|
|
72
|
-
id:
|
|
73
|
-
issuedAt:
|
|
53
|
+
instanceUrl: validateInstanceUrl(requireString(tokens.metadata?.instanceUrl, "token response instance_url")),
|
|
54
|
+
accessToken: tokens.accessToken,
|
|
55
|
+
tokenType: tokens.tokenType ?? "Bearer",
|
|
56
|
+
scope: tokens.scope,
|
|
57
|
+
id: tokens.metadata?.id,
|
|
58
|
+
issuedAt: tokens.metadata?.issuedAt,
|
|
74
59
|
};
|
|
75
60
|
}
|
|
76
61
|
export async function api(ctx, method, endpoint, body, qs, session = getSession(ctx)) {
|