@foir/sdk 0.12.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -108,6 +108,61 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
108
108
 
109
109
  Changes on `dev` that have not been published to npm.
110
110
 
111
+ ## [0.14.0] — 2026-10-01
112
+
113
+ Published under npm's `next` tag from `dev`, because it calls parts of the
114
+ platform that production serves from the next promotion; `latest` moves to it
115
+ then.
116
+
117
+ ### Added
118
+
119
+ - **`customers.get`, `customers.byEmail` and `customers.list`** on the app
120
+ client (`@foir/sdk/app`): an operation reads the customers its work is about.
121
+ `get(id)` and `byEmail(email)` answer one customer or null, and need
122
+ `customers:read:named`: the customer the operation names, by id or by their
123
+ exact email address at the install's level, and nothing wider. `list` pages
124
+ through the customers at the install's level and needs `customers:read`,
125
+ which covers the two lookups as well. An operation declaring only the named
126
+ read is refused the list. `AppCustomer` and `AppCustomerList` are their types.
127
+ - **`customers.ensure` answers `verified`**: whether the customer it found or
128
+ made has proved the address is theirs. It finds the customer holding the
129
+ email either way, so one shopper asking twice is one customer; a caller with
130
+ something that should wait for proof asks the flag first.
131
+
132
+ ## [0.13.0] — 2026-10-01
133
+
134
+ Published under npm's `next` tag from `dev`, because it calls parts of the
135
+ platform that production serves from the next promotion; `latest` moves to it
136
+ then.
137
+
138
+ ### Added
139
+
140
+ - **`operations.start`** on the app client (`@foir/sdk/app`): a webhook
141
+ delivery starts a run of one of the app's own operations, with an input, and
142
+ is answered with the run's id without waiting for it. For work a delivery
143
+ cannot do in place: a delivery is not a run, so it holds none of the
144
+ authority the platform gives only inside one, and it has the few seconds its
145
+ vendor waits. Only the token a delivery arrived with may call it.
146
+ `idempotencyKey` names one delivery, so a redelivery does not start the work
147
+ twice. `concurrencyKey` names work of which one run should be in flight at a
148
+ time: a start under it joins the run in flight and starts nothing, and once
149
+ that run has ended the next start is a new run. A start under a key is
150
+ retried when it gets no answer; one under no key is asked once.
151
+ `AppOperationStart` and `AppStartedOperation` are its types.
152
+ - **`records.list`** on the app client: the records matching a filter, in an
153
+ order, at most `first` of them, with `hasMore` saying the filter matched more.
154
+ For work an app hands out in turn, such as the next ten people waiting: list
155
+ them, then update the ones listed by id. A model's list is named by its
156
+ plural, which its operator may set, so the first list a client makes reads
157
+ the data API's surface descriptor (`GET /v1/manifest`) as the same token,
158
+ once. `AppRecordList` is its query's type.
159
+
160
+ ### Changed
161
+
162
+ - **Breaking: `OperationTrigger.type` gains `'webhook'`**, the trigger of a run
163
+ a delivery started with `operations.start`. A `switch` over the four earlier
164
+ values that TypeScript checked as exhaustive needs a case for it.
165
+
111
166
  ## [0.12.0] — 2026-10-01
112
167
 
113
168
  Breaking changes under the pre-1.0 note, so this release is a MINOR. Published
@@ -260,6 +260,25 @@ export interface AppClient {
260
260
  } | {
261
261
  id: string;
262
262
  }, fields: readonly string[]): Promise<AppRecord | null>;
263
+ /**
264
+ * The records matching a filter, in an order, at most `first` of them. For
265
+ * work handed out in turn, such as the next ten people waiting: list them,
266
+ * then update the ones listed by id.
267
+ *
268
+ * `where` is the model's typed filter and `orderBy` names model fields, as
269
+ * `updateMany` takes them. The platform orders by `createdAt`, `updatedAt`,
270
+ * `naturalKey` and `id`, and by a field the model indexes; ordering by any
271
+ * other is refused, naming it. `hasMore` says the filter matched more than
272
+ * were answered. Like `read`, it sees the PUBLISHED version.
273
+ *
274
+ * The name of a model's list is the operator's to set, so the first list a
275
+ * client makes asks the data API what this token is served (`GET
276
+ * /v1/manifest`), once.
277
+ */
278
+ list(model: string, query: AppRecordList): Promise<{
279
+ records: AppRecord[];
280
+ hasMore: boolean;
281
+ }>;
263
282
  /** Delete one record by its platform id, which only a read supplies. */
264
283
  delete(model: string, id: string): Promise<{
265
284
  deleted: boolean;
@@ -348,10 +367,40 @@ export interface AppClient {
348
367
  * The customer who has this email at the install's level, made when there
349
368
  * is none. The same customer every time for one email. Needs
350
369
  * `customers:write`.
370
+ *
371
+ * `verified` says whether that address is one the customer has proved is
372
+ * theirs. A customer this call makes has not, and neither has one an
373
+ * earlier call made: anybody can type an address. So what is written for
374
+ * an unverified customer is written for whoever holds the address, and
375
+ * anything that should wait for proof asks `verified` first.
351
376
  */
352
377
  ensure(email: string): Promise<{
353
378
  id: string;
354
379
  email: string;
380
+ verified: boolean;
381
+ }>;
382
+ /**
383
+ * One customer by id, or null when there is none at the install's level.
384
+ * Needs `customers:read:named`, or `customers:read`.
385
+ */
386
+ get(id: string): Promise<AppCustomer | null>;
387
+ /**
388
+ * The customer who has this email at the install's level, or null. The
389
+ * exact address, as `ensure` matches it, and nothing wider: it finds one
390
+ * customer and never searches. Needs `customers:read:named`, or
391
+ * `customers:read`.
392
+ */
393
+ byEmail(email: string): Promise<AppCustomer | null>;
394
+ /**
395
+ * The customers at the install's level, a page at a time, newest first.
396
+ * Needs `customers:read`: an operation declaring only
397
+ * `customers:read:named` reads the customers it names and is refused the
398
+ * list.
399
+ */
400
+ list(query?: AppCustomerList): Promise<{
401
+ customers: AppCustomer[];
402
+ hasMore: boolean;
403
+ endCursor: string | null;
355
404
  }>;
356
405
  };
357
406
  notifications: {
@@ -365,6 +414,78 @@ export interface AppClient {
365
414
  stale: boolean;
366
415
  }>;
367
416
  };
417
+ operations: {
418
+ /**
419
+ * Start a run of one of this app's own operations, from a webhook
420
+ * delivery. The run is queued and dispatched as a hook's is, and this
421
+ * answers with its id without waiting for it.
422
+ *
423
+ * For work a delivery cannot do in place. A delivery is not a run, so it
424
+ * holds none of the authority the platform gives only inside one (setting
425
+ * a customer's consent is one), and it has the few seconds its vendor
426
+ * waits. Hand the work to an operation and answer the vendor.
427
+ *
428
+ * Only the token a delivery arrived with may call it, for an operation
429
+ * that is active at the install. An operation's own token is refused: it
430
+ * carries that operation's capabilities and no other's.
431
+ */
432
+ start(operationKey: string, options?: AppOperationStart): Promise<AppStartedOperation>;
433
+ };
434
+ }
435
+ /** A customer, as an operation that declared a customer read sees them. */
436
+ export interface AppCustomer {
437
+ id: string;
438
+ email: string;
439
+ /** Whether the customer has proved the address is theirs. */
440
+ verified: boolean;
441
+ /** `ACTIVE`, `SUSPENDED` or `PENDING`, as the platform holds it. */
442
+ status: string;
443
+ }
444
+ export interface AppCustomerList {
445
+ /** Part of an email address to look for. Absent lists everyone. */
446
+ search?: string;
447
+ /** How many to answer, at most. The platform's own page size when absent. */
448
+ first?: number;
449
+ /** The `endCursor` of the page before, to read the next one. */
450
+ after?: string;
451
+ }
452
+ export interface AppRecordList {
453
+ /** The model's typed filter, by model field key. Absent matches every record. */
454
+ where?: JsonObject;
455
+ /** The order, most significant first. The record id breaks every tie. */
456
+ orderBy?: ReadonlyArray<{
457
+ field: string;
458
+ direction?: 'asc' | 'desc';
459
+ }>;
460
+ /** How many to answer, at most. */
461
+ first: number;
462
+ /** The model fields to read on each record. Its id, natural key and owner always come back. */
463
+ fields?: readonly string[];
464
+ }
465
+ export interface AppOperationStart {
466
+ /** The run's input. */
467
+ input?: JsonObject;
468
+ /**
469
+ * Names one delivery, so the vendor's redelivery of it does not start the
470
+ * work twice: pass the delivery's own id.
471
+ */
472
+ idempotencyKey?: string;
473
+ /**
474
+ * Names work of which one run should be in flight at a time. While a run of
475
+ * the operation started under the key is in flight, a start under it joins
476
+ * that run and starts nothing; once that run has ended, the next start is a
477
+ * new run. For work that recurs and must not overlap, where an idempotency
478
+ * key would adopt the first run for ever.
479
+ */
480
+ concurrencyKey?: string;
481
+ }
482
+ export interface AppStartedOperation {
483
+ /** The run: the one this call started, joined or adopted. */
484
+ executionId: string;
485
+ /** True when `concurrencyKey` matched a run in flight, so nothing was started. */
486
+ joined: boolean;
487
+ /** True when `idempotencyKey` matched a run in flight or completed, so nothing was started. */
488
+ deduplicated: boolean;
368
489
  }
