@fourier-labs/harbour-contracts 0.45.0 → 0.47.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 +4 -4
- package/contract-release.json +28 -28
- package/dist/gen-ts/ai-policy-snapshot.d.ts +19 -0
- package/dist/gen-ts/ai-runtime-contract.d.ts +1 -1
- package/dist/gen-ts/ai-trace-event.d.ts +4 -1
- package/dist/gen-ts/app-integrations-declaration.d.ts +8 -2
- package/dist/gen-ts/contract-release.d.ts +6 -6
- package/dist/gen-ts/contract-release.js +6 -6
- package/dist/gen-ts/data-grant-v2.d.ts +60 -16
- package/dist/gen-ts/integration-error-details.d.ts +2 -2
- package/dist/gen-ts/integration-execute-request.d.ts +6 -2
- package/dist/gen-ts/integration-execute-result.d.ts +21 -1
- package/dist/gen-ts/kit-bundle-manifest.d.ts +8 -0
- package/dist/gen-ts/kit-lock.d.ts +8 -0
- package/dist/gen-ts/saas-connection.d.ts +80 -29
- package/dist/resolver/index.d.ts +7 -1
- package/gen-ts/ai-policy-snapshot.ts +19 -0
- package/gen-ts/ai-runtime-contract.ts +1 -1
- package/gen-ts/ai-trace-event.ts +4 -1
- package/gen-ts/app-integrations-declaration.ts +8 -2
- package/gen-ts/contract-release.ts +6 -6
- package/gen-ts/data-grant-v2.ts +62 -16
- package/gen-ts/integration-error-details.ts +10 -2
- package/gen-ts/integration-execute-request.ts +7 -2
- package/gen-ts/integration-execute-result.ts +21 -1
- package/gen-ts/kit-bundle-manifest.ts +8 -0
- package/gen-ts/kit-lock.ts +8 -0
- package/gen-ts/saas-connection.ts +82 -29
- package/package.json +1 -1
- package/schema/ai-policy-snapshot.schema.json +13 -1
- package/schema/ai-runtime-contract.schema.json +1 -1
- package/schema/ai-trace-event.schema.json +7 -1
- package/schema/app-integrations-declaration.schema.json +19 -5
- package/schema/data-grant-v2.schema.json +102 -23
- package/schema/integration-error-details.schema.json +10 -2
- package/schema/integration-execute-request.schema.json +27 -6
- package/schema/integration-execute-result.schema.json +34 -1
- package/schema/kit-bundle-manifest.schema.json +9 -2
- package/schema/saas-connection.schema.json +97 -41
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://schemas.harbour.dev/integration-execute-result.schema.json",
|
|
4
4
|
"title": "IntegrationExecuteResult",
|
|
5
|
-
"description": "The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the
|
|
5
|
+
"description": "The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the five operation results: slack.channel.history -> {messages, hasMore} (plain text/IDs/time only, never the provider payload); slack.message.post -> {messageId}; gmail.thread.list -> {threads, hasMore} (newest inbox threads; each `messageId` is the thread's newest message, what gmail.message.read takes); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set); warehouse.view.read -> {rows} (decimal/64-bit values as strings, dates as ISO strings). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. Results over 1 MiB are rejected, not truncated.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
},
|
|
14
14
|
"hasMore": { "type": "boolean" },
|
|
15
15
|
"messageId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
16
|
+
"threads": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"maxItems": 15,
|
|
19
|
+
"items": { "$ref": "#/definitions/GmailThread" }
|
|
20
|
+
},
|
|
21
|
+
"id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
22
|
+
"threadId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
23
|
+
"subject": { "type": "string", "maxLength": 2048 },
|
|
24
|
+
"from": { "type": "string", "maxLength": 2048 },
|
|
25
|
+
"to": { "type": "string", "maxLength": 8192 },
|
|
26
|
+
"date": { "type": "string", "maxLength": 128 },
|
|
27
|
+
"text": { "type": "string", "maxLength": 16384 },
|
|
28
|
+
"truncated": { "type": "boolean" },
|
|
16
29
|
"rows": {
|
|
17
30
|
"type": "array",
|
|
18
31
|
"maxItems": 1000,
|
|
@@ -22,9 +35,24 @@
|
|
|
22
35
|
"oneOf": [
|
|
23
36
|
{ "required": ["messages", "hasMore"], "maxProperties": 2 },
|
|
24
37
|
{ "required": ["messageId"], "maxProperties": 1 },
|
|
38
|
+
{ "required": ["threads", "hasMore"], "maxProperties": 2 },
|
|
39
|
+
{ "required": ["id", "threadId", "subject", "from", "to", "date", "text", "truncated"], "maxProperties": 8 },
|
|
25
40
|
{ "required": ["rows"], "maxProperties": 1 }
|
|
26
41
|
],
|
|
27
42
|
"definitions": {
|
|
43
|
+
"GmailThread": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["id", "messageId", "subject", "from", "date", "snippet"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
49
|
+
"messageId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
50
|
+
"subject": { "type": "string", "maxLength": 2048 },
|
|
51
|
+
"from": { "type": "string", "maxLength": 2048 },
|
|
52
|
+
"date": { "type": "string", "maxLength": 128 },
|
|
53
|
+
"snippet": { "type": "string", "maxLength": 4096 }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
28
56
|
"ChannelMessage": {
|
|
29
57
|
"type": "object",
|
|
30
58
|
"additionalProperties": false,
|
|
@@ -43,6 +71,11 @@
|
|
|
43
71
|
"hasMore": false
|
|
44
72
|
},
|
|
45
73
|
{ "messageId": "1725840100.000200" },
|
|
74
|
+
{
|
|
75
|
+
"threads": [{ "id": "18f2c3a4b5d6e7f0", "messageId": "18f2c3a4b5d6e7f8", "subject": "Weekly report", "from": "Jane <jane@example.com>", "date": "2026-09-11T08:00:00Z", "snippet": "Shipped the weekly report." }],
|
|
76
|
+
"hasMore": false
|
|
77
|
+
},
|
|
78
|
+
{ "id": "18f2c3a4b5d6e7f8", "threadId": "18f2c3a4b5d6e7f0", "subject": "Weekly report", "from": "Jane <jane@example.com>", "to": "team@example.com", "date": "2026-09-11T08:00:00Z", "text": "Shipped the weekly report.", "truncated": false },
|
|
46
79
|
{ "rows": [{ "week": "2026-W36", "total": "1234.50" }] }
|
|
47
80
|
]
|
|
48
81
|
}
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"required": ["package", "tarballSha256"],
|
|
16
16
|
"properties": {
|
|
17
17
|
"package": { "type": "string", "const": "@harbour/app-sdk" },
|
|
18
|
-
"
|
|
18
|
+
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", "description": "The SDK package version the tarball was packed from (informational; compatibility is decided by tarballSha256)." },
|
|
19
|
+
"tarballSha256": { "$ref": "#/definitions/HexSha256", "description": "SHA-256 of the SDK tarball baked into this bundle; the CodeBuild gate compares it with the pipeline's own tarball." },
|
|
20
|
+
"url": { "type": "string", "pattern": "^https://[^\\s{}]+$", "maxLength": 2048, "description": "Where the CLI downloads the SDK tarball from when it is not shipped inline; the bytes are verified against tarballSha256 before install. The publisher may name the location by content address (for example an OCI blob whose path ends in sha256:<tarballSha256>)." }
|
|
19
21
|
}
|
|
20
22
|
},
|
|
21
23
|
"images": {
|
|
@@ -54,7 +56,12 @@
|
|
|
54
56
|
{
|
|
55
57
|
"schema": "harbour.kit-bundle/1.0",
|
|
56
58
|
"kitVersion": "0.2.0",
|
|
57
|
-
"sdk": {
|
|
59
|
+
"sdk": {
|
|
60
|
+
"package": "@harbour/app-sdk",
|
|
61
|
+
"version": "1.0.0",
|
|
62
|
+
"tarballSha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
|
63
|
+
"url": "https://public.ecr.aws/v2/y6t4p3i8/harbour-kit-bundle/blobs/sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
|
64
|
+
},
|
|
58
65
|
"images": {
|
|
59
66
|
"appGateway": "123456789012.dkr.ecr.us-east-1.amazonaws.com/harbour/app-gateway@sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
|
|
60
67
|
"sessionFixture": "123456789012.dkr.ecr.us-east-1.amazonaws.com/harbour/session-fixture@sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9"
|
|
@@ -2,70 +2,99 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://schemas.harbour.dev/saas-connection.schema.json",
|
|
4
4
|
"title": "SaasConnection",
|
|
5
|
-
"description": "Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both).
|
|
5
|
+
"description": "Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). One connection per tenant and provider: a single bot installation of the platform-owned provider app and a single logical channel map shared by every app environment (grants, requests and user consent links stay per environment). A Gmail connection has neither installation nor channels: user consent is the only step and its one resource is the fixed `inbox` alias. Contains references, never secrets: installation tokens live under their own token records. The pre-T25 per-environment shape (`clientId`, `clientSecretRef`, `environments`, `installations`) is deprecated: the control plane normalises such a stored record on read and never writes it.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
-
"required": ["sourceId", "kind", "provider", "displayName", "status", "version", "
|
|
8
|
+
"required": ["schema", "tenantId", "sourceId", "kind", "provider", "displayName", "status", "version", "channels", "operations", "developmentPreapproval", "createdAt", "updatedAt"],
|
|
9
9
|
"properties": {
|
|
10
|
+
"schema": { "type": "string", "const": "harbour.saas-connection/1.0" },
|
|
11
|
+
"tenantId": { "type": "string", "minLength": 1, "maxLength": 255 },
|
|
10
12
|
"sourceId": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
|
|
11
13
|
"kind": { "type": "string", "const": "saas" },
|
|
12
|
-
"provider": { "
|
|
13
|
-
"displayName": { "type": "string", "minLength": 1, "maxLength":
|
|
14
|
+
"provider": { "$ref": "#/definitions/SaasProvider" },
|
|
15
|
+
"displayName": { "type": "string", "minLength": 1, "maxLength": 120 },
|
|
14
16
|
"status": { "$ref": "#/definitions/SaasConnectionStatus" },
|
|
15
17
|
"version": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency version; every write is conditional on it." },
|
|
16
|
-
"
|
|
18
|
+
"installation": { "$ref": "#/definitions/SaasInstallation" },
|
|
19
|
+
"channels": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"maxProperties": 64,
|
|
22
|
+
"propertyNames": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
|
|
23
|
+
"additionalProperties": { "type": "string", "pattern": "^[A-Z0-9]{5,20}$", "description": "Provider channel ID; only public, non-archived channels are valid." },
|
|
24
|
+
"description": "Logical channel alias -> provider channel ID, shared by every environment. Always empty for Gmail."
|
|
25
|
+
},
|
|
26
|
+
"operations": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"minItems": 1,
|
|
29
|
+
"maxItems": 4,
|
|
30
|
+
"uniqueItems": true,
|
|
31
|
+
"items": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationOperation" },
|
|
32
|
+
"not": { "contains": { "const": "warehouse.view.read" } },
|
|
33
|
+
"description": "Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post; Gmail: gmail.thread.list, gmail.message.read). Never warehouse.view.read."
|
|
34
|
+
},
|
|
35
|
+
"developmentPreapproval": { "$ref": "#/definitions/DevelopmentPreapproval" },
|
|
36
|
+
"blockedDevelopmentBuilders": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"maxItems": 256,
|
|
39
|
+
"uniqueItems": true,
|
|
40
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 255 },
|
|
41
|
+
"description": "Lower-cased builder emails excluded from development preapproval."
|
|
42
|
+
},
|
|
43
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
44
|
+
"updatedAt": { "type": "string", "format": "date-time" },
|
|
45
|
+
"clientId": { "type": "string", "minLength": 1, "maxLength": 255, "deprecated": true, "description": "Deprecated (pre-T25 tenant-owned provider client). Dropped on read; never written." },
|
|
17
46
|
"clientSecretRef": {
|
|
18
47
|
"type": "string",
|
|
19
48
|
"pattern": "^secretref://[A-Za-z0-9][A-Za-z0-9/_.:-]*$",
|
|
20
|
-
"
|
|
49
|
+
"deprecated": true,
|
|
50
|
+
"description": "Deprecated (pre-T25 tenant-owned provider client secret reference). Dropped on read; never written."
|
|
21
51
|
},
|
|
22
52
|
"environments": {
|
|
23
53
|
"type": "object",
|
|
24
54
|
"additionalProperties": false,
|
|
25
55
|
"minProperties": 1,
|
|
56
|
+
"deprecated": true,
|
|
57
|
+
"description": "Deprecated pre-T25 per-environment mapping. On read the development entry's channels become `channels`; never written.",
|
|
26
58
|
"properties": {
|
|
27
59
|
"development": { "$ref": "#/definitions/SaasEnvironmentMapping" },
|
|
28
60
|
"preview": { "$ref": "#/definitions/SaasEnvironmentMapping" },
|
|
29
61
|
"production": { "$ref": "#/definitions/SaasEnvironmentMapping" }
|
|
30
62
|
}
|
|
31
63
|
},
|
|
32
|
-
"operations": {
|
|
33
|
-
"type": "array",
|
|
34
|
-
"minItems": 1,
|
|
35
|
-
"maxItems": 2,
|
|
36
|
-
"uniqueItems": true,
|
|
37
|
-
"items": { "type": "string", "enum": ["slack.channel.history", "slack.message.post"] },
|
|
38
|
-
"description": "Operations IT allows apps to request on this connection."
|
|
39
|
-
},
|
|
40
|
-
"developmentPreapproval": { "$ref": "#/definitions/DevelopmentPreapproval" },
|
|
41
64
|
"installations": {
|
|
42
65
|
"type": "object",
|
|
43
66
|
"propertyNames": { "$ref": "#/definitions/InstallationName" },
|
|
44
|
-
"additionalProperties": { "$ref": "#/definitions/
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"updatedAt": { "type": "string", "format": "date-time" }
|
|
67
|
+
"additionalProperties": { "$ref": "#/definitions/LegacySaasInstallation" },
|
|
68
|
+
"deprecated": true,
|
|
69
|
+
"description": "Deprecated pre-T25 named installations. On read the `nonprod` entry (or the development mapping's) becomes `installation`; never written."
|
|
70
|
+
}
|
|
49
71
|
},
|
|
50
72
|
"definitions": {
|
|
73
|
+
"SaasProvider": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"enum": ["slack", "gmail"],
|
|
76
|
+
"description": "Slack carries an installation and a channel map; Gmail is user-consent only and read-only."
|
|
77
|
+
},
|
|
51
78
|
"SaasConnectionStatus": {
|
|
52
79
|
"type": "string",
|
|
53
80
|
"enum": ["ACTIVE", "DISABLED"],
|
|
54
81
|
"description": "DISABLED stops every app call immediately (details.code CONNECTION_DISABLED)."
|
|
55
82
|
},
|
|
56
|
-
"
|
|
57
|
-
|
|
83
|
+
"SaasInstallationStatus": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": ["installed", "reconnect_required", "revoked"]
|
|
86
|
+
},
|
|
87
|
+
"SaasInstallation": {
|
|
58
88
|
"type": "object",
|
|
59
89
|
"additionalProperties": false,
|
|
60
|
-
"required": ["
|
|
90
|
+
"required": ["tokenRecordId", "workspaceId", "botUserId", "status", "installedAt"],
|
|
91
|
+
"description": "The one bot installation of the platform-owned provider app in this tenant's workspace.",
|
|
61
92
|
"properties": {
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"additionalProperties": { "type": "string", "pattern": "^[A-Z0-9]{1,32}$", "description": "Provider channel ID; only public, non-archived channels are valid." }
|
|
68
|
-
}
|
|
93
|
+
"tokenRecordId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
94
|
+
"workspaceId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
95
|
+
"botUserId": { "type": "string", "maxLength": 128, "description": "Empty only for a legacy record normalised without one." },
|
|
96
|
+
"status": { "$ref": "#/definitions/SaasInstallationStatus" },
|
|
97
|
+
"installedAt": { "type": "string", "format": "date-time" }
|
|
69
98
|
}
|
|
70
99
|
},
|
|
71
100
|
"DevelopmentPreapproval": {
|
|
@@ -77,10 +106,26 @@
|
|
|
77
106
|
"hours": { "type": "integer", "minimum": 1, "maximum": 168, "description": "Development grant duration; default 24." }
|
|
78
107
|
}
|
|
79
108
|
},
|
|
80
|
-
"
|
|
109
|
+
"InstallationName": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,62}$", "deprecated": true },
|
|
110
|
+
"SaasEnvironmentMapping": {
|
|
81
111
|
"type": "object",
|
|
82
112
|
"additionalProperties": false,
|
|
83
|
-
"required": ["
|
|
113
|
+
"required": ["installation", "channels"],
|
|
114
|
+
"deprecated": true,
|
|
115
|
+
"properties": {
|
|
116
|
+
"installation": { "$ref": "#/definitions/InstallationName" },
|
|
117
|
+
"channels": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"maxProperties": 64,
|
|
120
|
+
"propertyNames": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
|
|
121
|
+
"additionalProperties": { "type": "string", "pattern": "^[A-Z0-9]{1,32}$" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"LegacySaasInstallation": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": false,
|
|
128
|
+
"deprecated": true,
|
|
84
129
|
"properties": {
|
|
85
130
|
"tokenRecordId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
86
131
|
"workspaceId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
@@ -91,25 +136,36 @@
|
|
|
91
136
|
},
|
|
92
137
|
"examples": [
|
|
93
138
|
{
|
|
139
|
+
"schema": "harbour.saas-connection/1.0",
|
|
140
|
+
"tenantId": "fourier",
|
|
94
141
|
"sourceId": "company-slack",
|
|
95
142
|
"kind": "saas",
|
|
96
143
|
"provider": "slack",
|
|
97
144
|
"displayName": "Company Slack",
|
|
98
145
|
"status": "ACTIVE",
|
|
99
146
|
"version": 3,
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"environments": {
|
|
103
|
-
"development": { "installation": "nonprod", "channels": { "team-updates": "C0123NONPROD" } },
|
|
104
|
-
"production": { "installation": "prod", "channels": { "team-updates": "C0123PROD" } }
|
|
105
|
-
},
|
|
147
|
+
"installation": { "tokenRecordId": "tok_workspace", "workspaceId": "T0WORKSPACE", "botUserId": "U0BOT", "status": "installed", "installedAt": "2026-09-09T08:00:00Z" },
|
|
148
|
+
"channels": { "team-updates": "C0123TEAM" },
|
|
106
149
|
"operations": ["slack.channel.history", "slack.message.post"],
|
|
107
150
|
"developmentPreapproval": { "enabled": true, "hours": 24 },
|
|
108
|
-
"installations": {
|
|
109
|
-
"nonprod": { "tokenRecordId": "tok_nonprod", "workspaceId": "T0NONPROD", "botUserId": "U0BOT", "status": "ACTIVE" }
|
|
110
|
-
},
|
|
111
151
|
"createdAt": "2026-09-09T08:00:00Z",
|
|
112
152
|
"updatedAt": "2026-09-09T08:00:00Z"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"schema": "harbour.saas-connection/1.0",
|
|
156
|
+
"tenantId": "fourier",
|
|
157
|
+
"sourceId": "gmail",
|
|
158
|
+
"kind": "saas",
|
|
159
|
+
"provider": "gmail",
|
|
160
|
+
"displayName": "Gmail",
|
|
161
|
+
"status": "ACTIVE",
|
|
162
|
+
"version": 1,
|
|
163
|
+
"channels": {},
|
|
164
|
+
"operations": ["gmail.thread.list", "gmail.message.read"],
|
|
165
|
+
"developmentPreapproval": { "enabled": false, "hours": 24 },
|
|
166
|
+
"blockedDevelopmentBuilders": ["contractor@example.com"],
|
|
167
|
+
"createdAt": "2026-09-11T08:00:00Z",
|
|
168
|
+
"updatedAt": "2026-09-11T08:00:00Z"
|
|
113
169
|
}
|
|
114
170
|
]
|
|
115
171
|
}
|