Skip to content

Commit 9ddd561

Browse files
BaiqingLAndy Lyu
andauthored
feat(latchshot): expose paid-plan continuation links (oomol-lab#155)
## Summary - pass through the three stable paid-plan continuation URLs returned by Latchshot's `GET /v1/usage` - publish those URLs in the `get_usage` output schema - state explicitly that `get_usage` never initiates payment or an upgrade - reject missing or malformed continuation fields instead of returning an output that violates the action schema ## Why I maintain Latchshot. Its authenticated usage response now includes links to the public plan comparison, the human paid-plan request form, and the authenticated request API documentation. The accepted Open Connector provider currently drops those fields. This keeps commercial continuation discoverable to an Open Connector user while preserving the existing boundary: `get_usage` is read-only, and the provider still has no payment or plan-changing action. ## Scope and security - no new action, endpoint, redirect, credential behavior, or egress destination - the provider still calls only the fixed `https://latchshot.fly.dev` origin - no payment information is read or stored - output remains bounded JSON from the existing usage request ## Validation - `npm run fix-check` - `npm run generate:catalog` — 1,093 apps / 11,177 actions - focused Latchshot tests: 10/10 passed - full suite: 700/700 passed across 76 files - `git diff --check` Co-authored-by: Andy Lyu <andy@example.com>
1 parent f975e21 commit 9ddd561

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

src/providers/latchshot/actions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,20 @@ const upgradeRequestSchema = s.object(
149149
{ required: ["id", "keyId", "requestedPlan", "note", "status", "createdAt", "updatedAt"] },
150150
);
151151

152+
const usageLinksSchema = s.requiredObject("Owner-managed paid-plan continuation links.", {
153+
plans: s.url("The public Latchshot plan comparison."),
154+
requestPaidPlan: s.url("The human paid-plan request form."),
155+
requestPaidPlanDocs: s.url("The authenticated paid-plan request API documentation."),
156+
});
157+
152158
const usageOutputSchema = s.requiredObject("The current Latchshot plan and quota snapshot.", {
153159
customer: s.requiredObject("The display identity attached to the API key.", {
154160
name: s.nonEmptyString("The display name attached to the API key."),
155161
plan: planSchema,
156162
}),
157163
usage: usageSchema,
158164
upgradeRequest: s.nullable(upgradeRequestSchema),
165+
links: usageLinksSchema,
159166
});
160167

161168
export const latchshotActions: ActionDefinition[] = [
@@ -168,7 +175,8 @@ export const latchshotActions: ActionDefinition[] = [
168175
}),
169176
defineProviderAction(service, {
170177
name: "get_usage",
171-
description: "Read the current Latchshot plan, successful-render quota, reset time, and upgrade-request status.",
178+
description:
179+
"Read the current Latchshot plan, successful-render quota, reset time, upgrade-request status, and owner-managed paid-plan links. This action never initiates payment or an upgrade.",
172180
inputSchema: s.object("No input is required to read usage for the configured API key.", {}),
173181
outputSchema: usageOutputSchema,
174182
}),

src/providers/latchshot/executors.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,18 @@ describe("Latchshot provider", () => {
202202
createdAt: "2026-07-18T00:00:00.000Z",
203203
updatedAt: "2026-07-19T00:00:00.000Z",
204204
},
205+
links: usageLinks(),
206+
});
207+
});
208+
209+
it("rejects a usage response without owner-managed continuation links", async () => {
210+
const payload = usagePayload();
211+
delete payload.links;
212+
const context = createContext([], Response.json(payload));
213+
214+
await expect(latchshotActionHandlers.get_usage!({}, context)).rejects.toMatchObject({
215+
status: 502,
216+
message: "Latchshot usage response is missing continuation links.",
205217
});
206218
});
207219

@@ -278,6 +290,7 @@ function usagePayload(): Record<string, unknown> {
278290
updatedAt: null,
279291
},
280292
upgradeRequest: null,
293+
links: usageLinks(),
281294
};
282295
}
283296

@@ -307,5 +320,14 @@ function forwardCompatibleUsagePayload(): Record<string, unknown> {
307320
createdAt: "2026-07-18T00:00:00.000Z",
308321
updatedAt: "2026-07-19T00:00:00.000Z",
309322
},
323+
links: usageLinks(),
324+
};
325+
}
326+
327+
function usageLinks(): Record<string, string> {
328+
return {
329+
plans: "https://latchshot.fly.dev/#pricing",
330+
requestPaidPlan: "https://latchshot.fly.dev/#upgrade",
331+
requestPaidPlanDocs: "https://latchshot.fly.dev/docs.md#request-a-paid-plan",
310332
};
311333
}

src/providers/latchshot/executors.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ function normalizeUsagePayload(payload: unknown): {
236236
customer: { name: string; plan: string };
237237
usage: Record<string, unknown>;
238238
upgradeRequest: Record<string, unknown> | null;
239+
links: Record<string, string>;
239240
} {
240241
const record = requireObject(payload, "Latchshot usage response must be an object.");
241242
const customer = requireObject(record.customer, "Latchshot usage response is missing customer data.");
@@ -261,6 +262,16 @@ function normalizeUsagePayload(payload: unknown): {
261262
usage.updatedAt === null ? null : requiredString(usage.updatedAt, "usage.updatedAt", invalidResponseError),
262263
},
263264
upgradeRequest: normalizeUpgradeRequest(record.upgradeRequest),
265+
links: normalizeUsageLinks(record.links),
266+
};
267+
}
268+
269+
function normalizeUsageLinks(value: unknown): Record<string, string> {
270+
const links = requireObject(value, "Latchshot usage response is missing continuation links.");
271+
return {
272+
plans: requiredString(links.plans, "links.plans", invalidResponseError),
273+
requestPaidPlan: requiredString(links.requestPaidPlan, "links.requestPaidPlan", invalidResponseError),
274+
requestPaidPlanDocs: requiredString(links.requestPaidPlanDocs, "links.requestPaidPlanDocs", invalidResponseError),
264275
};
265276
}
266277

0 commit comments

Comments
 (0)