@avsbhq/node 1.2.0 → 1.5.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 CHANGED
@@ -93,6 +93,35 @@ const flag = server.getBoolFlag('checkout-v2', false, {
93
93
  })
94
94
  ```
95
95
 
96
+ ### Commerce targeting attributes
97
+
98
+ Audiences built with the commerce condition types (cart value, products viewed, purchase history) evaluate from attributes you supply on the context. The attribute names are fixed:
99
+
100
+ ```ts
101
+ const flag = server.getBoolFlag('big-cart-banner', false, {
102
+ context: {
103
+ kind: 'user',
104
+ key: 'u_123',
105
+ cartValue: 120.5, // decimal MAJOR units of the project currency
106
+ viewedSkus: ['SKU-1', 'SKU-2'], // for "products viewed" matching on SKU
107
+ viewedCategories: ['trainers'], // for "products viewed" matching on category
108
+ purchaseHistory: {
109
+ // PurchaseHistoryAttribute — exported for your own declarations
110
+ hasPurchased: true,
111
+ lastPurchaseAt: 1765500000000, // epoch milliseconds (optional)
112
+ lifetimeSpend: 540.0, // decimal MAJOR units (optional)
113
+ orderCount: 7, // optional
114
+ },
115
+ },
116
+ })
117
+ ```
118
+
119
+ Server-side semantics to be aware of:
120
+
121
+ - **All money is decimal major units.** Condition values are stored in minor units and converted using the project currency exponent from the datafile (default 2) — you never pass minor units.
122
+ - **Missing or mistyped attributes evaluate the condition to `false`.** A completely absent `purchaseHistory` makes ALL purchase-history operators false — including `has_not_purchased`. On the server, absence means "not provided", not "never purchased" (this deliberately diverges from the browser snippet, where a profile miss is authoritative).
123
+ - **Dataset-backed "products viewed" conditions evaluate `false` on the server.** Dataset view windows live in the browser; server callers use literal SKU/category lists. The session/30-day window distinction is also browser-only — supply whatever view list you consider current.
124
+
96
125
  ### `alias(previousContext, newContext)`
97
126
 
98
127
  Send an alias event linking two identities in the analytics pipeline. Typically called server-side when a user signs up or links an anonymous session to an account:
@@ -104,6 +133,10 @@ await server.alias(
104
133
  )
105
134
  ```
106
135
 
136
+ ### Sharing identity with the web snippet
137
+
138
+ To measure one metric across both the A vs B web snippet and this SDK (the setup behind cross-project metrics), the context `key` must equal the browser snippet's visitor id — the `_avsb_visitor` cookie UUID returned by `avsb.getVisitorId()`. Results join on raw id equality, so a different key means the two surfaces never line up. Forward the snippet id from the browser and use it as the key here, or link an internal account id to it with `alias()`. Full guide: [Shared Visitor Identity](https://avsb.cloud/docs/developer-reference/shared-visitor-identity).
139
+
107
140
  ---
108
141
 
109
142
  ## 5. Multi-context
package/dist/index.cjs CHANGED
@@ -40,6 +40,257 @@ module.exports = __toCommonJS(index_exports);
40
40
  var import_engine = require("@avsbhq/core/engine");
41
41
  var import_core2 = require("@avsbhq/core");
42
42
 
