@onabl/js 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/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # `@onabl/js`
2
+
3
+ Framework-agnostic core of the onabl SDK. No React, no DOM assumptions —
4
+ `@onabl/react` and `@onabl/next` build on top of this.
5
+
6
+ **Status:** skeleton only (Phase 14 §14.5.2, in progress). See
7
+ `docs/SDK-DESIGN.md` for the full intended API surface before adding to this
8
+ package, and `docs/INTEGRATION-LAYER-TODO.md` for current build status.
9
+
10
+ ## What belongs here
11
+
12
+ - `createOnablClient()` — the typed API client
13
+ - `OnablError` and its subclasses (`OnablNotFound`, `OnablUnauthorized`,
14
+ `OnablValidation`, `OnablNetwork`)
15
+ - The condition evaluator and form state machine, extracted from
16
+ `apps/web/components/form/MultiStepForm.tsx`
17
+ - `presentQuote` / `groupLineItems`, re-exported (and bundled — see
18
+ `tsup.config.ts`) from `@onabl/engine`, which is never published on its own
19
+
20
+ ## Build
21
+
22
+ Built with `tsup`, not plain `tsc` — `@onabl/engine`'s presentation code has
23
+ to be bundled into this package's output since `@onabl/engine` itself is
24
+ private and never installable on its own. See the comment in
25
+ `tsup.config.ts` before changing the dependency/build setup.
@@ -0,0 +1,146 @@
1
+ import { PublicForm, ValidateCodeResponse, Quote, Invoice, PresignResponse, PublicFormField } from '@onabl/contracts/public-api';
2
+ export { Invoice, PublicForm, PublicFormConfig, PublicFormField, Quote, ValidateCodeResponse } from '@onabl/contracts/public-api';
3
+ import { AnswerMap, FormStep } from '@onabl/types/config';
4
+ export { GroupedItems, PresentQuoteInput, QuotePresentation, QuoteRow, SeparateSection, groupLineItems, presentQuote } from '@onabl/engine/presentation';
5
+
6
+ interface OnablClientConfig {
7
+ handle: string;
8
+ secretKey: string;
9
+ baseUrl?: string;
10
+ apiVersion?: string;
11
+ /** Path template for the quote view page on the integrator's own site — see submissions.create()'s returned quoteUrl. */
12
+ quotePath?: string;
13
+ }
14
+ interface CallOpts {
15
+ /** Slug of a specific form, for tenants running more than one. Omitted, resolves to the tenant's dashboard-configured default. */
16
+ slug?: string;
17
+ }
18
+ /**
19
+ * Customer-token-scoped calls need a bearer token — @onabl/next supplies one
20
+ * automatically from its managed cookie (SDK-DESIGN.md §3, "Access cookie").
21
+ * Used directly (e.g. a Vite/plain-React app per SDK-DESIGN.md §4, "Route
22
+ * handler only" decision), the caller supplies it explicitly.
23
+ */
24
+ interface TokenOpts {
25
+ token: string;
26
+ }
27
+ interface OnablClient {
28
+ forms: {
29
+ get(opts?: CallOpts): Promise<PublicForm>;
30
+ validateDiscountCode(code: string, opts?: CallOpts): Promise<ValidateCodeResponse>;
31
+ };
32
+ quotes: {
33
+ get(uuid: string, opts: TokenOpts): Promise<Quote>;
34
+ accept(uuid: string, opts: TokenOpts): Promise<void>;
35
+ decline(uuid: string, reason: string, note: string | undefined, opts: TokenOpts): Promise<void>;
36
+ };
37
+ invoices: {
38
+ get(uuid: string, opts: TokenOpts): Promise<Invoice>;
39
+ /** milestone "deposit" saves the deposit POP; "balance"/"full" save the balance/full-payment POP. */
40
+ savePopUrl(uuid: string, key: string, milestone: "deposit" | "balance" | "full", opts: TokenOpts): Promise<void>;
41
+ };
42
+ storage: {
43
+ /** Presigns an upload for a customer-facing flow (currently only invoice POP uploads — pathPrefix is always "pop"). */
44
+ presignPop(resourceId: string, filename: string, contentType: string, fileSize: number, opts: TokenOpts): Promise<PresignResponse>;
45
+ };
46
+ submissions: {
47
+ /** isTenantSource marks a submission taken by the tenant on the customer's behalf (the on-site quote tool) rather than the customer directly — see docs/ROADMAP.md Phase 2.11. */
48
+ create(answers: AnswerMap, opts?: CallOpts & {
49
+ consentGiven?: boolean;
50
+ discountCode?: string;
51
+ isTenantSource?: boolean;
52
+ }): Promise<{
53
+ quoteId: string;
54
+ quoteUrl: string;
55
+ token: string;
56
+ }>;
57
+ };
58
+ }
59
+ declare function createOnablClient(config: OnablClientConfig): OnablClient;
60
+
61
+ /**
62
+ * Typed error classes so a catch block can discriminate without matching on
63
+ * HTTP status numbers or string messages. See docs/SDK-DESIGN.md §3.
64
+ */
65
+ declare class OnablError extends Error {
66
+ readonly status?: number | undefined;
67
+ constructor(message: string, status?: number | undefined);
68
+ }
69
+ /** 404, or a customer token that doesn't grant access to this resource. */
70
+ declare class OnablNotFound extends OnablError {
71
+ constructor(message?: string);
72
+ }
73
+ /** Missing or invalid secret key / customer token. */
74
+ declare class OnablUnauthorized extends OnablError {
75
+ constructor(message?: string);
76
+ }
77
+ /** 400 — carries field-level detail from the API's Zod validation errors. */
78
+ declare class OnablValidation extends OnablError {
79
+ readonly fieldErrors?: Record<string, string[]> | undefined;
80
+ constructor(message: string, fieldErrors?: Record<string, string[]> | undefined);
81
+ }
82
+ /** Transport failure — network error, timeout, or an unparseable response. */
83
+ declare class OnablNetwork extends OnablError {
84
+ constructor(message?: string, cause?: unknown);
85
+ }
86
+
87
+ /**
88
+ * The form state machine, extracted from apps/web/components/form/MultiStepForm.tsx
89
+ * (§14.5.2, part 2). Pure functions only — no React, no framework of any kind —
90
+ * so any UI layer (a React hook in @onabl/react, or something else entirely)
91
+ * can drive the same tested logic. Renders nothing; every function here takes
92
+ * a snapshot of state and returns a value, never mutates.
93
+ *
94
+ * Deliberately excludes three things apps/web's current implementation
95
+ * handles that the documented useOnablForm surface (docs/SDK-DESIGN.md §3)
96
+ * does not — decided 2026-08-24, see docs/INTEGRATION-LAYER-TODO.md:
97
+ *
98
+ * - Intro screen: not modeled as a step here at all. It's pure presentation
99
+ * (config.meta.intro) with no fields to collect or validate — the
100
+ * integrator reads it directly and renders it before ever touching this
101
+ * module's step navigation.
102
+ * - Auto-advance (single radio/toggle field steps skipping the Continue
103
+ * click): not built in here. It's a UI/interaction choice, not form logic —
104
+ * the render layer calls next() itself after setAnswer() when it wants
105
+ * that behavior, using visibleFields to detect the single-field case.
106
+ * - Discount code: folded in as ordinary state here (getDiscountCode-style
107
+ * handling lives in @onabl/react's hook, not this module) rather than
108
+ * modeled as a synthetic step the way apps/web's current buildSteps() does.
109
+ * Where to render the discount input within the step flow (inline on the
110
+ * last step, or as a separate screen before submit) is left to the
111
+ * integrator — this module has no opinion on it.
112
+ */
113
+
114
+ /** A step that currently has at least one visible field — steps a conditional field emptied out entirely are skipped. */
115
+ interface VisibleStep extends FormStep {
116
+ fields: PublicFormField[];
117
+ }
118
+ /**
119
+ * Every step the visitor can currently navigate to, in order, filtered to
120
+ * only those with ≥1 visible field. Ungrouped fields (stepId: null) are
121
+ * collected into a synthetic trailing step, matching apps/web's current
122
+ * behavior — labeled "Details" there today; integrators using this directly
123
+ * can relabel as they see fit since this is just a FormStep-shaped value.
124
+ */
125
+ declare function getVisibleSteps(form: PublicForm, answers: AnswerMap): VisibleStep[];
126
+ /** Every field on the form, regardless of visibility — for debugging a field that won't appear. */
127
+ declare function getAllFields(form: PublicForm): PublicFormField[];
128
+ /**
129
+ * Seeds the initial AnswerMap from field defaults, using @onabl/engine's
130
+ * canonical applyDefaults — including its type-based fallback (unset number
131
+ * fields → 0, unset toggle → false). This is a deliberate choice for this
132
+ * new implementation, not a port of apps/web's current (leaner) local
133
+ * version — see docs/INTEGRATION-LAYER-TODO.md's Stage 3 pre-work note for
134
+ * why apps/web wasn't swapped onto this same behavior directly. Verify this
135
+ * during apps/web's own SDK migration (§14.5.5), since it is a real,
136
+ * intentional behavior difference from what's live today.
137
+ */
138
+ declare function getInitialAnswers(form: PublicForm): AnswerMap;
139
+ /**
140
+ * Validates one step's fields against the current answers. Same rules as
141
+ * apps/web's current validateStep(), generalized to a plain field list
142
+ * rather than coupled to a Step union type.
143
+ */
144
+ declare function validateFields(fields: PublicFormField[], answers: AnswerMap): Record<string, string>;
145
+
146
+ export { type CallOpts, type OnablClient, type OnablClientConfig, OnablError, OnablNetwork, OnablNotFound, OnablUnauthorized, OnablValidation, type TokenOpts, type VisibleStep, createOnablClient, getAllFields, getInitialAnswers, getVisibleSteps, validateFields };
@@ -0,0 +1,146 @@
1
+ import { PublicForm, ValidateCodeResponse, Quote, Invoice, PresignResponse, PublicFormField } from '@onabl/contracts/public-api';
2
+ export { Invoice, PublicForm, PublicFormConfig, PublicFormField, Quote, ValidateCodeResponse } from '@onabl/contracts/public-api';
3
+ import { AnswerMap, FormStep } from '@onabl/types/config';
4
+ export { GroupedItems, PresentQuoteInput, QuotePresentation, QuoteRow, SeparateSection, groupLineItems, presentQuote } from '@onabl/engine/presentation';
5
+
6
+ interface OnablClientConfig {
7
+ handle: string;
8
+ secretKey: string;
9
+ baseUrl?: string;
10
+ apiVersion?: string;
11
+ /** Path template for the quote view page on the integrator's own site — see submissions.create()'s returned quoteUrl. */
12
+ quotePath?: string;
13
+ }
14
+ interface CallOpts {
15
+ /** Slug of a specific form, for tenants running more than one. Omitted, resolves to the tenant's dashboard-configured default. */
16
+ slug?: string;
17
+ }
18
+ /**
19
+ * Customer-token-scoped calls need a bearer token — @onabl/next supplies one
20
+ * automatically from its managed cookie (SDK-DESIGN.md §3, "Access cookie").
21
+ * Used directly (e.g. a Vite/plain-React app per SDK-DESIGN.md §4, "Route
22
+ * handler only" decision), the caller supplies it explicitly.
23
+ */
24
+ interface TokenOpts {
25
+ token: string;
26
+ }
27
+ interface OnablClient {
28
+ forms: {
29
+ get(opts?: CallOpts): Promise<PublicForm>;
30
+ validateDiscountCode(code: string, opts?: CallOpts): Promise<ValidateCodeResponse>;
31
+ };
32
+ quotes: {
33
+ get(uuid: string, opts: TokenOpts): Promise<Quote>;
34
+ accept(uuid: string, opts: TokenOpts): Promise<void>;
35
+ decline(uuid: string, reason: string, note: string | undefined, opts: TokenOpts): Promise<void>;
36
+ };
37
+ invoices: {
38
+ get(uuid: string, opts: TokenOpts): Promise<Invoice>;
39
+ /** milestone "deposit" saves the deposit POP; "balance"/"full" save the balance/full-payment POP. */
40
+ savePopUrl(uuid: string, key: string, milestone: "deposit" | "balance" | "full", opts: TokenOpts): Promise<void>;
41
+ };
42
+ storage: {
43
+ /** Presigns an upload for a customer-facing flow (currently only invoice POP uploads — pathPrefix is always "pop"). */
44
+ presignPop(resourceId: string, filename: string, contentType: string, fileSize: number, opts: TokenOpts): Promise<PresignResponse>;
45
+ };
46
+ submissions: {
47
+ /** isTenantSource marks a submission taken by the tenant on the customer's behalf (the on-site quote tool) rather than the customer directly — see docs/ROADMAP.md Phase 2.11. */
48
+ create(answers: AnswerMap, opts?: CallOpts & {
49
+ consentGiven?: boolean;
50
+ discountCode?: string;
51
+ isTenantSource?: boolean;
52
+ }): Promise<{
53
+ quoteId: string;
54
+ quoteUrl: string;
55
+ token: string;
56
+ }>;
57
+ };
58
+ }
59
+ declare function createOnablClient(config: OnablClientConfig): OnablClient;
60
+
61
+ /**
62
+ * Typed error classes so a catch block can discriminate without matching on
63
+ * HTTP status numbers or string messages. See docs/SDK-DESIGN.md §3.
64
+ */
65
+ declare class OnablError extends Error {
66
+ readonly status?: number | undefined;
67
+ constructor(message: string, status?: number | undefined);
68
+ }
69
+ /** 404, or a customer token that doesn't grant access to this resource. */
70
+ declare class OnablNotFound extends OnablError {
71
+ constructor(message?: string);
72
+ }
73
+ /** Missing or invalid secret key / customer token. */
74
+ declare class OnablUnauthorized extends OnablError {
75
+ constructor(message?: string);
76
+ }
77
+ /** 400 — carries field-level detail from the API's Zod validation errors. */
78
+ declare class OnablValidation extends OnablError {
79
+ readonly fieldErrors?: Record<string, string[]> | undefined;
80
+ constructor(message: string, fieldErrors?: Record<string, string[]> | undefined);
81
+ }
82
+ /** Transport failure — network error, timeout, or an unparseable response. */
83
+ declare class OnablNetwork extends OnablError {
84
+ constructor(message?: string, cause?: unknown);
85
+ }
86
+
87
+ /**
88
+ * The form state machine, extracted from apps/web/components/form/MultiStepForm.tsx
89
+ * (§14.5.2, part 2). Pure functions only — no React, no framework of any kind —
90
+ * so any UI layer (a React hook in @onabl/react, or something else entirely)
91
+ * can drive the same tested logic. Renders nothing; every function here takes
92
+ * a snapshot of state and returns a value, never mutates.
93
+ *
94
+ * Deliberately excludes three things apps/web's current implementation
95
+ * handles that the documented useOnablForm surface (docs/SDK-DESIGN.md §3)
96
+ * does not — decided 2026-08-24, see docs/INTEGRATION-LAYER-TODO.md:
97
+ *
98
+ * - Intro screen: not modeled as a step here at all. It's pure presentation
99
+ * (config.meta.intro) with no fields to collect or validate — the
100
+ * integrator reads it directly and renders it before ever touching this
101
+ * module's step navigation.
102
+ * - Auto-advance (single radio/toggle field steps skipping the Continue
103
+ * click): not built in here. It's a UI/interaction choice, not form logic —
104
+ * the render layer calls next() itself after setAnswer() when it wants
105
+ * that behavior, using visibleFields to detect the single-field case.
106
+ * - Discount code: folded in as ordinary state here (getDiscountCode-style
107
+ * handling lives in @onabl/react's hook, not this module) rather than
108
+ * modeled as a synthetic step the way apps/web's current buildSteps() does.
109
+ * Where to render the discount input within the step flow (inline on the
110
+ * last step, or as a separate screen before submit) is left to the
111
+ * integrator — this module has no opinion on it.
112
+ */
113
+
114
+ /** A step that currently has at least one visible field — steps a conditional field emptied out entirely are skipped. */
115
+ interface VisibleStep extends FormStep {
116
+ fields: PublicFormField[];
117
+ }
118
+ /**
119
+ * Every step the visitor can currently navigate to, in order, filtered to
120
+ * only those with ≥1 visible field. Ungrouped fields (stepId: null) are
121
+ * collected into a synthetic trailing step, matching apps/web's current
122
+ * behavior — labeled "Details" there today; integrators using this directly
123
+ * can relabel as they see fit since this is just a FormStep-shaped value.
124
+ */
125
+ declare function getVisibleSteps(form: PublicForm, answers: AnswerMap): VisibleStep[];
126
+ /** Every field on the form, regardless of visibility — for debugging a field that won't appear. */
127
+ declare function getAllFields(form: PublicForm): PublicFormField[];
128
+ /**
129
+ * Seeds the initial AnswerMap from field defaults, using @onabl/engine's
130
+ * canonical applyDefaults — including its type-based fallback (unset number
131
+ * fields → 0, unset toggle → false). This is a deliberate choice for this
132
+ * new implementation, not a port of apps/web's current (leaner) local
133
+ * version — see docs/INTEGRATION-LAYER-TODO.md's Stage 3 pre-work note for
134
+ * why apps/web wasn't swapped onto this same behavior directly. Verify this
135
+ * during apps/web's own SDK migration (§14.5.5), since it is a real,
136
+ * intentional behavior difference from what's live today.
137
+ */
138
+ declare function getInitialAnswers(form: PublicForm): AnswerMap;
139
+ /**
140
+ * Validates one step's fields against the current answers. Same rules as
141
+ * apps/web's current validateStep(), generalized to a plain field list
142
+ * rather than coupled to a Step union type.
143
+ */
144
+ declare function validateFields(fields: PublicFormField[], answers: AnswerMap): Record<string, string>;
145
+
146
+ export { type CallOpts, type OnablClient, type OnablClientConfig, OnablError, OnablNetwork, OnablNotFound, OnablUnauthorized, OnablValidation, type TokenOpts, type VisibleStep, createOnablClient, getAllFields, getInitialAnswers, getVisibleSteps, validateFields };