@flowmarkdev/node 0.0.0-stage → 0.1.0-alpha.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/dist/adapters/next.d.ts +17 -0
  4. package/dist/adapters/next.js +38 -0
  5. package/dist/adapters/proxy.d.ts +25 -0
  6. package/dist/adapters/proxy.js +94 -0
  7. package/dist/browser/index.d.ts +52 -0
  8. package/dist/browser/index.js +115 -0
  9. package/dist/client/create-client.d.ts +21 -0
  10. package/dist/client/create-client.js +136 -0
  11. package/dist/client/runtime.d.ts +5 -0
  12. package/dist/client/runtime.js +72 -0
  13. package/dist/config/api-origin.d.ts +2 -0
  14. package/dist/config/api-origin.js +12 -0
  15. package/dist/config/define-config.d.ts +11 -0
  16. package/dist/config/define-config.js +12 -0
  17. package/dist/config/settings.d.ts +11 -0
  18. package/dist/config/settings.js +31 -0
  19. package/dist/errors/index.d.ts +49 -0
  20. package/dist/errors/index.js +21 -0
  21. package/dist/features/delivery/adapters/http-transport.d.ts +8 -0
  22. package/dist/features/delivery/adapters/http-transport.js +31 -0
  23. package/dist/features/delivery/adapters/http.d.ts +4 -0
  24. package/dist/features/delivery/adapters/http.js +11 -0
  25. package/dist/features/delivery/adapters/memory-transport.d.ts +10 -0
  26. package/dist/features/delivery/adapters/memory-transport.js +12 -0
  27. package/dist/features/delivery/delivery-queue.d.ts +15 -0
  28. package/dist/features/delivery/delivery-queue.js +56 -0
  29. package/dist/features/delivery/ports/workflow-transport.d.ts +9 -0
  30. package/dist/features/delivery/ports/workflow-transport.js +3 -0
  31. package/dist/features/workflows/workflow-engine.d.ts +27 -0
  32. package/dist/features/workflows/workflow-engine.js +232 -0
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.js +5 -0
  35. package/dist/types/index.d.ts +167 -0
  36. package/dist/types/index.js +1 -0
  37. package/dist/utils/failure-from-unknown.d.ts +3 -0
  38. package/dist/utils/failure-from-unknown.js +36 -0
  39. package/dist/utils/operation-args.d.ts +5 -0
  40. package/dist/utils/operation-args.js +3 -0
  41. package/dist/utils/operation-callback.d.ts +2 -0
  42. package/dist/utils/operation-callback.js +18 -0
  43. package/package.json +75 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martin Persson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,174 @@
