@apostra/sdk 0.39.0 → 0.40.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.
@@ -52,8 +52,8 @@ export declare function exchangeCliLoginCode(options: Options, discovery: CliLog
52
52
  }): Promise<CliLoginTokens>;
53
53
  /**
54
54
  * Return a usable login for `baseUrl`, refreshing it if the access token is
55
- * about to expire. After taking the lock the file is read again: if another
56
- * process refreshed first, its token is used and no request is made.
55
+ * about to expire. After taking the lock the credential store is read again:
56
+ * if another process refreshed first, its token is used and no request is made.
57
57
  */
58
58
  export declare function refreshCliLogin(baseUrl: string, options?: Options): Promise<CliLoginEntry>;
59
59
  /**
package/dist/cli-login.js CHANGED
@@ -25,6 +25,7 @@ function node() {
25
25
  fs: get('node:fs'),
26
26
  os: get('node:os'),
27
27
  path: get('node:path'),
28
+ module: get('node:module'),
28
29
  };
29
30
  }
30
31
  const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -73,7 +74,7 @@ function parseEntry(value) {
73
74
  return undefined;
74
75
  return value;
75
76
  }
76
- function readAll() {
77
+ function readFileLogins() {
77
78
  const { fs } = node();
78
79
  try {
79
80
  const file = JSON.parse(fs.readFileSync(credentialsPath(), 'utf8'));
@@ -84,7 +85,7 @@ function readAll() {
84
85
  }
85
86
  }
86
87
  /** Atomic (temp file + rename), directory 0700, file 0600. */
