@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/data/builders.d.ts +111 -0
  4. package/dist/data/builders.js +103 -0
  5. package/dist/data/currency.d.ts +3 -0
  6. package/dist/data/currency.js +11 -0
  7. package/dist/data/dataset.d.ts +229 -0
  8. package/dist/data/dataset.js +70 -0
  9. package/dist/data/index.d.ts +13 -0
  10. package/dist/data/index.js +11 -0
  11. package/dist/data/result.d.ts +163 -0
  12. package/dist/data/result.js +171 -0
  13. package/dist/data/roles.d.ts +17 -0
  14. package/dist/data/roles.js +3 -0
  15. package/dist/data/series.d.ts +6 -0
  16. package/dist/data/series.js +5 -0
  17. package/dist/data/summary.d.ts +36 -0
  18. package/dist/data/summary.js +18 -0
  19. package/dist/data/view.d.ts +93 -0
  20. package/dist/data/view.js +74 -0
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.js +14 -0
  23. package/dist/integrations/index.d.ts +1 -0
  24. package/dist/integrations/index.js +4 -0
  25. package/dist/integrations/stripe.d.ts +45 -0
  26. package/dist/integrations/stripe.js +102 -0
  27. package/dist/libs.d.ts +2 -0
  28. package/dist/libs.js +6 -0
  29. package/dist/plugin/auth.d.ts +49 -0
  30. package/dist/plugin/auth.js +1 -0
  31. package/dist/plugin/browser.d.ts +26 -0
  32. package/dist/plugin/browser.js +8 -0
  33. package/dist/plugin/capability.d.ts +54 -0
  34. package/dist/plugin/capability.js +19 -0
  35. package/dist/plugin/config.d.ts +61 -0
  36. package/dist/plugin/config.js +22 -0
  37. package/dist/plugin/documents.d.ts +5 -0
  38. package/dist/plugin/documents.js +1 -0
  39. package/dist/plugin/meta.d.ts +21 -0
  40. package/dist/plugin/meta.js +1 -0
  41. package/dist/plugin/plugin.d.ts +26 -0
  42. package/dist/plugin/plugin.js +1 -0
  43. package/dist/plugin/session.d.ts +33 -0
  44. package/dist/plugin/session.js +5 -0
  45. package/dist/plugin/transport.d.ts +42 -0
  46. package/dist/plugin/transport.js +1 -0
  47. package/dist/presets/apikeys.d.ts +13 -0
  48. package/dist/presets/apikeys.js +24 -0
  49. package/dist/presets/billing.d.ts +56 -0
  50. package/dist/presets/billing.js +209 -0
  51. package/dist/presets/blocks.d.ts +43 -0
  52. package/dist/presets/blocks.js +99 -0
  53. package/dist/presets/index.d.ts +38 -0
  54. package/dist/presets/index.js +22 -0
  55. package/dist/presets/members.d.ts +11 -0
  56. package/dist/presets/members.js +15 -0
  57. package/dist/presets/mtd-basis.d.ts +11 -0
  58. package/dist/presets/mtd-basis.js +16 -0
  59. package/dist/presets/usage.d.ts +17 -0
  60. package/dist/presets/usage.js +61 -0
  61. package/dist/schema.d.ts +2 -0
  62. package/dist/schema.js +6 -0
  63. package/dist/testing/index.d.ts +2 -0
  64. package/dist/testing/index.js +6 -0
  65. package/dist/testing/synthetic.d.ts +65 -0
  66. package/dist/testing/synthetic.js +259 -0
  67. package/dist/util/date.d.ts +14 -0
  68. package/dist/util/date.js +49 -0
  69. package/dist/util/fx.d.ts +2 -0
  70. package/dist/util/fx.js +7 -0
  71. package/dist/util/index.d.ts +6 -0
  72. package/dist/util/index.js +9 -0
  73. package/dist/util/money.d.ts +6 -0
  74. package/dist/util/money.js +35 -0
  75. package/dist/util/object.d.ts +2 -0
  76. package/dist/util/object.js +7 -0
  77. package/dist/util/text.d.ts +2 -0
  78. package/dist/util/text.js +16 -0
  79. package/package.json +70 -0
