@ziggs-ai/api-client 0.3.0 → 0.4.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.
Files changed (37) hide show
  1. package/dist/capabilities/artifacts.d.ts +3 -0
  2. package/dist/capabilities/artifacts.js +86 -0
  3. package/dist/capabilities/chat.d.ts +11 -0
  4. package/dist/capabilities/chat.js +38 -0
  5. package/dist/capabilities/connections.d.ts +4 -0
  6. package/dist/capabilities/connections.js +112 -0
  7. package/dist/capabilities/context.d.ts +23 -0
  8. package/dist/capabilities/context.js +220 -0
  9. package/dist/capabilities/discovery.d.ts +4 -0
  10. package/dist/capabilities/discovery.js +77 -0
  11. package/dist/capabilities/grants.d.ts +9 -0
  12. package/dist/capabilities/grants.js +77 -0
  13. package/dist/capabilities/index.d.ts +9 -0
  14. package/dist/capabilities/index.js +9 -0
  15. package/dist/capabilities/links.d.ts +17 -0
  16. package/dist/capabilities/links.js +193 -0
  17. package/dist/capabilities/payments.d.ts +11 -0
  18. package/dist/capabilities/payments.js +404 -0
  19. package/dist/capabilities/types.d.ts +88 -0
  20. package/dist/capabilities/types.js +24 -0
  21. package/dist/http/AgreementClient.d.ts +7 -0
  22. package/dist/http/AgreementClient.js +2 -0
  23. package/dist/http/ConnectionsClient.d.ts +27 -1
  24. package/dist/http/ConnectionsClient.js +29 -0
  25. package/dist/http/GrantsClient.d.ts +16 -0
  26. package/dist/http/GrantsClient.js +3 -0
  27. package/dist/http/OrgsClient.d.ts +36 -0
  28. package/dist/http/OrgsClient.js +61 -0
  29. package/dist/http/PaymentsClient.d.ts +75 -10
  30. package/dist/http/PaymentsClient.js +26 -14
  31. package/dist/http/index.d.ts +5 -5
  32. package/dist/http/index.js +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/package.json +1 -1
  36. package/dist/http/grantRails.d.ts +0 -20
  37. package/dist/http/grantRails.js +0 -50
