@sinnu-dev/sdk 0.0.0-stage → 0.2.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 +161 -2
- package/dist/index.cjs +446 -0
- package/dist/index.d.cts +1285 -0
- package/dist/index.d.ts +1285 -0
- package/dist/index.js +391 -0
- package/package.json +63 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import createOpenApiClient from "openapi-fetch";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var SinnuError = class extends Error {
|
|
6
|
+
constructor(message, statusCode, body, response) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SinnuError";
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.body = body;
|
|
11
|
+
this.response = response;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var SinnuAuthError = class extends SinnuError {
|
|
15
|
+
constructor(message, statusCode = 401, body, response) {
|
|
16
|
+
super(message, statusCode, body, response);
|
|
17
|
+
this.name = "SinnuAuthError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var SinnuNotFoundError = class extends SinnuError {
|
|
21
|
+
constructor(message, body, response) {
|
|
22
|
+
super(message, 404, body, response);
|
|
23
|
+
this.name = "SinnuNotFoundError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var SinnuValidationError = class extends SinnuError {
|
|
27
|
+
constructor(message, statusCode, body, response) {
|
|
28
|
+
super(message, statusCode, body, response);
|
|
29
|
+
this.name = "SinnuValidationError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var SinnuRateLimitError = class extends SinnuError {
|
|
33
|
+
constructor(message, body, response) {
|
|
34
|
+
super(message, 429, body, response);
|
|
35
|
+
this.name = "SinnuRateLimitError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function toSinnuError(statusCode, body, response) {
|
|
39
|
+
const message = body?.message ?? `Requisi\xE7\xE3o falhou com status ${statusCode}`;
|
|
40
|
+
switch (statusCode) {
|
|
41
|
+
case 401:
|
|
42
|
+
case 403:
|
|
43
|
+
return new SinnuAuthError(message, statusCode, body, response);
|
|
44
|
+
case 404:
|
|
45
|
+
return new SinnuNotFoundError(message, body, response);
|
|
46
|
+
case 400:
|
|
47
|
+
case 422:
|
|
48
|
+
return new SinnuValidationError(message, statusCode, body, response);
|
|
49
|
+
case 429:
|
|
50
|
+
return new SinnuRateLimitError(message, body, response);
|
|
51
|
+
default:
|
|
52
|
+
return new SinnuError(message, statusCode, body, response);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/client.ts
|
|
57
|
+
var DEFAULT_BASE_URL = "https://api.sinnu.com.br";
|
|
58
|
+
var API_KEY_PREFIXES = ["sk_live_"];
|
|
59
|
+
function assertApiKey(apiKey) {
|
|
60
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"[@sinnu-dev/sdk] apiKey \xE9 obrigat\xF3ria. Gere uma chave secreta no painel sinnu (Integra\xE7\xF5es)."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!API_KEY_PREFIXES.some((p) => apiKey.startsWith(p))) {
|
|
66
|
+
console.warn(
|
|
67
|
+
"[@sinnu-dev/sdk] apiKey n\xE3o come\xE7a com 'sk_live_'. Use a chave secreta no backend, nunca uma chave no cliente."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function createTypedClient(options) {
|
|
72
|
+
assertApiKey(options.apiKey);
|
|
73
|
+
return createOpenApiClient({
|
|
74
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
75
|
+
fetch: options.fetch,
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
...options.headers
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function unwrap(p) {
|
|
84
|
+
const { data, error, response } = await p;
|
|
85
|
+
if (error || !response.ok) {
|
|
86
|
+
throw toSinnuError(
|
|
87
|
+
response.status,
|
|
88
|
+
error ?? void 0,
|
|
89
|
+
response
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (data === void 0 || data === null) {
|
|
93
|
+
throw toSinnuError(response.status, { message: "Resposta sem corpo" }, response);
|
|
94
|
+
}
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
var RawHttp = class {
|
|
98
|
+
constructor(baseUrl, apiKey, extraHeaders, fetchImpl) {
|
|
99
|
+
this.baseUrl = baseUrl;
|
|
100
|
+
this.apiKey = apiKey;
|
|
101
|
+
this.extraHeaders = extraHeaders;
|
|
102
|
+
this.fetchImpl = fetchImpl;
|
|
103
|
+
}
|
|
104
|
+
async request(path, options = {}) {
|
|
105
|
+
const url = new URL(path.replace(/^\//, ""), ensureTrailingSlash(this.baseUrl));
|
|
106
|
+
if (options.query) {
|
|
107
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
108
|
+
if (value !== void 0) {
|
|
109
|
+
url.searchParams.set(key, String(value));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
114
|
+
method: options.method ?? "GET",
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
Accept: "application/json",
|
|
119
|
+
...this.extraHeaders
|
|
120
|
+
},
|
|
121
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
122
|
+
});
|
|
123
|
+
const text = await response.text();
|
|
124
|
+
const parsed = text ? safeJsonParse(text) : void 0;
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw toSinnuError(response.status, parsed, response);
|
|
127
|
+
}
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function ensureTrailingSlash(url) {
|
|
132
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
133
|
+
}
|
|
134
|
+
function safeJsonParse(text) {
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(text);
|
|
137
|
+
} catch {
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/resources/checkout.ts
|
|
143
|
+
var DEFAULT_CHECKOUT_BASE_URL = "https://pay.sinnu.com.br";
|
|
144
|
+
var Checkout = class {
|
|
145
|
+
constructor(options = {}) {
|
|
146
|
+
this.options = options;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Monta a URL pública do checkout hosted para um `slug` (8 chars) gerado
|
|
150
|
+
* ao criar um link (`sinnu.links.create(...).url` já vem pronto; este
|
|
151
|
+
* helper é útil quando você só tem o slug).
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const url = sinnu.checkout.url("abcd1234", { reference: "pedido-123" });
|
|
155
|
+
* // => "https://pay.sinnu.com.br/abcd1234?ref=pedido-123"
|
|
156
|
+
*/
|
|
157
|
+
url(slug, options = {}) {
|
|
158
|
+
if (!slug || typeof slug !== "string") {
|
|
159
|
+
throw new Error("[@sinnu-dev/sdk] slug do checkout \xE9 obrigat\xF3rio.");
|
|
160
|
+
}
|
|
161
|
+
const base = options.checkoutBaseUrl ?? this.options.checkoutBaseUrl ?? DEFAULT_CHECKOUT_BASE_URL;
|
|
162
|
+
const url = new URL(encodeURIComponent(slug), ensureTrailingSlash2(base));
|
|
163
|
+
const query = { ...this.options.query, ...options.query };
|
|
164
|
+
for (const [key, value] of Object.entries(query)) {
|
|
165
|
+
if (value !== void 0) {
|
|
166
|
+
url.searchParams.set(key, String(value));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (options.reference !== void 0) {
|
|
170
|
+
if (options.reference.length > 255) {
|
|
171
|
+
throw new Error("[@sinnu-dev/sdk] reference tem no m\xE1ximo 255 caracteres.");
|
|
172
|
+
}
|
|
173
|
+
url.searchParams.set("ref", options.reference);
|
|
174
|
+
}
|
|
175
|
+
return url.toString();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
function ensureTrailingSlash2(url) {
|
|
179
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/pagination.ts
|
|
183
|
+
async function* paginate(fetchPage) {
|
|
184
|
+
let page = 1;
|
|
185
|
+
let seen = 0;
|
|
186
|
+
while (true) {
|
|
187
|
+
const result = await fetchPage(page);
|
|
188
|
+
for (const item of result.data) {
|
|
189
|
+
yield item;
|
|
190
|
+
}
|
|
191
|
+
seen += result.data.length;
|
|
192
|
+
const pageSize = result.pageSize || result.data.length;
|
|
193
|
+
if (result.data.length === 0 || seen >= result.total || pageSize === 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
page += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function collectAll(fetchPage) {
|
|
200
|
+
const out = [];
|
|
201
|
+
for await (const item of paginate(fetchPage)) {
|
|
202
|
+
out.push(item);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/resources/links.ts
|
|
208
|
+
var Links = class {
|
|
209
|
+
constructor(http) {
|
|
210
|
+
this.http = http;
|
|
211
|
+
}
|
|
212
|
+
/** Cria um link de pagamento (valor em centavos). */
|
|
213
|
+
async create(body) {
|
|
214
|
+
return this.http.request("/api/payment-links", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** Busca um link de pagamento pelo id. */
|
|
220
|
+
async get(id) {
|
|
221
|
+
return this.http.request(`/api/payment-links/${encodeURIComponent(id)}`);
|
|
222
|
+
}
|
|
223
|
+
/** Atualiza parcialmente um link de pagamento. */
|
|
224
|
+
async update(id, body) {
|
|
225
|
+
return this.http.request(`/api/payment-links/${encodeURIComponent(id)}`, {
|
|
226
|
+
method: "PATCH",
|
|
227
|
+
body
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** Arquiva um link de pagamento. */
|
|
231
|
+
async archive(id) {
|
|
232
|
+
await this.http.request(`/api/payment-links/${encodeURIComponent(id)}`, {
|
|
233
|
+
method: "DELETE"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** Lista links de pagamento (paginado). */
|
|
237
|
+
async list(params = {}) {
|
|
238
|
+
return this.http.request("/api/payment-links", {
|
|
239
|
+
query: { page: params.page, pageSize: params.pageSize }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/** Lista transações de um link de pagamento (paginado). */
|
|
243
|
+
async transactions(id, params = {}) {
|
|
244
|
+
return this.http.request(
|
|
245
|
+
`/api/payment-links/${encodeURIComponent(id)}/transactions`,
|
|
246
|
+
{ query: { page: params.page, pageSize: params.pageSize } }
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
/** Itera todos os links de pagamento, página a página. */
|
|
250
|
+
listAll(params = {}) {
|
|
251
|
+
return paginate((page) => this.list({ ...params, page }));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// src/resources/transactions.ts
|
|
256
|
+
var Transactions = class {
|
|
257
|
+
constructor(http) {
|
|
258
|
+
this.http = http;
|
|
259
|
+
}
|
|
260
|
+
/** Lista transações (paginado, com filtro opcional por status). */
|
|
261
|
+
async list(params = {}) {
|
|
262
|
+
return this.http.request("/api/transactions/", {
|
|
263
|
+
query: {
|
|
264
|
+
status: params.status,
|
|
265
|
+
search: params.search,
|
|
266
|
+
customer: params.customer,
|
|
267
|
+
page: params.page,
|
|
268
|
+
pageSize: params.pageSize
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/** Busca uma transação pelo id. */
|
|
273
|
+
async get(id) {
|
|
274
|
+
return this.http.request(`/api/transactions/${encodeURIComponent(id)}/`);
|
|
275
|
+
}
|
|
276
|
+
/** Estorna (refund) uma transação. */
|
|
277
|
+
async refund(id, body = {}) {
|
|
278
|
+
return this.http.request(
|
|
279
|
+
`/api/transactions/${encodeURIComponent(id)}/refund/`,
|
|
280
|
+
{ method: "POST", body }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
/** Itera todas as transações, página a página. */
|
|
284
|
+
listAll(params = {}) {
|
|
285
|
+
return paginate((page) => this.list({ ...params, page }));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/webhooks.ts
|
|
290
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
291
|
+
var SINNU_SIGNATURE_HEADER = "x-sinnu-signature";
|
|
292
|
+
var SinnuWebhookSignatureError = class extends SinnuError {
|
|
293
|
+
constructor(message) {
|
|
294
|
+
super(message, 0);
|
|
295
|
+
this.name = "SinnuWebhookSignatureError";
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function normalizeSignature(signature) {
|
|
299
|
+
if (signature === void 0) {
|
|
300
|
+
throw new SinnuWebhookSignatureError(
|
|
301
|
+
"[@sinnu-dev/sdk] header de assinatura ausente. Esperado 'X-Sinnu-Signature'."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const value = Array.isArray(signature) ? signature[0] : signature;
|
|
305
|
+
if (!value) {
|
|
306
|
+
throw new SinnuWebhookSignatureError("[@sinnu-dev/sdk] header de assinatura vazio.");
|
|
307
|
+
}
|
|
308
|
+
return value.startsWith("sha256=") ? value.slice("sha256=".length) : value;
|
|
309
|
+
}
|
|
310
|
+
function toBuffer(payload) {
|
|
311
|
+
return typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
|
|
312
|
+
}
|
|
313
|
+
function computeSignature(payload, secret) {
|
|
314
|
+
return createHmac("sha256", secret).update(toBuffer(payload)).digest("hex");
|
|
315
|
+
}
|
|
316
|
+
function verifySignature(options) {
|
|
317
|
+
const { payload, signature, secret } = options;
|
|
318
|
+
if (!secret) {
|
|
319
|
+
throw new SinnuWebhookSignatureError(
|
|
320
|
+
"[@sinnu-dev/sdk] webhook secret \xE9 obrigat\xF3rio para verificar a assinatura."
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const provided = normalizeSignature(signature);
|
|
324
|
+
const expected = computeSignature(payload, secret);
|
|
325
|
+
const providedBuf = Buffer.from(provided, "hex");
|
|
326
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
327
|
+
if (providedBuf.length !== expectedBuf.length) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return timingSafeEqual(providedBuf, expectedBuf);
|
|
331
|
+
}
|
|
332
|
+
function constructEvent(options) {
|
|
333
|
+
if (!verifySignature(options)) {
|
|
334
|
+
throw new SinnuWebhookSignatureError(
|
|
335
|
+
"[@sinnu-dev/sdk] assinatura de webhook inv\xE1lida. Requisi\xE7\xE3o rejeitada."
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const text = typeof options.payload === "string" ? options.payload : options.payload.toString("utf8");
|
|
339
|
+
try {
|
|
340
|
+
return JSON.parse(text);
|
|
341
|
+
} catch {
|
|
342
|
+
throw new SinnuWebhookSignatureError("[@sinnu-dev/sdk] corpo do webhook n\xE3o \xE9 um JSON v\xE1lido.");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
var webhooks = {
|
|
346
|
+
signatureHeader: SINNU_SIGNATURE_HEADER,
|
|
347
|
+
computeSignature,
|
|
348
|
+
verifySignature,
|
|
349
|
+
constructEvent
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/index.ts
|
|
353
|
+
var Sinnu = class {
|
|
354
|
+
constructor(options) {
|
|
355
|
+
/** Verificação de webhooks (HMAC SHA-256): não faz requisições. */
|
|
356
|
+
this.webhooks = webhooks;
|
|
357
|
+
this.http = createTypedClient(options);
|
|
358
|
+
const raw = new RawHttp(
|
|
359
|
+
options.baseUrl ?? DEFAULT_BASE_URL,
|
|
360
|
+
options.apiKey,
|
|
361
|
+
options.headers ?? {},
|
|
362
|
+
options.fetch ?? fetch
|
|
363
|
+
);
|
|
364
|
+
this.links = new Links(raw);
|
|
365
|
+
this.transactions = new Transactions(raw);
|
|
366
|
+
this.checkout = new Checkout({ checkoutBaseUrl: options.checkoutBaseUrl });
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var index_default = Sinnu;
|
|
370
|
+
export {
|
|
371
|
+
Checkout,
|
|
372
|
+
DEFAULT_BASE_URL,
|
|
373
|
+
DEFAULT_CHECKOUT_BASE_URL,
|
|
374
|
+
SINNU_SIGNATURE_HEADER,
|
|
375
|
+
Sinnu,
|
|
376
|
+
SinnuAuthError,
|
|
377
|
+
SinnuError,
|
|
378
|
+
SinnuNotFoundError,
|
|
379
|
+
SinnuRateLimitError,
|
|
380
|
+
SinnuValidationError,
|
|
381
|
+
SinnuWebhookSignatureError,
|
|
382
|
+
collectAll,
|
|
383
|
+
computeSignature,
|
|
384
|
+
constructEvent,
|
|
385
|
+
createTypedClient,
|
|
386
|
+
index_default as default,
|
|
387
|
+
paginate,
|
|
388
|
+
unwrap,
|
|
389
|
+
verifySignature,
|
|
390
|
+
webhooks
|
|
391
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@sinnu-dev/sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SDK oficial da Sinnu para Node.js e TypeScript: cobre com Pix e cartão por links de pagamento, acompanhe transações e valide webhooks.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sinnu",
|
|
7
|
+
"pix",
|
|
8
|
+
"cartao-de-credito",
|
|
9
|
+
"pagamentos",
|
|
10
|
+
"gateway-de-pagamento",
|
|
11
|
+
"link-de-pagamento",
|
|
12
|
+
"checkout",
|
|
13
|
+
"cobranca",
|
|
14
|
+
"webhooks",
|
|
15
|
+
"agencias",
|
|
16
|
+
"prestadores-de-servico",
|
|
17
|
+
"saas",
|
|
18
|
+
"brasil",
|
|
19
|
+
"typescript",
|
|
20
|
+
"nodejs"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://docs.sinnu.com.br/sdk",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Sinnu (https://sinnu.com.br)",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"require": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"generate": "node scripts/generate.mjs",
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"type-check": "tsc --noEmit",
|
|
47
|
+
"test": "node --test tests/*.test.mjs",
|
|
48
|
+
"check-release": "node scripts/check-release.mjs",
|
|
49
|
+
"prepublishOnly": "npm run generate && npm run build && npm run type-check && npm test"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"openapi-fetch": "^0.13.5",
|
|
53
|
+
"openapi-typescript-helpers": "^0.0.15"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.0.0",
|
|
57
|
+
"openapi-typescript": "^7.5.0",
|
|
58
|
+
"tsup": "^8.5.0",
|
|
59
|
+
"typescript": "^5.6.0"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|