@ontemper/edi 1.2.0-beta.3 → 1.2.0-beta.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ ## [1.1.7](https://github.com/ontemper/temper/releases/tag/%40ontemper%2Fedi-v1.1.7) (2026-07-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * stamp zero acknowledgment envelope control numbers ([#2421](https://github.com/ontemper/temper/pull/2421))
7
+
8
+ ## [1.0.13](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-edi-sdk-v1.0.12...unnbound-edi-sdk-v1.0.13) (2025-11-25)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * setup ci/cd ([dd2640f](https://github.com/unnbounddev/unnbound-sdks/commit/dd2640f01e39f1342f3e08dc882164b50331c7bb))
14
+ * setup ci/cd ([520c03a](https://github.com/unnbounddev/unnbound-sdks/commit/520c03aacd978d7923b3bcf45c4f74a870be019d))
15
+
16
+ ## [1.0.12](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-edi-sdk-v1.0.11...unnbound-edi-sdk-v1.0.12) (2025-11-25)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * remove unused files ([6dc00c2](https://github.com/unnbounddev/unnbound-sdks/commit/6dc00c20b092b17e9eb2108ef83dc5beafdef3f5))
22
+ * setup ci/cd ([d3759c1](https://github.com/unnbounddev/unnbound-sdks/commit/d3759c14f7863e4c8d2f5e80bdbbf2446df79f1a))
23
+ * setup ci/cd ([7e06fc8](https://github.com/unnbounddev/unnbound-sdks/commit/7e06fc84ae7bafba3e9c2c0225f9f9d614fa9207))
package/README.md CHANGED
@@ -354,21 +354,20 @@ run().catch(console.error);
354
354
 
355
355
  The SDK manages ISA13 and GS06 control numbers for each sender and receiver pair. `toX12()` automatically stamps
356
356
  empty or all-zero ISA13, GS06, and ST02 fields. It preserves valid non-zero values. ISA13 and GS06 use the
357
- centralized counters. ST02 is assigned `"0001"`.
357
+ centralized counter service. ST02 is assigned `"0001"`.
358
358
 
359
- Each Temper environment now has its own ISA13 and GS06 counter ledger.
359
+ Each Temper environment has a separate counter ledger. Production allocations continue the partner's production
360
+ sequence. Sandbox and staging allocations do not consume production control numbers. A ledger rolls over after
361
+ `999999999`, so the next value is `000000001`.
360
362
 
361
363
  ### Automatic environment routing
362
364
 
363
- Automatic allocation reads `UNNBOUND_ENVIRONMENT`. Its value must be exactly `sandbox`, `staging`, or
364
- `production`. If the variable is missing or invalid, the SDK throws a `ControlNumberError` with the
365
- `control_number_environment_missing` code before making an HTTP request. This fail-closed behavior prevents an
366
- automatic allocation from using the wrong environment.
365
+ Temper injects `TEMPER_ENVIRONMENT` into workflows. The SDK uses `UNNBOUND_ENVIRONMENT` only when
366
+ `TEMPER_ENVIRONMENT` is absent. The value must be `sandbox`, `staging`, or `production`. A missing or invalid value
367
+ causes a `ControlNumberError` with the `control_number_environment_missing` code before any HTTP request.
367
368
 
368
- `getNextControlNumber()` and `toX12()` always use this ambient environment. ISA15 (`UsageIndicator_15`) does not
369
- select a counter, and `toX12()` does not change it.
370
-
371
- ### Manage counters directly
369
+ `getNextControlNumber()`, `getCurrentControlNumber()`, and `toX12()` use this environment. They do not accept an
370
+ environment override. ISA15 (`UsageIndicator_15`) does not select a counter, and `toX12()` does not change it.
372
371
 
373
372
  ```typescript
374
373
  import { getCurrentControlNumber, getNextControlNumber, setControlNumber } from '@ontemper/edi';
@@ -379,25 +378,18 @@ const receiverId = 'ZZ:RECEIVER';
379
378
  const next = await getNextControlNumber('ISA13', senderId, receiverId);
380
379
  const current = await getCurrentControlNumber('ISA13', senderId, receiverId);
381
380
 
382
- // A sandbox onboarding workflow can inspect and seed the production ledger explicitly.
383
- const productionCurrent = await getCurrentControlNumber('ISA13', senderId, receiverId, 9, {
384
- environment: 'production',
385
- });
386
- await setControlNumber('ISA13', senderId, receiverId, 4600, 9, {
387
- environment: 'production',
388
- });
381
+ // Legacy production migration: store 4599 as the last-used value.
382
+ await setControlNumber('ISA13', senderId, receiverId, 4599);
389
383
  ```
390
384
 
391
- `getCurrentControlNumber()` and `setControlNumber()` accept an optional
392
- `{ environment: 'sandbox' | 'staging' | 'production' }` argument for cross-environment inspection and seeding.
393
- An explicit environment takes precedence over `UNNBOUND_ENVIRONMENT`. Without this option, both methods require
394
- the ambient environment. `getNextControlNumber()` accepts no environment override.
385
+ `setControlNumber()` is the legacy production setter. Its value is the last-used number, so setting `4599` makes the
386
+ next allocation `4600`. It uses the flat production route and does not use the environment variables.
395
387
 
396
- A seed is the last-used value: seeding 4600 makes the next allocation 4601. Seed a counter only before its first
397
- increment. A repeated seed with the same stored value is idempotent; a different seed after the counter starts
398
- throws a `ControlNumberError` with the `counter_already_started` code.
388
+ Use the Builder agent `set_edi_control_number` tool for per-environment seeding. The tool value is the next number.
389
+ For example, seeding `4600` makes the next allocation `4600`. Seeding is forward-only. Re-seeding the same value is
390
+ a no-op.
399
391
 
400
- These helpers require `UNNBOUND_API_URL`, which is preconfigured in workflow environments.
392
+ These helpers require `UNNBOUND_API_URL`. Temper configures it in workflow environments.
401
393
 
402
394
  ## Requirements
403
395
 
@@ -405,6 +397,7 @@ These helpers require `UNNBOUND_API_URL`, which is preconfigured in workflow env
405
397
  - TypeScript (for TypeScript projects)
406
398
  - The EDI connection env — `UNNBOUND_EDI_BASE_URL`, plus `UNNBOUND_EDI_API_KEY` where the path needs a credential — is platform-injected per environment in Temper workflows; nothing to configure. Supply a key yourself only for the standalone legacy EDINation-cloud path.
407
399
  - `UNNBOUND_API_URL` (for control number auto-stamping, pre-configured in workflow environments)
400
+ - `TEMPER_ENVIRONMENT` for counter routing, with `UNNBOUND_ENVIRONMENT` as the legacy fallback
408
401
 
409
402
  ## Dependencies
410
403
 
@@ -3,11 +3,7 @@ export type TemperEnvironment = 'sandbox' | 'staging' | 'production';
3
3
  /** @public */
4
4
  export type ControlNumberCounterName = 'ISA13' | 'GS06';
5
5
  /** @public */
6
- export interface ControlNumberEnvironmentOptions {
7
- readonly environment?: TemperEnvironment;
8
- }
9
- /** @public */
10
- export type ControlNumberErrorCode = 'control_number_environment_missing' | 'counter_already_started' | 'control_number_bad_request' | 'control_number_unauthorized' | 'control_number_unknown_error';
6
+ export type ControlNumberErrorCode = 'counter_already_started' | 'control_number_environment_missing' | 'control_number_bad_request' | 'control_number_unauthorized' | 'control_number_unknown_error';
11
7
  interface ControlNumberErrorOptions extends ErrorOptions {
12
8
  code: ControlNumberErrorCode;
13
9
  message: string;
@@ -18,12 +14,12 @@ export declare class ControlNumberError extends Error {
18
14
  constructor({ code, message, ...options }: ControlNumberErrorOptions);
19
15
  }
20
16
  /** @public */
21
- export declare const isControlNumberError: (error: unknown) => error is ControlNumberError;
17
+ export declare const isControlNumberError: (error: ErrorOptions['cause']) => error is ControlNumberError;
22
18
  /**
23
19
  * Get the next control number for an EDI counter.
24
20
  *
25
- * Calls the API-hosted Restate counter object which persists state
26
- * and handles rollover (999999999 1) automatically.
21
+ * The centralized service isolates counters by Temper environment.
22
+ * It rolls 999999999 over to 1.
27
23
  *
28
24
  * @param counterName - Counter type: 'ISA13' or 'GS06'
29
25
  * @param senderId - Sender qualifier + ID (e.g. "ZZ:SENDER123")
@@ -36,10 +32,12 @@ export declare function getNextControlNumber(counterName: ControlNumberCounterNa
36
32
  * Get the current control number value without incrementing.
37
33
  */
38
34
  /** @public */
39
- export declare function getCurrentControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, maxDigits?: number, options?: ControlNumberEnvironmentOptions): Promise<string>;
35
+ export declare function getCurrentControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, maxDigits?: number): Promise<string>;
40
36
  /**
41
- * Set a control number to a specific value before the counter starts (for migration/seeding).
37
+ * Set the last-used production control number through the legacy endpoint.
38
+ *
39
+ * Use the Builder agent `set_edi_control_number` tool for per-environment seeding.
42
40
  */
43
41
  /** @public */
44
- export declare function setControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, value: number, maxDigits?: number, options?: ControlNumberEnvironmentOptions): Promise<string>;
42
+ export declare function setControlNumber(counterName: ControlNumberCounterName, senderId: string, receiverId: string, value: number, maxDigits?: number): Promise<string>;
45
43
  export {};
@@ -79,19 +79,26 @@ exports.ControlNumberError = ControlNumberError;
79
79
  const isControlNumberError = (error) => error instanceof ControlNumberError;
80
80
  exports.isControlNumberError = isControlNumberError;
81
81
  const requireEnvironment = () => {
82
- const environment = process.env.UNNBOUND_ENVIRONMENT;
82
+ const environment = process.env.TEMPER_ENVIRONMENT ?? process.env.UNNBOUND_ENVIRONMENT;
83
83
  if (environment === 'sandbox' || environment === 'staging' || environment === 'production') {
84
84
  return environment;
85
85
  }
86
86
  throw new ControlNumberError({
87
87
  code: 'control_number_environment_missing',
88
- message: "UNNBOUND_ENVIRONMENT must be set to 'sandbox', 'staging', or 'production' because automatic control-number routing needs it.",
88
+ message: "TEMPER_ENVIRONMENT or UNNBOUND_ENVIRONMENT must be set to 'sandbox', 'staging', or 'production' because automatic control-number routing needs it.",
89
89
  });
90
90
  };
91
- function unwrapControlNumberError(error) {
91
+ function unwrapControlNumberError(error, environmentScoped = false) {
92
92
  if (error instanceof ControlNumberError)
93
93
  throw error;
94
94
  if ((0, axios_1.isAxiosError)(error)) {
95
+ if (environmentScoped && error.response?.status === 404) {
96
+ throw new ControlNumberError({
97
+ code: 'control_number_unknown_error',
98
+ message: 'The Temper API does not support environment-scoped EDI control numbers yet.',
99
+ cause: error,
100
+ });
101
+ }
95
102
  if (error.response?.status === 409) {
96
103
  throw new ControlNumberError({
97
104
  code: 'counter_already_started',
@@ -102,7 +109,7 @@ function unwrapControlNumberError(error) {
102
109
  if (error.response?.status === 400) {
103
110
  throw new ControlNumberError({
104
111
  code: 'control_number_bad_request',
105
- message: "Invalid EDI control number request. Use counter name 'ISA13' or 'GS06' with trading partner IDs and, for set, a value.",
112
+ message: "Invalid EDI control number request. Use counter name 'ISA13' or 'GS06' with trading partner IDs.",
106
113
  cause: error,
107
114
  });
108
115
  }
@@ -120,25 +127,24 @@ function unwrapControlNumberError(error) {
120
127
  cause: error,
121
128
  });
122
129
  }
123
- async function requestControlNumber(operation, input, environment) {
124
- const request = { ...input, environment: environment ?? requireEnvironment() };
130
+ async function requestControlNumber(operation, input) {
131
+ const environment = requireEnvironment();
132
+ const path = `/api/v2/internal/edi/control-numbers/${environment}/${operation}`;
125
133
  try {
126
134
  const response = operation === 'count'
127
- ? await getApiClient().get(`/api/v2/internal/edi/control-numbers/${operation}`, {
128
- params: request,
129
- })
130
- : await getApiClient().post(`/api/v2/internal/edi/control-numbers/${operation}`, request);
135
+ ? await getApiClient().get(path, { params: input })
136
+ : await getApiClient().post(path, input);
131
137
  return response.data;
132
138
  }
133
139
  catch (error) {
134
- return unwrapControlNumberError(error);
140
+ return unwrapControlNumberError(error, true);
135
141
  }
136
142
  }
137
143
  /**
138
144
  * Get the next control number for an EDI counter.
139
145
  *
140
- * Calls the API-hosted Restate counter object which persists state
141
- * and handles rollover (999999999 1) automatically.
146
+ * The centralized service isolates counters by Temper environment.
147
+ * It rolls 999999999 over to 1.
142
148
  *
143
149
  * @param counterName - Counter type: 'ISA13' or 'GS06'
144
150
  * @param senderId - Sender qualifier + ID (e.g. "ZZ:SENDER123")
@@ -157,22 +163,29 @@ async function getNextControlNumber(counterName, senderId, receiverId, maxDigits
157
163
  * Get the current control number value without incrementing.
158
164
  */
159
165
  /** @public */
160
- async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9, options) {
166
+ async function getCurrentControlNumber(counterName, senderId, receiverId, maxDigits = 9) {
161
167
  const data = await requestControlNumber('count', {
162
168
  tradingPartnerKey: `${senderId}:${receiverId}`,
163
169
  counterName,
164
- }, options?.environment);
170
+ });
165
171
  return String(data.value).padStart(maxDigits, '0');
166
172
  }
167
173
  /**
168
- * Set a control number to a specific value before the counter starts (for migration/seeding).
174
+ * Set the last-used production control number through the legacy endpoint.
175
+ *
176
+ * Use the Builder agent `set_edi_control_number` tool for per-environment seeding.
169
177
  */
170
178
  /** @public */
171
- async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9, options) {
172
- const data = await requestControlNumber('set', {
173
- tradingPartnerKey: `${senderId}:${receiverId}`,
174
- counterName,
175
- value,
176
- }, options?.environment);
177
- return String(data.value).padStart(maxDigits, '0');
179
+ async function setControlNumber(counterName, senderId, receiverId, value, maxDigits = 9) {
180
+ try {
181
+ const { data } = await getApiClient().post('/api/v2/internal/edi/control-numbers/set', {
182
+ tradingPartnerKey: `${senderId}:${receiverId}`,
183
+ counterName,
184
+ value,
185
+ });
186
+ return String(data.value).padStart(maxDigits, '0');
187
+ }
188
+ catch (error) {
189
+ return unwrapControlNumberError(error);
190
+ }
178
191
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { X12Interchange } from './edination-client';
2
- export type { ControlNumberCounterName, ControlNumberEnvironmentOptions, ControlNumberErrorCode, } from './control-numbers';
1
+ import { type X12Interchange } from './edination-client';
2
+ export type { ControlNumberErrorCode } from './control-numbers';
3
3
  export { ControlNumberError, getCurrentControlNumber, getNextControlNumber, isControlNumberError, setControlNumber, } from './control-numbers';
4
4
  export * from './edination-client/model';
5
5
  interface UnnboundErrorOptions<C extends string = string> extends ErrorOptions {
@@ -14,7 +14,7 @@ export type TemperEdiClientErrorCode = 'edi_read_error' | 'edi_write_error' | 'e
14
14
  export declare class TemperEdiClientError extends UnnboundError<TemperEdiClientErrorCode> {
15
15
  constructor({ message, code, ...o }: UnnboundErrorOptions<TemperEdiClientErrorCode>);
16
16
  }
17
- export declare const isTemperEdiClientError: (error: unknown) => error is TemperEdiClientError;
17
+ export declare const isTemperEdiClientError: (error: ErrorOptions['cause']) => error is TemperEdiClientError;
18
18
  /**
19
19
  * A retryable failure that does not indicate an invalid EDI document.
20
20
  */
@@ -22,7 +22,7 @@ export declare class EdiInfrastructureError extends UnnboundError<'infrastructur
22
22
  readonly retryable = true;
23
23
  constructor({ message, ...o }: Omit<UnnboundErrorOptions<'infrastructure_error'>, 'code'>);
24
24
  }
25
- export declare const isEdiInfrastructureError: (error: unknown) => error is EdiInfrastructureError;
25
+ export declare const isEdiInfrastructureError: (error: ErrorOptions['cause']) => error is EdiInfrastructureError;
26
26
  export interface FromX12Options {
27
27
  input: unknown;
28
28
  }
@@ -43,12 +43,12 @@ export declare class TemperEdiClient {
43
43
  fromX12({ input }: FromX12Options): Promise<X12Interchange[]>;
44
44
  /**
45
45
  * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
46
- * Uses the Restate-backed counter service for atomic, per-trading-partner sequencing.
46
+ * Uses the centralized per-environment service for atomic partner sequencing.
47
47
  * ST02 is always "0001" (one transaction per file per ticket).
48
48
  * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
49
49
  */
50
50
  private stampControlNumbers;
51
- toX12({ input }: ToX12Options): Promise<unknown>;
51
+ toX12({ input }: ToX12Options): Promise<File>;
52
52
  validateX12({ input }: ValidateX12Options): Promise<import("./edination-client").OperationResult>;
53
53
  acknowledgeX12({ input }: AcknolwedgeX12Options): Promise<X12Interchange[]>;
54
54
  }
package/dist/index.js CHANGED
@@ -118,12 +118,13 @@ class TemperEdiClient {
118
118
  });
119
119
  // Gateway normalises EdiFabric's camelCase to PascalCase before forwarding
120
120
  const responseData = error.response?.data;
121
- const details = typeof responseData === 'object' &&
122
- responseData !== null &&
121
+ let details;
122
+ if (responseData instanceof Object &&
123
123
  'Details' in responseData &&
124
- Array.isArray(responseData.Details)
125
- ? responseData.Details
126
- : undefined;
124
+ Array.isArray(responseData.Details) &&
125
+ responseData.Details.every((detail) => String(detail) === detail)) {
126
+ details = responseData.Details.map(String);
127
+ }
127
128
  if (status === 400 && details?.length) {
128
129
  throw new TemperEdiClientError({
129
130
  message: `EDI validation failed: ${details.join('; ')}`,
@@ -143,6 +144,7 @@ class TemperEdiClient {
143
144
  }
144
145
  fromX12({ input }) {
145
146
  return (0, unnbound_logger_sdk_1.startSpan)('X12 to JSON', () => {
147
+ // SAFETY: generated client accepts File; Buffer/Blob inputs are valid File-like bodies
146
148
  return this.X12.x12ReadPost({ body: input })
147
149
  .then(this.unwrap.bind(this))
148
150
  .catch((error) => this.unwrapError(error, 'edi_read_error'));
@@ -150,7 +152,7 @@ class TemperEdiClient {
150
152
  }
151
153
  /**
152
154
  * Stamp ISA13/GS06/ST02 control numbers on the interchange if not already set.
153
- * Uses the Restate-backed counter service for atomic, per-trading-partner sequencing.
155
+ * Uses the centralized per-environment service for atomic partner sequencing.
154
156
  * ST02 is always "0001" (one transaction per file per ticket).
155
157
  * Zero-filled values from acknowledgment generation are placeholders, not manual overrides.
156
158
  */
@@ -199,12 +201,11 @@ class TemperEdiClient {
199
201
  }
200
202
  }
201
203
  async toX12({ input }) {
204
+ // Auto-stamp control numbers before writing X12
202
205
  try {
203
206
  await this.stampControlNumbers(input);
204
207
  }
205
208
  catch (error) {
206
- if (error instanceof control_numbers_1.ControlNumberError)
207
- throw error;
208
209
  // Workflows can still serialize documents whose control numbers were set manually.
209
210
  unnbound_logger_sdk_1.logger.warn({ err: error }, '[EDI SDK] Failed to auto-stamp control numbers, proceeding without');
210
211
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ontemper/edi",
3
3
  "description": "An EDI client with structured logging.",
4
- "version": "1.2.0-beta.3",
4
+ "version": "1.2.0-beta.4",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "Unnbound Team",
@@ -16,12 +16,13 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "axios": "1.18.0",
19
- "unnbound-logger-sdk": "3.0.37"
19
+ "unnbound-logger-sdk": "3.1.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/jest": "^29.5.12",
23
23
  "@types/node": "^24.12.2",
24
- "vitest": "^4.0.15"
24
+ "vitest": "^4.0.15",
25
+ "zod": "3.25.76"
25
26
  },
26
27
  "files": [
27
28
  "examples/*",