43
+ // src/datasets.ts
44
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
45
+ function miss(slug, version) {
46
+ return { found: false, meta: { slug, version } };
47
+ }
48
+ function createDatasetsClient(options) {
49
+ const { sdkKey, getDatafile, logger } = options;
50
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
51
+ async function get(slug, key) {
52
+ if (!slug || !key) {
53
+ logger.warn("datasets.get called with an empty slug or key \u2014 returning not found");
54
+ return miss(slug, null);
55
+ }
56
+ const datafile = getDatafile();
57
+ if (!datafile) {
58
+ logger.debug(`SDK not ready, returning not found for dataset "${slug}"`);
59
+ return miss(slug, null);
60
+ }
61
+ const dataset = datafile.datasets?.find((d) => d.slug === slug);
62
+ if (!dataset) {
63
+ logger.debug(`Dataset "${slug}" not present in datafile \u2014 returning not found`);
64
+ return miss(slug, null);
65
+ }
66
+ if (!dataset.liveVersion) {
67
+ logger.debug(`Dataset "${slug}" has no live version \u2014 returning not found`);
68
+ return miss(slug, null);
69
+ }
70
+ const base = datafile.collectEndpoint.replace(/\/+$/, "");
71
+ const url = `${base}/v1/ds/${encodeURIComponent(sdkKey)}/${encodeURIComponent(slug)}/${dataset.liveVersion}/${encodeURIComponent(key)}`;
72
+ try {
73
+ const res = await fetch(url, { signal: AbortSignal.timeout(requestTimeout) });
74
+ if (!res.ok) {
75
+ logger.warn("Dataset lookup failed", { slug, key, status: res.status });
76
+ return miss(slug, dataset.liveVersion);
77
+ }
78
+ const body = await res.json();
79
+ if (!body || body.found !== true) {
80
+ return miss(slug, dataset.liveVersion);
81
+ }
82
+ return {
83
+ found: true,
84
+ value: body.value,
85
+ meta: { slug, version: dataset.liveVersion }
86
+ };
87
+ } catch (e) {
88
+ logger.warn("Dataset lookup error", { slug, key, error: String(e) });
89
+ return miss(slug, dataset.liveVersion);
90
+ }
91
+ }
92
+ return { get };
93
+ }
94
+
95
+ // src/recs.ts
96
+ var DEFAULT_REQUEST_TIMEOUT2 = 1e4;
97
+ var MAX_SEED_CHARS = 256;
98
+ var MAX_SEEDS = 3;
99
+ var MAX_ITEMS_CAP = 24;
100
+ var DEFAULT_MAX_ITEMS = 8;
101
+ var MAX_ID_CHARS = 64;
102
+ var MAX_ATTR_VALUE_CHARS = 512;
103
+ var GLOBAL_SEED = "_global";
104
+ function clampMaxItems(value) {
105
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return DEFAULT_MAX_ITEMS;
106
+ return Math.min(Math.floor(value), MAX_ITEMS_CAP);
107
+ }
108
+ function asText(value) {
109
+ if (typeof value === "string") return value;
110
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
111
+ return "";
112
+ }
113
+ function joinIds(ids) {
114
+ let out = "";
115
+ for (const id of ids) {
116
+ const next = out === "" ? id : `${out},${id}`;
117
+ if (next.length > MAX_ATTR_VALUE_CHARS) break;
118
+ out = next;
119
+ }
120
+ return out;
121
+ }
122
+ function toRecProducts(items, cap) {
123
+ const out = [];
124
+ for (const item of items) {
125
+ if (out.length >= cap) break;
126
+ if (item === null || typeof item !== "object") continue;
127
+ const fields = item;
128
+ const id = (asText(fields.id) || asText(fields.sku)).slice(0, MAX_ID_CHARS);
129
+ if (id === "") continue;
130
+ const product = { id };
131
+ const title = asText(fields.title);
132
+ if (title) product.title = title;
133
+ const image = asText(fields.image);
134
+ if (image) product.image = image;
135
+ const href = asText(fields.href) || asText(fields.url);
136
+ if (href && !/^\s*javascript:/i.test(href)) product.href = href;
137
+ const category = asText(fields.category);
138
+ if (category) product.category = category;
139
+ if (typeof fields.price === "number" && Number.isFinite(fields.price)) {
140
+ product.price = Math.round(fields.price);
141
+ }
142
+ if (typeof fields.compareAtPrice === "number" && Number.isFinite(fields.compareAtPrice)) {
143
+ product.compareAtPrice = Math.round(fields.compareAtPrice);
144
+ }
145
+ if (typeof fields.currency === "string" && fields.currency !== "") {
146
+ product.currency = fields.currency;
147
+ }
148
+ const availability = fields.availability;
149
+ if (availability === "in_stock" || availability === "out_of_stock" || availability === "removed") {
150
+ product.availability = availability;
151
+ }
152
+ out.push(product);
153
+ }
154
+ return out;
155
+ }
156
+ function miss2(recipe, degraded) {
157
+ return { items: [], served: false, recipe, source: null, fallbackStep: null, degraded };
158
+ }
159
+ function createRecsClient(options) {
160
+ const { sdkKey, getDatafile, getTracker, logger } = options;
161
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT2;
162
+ function resolveSeeds(context) {
163
+ const clip = (s) => s.slice(0, MAX_SEED_CHARS);
164
+ if (Array.isArray(context?.seeds)) {
165
+ const seeds = context.seeds.filter((s) => typeof s === "string" && s !== "").slice(0, MAX_SEEDS).map(clip);
166
+ if (seeds.length > 0) return seeds;
167
+ }
168
+ if (typeof context?.seed === "string" && context.seed !== "") return [clip(context.seed)];
169
+ return [GLOBAL_SEED];
170
+ }
171
+ async function get(req) {
172
+ try {
173
+ const recipeHandle = typeof req?.recipe === "string" ? req.recipe : "";
174
+ if (recipeHandle === "") {
175
+ logger.warn("recs.get called with an empty recipe \u2014 returning served:false");
176
+ return miss2("", false);
177
+ }
178
+ const datafile = getDatafile();
179
+ if (!datafile) {
180
+ logger.debug(`SDK not ready, returning served:false for recipe "${recipeHandle}"`);
181
+ return miss2(recipeHandle, false);
182
+ }
183
+ const recipes = datafile.recipes;
184
+ const entry = recipes?.find((r) => r.recipe === recipeHandle) ?? null;
185
+ if (!entry) {
186
+ logger.debug(`Recipe "${recipeHandle}" not present in datafile \u2014 returning served:false`);
187
+ return miss2(recipeHandle, false);
188
+ }
189
+ const seeds = resolveSeeds(req.context);
190
+ const maxItems = clampMaxItems(req.maxItems);
191
+ const excludeOutOfStock = typeof req.excludeOutOfStock === "boolean" ? req.excludeOutOfStock : entry.excludeOutOfStock;
192
+ const body = {
193
+ snippetKey: sdkKey,
194
+ assigned: 0,
195
+ sources: [{ chain: entry.chain }],
196
+ maxItems,
197
+ excludeOutOfStock,
198
+ merch: entry.merch ?? [],
199
+ sessionAffinity: entry.sessionAffinity
200
+ };
201
+ if (seeds.length === 1) body.seedKey = seeds[0];
202
+ else body.seedKeys = seeds;
203
+ const base = datafile.collectEndpoint.replace(/\/+$/, "").replace("/v1/collect/batch", "");
204
+ const res = await fetch(`${base}/v1/recs/resolve`, {
205
+ method: "POST",
206
+ headers: { "Content-Type": "application/json" },
207
+ body: JSON.stringify(body),
208
+ signal: AbortSignal.timeout(requestTimeout)
209
+ });
210
+ if (!res.ok) {
211
+ logger.warn("Recs resolve failed", { recipe: recipeHandle, status: res.status });
212
+ return miss2(recipeHandle, true);
213
+ }
214
+ const parsed = await res.json();
215
+ const data = parsed?.data;
216
+ if (!data || typeof data !== "object") return miss2(recipeHandle, true);
217
+ const items = toRecProducts(Array.isArray(data.items) ? data.items : [], maxItems);
218
+ const servedStep = data.served ?? null;
219
+ const served = servedStep !== null && items.length > 0;
220
+ if (req.attribution) {
221
+ const tracker = getTracker();
222
+ if (tracker) {
223
+ tracker.queue({
224
+ visitorId: req.attribution.visitorId,
225
+ experimentId: req.attribution.experimentId,
226
+ variationId: req.attribution.variationId,
227
+ eventName: "rec:impression",
228
+ revenue: null,
229
+ timestamp: Date.now(),
230
+ pageUrl: "",
231
+ attributes: {
232
+ recipe: entry.recipe,
233
+ seed: joinIds(seeds),
234
+ slug: servedStep?.slug ?? "",
235
+ version: servedStep ? String(servedStep.version) : "",
236
+ step: servedStep ? String(servedStep.step) : "",
237
+ served: served ? "true" : "false",
238
+ items: joinIds(items.map((i) => i.id))
239
+ }
240
+ });
241
+ } else {
242
+ logger.warn("recs.get attribution supplied before tracker is ready; impression dropped");
243
+ }
244
+ }
245
+ return {
246
+ items,
247
+ served,
248
+ recipe: entry.recipe,
249
+ source: servedStep?.slug ?? null,
250
+ fallbackStep: servedStep?.step ?? null,
251
+ degraded: false
252
+ };
253
+ } catch (e) {
254
+ logger.warn("Recs resolve error", {
255
+ recipe: typeof req?.recipe === "string" ? req.recipe : "",
256
+ error: String(e)
257
+ });
258
+ return miss2(typeof req?.recipe === "string" ? req.recipe : "", true);
259
+ }
260
+ }
261
+ function trackClick(productId, opts) {
262
+ if (typeof productId !== "string" || productId === "") {
263
+ logger.warn("recs.trackClick called with an empty productId \u2014 dropped");
264
+ return;
265
+ }
266
+ if (!opts || !opts.visitorId || !opts.experimentId || !opts.variationId) {
267
+ logger.warn("recs.trackClick requires visitorId/experimentId/variationId \u2014 dropped");
268
+ return;
269
+ }
270
+ const tracker = getTracker();
271
+ if (!tracker) {
272
+ logger.warn("recs.trackClick called before tracker is ready; click dropped");
273
+ return;
274
+ }
275
+ const position = typeof opts.position === "number" && Number.isInteger(opts.position) && opts.position >= 1 ? String(opts.position) : "";
276
+ tracker.queue({
277
+ visitorId: opts.visitorId,
278
+ experimentId: opts.experimentId,
279
+ variationId: opts.variationId,
280
+ eventName: "rec:click",
281
+ revenue: null,
282
+ timestamp: Date.now(),
283
+ pageUrl: "",
284
+ attributes: {
285
+ recipe: typeof opts.recipe === "string" ? opts.recipe : "",
286
+ item: productId.slice(0, MAX_ID_CHARS),
287
+ position
288
+ }
289
+ });
290
+ }
291
+ return { get, trackClick };
292
+ }
293
+
43
294
  // src/decisionLog.ts