369
490
  /** Build the client an installed app calls Foir with. See the module doc. */
370
491
  export declare function createAppClient(config: AppClientConfig): AppClient;
@@ -1414,6 +1414,21 @@ export enum ManifestCapability {
1414
1414
  */
1415
1415
  NOTIFICATIONS_CONSENT = 12,
1416
1416
 
1417
+ /**
1418
+ * Reading customers at the install's level. customers:read:named is one
1419
+ * customer the operation names, by id or by their exact email address, and
1420
+ * never the list. customers:read is that and the list. An operation that
1421
+ * declares the whole read holds the named one too.
1422
+ *
1423
+ * @generated from enum value: MANIFEST_CAPABILITY_CUSTOMERS_READ = 14;
1424
+ */
1425
+ CUSTOMERS_READ = 14,
1426
+
1427
+ /**
1428
+ * @generated from enum value: MANIFEST_CAPABILITY_CUSTOMERS_READ_NAMED = 15;
1429
+ */
1430
+ CUSTOMERS_READ_NAMED = 15,
1431
+
1417
1432
  /**
1418
1433
  * Reporting an ASYNC run's outcome.
1419
1434
  *
@@ -21,7 +21,7 @@
21
21
  * The manifest's own messages are on `@foir/sdk/app/contract`.
22
22
  */
23
23
  export { createAppClient } from './client';
24
- export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppCustomerEraseResult, AppCustomerSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppRecord, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
24
+ export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppCustomer, AppCustomerEraseResult, AppCustomerList, AppCustomerSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
25
25
  export { appCustomerUnlinkedOf, appLookupConflictOf, isAppFieldWithheld } from './errors';
26
26
  export type { AppLookupConflict } from './errors';
27
27
  export { appModelFieldName, appModelTypeName, appSelectorEnumValue } from './naming';
@@ -7,7 +7,7 @@ export declare const APP_HOOK_EVENTS: readonly ["RECORD_CREATED", "RECORD_UPDATE
7
7
  /** An event a hook can name. */
8
8
  export type AppHookEvent = (typeof APP_HOOK_EVENTS)[number];
9
9
  /** The capabilities an operation declares as written. */
10
- export declare const APP_CAPABILITIES: readonly ["config:read", "secrets:read.app", "secrets:put.app", "secrets:list.app", "secrets:delete.app", "status:write", "files:read", "files:list", "files:write", "customers:write", "notifications:write", "notifications:consent", "operations:complete"];
10
+ export declare const APP_CAPABILITIES: readonly ["config:read", "secrets:read.app", "secrets:put.app", "secrets:list.app", "secrets:delete.app", "status:write", "files:read", "files:list", "files:write", "customers:write", "notifications:write", "notifications:consent", "customers:read", "customers:read:named", "operations:complete"];
11
11
  /** A capability an operation declares as written. */
12
12
  export type AppCapability = (typeof APP_CAPABILITIES)[number];
13
13
  /**
@@ -8,7 +8,7 @@ var file_buf_validate_validate = /* @__PURE__ */ fileDesc("ChtidWYvdmFsaWRhdGUvd
8
8
 
9
9
  // ../proto-ts/src/apps/v1/apps_pb.js
10
10
  import { file_google_protobuf_descriptor as file_google_protobuf_descriptor2, file_google_protobuf_struct } from "@bufbuild/protobuf/wkt";
11
- var file_apps_v1_apps = /* @__PURE__ */ fileDesc2("ChJhcHBzL3YxL2FwcHMucHJvdG8SB2FwcHMudjEilgUKCUFwcENvbmZpZxI0CgRuYW1lGAEgASgJQia6SCNyIRhAMh1eW2Etel1bYS16MC05XSooLVthLXowLTldKykqJBIYCgd2ZXJzaW9uGAIgASgJQge6SARyAhABEiMKCG1ldGFkYXRhGAMgASgLMhEuYXBwcy52MS5NZXRhZGF0YRInCgptaWRkbGV3YXJlGAQgASgLMhMuYXBwcy52MS5NaWRkbGV3YXJlEikKC2NyZWRlbnRpYWxzGAUgASgLMhQuYXBwcy52MS5DcmVkZW50aWFscxIwCg9zZXR0aW5nc19zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjAKB3NvdXJjZXMYByADKAsyHy5hcHBzLnYxLkFwcENvbmZpZy5Tb3VyY2VzRW50cnkSLAoFc2lua3MYCCADKAsyHS5hcHBzLnYxLkFwcENvbmZpZy5TaW5rc0VudHJ5EiYKCm9wZXJhdGlvbnMYCSADKAsyEi5hcHBzLnYxLk9wZXJhdGlvbhIcCgVob29rcxgKIAMoCzINLmFwcHMudjEuSG9vaxIXCgJ1aRgLIAEoCzILLmFwcHMudjEuVUkSIQoHaW5ib3VuZBgMIAEoCzIQLmFwcHMudjEuSW5ib3VuZBIiCghmZWF0dXJlcxgNIAMoCzIQLmFwcHMudjEuRmVhdHVyZRpDCgxTb3VyY2VzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuYXBwcy52MS5Tb3VyY2VUeXBlOgI4ARpDCgpTaW5rc0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmFwcHMudjEuU2lua0NvbnRyYWN0OgI4ASKNAQoHRmVhdHVyZRIzCgNrZXkYASABKAlCJrpII3IhGEAyHV5bYS16XVthLXowLTldKigtW2EtejAtOV0rKSokEhYKBXRpdGxlGAIgASgJQge6SARyAhABEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg8KB3NldHRpbmcYBCABKAkSDwoHc291cmNlcxgFIAMoCSJlCghNZXRhZGF0YRIUCgxkaXNwbGF5X25hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSDAoEaWNvbhgDIAEoCRIOCgZhdXRob3IYBCABKAkSEAoIaG9tZXBhZ2UYBSABKAkiLQoKTWlkZGxld2FyZRIfCgN1cmwYASABKAlCErpID3INMgteaHR0cHM6Ly8uKyLXAgoLQ3JlZGVudGlhbHMSLwoIc3RyYXRlZ3kYASABKA4yHS5hcHBzLnYxLkNyZWRlbnRpYWxzLlN0cmF0ZWd5EiwKBnNjaGVtYRgCIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RIAIgBARIoCgVvYXV0aBgDIAEoCzIULmFwcHMudjEuT0F1dGhDb25maWdIAYgBASKpAQoIU3RyYXRlZ3kSGAoUU1RSQVRFR1lfVU5TUEVDSUZJRUQQABISCg5TVFJBVEVHWV9PQVVUSBABEhQKEFNUUkFURUdZX0FQSV9LRVkQAhIaChZTVFJBVEVHWV9TSEFSRURfU0VDUkVUEAMSFAoQU1RSQVRFR1lfU1NIX0tFWRAEEhEKDVNUUkFURUdZX05PTkUQBRIUChBTVFJBVEVHWV9NQU5BR0VEEAZCCQoHX3NjaGVtYUIICgZfb2F1dGgiTAoLT0F1dGhDb25maWcSHAoUYXV0aG9yaXplX3N0YXJ0X3BhdGgYASABKAkSHwoXYXV0aG9yaXplX2NhbGxiYWNrX3BhdGgYAiABKAkiVgoHSW5ib3VuZBIhCgd3ZWJob29rGAEgASgLMhAuYXBwcy52MS5XZWJob29rEigKB2FjY291bnQYAiABKAsyFy5hcHBzLnYxLkFjY291bnRXZWJob29rIhcKB1dlYmhvb2sSDAoEcGF0aBgBIAEoCSI/Cg5BY2NvdW50V2ViaG9vaxIMCgRwYXRoGAEgASgJEg8KB3NldHRpbmcYAiABKAkSDgoGc291cmNlGAMgASgJIt0CCgpTb3VyY2VUeXBlEg0KBWxhYmVsGAEgASgJEi8KBmZpZWxkcxgCIAMoCzIfLmFwcHMudjEuU291cmNlVHlwZS5GaWVsZHNFbnRyeRITCgtuYXR1cmFsX2tleRgDIAEoCRIQCghvcHRpb25hbBgEIAEoCBIWCg5wcm92aXNpb25fb25seRgFIAEoCBIrCglyZWZlcmVuY2UYBiABKAsyGC5hcHBzLnYxLlNvdXJjZVJlZmVyZW5jZRIkCgVvd25lchgHIAEoDjIVLmFwcHMudjEuU3RvcmFnZU93bmVyEhMKC3JlY29tbWVuZGVkGAggASgIEg4KBmxvb2t1cBgJIAMoCRIQCghpZGVudGl0eRgKIAEoCRpGCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSJgoFdmFsdWUYAiABKAsyFy5hcHBzLnYxLlN1Z2dlc3RlZEZpZWxkOgI4ASKqAgoPU291cmNlUmVmZXJlbmNlEhEKCWtleV9maWVsZBgBIAEoCRI0CgZmaWVsZHMYAiADKAsyJC5hcHBzLnYxLlNvdXJjZVJlZmVyZW5jZS5GaWVsZHNFbnRyeRIQCghlbmRwb2ludBgDIAEoCRINCgVob3N0cxgEIAMoCRIqCgdyZXF1ZXN0GAUgASgLMhkuYXBwcy52MS5SZWZlcmVuY2VSZXF1ZXN0EhoKEnJlc3BvbnNlX3RyYW5zZm9ybRgGIAEoCRIdChVjYWNoZV9tYXhfYWdlX3NlY29uZHMYByABKAUaRgoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiYKBXZhbHVlGAIgASgLMhcuYXBwcy52MS5TdWdnZXN0ZWRGaWVsZDoCOAEiswIKEFJlZmVyZW5jZVJlcXVlc3QSDgoGbWV0aG9kGAEgASgJEhUKDXBhdGhfdGVtcGxhdGUYAiABKAkSMwoFcXVlcnkYAyADKAsyJC5hcHBzLnYxLlJlZmVyZW5jZVJlcXVlc3QuUXVlcnlFbnRyeRI3CgdoZWFkZXJzGAQgAygLMiYuYXBwcy52MS5SZWZlcmVuY2VSZXF1ZXN0LkhlYWRlcnNFbnRyeRIVCg1ib2R5X3RlbXBsYXRlGAUgASgJEhUKDWJhdGNoX2NhcGFibGUYBiABKAgaLAoKUXVlcnlFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGi4KDEhlYWRlcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIroBCgxTaW5rQ29udHJhY3QSDQoFbGFiZWwYASABKAkSMQoGZmllbGRzGAIgAygLMiEuYXBwcy52MS5TaW5rQ29udHJhY3QuRmllbGRzRW50cnkSEwoLbmF0dXJhbF9rZXkYAyABKAkSEAoIb3B0aW9uYWwYBCABKAgaQQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiEKBXZhbHVlGAIgASgLMhIuYXBwcy52MS5TaW5rRmllbGQ6AjgBIpYCCg5TdWdnZXN0ZWRGaWVsZBIMCgR0eXBlGAEgASgJEhIKBWxhYmVsGAIgASgJSACIAQESFQoIcmVxdWlyZWQYAyABKAhIAYgBARIsCgZjb25maWcYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0SAKIAQESPQoTc3VwcG9ydGVkX2RpcmVjdGlvbhgFIAEoDjIbLmFwcHMudjEuRmllbGRTeW5jRGlyZWN0aW9uSAOIAQESFgoJcXVlcnlhYmxlGAYgASgISASIAQFCCAoGX2xhYmVsQgsKCV9yZXF1aXJlZEIJCgdfY29uZmlnQhYKFF9zdXBwb3J0ZWRfZGlyZWN0aW9uQgwKCl9xdWVyeWFibGUiWwoJU2lua0ZpZWxkEgwKBHR5cGUYASABKAkSEAoIc2VtYW50aWMYAiABKAkSEAoIcmVxdWlyZWQYAyABKAgSEgoFbGFiZWwYBCABKAlIAIgBAUIICgZfbGFiZWwimAQKCU9wZXJhdGlvbhI6CgNrZXkYASABKAlCLbpIKnIoGEAyJF4oX18pP1thLXpdW2EtejAtOV9dKigtW2EtejAtOV9dKykqJBIMCgRuYW1lGAIgASgJEhgKC2Rlc2NyaXB0aW9uGAMgASgJSACIAQESHQoIZW5kcG9pbnQYBCABKAlCC7pICHIGMgReLy4qEhQKDGNhcGFiaWxpdGllcxgFIAMoCRIXCgp0aW1lb3V0X21zGAYgASgFSAGIAQESMgoMcmV0cnlfcG9saWN5GAcgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgCiAEBEjIKDGlucHV0X3NjaGVtYRgIIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RIA4gBARIzCg1vdXRwdXRfc2NoZW1hGAkgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgEiAEBEhsKDnN1cHBvcnRzX2FzeW5jGAogASgISAWIAQESGQoRcnVuX2FmdGVyX2luc3RhbGwYCyABKAgSHgoWcnVuc19vbl9tYXBwZWRfc291cmNlcxgMIAEoCEIOCgxfZGVzY3JpcHRpb25CDQoLX3RpbWVvdXRfbXNCDwoNX3JldHJ5X3BvbGljeUIPCg1faW5wdXRfc2NoZW1hQhAKDl9vdXRwdXRfc2NoZW1hQhEKD19zdXBwb3J0c19hc3luYyI9ChZNYW5pZmVzdENhcGFiaWxpdHlTcGVjEhAKCGRlY2xhcmVzGAEgASgJEhEKCXBlcl9tb2RlbBgCIAEoCCKmAQoESG9vaxIzCgNrZXkYASABKAlCJrpII3IhGEAyHV5bYS16XVthLXowLTldKigtW2EtejAtOV0rKSokEgwKBG5hbWUYAiABKAkSFgoFZXZlbnQYAyABKAlCB7pIBHICEAESJwoGZmlsdGVyGAQgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdBIaCglvcGVyYXRpb24YBSABKAlCB7pIBHICEAEiNwoCVUkSMQoKcGxhY2VtZW50cxgBIAMoCzIdLmFwcHMudjEuUGxhY2VtZW50RGVjbGFyYXRpb24itgQKFFBsYWNlbWVudERlY2xhcmF0aW9uEjMKA2tleRgBIAEoCUImukgjciEYQDIdXlthLXpdW2EtejAtOV0qKC1bYS16MC05XSspKiQSNAoGdGFyZ2V0GAIgASgOMiQuYXBwcy52MS5QbGFjZW1lbnREZWNsYXJhdGlvbi5UYXJnZXQSEgoFbW9kZWwYAyABKAlIAIgBARISCgVmaWVsZBgEIAEoCUgBiAEBEiEKGWZpZWxkX3NlbGVjdGVkX2F0X2luc3RhbGwYBSABKAgSEAoDdGFiGAYgASgJSAKIAQESFgoFdGl0bGUYByABKAlCB7pIBHICEAESHwoDdXJsGAggASgJQhK6SA9yDTILXmh0dHBzOi8vLisSHQoQcmVwbGFjZXNfZGVmYXVsdBgJIAEoCEgDiAEBEisKBWhpbnRzGAogASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgEiAEBEg4KBnNjb3BlcxgLIAMoCRIQCghzZXR0aW5ncxgMIAMoCSJ0CgZUYXJnZXQSFgoSVEFSR0VUX1VOU1BFQ0lGSUVEEAASFAoQVEFSR0VUX0ZVTExfUEFHRRABEhYKElRBUkdFVF9NQUlOX0VESVRPUhACEhIKDlRBUkdFVF9TSURFQkFSEAMSEAoMVEFSR0VUX0ZJRUxEEARCCAoGX21vZGVsQggKBl9maWVsZEIGCgRfdGFiQhMKEV9yZXBsYWNlc19kZWZhdWx0QggKBl9oaW50cyqsAQoSRmllbGRTeW5jRGlyZWN0aW9uEiQKIEZJRUxEX1NZTkNfRElSRUNUSU9OX1VOU1BFQ0lGSUVEEAASJAogRklFTERfU1lOQ19ESVJFQ1RJT05fU09VUkNFX09OTFkQARIiCh5GSUVMRF9TWU5DX0RJUkVDVElPTl9TSU5LX09OTFkQAhImCiJGSUVMRF9TWU5DX0RJUkVDVElPTl9CSURJUkVDVElPTkFMEAMqhAEKDFN0b3JhZ2VPd25lchIdChlTVE9SQUdFX09XTkVSX1VOU1BFQ0lGSUVEEAASGQoVU1RPUkFHRV9PV05FUl9QUk9KRUNUEAESHgoaU1RPUkFHRV9PV05FUl9PUkdBTklaQVRJT04QAhIaChZTVE9SQUdFX09XTkVSX0NVU1RPTUVSEAMqvAkKEk1hbmlmZXN0Q2FwYWJpbGl0eRIjCh9NQU5JRkVTVF9DQVBBQklMSVRZX1VOU1BFQ0lGSUVEEAASNgofTUFOSUZFU1RfQ0FQQUJJTElUWV9DT05GSUdfUkVBRBABGhGaph0NCgtjb25maWc6cmVhZBJACiRNQU5JRkVTVF9DQVBBQklMSVRZX1NFQ1JFVFNfUkVBRF9BUFAQAhoWmqYdEgoQc2VjcmV0czpyZWFkLmFwcBI+CiNNQU5JRkVTVF9DQVBBQklMSVRZX1NFQ1JFVFNfUFVUX0FQUBADGhWaph0RCg9zZWNyZXRzOnB1dC5hcHASQAokTUFOSUZFU1RfQ0FQQUJJTElUWV9TRUNSRVRTX0xJU1RfQVBQEAQaFpqmHRIKEHNlY3JldHM6bGlzdC5hcHASRAomTUFOSUZFU1RfQ0FQQUJJTElUWV9TRUNSRVRTX0RFTEVURV9BUFAQBRoYmqYdFAoSc2VjcmV0czpkZWxldGUuYXBwEjgKIE1BTklGRVNUX0NBUEFCSUxJVFlfU1RBVFVTX1dSSVRFEAYaEpqmHQ4KDHN0YXR1czp3cml0ZRI0Ch5NQU5JRkVTVF9DQVBBQklMSVRZX0ZJTEVTX1JFQUQQBxoQmqYdDAoKZmlsZXM6cmVhZBI0Ch5NQU5JRkVTVF9DQVBBQklMSVRZX0ZJTEVTX0xJU1QQCBoQmqYdDAoKZmlsZXM6bGlzdBI2Ch9NQU5JRkVTVF9DQVBBQklMSVRZX0ZJTEVTX1dSSVRFEAkaEZqmHQ0KC2ZpbGVzOndyaXRlEj4KI01BTklGRVNUX0NBUEFCSUxJVFlfQ1VTVE9NRVJTX1dSSVRFEAoaFZqmHREKD2N1c3RvbWVyczp3cml0ZRJGCidNQU5JRkVTVF9DQVBBQklMSVRZX05PVElGSUNBVElPTlNfV1JJVEUQCxoZmqYdFQoTbm90aWZpY2F0aW9uczp3cml0ZRJKCilNQU5JRkVTVF9DQVBBQklMSVRZX05PVElGSUNBVElPTlNfQ09OU0VOVBAMGhuaph0XChVub3RpZmljYXRpb25zOmNvbnNlbnQSRgonTUFOSUZFU1RfQ0FQQUJJTElUWV9PUEVSQVRJT05TX0NPTVBMRVRFEA0aGZqmHRUKE29wZXJhdGlvbnM6Y29tcGxldGUSOwogTUFOSUZFU1RfQ0FQQUJJTElUWV9SRUNPUkRTX1JFQUQQFBoVmqYdEQoNcmVjb3JkczpyZWFkOhABEj0KIU1BTklGRVNUX0NBUEFCSUxJVFlfUkVDT1JEU19XUklURRAVGhaaph0SCg5yZWNvcmRzOndyaXRlOhABEj8KIk1BTklGRVNUX0NBUEFCSUxJVFlfUkVDT1JEU19ERUxFVEUQFhoXmqYdEwoPcmVjb3JkczpkZWxldGU6EAESQQojTUFOSUZFU1RfQ0FQQUJJTElUWV9SRUNPUkRTX1BVQkxJU0gQFxoYmqYdFAoQcmVjb3JkczpwdWJsaXNoOhABEkUKJU1BTklGRVNUX0NBUEFCSUxJVFlfUkVDT1JEU19BVFRSSUJVVEUQGBoamqYdFgoScmVjb3JkczphdHRyaWJ1dGU6EAE6dQoTbWFuaWZlc3RfY2FwYWJpbGl0eRIhLmdvb2dsZS5wcm90b2J1Zi5FbnVtVmFsdWVPcHRpb25zGOPUAyABKAsyHy5hcHBzLnYxLk1hbmlmZXN0Q2FwYWJpbGl0eVNwZWNSEm1hbmlmZXN0Q2FwYWJpbGl0eUI1WjNnaXRodWIuY29tL2VpZGVzdHVkaW8vZm9pci9nZW4vcHJvdG8vYXBwcy92MTthcHBzdjFiBnByb3RvMw", [file_buf_validate_validate, file_google_protobuf_descriptor2, file_google_protobuf_struct]);
11
+ var file_apps_v1_apps = /* @__PURE__ */ fileDesc2("ChJhcHBzL3YxL2FwcHMucHJvdG8SB2FwcHMudjEilgUKCUFwcENvbmZpZxI0CgRuYW1lGAEgASgJQia6SCNyIRhAMh1eW2Etel1bYS16MC05XSooLVthLXowLTldKykqJBIYCgd2ZXJzaW9uGAIgASgJQge6SARyAhABEiMKCG1ldGFkYXRhGAMgASgLMhEuYXBwcy52MS5NZXRhZGF0YRInCgptaWRkbGV3YXJlGAQgASgLMhMuYXBwcy52MS5NaWRkbGV3YXJlEikKC2NyZWRlbnRpYWxzGAUgASgLMhQuYXBwcy52MS5DcmVkZW50aWFscxIwCg9zZXR0aW5nc19zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjAKB3NvdXJjZXMYByADKAsyHy5hcHBzLnYxLkFwcENvbmZpZy5Tb3VyY2VzRW50cnkSLAoFc2lua3MYCCADKAsyHS5hcHBzLnYxLkFwcENvbmZpZy5TaW5rc0VudHJ5EiYKCm9wZXJhdGlvbnMYCSADKAsyEi5hcHBzLnYxLk9wZXJhdGlvbhIcCgVob29rcxgKIAMoCzINLmFwcHMudjEuSG9vaxIXCgJ1aRgLIAEoCzILLmFwcHMudjEuVUkSIQoHaW5ib3VuZBgMIAEoCzIQLmFwcHMudjEuSW5ib3VuZBIiCghmZWF0dXJlcxgNIAMoCzIQLmFwcHMudjEuRmVhdHVyZRpDCgxTb3VyY2VzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVlGAIgASgLMhMuYXBwcy52MS5Tb3VyY2VUeXBlOgI4ARpDCgpTaW5rc0VudHJ5EgsKA2tleRgBIAEoCRIkCgV2YWx1ZRgCIAEoCzIVLmFwcHMudjEuU2lua0NvbnRyYWN0OgI4ASKNAQoHRmVhdHVyZRIzCgNrZXkYASABKAlCJrpII3IhGEAyHV5bYS16XVthLXowLTldKigtW2EtejAtOV0rKSokEhYKBXRpdGxlGAIgASgJQge6SARyAhABEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg8KB3NldHRpbmcYBCABKAkSDwoHc291cmNlcxgFIAMoCSJlCghNZXRhZGF0YRIUCgxkaXNwbGF5X25hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSDAoEaWNvbhgDIAEoCRIOCgZhdXRob3IYBCABKAkSEAoIaG9tZXBhZ2UYBSABKAkiLQoKTWlkZGxld2FyZRIfCgN1cmwYASABKAlCErpID3INMgteaHR0cHM6Ly8uKyLXAgoLQ3JlZGVudGlhbHMSLwoIc3RyYXRlZ3kYASABKA4yHS5hcHBzLnYxLkNyZWRlbnRpYWxzLlN0cmF0ZWd5EiwKBnNjaGVtYRgCIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RIAIgBARIoCgVvYXV0aBgDIAEoCzIULmFwcHMudjEuT0F1dGhDb25maWdIAYgBASKpAQoIU3RyYXRlZ3kSGAoUU1RSQVRFR1lfVU5TUEVDSUZJRUQQABISCg5TVFJBVEVHWV9PQVVUSBABEhQKEFNUUkFURUdZX0FQSV9LRVkQAhIaChZTVFJBVEVHWV9TSEFSRURfU0VDUkVUEAMSFAoQU1RSQVRFR1lfU1NIX0tFWRAEEhEKDVNUUkFURUdZX05PTkUQBRIUChBTVFJBVEVHWV9NQU5BR0VEEAZCCQoHX3NjaGVtYUIICgZfb2F1dGgiTAoLT0F1dGhDb25maWcSHAoUYXV0aG9yaXplX3N0YXJ0X3BhdGgYASABKAkSHwoXYXV0aG9yaXplX2NhbGxiYWNrX3BhdGgYAiABKAkiVgoHSW5ib3VuZBIhCgd3ZWJob29rGAEgASgLMhAuYXBwcy52MS5XZWJob29rEigKB2FjY291bnQYAiABKAsyFy5hcHBzLnYxLkFjY291bnRXZWJob29rIhcKB1dlYmhvb2sSDAoEcGF0aBgBIAEoCSI/Cg5BY2NvdW50V2ViaG9vaxIMCgRwYXRoGAEgASgJEg8KB3NldHRpbmcYAiABKAkSDgoGc291cmNlGAMgASgJIt0CCgpTb3VyY2VUeXBlEg0KBWxhYmVsGAEgASgJEi8KBmZpZWxkcxgCIAMoCzIfLmFwcHMudjEuU291cmNlVHlwZS5GaWVsZHNFbnRyeRITCgtuYXR1cmFsX2tleRgDIAEoCRIQCghvcHRpb25hbBgEIAEoCBIWCg5wcm92aXNpb25fb25seRgFIAEoCBIrCglyZWZlcmVuY2UYBiABKAsyGC5hcHBzLnYxLlNvdXJjZVJlZmVyZW5jZRIkCgVvd25lchgHIAEoDjIVLmFwcHMudjEuU3RvcmFnZU93bmVyEhMKC3JlY29tbWVuZGVkGAggASgIEg4KBmxvb2t1cBgJIAMoCRIQCghpZGVudGl0eRgKIAEoCRpGCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSJgoFdmFsdWUYAiABKAsyFy5hcHBzLnYxLlN1Z2dlc3RlZEZpZWxkOgI4ASKqAgoPU291cmNlUmVmZXJlbmNlEhEKCWtleV9maWVsZBgBIAEoCRI0CgZmaWVsZHMYAiADKAsyJC5hcHBzLnYxLlNvdXJjZVJlZmVyZW5jZS5GaWVsZHNFbnRyeRIQCghlbmRwb2ludBgDIAEoCRINCgVob3N0cxgEIAMoCRIqCgdyZXF1ZXN0GAUgASgLMhkuYXBwcy52MS5SZWZlcmVuY2VSZXF1ZXN0EhoKEnJlc3BvbnNlX3RyYW5zZm9ybRgGIAEoCRIdChVjYWNoZV9tYXhfYWdlX3NlY29uZHMYByABKAUaRgoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiYKBXZhbHVlGAIgASgLMhcuYXBwcy52MS5TdWdnZXN0ZWRGaWVsZDoCOAEiswIKEFJlZmVyZW5jZVJlcXVlc3QSDgoGbWV0aG9kGAEgASgJEhUKDXBhdGhfdGVtcGxhdGUYAiABKAkSMwoFcXVlcnkYAyADKAsyJC5hcHBzLnYxLlJlZmVyZW5jZVJlcXVlc3QuUXVlcnlFbnRyeRI3CgdoZWFkZXJzGAQgAygLMiYuYXBwcy52MS5SZWZlcmVuY2VSZXF1ZXN0LkhlYWRlcnNFbnRyeRIVCg1ib2R5X3RlbXBsYXRlGAUgASgJEhUKDWJhdGNoX2NhcGFibGUYBiABKAgaLAoKUXVlcnlFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGi4KDEhlYWRlcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIroBCgxTaW5rQ29udHJhY3QSDQoFbGFiZWwYASABKAkSMQoGZmllbGRzGAIgAygLMiEuYXBwcy52MS5TaW5rQ29udHJhY3QuRmllbGRzRW50cnkSEwoLbmF0dXJhbF9rZXkYAyABKAkSEAoIb3B0aW9uYWwYBCABKAgaQQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiEKBXZhbHVlGAIgASgLMhIuYXBwcy52MS5TaW5rRmllbGQ6AjgBIpYCCg5TdWdnZXN0ZWRGaWVsZBIMCgR0eXBlGAEgASgJEhIKBWxhYmVsGAIgASgJSACIAQESFQoIcmVxdWlyZWQYAyABKAhIAYgBARIsCgZjb25maWcYBCABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0SAKIAQESPQoTc3VwcG9ydGVkX2RpcmVjdGlvbhgFIAEoDjIbLmFwcHMudjEuRmllbGRTeW5jRGlyZWN0aW9uSAOIAQESFgoJcXVlcnlhYmxlGAYgASgISASIAQFCCAoGX2xhYmVsQgsKCV9yZXF1aXJlZEIJCgdfY29uZmlnQhYKFF9zdXBwb3J0ZWRfZGlyZWN0aW9uQgwKCl9xdWVyeWFibGUiWwoJU2lua0ZpZWxkEgwKBHR5cGUYASABKAkSEAoIc2VtYW50aWMYAiABKAkSEAoIcmVxdWlyZWQYAyABKAgSEgoFbGFiZWwYBCABKAlIAIgBAUIICgZfbGFiZWwimAQKCU9wZXJhdGlvbhI6CgNrZXkYASABKAlCLbpIKnIoGEAyJF4oX18pP1thLXpdW2EtejAtOV9dKigtW2EtejAtOV9dKykqJBIMCgRuYW1lGAIgASgJEhgKC2Rlc2NyaXB0aW9uGAMgASgJSACIAQESHQoIZW5kcG9pbnQYBCABKAlCC7pICHIGMgReLy4qEhQKDGNhcGFiaWxpdGllcxgFIAMoCRIXCgp0aW1lb3V0X21zGAYgASgFSAGIAQESMgoMcmV0cnlfcG9saWN5GAcgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgCiAEBEjIKDGlucHV0X3NjaGVtYRgIIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RIA4gBARIzCg1vdXRwdXRfc2NoZW1hGAkgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgEiAEBEhsKDnN1cHBvcnRzX2FzeW5jGAogASgISAWIAQESGQoRcnVuX2FmdGVyX2luc3RhbGwYCyABKAgSHgoWcnVuc19vbl9tYXBwZWRfc291cmNlcxgMIAEoCEIOCgxfZGVzY3JpcHRpb25CDQoLX3RpbWVvdXRfbXNCDwoNX3JldHJ5X3BvbGljeUIPCg1faW5wdXRfc2NoZW1hQhAKDl9vdXRwdXRfc2NoZW1hQhEKD19zdXBwb3J0c19hc3luYyI9ChZNYW5pZmVzdENhcGFiaWxpdHlTcGVjEhAKCGRlY2xhcmVzGAEgASgJEhEKCXBlcl9tb2RlbBgCIAEoCCKmAQoESG9vaxIzCgNrZXkYASABKAlCJrpII3IhGEAyHV5bYS16XVthLXowLTldKigtW2EtejAtOV0rKSokEgwKBG5hbWUYAiABKAkSFgoFZXZlbnQYAyABKAlCB7pIBHICEAESJwoGZmlsdGVyGAQgASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdBIaCglvcGVyYXRpb24YBSABKAlCB7pIBHICEAEiNwoCVUkSMQoKcGxhY2VtZW50cxgBIAMoCzIdLmFwcHMudjEuUGxhY2VtZW50RGVjbGFyYXRpb24itgQKFFBsYWNlbWVudERlY2xhcmF0aW9uEjMKA2tleRgBIAEoCUImukgjciEYQDIdXlthLXpdW2EtejAtOV0qKC1bYS16MC05XSspKiQSNAoGdGFyZ2V0GAIgASgOMiQuYXBwcy52MS5QbGFjZW1lbnREZWNsYXJhdGlvbi5UYXJnZXQSEgoFbW9kZWwYAyABKAlIAIgBARISCgVmaWVsZBgEIAEoCUgBiAEBEiEKGWZpZWxkX3NlbGVjdGVkX2F0X2luc3RhbGwYBSABKAgSEAoDdGFiGAYgASgJSAKIAQESFgoFdGl0bGUYByABKAlCB7pIBHICEAESHwoDdXJsGAggASgJQhK6SA9yDTILXmh0dHBzOi8vLisSHQoQcmVwbGFjZXNfZGVmYXVsdBgJIAEoCEgDiAEBEisKBWhpbnRzGAogASgLMhcuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdEgEiAEBEg4KBnNjb3BlcxgLIAMoCRIQCghzZXR0aW5ncxgMIAMoCSJ0CgZUYXJnZXQSFgoSVEFSR0VUX1VOU1BFQ0lGSUVEEAASFAoQVEFSR0VUX0ZVTExfUEFHRRABEhYKElRBUkdFVF9NQUlOX0VESVRPUhACEhIKDlRBUkdFVF9TSURFQkFSEAMSEAoMVEFSR0VUX0ZJRUxEEARCCAoGX21vZGVsQggKBl9maWVsZEIGCgRfdGFiQhMKEV9yZXBsYWNlc19kZWZhdWx0QggKBl9oaW50cyqsAQoSRmllbGRTeW5jRGlyZWN0aW9uEiQKIEZJRUxEX1NZTkNfRElSRUNUSU9OX1VOU1BFQ0lGSUVEEAASJAogRklFTERfU1lOQ19ESVJFQ1RJT05fU09VUkNFX09OTFkQARIiCh5GSUVMRF9TWU5DX0RJUkVDVElPTl9TSU5LX09OTFkQAhImCiJGSUVMRF9TWU5DX0RJUkVDVElPTl9CSURJUkVDVElPTkFMEAMqhAEKDFN0b3JhZ2VPd25lchIdChlTVE9SQUdFX09XTkVSX1VOU1BFQ0lGSUVEEAASGQoVU1RPUkFHRV9PV05FUl9QUk9KRUNUEAESHgoaU1RPUkFHRV9PV05FUl9PUkdBTklaQVRJT04QAhIaChZTVE9SQUdFX09XTkVSX0NVU1RPTUVSEAMqxAoKEk1hbmlmZXN0Q2FwYWJpbGl0eRIjCh9NQU5JRkVTVF9DQVBBQklMSVRZX1VOU1BFQ0lGSUVEEAASNgofTUFOSUZFU1RfQ0FQQUJJTElUWV9DT05GSUdfUkVBRBABGhGaph0NCgtjb25maWc6cmVhZBJACiRNQU5JRkVTVF9DQVBBQklMSVRZX1NFQ1JFVFNfUkVBRF9BUFAQAhoWmqYdEgoQc2VjcmV0czpyZWFkLmFwcBI+CiNNQU5JRkVTVF9DQVBBQklMSVRZX1NFQ1JFVFNfUFVUX0FQUBADGhWaph0RCg9zZWNyZXRzOnB1dC5hcHASQAokTUFOSUZFU1RfQ0FQQUJJTElUWV9TRUNSRVRTX0xJU1RfQVBQEAQaFpqmHRIKEHNlY3JldHM6bGlzdC5hcHASRAomTUFOSUZFU1RfQ0FQQUJJTElUWV9TRUNSRVRTX0RFTEVURV9BUFAQBRoYmqYdFAoSc2VjcmV0czpkZWxldGUuYXBwEjgKIE1BTklGRVNUX0NBUEFCSUxJVFlfU1RBVFVTX1dSSVRFEAYaEpqmHQ4KDHN0YXR1czp3cml0ZRI0Ch5NQU5JRkVTVF9DQVBBQklMSVRZX0ZJTEVTX1JFQUQQBxoQmqYdDAoKZmlsZXM6cmVhZBI0Ch5NQU5JRkVTVF9DQVBBQklMSVRZX0ZJTEVTX0xJU1QQCBoQmqYdDAoKZmlsZXM6bGlzdBI2Ch9NQU5JRkVTVF9DQVBBQklMSVRZX0ZJTEVTX1dSSVRFEAkaEZqmHQ0KC2ZpbGVzOndyaXRlEj4KI01BTklGRVNUX0NBUEFCSUxJVFlfQ1VTVE9NRVJTX1dSSVRFEAoaFZqmHREKD2N1c3RvbWVyczp3cml0ZRJGCidNQU5JRkVTVF9DQVBBQklMSVRZX05PVElGSUNBVElPTlNfV1JJVEUQCxoZmqYdFQoTbm90aWZpY2F0aW9uczp3cml0ZRJKCilNQU5JRkVTVF9DQVBBQklMSVRZX05PVElGSUNBVElPTlNfQ09OU0VOVBAMGhuaph0XChVub3RpZmljYXRpb25zOmNvbnNlbnQSPAoiTUFOSUZFU1RfQ0FQQUJJTElUWV9DVVNUT01FUlNfUkVBRBAOGhSaph0QCg5jdXN0b21lcnM6cmVhZBJICihNQU5JRkVTVF9DQVBBQklMSVRZX0NVU1RPTUVSU19SRUFEX05BTUVEEA8aGpqmHRYKFGN1c3RvbWVyczpyZWFkOm5hbWVkEkYKJ01BTklGRVNUX0NBUEFCSUxJVFlfT1BFUkFUSU9OU19DT01QTEVURRANGhmaph0VChNvcGVyYXRpb25zOmNvbXBsZXRlEjsKIE1BTklGRVNUX0NBUEFCSUxJVFlfUkVDT1JEU19SRUFEEBQaFZqmHREKDXJlY29yZHM6cmVhZDoQARI9CiFNQU5JRkVTVF9DQVBBQklMSVRZX1JFQ09SRFNfV1JJVEUQFRoWmqYdEgoOcmVjb3Jkczp3cml0ZToQARI/CiJNQU5JRkVTVF9DQVBBQklMSVRZX1JFQ09SRFNfREVMRVRFEBYaF5qmHRMKD3JlY29yZHM6ZGVsZXRlOhABEkEKI01BTklGRVNUX0NBUEFCSUxJVFlfUkVDT1JEU19QVUJMSVNIEBcaGJqmHRQKEHJlY29yZHM6cHVibGlzaDoQARJFCiVNQU5JRkVTVF9DQVBBQklMSVRZX1JFQ09SRFNfQVRUUklCVVRFEBgaGpqmHRYKEnJlY29yZHM6YXR0cmlidXRlOhABOnUKE21hbmlmZXN0X2NhcGFiaWxpdHkSIS5nb29nbGUucHJvdG9idWYuRW51bVZhbHVlT3B0aW9ucxjj1AMgASgLMh8uYXBwcy52MS5NYW5pZmVzdENhcGFiaWxpdHlTcGVjUhJtYW5pZmVzdENhcGFiaWxpdHlCNVozZ2l0aHViLmNvbS9laWRlc3R1ZGlvL2ZvaXIvZ2VuL3Byb3RvL2FwcHMvdjE7YXBwc3YxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_descriptor2, file_google_protobuf_struct]);
12
12
  var AppConfigSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 0);
13
13
  var FeatureSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 1);
14
14
  var MetadataSchema = /* @__PURE__ */ messageDesc2(file_apps_v1_apps, 2);
package/dist/app.js CHANGED
@@ -279,6 +279,14 @@ function isSelectableFieldKey(fieldKey) {
279
279
  }
280
280
 
281
281
  // src/app/client.ts
282
+ var CUSTOMER_FIELDS = "id email status emailVerifiedAt";
283
+ function isVerified(customer) {
284
+ return typeof customer.emailVerifiedAt === "string" && customer.emailVerifiedAt !== "";
285
+ }
286
+ function customerOf(customer) {
287
+ if (!customer?.id) return null;
288
+ return { id: customer.id, email: customer.email ?? "", verified: isVerified(customer), status: customer.status ?? "" };
289
+ }
282
290
  var RECORD_ID_FIELD = "_id";
283
291
  var RECORD_NATURAL_KEY_FIELD = "_naturalKey";
284
292
  var RECORD_OWNER_FIELD = "_ownerId";
@@ -337,6 +345,83 @@ function createAppClient(config) {
337
345
  const refuse = (message) => {
338
346
  throw new FoirRequestError(`[foir] ${message}`);
339
347
  };
348
+ let surface;
349
+ function served() {
350
+ surface ??= withFoirRetry(async () => {
351
+ const token = (await config.dispatch.token())?.trim();
352
+ if (!token) {
353
+ throw new FoirRequestError(
354
+ "[foir] the dispatch token provider returned no token. An app client calls as the token it was built from."
355
+ );
356
+ }
357
+ let res;
358
+ try {
359
+ res = await doFetch(foirApiEndpoint(config.apiUrl, "/v1/manifest"), {
360
+ ...config.requestInit,
361
+ method: "GET",
362
+ headers: buildFoirHeaders({ dispatchToken: token, dispatchContext: config.dispatch.context ?? void 0 })
363
+ });
364
+ } catch (err) {
365
+ throw new FoirRequestError("[foir] network error", { cause: err });
366
+ }
367
+ if (!res.ok) {
368
+ throw new FoirRequestError(`[foir] GET /v1/manifest failed: HTTP ${res.status} ${res.statusText}`, { status: res.status });
369
+ }
370
+ return await res.json();
371
+ }, config.retry).catch((error) => {
372
+ surface = void 0;
373
+ throw error;
374
+ });
375
+ return surface;
376
+ }
377
+ async function listMany(model, query) {
378
+ if (!Number.isInteger(query.first) || query.first < 1) refuse(`a list of ${model} asked for ${query.first} records; it takes a whole number of at least 1`);
379
+ const op = (await served()).models?.[model]?.ops?.["findMany"];
380
+ const name = op?.field;
381
+ if (!name) {
382
+ return refuse(
383
+ `this token is not served a list of ${model}: the operation it was minted for declares no read of that model`
384
+ );
385
+ }
386
+ const type = appModelTypeName(model);
387
+ const fields = query.fields ?? [];
388
+ const unselectable = fields.filter((key) => !isSelectableFieldKey(key));
389
+ if (unselectable.length > 0) refuse(`a list of ${model} asked for fields that cannot be selected: ${unselectable.join(", ")}`);
390
+ const orderBy = (query.orderBy ?? []).map((entry) => {
391
+ if (!isSelectableFieldKey(entry.field)) refuse(`a list of ${model} was ordered by "${entry.field}", which is not a field name`);
392
+ return { [entry.field]: entry.direction === "desc" ? "DESC" : "ASC" };
393
+ });
394
+ const answer = await field(
395
+ name,
396
+ `query AppListRecords($where: ${op?.argTypes?.["where"] ?? `${type}WhereInput`}, $orderBy: ${op?.argTypes?.["orderBy"] ?? `[${type}OrderByInput]`}, $first: Int) { ${name}(where: $where, orderBy: $orderBy, first: $first) { edges { node { ${[RECORD_ID_FIELD, RECORD_NATURAL_KEY_FIELD, RECORD_OWNER_FIELD, ...fields].join(" ")} } } pageInfo { hasNextPage } } }`,
397
+ {
398
+ first: query.first,
399
+ ...query.where && Object.keys(query.where).length > 0 ? { where: query.where } : {},
400
+ ...orderBy.length > 0 ? { orderBy } : {}
401
+ }
402
+ );
403
+ if (!answer || !Array.isArray(answer.edges)) return refuse(`${name} reported no error and returned no list`);
404
+ const records = answer.edges.map((edge) => {
405
+ const node = edge?.node;
406
+ const id = node?.[RECORD_ID_FIELD];
407
+ if (!node || typeof id !== "string" || id === "") return refuse(`${name} answered a record carrying no id`);
408
+ const values = {};
409
+ for (const key2 of fields) {
410
+ const got = node[key2];
411
+ if (got !== void 0) values[key2] = got;
412
+ }
413
+ const key = node[RECORD_NATURAL_KEY_FIELD];
414
+ const owner = node[RECORD_OWNER_FIELD];
415
+ return {
416
+ id,
417
+ naturalKey: typeof key === "string" && key !== "" ? key : null,
418
+ ownerId: typeof owner === "string" && owner !== "" ? owner : null,
419
+ fields: values,
420
+ unreadable: []
421
+ };
422
+ });
423
+ return { records, hasMore: answer.pageInfo?.hasNextPage === true };
424
+ }
340
425
  async function readOne(model, by, fields) {
341
426
  const byId = "id" in by;
342
427
  const value = byId ? by.id : by.naturalKey;
@@ -512,6 +597,7 @@ function createAppClient(config) {
512
597
  };
513
598
  },
514
599
  read: readOne,
600
+ list: listMany,
515
601
  async delete(model, id) {
516
602
  if (id === "") refuse(`a record delete on ${model} was given an empty id`);
517
603
  const name = `delete${appModelTypeName(model)}`;
@@ -657,11 +743,53 @@ function createAppClient(config) {
657
743
  if (address === "") refuse("a customer was asked for with an empty email");
658
744
  const customer = await field(
659
745
  "createCustomer",
660
- `mutation AppEnsureCustomer($input: CreateCustomerInput!) { createCustomer(input: $input) { id email } }`,
746
+ `mutation AppEnsureCustomer($input: CreateCustomerInput!) { createCustomer(input: $input) { id email emailVerifiedAt } }`,
661
747
  { input: { email: address, returnExisting: true } }
662
748
  );
663
749
  if (!customer?.id) return refuse("createCustomer reported no error and returned no customer");
664
- return { id: customer.id, email: customer.email ?? address };
750
+ return { id: customer.id, email: customer.email ?? address, verified: isVerified(customer) };
751
+ },
752
+ async get(id) {
753
+ if (id.trim() === "") refuse("a customer was asked for with an empty id");
754
+ const customer = await field(
755
+ "customer",
756
+ `query AppCustomer($id: String) { customer(id: $id) { ${CUSTOMER_FIELDS} } }`,
757
+ { id }
758
+ );
759
+ return customerOf(customer);
760
+ },
761
+ async byEmail(email) {
762
+ const address = email.trim();
763
+ if (address === "") refuse("a customer was asked for with an empty email");
764
+ const customer = await field(
765
+ "customer",
766
+ `query AppCustomerByEmail($email: String) { customer(email: $email) { ${CUSTOMER_FIELDS} } }`,
767
+ { email: address }
768
+ );
769
+ return customerOf(customer);
770
+ },
771
+ async list(query = {}) {
772
+ const search = query.search?.trim() ?? "";
773
+ const page = await field(
774
+ "customers",
775
+ `query AppCustomers($first: Int, $after: String, $filter: CustomersFilterInput) {
776
+ customers(first: $first, after: $after, filter: $filter) {
777
+ edges { node { ${CUSTOMER_FIELDS} } }
778
+ pageInfo { hasNextPage endCursor }
779
+ }
780
+ }`,
781
+ {
782
+ ...query.first !== void 0 ? { first: query.first } : {},
783
+ ...query.after !== void 0 ? { after: query.after } : {},
784
+ ...search !== "" ? { filter: { search } } : {}
785
+ }
786
+ );
787
+ const customers = [];
788
+ for (const edge of page?.edges ?? []) {
789
+ const customer = customerOf(edge?.node ?? null);
790
+ if (customer) customers.push(customer);
791
+ }
792
+ return { customers, hasMore: page?.pageInfo?.hasNextPage === true, endCursor: page?.pageInfo?.endCursor ?? null };
665
793
  }
666
794
  },
667
795
  notifications: {
@@ -705,6 +833,36 @@ function createAppClient(config) {
705
833
  if (!answer) return refuse("setCustomerConsent reported no error and answered nothing");
706
834
  return { stale: answer.stale === true };
707
835
  }
836
+ },
837
+ operations: {
838
+ async start(operationKey, options = {}) {
839
+ const key = operationKey.trim();
840
+ if (key === "") refuse("an operation was started with no key");
841
+ const params = ["$operationKey: String!"];
842
+ const args = ["operationKey: $operationKey"];
843
+ const variables = { operationKey: key };
844
+ if (options.input !== void 0) {
845
+ params.push("$input: JSON");
846
+ args.push("input: $input");
847
+ variables.input = options.input;
848
+ }
849
+ if (options.idempotencyKey !== void 0) {
850
+ params.push("$idempotencyKey: String");
851
+ args.push("idempotencyKey: $idempotencyKey");
852
+ variables.idempotencyKey = options.idempotencyKey;
853
+ }
854
+ if (options.concurrencyKey !== void 0) {
855
+ params.push("$concurrencyKey: String");
856
+ args.push("concurrencyKey: $concurrencyKey");
857
+ variables.concurrencyKey = options.concurrencyKey;
858
+ }
859
+ const document = `mutation AppStartOperation(${params.join(", ")}) { startOperation(${args.join(", ")}) { executionId joined deduplicated } }`;
860
+ const keyed = Boolean(options.idempotencyKey || options.concurrencyKey);
861
+ const data = keyed ? await run(document, variables) : await send(document, variables);
862
+ const answer = data["startOperation"];
863
+ if (!answer?.executionId) return refuse("startOperation reported no error and named no run");
864
+ return { executionId: answer.executionId, joined: answer.joined === true, deduplicated: answer.deduplicated === true };
865
+ }
708
866
  }
709
867
  };
710
868
  }
@@ -967,6 +1125,8 @@ var APP_CAPABILITIES = [
967
1125
  "customers:write",
968
1126
  "notifications:write",
969
1127
  "notifications:consent",
1128
+ "customers:read",
1129
+ "customers:read:named",
970
1130
  "operations:complete"
971
1131
  ];
972
1132
  var APP_RECORD_CAPABILITY_PREFIXES = [
@@ -127,9 +127,11 @@ export interface OperationCallback {
127
127
  * - `lifecycle`: a hook on a platform event (a record published, an operation
128
128
  * completed).
129
129
  * - `schedule`: a cron schedule firing.
130
+ * - `webhook`: an app's webhook delivery handing its work to this operation
131
+ * (`operations.start` on the app client).
130
132
  */
131
133
  export interface OperationTrigger {
132
- type: 'ui' | 'api' | 'lifecycle' | 'schedule';
134
+ type: 'ui' | 'api' | 'lifecycle' | 'schedule' | 'webhook';
133
135
  }
134
136
  /**
135
137
  * The record block a caller attached to a run, forwarded as the caller wrote
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
5
  "type": "module",
6
6
  "publishConfig": {