@butinapp/sdk 0.1.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 +63 -0
- package/dist/data/builders.d.ts +111 -0
- package/dist/data/builders.js +103 -0
- package/dist/data/currency.d.ts +3 -0
- package/dist/data/currency.js +11 -0
- package/dist/data/dataset.d.ts +229 -0
- package/dist/data/dataset.js +70 -0
- package/dist/data/index.d.ts +13 -0
- package/dist/data/index.js +11 -0
- package/dist/data/result.d.ts +163 -0
- package/dist/data/result.js +171 -0
- package/dist/data/roles.d.ts +17 -0
- package/dist/data/roles.js +3 -0
- package/dist/data/series.d.ts +6 -0
- package/dist/data/series.js +5 -0
- package/dist/data/summary.d.ts +36 -0
- package/dist/data/summary.js +18 -0
- package/dist/data/view.d.ts +93 -0
- package/dist/data/view.js +74 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +14 -0
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.js +4 -0
- package/dist/integrations/stripe.d.ts +45 -0
- package/dist/integrations/stripe.js +102 -0
- package/dist/libs.d.ts +2 -0
- package/dist/libs.js +6 -0
- package/dist/plugin/auth.d.ts +49 -0
- package/dist/plugin/auth.js +1 -0
- package/dist/plugin/browser.d.ts +26 -0
- package/dist/plugin/browser.js +8 -0
- package/dist/plugin/capability.d.ts +54 -0
- package/dist/plugin/capability.js +19 -0
- package/dist/plugin/config.d.ts +61 -0
- package/dist/plugin/config.js +22 -0
- package/dist/plugin/documents.d.ts +5 -0
- package/dist/plugin/documents.js +1 -0
- package/dist/plugin/meta.d.ts +21 -0
- package/dist/plugin/meta.js +1 -0
- package/dist/plugin/plugin.d.ts +26 -0
- package/dist/plugin/plugin.js +1 -0
- package/dist/plugin/session.d.ts +33 -0
- package/dist/plugin/session.js +5 -0
- package/dist/plugin/transport.d.ts +42 -0
- package/dist/plugin/transport.js +1 -0
- package/dist/presets/apikeys.d.ts +13 -0
- package/dist/presets/apikeys.js +24 -0
- package/dist/presets/billing.d.ts +56 -0
- package/dist/presets/billing.js +209 -0
- package/dist/presets/blocks.d.ts +43 -0
- package/dist/presets/blocks.js +99 -0
- package/dist/presets/index.d.ts +38 -0
- package/dist/presets/index.js +22 -0
- package/dist/presets/members.d.ts +11 -0
- package/dist/presets/members.js +15 -0
- package/dist/presets/mtd-basis.d.ts +11 -0
- package/dist/presets/mtd-basis.js +16 -0
- package/dist/presets/usage.d.ts +17 -0
- package/dist/presets/usage.js +61 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +6 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/synthetic.d.ts +65 -0
- package/dist/testing/synthetic.js +259 -0
- package/dist/util/date.d.ts +14 -0
- package/dist/util/date.js +49 -0
- package/dist/util/fx.d.ts +2 -0
- package/dist/util/fx.js +7 -0
- package/dist/util/index.d.ts +6 -0
- package/dist/util/index.js +9 -0
- package/dist/util/money.d.ts +6 -0
- package/dist/util/money.js +35 -0
- package/dist/util/object.d.ts +2 -0
- package/dist/util/object.js +7 -0
- package/dist/util/text.d.ts +2 -0
- package/dist/util/text.js +16 -0
- package/package.json +70 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ButinClient } from '../plugin/transport.js';
|
|
2
|
+
export declare const STRIPE_PORTAL_VERSION = "2025-06-30.basil";
|
|
3
|
+
export interface StripePortalSession {
|
|
4
|
+
bps: string;
|
|
5
|
+
ek: string;
|
|
6
|
+
account: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const extractStripePortalTokens: (html: string) => StripePortalSession;
|
|
9
|
+
export declare const openStripePortal: (client: ButinClient, portalUrl: string) => Promise<StripePortalSession>;
|
|
10
|
+
export declare const openStripePortalViaRedirect: (client: ButinClient, redirectUrl: string) => Promise<StripePortalSession>;
|
|
11
|
+
export declare const fetchStripePortalResource: <T>(client: ButinClient, session: StripePortalSession, resource: string, params?: Record<string, number>) => Promise<T>;
|
|
12
|
+
export interface RawStripeLine {
|
|
13
|
+
amount?: number;
|
|
14
|
+
description?: string;
|
|
15
|
+
short_description?: string;
|
|
16
|
+
price_details?: {
|
|
17
|
+
product?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface RawStripeInvoice {
|
|
23
|
+
id?: string;
|
|
24
|
+
number?: string;
|
|
25
|
+
status?: string;
|
|
26
|
+
total?: number;
|
|
27
|
+
amount_due?: number;
|
|
28
|
+
amount_paid?: number;
|
|
29
|
+
currency?: string;
|
|
30
|
+
effective_at?: number;
|
|
31
|
+
finalized_at?: number;
|
|
32
|
+
created?: number;
|
|
33
|
+
due_date?: number;
|
|
34
|
+
hosted_invoice_url?: string;
|
|
35
|
+
invoice_pdf?: string;
|
|
36
|
+
lines?: {
|
|
37
|
+
data?: RawStripeLine[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface RawStripeInvoiceList {
|
|
41
|
+
data?: RawStripeInvoice[];
|
|
42
|
+
has_more?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare const stripePdfFileUrlEndpoint: (hosted?: string | null) => string | null;
|
|
45
|
+
export declare const fetchStripeHostedInvoicePdf: (client: ButinClient, hosted?: string | null) => Promise<Uint8Array>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Stripe billing mechanics shared by every plugin that proxies billing to Stripe. Two reusable shapes live
|
|
2
|
+
// here: the hosted billing-portal walk (services with no invoice API of their own — depot, novu, posthog) and
|
|
3
|
+
// the hosted-invoice → PDF download (services that expose only the Stripe-hosted invoice page — screenshotapi).
|
|
4
|
+
// Pure relative to a ButinClient: each helper takes the authed client and does the cross-origin Stripe calls,
|
|
5
|
+
// so a plugin only supplies how it ACQUIRES the one-time portal URL (a 302 from its own endpoint, a JSON
|
|
6
|
+
// field, or a value already in hand) and how it normalizes the result.
|
|
7
|
+
// ── hosted billing portal ────────────────────────────────────────────────────────────────
|
|
8
|
+
// The Stripe API version the hosted billing portal pins its ephemeral-key calls to. Stripe shifts response
|
|
9
|
+
// shapes across versions, so the portal-session calls send the same one the portal page does.
|
|
10
|
+
export const STRIPE_PORTAL_VERSION = '2025-06-30.basil';
|
|
11
|
+
const STRIPE_BILLING_ORIGIN = 'https://billing.stripe.com';
|
|
12
|
+
// Stripe's hosted portal page embeds the session id, an ephemeral key, and the account id in its bootstrap
|
|
13
|
+
// markup. The three tokens carry distinctive, stable prefixes, so a prefix regex survives surrounding-markup
|
|
14
|
+
// drift. Exported so the scrape is fixture-tested.
|
|
15
|
+
export const extractStripePortalTokens = (html) => {
|
|
16
|
+
const ek = html.match(/ek_live_[A-Za-z0-9_]+/)?.[0];
|
|
17
|
+
const bps = html.match(/bps_[A-Za-z0-9]+/)?.[0];
|
|
18
|
+
const account = html.match(/acct_[A-Za-z0-9]+/)?.[0];
|
|
19
|
+
if (!ek || !bps || !account) {
|
|
20
|
+
throw new Error('Stripe billing-portal session not found on the portal page (session expired?).');
|
|
21
|
+
}
|
|
22
|
+
return { bps, ek, account };
|
|
23
|
+
};
|
|
24
|
+
// Fetch a one-time Stripe portal-session page and scrape its tokens. `portalUrl` is the
|
|
25
|
+
// `billing.stripe.com/p/session/…` URL the plugin already holds (e.g. a JSON field). The page rides on
|
|
26
|
+
// Stripe's own ephemeral-key auth, so this hop carries neither the service's session cookie nor any Bearer.
|
|
27
|
+
export const openStripePortal = async (client, portalUrl) => {
|
|
28
|
+
if (!portalUrl.includes('/p/session/')) {
|
|
29
|
+
throw new Error('Stripe billing-portal URL is not a portal session (session expired?).');
|
|
30
|
+
}
|
|
31
|
+
const page = await client.request({
|
|
32
|
+
url: portalUrl,
|
|
33
|
+
responseType: 'text',
|
|
34
|
+
sendAuth: false,
|
|
35
|
+
sendCookie: false,
|
|
36
|
+
headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }
|
|
37
|
+
});
|
|
38
|
+
return extractStripePortalTokens(page.data);
|
|
39
|
+
};
|
|
40
|
+
// Open a Stripe portal session reached through a service endpoint that 302s to it (the common shape — the
|
|
41
|
+
// service mints the session server-side with its own secret key and hands back only the client redirect).
|
|
42
|
+
// The redirect hop keeps the service's session (cookie + any Bearer) to be authorized; the portal-page hop
|
|
43
|
+
// drops it. `maxRedirects: 0` reads the Location instead of chasing it cross-origin.
|
|
44
|
+
export const openStripePortalViaRedirect = async (client, redirectUrl) => {
|
|
45
|
+
const redirect = await client.request({ url: redirectUrl, maxRedirects: 0 });
|
|
46
|
+
const location = redirect.headers.location;
|
|
47
|
+
if (!location) {
|
|
48
|
+
throw new Error('Stripe billing-portal endpoint did not redirect to a portal session (session expired?).');
|
|
49
|
+
}
|
|
50
|
+
return openStripePortal(client, location);
|
|
51
|
+
};
|
|
52
|
+
// GET a portal-session resource (`invoices`, `subscriptions`, …), authorized by the ephemeral key — the same
|
|
53
|
+
// captcha-free calls the portal UI makes (hCaptcha only gates payment-method mutations). Cross-origin to
|
|
54
|
+
// Stripe, so it carries the Bearer/account/version headers and drops the service's session cookie.
|
|
55
|
+
export const fetchStripePortalResource = async (client, session, resource, params = {}) => {
|
|
56
|
+
const query = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString();
|
|
57
|
+
const res = await client.request({
|
|
58
|
+
url: `${STRIPE_BILLING_ORIGIN}/v1/billing_portal/sessions/${session.bps}/${resource}${query ? `?${query}` : ''}`,
|
|
59
|
+
sendAuth: false,
|
|
60
|
+
sendCookie: false,
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: 'application/json',
|
|
63
|
+
Authorization: `Bearer ${session.ek}`,
|
|
64
|
+
'stripe-account': session.account,
|
|
65
|
+
'stripe-version': STRIPE_PORTAL_VERSION,
|
|
66
|
+
Origin: STRIPE_BILLING_ORIGIN,
|
|
67
|
+
Referer: `${STRIPE_BILLING_ORIGIN}/`
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return res.data;
|
|
71
|
+
};
|
|
72
|
+
// ── hosted-invoice PDF download ────────────────────────────────────────────────────────────
|
|
73
|
+
// A Stripe hosted-invoice URL (https://invoice.stripe.com/i/<acct>/<liveId>?s=ap) serves an SPA shell, NOT a
|
|
74
|
+
// PDF. The real PDF is two hops away: GET invoicedata.stripe.com/invoice_pdf_file_url/<acct>/<liveId> for a
|
|
75
|
+
// JSON `{ file_url }` (a short-lived signed S3 link), then GET that. This derives the first-hop endpoint from
|
|
76
|
+
// the hosted URL (same acct + liveId, no query). Exported so the parse is fixture-tested.
|
|
77
|
+
const STRIPE_HOSTED_INVOICE_RE = /invoice\.stripe\.com\/i\/(acct_[^/]+)\/(live_[^/?]+)/;
|
|
78
|
+
export const stripePdfFileUrlEndpoint = (hosted) => {
|
|
79
|
+
const m = hosted ? STRIPE_HOSTED_INVOICE_RE.exec(hosted) : null;
|
|
80
|
+
return m ? `https://invoicedata.stripe.com/invoice_pdf_file_url/${m[1]}/${m[2]}` : null;
|
|
81
|
+
};
|
|
82
|
+
// Download the PDF behind a Stripe hosted-invoice URL via the two-hop file-url handoff — a drop-in `fetchFile`
|
|
83
|
+
// body for a `files` table whose rows carry the hosted URL. Both hops are public (the S3 link is signed), so
|
|
84
|
+
// neither carries the service's session cookie or Bearer.
|
|
85
|
+
export const fetchStripeHostedInvoicePdf = async (client, hosted) => {
|
|
86
|
+
const endpoint = stripePdfFileUrlEndpoint(hosted);
|
|
87
|
+
if (!endpoint) {
|
|
88
|
+
throw new Error('Stripe hosted-invoice URL is not in the expected invoice.stripe.com/i/<acct>/<live> shape.');
|
|
89
|
+
}
|
|
90
|
+
const meta = await client.request({ url: endpoint, sendAuth: false, sendCookie: false });
|
|
91
|
+
const fileUrl = meta.data?.file_url;
|
|
92
|
+
if (!fileUrl) {
|
|
93
|
+
throw new Error('Stripe invoice_pdf_file_url returned no PDF link (the hosted invoice link may have expired).');
|
|
94
|
+
}
|
|
95
|
+
const pdf = await client.request({
|
|
96
|
+
url: fileUrl,
|
|
97
|
+
responseType: 'arraybuffer',
|
|
98
|
+
sendAuth: false,
|
|
99
|
+
sendCookie: false
|
|
100
|
+
});
|
|
101
|
+
return new Uint8Array(pdf.data);
|
|
102
|
+
};
|
package/dist/libs.d.ts
ADDED
package/dist/libs.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Re-export of the vendored third-party utility libs. A plugin depends ONLY on `@butinapp/sdk`, so it reaches
|
|
2
|
+
// luxon + lodash through here instead of declaring (and version-pinning) those deps itself — the SDK owns them.
|
|
3
|
+
// Kept on its own subpath, NOT in `/main`, so lodash's broad named surface can't collide with the SDK's own
|
|
4
|
+
// exports. Tree-shaken at the consumer's bundle, so re-exporting the whole toolkit costs nothing unused.
|
|
5
|
+
export * from 'lodash-es';
|
|
6
|
+
export { DateTime, Duration, Interval } from 'luxon';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ButinClient } from './transport.js';
|
|
2
|
+
export type CredentialStore = {
|
|
3
|
+
get: (field?: string) => string | undefined;
|
|
4
|
+
set: (field: string, value: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export type AuthContext = {
|
|
7
|
+
client: ButinClient;
|
|
8
|
+
creds: CredentialStore;
|
|
9
|
+
config: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export type AuthAttachment = {
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
cookie?: string;
|
|
14
|
+
};
|
|
15
|
+
export type AuthResolveHook = (ctx: AuthContext) => Promise<AuthAttachment>;
|
|
16
|
+
type AuthBase = {
|
|
17
|
+
clearOnStatuses?: number[];
|
|
18
|
+
};
|
|
19
|
+
export type AuthStrategy = (AuthBase & {
|
|
20
|
+
kind: 'cookie';
|
|
21
|
+
}) | (AuthBase & {
|
|
22
|
+
kind: 'bearer-token';
|
|
23
|
+
tokenField?: string;
|
|
24
|
+
}) | (AuthBase & {
|
|
25
|
+
kind: 'external';
|
|
26
|
+
}) | (AuthBase & {
|
|
27
|
+
kind: 'api-key';
|
|
28
|
+
resolve?: AuthResolveHook;
|
|
29
|
+
}) | (AuthBase & {
|
|
30
|
+
kind: 'cookie-csrf';
|
|
31
|
+
resolve?: AuthResolveHook;
|
|
32
|
+
}) | (AuthBase & {
|
|
33
|
+
kind: 'minted-jwt';
|
|
34
|
+
resolve: AuthResolveHook;
|
|
35
|
+
}) | (AuthBase & {
|
|
36
|
+
kind: 'rotating-refresh';
|
|
37
|
+
resolve: AuthResolveHook;
|
|
38
|
+
}) | (AuthBase & {
|
|
39
|
+
kind: 'spa-bearer';
|
|
40
|
+
bootUrl: string;
|
|
41
|
+
authCaptureUrlPatterns: string[];
|
|
42
|
+
sessionStorageKeyPrefix?: string;
|
|
43
|
+
bootTimeoutMs?: number;
|
|
44
|
+
});
|
|
45
|
+
export type AuthKind = AuthStrategy['kind'];
|
|
46
|
+
export type SpaBearerAuth = Extract<AuthStrategy, {
|
|
47
|
+
kind: 'spa-bearer';
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type BrowserFetchInit = {
|
|
2
|
+
method?: string;
|
|
3
|
+
headers?: Record<string, string>;
|
|
4
|
+
body?: string;
|
|
5
|
+
};
|
|
6
|
+
export type BrowserContext = {
|
|
7
|
+
html: () => Promise<string>;
|
|
8
|
+
fetchText: (url: string, init?: BrowserFetchInit) => Promise<{
|
|
9
|
+
status: number;
|
|
10
|
+
text: string;
|
|
11
|
+
}>;
|
|
12
|
+
fetchBytes: (url: string, init?: BrowserFetchInit) => Promise<{
|
|
13
|
+
status: number;
|
|
14
|
+
bytes: Uint8Array;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export type BrowserPage = BrowserContext & {
|
|
18
|
+
navigate: (url: string) => Promise<void>;
|
|
19
|
+
loadFrame: (url: string) => Promise<BrowserContext>;
|
|
20
|
+
download: (url: string, init?: {
|
|
21
|
+
referer?: string;
|
|
22
|
+
}) => Promise<Uint8Array>;
|
|
23
|
+
};
|
|
24
|
+
export type BrowserSession = {
|
|
25
|
+
open: <T>(url: string, use: (page: BrowserPage) => Promise<T>) => Promise<T>;
|
|
26
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// A live, authenticated BROWSER session a collector can drive — for services whose session can't be
|
|
2
|
+
// replayed with a bare headless request. Some legacy backends keep a short-lived, server-side session that
|
|
3
|
+
// a real browser tab holds open but a one-shot `net.request` finds already torn down (it auto-logs-out).
|
|
4
|
+
// `BrowserSession.open` navigates an OFFSCREEN window — on the plugin's captured partition — to a URL (a
|
|
5
|
+
// real navigation: carries Sec-Fetch-*, runs the page's JS, keeps that session warm), hands the collector a
|
|
6
|
+
// `BrowserPage`, then tears the window down. Electron-backed in core; absent in embeds and off-Electron tests
|
|
7
|
+
// (so a collector that needs it should guard `if (!ctx.browser) …`).
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CapabilityResult } from '../data/result.js';
|
|
2
|
+
import { type SampleConfig, type SampleGenerator } from '../testing/synthetic.js';
|
|
3
|
+
import type { CredentialStore } from './auth.js';
|
|
4
|
+
import type { BrowserSession } from './browser.js';
|
|
5
|
+
import type { DocumentBytes } from './documents.js';
|
|
6
|
+
import type { ButinClient } from './transport.js';
|
|
7
|
+
export type CollectContext<TConfig = Record<string, unknown>> = {
|
|
8
|
+
client: ButinClient;
|
|
9
|
+
clientFor: (backendKey: string) => ButinClient;
|
|
10
|
+
creds: CredentialStore;
|
|
11
|
+
config: TConfig;
|
|
12
|
+
browser?: BrowserSession;
|
|
13
|
+
since?: string;
|
|
14
|
+
log: (message: string, extra?: Record<string, unknown>) => void;
|
|
15
|
+
};
|
|
16
|
+
export type IncrementalSpec = {
|
|
17
|
+
id: string;
|
|
18
|
+
timestamp: string;
|
|
19
|
+
window?: {
|
|
20
|
+
days: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type IncrementalCapability = IncrementalSpec & {
|
|
24
|
+
fetch: (ctx: CollectContext) => Promise<Record<string, unknown>[]>;
|
|
25
|
+
build: (rows: Record<string, unknown>[]) => CapabilityResult;
|
|
26
|
+
};
|
|
27
|
+
export type Capability<TConfig = Record<string, unknown>> = {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
collect: (ctx: CollectContext<TConfig>) => Promise<CapabilityResult>;
|
|
31
|
+
incremental?: IncrementalCapability;
|
|
32
|
+
sample?: (opts?: {
|
|
33
|
+
config?: SampleConfig;
|
|
34
|
+
seed?: string;
|
|
35
|
+
}) => CapabilityResult;
|
|
36
|
+
fetchFile?: (ctx: CollectContext<TConfig>, row: Record<string, unknown>) => Promise<DocumentBytes>;
|
|
37
|
+
};
|
|
38
|
+
type RowOf<TRaw> = TRaw extends readonly (infer R)[] ? R : never;
|
|
39
|
+
export declare const defineCapability: <TRaw, TConfig = Record<string, unknown>>(spec: {
|
|
40
|
+
id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
fetch: (ctx: CollectContext<TConfig>) => Promise<TRaw>;
|
|
43
|
+
build: (raw: TRaw) => CapabilityResult;
|
|
44
|
+
sample: SampleGenerator<TRaw>;
|
|
45
|
+
fetchFile?: (ctx: CollectContext<TConfig>, row: Record<string, unknown>) => Promise<DocumentBytes>;
|
|
46
|
+
incremental?: {
|
|
47
|
+
id: keyof RowOf<TRaw> & string;
|
|
48
|
+
timestamp: keyof RowOf<TRaw> & string;
|
|
49
|
+
window?: {
|
|
50
|
+
days: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}) => Capability<TConfig>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createSampleGen, resolveSampleConfig } from '../testing/synthetic.js';
|
|
2
|
+
export const defineCapability = (spec) => ({
|
|
3
|
+
id: spec.id,
|
|
4
|
+
label: spec.label,
|
|
5
|
+
collect: async (ctx) => spec.build(await spec.fetch(ctx)),
|
|
6
|
+
sample: (opts) => spec.build(spec.sample(createSampleGen(opts?.seed ?? spec.id), resolveSampleConfig(opts?.config))),
|
|
7
|
+
...(spec.fetchFile ? { fetchFile: spec.fetchFile } : {}),
|
|
8
|
+
...(spec.incremental
|
|
9
|
+
? {
|
|
10
|
+
incremental: {
|
|
11
|
+
id: spec.incremental.id,
|
|
12
|
+
timestamp: spec.incremental.timestamp,
|
|
13
|
+
...(spec.incremental.window ? { window: spec.incremental.window } : {}),
|
|
14
|
+
fetch: spec.fetch,
|
|
15
|
+
build: spec.build
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
: {})
|
|
19
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CollectContext } from './capability.js';
|
|
3
|
+
export type ConfigFieldKind = 'text' | 'secret' | 'select' | 'combobox';
|
|
4
|
+
export interface ConfigOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
recommended?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ConfigFieldCondition {
|
|
11
|
+
field: string;
|
|
12
|
+
equals: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ConfigField {
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
kind: ConfigFieldKind;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
help?: string;
|
|
21
|
+
options?: Array<{
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
}>;
|
|
25
|
+
showWhen?: ConfigFieldCondition;
|
|
26
|
+
loadOptions?: (ctx: CollectContext) => Promise<ConfigOption[]>;
|
|
27
|
+
}
|
|
28
|
+
export interface PluginConfigSchema<TValues = Record<string, unknown>> {
|
|
29
|
+
fields: ConfigField[];
|
|
30
|
+
readonly __values?: TValues;
|
|
31
|
+
}
|
|
32
|
+
type FieldShape = {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly required?: boolean;
|
|
35
|
+
readonly options?: readonly {
|
|
36
|
+
readonly value: string;
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
type FieldValue<F extends FieldShape> = F extends {
|
|
40
|
+
readonly options: readonly {
|
|
41
|
+
readonly value: infer V;
|
|
42
|
+
}[];
|
|
43
|
+
} ? V : string;
|
|
44
|
+
type RequiredKey<F extends readonly FieldShape[]> = Extract<F[number], {
|
|
45
|
+
readonly required: true;
|
|
46
|
+
}>['key'];
|
|
47
|
+
export type ConfigValues<F extends readonly FieldShape[]> = {
|
|
48
|
+
[K in F[number] as K['key'] extends RequiredKey<F> ? K['key'] : never]: FieldValue<K>;
|
|
49
|
+
} & {
|
|
50
|
+
[K in F[number] as K['key'] extends RequiredKey<F> ? never : K['key']]?: FieldValue<K>;
|
|
51
|
+
};
|
|
52
|
+
export declare const configValuesSchema: (fields: readonly ConfigField[]) => z.ZodType<Record<string, unknown>>;
|
|
53
|
+
type ConfigFieldInput = Omit<ConfigField, 'options'> & {
|
|
54
|
+
readonly options?: readonly {
|
|
55
|
+
readonly value: string;
|
|
56
|
+
readonly label: string;
|
|
57
|
+
}[];
|
|
58
|
+
};
|
|
59
|
+
export declare const defineConfigSchema: <const F extends readonly ConfigFieldInput[]>(fields: F) => PluginConfigSchema<ConfigValues<F>>;
|
|
60
|
+
export type ConfigOf<S> = S extends PluginConfigSchema<infer V> ? V : never;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Build a zod validator for stored config values from the field list. Used by core to validate (best-effort)
|
|
3
|
+
// what it reads off disk: a `select` value must be one of its options, every value must be a string. Kept
|
|
4
|
+
// LENIENT on presence (all-optional) — enforcing `required` is the form's job at save time, not the
|
|
5
|
+
// collector's at read time, so an unconfigured plugin doesn't spam validation errors.
|
|
6
|
+
export const configValuesSchema = (fields) => {
|
|
7
|
+
const shape = {};
|
|
8
|
+
for (const field of fields) {
|
|
9
|
+
const values = field.options?.map((o) => o.value);
|
|
10
|
+
const base = values && values.length > 0 ? z.enum(values) : z.string();
|
|
11
|
+
shape[field.key] = base.optional();
|
|
12
|
+
}
|
|
13
|
+
return z.object(shape).passthrough();
|
|
14
|
+
};
|
|
15
|
+
// THE recommended way to declare typed settings: pass the fields once and get back a config schema that carries
|
|
16
|
+
// the derived value type. `definePlugin` infers its `TConfig` from it (so `ctx.config` is typed with no generic
|
|
17
|
+
// and no per-collect annotation), and the runtime `fields` it produces is what the settings form renders.
|
|
18
|
+
// Name the value type with `ConfigOf`:
|
|
19
|
+
// const myConfig = defineConfigSchema([{ key: 'region', label: 'Region', kind: 'text', required: true }])
|
|
20
|
+
// type MyConfig = ConfigOf<typeof myConfig> // { region: string }
|
|
21
|
+
// export const myPlugin = definePlugin({ config: myConfig, capabilities: [...] }) // ctx.config: MyConfig
|
|
22
|
+
export const defineConfigSchema = (fields) => ({ fields: fields });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PluginCategory = 'finance' | 'cloud' | 'ai' | 'devtools' | 'productivity' | 'rental' | 'other';
|
|
2
|
+
export type TroubleshootingCause = 'session-not-captured' | 'session-expired' | 'config-missing' | 'config-invalid' | 'verification-required' | 'permission' | 'network' | 'data-invalid' | 'unknown';
|
|
3
|
+
export type TroubleshootingAction = 'retry' | 'reconnect' | 'edit-settings' | 'open-dashboard' | 'open-docs';
|
|
4
|
+
export type Troubleshooting = Partial<Record<TroubleshootingCause, {
|
|
5
|
+
hint: string;
|
|
6
|
+
docUrl?: string;
|
|
7
|
+
}>>;
|
|
8
|
+
export type PluginMeta = {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
vendor?: string;
|
|
12
|
+
category?: PluginCategory;
|
|
13
|
+
description?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
homepage?: string;
|
|
18
|
+
dashboardUrl?: string;
|
|
19
|
+
messages?: Record<string, Record<string, string>>;
|
|
20
|
+
troubleshooting?: Troubleshooting;
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CurrencyCode } from '../data/currency.js';
|
|
2
|
+
import type { AuthStrategy } from './auth.js';
|
|
3
|
+
import type { Capability, CollectContext } from './capability.js';
|
|
4
|
+
import type { PluginConfigSchema } from './config.js';
|
|
5
|
+
import type { PluginMeta } from './meta.js';
|
|
6
|
+
import type { SessionSource } from './session.js';
|
|
7
|
+
import type { ButinClient, TransportConfig } from './transport.js';
|
|
8
|
+
export type ButinPlugin<TConfig = Record<string, unknown>> = {
|
|
9
|
+
meta: PluginMeta;
|
|
10
|
+
reportingCurrency: CurrencyCode;
|
|
11
|
+
session?: SessionSource;
|
|
12
|
+
auth: AuthStrategy;
|
|
13
|
+
transport?: TransportConfig;
|
|
14
|
+
backends?: Record<string, PluginBackend>;
|
|
15
|
+
config?: PluginConfigSchema<TConfig>;
|
|
16
|
+
capabilities: Capability<TConfig>[];
|
|
17
|
+
probe: (ctx: CollectContext<TConfig>) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export type PluginBackend = {
|
|
20
|
+
transport?: TransportConfig;
|
|
21
|
+
auth: AuthStrategy;
|
|
22
|
+
session?: SessionSource;
|
|
23
|
+
label?: string;
|
|
24
|
+
probe?: (client: ButinClient) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
export declare const definePlugin: <TConfig = Record<string, unknown>>(plugin: ButinPlugin<TConfig>) => ButinPlugin<TConfig>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const definePlugin = (plugin) => plugin;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type LocalStorageToken = {
|
|
2
|
+
key?: string;
|
|
3
|
+
keyIncludes?: string[];
|
|
4
|
+
jsonPath?: string;
|
|
5
|
+
storeAs: string;
|
|
6
|
+
};
|
|
7
|
+
export type ManualField = {
|
|
8
|
+
label: string;
|
|
9
|
+
key: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type SessionSource = {
|
|
14
|
+
loginUrl: string;
|
|
15
|
+
partition?: string;
|
|
16
|
+
dashboardMarkers: string[];
|
|
17
|
+
cookieDomains: string[];
|
|
18
|
+
requiredCookie?: string;
|
|
19
|
+
localStorageTokens?: LocalStorageToken[];
|
|
20
|
+
clearCookiesBeforeCapture?: string[];
|
|
21
|
+
persistCookies?: boolean;
|
|
22
|
+
manualFields?: ManualField[];
|
|
23
|
+
manualCaptureOnly?: boolean;
|
|
24
|
+
captureFromUrl?: Array<{
|
|
25
|
+
pattern: string;
|
|
26
|
+
storeAs: string;
|
|
27
|
+
}>;
|
|
28
|
+
captureFromHeader?: Array<{
|
|
29
|
+
header: string;
|
|
30
|
+
storeAs: string;
|
|
31
|
+
on?: 'request' | 'response';
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Magic Login — declarative capture config. The core opens `loginUrl` in a persistent partition,
|
|
2
|
+
// you log in by hand (MFA, SSO, magic link — anything), and once the page settles on an
|
|
3
|
+
// authenticated marker the core grabs the session and stores it encrypted. The capture is declarative —
|
|
4
|
+
// every plugin describes it as data, not per-service code.
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type TransportEngine = 'node' | 'electron';
|
|
2
|
+
export type DownloadTransport = {
|
|
3
|
+
referer?: string;
|
|
4
|
+
concurrency?: number;
|
|
5
|
+
};
|
|
6
|
+
export type TransportConfig = {
|
|
7
|
+
engine?: TransportEngine;
|
|
8
|
+
requiresBrowserEngine?: boolean;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
userAgent?: string;
|
|
11
|
+
defaultHeaders?: Record<string, string>;
|
|
12
|
+
sendCookie?: boolean;
|
|
13
|
+
nativeBrowserHeaders?: boolean;
|
|
14
|
+
download?: DownloadTransport;
|
|
15
|
+
};
|
|
16
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
17
|
+
export type RequestOptions = {
|
|
18
|
+
url: string;
|
|
19
|
+
method?: HttpMethod;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
responseType?: 'json' | 'text' | 'arraybuffer';
|
|
23
|
+
referer?: string;
|
|
24
|
+
sendAuth?: boolean;
|
|
25
|
+
sendCookie?: boolean;
|
|
26
|
+
maxRedirects?: number;
|
|
27
|
+
cache?: boolean;
|
|
28
|
+
pace?: boolean;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
};
|
|
31
|
+
export type ButinResponse<T = unknown> = {
|
|
32
|
+
status: number;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
data: T;
|
|
35
|
+
};
|
|
36
|
+
export type ButinClient = {
|
|
37
|
+
request: <T = unknown>(opts: RequestOptions) => Promise<ButinResponse<T>>;
|
|
38
|
+
get: <T = unknown>(url: string, headers?: Record<string, string>) => Promise<T>;
|
|
39
|
+
post: <T = unknown>(url: string, body?: unknown, headers?: Record<string, string>) => Promise<T>;
|
|
40
|
+
graphql: <T = unknown>(url: string, query: string, variables?: Record<string, unknown>) => Promise<T>;
|
|
41
|
+
getText: (url: string, headers?: Record<string, string>) => Promise<string>;
|
|
42
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CapabilityResult } from '../data/result.js';
|
|
2
|
+
export interface ApiKeyInput {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
masked?: string;
|
|
6
|
+
createdAt?: string;
|
|
7
|
+
lastUsedAt?: string;
|
|
8
|
+
revoked?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ApiKeysInput {
|
|
11
|
+
keys: ApiKeyInput[];
|
|
12
|
+
}
|
|
13
|
+
export declare const apiKeysResult: (input: ApiKeysInput) => CapabilityResult;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { capabilityResult, table } from '../data/builders.js';
|
|
2
|
+
const day = (ts) => (ts ? ts.slice(0, 10) : null);
|
|
3
|
+
// The apiKeys preset: a single inventory table (name / key / created / last used / status). No summary —
|
|
4
|
+
// a key count isn't a meaningful cross-service rollup.
|
|
5
|
+
export const apiKeysResult = (input) => {
|
|
6
|
+
const keys = table({
|
|
7
|
+
id: 'keys',
|
|
8
|
+
columns: [
|
|
9
|
+
{ key: 'name', role: 'label', label: 'Name' },
|
|
10
|
+
{ key: 'masked', role: 'label', label: 'Key' },
|
|
11
|
+
{ key: 'createdAt', role: 'timestamp', label: 'Created' },
|
|
12
|
+
{ key: 'lastUsedAt', role: 'timestamp', label: 'Last used' },
|
|
13
|
+
{ key: 'status', role: 'status', label: 'Status' }
|
|
14
|
+
],
|
|
15
|
+
rows: input.keys.map((k) => ({
|
|
16
|
+
name: k.name ?? null,
|
|
17
|
+
masked: k.masked ?? null,
|
|
18
|
+
createdAt: day(k.createdAt),
|
|
19
|
+
lastUsedAt: day(k.lastUsedAt),
|
|
20
|
+
status: k.revoked ? 'revoked' : 'active'
|
|
21
|
+
}))
|
|
22
|
+
});
|
|
23
|
+
return capabilityResult({ sections: [keys.table({ title: 'API keys' })] });
|
|
24
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { BadgeTone, SemanticRole } from '../data/dataset.js';
|
|
2
|
+
import type { CapabilityResult } from '../data/result.js';
|
|
3
|
+
import type { MonthPoint } from '../data/series.js';
|
|
4
|
+
import { type TableFiles } from '../data/view.js';
|
|
5
|
+
import { type CreditsInput, type PaymentMethodInput } from './blocks.js';
|
|
6
|
+
import { type MtdBasis } from './mtd-basis.js';
|
|
7
|
+
export interface BillingInvoiceInput {
|
|
8
|
+
id?: string;
|
|
9
|
+
date?: string;
|
|
10
|
+
status: string;
|
|
11
|
+
amount: number;
|
|
12
|
+
hostedUrl?: string | null;
|
|
13
|
+
pdfUrl?: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface BillingInput {
|
|
16
|
+
currentMtd: number | null;
|
|
17
|
+
baseFee?: number | null;
|
|
18
|
+
meteredMtd?: number | null;
|
|
19
|
+
mtdBasis?: MtdBasis;
|
|
20
|
+
plan?: string;
|
|
21
|
+
currency?: string;
|
|
22
|
+
invoices: BillingInvoiceInput[];
|
|
23
|
+
statusTones?: Record<string, BadgeTone>;
|
|
24
|
+
credits?: CreditsInput;
|
|
25
|
+
paymentMethod?: PaymentMethodInput;
|
|
26
|
+
invoiceDownload?: TableFiles | true;
|
|
27
|
+
}
|
|
28
|
+
export declare const monthlySpend: (invoices: BillingInvoiceInput[]) => MonthPoint[];
|
|
29
|
+
export interface BillingStat {
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
role: SemanticRole;
|
|
33
|
+
value: unknown;
|
|
34
|
+
currency?: string;
|
|
35
|
+
badges?: Record<string, BadgeTone>;
|
|
36
|
+
max?: number;
|
|
37
|
+
unit?: string;
|
|
38
|
+
caption?: string;
|
|
39
|
+
tone?: 'positive' | 'negative' | 'muted';
|
|
40
|
+
}
|
|
41
|
+
export interface BillingSummaryInput {
|
|
42
|
+
currentMtd: number | null;
|
|
43
|
+
currentMtdLabel?: string;
|
|
44
|
+
currentMtdCaption?: string;
|
|
45
|
+
baseFee?: number | null;
|
|
46
|
+
meteredMtd?: number | null;
|
|
47
|
+
mtdBasis?: MtdBasis;
|
|
48
|
+
currency?: string;
|
|
49
|
+
plan?: string;
|
|
50
|
+
invoices: BillingInvoiceInput[];
|
|
51
|
+
stats?: BillingStat[];
|
|
52
|
+
monthlyTitle?: string;
|
|
53
|
+
showDelta?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export declare const billingSummaryResult: (input: BillingSummaryInput) => CapabilityResult;
|
|
56
|
+
export declare const billingResult: (input: BillingInput) => CapabilityResult;
|