@gscdump/contracts 7.11.0 → 7.12.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.
@@ -4,6 +4,7 @@ import { addPartnerTeamMemberSchema, bindPartnerSiteTeamSchema, bindPartnerTeamC
4
4
  import { siteVersionBeginRequestSchema, siteVersionCandidateRequestSchema, siteVersionCandidateSchema, siteVersionIdentityResponseSchema, siteVersionPreviewRequestSchema, siteVersionPreviewSchema } from "../site-version.mjs";
5
5
  import "./version.mjs";
6
6
  import { HTTP_V1_ERROR_CODES, defineHttpOperation, defineHttpSurface, defineResponseObject, defineSuccessResponse } from "./http-core.mjs";
7
+ import { userIntegrationActivityV1Schemas } from "./agent-usage.mjs";
7
8
  import { analyticsCoverageQueryV1Schema, analyticsCoverageV1Schemas } from "./analytics-coverage.mjs";
8
9
  import { bingConnectionV1Schemas } from "./bing.mjs";
9
10
  import { bingDataQueryV1Schema, bingDataV1Schemas } from "./bing-data.mjs";
@@ -12,6 +13,7 @@ import { GSCDUMP_REALTIME_ACK_POLICY, GSCDUMP_REALTIME_CLOSE_CODES, GSCDUMP_REAL
12
13
  import { GSCDUMP_V1_ANALYTICS_DIMENSIONS, createGscdumpV1BrowserSchemas } from "./browser.mjs";
13
14
  import { googleSubmissionReceiptV1Schemas, googleSubmitV1Schema, indexingApiGrantUpdateV1Schema, indexingApiGrantV1Schemas, siteIndexingApiGrantV1Schemas } from "./google-indexing.mjs";
14
15
  import { indexNowConfigureV1Schema, indexNowConnectionV1Schemas, indexNowSubmissionReceiptV1Schemas, indexNowSubmitV1Schema } from "./indexnow.mjs";
16
+ import { runReportV1DataSchemas, runReportV1RequestSchema } from "./report.mjs";
15
17
  import { gscdumpV1OperationRoute, gscdumpV1Surface } from "./route-catalog.mjs";
16
18
  import { sitemapEvidenceV1Schemas, sitemapInspectQueryV1Schema } from "./sitemap-evidence.mjs";
17
19
  import { sitemapSubmissionV1Schemas, sitemapSubmitResultV1Schemas } from "./sitemap-submission.mjs";
@@ -53,6 +55,8 @@ function createGscdumpV1Protocol() {
53
55
  surface: z.literal("partner"),
54
56
  version: z.literal("1.0")
55
57
  });
58
+ const userIntegrationActivityResponse = defineSuccessResponse(userIntegrationActivityV1Schemas, partnerResponseMeta);
59
+ const runReportResponse = defineSuccessResponse(runReportV1DataSchemas, responseMeta);
56
60
  const realtimeResponseMeta = defineResponseObject({
57
61
  requestId: realtimeSchemas.publicRequestId,
58
62
  surface: z.literal("realtime"),
@@ -5605,6 +5609,61 @@ function createGscdumpV1Protocol() {
5605
5609
  }
5606
5610
  }
5607
5611
  }),
