@pure01fx/dsh-openai-codex-auth 0.13.0 → 0.13.2

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.13.2
6
+
7
+ - Stops putting `is_error` on a replayed `function_call_output`. The Responses wire has no such field (Codex reports a failed tool through the output text and keeps its own `success` flag internal), one Lite model answers the unknown item field with a 400 `INVALID_REQUEST`, and because the item is replayed from durable history, a single failed tool call then fails every later request in that session.
8
+ - Logs the provider's own rejection code, message, status and request id when an HTTP/SSE failure cannot be classified into a known code, matching the existing WebSocket diagnostic, so an opaque `INVALID_REQUEST` names its cause in the operator log.
9
+
10
+ ## 0.13.1
11
+
12
+ - Serves a concurrent or pool-saturated request over HTTP/SSE instead of failing the turn.
13
+ - Moves a session to HTTP/SSE after a 400-class WebSocket rejection, so one rejected request can no longer fail every later turn in that session.
14
+ - Logs the provider's own rejection code, status and message instead of discarding them.
15
+ - Keeps a Fast alias resolvable when the catalog could not be read and retries one empty catalog read, so a transient catalog outage no longer fails the whole provider group in the model picker; a catalog that answers without Fast still refuses it.
16
+ - Memoizes the managed credential per credential-file signature with one shared resolution, so the model picker's per-model resolutions no longer take a dozen concurrent writer-lock acquisitions on the credential file (measured 2.1 s for 14 concurrent readers against a 2 s lock deadline before this change).
17
+ - Adds `locale/en.json` and `locale/zh.json` so the DSH 0.2.0 plugin panel shows a display name and description instead of the package id.
18
+
5
19
  ## 0.13.0
6
20
 
7
21
  - Targets DSH 0.2.0-rc.2 with exact SDK peers, development dependencies and runtime helpers; retains the tool-message, shell and provenance migration below.
package/lib/catalog.js CHANGED
@@ -265,7 +265,8 @@ export class NativeCodexCatalog {
265
265
  if (error instanceof LlmError && error.code === 'ABORTED')
266
266
  throw error;
267
267
  if (!(error instanceof LlmError) || error.code !== 'MISSING_CREDENTIAL') {
268
- this.options.warn?.('native Codex credential is unavailable; model metadata is advisory');
268
+ const code = error instanceof LlmError ? error.code : 'UNKNOWN';
269
+ this.options.warn?.(`native Codex credential is unavailable (${code}); model metadata is advisory`);
269
270
  }
270
271
  return { models: [] };
271
272
  }
