@permute/sdk 0.6.0 → 0.8.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 +358 -22
- package/dist/app.d.ts +24 -2
- package/dist/app.js +31 -5
- package/dist/auth/oauth.d.ts +64 -0
- package/dist/auth/oauth.js +221 -0
- package/dist/auth/store.d.ts +39 -0
- package/dist/auth/store.js +70 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +1 -0
- package/dist/bridge.d.ts +10 -0
- package/dist/bridge.js +62 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +127 -0
- package/dist/client.d.ts +152 -10
- package/dist/client.js +142 -105
- package/dist/deploy.d.ts +77 -3
- package/dist/deploy.js +136 -58
- package/dist/generated/openapi.d.ts +726 -16
- package/dist/generated/runtime-contract.d.ts +34 -27
- package/dist/generated/runtime-contract.js +123 -8680
- package/dist/index.d.ts +16 -15
- package/dist/index.js +1 -0
- package/dist/method.d.ts +10 -0
- package/dist/method.js +21 -0
- package/dist/sourceFiles.d.ts +16 -0
- package/dist/sourceFiles.js +114 -0
- package/dist/types.d.ts +45 -1
- package/package.json +15 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export * from './client.js';
|
|
3
|
+
export { PermuteSdkBridge } from './bridge.js';
|
|
3
4
|
export { Providers } from './generated/runtime-contract.js';
|
|
4
5
|
export declare const Context: {
|
|
5
|
-
readonly ACUMATICA:
|
|
6
|
-
readonly QUICKBOOKS:
|
|
7
|
-
readonly HUBSPOT:
|
|
8
|
-
readonly ATTIO:
|
|
9
|
-
readonly BIGQUERY:
|
|
10
|
-
readonly BILLCOM:
|
|
11
|
-
readonly BREX:
|
|
12
|
-
readonly DOUBLE:
|
|
13
|
-
readonly FIREFLIES:
|
|
14
|
-
readonly KLAVIYO:
|
|
15
|
-
readonly MERCURY:
|
|
16
|
-
readonly MONDAY:
|
|
17
|
-
readonly SHOPIFY:
|
|
18
|
-
readonly STRIPE:
|
|
19
|
-
readonly XERO:
|
|
6
|
+
readonly ACUMATICA: 'acumatica';
|
|
7
|
+
readonly QUICKBOOKS: 'quickbooks';
|
|
8
|
+
readonly HUBSPOT: 'hubspot';
|
|
9
|
+
readonly ATTIO: 'attio';
|
|
10
|
+
readonly BIGQUERY: 'bigquery';
|
|
11
|
+
readonly BILLCOM: 'billcom';
|
|
12
|
+
readonly BREX: 'brex';
|
|
13
|
+
readonly DOUBLE: 'double';
|
|
14
|
+
readonly FIREFLIES: 'fireflies';
|
|
15
|
+
readonly KLAVIYO: 'klaviyo';
|
|
16
|
+
readonly MERCURY: 'mercury';
|
|
17
|
+
readonly MONDAY: 'monday';
|
|
18
|
+
readonly SHOPIFY: 'shopify';
|
|
19
|
+
readonly STRIPE: 'stripe';
|
|
20
|
+
readonly XERO: 'xero';
|
|
20
21
|
};
|
package/dist/index.js
CHANGED
package/dist/method.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PublicApiOperationId } from './generated/runtime-contract.js';
|
|
2
|
+
type SdkMethod = (...args: never[]) => unknown;
|
|
3
|
+
/** Keep route metadata on the SDK functions that execute it, without a second method registry. */
|
|
4
|
+
export declare class SdkMethodFactory {
|
|
5
|
+
private static readonly operations;
|
|
6
|
+
static create<Method extends SdkMethod>(operationId: PublicApiOperationId, method: Method): Method;
|
|
7
|
+
static compose<Method extends SdkMethod>(dependencies: readonly SdkMethod[], method: Method): Method;
|
|
8
|
+
static operationsFor(method: SdkMethod): readonly PublicApiOperationId[] | undefined;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
package/dist/method.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Keep route metadata on the SDK functions that execute it, without a second method registry. */
|
|
2
|
+
export class SdkMethodFactory {
|
|
3
|
+
static operations = new WeakMap();
|
|
4
|
+
static create(operationId, method) {
|
|
5
|
+
this.operations.set(method, [operationId]);
|
|
6
|
+
return method;
|
|
7
|
+
}
|
|
8
|
+
static compose(dependencies, method) {
|
|
9
|
+
const operations = dependencies.flatMap((dependency) => {
|
|
10
|
+
const selected = this.operations.get(dependency);
|
|
11
|
+
if (!selected)
|
|
12
|
+
throw new Error('SDK method has no route binding.');
|
|
13
|
+
return selected;
|
|
14
|
+
});
|
|
15
|
+
this.operations.set(method, [...new Set(operations)]);
|
|
16
|
+
return method;
|
|
17
|
+
}
|
|
18
|
+
static operationsFor(method) {
|
|
19
|
+
return this.operations.get(method);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Read a bounded snapshot; never follow a file symlink or silently accept a size change. */
|
|
2
|
+
export declare function readLocalFile(absolutePath: string, size: number, signal?: AbortSignal): Promise<Buffer>;
|
|
3
|
+
/** Project ignore rules, independent of Git's index and each developer's global configuration. */
|
|
4
|
+
export declare class SourceFileSelection {
|
|
5
|
+
private readonly root;
|
|
6
|
+
private readonly outputDirectory;
|
|
7
|
+
private readonly signal?;
|
|
8
|
+
private readonly scopes;
|
|
9
|
+
constructor(root: string, outputDirectory: string, signal?: AbortSignal | undefined);
|
|
10
|
+
static excluded(path: string): boolean;
|
|
11
|
+
blocked(path: string): boolean;
|
|
12
|
+
ignored(path: string, directory: boolean, absolutePath?: string): Promise<boolean>;
|
|
13
|
+
resolveLink(absolutePath: string): Promise<string>;
|
|
14
|
+
ignoreContents(path: string): Buffer | undefined;
|
|
15
|
+
private scope;
|
|
16
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import ignore from 'ignore';
|
|
5
|
+
/** Read a bounded snapshot; never follow a file symlink or silently accept a size change. */
|
|
6
|
+
export async function readLocalFile(absolutePath, size, signal) {
|
|
7
|
+
const file = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
8
|
+
try {
|
|
9
|
+
const stat = await file.stat();
|
|
10
|
+
if (!stat.isFile() || stat.size !== size)
|
|
11
|
+
throw new Error(`App file changed: ${absolutePath}`);
|
|
12
|
+
const data = Buffer.alloc(size + 1);
|
|
13
|
+
let length = 0;
|
|
14
|
+
while (length < data.length) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
const { bytesRead } = await file.read(data, length, data.length - length, null);
|
|
17
|
+
if (!bytesRead)
|
|
18
|
+
break;
|
|
19
|
+
length += bytesRead;
|
|
20
|
+
}
|
|
21
|
+
if (length !== size)
|
|
22
|
+
throw new Error(`App file changed: ${absolutePath}`);
|
|
23
|
+
return data.subarray(0, length);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await file.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Project ignore rules, independent of Git's index and each developer's global configuration. */
|
|
30
|
+
export class SourceFileSelection {
|
|
31
|
+
root;
|
|
32
|
+
outputDirectory;
|
|
33
|
+
signal;
|
|
34
|
+
scopes = new Map();
|
|
35
|
+
constructor(root, outputDirectory, signal) {
|
|
36
|
+
this.root = root;
|
|
37
|
+
this.outputDirectory = outputDirectory;
|
|
38
|
+
this.signal = signal;
|
|
39
|
+
}
|
|
40
|
+
static excluded(path) {
|
|
41
|
+
return (path
|
|
42
|
+
.split('/')
|
|
43
|
+
.some((part) => /^(?:\.git|node_modules|\.aws|\.ssh|\.sst|\.cache|\.next|\.turbo|coverage|\.DS_Store)$/i.test(part) ||
|
|
44
|
+
/^(?:\.npmrc|\.netrc|CLAUDE\.local\.md)$/i.test(part) ||
|
|
45
|
+
(/^\.env(?:\..*)?$/i.test(part) && !/\.(?:example|sample|template)$/i.test(part)) ||
|
|
46
|
+
/\.(?:pem|key|p12|pfx)$/i.test(part)) ||
|
|
47
|
+
/(?:^|\/)\.claude\/(?:settings\.local\.json|worktrees|projects)(?:\/|$)/i.test(path) ||
|
|
48
|
+
/(?:^|\/)\.codex\/(?:auth\.json|history\.jsonl|session_index\.jsonl|sessions|logs)(?:\/|$)/i.test(path) ||
|
|
49
|
+
/(?:^|\/)\.gemini\/(?:oauth_creds\.json|google_accounts\.json|tmp)(?:\/|$)/i.test(path));
|
|
50
|
+
}
|
|
51
|
+
blocked(path) {
|
|
52
|
+
const output = this.outputDirectory.toLowerCase();
|
|
53
|
+
return (SourceFileSelection.excluded(path) || path.toLowerCase() === output || path.toLowerCase().startsWith(`${output}/`));
|
|
54
|
+
}
|
|
55
|
+
async ignored(path, directory, absolutePath = join(this.root, path)) {
|
|
56
|
+
const physical = relative(this.root, absolutePath).split(sep).join('/');
|
|
57
|
+
if (physical !== path && (this.blocked(physical) || (await this.ignored(physical, directory))))
|
|
58
|
+
return true;
|
|
59
|
+
// Retain visited ignore files so checkout inherits the same project selection rules.
|
|
60
|
+
const parts = path.split('/');
|
|
61
|
+
const scopes = [];
|
|
62
|
+
for (let depth = 0; depth < parts.length; depth++) {
|
|
63
|
+
const prefix = parts.slice(0, depth).join('/');
|
|
64
|
+
scopes.push({ prefix, rules: (await this.scope(prefix)).rules });
|
|
65
|
+
if (depth === parts.length - 1 && parts[depth] === '.gitignore')
|
|
66
|
+
return false;
|
|
67
|
+
const candidate = parts.slice(0, depth + 1).join('/');
|
|
68
|
+
const suffix = depth < parts.length - 1 || directory ? '/' : '';
|
|
69
|
+
let ignored = false;
|
|
70
|
+
for (const scope of scopes) {
|
|
71
|
+
const result = scope.rules.test(candidate.slice(scope.prefix ? scope.prefix.length + 1 : 0) + suffix);
|
|
72
|
+
if (result.ignored || result.unignored)
|
|
73
|
+
ignored = result.ignored;
|
|
74
|
+
}
|
|
75
|
+
if (ignored)
|
|
76
|
+
return true; // A child rule cannot reinclude a pruned parent directory.
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
async resolveLink(absolutePath) {
|
|
81
|
+
const target = await realpath(absolutePath);
|
|
82
|
+
const path = relative(this.root, target).split(sep).join('/');
|
|
83
|
+
if (!path || path === '..' || path.startsWith('../') || path.startsWith('/') || /^[A-Za-z]:/.test(path)) {
|
|
84
|
+
throw new Error('Source symlinks must target files or directories inside the project.');
|
|
85
|
+
}
|
|
86
|
+
if (this.blocked(path) || (await this.ignored(path, (await lstat(target)).isDirectory()))) {
|
|
87
|
+
throw new Error(`Source symlink target is excluded: ${path}`);
|
|
88
|
+
}
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
91
|
+
ignoreContents(path) {
|
|
92
|
+
const parts = path.split('/');
|
|
93
|
+
if (parts.pop() !== '.gitignore')
|
|
94
|
+
return undefined;
|
|
95
|
+
return this.scopes.get(parts.join('/'))?.contents;
|
|
96
|
+
}
|
|
97
|
+
async scope(prefix) {
|
|
98
|
+
const existing = this.scopes.get(prefix);
|
|
99
|
+
if (existing)
|
|
100
|
+
return existing;
|
|
101
|
+
const path = join(this.root, prefix, '.gitignore');
|
|
102
|
+
const stat = await lstat(path).catch((error) => {
|
|
103
|
+
if (error.code === 'ENOENT')
|
|
104
|
+
return null;
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
if (stat && (!stat.isFile() || stat.size > 4 * 1024 * 1024))
|
|
108
|
+
throw new Error('Invalid .gitignore file.');
|
|
109
|
+
const contents = stat ? await readLocalFile(path, stat.size, this.signal) : undefined;
|
|
110
|
+
const value = { rules: ignore({ ignorecase: false }).add(contents?.toString('utf8') ?? ''), contents };
|
|
111
|
+
this.scopes.set(prefix, value);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { components } from './generated/openapi.js';
|
|
2
2
|
type PublicApiSchema<Name extends keyof components['schemas']> = components['schemas'][Name];
|
|
3
|
+
export type EmailSender = PublicApiSchema<'EmailSender'>;
|
|
4
|
+
export type EmailSendRequest = PublicApiSchema<'EmailSendRequest'>;
|
|
5
|
+
export type EmailReceipt = PublicApiSchema<'EmailReceipt'>;
|
|
6
|
+
export type AppUser = PublicApiSchema<'AppUser'>;
|
|
3
7
|
export type AppGrants = PublicApiSchema<'AppGrants'>;
|
|
8
|
+
export type AppAccess = PublicApiSchema<'AppAccess'>;
|
|
9
|
+
export type AppAddressSettings = PublicApiSchema<'AppAddressSettings'>;
|
|
10
|
+
export type AppAddressSetRequest = PublicApiSchema<'AppAddressSetRequest'>;
|
|
11
|
+
export type AppAccessSetRequest = PublicApiSchema<'AppAccessSetRequest'>;
|
|
4
12
|
export type AppGrantSetRequest = PublicApiSchema<'AppGrantSetRequest'>;
|
|
5
13
|
export type BusinessAction = PublicApiSchema<'BusinessAction'>;
|
|
14
|
+
export type BusinessActionDetail = PublicApiSchema<'BusinessActionDetail'>;
|
|
6
15
|
export type BusinessActionList = PublicApiSchema<'BusinessActionList'>;
|
|
7
16
|
export type ActionCreateRequest = PublicApiSchema<'ActionCreateRequest'>;
|
|
8
17
|
export type ActionDefinition = PublicApiSchema<'ActionDefinition'>;
|
|
@@ -10,6 +19,10 @@ export type ActionDefinitionSaveRequest = PublicApiSchema<'ActionDefinitionSaveR
|
|
|
10
19
|
export type ActionDefinitionSaved = PublicApiSchema<'ActionDefinitionSaved'>;
|
|
11
20
|
export type ActionRequest = PublicApiSchema<'ActionRequest'>;
|
|
12
21
|
export type ActionReceipt = PublicApiSchema<'ActionReceipt'>;
|
|
22
|
+
export type ActionPublicationRequest = PublicApiSchema<'ActionPublicationRequest'>;
|
|
23
|
+
export type ActionPublicationRequestInput = PublicApiSchema<'ActionPublicationRequestInput'>;
|
|
24
|
+
export type ActionPublicationRequestList = PublicApiSchema<'ActionPublicationRequestList'>;
|
|
25
|
+
export type ActionPublicationDecision = PublicApiSchema<'ActionPublicationDecision'>;
|
|
13
26
|
export type ConnectorSession = PublicApiSchema<'ConnectorSession'>;
|
|
14
27
|
export type ConnectorSessionCreated = PublicApiSchema<'ConnectorSessionCreated'>;
|
|
15
28
|
export type ConnectorSessionCreateRequest = PublicApiSchema<'ConnectorSessionCreateRequest'>;
|
|
@@ -43,6 +56,12 @@ export type FileDatasetCreateRequest = Omit<Extract<DatasetApiCreateRequest, {
|
|
|
43
56
|
sizeBytes?: number;
|
|
44
57
|
headers?: Record<string, string>;
|
|
45
58
|
};
|
|
59
|
+
export type FileDatasetMetadataCreateRequest = Extract<DatasetApiCreateRequest, {
|
|
60
|
+
type: 'file';
|
|
61
|
+
}> & {
|
|
62
|
+
data?: undefined;
|
|
63
|
+
headers?: undefined;
|
|
64
|
+
};
|
|
46
65
|
export type DatasetCreateRequest = CustomDatasetCreateRequest | FileDatasetCreateRequest;
|
|
47
66
|
export type DatasetWriteTablesRequest = PublicApiSchema<'DatasetWriteTablesRequest'>;
|
|
48
67
|
export type DatasetWriteTablesResponse = PublicApiSchema<'DatasetWriteTablesResponse'>;
|
|
@@ -65,7 +84,9 @@ export interface PermuteRequestOptions {
|
|
|
65
84
|
pollTimeoutMs?: number;
|
|
66
85
|
}
|
|
67
86
|
export interface PermuteClientOptions {
|
|
68
|
-
apiKey
|
|
87
|
+
apiKey?: string;
|
|
88
|
+
/** Supplies a renewable OAuth access token; mutually exclusive with apiKey. */
|
|
89
|
+
accessToken?: () => Promise<string>;
|
|
69
90
|
baseUrl?: string;
|
|
70
91
|
workspaceId?: string;
|
|
71
92
|
fetch?: typeof fetch;
|
|
@@ -108,6 +129,9 @@ export type AppDeleted = PublicApiSchema<'AppDeleted'>;
|
|
|
108
129
|
export type AppUploadsRequest = PublicApiSchema<'AppUploadsRequest'>;
|
|
109
130
|
export type AppUploads = PublicApiSchema<'AppUploads'>;
|
|
110
131
|
export type AppSiteSaveRequest = PublicApiSchema<'AppSiteSaveRequest'>;
|
|
132
|
+
export type AppSource = AppSiteSaveRequest['source'];
|
|
133
|
+
export type AppSourceFileQuery = PublicApiSchema<'AppSourceFileQuery'>;
|
|
134
|
+
export type AppSourceFileContent = PublicApiSchema<'AppSourceFileContent'>;
|
|
111
135
|
export type AppUploadFile = {
|
|
112
136
|
path: string;
|
|
113
137
|
data: string | Blob | Uint8Array;
|
|
@@ -136,4 +160,24 @@ export type WriteSyncRun = PublicApiSchema<'WriteSyncRun'>;
|
|
|
136
160
|
export type WriteSyncRunList = PublicApiSchema<'WriteSyncRunList'>;
|
|
137
161
|
export type WriteSyncRunQuery = PublicApiSchema<'WriteSyncRunQuery'>;
|
|
138
162
|
export type WriteSyncExecution = PublicApiSchema<'WriteSyncExecution'>;
|
|
163
|
+
export type ChatSendRequest = PublicApiSchema<'ChatSendRequest'>;
|
|
164
|
+
export type ChatDecisionRequest = PublicApiSchema<'ChatDecisionRequest'>;
|
|
165
|
+
export type ChatResponse = PublicApiSchema<'ChatResponse'>;
|
|
166
|
+
export type AccessRequest = PublicApiSchema<'AccessRequest'>;
|
|
167
|
+
export type AccessRequestInput = PublicApiSchema<'AccessRequestInput'>;
|
|
168
|
+
export type AccessRequestQuery = PublicApiSchema<'AccessRequestQuery'>;
|
|
169
|
+
export type AccessRequestList = PublicApiSchema<'AccessRequestList'>;
|
|
170
|
+
export type AccessRequestDecision = PublicApiSchema<'AccessRequestDecision'>;
|
|
171
|
+
export type PermissionInspectionInput = PublicApiSchema<'PermissionInspectionInput'>;
|
|
172
|
+
export type PermissionInspection = PublicApiSchema<'PermissionInspection'>;
|
|
173
|
+
export type AppPublicationRequest = PublicApiSchema<'AppPublicationRequest'>;
|
|
174
|
+
export type AppPublicationRequestInput = PublicApiSchema<'AppPublicationRequestInput'>;
|
|
175
|
+
export type AppPublicationRequestList = PublicApiSchema<'AppPublicationRequestList'>;
|
|
176
|
+
export type AppPublicationDecision = PublicApiSchema<'AppPublicationDecision'>;
|
|
177
|
+
export type PermissionGrant = PublicApiSchema<'PermissionGrant'>;
|
|
178
|
+
export type PermissionGrantInput = PublicApiSchema<'PermissionGrantInput'>;
|
|
179
|
+
export type PermissionGrantUpdate = PublicApiSchema<'PermissionGrantUpdate'>;
|
|
180
|
+
export type PermissionGrantQuery = PublicApiSchema<'PermissionGrantQuery'>;
|
|
181
|
+
export type PermissionGrantList = PublicApiSchema<'PermissionGrantList'>;
|
|
182
|
+
export type PermissionGrantRevoked = PublicApiSchema<'PermissionGrantRevoked'>;
|
|
139
183
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@permute/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "TypeScript client for the Permute public API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"types": "./dist/deploy.d.ts",
|
|
39
39
|
"import": "./dist/deploy.js",
|
|
40
40
|
"default": "./dist/deploy.js"
|
|
41
|
+
},
|
|
42
|
+
"./auth": {
|
|
43
|
+
"types": "./dist/auth.d.ts",
|
|
44
|
+
"import": "./dist/auth.js",
|
|
45
|
+
"default": "./dist/auth.js"
|
|
41
46
|
}
|
|
42
47
|
},
|
|
43
48
|
"publishConfig": {
|
|
@@ -54,14 +59,21 @@
|
|
|
54
59
|
"typecheck": "bun run scripts/typecheck.ts"
|
|
55
60
|
},
|
|
56
61
|
"dependencies": {
|
|
57
|
-
"
|
|
62
|
+
"@napi-rs/keyring": "2.1.2",
|
|
63
|
+
"ajv": "^8.20.0",
|
|
64
|
+
"ignore": "7.0.5",
|
|
65
|
+
"zod": "^4.6.6"
|
|
58
66
|
},
|
|
59
67
|
"devDependencies": {
|
|
60
68
|
"@types/bun": "1.3.13",
|
|
61
69
|
"@types/node": "^24.19.1",
|
|
70
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
62
71
|
"openapi-typescript": "^7.13.0",
|
|
63
72
|
"prettier": "3.6.2",
|
|
64
73
|
"typescript": "^5.9.3",
|
|
65
|
-
"vitest": "^
|
|
74
|
+
"vitest": "^5.0.5"
|
|
75
|
+
},
|
|
76
|
+
"bin": {
|
|
77
|
+
"permute": "./dist/cli.js"
|
|
66
78
|
}
|
|
67
79
|
}
|