44
295
  function createDecisionLog() {
45
296
  const entries = [];
@@ -239,7 +490,7 @@ function createServerTracker(options) {
239
490
  var import_streaming = require("@avsbhq/utils/streaming");
240
491
 
241
492
  // src/version.ts
242
- var SDK_VERSION = "0.3.0";
493
+ var SDK_VERSION = "0.4.0";
243
494
 
244
495
  // src/UserBoundClient.ts
245
496
  function createUserBoundClient(server, context) {
@@ -415,6 +666,42 @@ var AvsbServer = class {
415
666
  }
416
667
  return this._utils;
417
668
  }
669
+ /**
670
+ * Server-side datasets read surface (e-commerce suite Phase 2).
671
+ * Constructed lazily on first access, mirroring the `utils` getter.
672
+ * `client.datasets.get(slug, key)` resolves the live version from the
673
+ * current datafile and never throws — see ./datasets.ts.
674
+ */
675
+ _datasets = null;
676
+ get datasets() {
677
+ if (!this._datasets) {
678
+ this._datasets = createDatasetsClient({
679
+ sdkKey: this.sdkKey,
680
+ getDatafile: () => this.datafile,
681
+ logger: this.logger
682
+ });
683
+ }
684
+ return this._datasets;
685
+ }
686
+ /**
687
+ * Server-side recommendations surface (e-commerce suite overhaul, C3).
688
+ * Constructed lazily on first access, mirroring the `datasets` getter.
689
+ * `client.recs.get({ recipe, … })` resolves the recipe registry from the
690
+ * current datafile and never throws; impressions/clicks ride the server
691
+ * tracker's queue and revenue attaches via the existing `trackPurchase`.
692
+ */
693
+ _recs = null;
694
+ get recs() {
695
+ if (!this._recs) {
696
+ this._recs = createRecsClient({
697
+ sdkKey: this.sdkKey,
698
+ getDatafile: () => this.datafile,
699
+ getTracker: () => this.tracker,
700
+ logger: this.logger
701
+ });
702
+ }
703
+ return this._recs;
704
+ }
418
705
  constructor(options) {
419
706
  this.sdkKey = options.sdkKey;
420
707
  this.pollingInterval = Math.max(options.pollingInterval ?? 6e4, 5e3);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FlagDatafile, StickyBucketService, Logger, EvaluationSource, StickyAssignment, DecideOption, Flag, TrackPayload, DecisionLog, InitResult, EvalContext } from '@avsbhq/core';
2
- export { DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
2
+ export { DatafileDataset, DatafileDatasetType, DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, PurchaseHistoryAttribute, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
3
3
  import { AvsbUtils } from '@avsbhq/utils';
4
4
  export { AvsbUtils } from '@avsbhq/utils';
5
5
  export { StreamingClient, StreamingOptions, createStreamingClient } from '@avsbhq/utils/streaming';
@@ -82,6 +82,13 @@ interface PurchaseOrder {
82
82
  coupon?: string;
83
83
  items?: Array<{
84
84
  sku: string;
85
+ /**
86
+ * Canonical product-grain key (e-commerce suite overhaul, contract C1).
87
+ * Pass when known so order items join catalog/recommendation analytics
88
+ * without alias resolution; the ingestion worker derives it from `sku`
89
+ * otherwise.
90
+ */
91
+ productKey?: string;
85
92
  name?: string;
86
93
  price?: number;
87
94
  quantity?: number;
@@ -89,6 +96,130 @@ interface PurchaseOrder {
89
96
  }>;
90
97
  }
91
98
 
99
+ /**
100
+ * Server-side datasets read client (e-commerce suite Phase 2).
101
+ *
102
+ * Resolves the live version of a dataset from the SDK's datafile
103
+ * (`datasets` field) and fetches a single key from the edge worker:
104
+ *
105
+ * GET {ingestionHost}/v1/ds/{sdkKey}/{slug}/{versionShortId}/{key}
106
+ *
107
+ * The ingestion host is derived from the datafile's `collectEndpoint`,
108
+ * exactly like the server tracker derives `/v1/collect/*`. The worker's
109
+ * key cache resolves SDK keys as well as snippet keys in the first path
110
+ * segment.
111
+ *
112
+ * Error posture matches flag evaluation: this client NEVER throws.
113
+ * Unknown slug, missing live version, SDK not ready, HTTP errors and
114
+ * network failures all degrade to `{ found: false }`.
115
+ */
116
+ interface DatasetGetResult {
117
+ found: boolean;
118
+ /** Parsed row for TABLE/FEED datasets; `true` for LIST membership hits. */
119
+ value?: unknown;
120
+ meta: {
121
+ slug: string;
122
+ /** Live version short id used for the lookup; null when unresolvable. */
123
+ version: number | null;
124
+ };
125
+ }
126
+ interface DatasetsClient {
127
+ /**
128
+ * Look up `key` in the live version of dataset `slug`.
129
+ * Resolves the version from the current datafile and never throws.
130
+ */
131
+ get(slug: string, key: string): Promise<DatasetGetResult>;
132
+ }
133
+
134
+ /**
135
+ * Server-side recommendations client (e-commerce suite overhaul, contract C3).
136
+ *
137
+ * Mirrors ./datasets.ts: resolves a recipe handle against the datafile's
138
+ * recipe registry (`datafile.recipes`, additive) and POSTs the standard
139
+ * resolve body to the edge worker:
140
+ *
141
+ * POST {ingestionHost}/v1/recs/resolve
142
+ *
143
+ * The ingestion host is derived from the datafile's `collectEndpoint`. The
144
+ * worker's key cache resolves SDK keys as well as snippet keys, so the body's
145
+ * key field simply carries the SDK key.
146
+ *
147
+ * Attribution is OPT-IN server-side: `get()` fires a `rec:impression` (via
148
+ * the server tracker) only when the caller passes `attribution` — a server
149
+ * has no ambient variation context the way a browser harness does.
150
+ *
151
+ * Error posture matches datasets/flag evaluation: this client NEVER throws.
152
+ * Unknown recipe, SDK not ready, HTTP errors, timeouts and network failures
153
+ * all degrade to `{ items: [], served: false }`.
154
+ */
155
+ interface RecProduct {
156
+ id: string;
157
+ title?: string;
158
+ image?: string;
159
+ href?: string;
160
+ category?: string;
161
+ /** MINOR units (ISO-4217). */
162
+ price?: number;
163
+ /** MINOR units. */
164
+ compareAtPrice?: number;
165
+ currency?: string;
166
+ availability?: 'in_stock' | 'out_of_stock' | 'removed';
167
+ }
168
+ interface RecsAttribution {
169
+ visitorId: string;
170
+ experimentId: string;
171
+ variationId: string;
172
+ }
173
+ interface RecsGetRequest {
174
+ recipe: string;
175
+ context?: {
176
+ /** Single seed product key (product page). */
177
+ seed?: string;
178
+ /** Up to 3 seed product keys (cart page). */
179
+ seeds?: string[];
180
+ };
181
+ maxItems?: number;
182
+ excludeOutOfStock?: boolean;
183
+ /** When present, a rec:impression is queued with these ids on success. */
184
+ attribution?: RecsAttribution;
185
+ }
186
+ interface RecsResult {
187
+ items: RecProduct[];
188
+ served: boolean;
189
+ recipe: string;
190
+ /** Slug of the chain step that served, null on a miss. */
191
+ source: string | null;
192
+ fallbackStep: number | null;
193
+ /** True when the resolve failed (timeout / network / HTTP / parse). */
194
+ degraded: boolean;
195
+ }
196
+ interface RecsClient {
197
+ /** Resolve items for a recipe. Never throws. */
198
+ get(req: RecsGetRequest): Promise<RecsResult>;
199
+ /**
200
+ * Queue a rec:click for a product the shopper clicked. Pass `recipe` when
201
+ * known — results group click-through by (variation, recipe).
202
+ */
203
+ trackClick(productId: string, opts: RecsAttribution & {
204
+ position?: number;
205
+ recipe?: string;
206
+ }): void;
207
+ }
208
+ /**
209
+ * One entry of the datafile recipe registry (contract C3). Additive on
210
+ * FlagDatafile — SDKs on older datafiles see it absent.
211
+ */
212
+ interface DatafileRecipe {
213
+ recipe: string;
214
+ chain: Array<{
215
+ slug: string;
216
+ version: number;
217
+ }>;
218
+ excludeOutOfStock: boolean;
219
+ sessionAffinity: boolean;
220
+ merch?: unknown[];
221
+ }
222
+
92
223
  /**
93
224
  * A per-request scoped client with a bound EvalContext.
94
225
  *
@@ -170,6 +301,23 @@ declare class AvsbServer {
170
301
  */
171
302
  private _utils;
172
303
  get utils(): AvsbUtils;
304
+ /**
305
+ * Server-side datasets read surface (e-commerce suite Phase 2).
306
+ * Constructed lazily on first access, mirroring the `utils` getter.
307
+ * `client.datasets.get(slug, key)` resolves the live version from the
308
+ * current datafile and never throws — see ./datasets.ts.
309
+ */
310
+ private _datasets;
311
+ get datasets(): DatasetsClient;
312
+ /**
313
+ * Server-side recommendations surface (e-commerce suite overhaul, C3).
314
+ * Constructed lazily on first access, mirroring the `datasets` getter.
315
+ * `client.recs.get({ recipe, … })` resolves the recipe registry from the
316
+ * current datafile and never throws; impressions/clicks ride the server
317
+ * tracker's queue and revenue attaches via the existing `trackPurchase`.
318
+ */
319
+ private _recs;
320
+ get recs(): RecsClient;
173
321
  constructor(options: AvsbServerOptions);
174
322
  onReady(): Promise<InitResult>;
175
323
  private initializeAndReady;
@@ -260,6 +408,6 @@ declare class RedisStickyBucketService implements StickyBucketService {
260
408
 
261
409
  declare function createDecisionLog(): DecisionLog;
262
410
 
263
- declare const SDK_VERSION = "0.3.0";
411
+ declare const SDK_VERSION = "0.4.0";
264
412
 
265
- export { AvsbServer, type AvsbServerOptions, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
413
+ export { AvsbServer, type AvsbServerOptions, type DatafileRecipe, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RecProduct, type RecsAttribution, type RecsClient, type RecsGetRequest, type RecsResult, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FlagDatafile, StickyBucketService, Logger, EvaluationSource, StickyAssignment, DecideOption, Flag, TrackPayload, DecisionLog, InitResult, EvalContext } from '@avsbhq/core';
2
- export { DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
2
+ export { DatafileDataset, DatafileDatasetType, DecideOption, DecisionLog, DecisionLogEntry, EvalContext, EvaluationSource, ExposureEvent, Flag, FlagDatafile, FlagEvaluationDetails, InitResult, Logger, MetricEvent, MultiContext, PurchaseHistoryAttribute, RuleType, SingleContext, StickyAssignment, StickyBucketService, TrackPayload, consoleLogger, consoleTransport, createLogger, noopLogger } from '@avsbhq/core';
3
3
  import { AvsbUtils } from '@avsbhq/utils';
4
4
  export { AvsbUtils } from '@avsbhq/utils';
5
5
  export { StreamingClient, StreamingOptions, createStreamingClient } from '@avsbhq/utils/streaming';
@@ -82,6 +82,13 @@ interface PurchaseOrder {
82
82
  coupon?: string;
83
83
  items?: Array<{
84
84
  sku: string;
85
+ /**
86
+ * Canonical product-grain key (e-commerce suite overhaul, contract C1).
87
+ * Pass when known so order items join catalog/recommendation analytics
88
+ * without alias resolution; the ingestion worker derives it from `sku`
89
+ * otherwise.
90
+ */
91
+ productKey?: string;
85
92
  name?: string;
86
93
  price?: number;
87
94
  quantity?: number;
@@ -89,6 +96,130 @@ interface PurchaseOrder {
89
96
  }>;
90
97
  }
91
98
 
99
+ /**
100
+ * Server-side datasets read client (e-commerce suite Phase 2).
101
+ *
102
+ * Resolves the live version of a dataset from the SDK's datafile
103
+ * (`datasets` field) and fetches a single key from the edge worker:
104
+ *
105
+ * GET {ingestionHost}/v1/ds/{sdkKey}/{slug}/{versionShortId}/{key}
106
+ *
107
+ * The ingestion host is derived from the datafile's `collectEndpoint`,
108
+ * exactly like the server tracker derives `/v1/collect/*`. The worker's
109
+ * key cache resolves SDK keys as well as snippet keys in the first path
110
+ * segment.
111
+ *
112
+ * Error posture matches flag evaluation: this client NEVER throws.
113
+ * Unknown slug, missing live version, SDK not ready, HTTP errors and
114
+ * network failures all degrade to `{ found: false }`.
115
+ */
116
+ interface DatasetGetResult {
117
+ found: boolean;
118
+ /** Parsed row for TABLE/FEED datasets; `true` for LIST membership hits. */
119
+ value?: unknown;
120
+ meta: {
121
+ slug: string;
122
+ /** Live version short id used for the lookup; null when unresolvable. */
123
+ version: number | null;
124
+ };
125
+ }
126
+ interface DatasetsClient {
127
+ /**
128
+ * Look up `key` in the live version of dataset `slug`.
129
+ * Resolves the version from the current datafile and never throws.
130
+ */
131
+ get(slug: string, key: string): Promise<DatasetGetResult>;
132
+ }
133
+
134
+ /**
135
+ * Server-side recommendations client (e-commerce suite overhaul, contract C3).
136
+ *
137
+ * Mirrors ./datasets.ts: resolves a recipe handle against the datafile's
138
+ * recipe registry (`datafile.recipes`, additive) and POSTs the standard
139
+ * resolve body to the edge worker:
140
+ *
141
+ * POST {ingestionHost}/v1/recs/resolve
142
+ *
143
+ * The ingestion host is derived from the datafile's `collectEndpoint`. The
144
+ * worker's key cache resolves SDK keys as well as snippet keys, so the body's
145
+ * key field simply carries the SDK key.
146
+ *
147
+ * Attribution is OPT-IN server-side: `get()` fires a `rec:impression` (via
148
+ * the server tracker) only when the caller passes `attribution` — a server
149
+ * has no ambient variation context the way a browser harness does.
150
+ *
151
+ * Error posture matches datasets/flag evaluation: this client NEVER throws.
152
+ * Unknown recipe, SDK not ready, HTTP errors, timeouts and network failures
153
+ * all degrade to `{ items: [], served: false }`.
154
+ */
155
+ interface RecProduct {
156
+ id: string;
157
+ title?: string;
158
+ image?: string;
159
+ href?: string;
160
+ category?: string;
161
+ /** MINOR units (ISO-4217). */
162
+ price?: number;
163
+ /** MINOR units. */
164
+ compareAtPrice?: number;
165
+ currency?: string;
166
+ availability?: 'in_stock' | 'out_of_stock' | 'removed';
167
+ }
168
+ interface RecsAttribution {
169
+ visitorId: string;
170
+ experimentId: string;
171
+ variationId: string;
172
+ }
173
+ interface RecsGetRequest {
174
+ recipe: string;
175
+ context?: {
176
+ /** Single seed product key (product page). */
177
+ seed?: string;
178
+ /** Up to 3 seed product keys (cart page). */
179
+ seeds?: string[];
180
+ };
181
+ maxItems?: number;
182
+ excludeOutOfStock?: boolean;
183
+ /** When present, a rec:impression is queued with these ids on success. */
184
+ attribution?: RecsAttribution;
185
+ }
186
+ interface RecsResult {
187
+ items: RecProduct[];
188
+ served: boolean;
189
+ recipe: string;
190
+ /** Slug of the chain step that served, null on a miss. */
191
+ source: string | null;
192
+ fallbackStep: number | null;
193
+ /** True when the resolve failed (timeout / network / HTTP / parse). */
194
+ degraded: boolean;
195
+ }
196
+ interface RecsClient {
197
+ /** Resolve items for a recipe. Never throws. */
198
+ get(req: RecsGetRequest): Promise<RecsResult>;
199
+ /**
200
+ * Queue a rec:click for a product the shopper clicked. Pass `recipe` when
201
+ * known — results group click-through by (variation, recipe).
202
+ */
203
+ trackClick(productId: string, opts: RecsAttribution & {
204
+ position?: number;
205
+ recipe?: string;
206
+ }): void;
207
+ }
208
+ /**
209
+ * One entry of the datafile recipe registry (contract C3). Additive on
210
+ * FlagDatafile — SDKs on older datafiles see it absent.
211
+ */
212
+ interface DatafileRecipe {
213
+ recipe: string;
214
+ chain: Array<{
215
+ slug: string;
216
+ version: number;
217
+ }>;
218
+ excludeOutOfStock: boolean;
219
+ sessionAffinity: boolean;
220
+ merch?: unknown[];
221
+ }
222
+
92
223
  /**
93
224
  * A per-request scoped client with a bound EvalContext.
94
225
  *
@@ -170,6 +301,23 @@ declare class AvsbServer {
170
301
  */
171
302
  private _utils;
172
303
  get utils(): AvsbUtils;
304
+ /**
305
+ * Server-side datasets read surface (e-commerce suite Phase 2).
306
+ * Constructed lazily on first access, mirroring the `utils` getter.
307
+ * `client.datasets.get(slug, key)` resolves the live version from the
308
+ * current datafile and never throws — see ./datasets.ts.
309
+ */
310
+ private _datasets;
311
+ get datasets(): DatasetsClient;
312
+ /**
313
+ * Server-side recommendations surface (e-commerce suite overhaul, C3).
314
+ * Constructed lazily on first access, mirroring the `datasets` getter.
315
+ * `client.recs.get({ recipe, … })` resolves the recipe registry from the
316
+ * current datafile and never throws; impressions/clicks ride the server
317
+ * tracker's queue and revenue attaches via the existing `trackPurchase`.
318
+ */
319
+ private _recs;
320
+ get recs(): RecsClient;
173
321
  constructor(options: AvsbServerOptions);
174
322
  onReady(): Promise<InitResult>;
175
323
  private initializeAndReady;
@@ -260,6 +408,6 @@ declare class RedisStickyBucketService implements StickyBucketService {
260
408
 
261
409
  declare function createDecisionLog(): DecisionLog;
262
410
 
263
- declare const SDK_VERSION = "0.3.0";
411
+ declare const SDK_VERSION = "0.4.0";
264
412
 
265
- export { AvsbServer, type AvsbServerOptions, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
413
+ export { AvsbServer, type AvsbServerOptions, type DatafileRecipe, type DatasetGetResult, type DatasetsClient, type EventOptions, type GetAllFlagsOptions, type GetFlagOptions, InMemoryStickyBucketService, type MiddlewareConfig, type PurchaseOrder, type RecProduct, type RecsAttribution, type RecsClient, type RecsGetRequest, type RecsResult, type RedisLike, type RedisStickyBucketOptions, RedisStickyBucketService, SDK_VERSION, type UserBoundClient, createDecisionLog, expressMiddleware, fastifyPlugin };
package/dist/index.js CHANGED
@@ -13,6 +13,257 @@ import {
13
13
  noopLogger
14
14
  } from "@avsbhq/core";
15
15
 
16
+ // src/datasets.ts
17
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
18
+ function miss(slug, version) {
19
+ return { found: false, meta: { slug, version } };
20
+ }
21
+ function createDatasetsClient(options) {
22
+ const { sdkKey, getDatafile, logger } = options;
23
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
24
+ async function get(slug, key) {
25
+ if (!slug || !key) {
26
+ logger.warn("datasets.get called with an empty slug or key \u2014 returning not found");
27
+ return miss(slug, null);
28
+ }
29
+ const datafile = getDatafile();
30
+ if (!datafile) {
31
+ logger.debug(`SDK not ready, returning not found for dataset "${slug}"`);
32
+ return miss(slug, null);
33
+ }
34
+ const dataset = datafile.datasets?.find((d) => d.slug === slug);
35
+ if (!dataset) {
36
+ logger.debug(`Dataset "${slug}" not present in datafile \u2014 returning not found`);
37
+ return miss(slug, null);
38
+ }
39
+ if (!dataset.liveVersion) {
40
+ logger.debug(`Dataset "${slug}" has no live version \u2014 returning not found`);
41
+ return miss(slug, null);
42
+ }
43
+ const base = datafile.collectEndpoint.replace(/\/+$/, "");
44
+ const url = `${base}/v1/ds/${encodeURIComponent(sdkKey)}/${encodeURIComponent(slug)}/${dataset.liveVersion}/${encodeURIComponent(key)}`;
45
+ try {
46
+ const res = await fetch(url, { signal: AbortSignal.timeout(requestTimeout) });
47
+ if (!res.ok) {
48
+ logger.warn("Dataset lookup failed", { slug, key, status: res.status });
49
+ return miss(slug, dataset.liveVersion);
50
+ }
51
+ const body = await res.json();
52
+ if (!body || body.found !== true) {
53
+ return miss(slug, dataset.liveVersion);
54
+ }
55
+ return {
56
+ found: true,
57
+ value: body.value,
58
+ meta: { slug, version: dataset.liveVersion }
59
+ };
60
+ } catch (e) {
61
+ logger.warn("Dataset lookup error", { slug, key, error: String(e) });
62
+ return miss(slug, dataset.liveVersion);
63
+ }
64
+ }
65
+ return { get };
66
+ }
67
+
68
+ // src/recs.ts
69
+ var DEFAULT_REQUEST_TIMEOUT2 = 1e4;
70
+ var MAX_SEED_CHARS = 256;
71
+ var MAX_SEEDS = 3;
72
+ var MAX_ITEMS_CAP = 24;
73
+ var DEFAULT_MAX_ITEMS = 8;
74
+ var MAX_ID_CHARS = 64;
75
+ var MAX_ATTR_VALUE_CHARS = 512;
76
+ var GLOBAL_SEED = "_global";
77
+ function clampMaxItems(value) {
78
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return DEFAULT_MAX_ITEMS;
79
+ return Math.min(Math.floor(value), MAX_ITEMS_CAP);
80
+ }
81
+ function asText(value) {
82
+ if (typeof value === "string") return value;
83
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
84
+ return "";
85
+ }
86
+ function joinIds(ids) {
87
+ let out = "";
88
+ for (const id of ids) {
89
+ const next = out === "" ? id : `${out},${id}`;
90
+ if (next.length > MAX_ATTR_VALUE_CHARS) break;
91
+ out = next;
92
+ }
93
+ return out;
94
+ }
95
+ function toRecProducts(items, cap) {
96
+ const out = [];
97
+ for (const item of items) {
98
+ if (out.length >= cap) break;
99
+ if (item === null || typeof item !== "object") continue;
100
+ const fields = item;
101
+ const id = (asText(fields.id) || asText(fields.sku)).slice(0, MAX_ID_CHARS);
102
+ if (id === "") continue;
103
+ const product = { id };
104
+ const title = asText(fields.title);
105
+ if (title) product.title = title;
106
+ const image = asText(fields.image);
107
+ if (image) product.image = image;
108
+ const href = asText(fields.href) || asText(fields.url);
109
+ if (href && !/^\s*javascript:/i.test(href)) product.href = href;
110
+ const category = asText(fields.category);
111
+ if (category) product.category = category;
112
+ if (typeof fields.price === "number" && Number.isFinite(fields.price)) {
113
+ product.price = Math.round(fields.price);
114
+ }
115
+ if (typeof fields.compareAtPrice === "number" && Number.isFinite(fields.compareAtPrice)) {
116
+ product.compareAtPrice = Math.round(fields.compareAtPrice);
117
+ }
118
+ if (typeof fields.currency === "string" && fields.currency !== "") {
119
+ product.currency = fields.currency;
120
+ }
121
+ const availability = fields.availability;
122
+ if (availability === "in_stock" || availability === "out_of_stock" || availability === "removed") {
123
+ product.availability = availability;
124
+ }
125
+ out.push(product);
126
+ }
127
+ return out;
128
+ }
129
+ function miss2(recipe, degraded) {
130
+ return { items: [], served: false, recipe, source: null, fallbackStep: null, degraded };
131
+ }
132
+ function createRecsClient(options) {
133
+ const { sdkKey, getDatafile, getTracker, logger } = options;
134
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT2;
135
+ function resolveSeeds(context) {
136
+ const clip = (s) => s.slice(0, MAX_SEED_CHARS);
137
+ if (Array.isArray(context?.seeds)) {
138
+ const seeds = context.seeds.filter((s) => typeof s === "string" && s !== "").slice(0, MAX_SEEDS).map(clip);
139
+ if (seeds.length > 0) return seeds;
140
+ }
141
+ if (typeof context?.seed === "string" && context.seed !== "") return [clip(context.seed)];
142
+ return [GLOBAL_SEED];
143
+ }
144
+ async function get(req) {
145
+ try {
146
+ const recipeHandle = typeof req?.recipe === "string" ? req.recipe : "";
147
+ if (recipeHandle === "") {
148
+ logger.warn("recs.get called with an empty recipe \u2014 returning served:false");
149
+ return miss2("", false);
150
+ }
151
+ const datafile = getDatafile();
152
+ if (!datafile) {
153
+ logger.debug(`SDK not ready, returning served:false for recipe "${recipeHandle}"`);
154
+ return miss2(recipeHandle, false);
155
+ }
156
+ const recipes = datafile.recipes;
157
+ const entry = recipes?.find((r) => r.recipe === recipeHandle) ?? null;
158
+ if (!entry) {
159
+ logger.debug(`Recipe "${recipeHandle}" not present in datafile \u2014 returning served:false`);
160
+ return miss2(recipeHandle, false);
161
+ }
162
+ const seeds = resolveSeeds(req.context);
163
+ const maxItems = clampMaxItems(req.maxItems);
164
+ const excludeOutOfStock = typeof req.excludeOutOfStock === "boolean" ? req.excludeOutOfStock : entry.excludeOutOfStock;
165
+ const body = {
166
+ snippetKey: sdkKey,
167
+ assigned: 0,
168
+ sources: [{ chain: entry.chain }],
169
+ maxItems,
170
+ excludeOutOfStock,
171
+ merch: entry.merch ?? [],
172
+ sessionAffinity: entry.sessionAffinity
173
+ };
174
+ if (seeds.length === 1) body.seedKey = seeds[0];
175
+ else body.seedKeys = seeds;
176
+ const base = datafile.collectEndpoint.replace(/\/+$/, "").replace("/v1/collect/batch", "");
177
+ const res = await fetch(`${base}/v1/recs/resolve`, {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify(body),
181
+ signal: AbortSignal.timeout(requestTimeout)
182
+ });
183
+ if (!res.ok) {
184
+ logger.warn("Recs resolve failed", { recipe: recipeHandle, status: res.status });
185
+ return miss2(recipeHandle, true);
186
+ }
187
+ const parsed = await res.json();
188
+ const data = parsed?.data;
189
+ if (!data || typeof data !== "object") return miss2(recipeHandle, true);
190
+ const items = toRecProducts(Array.isArray(data.items) ? data.items : [], maxItems);
191
+ const servedStep = data.served ?? null;
192
+ const served = servedStep !== null && items.length > 0;
193
+ if (req.attribution) {
194
+ const tracker = getTracker();
195
+ if (tracker) {
196
+ tracker.queue({
197
+ visitorId: req.attribution.visitorId,
198
+ experimentId: req.attribution.experimentId,
199
+ variationId: req.attribution.variationId,
200
+ eventName: "rec:impression",
201
+ revenue: null,
202
+ timestamp: Date.now(),
203
+ pageUrl: "",
204
+ attributes: {
205
+ recipe: entry.recipe,
206
+ seed: joinIds(seeds),
207
+ slug: servedStep?.slug ?? "",
208
+ version: servedStep ? String(servedStep.version) : "",
209
+ step: servedStep ? String(servedStep.step) : "",
210
+ served: served ? "true" : "false",
211
+ items: joinIds(items.map((i) => i.id))
212
+ }
213
+ });
214
+ } else {
215
+ logger.warn("recs.get attribution supplied before tracker is ready; impression dropped");
216
+ }
217
+ }
218
+ return {
219
+ items,
220
+ served,
221
+ recipe: entry.recipe,
222
+ source: servedStep?.slug ?? null,
223
+ fallbackStep: servedStep?.step ?? null,
224
+ degraded: false
225
+ };
226
+ } catch (e) {
227
+ logger.warn("Recs resolve error", {
228
+ recipe: typeof req?.recipe === "string" ? req.recipe : "",
229
+ error: String(e)
230
+ });
231
+ return miss2(typeof req?.recipe === "string" ? req.recipe : "", true);
232
+ }
233
+ }
234
+ function trackClick(productId, opts) {
235
+ if (typeof productId !== "string" || productId === "") {
236
+ logger.warn("recs.trackClick called with an empty productId \u2014 dropped");
237
+ return;
238
+ }
239
+ if (!opts || !opts.visitorId || !opts.experimentId || !opts.variationId) {
240
+ logger.warn("recs.trackClick requires visitorId/experimentId/variationId \u2014 dropped");
241
+ return;
242
+ }
243
+ const tracker = getTracker();
244
+ if (!tracker) {
245
+ logger.warn("recs.trackClick called before tracker is ready; click dropped");
246
+ return;
247
+ }
248
+ const position = typeof opts.position === "number" && Number.isInteger(opts.position) && opts.position >= 1 ? String(opts.position) : "";
249
+ tracker.queue({
250
+ visitorId: opts.visitorId,
251
+ experimentId: opts.experimentId,
252
+ variationId: opts.variationId,
253
+ eventName: "rec:click",
254
+ revenue: null,
255
+ timestamp: Date.now(),
256
+ pageUrl: "",
257
+ attributes: {
258
+ recipe: typeof opts.recipe === "string" ? opts.recipe : "",
259
+ item: productId.slice(0, MAX_ID_CHARS),
260
+ position
261
+ }
262
+ });
263
+ }
264
+ return { get, trackClick };
265
+ }
266
+
16
267
  // src/decisionLog.ts
17
268
  function createDecisionLog() {
18
269
  const entries = [];
@@ -215,7 +466,7 @@ import {
215
466
  } from "@avsbhq/utils/streaming";
216
467
 
217
468
  // src/version.ts
218
- var SDK_VERSION = "0.3.0";
469
+ var SDK_VERSION = "0.4.0";
219
470
 
220
471
  // src/UserBoundClient.ts
221
472
  function createUserBoundClient(server, context) {
@@ -391,6 +642,42 @@ var AvsbServer = class {
391
642
  }
392
643
  return this._utils;
393
644
  }
645
+ /**
646
+ * Server-side datasets read surface (e-commerce suite Phase 2).
647
+ * Constructed lazily on first access, mirroring the `utils` getter.
648
+ * `client.datasets.get(slug, key)` resolves the live version from the
649
+ * current datafile and never throws — see ./datasets.ts.
650
+ */
651
+ _datasets = null;
652
+ get datasets() {
653
+ if (!this._datasets) {
654
+ this._datasets = createDatasetsClient({
655
+ sdkKey: this.sdkKey,
656
+ getDatafile: () => this.datafile,
657
+ logger: this.logger
658
+ });
659
+ }
660
+ return this._datasets;
661
+ }
662
+ /**
663
+ * Server-side recommendations surface (e-commerce suite overhaul, C3).
664
+ * Constructed lazily on first access, mirroring the `datasets` getter.
665
+ * `client.recs.get({ recipe, … })` resolves the recipe registry from the
666
+ * current datafile and never throws; impressions/clicks ride the server
667
+ * tracker's queue and revenue attaches via the existing `trackPurchase`.
668
+ */
669
+ _recs = null;
670
+ get recs() {
671
+ if (!this._recs) {
672
+ this._recs = createRecsClient({
673
+ sdkKey: this.sdkKey,
674
+ getDatafile: () => this.datafile,
675
+ getTracker: () => this.tracker,
676
+ logger: this.logger
677
+ });
678
+ }
679
+ return this._recs;
680
+ }
394
681
  constructor(options) {
395
682
  this.sdkKey = options.sdkKey;
396
683
  this.pollingInterval = Math.max(options.pollingInterval ?? 6e4, 5e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avsbhq/node",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "test:watch": "vitest"
34
34
  },
35
35
  "dependencies": {
36
- "@avsbhq/core": "1.1.0",
36
+ "@avsbhq/core": "1.3.0",
37
37
  "@avsbhq/utils": "1.0.1"
38
38
  },
39
39
  "devDependencies": {