@@ -0,0 +1,163 @@
1
+ import { z } from 'zod';
2
+ export declare const CapabilityResultSchema: z.ZodObject<{
3
+ datasets: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
4
+ id: z.ZodString;
5
+ shape: z.ZodLiteral<"table">;
6
+ columns: z.ZodArray<z.ZodObject<{
7
+ key: z.ZodString;
8
+ label: z.ZodOptional<z.ZodString>;
9
+ role: z.ZodEnum<{
10
+ category: "category";
11
+ count: "count";
12
+ identifier: "identifier";
13
+ label: "label";
14
+ money: "money";
15
+ percent: "percent";
16
+ status: "status";
17
+ text: "text";
18
+ timestamp: "timestamp";
19
+ url: "url";
20
+ }>;
21
+ currency: z.ZodOptional<z.ZodString>;
22
+ badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
23
+ danger: "danger";
24
+ info: "info";
25
+ neutral: "neutral";
26
+ success: "success";
27
+ warning: "warning";
28
+ }>>>;
29
+ accrual: z.ZodOptional<z.ZodEnum<{
30
+ cumulative: "cumulative";
31
+ incremental: "incremental";
32
+ }>>;
33
+ resetPeriod: z.ZodOptional<z.ZodEnum<{
34
+ monthly: "monthly";
35
+ none: "none";
36
+ }>>;
37
+ truncate: z.ZodOptional<z.ZodBoolean>;
38
+ hidden: z.ZodOptional<z.ZodBoolean>;
39
+ }, z.core.$strip>>;
40
+ rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
41
+ key: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
42
+ }, z.core.$strip>, z.ZodObject<{
43
+ id: z.ZodString;
44
+ shape: z.ZodLiteral<"record">;
45
+ fields: z.ZodArray<z.ZodObject<{
46
+ key: z.ZodString;
47
+ label: z.ZodOptional<z.ZodString>;
48
+ role: z.ZodEnum<{
49
+ category: "category";
50
+ count: "count";
51
+ identifier: "identifier";
52
+ label: "label";
53
+ money: "money";
54
+ percent: "percent";
55
+ status: "status";
56
+ text: "text";
57
+ timestamp: "timestamp";
58
+ url: "url";
59
+ }>;
60
+ currency: z.ZodOptional<z.ZodString>;
61
+ badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
62
+ danger: "danger";
63
+ info: "info";
64
+ neutral: "neutral";
65
+ success: "success";
66
+ warning: "warning";
67
+ }>>>;
68
+ accrual: z.ZodOptional<z.ZodEnum<{
69
+ cumulative: "cumulative";
70
+ incremental: "incremental";
71
+ }>>;
72
+ resetPeriod: z.ZodOptional<z.ZodEnum<{
73
+ monthly: "monthly";
74
+ none: "none";
75
+ }>>;
76
+ truncate: z.ZodOptional<z.ZodBoolean>;
77
+ hidden: z.ZodOptional<z.ZodBoolean>;
78
+ }, z.core.$strip>>;
79
+ value: z.ZodRecord<z.ZodString, z.ZodUnknown>;
80
+ }, z.core.$strip>], "shape">>;
81
+ views: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
82
+ type: z.ZodLiteral<"stat">;
83
+ dataset: z.ZodString;
84
+ fields: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
85
+ key: z.ZodString;
86
+ max: z.ZodOptional<z.ZodNumber>;
87
+ unit: z.ZodOptional<z.ZodString>;
88
+ caption: z.ZodOptional<z.ZodString>;
89
+ tone: z.ZodOptional<z.ZodEnum<{
90
+ muted: "muted";
91
+ negative: "negative";
92
+ positive: "positive";
93
+ }>>;
94
+ }, z.core.$strip>]>>>;
95
+ title: z.ZodOptional<z.ZodString>;
96
+ }, z.core.$strip>, z.ZodObject<{
97
+ type: z.ZodLiteral<"timeseries">;
98
+ dataset: z.ZodString;
99
+ x: z.ZodString;
100
+ y: z.ZodString;
101
+ granularity: z.ZodOptional<z.ZodEnum<{
102
+ daily: "daily";
103
+ monthly: "monthly";
104
+ }>>;
105
+ stackBy: z.ZodOptional<z.ZodString>;
106
+ title: z.ZodOptional<z.ZodString>;
107
+ }, z.core.$strip>, z.ZodObject<{
108
+ type: z.ZodLiteral<"table">;
109
+ dataset: z.ZodString;
110
+ columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
111
+ title: z.ZodOptional<z.ZodString>;
112
+ groupBy: z.ZodOptional<z.ZodString>;
113
+ files: z.ZodOptional<z.ZodObject<{
114
+ name: z.ZodOptional<z.ZodString>;
115
+ source: z.ZodUnion<readonly [z.ZodObject<{
116
+ url: z.ZodString;
117
+ }, z.core.$strip>, z.ZodObject<{
118
+ fetch: z.ZodLiteral<true>;
119
+ }, z.core.$strip>]>;
120
+ ext: z.ZodOptional<z.ZodString>;
121
+ category: z.ZodOptional<z.ZodString>;
122
+ folder: z.ZodOptional<z.ZodString>;
123
+ }, z.core.$strip>>;
124
+ detail: z.ZodOptional<z.ZodObject<{
125
+ dataset: z.ZodString;
126
+ on: z.ZodString;
127
+ }, z.core.$strip>>;
128
+ }, z.core.$strip>, z.ZodObject<{
129
+ type: z.ZodLiteral<"keyvalue">;
130
+ dataset: z.ZodString;
131
+ title: z.ZodOptional<z.ZodString>;
132
+ }, z.core.$strip>], "type">>>;
133
+ summaries: z.ZodOptional<z.ZodArray<z.ZodObject<{
134
+ section: z.ZodEnum<{
135
+ balance: "balance";
136
+ other: "other";
137
+ spend: "spend";
138
+ }>;
139
+ label: z.ZodString;
140
+ value: z.ZodNumber;
141
+ role: z.ZodEnum<{
142
+ count: "count";
143
+ money: "money";
144
+ percent: "percent";
145
+ }>;
146
+ currency: z.ZodOptional<z.ZodString>;
147
+ basis: z.ZodOptional<z.ZodEnum<{
148
+ accrued: "accrued";
149
+ flat: "flat";
150
+ invoiced: "invoiced";
151
+ lastInvoice: "lastInvoice";
152
+ upcoming: "upcoming";
153
+ }>>;
154
+ spark: z.ZodOptional<z.ZodObject<{
155
+ dataset: z.ZodString;
156
+ x: z.ZodString;
157
+ y: z.ZodString;
158
+ }, z.core.$strip>>;
159
+ headline: z.ZodOptional<z.ZodBoolean>;
160
+ }, z.core.$strip>>>;
161
+ }, z.core.$strip>;
162
+ export type CapabilityResult = z.infer<typeof CapabilityResultSchema>;
163
+ export declare const validateCapabilityResult: (result: unknown) => string[];
@@ -0,0 +1,171 @@
1
+ import { z } from 'zod';
2
+ import { DatasetSchema } from './dataset.js';
3
+ import { SummarySchema } from './summary.js';
4
+ import { ViewSchema } from './view.js';
5
+ // collect() returns this. views/summaries omitted → a preset supplies defaults (see presets/). Each entry in
6
+ // `summaries` headlines one metric tagged by `section` (spend·balance·other); at most one `spend` summary per
7
+ // result (it's the only section that sums across services).
8
+ export const CapabilityResultSchema = z.object({
9
+ datasets: z.array(DatasetSchema),
10
+ views: z.array(ViewSchema).optional(),
11
+ summaries: z.array(SummarySchema).optional()
12
+ });
13
+ const roleOf = (ds, key) => {
14
+ const cols = ds.shape === 'table' ? ds.columns : ds.fields;
15
+ return cols.find((c) => c.key === key)?.role;
16
+ };
17
+ // Whether `key` is addressable on a table's rows: a declared column, or a hidden row-identity field carried on
18
+ // every row (the same rule the accumulation `key` check uses). An empty table can't disprove a hidden field.
19
+ const isFieldOf = (ds, key) => {
20
+ if (ds.shape !== 'table') {
21
+ return false;
22
+ }
23
+ if (ds.columns.some((c) => c.key === key)) {
24
+ return true;
25
+ }
26
+ return ds.rows.length > 0 && ds.rows.every((r) => key in r);
27
+ };
28
+ // Referential checks zod can't express ergonomically: refs resolve, view shape fits dataset shape,
29
+ // chart x/y point at compatible roles.
30
+ const crossRefErrors = (result) => {
31
+ const errors = [];
32
+ const byId = new Map();
33
+ for (const ds of result.datasets) {
34
+ if (byId.has(ds.id)) {
35
+ // keep the FIRST dataset for an id so later shape checks don't run against a clashing duplicate
36
+ errors.push(`duplicate dataset id '${ds.id}'`);
37
+ continue;
38
+ }
39
+ byId.set(ds.id, ds);
40
+ }
41
+ // Accumulation contract: a `key` must name real column(s); a cumulative column needs a key to hang its
42
+ // per-row daily series on.
43
+ for (const ds of result.datasets) {
44
+ if (ds.shape !== 'table') {
45
+ continue;
46
+ }
47
+ const colKeys = new Set(ds.columns.map((c) => c.key));
48
+ const keyParts = ds.key === undefined ? [] : Array.isArray(ds.key) ? ds.key : [ds.key];
49
+ // A key part is a column or a hidden row-identity field (members/invoice id). Only flag a part that is
50
+ // neither — and only when there are rows to judge against (an empty table can't disprove a hidden field).
51
+ for (const k of keyParts) {
52
+ if (!colKeys.has(k) && ds.rows.length > 0 && !ds.rows.every((r) => k in r)) {
53
+ errors.push(`key '${k}' is not a column or row field of '${ds.id}'`);
54
+ }
55
+ }
56
+ if (keyParts.length === 0) {
57
+ for (const c of ds.columns) {
58
+ if (c.accrual === 'cumulative') {
59
+ errors.push(`cumulative column '${c.key}' needs a keyed table (table '${ds.id}' is unkeyed)`);
60
+ }
61
+ }
62
+ }
63
+ // Every money value must name a currency (resolveCurrencies fills the plugin default first; an
64
+ // explicit per-value currency overrides it). A money value with no currency is unrenderable + unrollable.
65
+ for (const c of ds.columns) {
66
+ if (c.role === 'money' && !c.currency) {
67
+ errors.push(`money column '${c.key}' in '${ds.id}' has no currency`);
68
+ }
69
+ }
70
+ }
71
+ // Money-currency rule applies to record fields too (record datasets are skipped by the table-only loop above).
72
+ for (const ds of result.datasets) {
73
+ if (ds.shape !== 'record') {
74
+ continue;
75
+ }
76
+ for (const c of ds.fields) {
77
+ if (c.role === 'money' && !c.currency) {
78
+ errors.push(`money column '${c.key}' in '${ds.id}' has no currency`);
79
+ }
80
+ }
81
+ }
82
+ const need = (ref, where) => {
83
+ const ds = byId.get(ref);
84
+ if (!ds) {
85
+ errors.push(`${where} references unknown dataset '${ref}'`);
86
+ }
87
+ return ds;
88
+ };
89
+ const requireSpark = (ds, x, y, where) => {
90
+ if (roleOf(ds, x) !== 'timestamp') {
91
+ errors.push(`${where} x '${x}' must be a timestamp column`);
92
+ }
93
+ const yRole = roleOf(ds, y);
94
+ if (yRole !== 'money' && yRole !== 'count') {
95
+ errors.push(`${where} y '${y}' must be a money|count column`);
96
+ }
97
+ };
98
+ for (const v of result.views ?? []) {
99
+ const ds = need(v.dataset, `view '${v.type}'`);
100
+ if (!ds) {
101
+ continue;
102
+ }
103
+ if (v.type === 'timeseries') {
104
+ requireSpark(ds, v.x, v.y, 'timeseries');
105
+ }
106
+ if ((v.type === 'stat' || v.type === 'keyvalue') && ds.shape !== 'record') {
107
+ errors.push(`view '${v.type}' needs a record dataset; '${ds.id}' is a ${ds.shape}`);
108
+ }
109
+ if (v.type === 'table' && ds.shape !== 'table') {
110
+ errors.push(`view 'table' needs a table dataset; '${ds.id}' is a ${ds.shape}`);
111
+ }
112
+ // groupBy must point at a real column; the files source url / name / folder are row fields (may be hidden,
113
+ // not necessarily columns), so they aren't checked here — the "files needs a byte source" rule lives in
114
+ // core's download path (the only place the capability's fetchFile is visible).
115
+ if (v.type === 'table' && v.groupBy && roleOf(ds, v.groupBy) === undefined) {
116
+ errors.push(`table groupBy '${v.groupBy}' is not a column of '${ds.id}'`);
117
+ }
118
+ // A row-detail binding: the child resolves to a table, and the join field is a field (column or hidden row
119
+ // field) of BOTH the parent and the child — else the nested rows can't be matched to their parent.
120
+ if (v.type === 'table' && v.detail) {
121
+ const child = need(v.detail.dataset, `table detail`);
122
+ if (child && child.shape !== 'table') {
123
+ errors.push(`table detail '${v.detail.dataset}' must be a table dataset; it is a ${child.shape}`);
124
+ }
125
+ if (child && child.shape === 'table') {
126
+ for (const [where, target] of [
127
+ ['parent', ds],
128
+ ['child', child]
129
+ ]) {
130
+ if (!isFieldOf(target, v.detail.on)) {
131
+ errors.push(`table detail 'on' field '${v.detail.on}' is not a field of the ${where} table '${target.id}'`);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ let spendCount = 0;
138
+ for (const s of result.summaries ?? []) {
139
+ if (s.section === 'spend') {
140
+ spendCount += 1;
141
+ }
142
+ if (s.role === 'money' && !s.currency) {
143
+ errors.push(`money summary '${s.label}' has no currency`);
144
+ }
145
+ // A spend summary headlines the Overview's monthly chart + previous-month total, both read off its spark's
146
+ // monthly series. With no spark it silently contributes nothing there — require one (presence, not rows, so a
147
+ // brand-new account with an empty monthly table is still fine).
148
+ if (s.section === 'spend' && !s.spark) {
149
+ errors.push(`spend summary '${s.label}' needs a spark (the monthly series the Overview reads)`);
150
+ }
151
+ if (s.spark) {
152
+ const ds = need(s.spark.dataset, `summary '${s.label}' spark`);
153
+ if (ds) {
154
+ requireSpark(ds, s.spark.x, s.spark.y, `summary '${s.label}' spark`);
155
+ }
156
+ }
157
+ }
158
+ if (spendCount > 1) {
159
+ errors.push('more than one spend summary in a result');
160
+ }
161
+ return errors;
162
+ };
163
+ // Returns a list of human-readable contract errors; [] = valid. Structural errors come from zod,
164
+ // referential errors from crossRefErrors. Distinct from a collect() fetch failure.
165
+ export const validateCapabilityResult = (result) => {
166
+ const parsed = CapabilityResultSchema.safeParse(result);
167
+ if (!parsed.success) {
168
+ return parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`);
169
+ }
170
+ return crossRefErrors(parsed.data);
171
+ };
@@ -0,0 +1,17 @@
1
+ import type { SemanticRole } from './dataset.js';
2
+ type RoleValueMap = {
3
+ money: number | null;
4
+ count: number | null;
5
+ percent: number | null;
6
+ timestamp: string | null;
7
+ status: string | null;
8
+ category: string | null;
9
+ label: string | null;
10
+ identifier: string | null;
11
+ url: string | null;
12
+ text: string | null;
13
+ };
14
+ export type RolesFor<V> = {
15
+ [R in SemanticRole]: V | null extends RoleValueMap[R] ? R : never;
16
+ }[SemanticRole];
17
+ export {};
@@ -0,0 +1,3 @@
1
+ const _rolesCovered = true;
2
+ void _rolesCovered;
3
+ export {};
@@ -0,0 +1,6 @@
1
+ import { z } from 'zod';
2
+ export declare const MonthPointSchema: z.ZodObject<{
3
+ month: z.ZodString;
4
+ amount: z.ZodNumber;
5
+ }, z.core.$strip>;
6
+ export type MonthPoint = z.infer<typeof MonthPointSchema>;
@@ -0,0 +1,5 @@
1
+ import { z } from 'zod';
2
+ // A point on a monthly money axis ('YYYY-MM' → amount in major units). The shared shape the billing presets
3
+ // emit (monthlySpend) and the cross-service Overview rollup consumes, so the two can't drift. Schema-defined
4
+ // so the export-bundle validator can reuse it at the cross-version boundary.
5
+ export const MonthPointSchema = z.object({ month: z.string(), amount: z.number() });
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ export declare const SectionSchema: z.ZodEnum<{
3
+ balance: "balance";
4
+ other: "other";
5
+ spend: "spend";
6
+ }>;
7
+ export type Section = z.infer<typeof SectionSchema>;
8
+ export declare const SummarySchema: z.ZodObject<{
9
+ section: z.ZodEnum<{
10
+ balance: "balance";
11
+ other: "other";
12
+ spend: "spend";
13
+ }>;
14
+ label: z.ZodString;
15
+ value: z.ZodNumber;
16
+ role: z.ZodEnum<{
17
+ count: "count";
18
+ money: "money";
19
+ percent: "percent";
20
+ }>;
21
+ currency: z.ZodOptional<z.ZodString>;
22
+ basis: z.ZodOptional<z.ZodEnum<{
23
+ accrued: "accrued";
24
+ flat: "flat";
25
+ invoiced: "invoiced";
26
+ lastInvoice: "lastInvoice";
27
+ upcoming: "upcoming";
28
+ }>>;
29
+ spark: z.ZodOptional<z.ZodObject<{
30
+ dataset: z.ZodString;
31
+ x: z.ZodString;
32
+ y: z.ZodString;
33
+ }, z.core.$strip>>;
34
+ headline: z.ZodOptional<z.ZodBoolean>;
35
+ }, z.core.$strip>;
36
+ export type Summary = z.infer<typeof SummarySchema>;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import { MtdBasisSchema } from '../presets/mtd-basis.js';
3
+ // Which Overview section a summary feeds. Only 'spend' sums across services (the combined spend total + chart);
4
+ // 'balance' and 'other' are shown per-service, never summed. Additive: a future summing band (e.g. 'revenue')
5
+ // is a new enum value plus a one-line rollup rule.
6
+ export const SectionSchema = z.enum(['spend', 'balance', 'other']);
7
+ export const SummarySchema = z.object({
8
+ section: SectionSchema,
9
+ label: z.string(),
10
+ value: z.number(),
11
+ role: z.enum(['money', 'count', 'percent']),
12
+ currency: z.string().optional(),
13
+ // What a spend figure measures (accrued / invoiced / flat / upcoming / lastInvoice) — a display label.
14
+ basis: MtdBasisSchema.optional(),
15
+ spark: z.object({ dataset: z.string(), x: z.string(), y: z.string() }).optional(),
16
+ // Force this summary to be the service's headline (overrides the spend > balance > other default).
17
+ headline: z.boolean().optional()
18
+ });
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+ export declare const FileSourceSchema: z.ZodUnion<readonly [z.ZodObject<{
3
+ url: z.ZodString;
4
+ }, z.core.$strip>, z.ZodObject<{
5
+ fetch: z.ZodLiteral<true>;
6
+ }, z.core.$strip>]>;
7
+ export type FileSource = z.infer<typeof FileSourceSchema>;
8
+ export declare const TableFilesSchema: z.ZodObject<{
9
+ name: z.ZodOptional<z.ZodString>;
10
+ source: z.ZodUnion<readonly [z.ZodObject<{
11
+ url: z.ZodString;
12
+ }, z.core.$strip>, z.ZodObject<{
13
+ fetch: z.ZodLiteral<true>;
14
+ }, z.core.$strip>]>;
15
+ ext: z.ZodOptional<z.ZodString>;
16
+ category: z.ZodOptional<z.ZodString>;
17
+ folder: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>;
19
+ export type TableFiles = z.infer<typeof TableFilesSchema>;
20
+ export declare const resolveTableFiles: (f: TableFiles) => {
21
+ urlKey?: string;
22
+ nameKey?: string;
23
+ folderKey?: string;
24
+ ext?: string;
25
+ category?: string;
26
+ useFetch: boolean;
27
+ };
28
+ export type StatTone = 'positive' | 'negative' | 'muted';
29
+ export declare const StatFieldSchema: z.ZodObject<{
30
+ key: z.ZodString;
31
+ max: z.ZodOptional<z.ZodNumber>;
32
+ unit: z.ZodOptional<z.ZodString>;
33
+ caption: z.ZodOptional<z.ZodString>;
34
+ tone: z.ZodOptional<z.ZodEnum<{
35
+ muted: "muted";
36
+ negative: "negative";
37
+ positive: "positive";
38
+ }>>;
39
+ }, z.core.$strip>;
40
+ export type StatField = z.infer<typeof StatFieldSchema>;
41
+ export declare const ViewSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
42
+ type: z.ZodLiteral<"stat">;
43
+ dataset: z.ZodString;
44
+ fields: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
45
+ key: z.ZodString;
46
+ max: z.ZodOptional<z.ZodNumber>;
47
+ unit: z.ZodOptional<z.ZodString>;
48
+ caption: z.ZodOptional<z.ZodString>;
49
+ tone: z.ZodOptional<z.ZodEnum<{
50
+ muted: "muted";
51
+ negative: "negative";
52
+ positive: "positive";
53
+ }>>;
54
+ }, z.core.$strip>]>>>;
55
+ title: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strip>, z.ZodObject<{
57
+ type: z.ZodLiteral<"timeseries">;
58
+ dataset: z.ZodString;
59
+ x: z.ZodString;
60
+ y: z.ZodString;
61
+ granularity: z.ZodOptional<z.ZodEnum<{
62
+ daily: "daily";
63
+ monthly: "monthly";
64
+ }>>;
65
+ stackBy: z.ZodOptional<z.ZodString>;
66
+ title: z.ZodOptional<z.ZodString>;
67
+ }, z.core.$strip>, z.ZodObject<{
68
+ type: z.ZodLiteral<"table">;
69
+ dataset: z.ZodString;
70
+ columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ title: z.ZodOptional<z.ZodString>;
72
+ groupBy: z.ZodOptional<z.ZodString>;
73
+ files: z.ZodOptional<z.ZodObject<{
74
+ name: z.ZodOptional<z.ZodString>;
75
+ source: z.ZodUnion<readonly [z.ZodObject<{
76
+ url: z.ZodString;
77
+ }, z.core.$strip>, z.ZodObject<{
78
+ fetch: z.ZodLiteral<true>;
79
+ }, z.core.$strip>]>;
80
+ ext: z.ZodOptional<z.ZodString>;
81
+ category: z.ZodOptional<z.ZodString>;
82
+ folder: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strip>>;
84
+ detail: z.ZodOptional<z.ZodObject<{
85
+ dataset: z.ZodString;
86
+ on: z.ZodString;
87
+ }, z.core.$strip>>;
88
+ }, z.core.$strip>, z.ZodObject<{
89
+ type: z.ZodLiteral<"keyvalue">;
90
+ dataset: z.ZodString;
91
+ title: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>], "type">;
93
+ export type View = z.infer<typeof ViewSchema>;
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ // Byte source for a downloadable table's rows: a per-row url column (the host GETs it) OR the capability's
3
+ // fetchFile(ctx,row) hook (POST/multi-step downloads). Exactly one.
4
+ export const FileSourceSchema = z.union([z.object({ url: z.string() }), z.object({ fetch: z.literal(true) })]);
5
+ // Marks a table's rows as downloadable files. Presentation only — the renderer adds selection + a Size column
6
+ // + per-row View/Open. Download-transport knobs (referer/concurrency) live on the plugin's transport download
7
+ // config, not here.
8
+ export const TableFilesSchema = z.object({
9
+ name: z.string().optional(), // row field naming each file
10
+ source: FileSourceSchema,
11
+ ext: z.string().optional(),
12
+ category: z.string().optional(), // literal subfolder for all of the table's files
13
+ folder: z.string().optional() // per-row subfolder column (overrides category)
14
+ });
15
+ // Normalize a TableFiles descriptor (`source`/`name`/`folder`) to the flat accessor keys the renderer + the
16
+ // download path read. `useFetch` true means the byte source is the capability's fetchFile hook (no url column).
17
+ export const resolveTableFiles = (f) => ({
18
+ urlKey: 'url' in f.source ? f.source.url : undefined,
19
+ nameKey: f.name,
20
+ folderKey: f.folder,
21
+ ext: f.ext,
22
+ category: f.category,
23
+ useFetch: 'fetch' in f.source
24
+ });
25
+ const STAT_TONES = ['positive', 'negative', 'muted'];
26
+ // Per-field presentation for a stat card, beyond the bare value the column's role formats. All optional and
27
+ // all literals (collect() runs per fetch with live data, and the view round-trips through the manifest), so a
28
+ // plugin computes them at build time. `max` renders `value / max` + a progress bar (value/max); `unit` is a
29
+ // muted suffix (e.g. '/mo'); `caption` is a subtext/breakdown line; `tone` tints the value.
30
+ export const StatFieldSchema = z.object({
31
+ key: z.string(),
32
+ max: z.number().optional(),
33
+ unit: z.string().optional(),
34
+ caption: z.string().optional(),
35
+ tone: z.enum(STAT_TONES).optional()
36
+ });
37
+ // A view binds a dataset (by id) to a generic renderer. Adding a view = adding a descriptor.
38
+ export const ViewSchema = z.discriminatedUnion('type', [
39
+ z.object({
40
+ type: z.literal('stat'),
41
+ dataset: z.string(),
42
+ // A bare key shows the field's value as-is; a StatField adds denominator/progress, caption, unit, or tone.
43
+ fields: z.array(z.union([z.string(), StatFieldSchema])).optional(),
44
+ title: z.string().optional()
45
+ }),
46
+ z.object({
47
+ type: z.literal('timeseries'),
48
+ dataset: z.string(),
49
+ x: z.string(),
50
+ y: z.string(),
51
+ // Declared cadence of the series, so the renderer picks the chart (monthly spend vs daily trend) from
52
+ // intent rather than sniffing the x-axis label format.
53
+ granularity: z.enum(['monthly', 'daily']).optional(),
54
+ // A category column → a stacked breakdown chart: rows are long-format (each carries x, this category, and
55
+ // y), one stacked series per distinct category, with a legend. Absent → a single-series chart.
56
+ stackBy: z.string().optional(),
57
+ title: z.string().optional()
58
+ }),
59
+ z.object({
60
+ type: z.literal('table'),
61
+ dataset: z.string(),
62
+ columns: z.array(z.string()).optional(),
63
+ title: z.string().optional(),
64
+ // A column key → collapsible grouped sections (per-group select-all + Download).
65
+ groupBy: z.string().optional(),
66
+ files: TableFilesSchema.optional(),
67
+ // Expand each row into a nested table drawn from another (table) dataset in the result, joined on a field
68
+ // present on both sides: a parent row R shows the child rows where child[on] === R[on]. The child renders
69
+ // with its own column roles. This is the static-child-table expansion — distinct from the built-in daily
70
+ // drilldown a cumulative column derives; when both could apply, `detail` wins.
71
+ detail: z.object({ dataset: z.string(), on: z.string() }).optional()
72
+ }),
73
+ z.object({ type: z.literal('keyvalue'), dataset: z.string(), title: z.string().optional() })
74
+ ]);
@@ -0,0 +1,12 @@
1
+ export type { PluginCategory, TroubleshootingCause, TroubleshootingAction, Troubleshooting, PluginMeta } from './plugin/meta.js';
2
+ export type { LocalStorageToken, ManualField, SessionSource } from './plugin/session.js';
3
+ export type { CredentialStore, AuthContext, AuthAttachment, AuthResolveHook, AuthStrategy, AuthKind, SpaBearerAuth } from './plugin/auth.js';
4
+ export type { TransportEngine, DownloadTransport, TransportConfig, HttpMethod, RequestOptions, ButinResponse, ButinClient } from './plugin/transport.js';
5
+ export type { BrowserFetchInit, BrowserContext, BrowserPage, BrowserSession } from './plugin/browser.js';
6
+ export { configValuesSchema, defineConfigSchema } from './plugin/config.js';
7
+ export type { ConfigFieldKind, ConfigOption, ConfigFieldCondition, ConfigField, PluginConfigSchema, ConfigValues, ConfigOf } from './plugin/config.js';
8
+ export { defineCapability } from './plugin/capability.js';
9
+ export type { CollectContext, Capability, IncrementalSpec, IncrementalCapability } from './plugin/capability.js';
10
+ export type { DocumentBytes } from './plugin/documents.js';
11
+ export { definePlugin } from './plugin/plugin.js';
12
+ export type { ButinPlugin, PluginBackend } from './plugin/plugin.js';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // @butinapp/sdk — the plugin-authoring contract: everything needed to DECLARE a plugin — the descriptor
2
+ // (meta · session · auth · transport · browser · config · capability · documents · plugin) and the define*
3
+ // helpers. The separate "what a capability returns" tier — the data-view contract (datasets · views ·
4
+ // summary · result) + the typed builders (table · record · capabilityResult) — is '@butinapp/sdk/data'. The
5
+ // high-altitude preset builders live on '@butinapp/sdk/presets'; shared third-party mechanics on
6
+ // '@butinapp/sdk/integrations'; edge normalizers on '@butinapp/sdk/util'; the synthetic sample toolkit on
7
+ // '@butinapp/sdk/testing'; lodash + luxon on '@butinapp/sdk/libs'. The host-layer rollup/wire shapes core +
8
+ // the viewer consume are NOT here — they live in '@butinapp/shapes' (and the wire format on
9
+ // '@butinapp/shapes/bundle'), so the author SDK stays purely the plugin-authoring contract.
10
+ //
11
+ // This barrel is an explicit allowlist, not a wildcard: a symbol is public only when it's named here.
12
+ export { configValuesSchema, defineConfigSchema } from './plugin/config.js';
13
+ export { defineCapability } from './plugin/capability.js';
14
+ export { definePlugin } from './plugin/plugin.js';
@@ -0,0 +1 @@
1
+ export * from './stripe.js';
@@ -0,0 +1,4 @@
1
+ // @butinapp/sdk/integrations — shared mechanics for third-party providers that more than one service proxies
2
+ // to. Not part of the universal plugin contract (a plugin that doesn't touch the provider never sees these),
3
+ // so they live off the root barrel. Today: Stripe's hosted billing-portal walk + hosted-invoice PDF download.
4
+ export * from './stripe.js';