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.
Files changed (69) hide show
  1. package/dist/auth/errors.d.ts +22 -0
  2. package/dist/auth/errors.js +25 -0
  3. package/dist/auth/oauth2.d.ts +21 -0
  4. package/dist/auth/oauth2.js +154 -0
  5. package/dist/auth/token.d.ts +11 -0
  6. package/dist/auth/token.js +231 -0
  7. package/dist/auth/types.d.ts +93 -0
  8. package/dist/auth/types.js +1 -0
  9. package/dist/commands/auth.js +26 -13
  10. package/dist/commands/connection.js +3 -6
  11. package/dist/commands/exec.js +7 -2
  12. package/dist/commands/init.js +9 -2
  13. package/dist/config/loader.d.ts +7 -13
  14. package/dist/config/loader.js +58 -88
  15. package/dist/config/store.d.ts +17 -0
  16. package/dist/config/store.js +85 -0
  17. package/dist/connections/file.d.ts +13 -0
  18. package/dist/connections/file.js +53 -0
  19. package/dist/connections/memory.d.ts +15 -0
  20. package/dist/connections/memory.js +70 -0
  21. package/dist/connections/types.d.ts +47 -0
  22. package/dist/connections/types.js +10 -0
  23. package/dist/core/engine.d.ts +5 -2
  24. package/dist/core/engine.js +48 -55
  25. package/dist/core/oauth.d.ts +20 -15
  26. package/dist/core/oauth.js +195 -98
  27. package/dist/credentials/http.d.ts +19 -0
  28. package/dist/credentials/http.js +109 -0
  29. package/dist/credentials/policy.d.ts +5 -0
  30. package/dist/credentials/policy.js +92 -0
  31. package/dist/credentials/registry.d.ts +31 -0
  32. package/dist/credentials/registry.js +171 -0
  33. package/dist/credentials/transport.d.ts +38 -0
  34. package/dist/credentials/transport.js +283 -0
  35. package/dist/credentials/types.d.ts +80 -0
  36. package/dist/credentials/types.js +1 -0
  37. package/dist/index.d.ts +13 -1
  38. package/dist/index.js +9 -1
  39. package/dist/plugin/types.d.ts +30 -17
  40. package/dist/plugins/_shared/credentialAdapter.js +88 -0
  41. package/dist/plugins/_shared/googleAuth.js +89 -103
  42. package/dist/plugins/_shared/googleCredentials.js +160 -0
  43. package/dist/plugins/_shared/microsoftAuth.js +81 -74
  44. package/dist/plugins/_shared/microsoftCredentials.js +90 -0
  45. package/dist/plugins/_shared/tokenRefresh.js +39 -0
  46. package/dist/plugins/bitwarden/src/index.js +13 -31
  47. package/dist/plugins/gmail/src/index.js +8 -44
  48. package/dist/plugins/googleAppsScript/src/index.js +3 -25
  49. package/dist/plugins/googleCalendar/src/index.js +3 -41
  50. package/dist/plugins/googleContacts/src/index.js +3 -36
  51. package/dist/plugins/googleDocs/src/index.js +5 -0
  52. package/dist/plugins/googleDocs/src/shared.js +2 -30
  53. package/dist/plugins/googleDrive/src/index.js +52 -98
  54. package/dist/plugins/googleSheets/src/index.js +3 -40
  55. package/dist/plugins/googleSlides/src/index.js +5 -39
  56. package/dist/plugins/googleTasks/src/index.js +3 -30
  57. package/dist/plugins/halopsa/src/index.js +16 -31
  58. package/dist/plugins/microsoftCalendar/src/index.js +2 -1
  59. package/dist/plugins/microsoftFiles/src/index.js +10 -24
  60. package/dist/plugins/microsoftMail/src/index.js +2 -1
  61. package/dist/plugins/paypal/src/index.js +14 -29
  62. package/dist/plugins/plaud/src/index.js +210 -0
  63. package/dist/plugins/plaud/src/shared.js +221 -0
  64. package/dist/plugins/salesforce/src/shared.js +17 -32
  65. package/dist/sdk.d.ts +13 -5
  66. package/dist/sdk.js +35 -29
  67. package/dist/utils/observer.d.ts +2 -0
  68. package/dist/utils/observer.js +11 -0
  69. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { graphRequest, microsoftAccessToken, microsoftSetupHelp, userBase, } from "../../_shared/microsoftAuth.js";
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
- const cfg = ctx.connection.config;
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
- const token = await microsoftAccessToken(ctx, NAME, SCOPES);
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 binaryFetch(ctx, "GET", `${driveBase(ctx)}/items/${input.id}/content`);
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 ${input.id} → ${res.status} ${await res.text()}`);
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 ${input.path} → ${res.status} ${await res.text()}`);
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
- let cachedToken = null;
2
- function getConn(ctx) {
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 getAccessToken(conn) {
11
- if (cachedToken && Date.now() < cachedToken.expiresAt)
12
- return cachedToken.token;
13
- const res = await fetch(`${conn.base}/v1/oauth2/token`, {
14
- method: "POST",
15
- headers: {
16
- Authorization: "Basic " + btoa(`${conn.clientId}:${conn.secret}`),
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
- if (!res.ok)
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(getConn(ctx), "POST", "/payments/payouts", {
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(getConn(ctx), "GET", `/payments/payouts/${p.payoutBatchId}`, undefined, qs));
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(getConn(ctx), "GET", `/payments/payouts-item/${payoutItemId}`);
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(getConn(ctx), "POST", `/payments/payouts-item/${payoutItemId}/cancel`);
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 body = new URLSearchParams();
41
- body.set("grant_type", "client_credentials");
42
- body.set("client_id", clientId);
43
- body.set("client_secret", clientSecret);
44
- const res = await fetch(`${loginUrl}/services/oauth2/token`, {
45
- method: "POST",
46
- headers: {
47
- Accept: "application/json",
48
- "Content-Type": "application/x-www-form-urlencoded",
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
- body,
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(data.instance_url, "token response instance_url")),
69
- accessToken: requireString(data.access_token, "token response access_token"),
70
- tokenType: typeof data.token_type === "string" ? data.token_type : "Bearer",
71
- scope: typeof data.scope === "string" ? data.scope : undefined,
72
- id: typeof data.id === "string" ? data.id : undefined,
73
- issuedAt: typeof data.issued_at === "string" ? data.issued_at : undefined,
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)) {