@withloops.ai/contracts 0.2.1 → 0.3.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/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export * from './events.ts';
4
4
  export * from './manifest.ts';
5
5
  export * from './api.ts';
6
6
  export * from './registry.ts';
7
+ export * from './trust.ts';
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export * from "./events.js";
4
4
  export * from "./manifest.js";
5
5
  export * from "./api.js";
6
6
  export * from "./registry.js";
7
+ export * from "./trust.js";
@@ -0,0 +1,175 @@
1
+ import { z } from 'zod';
2
+ import { AppRecord, Caller } from './registry.ts';
3
+ /**
4
+ * Layer 1 (Trust): Loops as an OAuth provider, consent, the audit log, the developer switch and the apps vault.
5
+ * Skill devops-loops-developer-platform §1 (developer access), §6 (vault), §7 (OAuth both ways). Only ever ADD.
6
+ */
7
+ /** The issuer. Discovery at `${LOOPS_OAUTH_ISSUER}/.well-known/oauth-authorization-server`. */
8
+ export declare const LOOPS_OAUTH_ISSUER = "https://api.withloops.ai";
9
+ export declare const OAUTH_PATHS: {
10
+ readonly authorize: "/oauth/authorize";
11
+ readonly token: "/oauth/token";
12
+ readonly register: "/oauth/register";
13
+ };
14
+ /** The `loops` CLI: a public client (PKCE, loopback redirect on 127.0.0.1, no secret). */
15
+ export declare const LOOPS_CLI_CLIENT_ID = "loops-cli";
16
+ /** The scope the CLI asks for: build, submit and dev-install apps as the signed-in developer. */
17
+ export declare const DEVELOPER_SCOPE: "developer";
18
+ export declare const OAuthClientRecord: z.ZodObject<{
19
+ clientId: z.ZodString;
20
+ appId: z.ZodNullable<z.ZodString>;
21
+ name: z.ZodString;
22
+ redirectUris: z.ZodArray<z.ZodString>;
23
+ createdBy: z.ZodString;
24
+ createdAt: z.ZodISODateTime;
25
+ }, z.core.$strip>;
26
+ export type OAuthClientRecord = z.infer<typeof OAuthClientRecord>;
27
+ /** Returned ONCE when a client is minted (`loops create`); the registry keeps only a hash of the secret. */
28
+ export declare const MintedClient: z.ZodObject<{
29
+ clientId: z.ZodString;
30
+ appId: z.ZodNullable<z.ZodString>;
31
+ name: z.ZodString;
32
+ redirectUris: z.ZodArray<z.ZodString>;
33
+ createdBy: z.ZodString;
34
+ createdAt: z.ZodISODateTime;
35
+ clientSecret: z.ZodString;
36
+ }, z.core.$strip>;
37
+ export type MintedClient = z.infer<typeof MintedClient>;
38
+ export declare const CreateAppInput: z.ZodObject<{
39
+ id: z.ZodString;
40
+ name: z.ZodString;
41
+ redirectUris: z.ZodDefault<z.ZodArray<z.ZodString>>;
42
+ }, z.core.$strip>;
43
+ export type CreateAppInput = z.infer<typeof CreateAppInput>;
44
+ /** What a Loops access token stands for, resolved by the registry for backend-api `/v1`. */
45
+ export declare const TokenCaller: z.ZodObject<{
46
+ caller: z.ZodObject<{
47
+ workspaceId: z.ZodString;
48
+ userId: z.ZodString;
49
+ role: z.ZodEnum<{
50
+ owner: "owner";
51
+ admin: "admin";
52
+ member: "member";
53
+ }>;
54
+ developer: z.ZodBoolean;
55
+ superUser: z.ZodBoolean;
56
+ }, z.core.$strip>;
57
+ scopes: z.ZodArray<z.ZodString>;
58
+ clientId: z.ZodString;
59
+ appId: z.ZodNullable<z.ZodString>;
60
+ installId: z.ZodNullable<z.ZodString>;
61
+ }, z.core.$strip>;
62
+ export type TokenCaller = z.infer<typeof TokenCaller>;
63
+ /** One row per consent sheet answered: who approved which scopes for which version. */
64
+ export declare const ConsentRecord: z.ZodObject<{
65
+ id: z.ZodString;
66
+ installId: z.ZodString;
67
+ appId: z.ZodString;
68
+ versionId: z.ZodString;
69
+ workspaceId: z.ZodString;
70
+ grantedBy: z.ZodString;
71
+ scopes: z.ZodArray<z.ZodEnum<{
72
+ "contacts:read": "contacts:read";
73
+ "contacts:write": "contacts:write";
74
+ "companies:read": "companies:read";
75
+ "companies:write": "companies:write";
76
+ "leads:read": "leads:read";
77
+ "leads:write": "leads:write";
78
+ "deals:read": "deals:read";
79
+ "deals:write": "deals:write";
80
+ "notes:write": "notes:write";
81
+ "actions:write": "actions:write";
82
+ "loops:read": "loops:read";
83
+ "loops:write": "loops:write";
84
+ "calls:read": "calls:read";
85
+ "meetings:read": "meetings:read";
86
+ "webhooks:manage": "webhooks:manage";
87
+ }>>;
88
+ consentedAt: z.ZodISODateTime;
89
+ }, z.core.$strip>;
90
+ export type ConsentRecord = z.infer<typeof ConsentRecord>;
91
+ export declare const AuditEntry: z.ZodObject<{
92
+ id: z.ZodNumber;
93
+ at: z.ZodISODateTime;
94
+ actor: z.ZodString;
95
+ workspaceId: z.ZodNullable<z.ZodString>;
96
+ appId: z.ZodNullable<z.ZodString>;
97
+ versionId: z.ZodNullable<z.ZodString>;
98
+ installId: z.ZodNullable<z.ZodString>;
99
+ action: z.ZodString;
100
+ detail: z.ZodRecord<z.ZodString, z.ZodUnknown>;
101
+ }, z.core.$strip>;
102
+ export type AuditEntry = z.infer<typeof AuditEntry>;
103
+ export declare const AuditQuery: z.ZodObject<{
104
+ appId: z.ZodOptional<z.ZodString>;
105
+ installId: z.ZodOptional<z.ZodString>;
106
+ cursor: z.ZodOptional<z.ZodString>;
107
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
108
+ }, z.core.$strip>;
109
+ export type AuditQuery = z.infer<typeof AuditQuery>;
110
+ export declare const WorkspaceDeveloper: z.ZodObject<{
111
+ workspaceId: z.ZodString;
112
+ userId: z.ZodString;
113
+ grantedBy: z.ZodString;
114
+ grantedAt: z.ZodISODateTime;
115
+ }, z.core.$strip>;
116
+ export type WorkspaceDeveloper = z.infer<typeof WorkspaceDeveloper>;
117
+ /** `access_token`, `refresh_token`, `api_key`, `webhook_secret`… */
118
+ export declare const SecretName: z.ZodString;
119
+ export type SecretName = z.infer<typeof SecretName>;
120
+ /** Where a secret belongs. All four parts are the encryption's additional data, so a copied row will not decrypt. */
121
+ export declare const SecretRef: z.ZodObject<{
122
+ installId: z.ZodString;
123
+ workspaceId: z.ZodString;
124
+ appId: z.ZodString;
125
+ name: z.ZodString;
126
+ }, z.core.$strip>;
127
+ export type SecretRef = z.infer<typeof SecretRef>;
128
+ /** What anyone outside the engine may see about a secret: never the value. */
129
+ export declare const SecretInfo: z.ZodObject<{
130
+ name: z.ZodString;
131
+ keyVersion: z.ZodString;
132
+ expiresAt: z.ZodNullable<z.ZodISODateTime>;
133
+ updatedAt: z.ZodISODateTime;
134
+ }, z.core.$strip>;
135
+ export type SecretInfo = z.infer<typeof SecretInfo>;
136
+ /** `backend-apps-registry` default WorkerEntrypoint implements this as well as `RegistryRpc`. */
137
+ export interface RegistryTrustRpc {
138
+ /** Create an app owned by the caller's workspace and mint its OAuth client (`loops create`). Developer only. */
139
+ createApp(caller: Caller, input: CreateAppInput): Promise<{
140
+ ok: true;
141
+ app: AppRecord;
142
+ client: MintedClient;
143
+ } | {
144
+ ok: false;
145
+ error: string;
146
+ }>;
147
+ /** Resolve a Loops OAuth access token (`Authorization: Bearer …`) for `/v1`; null when unknown, expired or revoked. */
148
+ resolveToken(bearer: string): Promise<TokenCaller | null>;
149
+ /** True when the user has the developer switch on in that workspace. */
150
+ isDeveloper(workspaceId: string, userId: string): Promise<boolean>;
151
+ /** Owner/Admin only. */
152
+ setDeveloper(caller: Caller, userId: string, on: boolean): Promise<{
153
+ ok: boolean;
154
+ error?: string;
155
+ }>;
156
+ listDevelopers(caller: Caller): Promise<WorkspaceDeveloper[]>;
157
+ /** Consents for one install of the caller's workspace, newest first. */
158
+ listConsents(caller: Caller, installId: string): Promise<ConsentRecord[]>;
159
+ /** Audit rows for the caller's workspace (SUPER sees all), newest first. */
160
+ listAudit(caller: Caller, query: AuditQuery): Promise<{
161
+ items: AuditEntry[];
162
+ nextCursor: string | null;
163
+ }>;
164
+ }
165
+ /** `backend-apps-engine` default WorkerEntrypoint implements this as well as `EngineRpc`. Values go in, never out. */
166
+ export interface EngineVaultRpc {
167
+ putSecret(ref: SecretRef, value: string, expiresAt?: string | null): Promise<{
168
+ ok: true;
169
+ }>;
170
+ listSecrets(installId: string): Promise<SecretInfo[]>;
171
+ /** Uninstall calls this; returns how many rows went. */
172
+ deleteSecrets(installId: string): Promise<{
173
+ deleted: number;
174
+ }>;
175
+ }
package/dist/trust.js ADDED
@@ -0,0 +1,101 @@
1
+ import { z } from 'zod';
2
+ import { AppId, InstallId, Timestamp, UserId, VersionId, WorkspaceId } from "./ids.js";
3
+ import { ScopeSchema } from "./scopes.js";
4
+ import { AppRecord, Caller } from "./registry.js";
5
+ /**
6
+ * Layer 1 (Trust): Loops as an OAuth provider, consent, the audit log, the developer switch and the apps vault.
7
+ * Skill devops-loops-developer-platform §1 (developer access), §6 (vault), §7 (OAuth both ways). Only ever ADD.
8
+ */
9
+ // ── Loops as an OAuth provider (@cloudflare/workers-oauth-provider 1.x in backend-apps-registry) ──
10
+ /** The issuer. Discovery at `${LOOPS_OAUTH_ISSUER}/.well-known/oauth-authorization-server`. */
11
+ export const LOOPS_OAUTH_ISSUER = 'https://api.withloops.ai';
12
+ export const OAUTH_PATHS = {
13
+ authorize: '/oauth/authorize',
14
+ token: '/oauth/token',
15
+ register: '/oauth/register',
16
+ };
17
+ /** The `loops` CLI: a public client (PKCE, loopback redirect on 127.0.0.1, no secret). */
18
+ export const LOOPS_CLI_CLIENT_ID = 'loops-cli';
19
+ /** The scope the CLI asks for: build, submit and dev-install apps as the signed-in developer. */
20
+ export const DEVELOPER_SCOPE = 'developer';
21
+ export const OAuthClientRecord = z.object({
22
+ clientId: z.string().min(8).max(128),
23
+ /** The app this client belongs to; null for first-party clients (the CLI). */
24
+ appId: AppId.nullable(),
25
+ name: z.string(),
26
+ /** https only, except loopback (127.0.0.1 / [::1]) for public native clients. */
27
+ redirectUris: z.array(z.string().url()),
28
+ createdBy: UserId,
29
+ createdAt: Timestamp,
30
+ });
31
+ /** Returned ONCE when a client is minted (`loops create`); the registry keeps only a hash of the secret. */
32
+ export const MintedClient = OAuthClientRecord.extend({ clientSecret: z.string().min(32) });
33
+ export const CreateAppInput = z.object({
34
+ id: AppId,
35
+ name: z.string().min(1).max(60),
36
+ redirectUris: z.array(z.string().url()).default([]),
37
+ });
38
+ /** What a Loops access token stands for, resolved by the registry for backend-api `/v1`. */
39
+ export const TokenCaller = z.object({
40
+ caller: Caller,
41
+ /** Scopes the token carries: app scopes, or `developer` for the CLI. */
42
+ scopes: z.array(z.string()),
43
+ clientId: z.string(),
44
+ /** Set when the token was issued to an app for one install. */
45
+ appId: AppId.nullable(),
46
+ installId: InstallId.nullable(),
47
+ });
48
+ // ── Consent and audit ─────────────────────────────────────────────────────────
49
+ /** One row per consent sheet answered: who approved which scopes for which version. */
50
+ export const ConsentRecord = z.object({
51
+ id: z.string().regex(/^cns_[A-Za-z0-9]{8,40}$/),
52
+ installId: InstallId,
53
+ appId: AppId,
54
+ versionId: VersionId,
55
+ workspaceId: WorkspaceId,
56
+ grantedBy: UserId,
57
+ scopes: z.array(ScopeSchema),
58
+ consentedAt: Timestamp,
59
+ });
60
+ export const AuditEntry = z.object({
61
+ id: z.number().int(),
62
+ at: Timestamp,
63
+ actor: z.string(),
64
+ workspaceId: WorkspaceId.nullable(),
65
+ appId: AppId.nullable(),
66
+ versionId: VersionId.nullable(),
67
+ installId: InstallId.nullable(),
68
+ /** e.g. `install.created`, `consent.granted`, `developer.granted`, `oauth.client_created`. */
69
+ action: z.string(),
70
+ detail: z.record(z.string(), z.unknown()),
71
+ });
72
+ export const AuditQuery = z.object({
73
+ appId: AppId.optional(),
74
+ installId: InstallId.optional(),
75
+ cursor: z.string().optional(),
76
+ limit: z.coerce.number().int().min(1).max(100).default(25),
77
+ });
78
+ // ── The developer switch ────────────────────────────────────────────────────
79
+ export const WorkspaceDeveloper = z.object({
80
+ workspaceId: WorkspaceId,
81
+ userId: UserId,
82
+ grantedBy: UserId,
83
+ grantedAt: Timestamp,
84
+ });
85
+ // ── The apps vault (backend-apps-engine, D1 apps-loops-vault) ────────────────
86
+ /** `access_token`, `refresh_token`, `api_key`, `webhook_secret`… */
87
+ export const SecretName = z.string().regex(/^[a-z][a-z0-9_]{0,62}$/, 'Secret name: lowercase letters, digits and underscores');
88
+ /** Where a secret belongs. All four parts are the encryption's additional data, so a copied row will not decrypt. */
89
+ export const SecretRef = z.object({
90
+ installId: InstallId,
91
+ workspaceId: WorkspaceId,
92
+ appId: AppId,
93
+ name: SecretName,
94
+ });
95
+ /** What anyone outside the engine may see about a secret: never the value. */
96
+ export const SecretInfo = z.object({
97
+ name: SecretName,
98
+ keyVersion: z.string(),
99
+ expiresAt: Timestamp.nullable(),
100
+ updatedAt: Timestamp,
101
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withloops.ai/contracts",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "The Loops developer platform contracts (zod): app manifest v1, scopes, events, the /v1 API envelope and registry records. Everything else builds on these.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/index.ts CHANGED
@@ -4,3 +4,4 @@ export * from './events.ts';
4
4
  export * from './manifest.ts';
5
5
  export * from './api.ts';
6
6
  export * from './registry.ts';
7
+ export * from './trust.ts';
package/src/trust.ts ADDED
@@ -0,0 +1,159 @@
1
+ import { z } from 'zod';
2
+ import { AppId, InstallId, Timestamp, UserId, VersionId, WorkspaceId } from './ids.ts';
3
+ import { ScopeSchema } from './scopes.ts';
4
+ import { AppRecord, Caller } from './registry.ts';
5
+
6
+ /**
7
+ * Layer 1 (Trust): Loops as an OAuth provider, consent, the audit log, the developer switch and the apps vault.
8
+ * Skill devops-loops-developer-platform §1 (developer access), §6 (vault), §7 (OAuth both ways). Only ever ADD.
9
+ */
10
+
11
+ // ── Loops as an OAuth provider (@cloudflare/workers-oauth-provider 1.x in backend-apps-registry) ──
12
+
13
+ /** The issuer. Discovery at `${LOOPS_OAUTH_ISSUER}/.well-known/oauth-authorization-server`. */
14
+ export const LOOPS_OAUTH_ISSUER = 'https://api.withloops.ai';
15
+
16
+ export const OAUTH_PATHS = {
17
+ authorize: '/oauth/authorize',
18
+ token: '/oauth/token',
19
+ register: '/oauth/register',
20
+ } as const;
21
+
22
+ /** The `loops` CLI: a public client (PKCE, loopback redirect on 127.0.0.1, no secret). */
23
+ export const LOOPS_CLI_CLIENT_ID = 'loops-cli';
24
+
25
+ /** The scope the CLI asks for: build, submit and dev-install apps as the signed-in developer. */
26
+ export const DEVELOPER_SCOPE = 'developer' as const;
27
+
28
+ export const OAuthClientRecord = z.object({
29
+ clientId: z.string().min(8).max(128),
30
+ /** The app this client belongs to; null for first-party clients (the CLI). */
31
+ appId: AppId.nullable(),
32
+ name: z.string(),
33
+ /** https only, except loopback (127.0.0.1 / [::1]) for public native clients. */
34
+ redirectUris: z.array(z.string().url()),
35
+ createdBy: UserId,
36
+ createdAt: Timestamp,
37
+ });
38
+ export type OAuthClientRecord = z.infer<typeof OAuthClientRecord>;
39
+
40
+ /** Returned ONCE when a client is minted (`loops create`); the registry keeps only a hash of the secret. */
41
+ export const MintedClient = OAuthClientRecord.extend({ clientSecret: z.string().min(32) });
42
+ export type MintedClient = z.infer<typeof MintedClient>;
43
+
44
+ export const CreateAppInput = z.object({
45
+ id: AppId,
46
+ name: z.string().min(1).max(60),
47
+ redirectUris: z.array(z.string().url()).default([]),
48
+ });
49
+ export type CreateAppInput = z.infer<typeof CreateAppInput>;
50
+
51
+ /** What a Loops access token stands for, resolved by the registry for backend-api `/v1`. */
52
+ export const TokenCaller = z.object({
53
+ caller: Caller,
54
+ /** Scopes the token carries: app scopes, or `developer` for the CLI. */
55
+ scopes: z.array(z.string()),
56
+ clientId: z.string(),
57
+ /** Set when the token was issued to an app for one install. */
58
+ appId: AppId.nullable(),
59
+ installId: InstallId.nullable(),
60
+ });
61
+ export type TokenCaller = z.infer<typeof TokenCaller>;
62
+
63
+ // ── Consent and audit ─────────────────────────────────────────────────────────
64
+
65
+ /** One row per consent sheet answered: who approved which scopes for which version. */
66
+ export const ConsentRecord = z.object({
67
+ id: z.string().regex(/^cns_[A-Za-z0-9]{8,40}$/),
68
+ installId: InstallId,
69
+ appId: AppId,
70
+ versionId: VersionId,
71
+ workspaceId: WorkspaceId,
72
+ grantedBy: UserId,
73
+ scopes: z.array(ScopeSchema),
74
+ consentedAt: Timestamp,
75
+ });
76
+ export type ConsentRecord = z.infer<typeof ConsentRecord>;
77
+
78
+ export const AuditEntry = z.object({
79
+ id: z.number().int(),
80
+ at: Timestamp,
81
+ actor: z.string(),
82
+ workspaceId: WorkspaceId.nullable(),
83
+ appId: AppId.nullable(),
84
+ versionId: VersionId.nullable(),
85
+ installId: InstallId.nullable(),
86
+ /** e.g. `install.created`, `consent.granted`, `developer.granted`, `oauth.client_created`. */
87
+ action: z.string(),
88
+ detail: z.record(z.string(), z.unknown()),
89
+ });
90
+ export type AuditEntry = z.infer<typeof AuditEntry>;
91
+
92
+ export const AuditQuery = z.object({
93
+ appId: AppId.optional(),
94
+ installId: InstallId.optional(),
95
+ cursor: z.string().optional(),
96
+ limit: z.coerce.number().int().min(1).max(100).default(25),
97
+ });
98
+ export type AuditQuery = z.infer<typeof AuditQuery>;
99
+
100
+ // ── The developer switch ────────────────────────────────────────────────────
101
+
102
+ export const WorkspaceDeveloper = z.object({
103
+ workspaceId: WorkspaceId,
104
+ userId: UserId,
105
+ grantedBy: UserId,
106
+ grantedAt: Timestamp,
107
+ });
108
+ export type WorkspaceDeveloper = z.infer<typeof WorkspaceDeveloper>;
109
+
110
+ // ── The apps vault (backend-apps-engine, D1 apps-loops-vault) ────────────────
111
+
112
+ /** `access_token`, `refresh_token`, `api_key`, `webhook_secret`… */
113
+ export const SecretName = z.string().regex(/^[a-z][a-z0-9_]{0,62}$/, 'Secret name: lowercase letters, digits and underscores');
114
+ export type SecretName = z.infer<typeof SecretName>;
115
+
116
+ /** Where a secret belongs. All four parts are the encryption's additional data, so a copied row will not decrypt. */
117
+ export const SecretRef = z.object({
118
+ installId: InstallId,
119
+ workspaceId: WorkspaceId,
120
+ appId: AppId,
121
+ name: SecretName,
122
+ });
123
+ export type SecretRef = z.infer<typeof SecretRef>;
124
+
125
+ /** What anyone outside the engine may see about a secret: never the value. */
126
+ export const SecretInfo = z.object({
127
+ name: SecretName,
128
+ keyVersion: z.string(),
129
+ expiresAt: Timestamp.nullable(),
130
+ updatedAt: Timestamp,
131
+ });
132
+ export type SecretInfo = z.infer<typeof SecretInfo>;
133
+
134
+ // ── RPC surfaces added in L1 (alongside RegistryRpc / EngineRpc) ─────────────
135
+
136
+ /** `backend-apps-registry` default WorkerEntrypoint implements this as well as `RegistryRpc`. */
137
+ export interface RegistryTrustRpc {
138
+ /** Create an app owned by the caller's workspace and mint its OAuth client (`loops create`). Developer only. */
139
+ createApp(caller: Caller, input: CreateAppInput): Promise<{ ok: true; app: AppRecord; client: MintedClient } | { ok: false; error: string }>;
140
+ /** Resolve a Loops OAuth access token (`Authorization: Bearer …`) for `/v1`; null when unknown, expired or revoked. */
141
+ resolveToken(bearer: string): Promise<TokenCaller | null>;
142
+ /** True when the user has the developer switch on in that workspace. */
143
+ isDeveloper(workspaceId: string, userId: string): Promise<boolean>;
144
+ /** Owner/Admin only. */
145
+ setDeveloper(caller: Caller, userId: string, on: boolean): Promise<{ ok: boolean; error?: string }>;
146
+ listDevelopers(caller: Caller): Promise<WorkspaceDeveloper[]>;
147
+ /** Consents for one install of the caller's workspace, newest first. */
148
+ listConsents(caller: Caller, installId: string): Promise<ConsentRecord[]>;
149
+ /** Audit rows for the caller's workspace (SUPER sees all), newest first. */
150
+ listAudit(caller: Caller, query: AuditQuery): Promise<{ items: AuditEntry[]; nextCursor: string | null }>;
151
+ }
152
+
153
+ /** `backend-apps-engine` default WorkerEntrypoint implements this as well as `EngineRpc`. Values go in, never out. */
154
+ export interface EngineVaultRpc {
155
+ putSecret(ref: SecretRef, value: string, expiresAt?: string | null): Promise<{ ok: true }>;
156
+ listSecrets(installId: string): Promise<SecretInfo[]>;
157
+ /** Uninstall calls this; returns how many rows went. */
158
+ deleteSecrets(installId: string): Promise<{ deleted: number }>;
159
+ }