5612
+ getUserIntegrationActivity: defineHttpOperation({
5613
+ ...gscdumpV1OperationRoute("partner.users.integration_activity.get"),
5614
+ visibility: "public",
5615
+ semantics: {
5616
+ kind: "query",
5617
+ sideEffects: "none",
5618
+ idempotent: true,
5619
+ retry: "idempotent",
5620
+ readConsistency: "primary"
5621
+ },
5622
+ auth: {
5623
+ credentials: ["partner_key"],
5624
+ scopes: ["users:read"],
5625
+ ownership: [{
5626
+ credential: "partner_key",
5627
+ rule: "linked_user"
5628
+ }]
5629
+ },
5630
+ request: {
5631
+ params: z.strictObject({ userId: realtimeSchemas.publicUserId }),
5632
+ query: null,
5633
+ headers: requestHeaders,
5634
+ body: null
5635
+ },
5636
+ responses: { 200: userIntegrationActivityResponse },
5637
+ errors: partnerUserErrors,
5638
+ errorResponse: errorEnvelopeSchemas(partnerUserErrors, realtimeSchemas.publicRequestId),
5639
+ resources: {
5640
+ reads: [{
5641
+ type: "partner.user",
5642
+ idFrom: "params.userId"
5643
+ }],
5644
+ changes: []
5645
+ },
5646
+ lifecycle: { introduced: "7.12.0" },
5647
+ docs: {
5648
+ summary: "Read partner integration activity",
5649
+ description: "Reads retained activity attributed to credentials issued by this partner. Unknown counts remain null.",
5650
+ tags: ["Users"],
5651
+ examples: {
5652
+ request: { params: { userId: "u_01" } },
5653
+ response: {
5654
+ data: {
5655
+ measuredAt: "2026-11-10T00:00:00.000Z",
5656
+ activities: []
5657
+ },
5658
+ meta: {
5659
+ requestId: "req_01",
5660
+ surface: "partner",
5661
+ version: "1.0"
5662
+ }
5663
+ }
5664
+ }
5665
+ }
5666
+ }),
5608
5667
  deleteUser: defineHttpOperation({
5609
5668
  ...gscdumpV1OperationRoute("partner.users.delete"),
5610
5669
  visibility: "public",
@@ -6423,6 +6482,80 @@ function createGscdumpV1Protocol() {
6423
6482
  const analytics = defineHttpSurface({
6424
6483
  ...gscdumpV1Surface("analytics"),
6425
6484
  operations: {
6485
+ runReport: defineHttpOperation({
6486
+ ...gscdumpV1OperationRoute("analytics.reports.run"),
6487
+ visibility: "public",
6488
+ semantics: {
6489
+ kind: "query",
6490
+ sideEffects: "none",
6491
+ idempotent: true,
6492
+ retry: "idempotent",
6493
+ readConsistency: "primary"
6494
+ },
6495
+ auth: {
6496
+ credentials: ["user_key", "partner_key"],
6497
+ scopes: ["analytics:execute"],
6498
+ ownership: [{
6499
+ credential: "user_key",
6500
+ rule: "authorized_site"
6501
+ }, {
6502
+ credential: "partner_key",
6503
+ rule: "authorized_site"
6504
+ }]
6505
+ },
6506
+ request: {
6507
+ params: z.strictObject({ siteId: realtimeSchemas.publicSiteId }),
6508
+ query: null,
6509
+ headers: requestHeaders,
6510
+ body: runReportV1RequestSchema
6511
+ },
6512
+ responses: { 200: runReportResponse },
6513
+ errors: analyticsRowsErrors,
6514
+ errorResponse: errorEnvelopeSchemas(analyticsRowsErrors, realtimeSchemas.publicRequestId),
6515
+ resources: {
6516
+ reads: [{
6517
+ type: "site.analytics",
6518
+ idFrom: "params.siteId"
6519
+ }],
6520
+ changes: []
6521
+ },
6522
+ lifecycle: { introduced: "7.12.0" },
6523
+ docs: {
6524
+ summary: "Run a retained search evidence report",
6525
+ description: "Returns deterministic report evidence, coverage, and capture clocks without advice or severity.",
6526
+ tags: ["Analytics reports"],
6527
+ examples: {
6528
+ request: {
6529
+ params: { siteId: "s_01" },
6530
+ body: { reportId: "health" }
6531
+ },
6532
+ response: {
6533
+ data: {
6534
+ site: "https://example.com/",
6535
+ generatedAt: "2026-11-10T00:00:00.000Z",
6536
+ cached: false,
6537
+ window: {
6538
+ start: "2026-10-01",
6539
+ end: "2026-10-28",
6540
+ days: 28
6541
+ },
6542
+ meta: {
6543
+ durationMs: 1,
6544
+ rowsScanned: 0,
6545
+ degraded: false,
6546
+ steps: []
6547
+ },
6548
+ sections: []
6549
+ },
6550
+ meta: {
6551
+ requestId: "req_01",
6552
+ surface: "analytics",
6553
+ version: "1.0"
6554
+ }
6555
+ }
6556
+ }
6557
+ }
6558
+ }),
6426
6559
  queryRows: defineHttpOperation({
6427
6560
  ...gscdumpV1OperationRoute("analytics.rows.query"),
6428
6561
  visibility: "public",
@@ -0,0 +1,198 @@
1
+ import { CompatibleResponseSchema } from "./http-core.mjs";
2
+ import { z } from "zod";
3
+ export declare const runReportV1RequestSchema: z.ZodObject<{
4
+ reportId: z.ZodEnum<{
5
+ health: "health";
6
+ risks: "risks";
7
+ opportunities: "opportunities";
8
+ movers: "movers";
9
+ growth: "growth";
10
+ triage: "triage";
11
+ brand: "brand";
12
+ "pre-publish": "pre-publish";
13
+ }>;
14
+ period: z.ZodOptional<z.ZodEnum<{
15
+ custom: "custom";
16
+ "last-7d": "last-7d";
17
+ "last-28d": "last-28d";
18
+ "last-30d": "last-30d";
19
+ "last-90d": "last-90d";
20
+ "last-180d": "last-180d";
21
+ "last-365d": "last-365d";
22
+ mtd: "mtd";
23
+ qtd: "qtd";
24
+ ytd: "ytd";
25
+ "last-quarter": "last-quarter";
26
+ }>>;
27
+ comparison: z.ZodOptional<z.ZodEnum<{
28
+ none: "none";
29
+ "prev-period": "prev-period";
30
+ yoy: "yoy";
31
+ }>>;
32
+ start: z.ZodOptional<z.ZodISODate>;
33
+ end: z.ZodOptional<z.ZodISODate>;
34
+ target: z.ZodOptional<z.ZodString>;
35
+ targetKind: z.ZodOptional<z.ZodEnum<{
36
+ page: "page";
37
+ query: "query";
38
+ }>>;
39
+ brandTerms: z.ZodOptional<z.ZodString>;
40
+ topic: z.ZodOptional<z.ZodString>;
41
+ maxFindings: z.ZodOptional<z.ZodNumber>;
42
+ minClicksChange: z.ZodOptional<z.ZodNumber>;
43
+ force: z.ZodDefault<z.ZodBoolean>;
44
+ }, z.core.$strict>;
45
+ /** Evidence only. Advice and severity are intentionally outside the hosted data contract. */
46
+ export declare const runReportV1DataSchemas: CompatibleResponseSchema<z.ZodObject<{
47
+ readonly site: z.ZodString;
48
+ readonly generatedAt: z.ZodISODateTime;
49
+ readonly cached: z.ZodBoolean;
50
+ readonly window: z.ZodObject<{
51
+ start: z.ZodISODate;
52
+ end: z.ZodISODate;
53
+ days: z.ZodNumber;
54
+ comparison: z.ZodOptional<z.ZodObject<{
55
+ start: z.ZodISODate;
56
+ end: z.ZodISODate;
57
+ }, z.core.$strict>>;
58
+ }, z.core.$strict>;
59
+ readonly meta: z.ZodObject<{
60
+ durationMs: z.ZodNumber;
61
+ rowsScanned: z.ZodNumber;
62
+ degraded: z.ZodBoolean;
63
+ steps: z.ZodArray<z.ZodObject<{
64
+ key: z.ZodString;
65
+ type: z.ZodString;
66
+ status: z.ZodEnum<{
67
+ error: "error";
68
+ skipped: "skipped";
69
+ pending: "pending";
70
+ done: "done";
71
+ running: "running";
72
+ }>;
73
+ error: z.ZodOptional<z.ZodString>;
74
+ coverage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
75
+ kind: z.ZodLiteral<"complete">;
76
+ }, z.core.$strict>, z.ZodObject<{
77
+ kind: z.ZodLiteral<"truncated">;
78
+ fetched: z.ZodNumber;
79
+ }, z.core.$strict>], "kind">>;
80
+ }, z.core.$strict>>;
81
+ }, z.core.$strict>;
82
+ readonly sections: z.ZodArray<z.ZodObject<{
83
+ id: z.ZodString;
84
+ title: z.ZodString;
85
+ summary: z.ZodObject<{
86
+ delta: z.ZodOptional<z.ZodNumber>;
87
+ direction: z.ZodOptional<z.ZodEnum<{
88
+ flat: "flat";
89
+ up: "up";
90
+ down: "down";
91
+ }>>;
92
+ magnitudeLabel: z.ZodOptional<z.ZodString>;
93
+ }, z.core.$strict>;
94
+ coverage: z.ZodEnum<{
95
+ partial: "partial";
96
+ full: "full";
97
+ }>;
98
+ truncated: z.ZodOptional<z.ZodObject<{
99
+ kept: z.ZodNumber;
100
+ total: z.ZodNumber;
101
+ }, z.core.$strict>>;
102
+ dataUnavailable: z.ZodOptional<z.ZodLiteral<true>>;
103
+ findings: z.ZodArray<z.ZodObject<{
104
+ entity: z.ZodObject<{
105
+ kind: z.ZodEnum<{
106
+ page: "page";
107
+ query: "query";
108
+ }>;
109
+ value: z.ZodString;
110
+ }, z.core.$strict>;
111
+ metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
112
+ delta: z.ZodOptional<z.ZodObject<{
113
+ metric: z.ZodString;
114
+ prior: z.ZodNumber;
115
+ current: z.ZodNumber;
116
+ pct: z.ZodNumber;
117
+ }, z.core.$strict>>;
118
+ why: z.ZodOptional<z.ZodString>;
119
+ }, z.core.$strict>>;
120
+ }, z.core.$strict>>;
121
+ }, z.core.$strip>, z.ZodObject<{
122
+ readonly site: z.ZodString;
123
+ readonly generatedAt: z.ZodISODateTime;
124
+ readonly cached: z.ZodBoolean;
125
+ readonly window: z.ZodObject<{
126
+ start: z.ZodISODate;
127
+ end: z.ZodISODate;
128
+ days: z.ZodNumber;
129
+ comparison: z.ZodOptional<z.ZodObject<{
130
+ start: z.ZodISODate;
131
+ end: z.ZodISODate;
132
+ }, z.core.$strict>>;
133
+ }, z.core.$strict>;
134
+ readonly meta: z.ZodObject<{
135
+ durationMs: z.ZodNumber;
136
+ rowsScanned: z.ZodNumber;
137
+ degraded: z.ZodBoolean;
138
+ steps: z.ZodArray<z.ZodObject<{
139
+ key: z.ZodString;
140
+ type: z.ZodString;
141
+ status: z.ZodEnum<{
142
+ error: "error";
143
+ skipped: "skipped";
144
+ pending: "pending";
145
+ done: "done";
146
+ running: "running";
147
+ }>;
148
+ error: z.ZodOptional<z.ZodString>;
149
+ coverage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
150
+ kind: z.ZodLiteral<"complete">;
151
+ }, z.core.$strict>, z.ZodObject<{
152
+ kind: z.ZodLiteral<"truncated">;
153
+ fetched: z.ZodNumber;
154
+ }, z.core.$strict>], "kind">>;
155
+ }, z.core.$strict>>;
156
+ }, z.core.$strict>;
157
+ readonly sections: z.ZodArray<z.ZodObject<{
158
+ id: z.ZodString;
159
+ title: z.ZodString;
160
+ summary: z.ZodObject<{
161
+ delta: z.ZodOptional<z.ZodNumber>;
162
+ direction: z.ZodOptional<z.ZodEnum<{
163
+ flat: "flat";
164
+ up: "up";
165
+ down: "down";
166
+ }>>;
167
+ magnitudeLabel: z.ZodOptional<z.ZodString>;
168
+ }, z.core.$strict>;
169
+ coverage: z.ZodEnum<{
170
+ partial: "partial";
171
+ full: "full";
172
+ }>;
173
+ truncated: z.ZodOptional<z.ZodObject<{
174
+ kept: z.ZodNumber;
175
+ total: z.ZodNumber;
176
+ }, z.core.$strict>>;
177
+ dataUnavailable: z.ZodOptional<z.ZodLiteral<true>>;
178
+ findings: z.ZodArray<z.ZodObject<{
179
+ entity: z.ZodObject<{
180
+ kind: z.ZodEnum<{
181
+ page: "page";
182
+ query: "query";
183
+ }>;
184
+ value: z.ZodString;
185
+ }, z.core.$strict>;
186
+ metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
187
+ delta: z.ZodOptional<z.ZodObject<{
188
+ metric: z.ZodString;
189
+ prior: z.ZodNumber;
190
+ current: z.ZodNumber;
191
+ pct: z.ZodNumber;
192
+ }, z.core.$strict>>;
193
+ why: z.ZodOptional<z.ZodString>;
194
+ }, z.core.$strict>>;
195
+ }, z.core.$strict>>;
196
+ }, z.core.$strip>>;
197
+ export type RunReportV1Request = z.input<typeof runReportV1RequestSchema>;
198
+ export type RunReportV1Data = z.infer<typeof runReportV1DataSchemas.client>;
@@ -0,0 +1,137 @@
1
+ import { defineResponseObject } from "./http-core.mjs";
2
+ import { z } from "zod";
3
+ const runReportV1RequestSchema = z.strictObject({
4
+ reportId: z.enum([
5
+ "health",
6
+ "risks",
7
+ "opportunities",
8
+ "movers",
9
+ "growth",
10
+ "triage",
11
+ "brand",
12
+ "pre-publish"
13
+ ]),
14
+ period: z.enum([
15
+ "last-7d",
16
+ "last-28d",
17
+ "last-30d",
18
+ "last-90d",
19
+ "last-180d",
20
+ "last-365d",
21
+ "mtd",
22
+ "qtd",
23
+ "ytd",
24
+ "last-quarter",
25
+ "custom"
26
+ ]).optional(),
27
+ comparison: z.enum([
28
+ "none",
29
+ "prev-period",
30
+ "yoy"
31
+ ]).optional(),
32
+ start: z.iso.date().optional(),
33
+ end: z.iso.date().optional(),
34
+ target: z.string().min(1).max(2048).optional(),
35
+ targetKind: z.enum(["page", "query"]).optional(),
36
+ brandTerms: z.string().min(1).max(2e3).optional(),
37
+ topic: z.string().min(1).max(2e3).optional(),
38
+ maxFindings: z.number().int().min(1).max(1e3).optional(),
39
+ minClicksChange: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
40
+ force: z.boolean().default(false)
41
+ }).superRefine((value, context) => {
42
+ if (Boolean(value.start) !== Boolean(value.end) || value.period === "custom" && !value.start) context.addIssue({
43
+ code: "custom",
44
+ path: ["start"],
45
+ message: "Custom windows require both start and end."
46
+ });
47
+ if (value.start && value.end && value.start > value.end) context.addIssue({
48
+ code: "custom",
49
+ path: ["end"],
50
+ message: "The end must be on or after the start."
51
+ });
52
+ if (value.reportId === "triage" && (!value.target || !value.targetKind)) context.addIssue({
53
+ code: "custom",
54
+ path: ["target"],
55
+ message: "Triage requires target and targetKind."
56
+ });
57
+ if (value.reportId === "pre-publish" && !value.topic) context.addIssue({
58
+ code: "custom",
59
+ path: ["topic"],
60
+ message: "Pre-publish requires topic."
61
+ });
62
+ if (value.reportId !== "movers" && value.minClicksChange !== void 0) context.addIssue({
63
+ code: "custom",
64
+ path: ["minClicksChange"],
65
+ message: "minClicksChange applies to movers."
66
+ });
67
+ });
68
+ const range = z.strictObject({
69
+ start: z.iso.date(),
70
+ end: z.iso.date()
71
+ });
72
+ const finding = z.strictObject({
73
+ entity: z.strictObject({
74
+ kind: z.enum(["page", "query"]),
75
+ value: z.string()
76
+ }),
77
+ metrics: z.record(z.string(), z.number().finite()),
78
+ delta: z.strictObject({
79
+ metric: z.string(),
80
+ prior: z.number().finite(),
81
+ current: z.number().finite(),
82
+ pct: z.number().finite()
83
+ }).optional(),
84
+ why: z.string().optional()
85
+ });
86
+ const section = z.strictObject({
87
+ id: z.string(),
88
+ title: z.string(),
89
+ summary: z.strictObject({
90
+ delta: z.number().finite().optional(),
91
+ direction: z.enum([
92
+ "up",
93
+ "down",
94
+ "flat"
95
+ ]).optional(),
96
+ magnitudeLabel: z.string().optional()
97
+ }),
98
+ coverage: z.enum(["full", "partial"]),
99
+ truncated: z.strictObject({
100
+ kept: z.number().int().nonnegative(),
101
+ total: z.number().int().nonnegative()
102
+ }).optional(),
103
+ dataUnavailable: z.literal(true).optional(),
104
+ findings: z.array(finding)
105
+ });
106
+ const runReportV1DataSchemas = defineResponseObject({
107
+ site: z.string().min(1),
108
+ generatedAt: z.iso.datetime(),
109
+ cached: z.boolean(),
110
+ window: range.extend({
111
+ days: z.number().int().positive(),
112
+ comparison: range.optional()
113
+ }),
114
+ meta: z.strictObject({
115
+ durationMs: z.number().nonnegative(),
116
+ rowsScanned: z.number().nonnegative(),
117
+ degraded: z.boolean(),
118
+ steps: z.array(z.strictObject({
119
+ key: z.string(),
120
+ type: z.string(),
121
+ status: z.enum([
122
+ "pending",
123
+ "running",
124
+ "done",
125
+ "skipped",
126
+ "error"
127
+ ]),
128
+ error: z.string().optional(),
129
+ coverage: z.discriminatedUnion("kind", [z.strictObject({ kind: z.literal("complete") }), z.strictObject({
130
+ kind: z.literal("truncated"),
131
+ fetched: z.number().int().nonnegative()
132
+ })]).optional()
133
+ }))
134
+ }),
135
+ sections: z.array(section)
136
+ });
137
+ export { runReportV1DataSchemas, runReportV1RequestSchema };
@@ -61,6 +61,11 @@ export declare const GSCDUMP_V1_ROUTE_CATALOG: {
61
61
  readonly method: "GET";
62
62
  readonly template: "/users/{userId}/lifecycle";
63
63
  };
64
+ readonly 'partner.users.integration_activity.get': {
65
+ readonly surface: "partner";
66
+ readonly method: "GET";
67
+ readonly template: "/users/{userId}/integration-activity";
68
+ };
64
69
  readonly 'partner.users.entitlements.get': {
65
70
  readonly surface: "partner";
66
71
  readonly method: "GET";
@@ -441,6 +446,11 @@ export declare const GSCDUMP_V1_ROUTE_CATALOG: {
441
446
  readonly method: "POST";
442
447
  readonly template: "/sites/{siteId}/reports";
443
448
  };
449
+ readonly 'analytics.reports.run': {
450
+ readonly surface: "analytics";
451
+ readonly method: "POST";
452
+ readonly template: "/sites/{siteId}/reports/run";
453
+ };
444
454
  readonly 'analytics.reports.detail.query': {
445
455
  readonly surface: "analytics";
446
456
  readonly method: "POST";
@@ -48,6 +48,11 @@ const GSCDUMP_V1_ROUTE_CATALOG = {
48
48
  method: "GET",
49
49
  template: "/users/{userId}/lifecycle"
50
50
  },
51
+ "partner.users.integration_activity.get": {
52
+ surface: "partner",
53
+ method: "GET",
54
+ template: "/users/{userId}/integration-activity"
55
+ },
51
56
  "partner.users.entitlements.get": {
52
57
  surface: "partner",
53
58
  method: "GET",
@@ -428,6 +433,11 @@ const GSCDUMP_V1_ROUTE_CATALOG = {
428
433
  method: "POST",
429
434
  template: "/sites/{siteId}/reports"
430
435
  },
436
+ "analytics.reports.run": {
437
+ surface: "analytics",
438
+ method: "POST",
439
+ template: "/sites/{siteId}/reports/run"
440
+ },
431
441
  "analytics.reports.detail.query": {
432
442
  surface: "analytics",
433
443
  method: "POST",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/contracts",
3
3
  "type": "module",
4
- "version": "7.11.0",
4
+ "version": "7.12.0",
5
5
  "description": "Shared gscdump.com API, webhook, realtime, and lifecycle contracts.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",