1
- # Temporary Holding Version
1
+ # @flowmarkdev/node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Flowmark observes a process your app already runs. It does not execute,
4
+ schedule, or retry that work. There is no `track()`, `span()`, or `metric()`
5
+ API. Use only the methods below.
6
+
7
+ This README is the implementation contract. Read it before adding markers.
8
+
9
+ ## Implement
10
+
11
+ ### 1. Pick the entry
12
+
13
+ | Code | Import |
14
+ | --- | --- |
15
+ | Next.js Route Handler or Server Action | `createFlowmark` from `@flowmarkdev/node/next` |
16
+ | Node worker, script, or long-lived server | `createFlowmark` from `@flowmarkdev/node` |
17
+ | Browser / Client Component | `createFlowmark` from `@flowmarkdev/node/browser` |
18
+
19
+ Create **one** shared client in a server-only module. Do not construct a new
20
+ client per file. Do not import the Node or Next entry in Client Components.
21
+ `@flowmarkdev/node/next` drains via `after()` — do not call it from middleware,
22
+ render, or the browser.
23
+
24
+ ```ts
25
+ // lib/flowmark.ts — Next.js server
26
+ import { createFlowmark } from "@flowmarkdev/node/next"
27
+ export const flowmark = createFlowmark()
28
+ ```
29
+
30
+ ```ts
31
+ // lib/flowmark.ts — Node
32
+ import { createFlowmark } from "@flowmarkdev/node"
33
+ export const flowmark = createFlowmark()
34
+ ```
35
+
36
+ ### 2. Server environment
37
+
38
+ Set these on the server only. Never `NEXT_PUBLIC_*`. Never a browser bundle.
39
+
40
+ ```bash
41
+ FLOWMARK_API_KEY=<project server write key>
42
+ FLOWMARK_API_URL=https://your-flowmark-api.example
43
+ ```
44
+
45
+ `FLOWMARK_API_URL` is the API origin, not `/v1/events`. In development it
46
+ defaults to `http://localhost:3003`. Production requires an explicit origin.
47
+
48
+ ### 3. Public API
49
+
50
+ This is the entire application surface:
51
+
52
+ - `createFlowmark()`
53
+ - `flowmark.workflow(name, { id?, properties?, environment?, release?, correlationId?, parentRunId? })`
54
+ - `flow.runId`
55
+ - `flow.step(name, { properties? })`
56
+ - `flow.operation(name, callback)` or `handle.done()` / `handle.fail({ code })`
57
+ - `flow.progress({ completed, total?, unit? })`
58
+ - `flow.error({ code, handled, retryable? })`
59
+ - `flow.setProperties(properties)`
60
+ - `flow.done({ result? })` / `flow.fail({ code, result? })`
61
+ - `flowmark.flush()` / `flowmark.shutdown()` — Node only; Next.js drains itself
62
+
63
+ Do not invent wrappers, extra clients, or methods.
64
+
65
+ ### 4. One request
66
+
67
+ Start, mark, then finish in the same handler. Keep the function's return and
68
+ thrown error. Telemetry failures must not change business behavior.
69
+
70
+ ```ts
71
+ import { flowmark } from "./flowmark"
72
+
73
+ export async function completeCheckout(orderId: string) {
74
+ const flow = flowmark.workflow("checkout.order", {
75
+ properties: { orderId },
76
+ })
77
+ try {
78
+ flow.step("payment-captured")
79
+ const shipment = await flow.operation("create-shipment", () =>
80
+ createShipment(orderId),
81
+ )
82
+ flow.done({ result: { shipmentId: shipment.id } })
83
+ return shipment
84
+ } catch (error) {
85
+ flow.fail({ code: "CHECKOUT_FAILED" })
86
+ throw error
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### 5. Same process, later request
92
+
93
+ Pass the same workflow name and `id`. Persist `flow.runId` (or your own
94
+ execution id) on the application record. A second `started` is ignored.
95
+
96
+ **Do not call `done()` or `fail()` until the whole process is finished.**
97
+ If this handler is only the start, record a step and return.
98
+
99
+ ```ts
100
+ // POST /expeditions — start
101
+ const flow = flowmark.workflow("expedition.run", { id: expedition.id })
102
+ flow.step("draft-started")
103
+
104
+ // POST /expeditions/:id/claim — later
105
+ const flow = flowmark.workflow("expedition.run", { id: expedition.id })
106
+ await flow.operation("claim-battle", () => claimBattle(expedition.id))
107
+ flow.step("battle-claimed")
108
+ flow.done({ result: { battlesWon: 2 } })
109
+ ```
110
+
111
+ Do not reuse a durable entity id that outlives one execution (the same order
112
+ across retries or later jobs). Put those in `properties`.
113
+
114
+ ### 6. A different process
115
+
116
+ New `workflow()` plus `correlationId` and/or `parentRunId`. Nothing is
117
+ propagated automatically — pass the ids through your own payload.
118
+
119
+ ### 7. Properties and results
120
+
121
+ Small JSON-safe objects: counts, status enums, opaque ids you already persist.
122
+ No secrets, emails, tokens, request bodies, raw `Error`s, or unnecessary
123
+ personal data.
124
+
125
+ Limits: depth 5, 128 keys, 128-character keys, 2,048-character strings, 16 KiB
126
+ per object.
127
+
128
+ ## Configuration
129
+
130
+ `defineConfig()` is a typed identity helper. It does not read the environment
131
+ or create a client. A `flowmark.config.ts` file is only a convention — import
132
+ it and pass it to `createFlowmark(config)`. Flowmark does not discover files.
133
+
134
+ Server config may omit credentials and use `FLOWMARK_API_KEY` /
135
+ `FLOWMARK_API_URL`. Browser code must use `@flowmarkdev/node/browser` and a public
136
+ browser key, never the server write key. Keep server and browser clients in
137
+ separate modules.
138
+
139
+ ## `done()`, `fail()`, and operations
140
+
141
+ `done({ result? })` and `fail({ code, result? })` end the run. The first
142
+ terminal wins. If operations are still open, completion waits until they close.
143
+ `fail()` can supersede a pending `done()`. Later markers are ignored.
144
+
145
+ `error({ code, handled, retryable? })` stays on the timeline and does not end
146
+ the run. Only `fail()` makes the run failed.
147
+
148
+ `operation(name, callback)` measures duration, returns the callback result, and
149
+ rethrows the original rejection. It does not fail the workflow.
150
+
151
+ ```ts
152
+ const receipt = await flow.operation("charge-payment", () => chargeCustomer())
153
+ flow.progress({ completed: 125, total: 500, unit: "rows" })
154
+ ```
155
+
156
+ Node processes must `await flowmark.shutdown()` before exit. Long-lived
157
+ workers can `await flowmark.flush()`. Those promises reject on delivery
158
+ failure; `step` / `done` / `fail` never throw delivery errors into app code.
159
+
160
+ ## Browser
161
+
162
+ `@flowmarkdev/node/browser` never reads server env. Direct delivery needs a public
163
+ browser key and `POST /v1/browser-events`. Or omit the key and proxy through
164
+ `createFlowmarkProxy` from `@flowmarkdev/node/next/proxy` with `FLOWMARK_API_KEY`
165
+ only on the server.
166
+
167
+ Browser telemetry is untrusted. Create workflows in event handlers or effects,
168
+ not during render.
169
+
170
+ ## Delivery
171
+
172
+ Best-effort, in memory, not a durable queue. The API deduplicates retries.
173
+ `MemoryTransport` and `workflow(name, { transport, id })` are for tests, not
174
+ ordinary setup.
@@ -0,0 +1,17 @@
1
+ import type { FlowmarkClient, FlowmarkConfig } from "../types/index.js";
2
+ export type { Flow, FlowmarkClient, FlowmarkConfig, WorkflowDoneOptions, WorkflowFailOptions, WorkflowOptions, WorkflowOperation, WorkflowOperationOptions, WorkflowRunOptions, WorkflowStepOptions, } from "../types/index.js";
3
+ export type { JsonValue, WorkflowRunId, WorkflowError, WorkflowCorrelationId, WorkflowFailure, WorkflowProperties, WorkflowProgress, WorkflowOperationFailure, WorkflowResult, } from "@flowmarkdev/core";
4
+ export { defineConfig } from "../config/define-config.js";
5
+ /**
6
+ * Next.js server client. Import from `@flowmarkdev/node/next` only.
7
+ * Use in Route Handlers or Server Actions. Not Client Components, middleware, or render.
8
+ * Reads FLOWMARK_API_KEY and FLOWMARK_API_URL. Never NEXT_PUBLIC_*.
9
+ * Drains via after() — do not call shutdown() yourself.
10
+ * One shared module-level client; wrap the places the developer named.
11
+ * @example
12
+ * import { createFlowmark } from "@flowmarkdev/node/next"
13
+ * export const flowmark = createFlowmark()
14
+ * const flow = flowmark.workflow("checkout.order")
15
+ * flow.done({ result: { ok: true } })
16
+ */
17
+ export declare function createFlowmark(config?: FlowmarkConfig): Pick<FlowmarkClient, "workflow">;
@@ -0,0 +1,38 @@
1
+ import { after } from "next/server";
2
+ import { createFlowmark as createClient } from "../client/create-client.js";
3
+ export { defineConfig } from "../config/define-config.js";
4
+ /**
5
+ * Next.js server client. Import from `@flowmarkdev/node/next` only.
6
+ * Use in Route Handlers or Server Actions. Not Client Components, middleware, or render.
7
+ * Reads FLOWMARK_API_KEY and FLOWMARK_API_URL. Never NEXT_PUBLIC_*.
8
+ * Drains via after() — do not call shutdown() yourself.
9
+ * One shared module-level client; wrap the places the developer named.
10
+ * @example
11
+ * import { createFlowmark } from "@flowmarkdev/node/next"
12
+ * export const flowmark = createFlowmark()
13
+ * const flow = flowmark.workflow("checkout.order")
14
+ * flow.done({ result: { ok: true } })
15
+ */
16
+ export function createFlowmark(config = {}) {
17
+ return {
18
+ workflow(name, options) {
19
+ const lifecycle = {};
20
+ // Register before starting work so unsupported contexts cannot orphan delivery.
21
+ after(async () => {
22
+ const client = lifecycle.client;
23
+ if (!client) {
24
+ return;
25
+ }
26
+ try {
27
+ await client.shutdown();
28
+ }
29
+ catch (cause) {
30
+ console.error("[Flowmark] Workflow delivery failed; application changes are unaffected.", cause);
31
+ }
32
+ });
33
+ const client = createClient(config);
34
+ lifecycle.client = client;
35
+ return client.workflow(name, options);
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,25 @@
1
+ import type { FlowmarkConfig } from "../types/index.js";
2
+ /** Server-only proxy options. Never pass client-supplied destinations or credentials. */
3
+ export interface FlowmarkProxyConfig extends Pick<FlowmarkConfig, "apiKey" | "apiUrl"> {
4
+ /**
5
+ * Your session/rate-limit check, run before reading the body.
6
+ * Return false for HTTP 403; errors are logged and produce HTTP 502.
7
+ * Optional for public demos only: Origin validation alone is not authentication or rate limiting.
8
+ */
9
+ readonly authorize?: (request: Request) => boolean | Promise<boolean>;
10
+ }
11
+ /**
12
+ * Creates a server-only Web Request/Response POST handler for a same-origin browser SDK proxy.
13
+ * Works with Next.js Route Handlers or any backend supporting standard Request/Response.
14
+ * Adds the server's write key, validates events, and limits streamed input to 64 KiB.
15
+ * Requires an Origin matching request.url; register this behind the browser's own origin.
16
+ * Returns 202 after upstream acceptance, 400 for bad input, 403 for denied access,
17
+ * 409/429 for upstream conflict/throttling, or 502 for delivery/configuration failures.
18
+ * @param config Server credentials/API origin (default FLOWMARK_* env) and a production access check.
19
+ * @example
20
+ * import { createFlowmarkProxy } from "@flowmarkdev/node/proxy"
21
+ * export const POST = createFlowmarkProxy({
22
+ * authorize: async (request) => Boolean(await getSession(request)),
23
+ * })
24
+ */
25
+ export declare function createFlowmarkProxy(config?: FlowmarkProxyConfig): (request: Request) => Promise<Response>;
@@ -0,0 +1,94 @@
1
+ import { FetchHttpClient, HttpClient, HttpClientRequest, } from "@effect/platform";
2
+ import { Data, Effect, Exit, Match, Schema, Stream } from "effect";
3
+ import { EventEnvelopeSchema } from "@flowmarkdev/core";
4
+ import { resolveSettings } from "../config/settings.js";
5
+ class InvalidProxyInput extends Data.TaggedError("InvalidProxyInput") {
6
+ }
7
+ class ProxyForbidden extends Data.TaggedError("ProxyForbidden") {
8
+ }
9
+ class ProxyAuthorizationError extends Data.TaggedError("ProxyAuthorizationError") {
10
+ }
11
+ const readEnvelope = Effect.fn("Flowmark.Proxy.readEnvelope")(function* (request) {
12
+ const stream = request.body;
13
+ if (Number(request.headers.get("content-length")) > 65536 || !stream) {
14
+ return yield* Effect.fail(new InvalidProxyInput({
15
+ cause: "Body is required and must not exceed 64 KiB",
16
+ }));
17
+ }
18
+ const decoder = new TextDecoder();
19
+ const body = yield* Stream.fromReadableStream({
20
+ evaluate: () => stream,
21
+ onError: (cause) => new InvalidProxyInput({ cause }),
22
+ releaseLockOnEnd: true,
23
+ }).pipe(Stream.runFoldEffect({ size: 0, text: "" }, (state, chunk) => {
24
+ const size = state.size + chunk.byteLength;
25
+ return size > 65536
26
+ ? Effect.fail(new InvalidProxyInput({ cause: "Body exceeds 64 KiB" }))
27
+ : Effect.succeed({
28
+ size,
29
+ text: state.text + decoder.decode(chunk, { stream: true }),
30
+ });
31
+ }), Effect.ensuring(Effect.tryPromise({
32
+ try: () => stream.cancel(),
33
+ catch: (cause) => new InvalidProxyInput({ cause }),
34
+ }).pipe(Effect.catchAll((error) => Effect.logWarning("[Flowmark proxy] Could not cancel request body", error)))));
35
+ return yield* Schema.decodeUnknown(Schema.parseJson(EventEnvelopeSchema))(body.text + decoder.decode()).pipe(Effect.mapError((cause) => new InvalidProxyInput({ cause })));
36
+ });
37
+ /**
38
+ * Creates a server-only Web Request/Response POST handler for a same-origin browser SDK proxy.
39
+ * Works with Next.js Route Handlers or any backend supporting standard Request/Response.
40
+ * Adds the server's write key, validates events, and limits streamed input to 64 KiB.
41
+ * Requires an Origin matching request.url; register this behind the browser's own origin.
42
+ * Returns 202 after upstream acceptance, 400 for bad input, 403 for denied access,
43
+ * 409/429 for upstream conflict/throttling, or 502 for delivery/configuration failures.
44
+ * @param config Server credentials/API origin (default FLOWMARK_* env) and a production access check.
45
+ * @example
46
+ * import { createFlowmarkProxy } from "@flowmarkdev/node/proxy"
47
+ * export const POST = createFlowmarkProxy({
48
+ * authorize: async (request) => Boolean(await getSession(request)),
49
+ * })
50
+ */
51
+ export function createFlowmarkProxy(config = {}) {
52
+ return async function POST(request) {
53
+ const operation = Effect.gen(function* () {
54
+ const origin = new URL(request.url).origin;
55
+ if (request.method !== "POST" ||
56
+ request.headers.get("origin") !== origin ||
57
+ request.headers.get("sec-fetch-site") === "cross-site") {
58
+ return yield* Effect.fail(new ProxyForbidden());
59
+ }
60
+ const authorize = config.authorize;
61
+ if (authorize) {
62
+ const allowed = yield* Effect.tryPromise({
63
+ try: async () => authorize(request),
64
+ catch: (cause) => new ProxyAuthorizationError({ cause }),
65
+ });
66
+ if (!allowed) {
67
+ return yield* Effect.fail(new ProxyForbidden());
68
+ }
69
+ }
70
+ const envelope = yield* readEnvelope(request);
71
+ const settings = yield* Effect.try(() => resolveSettings(config));
72
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.withScope);
73
+ const upstream = yield* HttpClientRequest.post(new URL("/v1/events", settings.apiUrl).toString()).pipe(HttpClientRequest.bearerToken(settings.apiKey), HttpClientRequest.bodyJson(envelope));
74
+ const response = yield* Effect.scoped(client.execute(upstream)).pipe(Effect.timeout("10 seconds"));
75
+ return Match.value(response.status).pipe(Match.when(202, () => Response.json({ accepted: true }, { status: 202 })), Match.when((status) => [400, 409, 429].includes(status), (status) => Response.json({ error: "Workflow event rejected." }, { status })), Match.orElse(() => {
76
+ console.error(`[Flowmark proxy] Ingestion returned HTTP ${response.status}`);
77
+ return Response.json({ error: "Workflow delivery is unavailable." }, { status: 502 });
78
+ }));
79
+ }).pipe(Effect.catchTags({
80
+ ProxyForbidden: () => Effect.succeed(Response.json({ error: "Workflow proxy access denied." }, { status: 403 })),
81
+ InvalidProxyInput: () => Effect.succeed(Response.json({ error: "Invalid workflow event (maximum 64 KiB)." }, { status: 400 })),
82
+ }), Effect.provide(FetchHttpClient.layer));
83
+ const result = await Effect.runPromiseExit(operation, {
84
+ signal: request.signal,
85
+ });
86
+ return Exit.match(result, {
87
+ onSuccess: (response) => response,
88
+ onFailure: (cause) => {
89
+ console.error("[Flowmark proxy] Delivery failed", cause);
90
+ return Response.json({ error: "Workflow delivery is unavailable." }, { status: 502 });
91
+ },
92
+ });
93
+ };
94
+ }
@@ -0,0 +1,52 @@
1
+ import type { FlowmarkClient, InstrumentationErrorHandler } from "../types/index.js";
2
+ export type { Flow, FlowmarkClient, WorkflowDoneOptions, WorkflowFailOptions, WorkflowOptions, WorkflowOperation, WorkflowOperationOptions, WorkflowRunOptions, WorkflowStepOptions, } from "../types/index.js";
3
+ export type { JsonValue, WorkflowRunId, WorkflowError, WorkflowCorrelationId, WorkflowFailure, WorkflowProperties, WorkflowProgress, WorkflowOperationFailure, WorkflowResult, } from "@flowmarkdev/core";
4
+ /** Browser options: a public browser key for direct delivery, or a same-origin proxy path. Never include server secrets. */
5
+ export interface BrowserFlowmarkConfig {
6
+ /** Same-origin absolute proxy path, default "/api/flowmark". External URLs, queries, and fragments are rejected. */
7
+ readonly proxyPath?: string;
8
+ /**
9
+ * Public browser-only project key for direct delivery without your own backend.
10
+ * Never supply a secret server write key. Requires apiUrl; cannot be combined with proxyPath.
11
+ */
12
+ readonly publicKey?: string;
13
+ /** Flowmark API origin for direct delivery. Required with publicKey; never an ingestion path. */
14
+ readonly apiUrl?: string;
15
+ /** Optional deployment environment sent with each started workflow. */
16
+ readonly environment?: string;
17
+ /** Optional release/version sent with each started workflow. */
18
+ readonly release?: string;
19
+ /** Receives typed delivery failures. If omitted, warnings are logged without interrupting UI code. */
20
+ readonly onInstrumentationError?: InstrumentationErrorHandler;
21
+ }
22
+ /**
23
+ * Defines browser configuration with autocomplete and preserved literal types.
24
+ * Returns the same object without accessing browser globals or creating a client.
25
+ * Safe during SSR; validation happens when the client starts its first workflow.
26
+ * Export from flowmark.config.ts and pass to createFlowmark(); the file is not auto-loaded.
27
+ * Only public browser keys belong here, never secret server keys.
28
+ * @example
29
+ * export default defineConfig({
30
+ * publicKey: "fm_browser_...",
31
+ * apiUrl: "http://localhost:3003",
32
+ * })
33
+ */
34
+ export declare function defineConfig<const T extends BrowserFlowmarkConfig>(config: T & Record<Exclude<keyof T, keyof BrowserFlowmarkConfig>, never>): T;
35
+ /**
36
+ * Creates a browser client. Works in ordinary React/Vite apps as well as Next.js.
37
+ * Pass publicKey and apiUrl to send directly to Flowmark without your own backend.
38
+ * Without these options, uses your same-origin backend proxy at /api/flowmark.
39
+ * Never pass a secret server write key or import `@flowmarkdev/node` / `@flowmarkdev/node/next` here.
40
+ * Safe to create during SSR, but call workflow() only in browser event handlers or effects.
41
+ * Delivery starts automatically and uses keepalive; offline/unload delivery is not guaranteed.
42
+ * No config file, React provider, or Next.js dependency is required.
43
+ * @param config Public browser key/API origin for direct delivery, or a proxy path; optional error callback.
44
+ * @example
45
+ * import { createFlowmark } from "@flowmarkdev/node/browser"
46
+ * const flowmark = createFlowmark({ publicKey: "fm_browser_...", apiUrl: "https://your-flowmark-api" })
47
+ * // Inside a click/submit handler:
48
+ * const flow = flowmark.workflow("checkout.ui")
49
+ * flow.step("form-submitted")
50
+ * flow.done()
51
+ */
52
+ export declare function createFlowmark(config?: BrowserFlowmarkConfig): FlowmarkClient;
@@ -0,0 +1,115 @@
1
+ import { FetchHttpClient } from "@effect/platform";
2
+ import { Either, Layer, Redacted, Schema } from "effect";
3
+ import { makeHttpTransport } from "../features/delivery/adapters/http-transport.js";
4
+ import { FlowmarkConfigError } from "../errors/index.js";
5
+ import { makeClient } from "../client/runtime.js";
6
+ import { WorkflowTransport } from "../features/delivery/ports/workflow-transport.js";
7
+ import { ApiOriginSchema } from "../config/api-origin.js";
8
+ /**
9
+ * Defines browser configuration with autocomplete and preserved literal types.
10
+ * Returns the same object without accessing browser globals or creating a client.
11
+ * Safe during SSR; validation happens when the client starts its first workflow.
12
+ * Export from flowmark.config.ts and pass to createFlowmark(); the file is not auto-loaded.
13
+ * Only public browser keys belong here, never secret server keys.
14
+ * @example
15
+ * export default defineConfig({
16
+ * publicKey: "fm_browser_...",
17
+ * apiUrl: "http://localhost:3003",
18
+ * })
19
+ */
20
+ export function defineConfig(config) {
21
+ return config;
22
+ }
23
+ /**
24
+ * Creates a browser client. Works in ordinary React/Vite apps as well as Next.js.
25
+ * Pass publicKey and apiUrl to send directly to Flowmark without your own backend.
26
+ * Without these options, uses your same-origin backend proxy at /api/flowmark.
27
+ * Never pass a secret server write key or import `@flowmarkdev/node` / `@flowmarkdev/node/next` here.
28
+ * Safe to create during SSR, but call workflow() only in browser event handlers or effects.
29
+ * Delivery starts automatically and uses keepalive; offline/unload delivery is not guaranteed.
30
+ * No config file, React provider, or Next.js dependency is required.
31
+ * @param config Public browser key/API origin for direct delivery, or a proxy path; optional error callback.
32
+ * @example
33
+ * import { createFlowmark } from "@flowmarkdev/node/browser"
34
+ * const flowmark = createFlowmark({ publicKey: "fm_browser_...", apiUrl: "https://your-flowmark-api" })
35
+ * // Inside a click/submit handler:
36
+ * const flow = flowmark.workflow("checkout.ui")
37
+ * flow.step("form-submitted")
38
+ * flow.done()
39
+ */
40
+ export function createFlowmark(config = {}) {
41
+ let client;
42
+ let closed = false;
43
+ const getClient = () => {
44
+ if (closed) {
45
+ throw new FlowmarkConfigError({ cause: "Client has been shut down" });
46
+ }
47
+ if (!client) {
48
+ const location = globalThis.location;
49
+ if (!location) {
50
+ throw new FlowmarkConfigError({
51
+ cause: "Browser workflows must run in the browser, not during server rendering",
52
+ });
53
+ }
54
+ const direct = config.publicKey !== undefined || config.apiUrl !== undefined;
55
+ let endpoint;
56
+ let publicKey;
57
+ if (direct) {
58
+ if (config.proxyPath !== undefined) {
59
+ throw new FlowmarkConfigError({
60
+ cause: "Choose either direct browser delivery or proxyPath, not both",
61
+ });
62
+ }
63
+ const settings = Either.match(Schema.decodeUnknownEither(Schema.Struct({
64
+ publicKey: Schema.NonEmptyString,
65
+ apiUrl: ApiOriginSchema,
66
+ }))({ publicKey: config.publicKey, apiUrl: config.apiUrl }), {
67
+ onLeft: (cause) => {
68
+ throw new FlowmarkConfigError({ cause });
69
+ },
70
+ onRight: (value) => value,
71
+ });
72
+ endpoint = new URL("/v1/browser-events", settings.apiUrl);
73
+ publicKey = settings.publicKey;
74
+ }
75
+ else {
76
+ const path = config.proxyPath ?? "/api/flowmark";
77
+ if (!path.startsWith("/") ||
78
+ path.startsWith("//") ||
79
+ path.includes("\\") ||
80
+ path.includes("?") ||
81
+ path.includes("#")) {
82
+ throw new FlowmarkConfigError({
83
+ cause: "proxyPath must be a same-origin absolute path",
84
+ });
85
+ }
86
+ endpoint = new URL(path, location.origin);
87
+ if (endpoint.origin !== location.origin) {
88
+ throw new FlowmarkConfigError({
89
+ cause: "Proxy must be on the same origin",
90
+ });
91
+ }
92
+ }
93
+ const http = FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.RequestInit, {
94
+ credentials: direct ? "omit" : "same-origin",
95
+ mode: direct ? "cors" : "same-origin",
96
+ keepalive: true,
97
+ })));
98
+ client = makeClient(Layer.effect(WorkflowTransport, makeHttpTransport({
99
+ endpoint: endpoint.toString(),
100
+ ...(publicKey === undefined
101
+ ? {}
102
+ : { apiKey: Redacted.make(publicKey) }),
103
+ })).pipe(Layer.provide(http)), config.onInstrumentationError, { environment: config.environment, release: config.release });
104
+ }
105
+ return client;
106
+ };
107
+ return {
108
+ workflow: (name, options) => getClient().workflow(name, options),
109
+ flush: () => client?.flush() ?? Promise.resolve(),
110
+ shutdown() {
111
+ closed = true;
112
+ return client?.shutdown() ?? Promise.resolve();
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,21 @@
1
+ import type { Flow, FlowmarkClient, FlowmarkConfig, WorkflowOptions } from "../types/index.js";
2
+ import type { WorkflowName } from "@flowmarkdev/core";
3
+ /**
4
+ * Node server client. Import this from `@flowmarkdev/node` in workers and scripts.
5
+ * Next.js Route Handlers must import createFlowmark from `@flowmarkdev/node/next`.
6
+ * Browser code must import it from `@flowmarkdev/node/browser`.
7
+ * Reads FLOWMARK_API_KEY and FLOWMARK_API_URL; never put those in NEXT_PUBLIC_*.
8
+ * Call shutdown() before process exit. Create one shared client.
9
+ * @throws FlowmarkConfigError If credentials or API origin are invalid.
10
+ */
11
+ export declare function createFlowmark(config?: FlowmarkConfig): FlowmarkClient;
12
+ /**
13
+ * Advanced entry point for a custom Effect transport, primarily tests and custom integrations.
14
+ * Use createFlowmark().workflow() for ordinary application code.
15
+ * Delivery resources are drained and released after terminal completion. If
16
+ * operations are open, done() waits for their handles to close first.
17
+ * @param name Stable workflow name.
18
+ * @param options Explicit execution ID, transport, and optional diagnostics/metadata.
19
+ * @throws FlowmarkConfigError If no transport is supplied.
20
+ */
21
+ export declare function workflow(name: WorkflowName, options: WorkflowOptions): Flow;