87
- function writeAll(logins) {
88
+ function writeFileLogins(logins) {
88
89
  const { fs, path } = node();
89
90
  const file = credentialsPath();
90
91
  const dir = path.dirname(file);
@@ -104,8 +105,141 @@ function writeAll(logins) {
104
105
  }
105
106
  fs.renameSync(temp, file);
106
107
  }
108
+ const keyringService = 'Apostra CLI login';
109
+ /**
110
+ * Resolve the native binding only when this Node-only feature is used. A static
111
+ * import would make the browser SDK bundle a native module.
112
+ */
113
+ function keyringEntry(resource) {
114
+ const { module } = node();
115
+ const required = module.createRequire(import.meta.url)('@napi-rs/keyring');
116
+ if (!isRecord(required) || typeof required.Entry !== 'function')
117
+ throw new Error('Apostra credential-store binding is unavailable');
118
+ const Entry = required.Entry;
119
+ // Do not accept keyutils on Linux. It is ephemeral and is not the requested
120
+ // Secret Service store, so an inaccessible service deliberately uses the
121
+ // documented 0600-file fallback instead.
122
+ return new Entry(keyringService, resource, proc().platform === 'linux'
123
+ ? { linux: { store: 'secret-service' } }
124
+ : undefined);
125
+ }
126
+ function readSecureLogin(resource) {
127
+ try {
128
+ const password = keyringEntry(resource).getPassword();
129
+ if (!password)
130
+ return { available: true };
131
+ return { available: true, entry: parseEntry(JSON.parse(password)) };
132
+ }
133
+ catch {
134
+ return { available: false };
135
+ }
136
+ }
137
+ function writeSecureLogin(resource, entry) {
138
+ try {
139
+ keyringEntry(resource).setPassword(JSON.stringify(entry));
140
+ return true;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ function deleteSecureLogin(resource) {
147
+ try {
148
+ return keyringEntry(resource).deletePassword();
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ }
154
+ function warnFileFallback() {
155
+ const { fs, path } = node();
156
+ const dir = path.dirname(credentialsPath());
157
+ const marker = path.join(dir, '.file-store-warning');
158
+ try {
159
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
160
+ fs.chmodSync(dir, 0o700);
161
+ const fd = fs.openSync(marker, 'wx', 0o600);
162
+ fs.closeSync(fd);
163
+ }
164
+ catch (error) {
165
+ if (errorCode(error) === 'EEXIST')
166
+ return;
167
+ // If the marker cannot be written, warn for this process rather than
168
+ // silently accepting plaintext storage.
169
+ }
170
+ proc().emitWarning?.(`Apostra could not access your OS credential store. Login tokens are stored in ${credentialsPath()} with 0600 permissions. Use Keychain, Credential Manager, or Secret Service when available.`);
171
+ }
172
+ /**
173
+ * Move every legacy entry while holding the normal refresh lock. Reads that
174
+ * arrive first use a short synchronous instance of the same lock; contenders
175
+ * keep reading the legacy file until the migration finishes.
176
+ */
177
+ function migrateFileLogins(lockHeld = false) {
178
+ const { fs, path } = node();
179
+ const fileLogins = readFileLogins();
180
+ const entries = Object.entries(fileLogins).flatMap(([resource, value]) => {
181
+ const entry = parseEntry(value);
182
+ return entry ? [[resource, entry]] : [];
183
+ });
184
+ if (entries.length === 0)
185
+ return false;
186
+ const lock = `${credentialsPath()}.lock`;
187
+ let owner;
188
+ if (!lockHeld) {
189
+ fs.mkdirSync(path.dirname(lock), { recursive: true, mode: 0o700 });
190
+ owner = `migration:${proc().pid}:${globalThis.crypto.randomUUID()}`;
191
+ try {
192
+ const fd = fs.openSync(lock, 'wx', 0o600);
193
+ try {
194
+ fs.writeSync(fd, owner);
195
+ }
196
+ finally {
197
+ fs.closeSync(fd);
198
+ }
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
204
+ try {
205
+ for (const [resource, entry] of entries) {
206
+ // A keyring value is authoritative: preserving it avoids overwriting a
207
+ // token another process has already refreshed during an interrupted
208
+ // earlier migration.
209
+ const stored = readSecureLogin(resource);
210
+ if (stored.entry)
211
+ continue;
212
+ if (!stored.available || !writeSecureLogin(resource, entry))
213
+ return false;
214
+ }
215
+ fs.rmSync(credentialsPath(), { force: true });
216
+ return true;
217
+ }
218
+ finally {
219
+ if (owner) {
220
+ try {
221
+ if (fs.readFileSync(lock, 'utf8') === owner)
222
+ fs.rmSync(lock, { force: true });
223
+ }
224
+ catch {
225
+ // The regular lock protocol protects against removing another owner.
226
+ }
227
+ }
228
+ }
229
+ }
107
230
  export function readCliLogin(baseUrl) {
108
- return parseEntry(readAll()[normalizeBaseUrl(baseUrl)]);
231
+ const resource = normalizeBaseUrl(baseUrl);
232
+ const secure = readSecureLogin(resource);
233
+ if (secure.entry)
234
+ return secure.entry;
235
+ const fileLogins = readFileLogins();
236
+ if (Object.keys(fileLogins).length === 0)
237
+ return undefined;
238
+ if (migrateFileLogins())
239
+ return readSecureLogin(resource).entry;
240
+ if (!secure.available)
241
+ warnFileFallback();
242
+ return parseEntry(fileLogins[resource]);
109
243
  }
110
244
  /** Like `readCliLogin`, but never throws: for the SDK's default credential chain. */
111
245
  export function storedCliLogin(baseUrl) {
@@ -117,6 +251,43 @@ export function storedCliLogin(baseUrl) {
117
251
  }
118
252
  }
119
253
  export const isFresh = (entry) => Date.parse(entry.expiresAt) - FRESH_MARGIN_MS > Date.now();
254
+ /**
255
+ * Remove a lock whose heartbeat stopped, without ever removing a live one.
256
+ * Only the holder of the takeover guard checks and removes. Checking again
257
+ * under the guard means a lock another process took after the first check is
258
+ * fresh and is left alone. A guard a crashed process left behind goes stale
259
+ * like a lock does. Returns whether this call held the guard, so the caller
260
+ * retries at once; otherwise it waits like any other contender.
261
+ */
262
+ function breakStaleLock(fs, lock) {
263
+ const guard = `${lock}.takeover`;
264
+ try {
265
+ fs.closeSync(fs.openSync(guard, 'wx', 0o600));
266
+ }
267
+ catch (error) {
268
+ if (errorCode(error) !== 'EEXIST')
269
+ throw error;
270
+ try {
271
+ if (Date.now() - fs.statSync(guard).mtimeMs > LOCK_STALE_MS)
272
+ fs.rmSync(guard, { force: true });
273
+ }
274
+ catch {
275
+ // The guard went away between the two calls; the caller retries.
276
+ }
277
+ return false;
278
+ }
279
+ try {
280
+ if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS)
281
+ fs.rmSync(lock, { force: true });
282
+ }
283
+ catch {
284
+ // The lock went away; the caller retries.
285
+ }
286
+ finally {
287
+ fs.rmSync(guard, { force: true });
288
+ }
289
+ return true;
290
+ }
120
291
  /**
121
292
  * Serialise every read-modify-write of the store, across processes. Refresh
122
293
  * tokens rotate, so two processes must never present the same one outside the
@@ -154,11 +325,9 @@ export async function withCliLoginLock(fn) {
154
325
  if (errorCode(error) !== 'EEXIST')
155
326
  throw error;
156
327
  try {
157
- const stale = ownerOf();
158
328
  if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
159
- if (ownerOf() === stale)
160
- fs.rmSync(lock, { force: true });
161
- continue;
329
+ if (breakStaleLock(fs, lock))
330
+ continue;
162
331
  }
163
332
  }
164
333
  catch {
@@ -190,7 +359,14 @@ export async function withCliLoginLock(fn) {
190
359
  }
191
360
  /** Save a login. Call inside `withCliLoginLock`. */
192
361
  export function writeCliLogin(baseUrl, entry) {
193
- writeAll({ ...readAll(), [normalizeBaseUrl(baseUrl)]: entry });
362
+ const resource = normalizeBaseUrl(baseUrl);
363
+ // Callers hold the cross-process lock, so the legacy file cannot overwrite
364
+ // a rotated credential while it is being migrated.
365
+ migrateFileLogins(true);
366
+ if (writeSecureLogin(resource, entry))
367
+ return;
368
+ warnFileFallback();
369
+ writeFileLogins({ ...readFileLogins(), [resource]: entry });
194
370
  }
195
371
  async function send(fetcher, url, init = {}) {
196
372
  return fetcher(url, {
@@ -287,8 +463,8 @@ export function exchangeCliLoginCode(options, discovery, resource, grant) {
287
463
  }
288
464
  /**
289
465
  * Return a usable login for `baseUrl`, refreshing it if the access token is
290
- * about to expire. After taking the lock the file is read again: if another
291
- * process refreshed first, its token is used and no request is made.
466
+ * about to expire. After taking the lock the credential store is read again:
467
+ * if another process refreshed first, its token is used and no request is made.
292
468
  */
293
469
  export async function refreshCliLogin(baseUrl, options = {}) {
294
470
  const key = normalizeBaseUrl(baseUrl);
@@ -350,8 +526,12 @@ export async function logoutCliLogin(baseUrl, { fetch: f = globalThis.fetch } =
350
526
  catch {
351
527
  revoked = false;
352
528
  }
353
- const { [key]: _removed, ...rest } = readAll();
354
- writeAll(rest);
529
+ deleteSecureLogin(key);
530
+ // A prior fallback login can still be present even when the credential
531
+ // store has no entry (or is available again). Remove it unconditionally:
532
+ // otherwise its refresh token could be migrated back after logout.
533
+ const { [key]: _removed, ...rest } = readFileLogins();
534
+ writeFileLogins(rest);
355
535
  return { found: true, revoked };
356
536
  });
357
537
  }
@@ -43,6 +43,9 @@ export type GetSellerReportingReceiptsError = Wire.GetSellerReportingReceiptsErr
43
43
  export type GetStatusInput = Wire.GetStatusData['body'];
44
44
  export type GetStatusResult = Wire.GetStatusResponses[200]['data'];
45
45
  export type GetStatusError = Wire.GetStatusErrors[keyof Wire.GetStatusErrors];
46
+ export type LogEventInput = OptionalWireField<Wire.LogEventData['body'], "idempotencyKey">;
47
+ export type LogEventResult = Wire.LogEventResponses[200]['data'];
48
+ export type LogEventError = Wire.LogEventErrors[keyof Wire.LogEventErrors];
46
49
  export type OpenAdvertisersPageInput = Wire.OpenAdvertisersPageData['body'];
47
50
  export type OpenAdvertisersPageResult = Wire.OpenAdvertisersPageResponses[200]['data'];
48
51
  export type OpenAdvertisersPageError = Wire.OpenAdvertisersPageErrors[keyof Wire.OpenAdvertisersPageErrors];
@@ -276,6 +279,10 @@ export interface OperationTypes {
276
279
  input: GetStatusInput;
277
280
  result: GetStatusResult;
278
281
  };
282
+ "log_event": {
283
+ input: LogEventInput;
284
+ result: LogEventResult;
285
+ };
279
286
  "open_advertisers_page": {
280
287
  input: OpenAdvertisersPageInput;
281
288
  result: OpenAdvertisersPageResult;
@@ -624,6 +631,15 @@ export declare class Apostra extends Transport {
624
631
  getStatus(input?: GetStatusInput, options?: RequestOptions): Promise<GetStatusResult>;
625
632
  /** Current account, state, blockers, exact fixes, and reachable accounts. For seller demand, use it to tell whether readiness stops requests before source calls; read before diagnosing source health, empty responses, or demand. A Developer account adds each agent's stage, blocker, and next call. Returns the data together with response headers and request ID. */
626
633
  getStatusWithResponse(input?: GetStatusInput, options?: RequestOptions): Promise<ResponseDetails<GetStatusResult>>;
634
+ /**
635
+ * Send up to 10,000 AdCP conversion events (log_event) to an event source the advertiser registered with save_event_source. Events must also pass the v2 rules: no raw email or phone in user_match, each event_type + event_id once per call. One invalid event refuses the whole call.
636
+ *
637
+ * @example
638
+ * await client.logEvent(input, { idempotencyKey: 'stable-key' })
639
+ */
640
+ logEvent(input: LogEventInput, options: WriteRequestOptions): Promise<LogEventResult>;
641
+ /** Send up to 10,000 AdCP conversion events (log_event) to an event source the advertiser registered with save_event_source. Events must also pass the v2 rules: no raw email or phone in user_match, each event_type + event_id once per call. One invalid event refuses the whole call. Returns the data together with response headers and request ID. */
642
+ logEventWithResponse(input: LogEventInput, options: WriteRequestOptions): Promise<ResponseDetails<LogEventResult>>;
627
643
  /**
628
644
  * Open the Advertisers Page: every advertiser with its campaign and draft counts and one next action each. Use it for "show my advertisers" or "where do I start"; the Page self-fetches. To add or set up an advertiser, pass intent: "add". For a text answer use search(kind: "advertiser").
629
645
  *
@@ -913,13 +929,13 @@ export declare class Apostra extends Transport {
913
929
  /** Sync first-party CRM audiences for a buyer advertiser. Each audiences[] item may add, remove, or delete members. Returns an operationId; use get(kind: audience, advertiserId: "...") to read match status after it settles. Returns the data together with response headers and request ID. */
914
930
  saveAudienceWithResponse(input: SaveAudienceInput, options: WriteRequestOptions): Promise<ResponseDetails<SaveAudienceResult>>;
915
931
  /**
916
- * Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Cards stay off MCP.
932
+ * Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Accepting a plan needs an administrator to approve it on our site. Cards stay off MCP.
917
933
  *
918
934
  * @example
919
935
  * await client.saveBilling(input, { idempotencyKey: 'stable-key' })
920
936
  */
921
937
  saveBilling(input: SaveBillingInput, options: WriteRequestOptions): Promise<SaveBillingResult>;
922
- /** Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Cards stay off MCP. Returns the data together with response headers and request ID. */
938
+ /** Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Accepting a plan needs an administrator to approve it on our site. Cards stay off MCP. Returns the data together with response headers and request ID. */
923
939
  saveBillingWithResponse(input: SaveBillingInput, options: WriteRequestOptions): Promise<ResponseDetails<SaveBillingResult>>;
924
940
  /**
925
941
  * Write seller AI Business Rules. Policy content needs an account admin and both policy fields. Approval auto needs acknowledgeNoHumanReview.
@@ -1048,13 +1064,13 @@ export declare class Apostra extends Transport {
1048
1064
  /** Create or update where a seller's inventory comes from. Pass `id` to change a source or omit it to add one. Pass only `agentId` or `adServer` to open the form that connects an agent or an ad server. Credentials are never passed here; a source that needs one returns the page that collects it. Returns the data together with response headers and request ID. */
1049
1065
  saveInventorySourceWithResponse(input: SaveInventorySourceInput, options: WriteRequestOptions): Promise<ResponseDetails<SaveInventorySourceResult>>;
1050
1066
  /**
1051
- * Manage seller lessons in a seller account context; use switch_account first. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear.
1067
+ * Manage seller lessons in a seller account; send the accountId of that account. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear.
1052
1068
  *
1053
1069
  * @example
1054
1070
  * await client.saveLesson(input, { idempotencyKey: 'stable-key' })
1055
1071
  */
1056
1072
  saveLesson(input: SaveLessonInput, options: WriteRequestOptions): Promise<SaveLessonResult>;
1057
- /** Manage seller lessons in a seller account context; use switch_account first. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear. Returns the data together with response headers and request ID. */
1073
+ /** Manage seller lessons in a seller account; send the accountId of that account. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear. Returns the data together with response headers and request ID. */
1058
1074
  saveLessonWithResponse(input: SaveLessonInput, options: WriteRequestOptions): Promise<ResponseDetails<SaveLessonResult>>;
1059
1075
  /**
1060
1076
  * Open a seller library request or close it with seller Material.
@@ -90,6 +90,15 @@ export class Apostra extends Transport {
90
90
  getStatus(input = {}, options) { return this.request("get_status", input, options); }
91
91
  /** Current account, state, blockers, exact fixes, and reachable accounts. For seller demand, use it to tell whether readiness stops requests before source calls; read before diagnosing source health, empty responses, or demand. A Developer account adds each agent's stage, blocker, and next call. Returns the data together with response headers and request ID. */
92
92
  getStatusWithResponse(input = {}, options) { return this.requestDetailed("get_status", input, options); }
93
+ /**
94
+ * Send up to 10,000 AdCP conversion events (log_event) to an event source the advertiser registered with save_event_source. Events must also pass the v2 rules: no raw email or phone in user_match, each event_type + event_id once per call. One invalid event refuses the whole call.
95
+ *
96
+ * @example
97
+ * await client.logEvent(input, { idempotencyKey: 'stable-key' })
98
+ */
99
+ logEvent(input, options) { return this.request("log_event", input, options); }
100
+ /** Send up to 10,000 AdCP conversion events (log_event) to an event source the advertiser registered with save_event_source. Events must also pass the v2 rules: no raw email or phone in user_match, each event_type + event_id once per call. One invalid event refuses the whole call. Returns the data together with response headers and request ID. */
101
+ logEventWithResponse(input, options) { return this.requestDetailed("log_event", input, options); }
93
102
  /**
94
103
  * Open the Advertisers Page: every advertiser with its campaign and draft counts and one next action each. Use it for "show my advertisers" or "where do I start"; the Page self-fetches. To add or set up an advertiser, pass intent: "add". For a text answer use search(kind: "advertiser").
95
104
  *
@@ -379,13 +388,13 @@ export class Apostra extends Transport {
379
388
  /** Sync first-party CRM audiences for a buyer advertiser. Each audiences[] item may add, remove, or delete members. Returns an operationId; use get(kind: audience, advertiserId: "...") to read match status after it settles. Returns the data together with response headers and request ID. */
380
389
  saveAudienceWithResponse(input, options) { return this.requestDetailed("save_audience", input, options); }
381
390
  /**
382
- * Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Cards stay off MCP.
391
+ * Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Accepting a plan needs an administrator to approve it on our site. Cards stay off MCP.
383
392
  *
384
393
  * @example
385
394
  * await client.saveBilling(input, { idempotencyKey: 'stable-key' })
386
395
  */
387
396
  saveBilling(input, options) { return this.request("save_billing", input, options); }
388
- /** Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Cards stay off MCP. Returns the data together with response headers and request ID. */
397
+ /** Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Accepting a plan needs an administrator to approve it on our site. Cards stay off MCP. Returns the data together with response headers and request ID. */
389
398
  saveBillingWithResponse(input, options) { return this.requestDetailed("save_billing", input, options); }
390
399
  /**
391
400
  * Write seller AI Business Rules. Policy content needs an account admin and both policy fields. Approval auto needs acknowledgeNoHumanReview.
@@ -514,13 +523,13 @@ export class Apostra extends Transport {
514
523
  /** Create or update where a seller's inventory comes from. Pass `id` to change a source or omit it to add one. Pass only `agentId` or `adServer` to open the form that connects an agent or an ad server. Credentials are never passed here; a source that needs one returns the page that collects it. Returns the data together with response headers and request ID. */
515
524
  saveInventorySourceWithResponse(input, options) { return this.requestDetailed("save_inventory_source", input, options); }
516
525
  /**
517
- * Manage seller lessons in a seller account context; use switch_account first. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear.
526
+ * Manage seller lessons in a seller account; send the accountId of that account. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear.
518
527
  *
519
528
  * @example
520
529
  * await client.saveLesson(input, { idempotencyKey: 'stable-key' })
521
530
  */
522
531
  saveLesson(input, options) { return this.request("save_lesson", input, options); }
523
- /** Manage seller lessons in a seller account context; use switch_account first. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear. Returns the data together with response headers and request ID. */
532
+ /** Manage seller lessons in a seller account; send the accountId of that account. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear. Returns the data together with response headers and request ID. */
524
533
  saveLessonWithResponse(input, options) { return this.requestDetailed("save_lesson", input, options); }
525
534
  /**
526
535
  * Open a seller library request or close it with seller Material.
@@ -1,4 +1,4 @@
1
- export declare const version = "0.39.0";
1
+ export declare const version = "0.40.0";
2
2
  export declare const baseUrl = "https://api.apostra.com/api/v3";
3
3
  export declare const m2mTokenUrl = "https://identity.scope3.com/oauth2/token";
4
4
  export declare const operations: {
@@ -180,6 +180,21 @@ export declare const operations: {
180
180
  }];
181
181
  readonly summary: "Current account, state, blockers, exact fixes, and reachable accounts. For seller demand, use it to tell whether readiness stops requests before source calls; read before diagnosing source health, empty responses, or demand. A Developer account adds each agent's stage, blocker, and next call.";
182
182
  };
183
+ readonly log_event: {
184
+ readonly path: "/tools/log_event";
185
+ readonly method: "POST";
186
+ readonly body: true;
187
+ readonly binary: false;
188
+ readonly idempotencyKey: true;
189
+ readonly bodyKeyField: "idempotencyKey";
190
+ readonly parameters: readonly [];
191
+ readonly security: readonly [{
192
+ readonly apiKeyAuth: readonly [];
193
+ }, {
194
+ readonly m2mOAuth: readonly ["interchange:write"];
195
+ }];
196
+ readonly summary: "Send up to 10,000 AdCP conversion events (log_event) to an event source the advertiser registered with save_event_source. Events must also pass the v2 rules: no raw email or phone in user_match, each event_type + event_id once per call. One invalid event refuses the whole call.";
197
+ };
183
198
  readonly open_advertisers_page: {
184
199
  readonly path: "/tools/open_advertisers_page";
185
200
  readonly method: "POST";
@@ -642,7 +657,7 @@ export declare const operations: {
642
657
  }, {
643
658
  readonly m2mOAuth: readonly ["interchange:read"];
644
659
  }];
645
- readonly summary: "Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Cards stay off MCP.";
660
+ readonly summary: "Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Accepting a plan needs an administrator to approve it on our site. Cards stay off MCP.";
646
661
  };
647
662
  readonly save_business_rules: {
648
663
  readonly path: "/tools/save_business_rules";
@@ -856,7 +871,7 @@ export declare const operations: {
856
871
  }, {
857
872
  readonly m2mOAuth: readonly ["interchange:write"];
858
873
  }];
859
- readonly summary: "Manage seller lessons in a seller account context; use switch_account first. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear.";
874
+ readonly summary: "Manage seller lessons in a seller account; send the accountId of that account. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear.";
860
875
  };
861
876
  readonly save_library_request: {
862
877
  readonly path: "/tools/save_library_request";
@@ -1,5 +1,5 @@
1
1
  // Generated. Do not edit.
2
- export const version = '0.39.0';
2
+ export const version = '0.40.0';
3
3
  export const baseUrl = "https://api.apostra.com/api/v3";
4
4
  export const m2mTokenUrl = "https://identity.scope3.com/oauth2/token";
5
5
  export const operations = {
@@ -246,6 +246,26 @@ export const operations = {
246
246
  ],
247
247
  "summary": "Current account, state, blockers, exact fixes, and reachable accounts. For seller demand, use it to tell whether readiness stops requests before source calls; read before diagnosing source health, empty responses, or demand. A Developer account adds each agent's stage, blocker, and next call."
248
248
  },
249
+ "log_event": {
250
+ "path": "/tools/log_event",
251
+ "method": "POST",
252
+ "body": true,
253
+ "binary": false,
254
+ "idempotencyKey": true,
255
+ "bodyKeyField": "idempotencyKey",
256
+ "parameters": [],
257
+ "security": [
258
+ {
259
+ "apiKeyAuth": []
260
+ },
261
+ {
262
+ "m2mOAuth": [
263
+ "interchange:write"
264
+ ]
265
+ }
266
+ ],
267
+ "summary": "Send up to 10,000 AdCP conversion events (log_event) to an event source the advertiser registered with save_event_source. Events must also pass the v2 rules: no raw email or phone in user_match, each event_type + event_id once per call. One invalid event refuses the whole call."
268
+ },
249
269
  "open_advertisers_page": {
250
270
  "path": "/tools/open_advertisers_page",
251
271
  "method": "POST",
@@ -873,7 +893,7 @@ export const operations = {
873
893
  ]
874
894
  }
875
895
  ],
876
- "summary": "Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Cards stay off MCP."
896
+ "summary": "Accept Terms, set the payment terms sellers are asked for, or accept a plan, answer its offer and change its renewal. Accepting a plan needs an administrator to approve it on our site. Cards stay off MCP."
877
897
  },
878
898
  "save_business_rules": {
879
899
  "path": "/tools/save_business_rules",
@@ -1162,7 +1182,7 @@ export const operations = {
1162
1182
  ]
1163
1183
  }
1164
1184
  ],
1165
- "summary": "Manage seller lessons in a seller account context; use switch_account first. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear."
1185
+ "summary": "Manage seller lessons in a seller account; send the accountId of that account. propose requires lesson; approve and retire require lessonId; preview requires lessonId and brief. Preserve seller words from a private Session note and ask if kind or scope is unclear."
1166
1186
  },
1167
1187
  "save_library_request": {
1168
1188
  "path": "/tools/save_library_request",