@@ -0,0 +1,36 @@
1
+ import 'dotenv/config';
2
+ import type { Creds } from '../types.js';
3
+ export interface MyOrg {
4
+ orgId: string;
5
+ name: string;
6
+ kind: string;
7
+ role?: string;
8
+ }
9
+ /**
10
+ * ZIG-739 / ZIG-956 — the operator's full org membership (not just granted
11
+ * scopes, which is all the grant listers see). Lets a delegate resolve an org
12
+ * name to an id and offer a pick-list instead of demanding a pasted org_... id.
13
+ * Moved here from ziggs-mcp so every surface rides the one client (ZIG-894).
14
+ */
15
+ export declare function fetchMyOrgs(creds: Creds, baseUrl?: string): Promise<MyOrg[]>;
16
+ export type OrgResolution = {
17
+ status: 'ok';
18
+ orgId: string;
19
+ } | {
20
+ status: 'ambiguous';
21
+ matches: MyOrg[];
22
+ } | {
23
+ status: 'not-found';
24
+ };
25
+ /**
26
+ * ZIG-739 — resolve an org selector (exact org_... id OR a name/handle) against
27
+ * the operator's memberships. Exact id wins; otherwise case-insensitive name
28
+ * match. Ambiguous names return the candidates rather than guessing.
29
+ */
30
+ export declare function resolveOrgSelector(orgs: MyOrg[], selector: string): OrgResolution;
31
+ /**
32
+ * ZIG-640 / ZIG-956 — runtime acting org from the server (self-hire / agent
33
+ * row): GET /agents/claude-delegate/access. Moved here from ziggs-mcp's inline
34
+ * fetch (ZIG-894 "one client for every surface").
35
+ */
36
+ export declare function fetchDelegateAccess(creds: Creds, baseUrl?: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,61 @@
1
+ import 'dotenv/config';
2
+ import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { buildOperatorHeaders } from './operatorHeaders.js';
4
+ /**
5
+ * ZIG-739 / ZIG-956 — the operator's full org membership (not just granted
6
+ * scopes, which is all the grant listers see). Lets a delegate resolve an org
7
+ * name to an id and offer a pick-list instead of demanding a pasted org_... id.
8
+ * Moved here from ziggs-mcp so every surface rides the one client (ZIG-894).
9
+ */
10
+ export async function fetchMyOrgs(creds, baseUrl) {
11
+ const url = `${baseUrl || getBackendUrl()}/orgs/me`;
12
+ const res = await fetch(url, {
13
+ method: 'GET',
14
+ headers: buildOperatorHeaders(creds.operatorKey, creds.agentId),
15
+ });
16
+ const body = await res.text().catch(() => '');
17
+ if (!res.ok) {
18
+ throw new Error(`GET /orgs/me ${res.status} ${body.slice(0, 200)}`);
19
+ }
20
+ const parsed = body ? JSON.parse(body) : {};
21
+ return (parsed.orgs ?? []).map((o) => ({
22
+ orgId: o.orgId,
23
+ name: o.name,
24
+ kind: o.kind,
25
+ role: o.role,
26
+ }));
27
+ }
28
+ /**
29
+ * ZIG-739 — resolve an org selector (exact org_... id OR a name/handle) against
30
+ * the operator's memberships. Exact id wins; otherwise case-insensitive name
31
+ * match. Ambiguous names return the candidates rather than guessing.
32
+ */
33
+ export function resolveOrgSelector(orgs, selector) {
34
+ const byId = orgs.find((o) => o.orgId === selector);
35
+ if (byId)
36
+ return { status: 'ok', orgId: byId.orgId };
37
+ const needle = selector.toLowerCase();
38
+ const byName = orgs.filter((o) => o.name.toLowerCase() === needle);
39
+ if (byName.length === 1)
40
+ return { status: 'ok', orgId: byName[0].orgId };
41
+ if (byName.length > 1)
42
+ return { status: 'ambiguous', matches: byName };
43
+ return { status: 'not-found' };
44
+ }
45
+ /**
46
+ * ZIG-640 / ZIG-956 — runtime acting org from the server (self-hire / agent
47
+ * row): GET /agents/claude-delegate/access. Moved here from ziggs-mcp's inline
48
+ * fetch (ZIG-894 "one client for every surface").
49
+ */
50
+ export async function fetchDelegateAccess(creds, baseUrl) {
51
+ const url = `${baseUrl || getBackendUrl()}/agents/claude-delegate/access`;
52
+ const res = await fetch(url, {
53
+ method: 'GET',
54
+ headers: buildOperatorHeaders(creds.operatorKey, creds.agentId),
55
+ });
56
+ const body = await res.text().catch(() => '');
57
+ if (!res.ok) {
58
+ throw new Error(`GET /agents/claude-delegate/access ${res.status} ${body.slice(0, 200)}`);
59
+ }
60
+ return body ? JSON.parse(body) : {};
61
+ }
@@ -11,6 +11,71 @@ export interface WalletBalance {
11
11
  balance: number;
12
12
  availableBalance: number;
13
13
  }
14
+ export interface PaymentTransactionView {
15
+ transactionId?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ /** A resolved wallet reference (GET /payments/wallets/resolve). */
19
+ export interface WalletRef {
20
+ walletId?: string;
21
+ ownerId?: string;
22
+ currency?: string;
23
+ status?: string;
24
+ [key: string]: unknown;
25
+ }
26
+ export type TransferResult = {
27
+ status: 'approval_required';
28
+ approvalId: string | null;
29
+ expiresAt: string | null;
30
+ reason: string | null;
31
+ toWalletId: string;
32
+ amount: number;
33
+ } | {
34
+ status: 'transferred';
35
+ transactionId: string | null;
36
+ toWalletId: string;
37
+ amount: number;
38
+ };
39
+ export interface HoldResult {
40
+ transaction?: PaymentTransactionView;
41
+ [key: string]: unknown;
42
+ }
43
+ export interface ReleaseResult {
44
+ transaction?: PaymentTransactionView;
45
+ [key: string]: unknown;
46
+ }
47
+ export interface PaymentGrantView {
48
+ grantId?: string;
49
+ holderId?: string;
50
+ parentGrantId?: string;
51
+ caveats?: unknown[];
52
+ expiresAt?: string | null;
53
+ [key: string]: unknown;
54
+ }
55
+ export interface PaymentGrantEnvelope {
56
+ grant?: PaymentGrantView;
57
+ [key: string]: unknown;
58
+ }
59
+ export interface RevokeGrantResult {
60
+ revoked?: unknown;
61
+ [key: string]: unknown;
62
+ }
63
+ export interface PaymentApproval {
64
+ approvalId?: string;
65
+ status?: string;
66
+ executedTransactionId?: string | null;
67
+ [key: string]: unknown;
68
+ }
69
+ export type WaitForApprovalResult = {
70
+ status: 'gone';
71
+ } | {
72
+ status: 'executed';
73
+ approval: PaymentApproval;
74
+ transactionId: string | null;
75
+ } | {
76
+ status: 'rejected' | 'expired' | 'timeout';
77
+ approval: PaymentApproval;
78
+ };
14
79
  /**
15
80
  * ZIG-894 — the one payments client for every surface (agent-sdk, ziggs-mcp,
16
81
  * scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
@@ -28,25 +93,25 @@ export declare class PaymentsClient {
28
93
  resolve({ userId, agentId }?: {
29
94
  userId?: string;
30
95
  agentId?: string;
31
- }): Promise<unknown>;
96
+ }): Promise<WalletRef | null>;
32
97
  transfer({ to, amount, idempotencyKey, description, paymentGrantId, }: {
33
98
  to: string;
34
99
  amount: number;
35
100
  idempotencyKey?: string;
36
101
  description?: string;
37
102
  paymentGrantId?: string;
38
- }): Promise<unknown>;
103
+ }): Promise<TransferResult>;
39
104
  hold({ amount, idempotencyKey, description, }: {
40
105
  amount: number;
41
106
  idempotencyKey?: string;
42
107
  description?: string;
43
- }): Promise<unknown>;
108
+ }): Promise<HoldResult>;
44
109
  release({ holdId, action, toWalletId, idempotencyKey, }: {
45
110
  holdId: string;
46
111
  action?: string;
47
112
  toWalletId?: string;
48
113
  idempotencyKey?: string;
49
- }): Promise<unknown>;
114
+ }): Promise<ReleaseResult>;
50
115
  history(params?: {
51
116
  limit?: number;
52
117
  offset?: number;
@@ -55,13 +120,13 @@ export declare class PaymentsClient {
55
120
  issueGrant({ holderId, caveats }: {
56
121
  holderId: string;
57
122
  caveats?: unknown;
58
- }): Promise<unknown>;
123
+ }): Promise<PaymentGrantEnvelope>;
59
124
  attenuateGrant({ grantId, holderId, caveats, }: {
60
125
  grantId: string;
61
126
  holderId: string;
62
127
  caveats?: unknown;
63
- }): Promise<unknown>;
64
- revokeGrant(grantId: string): Promise<unknown>;
128
+ }): Promise<PaymentGrantEnvelope>;
129
+ revokeGrant(grantId: string): Promise<RevokeGrantResult>;
65
130
  /**
66
131
  * Live wallet grants the acting agent holds, via the unified GET /grants
67
132
  * (ZIG-648 retired GET /payments/grants), following the cursor to completion.
@@ -80,14 +145,14 @@ export declare class PaymentsClient {
80
145
  }): Promise<unknown>;
81
146
  approvals({ status }?: {
82
147
  status?: string;
83
- }): Promise<unknown[]>;
84
- getApproval(approvalId: string): Promise<unknown | null>;
148
+ }): Promise<PaymentApproval[]>;
149
+ getApproval(approvalId: string): Promise<PaymentApproval | null>;
85
150
  decide(approvalId: string, decision: string, note?: string): Promise<unknown>;
86
151
  waitForApproval(approvalId: string, opts?: {
87
152
  pollMs?: number;
88
153
  timeoutMs?: number;
89
154
  signal?: AbortSignal;
90
- }): Promise<unknown>;
155
+ }): Promise<WaitForApprovalResult>;
91
156
  private _request;
92
157
  private _get;
93
158
  private _post;
@@ -65,10 +65,10 @@ export class PaymentsClient {
65
65
  throw new Error('transfer: paymentGrantId is required for agent-impersonated transfers. The wallet owner must have issued a payment grant to this agentId.');
66
66
  let toWalletId = to;
67
67
  if (!to.startsWith('wal_')) {
68
- const w = (await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to }));
69
- if (!w?.['walletId'])
68
+ const w = await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to });
69
+ if (!w?.walletId)
70
70
  throw new Error(`transfer: could not resolve wallet for "${to}"`);
71
- toWalletId = w['walletId'];
71
+ toWalletId = w.walletId;
72
72
  }
73
73
  const result = (await this._post('/payments/transfer', {
74
74
  toWalletId,
@@ -89,23 +89,28 @@ export class PaymentsClient {
89
89
  };
90
90
  }
91
91
  const tx = result?.['transaction'];
92
- return { status: 'transferred', transactionId: tx?.['transactionId'] || null, toWalletId, amount };
92
+ return {
93
+ status: 'transferred',
94
+ transactionId: tx?.['transactionId'] || null,
95
+ toWalletId,
96
+ amount,
97
+ };
93
98
  }
94
99
  async hold({ amount, idempotencyKey, description, }) {
95
100
  if (!(Number.isInteger(amount) && amount > 0))
96
101
  throw new Error('hold: `amount` must be a positive integer (cents)');
97
- return this._post('/payments/hold', {
102
+ return (await this._post('/payments/hold', {
98
103
  amount,
99
104
  idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'),
100
105
  description,
101
- });
106
+ }));
102
107
  }
103
108
  async release({ holdId, action = 'complete', toWalletId, idempotencyKey, }) {
104
- return this._post(`/payments/release/${holdId}`, {
109
+ return (await this._post(`/payments/release/${holdId}`, {
105
110
  idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'),
106
111
  action,
107
112
  toWalletId,
108
- });
113
+ }));
109
114
  }
110
115
  async history(params = {}) {
111
116
  const q = new URLSearchParams();
@@ -118,13 +123,16 @@ export class PaymentsClient {
118
123
  return this._get('/payments/history' + (q.toString() ? `?${q}` : ''));
119
124
  }
120
125
  async issueGrant({ holderId, caveats }) {
121
- return this._post('/payments/grants', { holderId, caveats });
126
+ return (await this._post('/payments/grants', { holderId, caveats }));
122
127
  }
123
128
  async attenuateGrant({ grantId, holderId, caveats, }) {
124
- return this._post(`/payments/grants/${grantId}/attenuate`, { holderId, caveats });
129
+ return (await this._post(`/payments/grants/${grantId}/attenuate`, {
130
+ holderId,
131
+ caveats,
132
+ }));
125
133
  }
126
134
  async revokeGrant(grantId) {
127
- return this._request('DELETE', `/payments/grants/${grantId}`, undefined);
135
+ return (await this._request('DELETE', `/payments/grants/${grantId}`, undefined));
128
136
  }
129
137
  /**
130
138
  * Live wallet grants the acting agent holds, via the unified GET /grants
@@ -189,12 +197,16 @@ export class PaymentsClient {
189
197
  while (true) {
190
198
  if (signal?.aborted)
191
199
  throw new Error('aborted');
192
- const approval = (await this.getApproval(approvalId));
200
+ const approval = await this.getApproval(approvalId);
193
201
  if (!approval)
194
202
  return { status: 'gone' };
195
- const s = approval['status'];
203
+ const s = approval.status;
196
204
  if (s === 'executed')
197
- return { status: 'executed', approval, transactionId: approval['executedTransactionId'] || null };
205
+ return {
206
+ status: 'executed',
207
+ approval,
208
+ transactionId: approval.executedTransactionId || null,
209
+ };
198
210
  if (s === 'rejected')
199
211
  return { status: 'rejected', approval };
200
212
  if (s === 'expired')
@@ -11,17 +11,17 @@ export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, ContextSna
11
11
  export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
12
12
  export type { DiscoverableItem } from './ContextDiscoveryClient.js';
13
13
  export { GrantsClient } from './GrantsClient.js';
14
- export type { ListGrantsQuery, ListGrantsResult } from './GrantsClient.js';
14
+ export type { ListGrantsQuery, ListGrantsResult, UnreadableRail } from './GrantsClient.js';
15
15
  export { ContextGrantsClient } from './ContextGrantsClient.js';
16
16
  export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, DelegateContextGrantResult, ReachEntry, GrantReachResult, } from './ContextGrantsClient.js';
17
17
  export { grantCaveat } from './grants.js';
18
18
  export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, } from './grants.js';
19
- export { decodeOperatorScopes, unreadableGrantRails } from './grantRails.js';
20
- export type { UnreadableRail } from './grantRails.js';
21
19
  export { PaymentsClient } from './PaymentsClient.js';
22
- export type { PaymentsError, WalletBalance } from './PaymentsClient.js';
20
+ export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
23
21
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
24
- export type { ConnectionsError, ConnectionProxyParams, McpConnectionRequestParams, } from './ConnectionsClient.js';
22
+ export type { ConnectionsError, ConnectionProxyParams, ConnectionGrant, ConnectionWithGrants, McpConnectionRequestParams, } from './ConnectionsClient.js';
23
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, } from './OrgsClient.js';
24
+ export type { MyOrg, OrgResolution } from './OrgsClient.js';
25
25
  export { AgentSearchClient } from './AgentSearchClient.js';
26
26
  export { TelemetryClient } from './TelemetryClient.js';
27
27
  export { InboxClient } from './InboxClient.js';
@@ -9,9 +9,9 @@ export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
9
9
  export { GrantsClient } from './GrantsClient.js';
10
10
  export { ContextGrantsClient } from './ContextGrantsClient.js';
11
11
  export { grantCaveat } from './grants.js';
12
- export { decodeOperatorScopes, unreadableGrantRails } from './grantRails.js';
13
12
  export { PaymentsClient } from './PaymentsClient.js';
14
13
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
14
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, } from './OrgsClient.js';
15
15
  export { AgentSearchClient } from './AgentSearchClient.js';
16
16
  export { TelemetryClient } from './TelemetryClient.js';
17
17
  export { InboxClient } from './InboxClient.js';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './http/index.js';
2
+ export * from './capabilities/index.js';
2
3
  export * from './relay/provisionRelayWorkers.js';
3
4
  export { ConnectionManager } from './ConnectionManager.js';
4
5
  export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, AGREEMENT_ENGAGEMENT_KIND, isValidContentType } from './types.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './http/index.js';
2
+ export * from './capabilities/index.js';
2
3
  export * from './relay/provisionRelayWorkers.js';
3
4
  export { ConnectionManager } from './ConnectionManager.js';
4
5
  export { EntryTypes, ContentTypes, OPEN_AGREEMENT_TARGET, ORG_AGREEMENT_TARGET, BROADCAST_TARGETS, isBroadcastTarget, AGREEMENT_ENGAGEMENT_KIND, isValidContentType } from './types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/api-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,20 +0,0 @@
1
- import type { GrantScopeKind } from './grants.js';
2
- /**
3
- * Read the `scopes` claim of an operator JWT without verifying its signature —
4
- * an identity hint only, exactly as the backend re-checks the key row on every
5
- * request. Returns null when the token isn't a decodable JWT (so callers treat
6
- * entitlement as unknown rather than empty).
7
- */
8
- export declare function decodeOperatorScopes(operatorKey: string): string[] | null;
9
- export interface UnreadableRail {
10
- rail: 'context' | 'connection' | 'wallet';
11
- requiredScope: string;
12
- }
13
- /**
14
- * Which grant rails the operator key can't read, among those requested (or all
15
- * rails when no filter is given). Returns null when the key's scopes can't be
16
- * decoded — the caller then omits the "what I can't see" note rather than
17
- * guessing. `GET /grants` enforces the same boundary server-side; this only
18
- * lets the tool surface report it.
19
- */
20
- export declare function unreadableGrantRails(operatorKey: string, requested?: readonly GrantScopeKind[]): UnreadableRail[] | null;
@@ -1,50 +0,0 @@
1
- const RAIL_GROUPS = [
2
- { rail: 'context', kinds: ['chat', 'agreement', 'org'], requiredScope: 'context:read' },
3
- { rail: 'connection', kinds: ['connection'], requiredScope: 'connections:read' },
4
- { rail: 'wallet', kinds: ['wallet'], requiredScope: 'payments:read' },
5
- ];
6
- /**
7
- * Read the `scopes` claim of an operator JWT without verifying its signature —
8
- * an identity hint only, exactly as the backend re-checks the key row on every
9
- * request. Returns null when the token isn't a decodable JWT (so callers treat
10
- * entitlement as unknown rather than empty).
11
- */
12
- export function decodeOperatorScopes(operatorKey) {
13
- const parts = (operatorKey ?? '').trim().split('.');
14
- if (parts.length !== 3)
15
- return null;
16
- try {
17
- const json = Buffer.from(parts[1], 'base64url').toString('utf8');
18
- const payload = JSON.parse(json);
19
- if (!Array.isArray(payload.scopes))
20
- return null;
21
- return payload.scopes.filter((s) => typeof s === 'string');
22
- }
23
- catch {
24
- return null;
25
- }
26
- }
27
- /**
28
- * Which grant rails the operator key can't read, among those requested (or all
29
- * rails when no filter is given). Returns null when the key's scopes can't be
30
- * decoded — the caller then omits the "what I can't see" note rather than
31
- * guessing. `GET /grants` enforces the same boundary server-side; this only
32
- * lets the tool surface report it.
33
- */
34
- export function unreadableGrantRails(operatorKey, requested) {
35
- const scopes = decodeOperatorScopes(operatorKey);
36
- if (scopes === null)
37
- return null;
38
- const held = new Set(scopes);
39
- const wanted = requested && requested.length > 0 ? new Set(requested) : null;
40
- const out = [];
41
- for (const group of RAIL_GROUPS) {
42
- const inScope = wanted ? group.kinds.some((k) => wanted.has(k)) : true;
43
- if (!inScope)
44
- continue;
45
- if (!held.has(group.requiredScope)) {
46
- out.push({ rail: group.rail, requiredScope: group.requiredScope });
47
- }
48
- }
49
- return out;
50
- }