package/lib/index.d.ts CHANGED
@@ -132,6 +132,8 @@ export declare class OpenAICodexAuth extends Service {
132
132
  private usageAccountId;
133
133
  private responseUsage;
134
134
  private credentialAccountId;
135
+ private credentialMemo;
136
+ private credentialResolution;
135
137
  private usageError;
136
138
  private usageRefresh;
137
139
  private usageGeneration;
@@ -163,6 +165,15 @@ export declare class OpenAICodexAuth extends Service {
163
165
  private commitCredential;
164
166
  /** Return the current managed bearer token, refreshing and migrating it when needed. */
165
167
  bearerToken(signal?: AbortSignal): Promise<string | undefined>;
168
+ /**
169
+ * Resolve the managed credential for one native call, reusing a lock-free memo of
170
+ * the file it was read from. The model picker resolves one credential per listed
171
+ * model, so one shared resolution replaces a burst of lock acquisitions on the
172
+ * same credential file.
173
+ */
174
+ private cachedManagedCredential;
175
+ /** Resolve once for every concurrent caller; never memo a credential the file no longer matches. */
176
+ private resolveManagedCredential;
166
177
  private managedCredential;
167
178
  private externalNativeCredential;
168
179
  private resolveNativeCredential;
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
6
6
  import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
7
7
  import { createServer } from 'node:http';
8
8
  import { createHash, randomBytes } from 'node:crypto';
9
- import { readFile, unlink } from 'node:fs/promises';
9
+ import { readFile, stat, unlink } from 'node:fs/promises';
10
10
  import { join, resolve } from 'node:path';
11
11
  import { INVALID_CREDENTIAL_CODE, LlmError } from '@deepseek-ai/dsh-llm';
12
12
  import { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER, NativeCodexAdapter, } from './native-adapter.js';
@@ -256,6 +256,26 @@ async function readCredentialDocument(filename) {
256
256
  throw error;
257
257
  }
258
258
  }
259
+ /**
260
+ * Cheap lock-free identity of the credential file. Every writer replaces it
261
+ * atomically, so both parts change and a cached read cannot outlive its file.
262
+ */
263
+ async function credentialFileSignature(filename) {
264
+ try {
265
+ const info = await stat(filename);
266
+ // An atomic replacement renames a fresh file, so the inode changes with it.
267
+ return `${info.ino}:${info.mtimeMs}:${info.size}`;
268
+ }
269
+ catch (error) {
270
+ if (error.code === 'ENOENT')
271
+ return 'missing';
272
+ throw error;
273
+ }
274
+ }
275
+ /** Whether a managed credential still avoids the pre-refresh window `bearerToken` enforces. */
276
+ function managedCredentialUsable(credential, now = Date.now()) {
277
+ return credential.expires > now + TOKEN_REFRESH_PREEMPT_MS;
278
+ }
259
279
  function currentCredential(document) {
260
280
  if (document?.currentAccountId === null || document === undefined)
261
281
  return undefined;
@@ -722,6 +742,8 @@ export class OpenAICodexAuth extends Service {
722
742
  usageAccountId;
723
743
  responseUsage;
724
744
  credentialAccountId;
745
+ credentialMemo;
746
+ credentialResolution;
725
747
  usageError;
726
748
  usageRefresh;
727
749
  usageGeneration = 0;
@@ -1036,6 +1058,8 @@ export class OpenAICodexAuth extends Service {
1036
1058
  throw failure;
1037
1059
  }
1038
1060
  async commitDocument(document, signal) {
1061
+ // A write may publish or replace the managed account, so no read memo survives it.
1062
+ this.credentialMemo = undefined;
1039
1063
  const nextToken = currentCredential(document)?.access;
1040
1064
  const previous = await this.ctx.credentials.resolve(TOKEN_REF);
1041
1065
  throwIfCancelled(signal);
@@ -1143,6 +1167,37 @@ export class OpenAICodexAuth extends Service {
1143
1167
  return access;
1144
1168
  }
1145
1169
  }
1170
+ /**
1171
+ * Resolve the managed credential for one native call, reusing a lock-free memo of
1172
+ * the file it was read from. The model picker resolves one credential per listed
1173
+ * model, so one shared resolution replaces a burst of lock acquisitions on the
1174
+ * same credential file.
1175
+ */
1176
+ async cachedManagedCredential(signal) {
1177
+ const signature = await credentialFileSignature(this.filename);
1178
+ const memo = this.credentialMemo;
1179
+ if (memo !== undefined && memo.signature === signature && managedCredentialUsable(memo.credential)) {
1180
+ this.setCredentialAccount(memo.credential.accountId);
1181
+ return memo.credential;
1182
+ }
1183
+ const pending = this.credentialResolution ??= this.resolveManagedCredential(signature);
1184
+ return await waitForSignal(pending, signal);
1185
+ }
1186
+ /** Resolve once for every concurrent caller; never memo a credential the file no longer matches. */
1187
+ resolveManagedCredential(signature) {
1188
+ const resolution = this.managedCredential().then(async (credential) => {
1189
+ if (credential !== undefined && await credentialFileSignature(this.filename) === signature) {
1190
+ this.credentialMemo = { signature, credential };
1191
+ }
1192
+ return credential;
1193
+ });
1194
+ const settled = () => {
1195
+ if (this.credentialResolution === resolution)
1196
+ this.credentialResolution = undefined;
1197
+ };
1198
+ void resolution.then(settled, settled);
1199
+ return resolution;
1200
+ }
1146
1201
  async managedCredential(signal) {
1147
1202
  while (true) {
1148
1203
  const access = await this.bearerToken(signal);
@@ -1172,7 +1227,7 @@ export class OpenAICodexAuth extends Service {
1172
1227
  async resolveNativeCredential(signal) {
1173
1228
  try {
1174
1229
  throwIfCancelled(signal);
1175
- const managed = await this.managedCredential(signal);
1230
+ const managed = await this.cachedManagedCredential(signal);
1176
1231
  if (managed !== undefined) {
1177
1232
  throwIfCancelled(signal);
1178
1233
  return { accessToken: managed.access, accountId: managed.accountId };
@@ -44,6 +44,13 @@ export declare class NativeCodexAdapter extends LlmAdapter {
44
44
  private assertNotAborted;
45
45
  providerInfo(provider: string): LlmProviderInfo;
46
46
  providerRetryPolicy(provider: string): ResolvedRetryPolicy;
47
+ /**
48
+ * Read model metadata, retrying once when a fetch returned nothing. The catalog
49
+ * caches nothing on failure, so a momentary outage otherwise empties every model
50
+ * list it touches; metadata is advisory and one extra request is cheaper than a
51
+ * provider that disappears from the picker until the next attempt.
52
+ */
53
+ private catalogModels;
47
54
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
48
55
  resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
49
56
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
@@ -189,9 +189,22 @@ export class NativeCodexAdapter extends LlmAdapter {
189
189
  this.assertProvider(provider);
190
190
  return NATIVE_RETRY_POLICY;
191
191
  }
192
+ /**
193
+ * Read model metadata, retrying once when a fetch returned nothing. The catalog
194
+ * caches nothing on failure, so a momentary outage otherwise empties every model
195
+ * list it touches; metadata is advisory and one extra request is cheaper than a
196
+ * provider that disappears from the picker until the next attempt.
197
+ */
198
+ async catalogModels(signal) {
199
+ const first = await this.catalog?.list(signal) ?? [];
200
+ if (first.length > 0)
201
+ return first;
202
+ this.assertNotAborted(signal);
203
+ return await this.catalog?.list(signal) ?? [];
204
+ }
192
205
  async listModels(provider) {
193
206
  this.assertProvider(provider);
194
- const models = await this.catalog?.list() ?? [];
207
+ const models = await this.catalogModels();
195
208
  const aliases = fastRoutes(models);
196
209
  return models
197
210
  .filter(model => model.visibility === 'list')
@@ -207,7 +220,7 @@ export class NativeCodexAdapter extends LlmAdapter {
207
220
  async resolveModel(provider, model, signal) {
208
221
  this.assertNotAborted(signal);
209
222
  this.assertProvider(provider);
210
- const models = await this.catalog?.list(signal) ?? [];
223
+ const models = await this.catalogModels(signal);
211
224
  this.assertNotAborted(signal);
212
225
  const exact = models.find(candidate => candidate.slug === model);
213
226
  if (exact !== undefined)
@@ -216,7 +229,14 @@ export class NativeCodexAdapter extends LlmAdapter {
216
229
  if (fast !== undefined)
217
230
  return resolvedModel(provider, fast, true);
218
231
  if (model.endsWith(CODEX_FAST_ALIAS_SUFFIX)) {
219
- throw new LlmError('native Codex Fast is not advertised for the selected model', 'FAST_UNSUPPORTED');
232
+ const base = model.slice(0, -CODEX_FAST_ALIAS_SUFFIX.length);
233
+ // Only a catalog that knows the base model can answer this negatively.
234
+ // Without any entry for it the metadata is missing rather than negative, and
235
+ // a caller that resolves every listed model (the picker) must not lose the
236
+ // whole provider group to an advisory fetch.
237
+ if (models.some(candidate => candidate.slug === base)) {
238
+ throw new LlmError('native Codex Fast is not advertised for the selected model', 'FAST_UNSUPPORTED');
239
+ }
220
240
  }
221
241
  return { provider, id: model, name: model };
222
242
  }
@@ -6,7 +6,7 @@ import { nativeCodexAuthorityHash } from './catalog.js';
6
6
  import { nativeCodexEndpoint } from './endpoint.js';
7
7
  import { retryAfterMs, retryError, remainingRetryDelay, sleepRetry, inheritRetryDeadline } from './retry-after.js';
8
8
  import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
9
- import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, responsesFailure, } from './responses.js';
9
+ import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, rejectionDiagnostic, streamResponses, responsesFailure, } from './responses.js';
10
10
  import { hasNativeCodexReplayKind } from './replay.js';
11
11
  import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
12
12
  import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.js';
@@ -246,7 +246,7 @@ function errorFacts(response) {
246
246
  : { requestId: ProviderRequestId(requestId) },
247
247
  };
248
248
  }
249
- async function httpFailure(response, signal, telemetry) {
249
+ async function httpFailure(response, signal, telemetry, warn) {
250
250
  const facts = errorFacts(response);
251
251
  const advice = retryError('native Codex HTTP retry advice', 'SERVER', facts);
252
252
  const fail = (message, code, options) => inheritRetryDeadline(fixedFailure(message, code, options), advice);
@@ -271,6 +271,15 @@ async function httpFailure(response, signal, telemetry) {
271
271
  if (response.status >= 500) {
272
272
  return fail('native Codex server request failed', 'SERVER', facts);
273
273
  }
274
+ // Every classified refusal above names itself. This branch is the only place
275
+ // where the provider's own reason would otherwise be discarded, so record it
276
+ // for the operator without turning it into the provider-neutral message.
277
+ warn?.(rejectionDiagnostic('HTTP', {
278
+ status: response.status,
279
+ ...detail.code === undefined ? {} : { code: detail.code },
280
+ ...detail.message === undefined ? {} : { message: detail.message },
281
+ ...facts?.requestId === undefined ? {} : { requestId: String(facts.requestId) },
282
+ }));
274
283
  return fail('native Codex request was rejected', 'INVALID_REQUEST', facts);
275
284
  }
276
285
  function mappedFailure(error, watchdog, parent) {
@@ -520,7 +529,7 @@ export class NativeCodexHttpTransport {
520
529
  }
521
530
  }
522
531
  if (!response.ok) {
523
- let failure = await httpFailure(response, watchdog.signal, telemetry);
532
+ let failure = await httpFailure(response, watchdog.signal, telemetry, this.options.warn);
524
533
  if (generation.signal?.aborted || watchdog.timedOut()) {
525
534
  failure = mappedFailure(failure, watchdog, generation.signal);
526
535
  }
@@ -9,7 +9,7 @@ import { NativeCodexHttpTransport, } from './native-http.js';
9
9
  import { replayableItemId } from './replay.js';
10
10
  import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
11
11
  import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.js';
12
- import { ResponsesStreamTranslator, responsesFailure, codexResponseTurnState, } from './responses.js';
12
+ import { ResponsesStreamTranslator, rejectionDiagnostic, responsesFailure, codexResponseTurnState, } from './responses.js';
13
13
  import { NodeNativeCodexWebSocketFactory, } from './native-websocket-socket.js';
14
14
  import { NativeCodexWebSocketSessionState } from './native-websocket-session.js';
15
15
  const WS_BETA = 'responses_websockets=2026-02-06';
@@ -191,6 +191,18 @@ function eventFailure(event) {
191
191
  }
192
192
  return retryError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
193
193
  }
194
+ /** Adapt one raw socket failure event to the shared operator-facing rejection line. */
195
+ function socketRejectionDiagnostic(event) {
196
+ const error = typeof event.error === 'object' && event.error !== null
197
+ ? event.error : event;
198
+ const requestId = eventHeader(event, 'x-request-id');
199
+ return rejectionDiagnostic('WebSocket', {
200
+ ...typeof event.status === 'number' ? { status: event.status } : {},
201
+ ...typeof error.code === 'string' ? { code: error.code } : {},
202
+ ...typeof error.message === 'string' ? { message: error.message } : {},
203
+ ...requestId === undefined ? {} : { requestId },
204
+ });
205
+ }
194
206
  /** Prefer WebSocket v2; once safe retries exhaust, keep that DSH session on HTTP. */
195
207
  export class NativeCodexWebSocketTransport {
196
208
  options;
@@ -399,8 +411,10 @@ export class NativeCodexWebSocketTransport {
399
411
  publishCodexRateLimits(accountId, parseCodexRateLimitHeaders(typeof rawEvent.headers === 'object' && rawEvent.headers !== null
400
412
  ? rawEvent.headers : undefined), this.options.onRateLimits, this.options.warn);
401
413
  const wrapped = eventFailure(rawEvent);
402
- if (wrapped !== undefined)
414
+ if (wrapped !== undefined) {
415
+ this.options.warn?.(socketRejectionDiagnostic(rawEvent));
403
416
  throw wrapped;
417
+ }
404
418
  if (entry.turnState === undefined) {
405
419
  const nextTurnState = codexResponseTurnState(event);
406
420
  if (nextTurnState !== undefined)
@@ -478,10 +492,16 @@ export class NativeCodexWebSocketTransport {
478
492
  throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
479
493
  const key = sessionKey(generation, prepared.routingId);
480
494
  const entry = this.entry(key);
481
- if (entry.busy)
482
- throw failure('native Codex WebSocket session already has an active request', 'CONCURRENT_REQUEST');
483
- if (this.active.size >= this.maxSessions) {
484
- throw failure('native Codex WebSocket active session limit was reached', 'WS_SESSION_LIMIT');
495
+ // A second request for the same DSH session (the session-title call racing
496
+ // the first step, for example) cannot share one socket chain, and a
497
+ // saturated pool has no socket to lend. Both still have a correct transport
498
+ // over HTTP/SSE, so neither may fail the turn.
499
+ if (entry.busy || this.active.size >= this.maxSessions) {
500
+ this.options.warn?.(entry.busy
501
+ ? 'native Codex WebSocket session already has an active request; serving this request over HTTP/SSE'
502
+ : 'native Codex WebSocket active session limit was reached; serving this request over HTTP/SSE');
503
+ yield* this.http.stream(activeGeneration, mode, telemetry);
504
+ return;
485
505
  }
486
506
  entry.busy = true;
487
507
  this.active.set(entry, lifecycle);
@@ -545,6 +565,16 @@ export class NativeCodexWebSocketTransport {
545
565
  throw failedStepRetry(failureValue);
546
566
  throw failureValue;
547
567
  }
568
+ // A rejected request is not a transient socket condition: retrying the
569
+ // socket repeats the rejection. The same payload has a correct
570
+ // transport over HTTP/SSE, so keep this DSH session on HTTP from now on
571
+ // instead of failing every later turn.
572
+ if (failureValue.code === 'INVALID_REQUEST') {
573
+ entry.disabled = true;
574
+ yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
575
+ requestCompleted = true;
576
+ return;
577
+ }
548
578
  // Credential resolution may need the network itself, so wait until it recovers.
549
579
  // Once a credential is available, however, a WebSocket-only outage must consume
550
580
  // the bounded reconnect budget and then fall back to HTTP/SSE instead of leaving
@@ -54,6 +54,19 @@ export interface ResponsesUsage {
54
54
  export declare function mapResponsesUsage(usage: ResponsesUsage): TokenUsage;
55
55
  /** Classify in-band failure data without reflecting provider text. */
56
56
  export declare function responsesFailure(code?: string, message?: string, headers?: unknown): LlmError;
57
+ export interface ProviderRejectionFacts {
58
+ status?: number;
59
+ code?: string;
60
+ message?: string;
61
+ requestId?: string;
62
+ }
63
+ /**
64
+ * One bounded operator-facing line naming the provider's own rejection, so a
65
+ * transport that cannot classify a refusal still records why it happened. The
66
+ * provider's free-form text stays in the log; it never becomes the
67
+ * provider-neutral error message.
68
+ */
69
+ export declare function rejectionDiagnostic(transport: 'HTTP' | 'WebSocket', facts: ProviderRejectionFacts): string;
57
70
  interface ResponsesOutputItem {
58
71
  type?: string;
59
72
  id?: string;
package/lib/responses.js CHANGED
@@ -77,7 +77,12 @@ export function toResponsesInput(messages, system) {
77
77
  type: 'function_call_output',
78
78
  call_id: String(message.toolCallId),
79
79
  output: toolOutput(message.content),
80
- ...message.isError === true ? { is_error: true } : {},
80
+ // The Responses wire has no error flag on a function call output: Codex
81
+ // keeps its own `success` metadata off the wire and reports a failed tool
82
+ // only through the output text. A Lite model rejects the unknown item
83
+ // field with a 400 INVALID_REQUEST, and because the item is replayed from
84
+ // durable history, every later request in that session fails the same way.
85
+ // `message.isError` stays DSH-side provenance for cloud context only.
81
86
  });
82
87
  continue;
83
88
  }
@@ -326,6 +331,23 @@ export function responsesFailure(code, message, headers) {
326
331
  ...delay === undefined ? {} : { providerRetryAfterMs: delay },
327
332
  });
328
333
  }
334
+ /**
335
+ * One bounded operator-facing line naming the provider's own rejection, so a
336
+ * transport that cannot classify a refusal still records why it happened. The
337
+ * provider's free-form text stays in the log; it never becomes the
338
+ * provider-neutral error message.
339
+ */
340
+ export function rejectionDiagnostic(transport, facts) {
341
+ const code = facts.code !== undefined && facts.code.length > 0 ? facts.code : undefined;
342
+ const message = (facts.message ?? '').replace(/\s+/g, ' ').trim().slice(0, 300);
343
+ const status = facts.status === undefined ? '' : ` status=${String(facts.status)}`;
344
+ const requestId = facts.requestId === undefined || facts.requestId.length === 0
345
+ ? '' : ` request_id=${facts.requestId}`;
346
+ const detail = [code, message.length === 0 ? undefined : message]
347
+ .filter((part) => part !== undefined).join(': ');
348
+ return `native Codex ${transport} rejected the request${status}${requestId}`
349
+ + `${detail.length === 0 ? '' : ` (${detail})`}`;
350
+ }
329
351
  function eventItemId(event) {
330
352
  if (typeof event.item_id === 'string' && event.item_id.length > 0)
331
353
  return event.item_id;
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "OpenAI Codex Auth",
4
+ "description": "Native ChatGPT Codex provider with device-code login, its own model catalog and usage reporting, wired into the DSH Web GUI from the same origin."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "OpenAI Codex 认证",
4
+ "description": "原生 ChatGPT Codex 提供方:设备码登录、自带的模型目录与用量统计,与 DSH Web GUI 同源集成。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure01fx/dsh-openai-codex-auth",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Native ChatGPT Codex provider, device-code-first login, and same-origin Web integration for DeepSeek Harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -28,9 +28,11 @@
28
28
  "default": "./lib/index.js"
29
29
  },
30
30
  "./client": "./client.js",
31
+ "./locale/*.json": "./locale/*.json",
31
32
  "./package.json": "./package.json"
32
33
  },
33
34
  "files": [
35
+ "locale",
34
36
  "lib/index.js",
35
37
  "lib/index.d.ts",
36
38
  "lib/catalog.js",